Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configurable protocols #5883

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs-internal/nima-connections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
Connections
----

# Server Connection Providers

Níma server uses SPI to discover how to handle a specific incoming connection.
The main entry point is `ServerConnectionProvider` - a service loader provider interface to discover
which connections are supported.

We have the following implementations currently:

- `Http1ConnectionProvider` - support for the usual HTTP/1.1 connections
- `Http2ConnectionProvider` - support for "prior-knowledge" HTTP/2 connections

The connection provider creates a configured `ServerConnectionSelector`, which is then used at runtime.
The selector works based on initial bytes of the connection.


# HTTP/1.1 Upgrade Providers

HTTP/1.1 supports the concept of upgrading a connection. This is supported through
`Http1UpgradeProvider` - a service loader provider interface to discover which upgrades are supported.

We have the following implementations currently:

- `Http2UpgradeProvider` - upgrade to HTTP/2 using upgrade
- `WsUpgradeProvider` - upgrade to Níma WebSocket implementation
- `TyrusUpgradeProvider` - upgrade to MP Tyrus WebSocket implementation (higher weight than WsUpgradeProvider)

The upgrade provider creates a configured `Http1Upgrader`, which is then used at runtime.
Upgraders work based on protocol identificator (`h2c`, `websocket`). When more than one for the same protocol is configured,
the provider with higher weight will be used.

# Configurability

ALl of connection providers, HTTP/1.1 upgrade providers and HTTP/2 subprotocols are configured under `server.connection-providers`, to have a single configuration of a protocol regardless whether this is accessed directly or through upgrade mechanism.

The configuration key is the one provided by the Connection provider, HTTP/1.1 Upgrade provider, or HTTP/2 SubProtocol provider `configKey()` or `configKeys()` method.

As all providers are configured on the same leave, each provider should have a descriptive and unique configuration key
relevant to its purpose.

Example of such configuration (Tyrus and Níma WebSocket both use `websocket`, as only one of them can be active):
```yaml
server:
connection-providers:
http_1_1:
max-prologue-length: 4096
websocket:
origins: ["origin1"]
http_2:
max-frame-size: 128000
grpc:
something: "value"
```
6 changes: 5 additions & 1 deletion etc/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<!--

Copyright (c) 2016, 2022 Oracle and/or its affiliates.
Copyright (c) 2016, 2023 Oracle and/or its affiliates.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -57,6 +57,10 @@
<suppress checks="IllegalImport"
files="DerUtils\.java"/>

<!-- this is a record style, all parameters are always needed, no benefit of changing to builder -->
<suppress files="nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolSelector.java"
checks="ParameterNumber"/>

<!-- this is a record style, all parameters are always needed, no benefit of changing to builder -->
<suppress files="nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionContext.java"
checks="ParameterNumber"/>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,216 +16,75 @@

package io.helidon.microprofile.tyrus;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import io.helidon.common.Weight;
import io.helidon.common.Weighted;
import io.helidon.common.buffers.BufferData;
import io.helidon.common.buffers.DataWriter;
import io.helidon.common.http.DirectHandler;
import io.helidon.common.http.Http;
import io.helidon.common.http.HttpPrologue;
import io.helidon.common.http.RequestException;
import io.helidon.common.http.WritableHeaders;
import io.helidon.common.uri.UriQuery;
import io.helidon.nima.webserver.ConnectionContext;
import io.helidon.nima.webserver.spi.ServerConnection;
import io.helidon.nima.websocket.webserver.WsUpgradeProvider;
import java.util.function.Function;

import jakarta.enterprise.inject.spi.CDI;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.Extension;
import jakarta.websocket.server.ServerEndpointConfig;
import org.glassfish.tyrus.core.RequestContext;
import org.glassfish.tyrus.core.TyrusUpgradeResponse;
import org.glassfish.tyrus.core.TyrusWebSocketEngine;
import org.glassfish.tyrus.server.TyrusServerContainer;
import org.glassfish.tyrus.spi.WebSocketEngine;
import io.helidon.config.Config;
import io.helidon.nima.webserver.http1.spi.Http1Upgrader;
import io.helidon.nima.websocket.webserver.WsUpgradeProvider;

/**
* Tyrus connection upgrade provider.
* {@link java.util.ServiceLoader} provider implementation for upgrade from HTTP/1.1 to Tyrus connection.
*/
@Weight(Weighted.DEFAULT_WEIGHT + 100) // higher than base class
public class TyrusUpgradeProvider extends WsUpgradeProvider {
private static final Logger LOGGER = Logger.getLogger(TyrusUpgradeProvider.class.getName());

private String path;
private String queryString;
private final TyrusRouting tyrusRouting;
private final WebSocketEngine engine;
TyrusUpgradeProvider(Builder builder) {
super(builder);
}

/**
* @deprecated This constructor is only to be used by {@link java.util.ServiceLoader}.
* @deprecated This constructor is only to be used by {@link java.util.ServiceLoader}, use {@link #builder()}
*/
@Deprecated()
public TyrusUpgradeProvider() {
TyrusCdiExtension extension = CDI.current().select(TyrusCdiExtension.class).get();
Objects.requireNonNull(extension);
this.tyrusRouting = extension.tyrusRouting();
TyrusServerContainer tyrusServerContainer = initializeTyrus();
this.engine = tyrusServerContainer.getWebSocketEngine();
this(tyrusBuilder());
}

/**
* New builder.
*
* @return builder
*/
public static Builder tyrusBuilder() {
return new Builder();
}

@Override
public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders<?> headers) {
// Check required header
String wsKey;
if (headers.contains(WS_KEY)) {
wsKey = headers.get(WS_KEY).value();
public Http1Upgrader create(Function<String, Config> config) {
Set<String> usedOrigins;

if (origins().isEmpty()) {
usedOrigins = config.apply(CONFIG_NAME)
.get("origins")
.asList(String.class)
.map(Set::copyOf)
.orElseGet(Set::of);
} else {
// this header is required
return null;
usedOrigins = origins();
}

// Verify protocol version
String version;
if (headers.contains(WS_VERSION)) {
version = headers.get(WS_VERSION).value();
} else {
version = SUPPORTED_VERSION;
}
if (!SUPPORTED_VERSION.equals(version)) {
throw RequestException.builder()
.type(DirectHandler.EventType.BAD_REQUEST)
.message("Unsupported WebSocket Version")
.header(SUPPORTED_VERSION_HEADER)
.build();
}
return new TyrusUpgrader(usedOrigins);
}

// Initialize path and queryString
path = prologue.uriPath().path();
int k = path.indexOf('?');
if (k > 0) {
this.path = path.substring(0, k);
this.queryString = path.substring(k + 1);
} else {
this.queryString = "";
}
// jUnit test accessor for origins set (package private only)
protected Set<String> origins() {
return super.origins();
}

// Check if this a Tyrus route exists
TyrusRoute route = ctx.router()
.routing(TyrusRouting.class, tyrusRouting)
.findRoute(prologue);
if (route == null) {
return null;
}
/**
* Fluent API builder for {@link TyrusUpgradeProvider}.
*/
public static final class Builder
extends WsUpgradeProvider.AbstractBuilder<TyrusUpgradeProvider.Builder, TyrusUpgradeProvider> {

// Validate origin
if (!anyOrigin()) {
if (headers.contains(Http.Header.ORIGIN)) {
String origin = headers.get(Http.Header.ORIGIN).value();
if (!origins().contains(origin)) {
throw RequestException.builder()
.message("Invalid Origin")
.type(DirectHandler.EventType.FORBIDDEN)
.build();
}
}
private Builder() {
}

// Protocol handshake with Tyrus
WebSocketEngine.UpgradeInfo upgradeInfo = protocolHandshake(headers);

// todo support subprotocols (must be provided by route)
// Sec-WebSocket-Protocol: sub-protocol (list provided in PROTOCOL header, separated by comma space
DataWriter dataWriter = ctx.dataWriter();
String switchingProtocols = SWITCHING_PROTOCOL_PREFIX + hash(ctx, wsKey) + SWITCHING_PROTOCOLS_SUFFIX;
dataWriter.write(BufferData.create(switchingProtocols.getBytes(StandardCharsets.US_ASCII)));

if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Upgraded to websocket version " + version);
@Override
public TyrusUpgradeProvider build() {
return new TyrusUpgradeProvider(this);
}
return new TyrusConnection(ctx, upgradeInfo);
}

TyrusServerContainer initializeTyrus() {
Set<Class<?>> allEndpointClasses = tyrusRouting.routes()
.stream()
.map(TyrusRoute::endpointClass)
.collect(Collectors.toSet());

TyrusServerContainer tyrusServerContainer = new TyrusServerContainer(allEndpointClasses) {
private final WebSocketEngine engine =
TyrusWebSocketEngine.builder(this).build();

@Override
public void register(Class<?> endpointClass) {
throw new UnsupportedOperationException("Cannot register endpoint class");
}

@Override
public void register(ServerEndpointConfig serverEndpointConfig) {
throw new UnsupportedOperationException("Cannot register ServerEndpointConfig");
}

@Override
public Set<Extension> getInstalledExtensions() {
return tyrusRouting.extensions();
}

@Override
public WebSocketEngine getWebSocketEngine() {
return engine;
}
};

// Register classes with context path "/"
WebSocketEngine engine = tyrusServerContainer.getWebSocketEngine();
tyrusRouting.routes().forEach(route -> {
try {
if (route.serverEndpointConfig() != null) {
LOGGER.log(Level.FINE, () -> "Registering ws endpoint "
+ route.path()
+ route.serverEndpointConfig().getPath());
engine.register(route.serverEndpointConfig(), route.path());
} else {
LOGGER.log(Level.FINE, () -> "Registering annotated ws endpoint " + route.path());
engine.register(route.endpointClass(), route.path());
}
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
});

return tyrusServerContainer;
}

WebSocketEngine.UpgradeInfo protocolHandshake(WritableHeaders<?> headers) {
LOGGER.log(Level.FINE, "Initiating WebSocket handshake with Tyrus...");

// Create Tyrus request context, copy request headers and query params
Map<String, String[]> paramsMap = new HashMap<>();
UriQuery uriQuery = UriQuery.create(queryString);
for (String name : uriQuery.names()) {
paramsMap.put(name, uriQuery.all(name).toArray(new String[0]));
}
RequestContext requestContext = RequestContext.Builder.create()
.requestURI(URI.create(path)) // excludes context path
.queryString(queryString)
.parameterMap(paramsMap)
.build();
headers.forEach(e -> requestContext.getHeaders().put(e.name(), List.of(e.values())));

// Use Tyrus to process a WebSocket upgrade request
final TyrusUpgradeResponse upgradeResponse = new TyrusUpgradeResponse();
final WebSocketEngine.UpgradeInfo upgradeInfo = engine.upgrade(requestContext, upgradeResponse);

// Map Tyrus response headers back to Nima
upgradeResponse.getHeaders()
.forEach((key, value) -> headers.add(
Http.Header.create(
Http.Header.createName(key, key.toLowerCase(Locale.ROOT)),
value)));
return upgradeInfo;
}
}
Loading