From ab8a9721f511c85a5bb5b58046597d153a8ea532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Wed, 14 Dec 2022 17:48:48 +0100 Subject: [PATCH 1/2] Issue 5416: Transfer configuration to connection providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus Issue 5416: Fixed javadoc error. Signed-off-by: Tomáš Kraus --- .../tyrus/TyrusUpgradeProvider.java | 222 +++------------ .../microprofile/tyrus/TyrusUpgrader.java | 230 ++++++++++++++++ .../tyrus/TyrusUpgradeProviderConfigTest.java | 41 +++ nima/http2/webserver/pom.xml | 7 +- .../nima/http2/webserver/Http2Config.java | 111 ++++++++ .../nima/http2/webserver/Http2Connection.java | 25 +- .../webserver/Http2ConnectionProvider.java | 113 ++++++-- .../webserver/Http2ConnectionSelector.java | 73 +++++ .../http2/webserver/Http2UpgradeProvider.java | 137 ++++++---- .../nima/http2/webserver/Http2Upgrader.java | 93 +++++++ .../http2/webserver/ConnectionConfigTest.java | 179 ++++++++++++ .../src/test/resources/application.yaml | 24 ++ .../webserver/DirectClientConnection.java | 3 +- .../websocket/webserver/EchoService.java | 14 +- nima/webserver/webserver/pom.xml | 7 +- .../nima/webserver/ConnectionHandler.java | 11 +- .../nima/webserver/ConnectionProviders.java | 20 +- .../io/helidon/nima/webserver/LoomServer.java | 3 +- .../webserver/ServerConnectionSelector.java | 78 ++++++ .../nima/webserver/ServerListener.java | 3 +- .../io/helidon/nima/webserver/WebServer.java | 20 +- .../nima/webserver/http1/Http1Config.java | 256 +++++++++++++++++ .../nima/webserver/http1/Http1Connection.java | 53 ++-- .../http1/Http1ConnectionListenerUtil.java | 17 +- .../http1/Http1ConnectionProvider.java | 246 ++++++++--------- .../http1/Http1ConnectionSelector.java | 82 ++++++ .../nima/webserver/http1/Http1Upgrader.java | 46 ++++ .../http1/spi/Http1UpgradeProvider.java | 39 +-- .../nima/webserver/spi/ServerConnection.java | 4 +- .../spi/ServerConnectionProvider.java | 59 ++-- .../webserver/http1/ConnectionConfigTest.java | 202 ++++++++++++++ .../src/test/resources/application.yaml | 26 ++ .../webserver/etc/spotbugs/exclude.xml | 4 +- nima/websocket/webserver/pom.xml | 5 + .../websocket/webserver/WsConnection.java | 2 +- .../webserver/WsUpgradeProvider.java | 258 ++++-------------- .../nima/websocket/webserver/WsUpgrader.java | 224 +++++++++++++++ .../WsUpgradeProviderConfigTest.java | 180 ++++++++++++ .../src/test/resources/application.yaml | 23 ++ 39 files changed, 2405 insertions(+), 735 deletions(-) create mode 100644 microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgrader.java create mode 100644 microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/TyrusUpgradeProviderConfigTest.java create mode 100644 nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java create mode 100644 nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionSelector.java create mode 100644 nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Upgrader.java create mode 100644 nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java create mode 100644 nima/http2/webserver/src/test/resources/application.yaml create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConnectionSelector.java create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionSelector.java create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Upgrader.java create mode 100644 nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java create mode 100644 nima/webserver/webserver/src/test/resources/application.yaml create mode 100644 nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java create mode 100644 nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java create mode 100644 nima/websocket/webserver/src/test/resources/application.yaml diff --git a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java index 8d6acdcb23d..ec0d171b31e 100644 --- a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java +++ b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java @@ -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. @@ -16,216 +16,62 @@ 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.HashSet; 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.webserver.http1.Http1Upgrader; import io.helidon.nima.websocket.webserver.WsUpgradeProvider; -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; - /** - * 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; /** - * @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(new HashSet<>()); } - @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(); - } else { - // this header is required - return null; - } - - // 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(); - } - - // 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 = ""; - } - - // Check if this a Tyrus route exists - TyrusRoute route = ctx.router() - .routing(TyrusRouting.class, tyrusRouting) - .findRoute(prologue); - if (route == null) { - return null; - } - - // 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(); - } - } - } - - // 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); - } - return new TyrusConnection(ctx, upgradeInfo); + TyrusUpgradeProvider(Set origins) { + super(origins); } - TyrusServerContainer initializeTyrus() { - Set> 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 getInstalledExtensions() { - return tyrusRouting.extensions(); - } - - @Override - public WebSocketEngine getWebSocketEngine() { - return engine; - } - }; + @Override + public Http1Upgrader create() { + return new TyrusUpgrader(Set.copyOf(origins())); + } - // 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); - } - }); + // jUnit test accessor for origins set (package private only) + protected Set origins() { + return super.origins(); + } - return tyrusServerContainer; + /** + * New builder. + * + * @return builder + */ + public static Builder tyrusBuilder() { + return new Builder(); } - WebSocketEngine.UpgradeInfo protocolHandshake(WritableHeaders headers) { - LOGGER.log(Level.FINE, "Initiating WebSocket handshake with Tyrus..."); + /** + * Fluent API builder for {@link TyrusUpgradeProvider}. + */ + public static final class Builder + extends WsUpgradeProvider.AbstractBuilder { - // Create Tyrus request context, copy request headers and query params - Map paramsMap = new HashMap<>(); - UriQuery uriQuery = UriQuery.create(queryString); - for (String name : uriQuery.names()) { - paramsMap.put(name, uriQuery.all(name).toArray(new String[0])); + private Builder() { } - 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); + @Override + public TyrusUpgradeProvider build() { + return new TyrusUpgradeProvider(origins()); + } - // 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; } + } diff --git a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgrader.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgrader.java new file mode 100644 index 00000000000..e189d1e9acd --- /dev/null +++ b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgrader.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2022 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.WsUpgrader; + +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; + +/** + * Tyrus connection upgrade provider. + */ +@Weight(Weighted.DEFAULT_WEIGHT + 100) // higher than base class +public class TyrusUpgrader extends WsUpgrader { + private static final Logger LOGGER = Logger.getLogger(TyrusUpgrader.class.getName()); + + private String path; + private String queryString; + private final TyrusRouting tyrusRouting; + private final WebSocketEngine engine; + + TyrusUpgrader(Set origins) { + super(origins); + TyrusCdiExtension extension = CDI.current().select(TyrusCdiExtension.class).get(); + Objects.requireNonNull(extension); + this.tyrusRouting = extension.tyrusRouting(); + TyrusServerContainer tyrusServerContainer = initializeTyrus(); + this.engine = tyrusServerContainer.getWebSocketEngine(); + } + + + @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(); + } else { + // this header is required + return null; + } + + // 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(); + } + + // 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 = ""; + } + + // Check if this a Tyrus route exists + TyrusRoute route = ctx.router() + .routing(TyrusRouting.class, tyrusRouting) + .findRoute(prologue); + if (route == null) { + return null; + } + + // 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(); + } + } + } + + // 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); + } + return new TyrusConnection(ctx, upgradeInfo); + } + + TyrusServerContainer initializeTyrus() { + Set> 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 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 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; + } + +} diff --git a/microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/TyrusUpgradeProviderConfigTest.java b/microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/TyrusUpgradeProviderConfigTest.java new file mode 100644 index 00000000000..e02e5bb1fac --- /dev/null +++ b/microprofile/websocket/src/test/java/io/helidon/microprofile/tyrus/TyrusUpgradeProviderConfigTest.java @@ -0,0 +1,41 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.microprofile.tyrus; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class TyrusUpgradeProviderConfigTest { + + // Verify that TyrusUpgradeProvider is properly configured from builder. + // TyrusUpgrader requires CDI so only providefr is tested. + @Test + void testUpgraderConfigBuilder() { + TyrusUpgradeProvider upgrader = TyrusUpgradeProvider.tyrusBuilder() + .addOrigin("origin1") + .addOrigin("origin2") + .build(); + + Set origins = upgrader.origins(); + assertThat(origins, containsInAnyOrder("origin1", "origin2")); + } + +} diff --git a/nima/http2/webserver/pom.xml b/nima/http2/webserver/pom.xml index 7057571af87..afdf328b5e4 100644 --- a/nima/http2/webserver/pom.xml +++ b/nima/http2/webserver/pom.xml @@ -1,6 +1,6 @@ - + diff --git a/nima/websocket/webserver/pom.xml b/nima/websocket/webserver/pom.xml index 42b3c8adc6e..c1e5b19ead6 100644 --- a/nima/websocket/webserver/pom.xml +++ b/nima/websocket/webserver/pom.xml @@ -46,6 +46,11 @@ provided true + + io.helidon.config + helidon-config-yaml + test + org.junit.jupiter junit-jupiter-api diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsConnection.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsConnection.java index 514e6f6e46f..d0d493b690d 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsConnection.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsConnection.java @@ -35,7 +35,7 @@ import io.helidon.nima.websocket.WsOpCode; import io.helidon.nima.websocket.WsSession; -import static io.helidon.nima.websocket.webserver.WsUpgradeProvider.PROTOCOL; +import static io.helidon.nima.websocket.webserver.WsUpgrader.PROTOCOL; /** * WebSocket connection, server side session implementation. diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java index 389615fd635..14c9a54d30b 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 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. @@ -16,102 +16,57 @@ package io.helidon.nima.websocket.webserver; -import java.lang.System.Logger.Level; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; import java.util.HashSet; -import java.util.Optional; import java.util.Set; -import io.helidon.common.buffers.BufferData; -import io.helidon.common.buffers.DataWriter; -import io.helidon.common.http.DirectHandler; -import io.helidon.common.http.Headers; -import io.helidon.common.http.Http; -import io.helidon.common.http.Http.Header; -import io.helidon.common.http.Http.HeaderName; -import io.helidon.common.http.HttpPrologue; -import io.helidon.common.http.NotFoundException; -import io.helidon.common.http.RequestException; -import io.helidon.common.http.WritableHeaders; -import io.helidon.nima.webserver.ConnectionContext; +import io.helidon.config.Config; +import io.helidon.nima.webserver.http1.Http1Upgrader; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; -import io.helidon.nima.webserver.spi.ServerConnection; -import io.helidon.nima.websocket.WsUpgradeException; - -import static java.nio.charset.StandardCharsets.US_ASCII; /** * {@link java.util.ServiceLoader} provider implementation for upgrade from HTTP/1.1 to WebSocket. */ public class WsUpgradeProvider implements Http1UpgradeProvider { - /** - * Websocket key header name. - */ - public static final HeaderName WS_KEY = Header.create("Sec-WebSocket-Key"); - - /** - * Websocket version header name. - */ - public static final HeaderName WS_VERSION = Header.create("Sec-WebSocket-Version"); - - /** - * Websocket protocol header name. - */ - public static final HeaderName PROTOCOL = Header.create("Sec-WebSocket-Protocol"); - - /** - * Websocket protocol header name. - */ - public static final HeaderName EXTENSIONS = Header.create("Sec-WebSocket-Extensions"); - - /** - * Switching response prefix. - */ - protected static final String SWITCHING_PROTOCOL_PREFIX = "HTTP/1.1 101 Switching Protocols\r\n" - + "Connection: Upgrade\r\n" - + "Upgrade: websocket\r\n" - + "Sec-WebSocket-Accept: "; + private final Set origins; /** - * Switching response suffix. + * Create a new instance with default configuration. + * + * @deprecated This constructor is only to be used by {@link java.util.ServiceLoader}, use {@link #builder()} */ - protected static final String SWITCHING_PROTOCOLS_SUFFIX = "\r\n\r\n"; + @Deprecated() + public WsUpgradeProvider() { + this(new HashSet<>()); + } - /** - * Supported version. - */ - protected static final String SUPPORTED_VERSION = "13"; + protected WsUpgradeProvider(Set origins) { + this.origins = origins; + } - /** - * Supported version header. - */ - protected static final Http.HeaderValue SUPPORTED_VERSION_HEADER = Header.create(WS_VERSION, SUPPORTED_VERSION); + /** HTTP/2 server connection provider configuration node name. */ + private static final String CONFIG_NAME = "websocket"; - private static final System.Logger LOGGER = System.getLogger(WsUpgradeProvider.class.getName()); - private static final byte[] KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(US_ASCII); - private static final int KEY_SUFFIX_LENGTH = KEY_SUFFIX.length; - private static final Base64.Decoder B64_DECODER = Base64.getDecoder(); - private static final Base64.Encoder B64_ENCODER = Base64.getEncoder(); - private static final byte[] HEADERS_SEPARATOR = "\r\n".getBytes(US_ASCII); - static final Headers EMPTY_HEADERS = WritableHeaders.create(); + @Override + public String configKey() { + return CONFIG_NAME; + } - private final Set origins; - private final boolean anyOrigin; + @Override + public void config(Config config) { + // Accept origins as list of String values from config file + config.get("origins") + .asList(String.class) + .ifPresent(origins::addAll); + } - /** - * @deprecated This constructor is only to be used by {@link java.util.ServiceLoader}, use {@link #builder()} - */ - @Deprecated() - public WsUpgradeProvider() { - this(builder()); + @Override + public Http1Upgrader create() { + return new WsUpgrader(Set.copyOf(origins)); } - WsUpgradeProvider(Builder builder) { - this.origins = Set.copyOf(builder.origins); - this.anyOrigin = this.origins.isEmpty(); + protected Set origins() { + return origins; } /** @@ -123,149 +78,50 @@ public static Builder builder() { return new Builder(); } - @Override - public String supportedProtocol() { - return "websocket"; - } - - @Override - public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders headers) { - String wsKey; - if (headers.contains(WS_KEY)) { - wsKey = headers.get(WS_KEY).value(); - } else { - // this header is required - return null; - } - // 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(); - } - - WsRoute route; - - try { - route = ctx.router().routing(WsRouting.class, WsRouting.empty()) - .findRoute(prologue); - } catch (NotFoundException e) { - return null; - } + /** + * Abstract Fluent API builder for {@link WsUpgradeProvider} and child classes. + * + * @param Type of the builder + * @param Type of the built instance + */ + protected abstract static class AbstractBuilder, T> + implements io.helidon.common.Builder { - if (!anyOrigin()) { - if (headers.contains(Header.ORIGIN)) { - String origin = headers.get(Header.ORIGIN).value(); - if (!origins().contains(origin)) { - throw RequestException.builder() - .message("Invalid Origin") - .type(DirectHandler.EventType.FORBIDDEN) - .build(); - } - } - } + private final Set origins = new HashSet<>(); - // invoke user-provided HTTP upgrade handler - Optional upgradeHeaders; - try { - upgradeHeaders = route.listener().onHttpUpgrade(prologue, headers); - } catch (WsUpgradeException e) { - LOGGER.log(Level.TRACE, "Websocket upgrade rejected", e); - return null; + protected AbstractBuilder() { } - // write switch protocol response including headers from listener - DataWriter dataWriter = ctx.dataWriter(); - String switchingProtocols = SWITCHING_PROTOCOL_PREFIX + hash(ctx, wsKey); - dataWriter.write(BufferData.create(switchingProtocols.getBytes(US_ASCII))); - BufferData separator = BufferData.create(HEADERS_SEPARATOR); - dataWriter.write(separator); - upgradeHeaders.ifPresent(hs -> { - BufferData headerData = BufferData.growing(128); - hs.forEach(h -> h.writeHttp1Header(headerData)); - dataWriter.write(headerData); - }); - dataWriter.write(separator.rewind()); - - if (LOGGER.isLoggable(Level.TRACE)) { - LOGGER.log(Level.TRACE, "Upgraded to websocket version " + version); + /** + * Add supported origin. + * + * @param origin origin to add + * @return updated builder + */ + public B addOrigin(String origin) { + origins.add(origin); + return identity(); } - return WsConnection.create(ctx, prologue, upgradeHeaders.orElse(EMPTY_HEADERS), wsKey, route); - } - - protected boolean anyOrigin() { - return anyOrigin; - } - - protected Set origins() { - return origins; - } - - protected String hash(ConnectionContext ctx, String wsKey) { - byte[] decodedBytes = B64_DECODER.decode(wsKey); - if (decodedBytes.length != 16) { - // this is required by the specification (RFC-6455) - /* - The request MUST include a header field with the name - |Sec-WebSocket-Key|. The value of this header field MUST be a - nonce consisting of a randomly selected 16-byte value that has - been base64-encoded (see Section 4 of [RFC4648]). The nonce - MUST be selected randomly for each connection. - */ - throw RequestException.builder() - .type(DirectHandler.EventType.BAD_REQUEST) - .message("Invalid Sec-WebSocket-Key header") - .build(); + protected Set origins() { + return origins; } - byte[] wsKeyBytes = wsKey.getBytes(US_ASCII); - int wsKeyBytesLength = wsKeyBytes.length; - byte[] toHash = new byte[wsKeyBytesLength + KEY_SUFFIX_LENGTH]; - System.arraycopy(wsKeyBytes, 0, toHash, 0, wsKeyBytesLength); - System.arraycopy(KEY_SUFFIX, 0, toHash, wsKeyBytesLength, KEY_SUFFIX_LENGTH); - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA-1"); - return B64_ENCODER.encodeToString(digest.digest(toHash)); - } catch (NoSuchAlgorithmException e) { - ctx.log(LOGGER, Level.ERROR, "SHA-1 must be provided for WebSocket to work", e); - throw new IllegalStateException("SHA-1 not provided", e); - } } /** - * Fluent API builder for {@link io.helidon.nima.websocket.webserver.WsUpgradeProvider}. + * Fluent API builder for {@link WsUpgradeProvider}. */ - public static class Builder implements io.helidon.common.Builder { - private final Set origins = new HashSet<>(); + public static final class Builder extends AbstractBuilder { private Builder() { } @Override public WsUpgradeProvider build() { - return new WsUpgradeProvider(this); + return new WsUpgradeProvider(origins()); } - /** - * Add supported origin. - * - * @param origin origin to add - * @return updated builder - */ - public Builder addOrigin(String origin) { - origins.add(origin); - return this; - } } + } diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java new file mode 100644 index 00000000000..eb29fcc3bbb --- /dev/null +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2022, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.websocket.webserver; + +import java.lang.System.Logger.Level; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.buffers.BufferData; +import io.helidon.common.buffers.DataWriter; +import io.helidon.common.http.DirectHandler; +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.common.http.Http.Header; +import io.helidon.common.http.Http.HeaderName; +import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.RequestException; +import io.helidon.common.http.WritableHeaders; +import io.helidon.nima.webserver.ConnectionContext; +import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.nima.webserver.spi.ServerConnection; +import io.helidon.nima.websocket.WsUpgradeException; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +/** + * {@link java.util.ServiceLoader} provider implementation for upgrade from HTTP/1.1 to WebSocket. + */ +public class WsUpgrader implements Http1Upgrader { + + /** + * Websocket key header name. + */ + public static final HeaderName WS_KEY = Header.create("Sec-WebSocket-Key"); + + /** + * Websocket version header name. + */ + public static final HeaderName WS_VERSION = Header.create("Sec-WebSocket-Version"); + + /** + * Websocket protocol header name. + */ + public static final HeaderName PROTOCOL = Header.create("Sec-WebSocket-Protocol"); + + /** + * Websocket protocol header name. + */ + public static final HeaderName EXTENSIONS = Header.create("Sec-WebSocket-Extensions"); + + /** + * Switching response prefix. + */ + protected static final String SWITCHING_PROTOCOL_PREFIX = "HTTP/1.1 101 Switching Protocols\r\n" + + "Connection: Upgrade\r\n" + + "Upgrade: websocket\r\n" + + "Sec-WebSocket-Accept: "; + + /** + * Switching response suffix. + */ + protected static final String SWITCHING_PROTOCOLS_SUFFIX = "\r\n\r\n"; + + /** + * Supported version. + */ + protected static final String SUPPORTED_VERSION = "13"; + + /** + * Supported version header. + */ + protected static final Http.HeaderValue SUPPORTED_VERSION_HEADER = Header.create(WS_VERSION, SUPPORTED_VERSION); + + private static final System.Logger LOGGER = System.getLogger(WsUpgrader.class.getName()); + private static final byte[] KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(US_ASCII); + private static final int KEY_SUFFIX_LENGTH = KEY_SUFFIX.length; + private static final Base64.Decoder B64_DECODER = Base64.getDecoder(); + private static final Base64.Encoder B64_ENCODER = Base64.getEncoder(); + private static final byte[] HEADERS_SEPARATOR = "\r\n".getBytes(US_ASCII); + + private final Set origins; + private final boolean anyOrigin; + + protected WsUpgrader(Set origins) { + this.origins = origins; + this.anyOrigin = this.origins.isEmpty(); + } + + @Override + public String supportedProtocol() { + return "websocket"; + } + + @Override + public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders headers) { + String wsKey; + if (headers.contains(WS_KEY)) { + wsKey = headers.get(WS_KEY).value(); + } else { + // this header is required + return null; + } + // 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(); + } + + WebSocket route = ctx.router().routing(WebSocketRouting.class, WebSocketRouting.empty()) + .findRoute(prologue); + + if (route == null) { + return null; + } + + if (!anyOrigin()) { + if (headers.contains(Header.ORIGIN)) { + String origin = headers.get(Header.ORIGIN).value(); + if (!origins().contains(origin)) { + throw RequestException.builder() + .message("Invalid Origin") + .type(DirectHandler.EventType.FORBIDDEN) + .build(); + } + } + } + + // invoke user-provided HTTP upgrade handler + Optional upgradeHeaders; + try { + upgradeHeaders = route.listener().onHttpUpgrade(prologue, headers); + } catch (WsUpgradeException e) { + LOGGER.log(Level.TRACE, "Websocket upgrade rejected", e); + return null; + } + + // write switch protocol response including headers from listener + DataWriter dataWriter = ctx.dataWriter(); + String switchingProtocols = SWITCHING_PROTOCOL_PREFIX + hash(ctx, wsKey); + dataWriter.write(BufferData.create(switchingProtocols.getBytes(US_ASCII))); + BufferData separator = BufferData.create(HEADERS_SEPARATOR); + dataWriter.write(separator); + upgradeHeaders.ifPresent(hs -> { + BufferData headerData = BufferData.growing(128); + hs.forEach(h -> h.writeHttp1Header(headerData)); + dataWriter.write(headerData); + }); + dataWriter.write(separator.rewind()); + + if (LOGGER.isLoggable(Level.TRACE)) { + LOGGER.log(Level.TRACE, "Upgraded to websocket version " + version); + } + + return new WsConnection(ctx, prologue, headers, upgradeHeaders.orElse(null), wsKey, route); + } + + protected boolean anyOrigin() { + return anyOrigin; + } + + protected Set origins() { + return origins; + } + + protected String hash(ConnectionContext ctx, String wsKey) { + byte[] decodedBytes = B64_DECODER.decode(wsKey); + if (decodedBytes.length != 16) { + // this is required by the specification (RFC-6455) + /* + The request MUST include a header field with the name + |Sec-WebSocket-Key|. The value of this header field MUST be a + nonce consisting of a randomly selected 16-byte value that has + been base64-encoded (see Section 4 of [RFC4648]). The nonce + MUST be selected randomly for each connection. + */ + throw RequestException.builder() + .type(DirectHandler.EventType.BAD_REQUEST) + .message("Invalid Sec-WebSocket-Key header") + .build(); + } + byte[] wsKeyBytes = wsKey.getBytes(US_ASCII); + int wsKeyBytesLength = wsKeyBytes.length; + byte[] toHash = new byte[wsKeyBytesLength + KEY_SUFFIX_LENGTH]; + System.arraycopy(wsKeyBytes, 0, toHash, 0, wsKeyBytesLength); + System.arraycopy(KEY_SUFFIX, 0, toHash, wsKeyBytesLength, KEY_SUFFIX_LENGTH); + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + return B64_ENCODER.encodeToString(digest.digest(toHash)); + } catch (NoSuchAlgorithmException e) { + ctx.log(LOGGER, Level.ERROR, "SHA-1 must be provided for WebSocket to work", e); + throw new IllegalStateException("SHA-1 not provided", e); + } + } + +} diff --git a/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java b/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java new file mode 100644 index 00000000000..f6e5df3c4bd --- /dev/null +++ b/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java @@ -0,0 +1,180 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.websocket.webserver; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import io.helidon.common.buffers.DataReader; +import io.helidon.common.buffers.DataWriter; +import io.helidon.common.socket.PeerInfo; +import io.helidon.config.Config; +import io.helidon.nima.webserver.ConnectionContext; +import io.helidon.nima.webserver.Router; +import io.helidon.nima.webserver.Routing; +import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.webserver.ServerContext; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http.DirectHandlers; +import io.helidon.nima.webserver.http1.Http1Connection; +import io.helidon.nima.webserver.http1.Http1ConnectionSelector; +import io.helidon.nima.webserver.http1.Http1Upgrader; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class WsUpgradeProviderConfigTest { + + // ConnectionContext mockup + private static class TestContext implements ConnectionContext { + + @Override + public PeerInfo remotePeer() { + return null; + } + + @Override + public PeerInfo localPeer() { + return null; + } + + @Override + public boolean isSecure() { + return false; + } + + @Override + public String socketId() { + return null; + } + + @Override + public String childSocketId() { + return null; + } + + @Override + public ServerContext serverContext() { + return null; + } + + @Override + public ExecutorService sharedExecutor() { + return null; + } + + @Override + public DataWriter dataWriter() { + return null; + } + + @Override + public DataReader dataReader() { + return new DataReader(() -> new byte[0]); + } + + @Override + public Router router() { + return new Router() { + @Override + public T routing(Class routingType, T defaultValue) { + return null; + } + + @Override + public void afterStop() { + } + + @Override + public void beforeStart() { + } + }; + } + + @Override + public long maxPayloadSize() { + return 0; + } + + @Override + public DirectHandlers directHandlers() { + return null; + } + + } + + // Verify that WsUpgrader is properly configured from config file + @Test + void testConnectionConfig() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { + + // This will pick up application.yaml from the classpath as default configuration file + Config config = Config.create(); + + // Builds LoomServer instance including connectionProviders list. + WebServer.Builder wsBuilder = WebServer.builder() + .config(config.get("server")); + + // Call wsBuilder.connectionProviders() trough reflection + Method connectionProviders + = WebServer.Builder.class.getDeclaredMethod("connectionProviders", (Class[]) null); + connectionProviders.setAccessible(true); + @SuppressWarnings("unchecked") + List providers + = (List) connectionProviders.invoke(wsBuilder, (Object[]) null); + + for (ServerConnectionSelector provider : providers) { + if (provider instanceof Http1ConnectionSelector) { + Http1Connection conn = (Http1Connection) provider.connection(new TestContext()); + assertThat(conn, notNullValue()); + + // Retrieve private upgradeProviderMap from Http1Connection trough reflection + Field upgradeProviderMapField = Http1Connection.class.getDeclaredField("upgradeProviderMap"); + upgradeProviderMapField.setAccessible(true); + @SuppressWarnings("unchecked") + Map upgradeProviderMap = (Map) upgradeProviderMapField.get(conn); + + WsUpgrader upgrader = (WsUpgrader) upgradeProviderMap.get("websocket"); + Set origins = upgrader.origins(); + assertThat(origins, containsInAnyOrder("origin1", "origin2", "origin3")); + } + } + + } + + // Verify that WsUpgrader is properly configured from builder + @Test + void testUpgraderConfigBuilder() { + WsUpgrader upgrader = (WsUpgrader) WsUpgradeProvider.builder() + .addOrigin("bOrigin1") + .addOrigin("bOrigin2") + .build() + .create(); + + Set origins = upgrader.origins(); + assertThat(origins, containsInAnyOrder("bOrigin1", "bOrigin2")); + } + +} diff --git a/nima/websocket/webserver/src/test/resources/application.yaml b/nima/websocket/webserver/src/test/resources/application.yaml new file mode 100644 index 00000000000..39b338064ea --- /dev/null +++ b/nima/websocket/webserver/src/test/resources/application.yaml @@ -0,0 +1,23 @@ +# +# 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. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server: + port: 8079 + host: 127.0.0.1 + + connection-providers: + websocket: + origins: [origin1,origin2,origin3] From adbb2b1d273d48605c96c527f3761ad3e2778e26 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Tue, 17 Jan 2023 22:59:14 +0100 Subject: [PATCH 2/2] Refactor to use immutable approach to configuration and to use @ConfigBean --- docs-internal/nima-connections.md | 55 +++++ etc/checkstyle-suppressions.xml | 6 +- .../native-image.properties | 17 -- .../helidon-media-jsonp/reflect-config.json | 7 - .../tyrus/TyrusUpgradeProvider.java | 47 ++-- .../grpc/webserver/GrpcProtocolHandler.java | 6 +- .../GrpcProtocolHandlerNotFound.java | 6 +- .../grpc/webserver/GrpcProtocolProvider.java | 78 +++--- nima/http2/webserver/pom.xml | 27 +- .../nima/http2/webserver/Http2Config.java | 133 ++++------ .../nima/http2/webserver/Http2Connection.java | 60 +++-- .../webserver/Http2ConnectionProvider.java | 98 ++++---- .../webserver/Http2ConnectionSelector.java | 14 +- .../nima/http2/webserver/Http2Stream.java | 22 +- .../http2/webserver/Http2UpgradeProvider.java | 110 +++++---- .../nima/http2/webserver/Http2Upgrader.java | 10 +- .../spi/Http2SubProtocolProvider.java | 85 +------ .../spi/Http2SubProtocolSelector.java | 106 ++++++++ .../webserver/spi/SubProtocolResult.java | 4 +- .../webserver/src/main/java/module-info.java | 18 +- .../http2/webserver/ConnectionConfigTest.java | 100 ++------ .../src/test/resources/application.yaml | 2 +- .../webserver/DirectClientConnection.java | 3 +- nima/testing/junit5/websocket/pom.xml | 4 + .../server/ConfiguredLimitsTest.java | 9 +- nima/webserver/webserver/pom.xml | 29 ++- .../nima/webserver/ConnectionHandler.java | 1 + .../nima/webserver/ConnectionProviders.java | 2 + .../io/helidon/nima/webserver/LoomServer.java | 1 + .../nima/webserver/ServerListener.java | 1 + .../io/helidon/nima/webserver/WebServer.java | 13 +- .../http1/Http1BuilderInterceptor.java | 47 ++++ .../nima/webserver/http1/Http1Config.java | 232 ++++-------------- .../nima/webserver/http1/Http1Connection.java | 33 +-- .../http1/Http1ConnectionProvider.java | 218 +++++----------- .../http1/Http1ConnectionSelector.java | 5 +- .../http1/spi/Http1UpgradeProvider.java | 22 +- .../http1/{ => spi}/Http1Upgrader.java | 7 +- .../nima/webserver/spi/ServerConnection.java | 2 +- .../spi/ServerConnectionProvider.java | 19 +- .../{ => spi}/ServerConnectionSelector.java | 4 +- .../webserver/src/main/java/module-info.java | 6 +- .../webserver/http1/ConnectionConfigTest.java | 2 +- .../webserver/WsUpgradeProvider.java | 63 ++--- .../nima/websocket/webserver/WsUpgrader.java | 16 +- .../WsUpgradeProviderConfigTest.java | 6 +- 46 files changed, 849 insertions(+), 907 deletions(-) create mode 100644 docs-internal/nima-connections.md delete mode 100644 media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/native-image.properties delete mode 100644 media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/reflect-config.json create mode 100644 nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolSelector.java create mode 100644 nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1BuilderInterceptor.java rename nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/{ => spi}/Http1Upgrader.java (88%) rename nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/{ => spi}/ServerConnectionSelector.java (96%) diff --git a/docs-internal/nima-connections.md b/docs-internal/nima-connections.md new file mode 100644 index 00000000000..e2605aa22e1 --- /dev/null +++ b/docs-internal/nima-connections.md @@ -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" +``` diff --git a/etc/checkstyle-suppressions.xml b/etc/checkstyle-suppressions.xml index 059f54a81e7..55af2b3c215 100644 --- a/etc/checkstyle-suppressions.xml +++ b/etc/checkstyle-suppressions.xml @@ -1,7 +1,7 @@ + + diff --git a/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/native-image.properties b/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/native-image.properties deleted file mode 100644 index 7f3a3e2cfeb..00000000000 --- a/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright (c) 2022 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. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args=--initialize-at-build-time=org.eclipse.parsson diff --git a/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/reflect-config.json b/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/reflect-config.json deleted file mode 100644 index ad999b261ad..00000000000 --- a/media/jsonp/src/main/resources/META-INF/native-image/io.helidon.media/helidon-media-jsonp/reflect-config.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "name": "org.eclipse.parsson.JsonProviderImpl", - "allPublicConstructors": true, - "allPublicMethods": true - } -] \ No newline at end of file diff --git a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java index ec0d171b31e..99396d5cfa4 100644 --- a/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java +++ b/microprofile/websocket/src/main/java/io/helidon/microprofile/tyrus/TyrusUpgradeProvider.java @@ -16,10 +16,11 @@ package io.helidon.microprofile.tyrus; -import java.util.HashSet; import java.util.Set; +import java.util.function.Function; -import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.config.Config; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.websocket.webserver.WsUpgradeProvider; /** @@ -27,21 +28,42 @@ */ public class TyrusUpgradeProvider extends WsUpgradeProvider { + TyrusUpgradeProvider(Builder builder) { + super(builder); + } + /** * @deprecated This constructor is only to be used by {@link java.util.ServiceLoader}, use {@link #builder()} */ @Deprecated() public TyrusUpgradeProvider() { - this(new HashSet<>()); + this(tyrusBuilder()); } - TyrusUpgradeProvider(Set origins) { - super(origins); + /** + * New builder. + * + * @return builder + */ + public static Builder tyrusBuilder() { + return new Builder(); } @Override - public Http1Upgrader create() { - return new TyrusUpgrader(Set.copyOf(origins())); + public Http1Upgrader create(Function config) { + Set usedOrigins; + + if (origins().isEmpty()) { + usedOrigins = config.apply(CONFIG_NAME) + .get("origins") + .asList(String.class) + .map(Set::copyOf) + .orElseGet(Set::of); + } else { + usedOrigins = origins(); + } + + return new TyrusUpgrader(usedOrigins); } // jUnit test accessor for origins set (package private only) @@ -49,15 +71,6 @@ protected Set origins() { return super.origins(); } - /** - * New builder. - * - * @return builder - */ - public static Builder tyrusBuilder() { - return new Builder(); - } - /** * Fluent API builder for {@link TyrusUpgradeProvider}. */ @@ -69,7 +82,7 @@ private Builder() { @Override public TyrusUpgradeProvider build() { - return new TyrusUpgradeProvider(origins()); + return new TyrusUpgradeProvider(this); } } diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java index f1365f56560..4f455c49cd8 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -37,7 +37,7 @@ import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2StreamWriter; import io.helidon.nima.http2.Http2WindowUpdate; -import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.grpc.Metadata; import io.grpc.MethodDescriptor; @@ -45,7 +45,7 @@ import io.grpc.ServerCallHandler; import io.grpc.Status; -class GrpcProtocolHandler implements Http2SubProtocolProvider.SubProtocolHandler { +class GrpcProtocolHandler implements Http2SubProtocolSelector.SubProtocolHandler { private static final System.Logger LOGGER = System.getLogger(GrpcProtocolHandler.class.getName()); private static final HeaderValue GRPC_CONTENT_TYPE = Header.createCached(Header.CONTENT_TYPE, "application/grpc"); private static final HeaderValue GRPC_ENCODING_IDENTITY = Header.createCached("grpc-encoding", "identity"); diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java index ea09cc50ca8..da8f7315660 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -26,9 +26,9 @@ import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2StreamWriter; import io.helidon.nima.http2.Http2WindowUpdate; -import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; -class GrpcProtocolHandlerNotFound implements Http2SubProtocolProvider.SubProtocolHandler { +class GrpcProtocolHandlerNotFound implements Http2SubProtocolSelector.SubProtocolHandler { private final Http2StreamWriter streamWriter; private final int streamId; private Http2StreamState currentStreamState; diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolProvider.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolProvider.java index 9458edca2bd..250874940eb 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolProvider.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -20,11 +20,13 @@ import io.helidon.common.http.Http; import io.helidon.common.http.Http.Header; import io.helidon.common.http.HttpPrologue; +import io.helidon.config.Config; import io.helidon.nima.http2.Http2Headers; import io.helidon.nima.http2.Http2Settings; import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2StreamWriter; import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.http2.webserver.spi.SubProtocolResult; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; @@ -43,45 +45,57 @@ public GrpcProtocolProvider() { } @Override - public SubProtocolResult subProtocol(ConnectionContext ctx, - HttpPrologue prologue, - Http2Headers headers, - Http2StreamWriter streamWriter, - int streamId, - Http2Settings serverSettings, - Http2Settings clientSettings, - Http2StreamState currentStreamState, - Router router) { - if (prologue.method() != Http.Method.POST) { - return NOT_SUPPORTED; - } + public String configKey() { + return "grpc"; + } - // we know this is HTTP/2, so no need to check protocol and version - Headers httpHeaders = headers.httpHeaders(); + @Override + public Http2SubProtocolSelector create(Config config) { + return new GrpcProtocolSelector(); + } - if (httpHeaders.contains(Header.CONTENT_TYPE)) { - String contentType = httpHeaders.get(Header.CONTENT_TYPE).value(); + private static class GrpcProtocolSelector implements Http2SubProtocolSelector { + @Override + public SubProtocolResult subProtocol(ConnectionContext ctx, + HttpPrologue prologue, + Http2Headers headers, + Http2StreamWriter streamWriter, + int streamId, + Http2Settings serverSettings, + Http2Settings clientSettings, + Http2StreamState currentStreamState, + Router router) { + if (prologue.method() != Http.Method.POST) { + return NOT_SUPPORTED; + } + + // we know this is HTTP/2, so no need to check protocol and version + Headers httpHeaders = headers.httpHeaders(); - if (contentType.startsWith("application/grpc")) { - GrpcRouting routing = router.routing(GrpcRouting.class, GrpcRouting.empty()); + if (httpHeaders.contains(Header.CONTENT_TYPE)) { + String contentType = httpHeaders.get(Header.CONTENT_TYPE).value(); - Grpc route = routing.findRoute(prologue); + if (contentType.startsWith("application/grpc")) { + GrpcRouting routing = router.routing(GrpcRouting.class, GrpcRouting.empty()); - if (route == null) { + Grpc route = routing.findRoute(prologue); + + if (route == null) { + return new SubProtocolResult(true, + new GrpcProtocolHandlerNotFound(streamWriter, streamId, currentStreamState)); + } return new SubProtocolResult(true, - new GrpcProtocolHandlerNotFound(streamWriter, streamId, currentStreamState)); + new GrpcProtocolHandler(prologue, + headers, + streamWriter, + streamId, + serverSettings, + clientSettings, + currentStreamState, + route)); } - return new SubProtocolResult(true, - new GrpcProtocolHandler(prologue, - headers, - streamWriter, - streamId, - serverSettings, - clientSettings, - currentStreamState, - route)); } + return NOT_SUPPORTED; } - return NOT_SUPPORTED; } } diff --git a/nima/http2/webserver/pom.xml b/nima/http2/webserver/pom.xml index afdf328b5e4..c9ff2b27308 100644 --- a/nima/http2/webserver/pom.xml +++ b/nima/http2/webserver/pom.xml @@ -16,7 +16,7 @@ --> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.helidon.nima.http2 @@ -36,12 +36,22 @@ io.helidon.nima.http2 helidon-nima-http2 + + io.helidon.pico.builder.config + helidon-pico-builder-config + io.helidon.common.features helidon-common-features-api provided true + + io.helidon.config + helidon-config-metadata + provided + true + io.helidon.config helidon-config-yaml @@ -57,6 +67,11 @@ hamcrest-all test + + org.mockito + mockito-core + test + @@ -71,6 +86,16 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.pico.builder.config + helidon-pico-builder-config-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index a59362b8796..4d8679711aa 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -13,99 +13,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.helidon.nima.http2.webserver; -import io.helidon.config.Config; -import io.helidon.nima.http2.Http2Setting; -import io.helidon.nima.http2.Http2Settings; +import io.helidon.builder.Builder; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.builder.config.ConfigBean; /** * HTTP/2 server configuration. */ -class Http2Config { - - private static final String CONFIG_MAX_FRAME_SIZE = "max-frame-size"; - private static final String CONFIG_MAX_HEADER_LIST_SIZE = "max-header-size"; - - private final long maxFrameSize; - private final long maxHeaderListSize; - - private Http2Config(long maxFrameSize, long maxHeaderListSize) { - this.maxFrameSize = maxFrameSize; - this.maxHeaderListSize = maxHeaderListSize; - } - - Long maxFrameSize() { - return maxFrameSize; - } - - Long maxHeaderListSize() { - return maxHeaderListSize; - } - - // Apply configuration values on HTTP settings frame builder - Http2Settings.Builder apply(Http2Settings.Builder builder) { - applySetting(builder, maxFrameSize, Http2Setting.MAX_FRAME_SIZE); - applySetting(builder, maxHeaderListSize, Http2Setting.MAX_HEADER_LIST_SIZE); - return builder; - } - - // Add value to the builder only when differs from default - private void applySetting(Http2Settings.Builder builder, long value, Http2Setting settings) { - if (value != settings.defaultValue()) { - builder.add(settings, value); - } - } - - // Return builder with default values - static Builder builder() { - return new Builder(); - } - - // Return builder with values initialized from existing configuration - static Builder builder(Http2Config config) { - return new Builder(config); - } - - static class Builder { - - private long maxFrameSize; - private long maxHeaderListSize; - - private Builder() { - maxFrameSize = Http2Setting.MAX_FRAME_SIZE.defaultValue(); - maxHeaderListSize = Http2Setting.MAX_HEADER_LIST_SIZE.defaultValue(); - } - - private Builder(Http2Config config) { - maxFrameSize = config.maxFrameSize; - maxHeaderListSize = config.maxHeaderListSize; - } - - - // Get values from config. - Builder config(Config config) { - config.get(CONFIG_MAX_FRAME_SIZE).asLong() - .ifPresent(value -> maxFrameSize = value); - config.get(CONFIG_MAX_HEADER_LIST_SIZE).asLong() - .ifPresent(value -> maxHeaderListSize = value); - return this; - } - - Builder maxFrameSize(long maxFrameSize) { - this.maxFrameSize = maxFrameSize; - return this; - } - - Builder maxHeaderListSize(long maxHeaderListSize) { - this.maxHeaderListSize = maxHeaderListSize; - return this; - } - - Http2Config build() { - return new Http2Config(maxFrameSize, maxHeaderListSize); - } - - } - +@Builder +@ConfigBean(key = "server.connection-providers.http_2") +public interface Http2Config { + /** + * The size of the largest frame payload that the sender is willing to receive in bytes. + * See RFC 9113 section 6.5.2 for details. + * + * @return maximal frame size + */ + @ConfiguredOption("16_384") + long maxFrameSize(); + + /** + * The maximum field section size that the sender is prepared to accept in bytes. + * See RFC 9113 section 6.5.2 for details. + * Default is maximal unsigned int. + * + * @return maximal header list size in bytes + */ + @ConfiguredOption("0xFFFFFFFFL") + long maxHeaderListSize(); + + /** + * Initial maximal size of client frames. + * + * @return maximal size in bytes + */ + @ConfiguredOption("16384") + int maxClientFrameSize(); + + /** + * Whether to send error message over HTTP to client. + * Defaults to {@code false}, as exception message may contain internal information that could be used as an + * attack vector. Use with care and in cases where both server and clients are under your full control (such as for + * testing). + * + * @return whether to send error messages over the network + */ + @ConfiguredOption("false") + boolean sendErrorDetails(); } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index d60cb4d161e..cce802f7aa3 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -52,6 +52,7 @@ import io.helidon.nima.http2.Http2Util; import io.helidon.nima.http2.Http2WindowUpdate; import io.helidon.nima.http2.WindowSize; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.http.HttpRouting; @@ -76,17 +77,20 @@ public class Http2Connection implements ServerConnection { private final Map streams = new HashMap<>(1000); private final ConnectionContext ctx; - private final Http2Config config; + private final Http2Config http2Config; private final HttpRouting routing; private final Http2Headers.DynamicTable requestDynamicTable; private final Http2HuffmanDecoder requestHuffman; private final Http2FrameListener receiveFrameListener = // Http2FrameListener.create(List.of()); Http2FrameListener.create(List.of(new Http2LoggingFrameListener("recv"))); private final Http2ConnectionWriter connectionWriter; + private final List subProviders; private final WindowSize connectionWindowSize = new WindowSize(); private final DataReader reader; private final Http2Settings serverSettings; + private final boolean sendErrorDetails; + // initial client settings, until we receive real ones private Http2Settings clientSettings = Http2Settings.builder() .build(); @@ -96,25 +100,27 @@ public class Http2Connection implements ServerConnection { private boolean expectPreface; private HttpPrologue upgradePrologue; private Http2Headers upgradeHeaders; - private boolean returnErrorDetails = true; private State state = State.WRITE_SERVER_SETTINGS; private int continuationExpectedStreamId; private int lastStreamId; - private long maxClientFrameSize = 16_384; + private long maxClientFrameSize; private int streamInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; - Http2Connection(ConnectionContext ctx, Http2Config config) { + Http2Connection(ConnectionContext ctx, Http2Config http2Config, List subProviders) { this.ctx = ctx; - this.config = config; - this.serverSettings = config.apply( - Http2Settings.builder() - .add(Http2Setting.ENABLE_PUSH, false)) + this.http2Config = http2Config; + this.serverSettings = Http2Settings.builder() + .update(builder -> settingsUpdate(http2Config, builder)) + .add(Http2Setting.ENABLE_PUSH, false) .build(); this.connectionWriter = new Http2ConnectionWriter(ctx, ctx.dataWriter(), List.of(new Http2LoggingFrameListener("send"))); + this.subProviders = subProviders; this.requestDynamicTable = Http2Headers.DynamicTable.create(serverSettings.value(Http2Setting.HEADER_TABLE_SIZE)); this.requestHuffman = new Http2HuffmanDecoder(); this.routing = ctx.router().routing(HttpRouting.class, HttpRouting.empty()); this.reader = ctx.dataReader(); + this.sendErrorDetails = http2Config.sendErrorDetails(); + this.maxClientFrameSize = http2Config.maxClientFrameSize(); } @Override @@ -134,7 +140,7 @@ public void handle() throws InterruptedException { Http2GoAway frame = new Http2GoAway(0, e.code(), - returnErrorDetails ? e.getMessage() : ""); + sendErrorDetails ? e.getMessage() : ""); connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); state = State.FINISHED; } catch (CloseConnectionException | InterruptedException e) { @@ -146,7 +152,7 @@ public void handle() throws InterruptedException { } Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.INTERNAL, - returnErrorDetails ? e.getClass().getName() + ": " + e.getMessage() : ""); + sendErrorDetails ? e.getClass().getName() + ": " + e.getMessage() : ""); connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); state = State.FINISHED; throw e; @@ -188,6 +194,28 @@ public String toString() { return "[" + ctx.socketId() + " " + ctx.childSocketId() + "]"; } + // jUnit Http2Config pkg only visible test accessor. + Http2Config config() { + return http2Config; + } + + // jUnit Http2Settings pkg only visible test accessor. + Http2Settings serverSettings() { + return serverSettings; + } + + private static void settingsUpdate(Http2Config config, Http2Settings.Builder builder) { + applySetting(builder, config.maxFrameSize(), Http2Setting.MAX_FRAME_SIZE); + applySetting(builder, config.maxHeaderListSize(), Http2Setting.MAX_HEADER_LIST_SIZE); + } + + // Add value to the builder only when differs from default + private static void applySetting(Http2Settings.Builder builder, long value, Http2Setting settings) { + if (value != settings.defaultValue()) { + builder.add(settings, value); + } + } + private void doHandle() throws InterruptedException { while (state != State.FINISHED) { @@ -630,6 +658,8 @@ private StreamContext stream(int streamId) { streamContext = new StreamContext(streamId, new Http2Stream(ctx, routing, + http2Config, + subProviders, streamId, serverSettings, clientSettings, @@ -660,16 +690,6 @@ private void updateHeaderTableSize(long value) { } } - // jUnit Http2Config pkg only visible test accessor. - Http2Config config() { - return config; - } - - // jUnit Http2Settings pkg only visible test accessor. - Http2Settings serverSettings() { - return serverSettings; - } - private enum State { READ_FRAME, DATA, diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionProvider.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionProvider.java index d5178f1cb8f..562b98a489e 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionProvider.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionProvider.java @@ -17,20 +17,32 @@ package io.helidon.nima.http2.webserver; import java.util.List; +import java.util.ServiceLoader; +import java.util.function.Function; +import io.helidon.common.HelidonServiceLoader; import io.helidon.config.Config; -import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; import io.helidon.nima.webserver.spi.ServerConnectionProvider; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; /** * {@link io.helidon.nima.webserver.spi.ServerConnectionProvider} implementation for HTTP/2 server connection provider. */ public class Http2ConnectionProvider implements ServerConnectionProvider { - /** HTTP/2 server connection provider configuration node name. */ + /** + * HTTP/2 server connection provider configuration node name. + */ static final String CONFIG_NAME = "http_2"; - private Http2Config config; + private final Http2Config http2Config; + private final List subProtocolProviders; + + private Http2ConnectionProvider(Builder builder) { + this.subProtocolProviders = builder.subProtocolProviders.build().asList(); + this.http2Config = builder.http2Config; + } /** * Creates an instance of HTTP/2 server connection provider. @@ -39,11 +51,16 @@ public class Http2ConnectionProvider implements ServerConnectionProvider { */ @Deprecated public Http2ConnectionProvider() { - this.config = Http2Config.builder().build(); + this(builder()); } - private Http2ConnectionProvider(Http2Config config) { - this.config = config; + /** + * Builder to set up this provider. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); } @Override @@ -52,71 +69,60 @@ public List configKeys() { } @Override - public void config(Config config) { - // Empty node can't overwrite existing configuration. - if (config.exists()) { - // Initialize builder with existing configuration - this.config = Http2Config.builder(this.config) - // Overwrite values from config node - .config(config) - .build(); + public ServerConnectionSelector create(Function configs) { + Http2Config config; + if (http2Config == null) { + config = DefaultHttp2Config.toBuilder(configs.apply(CONFIG_NAME)).build(); + } else { + config = http2Config; } - } - @Override - public ServerConnectionSelector create() { - return new Http2ConnectionSelector(config); - } + var subProviders = subProtocolProviders.stream() + .map(it -> it.create(configs.apply(it.configKey()))) + .toList(); - /** - * Builder to set up this provider. - * - * @return a new builder - */ - public static Builder builder() { - return new Builder(); + return new Http2ConnectionSelector(config, subProviders); } /** * Fluent API builder for {@link Http2ConnectionProvider}. */ public static class Builder implements io.helidon.common.Builder { - - private final Http2Config.Builder configBuilder; + private final HelidonServiceLoader.Builder subProtocolProviders = HelidonServiceLoader.builder( + ServiceLoader.load(Http2SubProtocolProvider.class)); + private Http2Config http2Config; private Builder() { - configBuilder = Http2Config.builder(); + } + + @Override + public Http2ConnectionProvider build() { + return new Http2ConnectionProvider(this); } /** - * The size of the largest frame payload that the sender is willing to receive in bytes. - * See RFC 9113 section 6.5.2 for details. + * Custom configuration of HTTP/2 connection provider. + * If not defined, it will be configured from config, or defaults would be used. * - * @param maxFrameSize maximum length of the frame payload + * @param http2Config HTTP/2 configuration * @return updated builder */ - public Builder maxFrameSize(long maxFrameSize) { - configBuilder.maxFrameSize(maxFrameSize); + public Builder http2Config(Http2Config http2Config) { + this.http2Config = http2Config; return this; } /** - * The maximum field section size that the sender is prepared to accept in bytes. - * See RFC 9113 section 6.5.2 for details. + * Add a configured sub-protocol provider. This will replace the instance discovered through service loader (if one + * exists). * - * @param maxHeaderListSize maximum field section size - * @return updated builder + * @param provider provider to add + * @return updated builer */ - public Builder maxHeaderListSize(long maxHeaderListSize) { - configBuilder.maxHeaderListSize(maxHeaderListSize); + public Builder addSubProtocolProvider(Http2SubProtocolProvider provider) { + subProtocolProviders.addService(provider); return this; } - - @Override - public Http2ConnectionProvider build() { - return new Http2ConnectionProvider(configBuilder.build()); - } - } } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionSelector.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionSelector.java index b93f33ae6f5..04daa30007b 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionSelector.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ConnectionSelector.java @@ -16,12 +16,14 @@ package io.helidon.nima.http2.webserver; +import java.util.List; import java.util.Set; import io.helidon.common.buffers.BufferData; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.ServerConnectionSelector; import io.helidon.nima.webserver.spi.ServerConnection; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import static io.helidon.nima.http2.Http2Util.PREFACE_LENGTH; import static io.helidon.nima.http2.Http2Util.isPreface; @@ -31,11 +33,13 @@ */ public class Http2ConnectionSelector implements ServerConnectionSelector { - private final Http2Config config; + private final Http2Config http2Config; + private final List subProviders; // Creates an instance of HTTP/2 server connection selector. - Http2ConnectionSelector(Http2Config config) { - this.config = config; + Http2ConnectionSelector(Http2Config http2Config, List subProviders) { + this.http2Config = http2Config; + this.subProviders = subProviders; } @Override @@ -64,7 +68,7 @@ public Set supportedApplicationProtocols() { @Override public ServerConnection connection(ConnectionContext ctx) { - Http2Connection result = new Http2Connection(ctx, config); + Http2Connection result = new Http2Connection(ctx, http2Config, subProviders); result.expectPreface(); return result; diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index ffa90b1d21e..77fcca0d7ed 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -18,10 +18,8 @@ import java.io.UncheckedIOException; import java.util.List; -import java.util.ServiceLoader; import java.util.concurrent.ArrayBlockingQueue; -import io.helidon.common.HelidonServiceLoader; import io.helidon.common.buffers.BufferData; import io.helidon.common.http.DirectHandler; import io.helidon.common.http.Headers; @@ -45,7 +43,7 @@ import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2StreamWriter; import io.helidon.nima.http2.Http2WindowUpdate; -import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.http2.webserver.spi.SubProtocolResult; import io.helidon.nima.webserver.CloseConnectionException; import io.helidon.nima.webserver.ConnectionContext; @@ -62,13 +60,13 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream Http2Flag.DataFlags.create(Http2Flag.DataFlags.END_OF_STREAM), 0), BufferData.empty()); private static final System.Logger LOGGER = System.getLogger(Http2Stream.class.getName()); - private static final List SUB_PROTOCOL_PROVIDERS = - HelidonServiceLoader.create(ServiceLoader.load(Http2SubProtocolProvider.class)) - .asList(); + // todo nima - use or remove //private final ContentEncodingContext contentEncodingContext = ContentEncodingContext.create(); private final FlowControl flowControl; private final ConnectionContext ctx; + private final Http2Config http2Config; + private final List subProviders; private final int streamId; private final Http2Settings serverSettings; private final Http2Settings clientSettings; @@ -80,7 +78,7 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream private volatile Http2Priority priority; // used from this instance and from connection private volatile Http2StreamState state = Http2StreamState.IDLE; - private Http2SubProtocolProvider.SubProtocolHandler subProtocolHandler; + private Http2SubProtocolSelector.SubProtocolHandler subProtocolHandler; private long expectedLength = -1; private HttpRouting routing; private HttpPrologue prologue; @@ -90,6 +88,8 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream * * @param ctx connection context * @param routing HTTP routing + * @param http2Config HTTP/2 configuration + * @param subProviders * @param streamId stream id * @param serverSettings server settings * @param clientSettings client settings @@ -98,6 +98,8 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream */ public Http2Stream(ConnectionContext ctx, HttpRouting routing, + Http2Config http2Config, + List subProviders, int streamId, Http2Settings serverSettings, Http2Settings clientSettings, @@ -105,6 +107,8 @@ public Http2Stream(ConnectionContext ctx, FlowControl flowControl) { this.ctx = ctx; this.routing = routing; + this.http2Config = http2Config; + this.subProviders = subProviders; this.streamId = streamId; this.serverSettings = serverSettings; this.clientSettings = clientSettings; @@ -329,7 +333,7 @@ private void handle() { subProtocolHandler = null; - for (Http2SubProtocolProvider provider : SUB_PROTOCOL_PROVIDERS) { + for (Http2SubProtocolSelector provider : subProviders) { SubProtocolResult subProtocolResult = provider.subProtocol(ctx, prologue, headers, diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2UpgradeProvider.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2UpgradeProvider.java index 44717b30b95..f4c80d91741 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2UpgradeProvider.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2UpgradeProvider.java @@ -16,16 +16,30 @@ package io.helidon.nima.http2.webserver; +import java.util.HashSet; +import java.util.List; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.helidon.common.HelidonServiceLoader; import io.helidon.config.Config; -import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; /** * {@link java.util.ServiceLoader} upgrade protocol provider to upgrade from HTTP/1.1 to HTTP/2. */ public class Http2UpgradeProvider implements Http1UpgradeProvider { + private final Http2Config http2Config; + private final List subProtocolProviders; - private Http2Config config; + private Http2UpgradeProvider(Builder builder) { + this.http2Config = builder.http2Config; + this.subProtocolProviders = builder.subProtocolProviders.build().asList(); + } /** * Create a new instance with default configuration. @@ -33,84 +47,86 @@ public class Http2UpgradeProvider implements Http1UpgradeProvider { * @deprecated to be used solely by {@link java.util.ServiceLoader} */ public Http2UpgradeProvider() { - config = Http2Config.builder().build(); + this(builder()); } - private Http2UpgradeProvider(Http2Config config) { - this.config = config; + /** + * Builder to set up this provider. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); } @Override - public String configKey() { - return Http2ConnectionProvider.CONFIG_NAME; + public Set configKeys() { + Set result = new HashSet<>(); + result.add(Http2ConnectionProvider.CONFIG_NAME); + + result.addAll(subProtocolProviders.stream() + .map(Http2SubProtocolProvider::configKey) + .collect(Collectors.toSet())); + + return result; } @Override - public void config(Config config) { - // Empty node can't overwrite existing configuration. - if (config.exists()) { - // Initialize builder with existing configuration - this.config = Http2Config.builder(this.config) - // Overwrite values from config node - .config(config) - .build(); + public Http1Upgrader create(Function config) { + Http2Config usedConfig; + + if (http2Config == null) { + usedConfig = DefaultHttp2Config.toBuilder(config.apply(Http2ConnectionProvider.CONFIG_NAME)).build(); + } else { + usedConfig = http2Config; } - } - @Override - public Http1Upgrader create() { - return new Http2Upgrader(config); - } + var subProtocolSelectors = subProtocolProviders.stream() + .map(it -> it.create(config.apply(it.configKey()))) + .toList(); - /** - * Builder to set up this provider. - * - * @return a new builder - */ - public static Builder builder() { - return new Builder(); + return new Http2Upgrader(usedConfig, subProtocolSelectors); } /** * Fluent API builder for {@link Http2UpgradeProvider}. */ public static class Builder implements io.helidon.common.Builder { + private final HelidonServiceLoader.Builder subProtocolProviders = HelidonServiceLoader.builder( + ServiceLoader.load(Http2SubProtocolProvider.class)); - private final Http2Config.Builder configBuilder; + private Http2Config http2Config; private Builder() { - this.configBuilder = Http2Config.builder(); + } + + @Override + public Http2UpgradeProvider build() { + return new Http2UpgradeProvider(this); } /** - * The size of the largest frame payload that the sender is willing to receive in bytes. - * See RFC 9113 section 6.5.2 for details. + * Custom configuration of HTTP/2 connections. + * If not defined, it will be configured from config, or defaults would be used. * - * @param maxFrameSize maximum length of the frame payload + * @param http2Config HTTP/2 configuration * @return updated builder */ - public Builder maxFrameSize(long maxFrameSize) { - configBuilder.maxFrameSize(maxFrameSize); + public Builder http2Config(Http2Config http2Config) { + this.http2Config = http2Config; return this; } /** - * The maximum field section size that the sender is prepared to accept in bytes. - * See RFC 9113 section 6.5.2 for details. + * Add a configured sub-protocol provider. This will replace the instance discovered through service loader (if one + * exists). * - * @param maxHeaderListSize maximum field section size - * @return updated builder + * @param provider provider to add + * @return updated builer */ - public Builder maxHeaderListSize(long maxHeaderListSize) { - configBuilder.maxHeaderListSize(maxHeaderListSize); + public Builder addSubProtocolProvider(Http2SubProtocolProvider provider) { + subProtocolProviders.addService(provider); return this; } - - @Override - public Http2UpgradeProvider build() { - return new Http2UpgradeProvider(configBuilder.build()); - } - } - } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Upgrader.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Upgrader.java index 0c7c89daa40..71f3e9ba9ae 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Upgrader.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Upgrader.java @@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.List; import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataWriter; @@ -27,8 +28,9 @@ import io.helidon.common.http.WritableHeaders; import io.helidon.nima.http2.Http2Headers; import io.helidon.nima.http2.Http2Settings; +import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.webserver.spi.ServerConnection; /** @@ -44,12 +46,14 @@ public class Http2Upgrader implements Http1Upgrader { private static final Base64.Decoder BASE_64_DECODER = Base64.getDecoder(); private final Http2Config config; + private final List subProtocolProviders; /** * Creates an instance of HTTP/1.1 to HTTP/2 protocol upgrade. */ - Http2Upgrader(Http2Config config) { + Http2Upgrader(Http2Config config, List subProtocolProviders) { this.config = config; + this.subProtocolProviders = subProtocolProviders; } @Override @@ -61,7 +65,7 @@ public String supportedProtocol() { public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders headers) { - Http2Connection connection = new Http2Connection(ctx, config); + Http2Connection connection = new Http2Connection(ctx, config, subProtocolProviders); if (headers.contains(HTTP2_SETTINGS_HEADER_NAME)) { connection.clientSettings(Http2Settings.create(BufferData.create(BASE_64_DECODER.decode(headers.get( HTTP2_SETTINGS_HEADER_NAME).value().getBytes(StandardCharsets.US_ASCII))))); diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolProvider.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolProvider.java index bc58afb9007..0dc24bd7d68 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolProvider.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -16,91 +16,26 @@ package io.helidon.nima.http2.webserver.spi; -import io.helidon.common.buffers.BufferData; -import io.helidon.common.http.HttpPrologue; -import io.helidon.nima.http2.Http2FrameHeader; -import io.helidon.nima.http2.Http2Headers; -import io.helidon.nima.http2.Http2RstStream; -import io.helidon.nima.http2.Http2Settings; -import io.helidon.nima.http2.Http2StreamState; -import io.helidon.nima.http2.Http2StreamWriter; -import io.helidon.nima.http2.Http2WindowUpdate; -import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.Router; +import io.helidon.config.Config; /** * {@link java.util.ServiceLoader} provider interface for HTTP/2 sub-protocols. */ public interface Http2SubProtocolProvider { - /** - * Not supported sub-protocol result. - */ - SubProtocolResult NOT_SUPPORTED = new SubProtocolResult(false, null); /** - * Check if this is a sub-protocol request and return appropriate result. + * Provider's specific configuration node name. * - * @param ctx connection context - * @param prologue received prologue - * @param headers received headers - * @param streamWriter stream writer - * @param streamId stream id - * @param serverSettings server settings - * @param clientSettings client settings - * @param currentStreamState current stream state - * @param router router - * @return sub-protocol result + * @return name of the node to request */ - SubProtocolResult subProtocol(ConnectionContext ctx, - HttpPrologue prologue, - Http2Headers headers, - Http2StreamWriter streamWriter, - int streamId, - Http2Settings serverSettings, - Http2Settings clientSettings, - Http2StreamState currentStreamState, - Router router); + String configKey(); /** - * Handler of a sub-protocol. + * Creates an instance of HTTP/2 sub-protocol selector. + * + * @param config {@link io.helidon.config.Config} configuration node located on the node returned by {@link #configKey()} + * @return new HTTP/2 sub-protocol selector */ - interface SubProtocolHandler { - /** - * Called once the sub-protocol handler is available. - */ - void init(); - - /** - * Current stream state. - * - * @return stream state - */ - Http2StreamState streamState(); - - /** - * RST stream was received. - * - * @param rstStream RST stream frame - */ - void rstStream(Http2RstStream rstStream); - - /** - * Window update was received. - * - * @param update window update frame - */ - void windowUpdate(Http2WindowUpdate update); + Http2SubProtocolSelector create(Config config); - /** - * Data was received. - * The data may be empty. Check the - * {@link io.helidon.nima.http2.Http2FrameHeader#flags(io.helidon.nima.http2.Http2FrameTypes)} - * if this is {@link io.helidon.nima.http2.Http2Flag.DataFlags#END_OF_STREAM} to identify if this is the last data - * incoming. - * - * @param header frame header - * @param data frame data - */ - void data(Http2FrameHeader header, BufferData data); - } } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolSelector.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolSelector.java new file mode 100644 index 00000000000..660d6b0075a --- /dev/null +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/Http2SubProtocolSelector.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022, 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.http2.webserver.spi; + +import io.helidon.common.buffers.BufferData; +import io.helidon.common.http.HttpPrologue; +import io.helidon.nima.http2.Http2FrameHeader; +import io.helidon.nima.http2.Http2Headers; +import io.helidon.nima.http2.Http2RstStream; +import io.helidon.nima.http2.Http2Settings; +import io.helidon.nima.http2.Http2StreamState; +import io.helidon.nima.http2.Http2StreamWriter; +import io.helidon.nima.http2.Http2WindowUpdate; +import io.helidon.nima.webserver.ConnectionContext; +import io.helidon.nima.webserver.Router; + +/** + * A selector of HTTP/2 sub-protocols. + */ +public interface Http2SubProtocolSelector { + /** + * Not supported sub-protocol result. + */ + SubProtocolResult NOT_SUPPORTED = new SubProtocolResult(false, null); + + /** + * Check if this is a sub-protocol request and return appropriate result. + * + * @param ctx connection context + * @param prologue received prologue + * @param headers received headers + * @param streamWriter stream writer + * @param streamId stream id + * @param serverSettings server settings + * @param clientSettings client settings + * @param currentStreamState current stream state + * @param router router + * @return sub-protocol result + */ + SubProtocolResult subProtocol(ConnectionContext ctx, + HttpPrologue prologue, + Http2Headers headers, + Http2StreamWriter streamWriter, + int streamId, + Http2Settings serverSettings, + Http2Settings clientSettings, + Http2StreamState currentStreamState, + Router router); + + /** + * Handler of a sub-protocol. + */ + interface SubProtocolHandler { + /** + * Called once the sub-protocol handler is available. + */ + void init(); + + /** + * Current stream state. + * + * @return stream state + */ + Http2StreamState streamState(); + + /** + * RST stream was received. + * + * @param rstStream RST stream frame + */ + void rstStream(Http2RstStream rstStream); + + /** + * Window update was received. + * + * @param update window update frame + */ + void windowUpdate(Http2WindowUpdate update); + + /** + * Data was received. + * The data may be empty. Check the + * {@link io.helidon.nima.http2.Http2FrameHeader#flags(io.helidon.nima.http2.Http2FrameTypes)} + * if this is {@link io.helidon.nima.http2.Http2Flag.DataFlags#END_OF_STREAM} to identify if this is the last data + * incoming. + * + * @param header frame header + * @param data frame data + */ + void data(Http2FrameHeader header, BufferData data); + } +} diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/SubProtocolResult.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/SubProtocolResult.java index 5b5190825f2..b15588b014c 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/SubProtocolResult.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/spi/SubProtocolResult.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -22,5 +22,5 @@ * @param supported whether this provider supports the current request * @param subProtocol handler to use for this request, may be null for unsupported requests */ -public record SubProtocolResult(boolean supported, Http2SubProtocolProvider.SubProtocolHandler subProtocol) { +public record SubProtocolResult(boolean supported, Http2SubProtocolSelector.SubProtocolHandler subProtocol) { } diff --git a/nima/http2/webserver/src/main/java/module-info.java b/nima/http2/webserver/src/main/java/module-info.java index ae98391fab2..33ab3001739 100644 --- a/nima/http2/webserver/src/main/java/module-info.java +++ b/nima/http2/webserver/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -16,19 +16,19 @@ import io.helidon.common.features.api.Feature; import io.helidon.common.features.api.HelidonFlavor; -import io.helidon.nima.webserver.spi.ServerConnectionProvider; /** * HTTP/2 WebServer. */ @Feature(value = "HTTP/2", - description = "HTTP/2 WebServer", - in = HelidonFlavor.NIMA, - invalidIn = HelidonFlavor.SE, - path = {"WebServer", "HTTP/2"} + description = "HTTP/2 WebServer", + in = HelidonFlavor.NIMA, + invalidIn = HelidonFlavor.SE, + path = {"WebServer", "HTTP/2"} ) module io.helidon.nima.http2.webserver { requires static io.helidon.common.features.api; + requires static io.helidon.config.metadata; requires transitive io.helidon.common; requires transitive io.helidon.common.uri; @@ -38,14 +38,18 @@ requires transitive io.helidon.nima.http2; requires transitive io.helidon.nima.http.encoding; requires transitive io.helidon.nima.http.media; + requires io.helidon.pico.builder.config; + requires io.helidon.builder; exports io.helidon.nima.http2.webserver; exports io.helidon.nima.http2.webserver.spi; // to support prior knowledge for h2c - provides ServerConnectionProvider + provides io.helidon.nima.webserver.spi.ServerConnectionProvider with io.helidon.nima.http2.webserver.Http2ConnectionProvider; // to support upgrade requests for h2c provides io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider with io.helidon.nima.http2.webserver.Http2UpgradeProvider; + + uses io.helidon.nima.http2.webserver.spi.Http2SubProtocolProvider; } diff --git a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java index 635fd6128eb..b19b719f736 100644 --- a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java +++ b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java @@ -29,7 +29,7 @@ import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; import io.helidon.nima.webserver.Routing; -import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import io.helidon.nima.webserver.ServerContext; import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.DirectHandlers; @@ -38,87 +38,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class ConnectionConfigTest { - // ConnectionContext mockup - private static class TestContext implements ConnectionContext { - - @Override - public PeerInfo remotePeer() { - return null; - } - - @Override - public PeerInfo localPeer() { - return null; - } - - @Override - public boolean isSecure() { - return false; - } - - @Override - public String socketId() { - return null; - } - - @Override - public String childSocketId() { - return null; - } - - @Override - public ServerContext serverContext() { - return null; - } - - @Override - public ExecutorService sharedExecutor() { - return null; - } - - @Override - public DataWriter dataWriter() { - return null; - } - - @Override - public DataReader dataReader() { - return null; - } - - @Override - public Router router() { - return new Router() { - @Override - public T routing(Class routingType, T defaultValue) { - return null; - } - - @Override - public void afterStop() { - } - - @Override - public void beforeStart() { - } - }; - } - - @Override - public long maxPayloadSize() { - return 0; - } - - @Override - public DirectHandlers directHandlers() { - return null; - } - - } - // Verify that HTTP/2 connection provider is properly configured from config file @Test void testConnectionConfig() @@ -145,7 +69,7 @@ void testConnectionConfig() for (ServerConnectionSelector provider : providers) { if (provider instanceof Http2ConnectionSelector) { haveHttp2Provider = true; - Http2Connection conn = (Http2Connection) provider.connection(new TestContext()); + Http2Connection conn = (Http2Connection) provider.connection(mockContext()); // Verify values to be updated from configuration file assertThat(conn.config().maxFrameSize(), is(8192L)); assertThat(conn.config().maxHeaderListSize(), is(4096L)); @@ -162,12 +86,15 @@ void testConnectionConfig() void testProviderConfigBuilder() { Http2ConnectionSelector provider = (Http2ConnectionSelector) Http2ConnectionProvider.builder() - .maxFrameSize(4096L) - .maxHeaderListSize(2048L) + .http2Config(DefaultHttp2Config.builder() + .maxFrameSize(4096L) + .maxHeaderListSize(2048L) + .build()) .build() - .create(); + .create(it -> Config.empty()); + - Http2Connection conn = (Http2Connection) provider.connection(new TestContext()); + Http2Connection conn = (Http2Connection) provider.connection(mockContext()); // Verify values to be updated from configuration file assertThat(conn.config().maxFrameSize(), is(4096L)); assertThat(conn.config().maxHeaderListSize(), is(2048L)); @@ -176,4 +103,9 @@ void testProviderConfigBuilder() { assertThat(conn.serverSettings().value(Http2Setting.MAX_HEADER_LIST_SIZE), is(2048L)); } + private static ConnectionContext mockContext() { + ConnectionContext ctx = mock(ConnectionContext.class); + when(ctx.router()).thenReturn(Router.empty()); + return ctx; + } } diff --git a/nima/http2/webserver/src/test/resources/application.yaml b/nima/http2/webserver/src/test/resources/application.yaml index a1f8da86365..2f424c70385 100644 --- a/nima/http2/webserver/src/test/resources/application.yaml +++ b/nima/http2/webserver/src/test/resources/application.yaml @@ -21,4 +21,4 @@ server: connection-providers: http_2: max-frame-size: 8192 - max-header-size: 4096 + max-header-list-size: 4096 diff --git a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java index ecf54d84da5..60ad024c87f 100644 --- a/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java +++ b/nima/testing/junit5/webserver/src/main/java/io/helidon/nima/testing/junit5/webserver/DirectClientConnection.java @@ -26,6 +26,7 @@ import io.helidon.common.buffers.DataWriter; import io.helidon.common.context.Context; import io.helidon.common.socket.PeerInfo; +import io.helidon.config.Config; import io.helidon.nima.http.encoding.ContentEncodingContext; import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webclient.ClientConnection; @@ -159,7 +160,7 @@ private void startServer() { ServerConnection connection = Http1ConnectionProvider.builder() .build() - .create() + .create(it -> Config.empty()) .connection(ctx); executorService.submit(() -> { try { diff --git a/nima/testing/junit5/websocket/pom.xml b/nima/testing/junit5/websocket/pom.xml index bf3a69b77a3..904682edbd5 100644 --- a/nima/testing/junit5/websocket/pom.xml +++ b/nima/testing/junit5/websocket/pom.xml @@ -45,6 +45,10 @@ io.helidon.nima.websocket helidon-nima-websocket-webserver + + io.helidon.security.integration + helidon-security-integration-nima + org.junit.jupiter junit-jupiter-api diff --git a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ConfiguredLimitsTest.java b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ConfiguredLimitsTest.java index 9ac2dbcca00..f09bf1a73fe 100644 --- a/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ConfiguredLimitsTest.java +++ b/nima/tests/integration/webserver/webserver/src/test/java/io/helidon/nima/tests/integration/server/ConfiguredLimitsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -30,6 +30,7 @@ import io.helidon.nima.webserver.http.HttpRules; import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; +import io.helidon.nima.webserver.http1.DefaultHttp1Config; import io.helidon.nima.webserver.http1.Http1ConnectionProvider; import io.helidon.nima.webserver.spi.ServerConnectionProvider; @@ -58,8 +59,10 @@ class ConfiguredLimitsTest { @SetUpServer static void server(WebServer.Builder server) { ServerConnectionProvider http1 = Http1ConnectionProvider.builder() - .maxHeadersSize(1024) - .maxPrologueLength(512) + .http1Config(DefaultHttp1Config.builder() + .maxHeadersSize(1024) + .maxPrologueLength(512) + .build()) .build(); server.addConnectionProvider(http1); diff --git a/nima/webserver/webserver/pom.xml b/nima/webserver/webserver/pom.xml index 88063604e12..44f8e14af0c 100644 --- a/nima/webserver/webserver/pom.xml +++ b/nima/webserver/webserver/pom.xml @@ -16,7 +16,7 @@ --> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.helidon.nima.webserver @@ -64,12 +64,29 @@ io.helidon.nima.http.encoding helidon-nima-http-encoding + + io.helidon.pico.builder.config + helidon-pico-builder-config + io.helidon.common.features helidon-common-features-api provided true + + io.helidon.config + helidon-config-metadata + provided + true + + + + io.helidon.pico.builder.config + helidon-pico-builder-config-processor + provided + true + io.helidon.config helidon-config-yaml @@ -114,6 +131,16 @@ helidon-common-features-processor ${helidon.version} + + io.helidon.pico.builder.config + helidon-pico-builder-config-processor + ${helidon.version} + + + io.helidon.config + helidon-config-metadata-processor + ${helidon.version} + diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java index 522c98654f9..f1f6bc2b7a2 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionHandler.java @@ -29,6 +29,7 @@ import io.helidon.common.socket.SocketWriter; import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.spi.ServerConnection; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.TRACE; diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionProviders.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionProviders.java index 2de39d202ad..165c89bca4b 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionProviders.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ConnectionProviders.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Set; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; + /** * Connection provider candidates. */ diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java index 1575079d90a..90bd05a7e5e 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/LoomServer.java @@ -35,6 +35,7 @@ import io.helidon.common.Version; import io.helidon.common.context.Context; import io.helidon.nima.webserver.http.DirectHandlers; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; class LoomServer implements WebServer { private static final System.Logger LOGGER = System.getLogger(LoomServer.class.getName()); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java index e8fa2f5b553..f21b1930058 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerListener.java @@ -42,6 +42,7 @@ import io.helidon.common.socket.TlsSocket; import io.helidon.nima.common.tls.Tls; import io.helidon.nima.webserver.http.DirectHandlers; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.INFO; diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java index 97b6bc293f8..47cc9c41997 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/WebServer.java @@ -37,6 +37,7 @@ import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.http.HttpRouting; import io.helidon.nima.webserver.spi.ServerConnectionProvider; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; /** * Server that opens server sockets and handles requests through routing. @@ -389,6 +390,7 @@ public boolean hasSocket(String socketName) { /** * Configure the default {@link MediaContext}. * This method discards all previously registered MediaContext. + * * @param mediaContext media context * @return updated instance of the builder */ @@ -401,6 +403,7 @@ public Builder mediaContext(MediaContext mediaContext) { /** * Configure the default {@link ContentEncodingContext}. * This method discards all previously registered ContentEncodingContext. + * * @param contentEncodingContext content encoding context * @return updated instance of the builder */ @@ -412,6 +415,7 @@ public Builder contentEncodingContext(ContentEncodingContext contentEncodingCont /** * Configure the application scoped context to be used as a parent for webserver request contexts. + * * @param context top level context * @return an updated builder */ @@ -437,6 +441,7 @@ public Builder shutdownHook(boolean shutdownHook) { /** * Configure whether server threads should inherit inheritable thread locals. * Default value is {@code false}. + * * @param inheritThreadLocals whether to inherit thread locals * @return an updated builder */ @@ -468,6 +473,7 @@ ContentEncodingContext contentEncodingContext() { Map socketBuilders() { return socketBuilder; } + /** * Map of socket name to router. * @@ -482,10 +488,9 @@ Map routers() { List connectionProviders() { List providers = connectionProviders.build().asList(); // Send configuration nodes to providers - providers.forEach( - provider -> provider.configKeys().forEach( - key -> provider.config(providersConfig.get(key)))); - return providers.stream().map(ServerConnectionProvider::create).toList(); + return providers.stream() + .map(it -> it.create(providersConfig::get)) + .toList(); } private ListenerConfiguration.Builder socket(String socketName) { diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1BuilderInterceptor.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1BuilderInterceptor.java new file mode 100644 index 00000000000..54ef42df3d4 --- /dev/null +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1BuilderInterceptor.java @@ -0,0 +1,47 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.nima.webserver.http1; + +import io.helidon.builder.BuilderInterceptor; + +class Http1BuilderInterceptor implements BuilderInterceptor { + @Override + public DefaultHttp1Config.Builder intercept(DefaultHttp1Config.Builder target) { + receiveListeners(target); + sentListeners(target); + + return target; + } + + private void sentListeners(DefaultHttp1Config.Builder target) { + var listeners = target.sendListeners(); + if (listeners.isEmpty() && target.sendLog()) { + target.addReceiveListener(new Http1LoggingConnectionListener("send")); + } + listeners = target.sendListeners(); + target.compositeSendListener(Http1ConnectionListener.create(listeners)); + } + + private void receiveListeners(DefaultHttp1Config.Builder target) { + var listeners = target.receiveListeners(); + if (listeners.isEmpty() && target.receiveLog()) { + target.addReceiveListener(new Http1LoggingConnectionListener("recv")); + } + listeners = target.receiveListeners(); + target.compositeReceiveListener(Http1ConnectionListener.create(listeners)); + } +} diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java index 74fd8f528d1..67a8aa7fb03 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Config.java @@ -15,59 +15,34 @@ */ package io.helidon.nima.webserver.http1; -import java.util.LinkedList; import java.util.List; -import io.helidon.config.Config; +import io.helidon.builder.Builder; +import io.helidon.builder.Singular; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.pico.builder.config.ConfigBean; /** * HTTP/1.1 server configuration. */ -public class Http1Config { - - private static final int DEFAULT_MAX_PROLOGUE_LENGTH = 2048; - private static final int DEFAULT_MAX_HEADERS_SIZE = 16384; - private static final boolean DEFAULT_VALIDATE_HEADERS = true; - private static final boolean DEFAULT_VALIDATE_PATH = true; - - private final int maxPrologueLength; - private final int maxHeadersSize; - private final boolean validateHeaders; - private final boolean validatePath; - private final Http1ConnectionListener sendListeners; - private final Http1ConnectionListener recvListeners; - - private Http1Config(int maxPrologueLength, - int maxHeadersSize, - boolean validateHeaders, - boolean validatePath, - Http1ConnectionListener sendListeners, - Http1ConnectionListener recvListeners) { - this.maxPrologueLength = maxPrologueLength; - this.maxHeadersSize = maxHeadersSize; - this.validateHeaders = validateHeaders; - this.validatePath = validatePath; - this.sendListeners = sendListeners; - this.recvListeners = recvListeners; - } - +@Builder(interceptor = Http1BuilderInterceptor.class) +@ConfigBean(key = "server.connection-providers.http_1_1") +public interface Http1Config { /** * Maximal size of received HTTP prologue (GET /path HTTP/1.1). * * @return maximal size in bytes */ - public int maxPrologueLength() { - return maxPrologueLength; - } + @ConfiguredOption("2048") + int maxPrologueLength(); /** * Maximal size of received headers in bytes. * * @return maximal header size */ - public int maxHeadersSize() { - return maxHeadersSize; - } + @ConfiguredOption("16384") + int maxHeadersSize(); /** * Whether to validate headers. @@ -78,179 +53,62 @@ public int maxHeadersSize() { * * @return whether to validate headers */ - public boolean validateHeaders() { - return validateHeaders; - } + @ConfiguredOption("true") + boolean validateHeaders(); /** * If set to false, any path is accepted (even containing illegal characters). * * @return whether to validate path */ - public boolean validatePath() { - return validatePath; - } + @ConfiguredOption("true") + boolean validatePath(); - // Return builder with default values - static Builder builder() { - return new Builder(); - } + /** + * Logging of received packets. Uses trace and debug levels on logger of + * {@link io.helidon.nima.webserver.http1.Http1LoggingConnectionListener} with suffix of {@code .recv`}. + * + * @return {@code true} if logging should be enabled for received packets, {@code false} if no logging should be done + */ + @ConfiguredOption(key = "recv-log", value = "true") + boolean receiveLog(); - // Return builder with values initialized from existing configuration - static Builder builder(Http1Config config) { - return new Builder(config); - } + /** + * Logging of sent packets. Uses trace and debug levels on logger of + * {@link io.helidon.nima.webserver.http1.Http1LoggingConnectionListener} with suffix of {@code .send`}. + * + * @return {@code true} if logging should be enabled for sent packets, {@code false} if no logging should be done + */ + @ConfiguredOption(key = "send-log", value = "true") + boolean sendLog(); /** * Connection send event listeners for HTTP/1.1. * * @return send event listeners */ - public Http1ConnectionListener sendListeners() { - return sendListeners; - } + @Singular + List sendListeners(); /** * Connection receive event listeners for HTTP/1.1. * * @return receive event listeners */ - public Http1ConnectionListener recvListeners() { - return recvListeners; - } + @Singular + List receiveListeners(); /** - * HTTP/1.1 server configuration fluent API builder. + * A single send listener, this value is computed. + * + * @return send listener */ - public static class Builder implements io.helidon.common.Builder { - - private int maxPrologueLength; - private int maxHeaderSize; - private boolean validateHeaders; - private boolean validatePath; - private final List sendListeners; - private final List recvListeners; - - private Builder() { - this.maxPrologueLength = DEFAULT_MAX_PROLOGUE_LENGTH; - this.maxHeaderSize = DEFAULT_MAX_HEADERS_SIZE; - this.validateHeaders = DEFAULT_VALIDATE_HEADERS; - this.validatePath = DEFAULT_VALIDATE_PATH; - this.sendListeners = new LinkedList<>(); - this.recvListeners = new LinkedList<>(); - } - - private Builder(Http1Config config) { - this.maxPrologueLength = config.maxPrologueLength; - this.maxHeaderSize = config.maxHeadersSize; - this.validateHeaders = config.validateHeaders; - this.validatePath = config.validatePath; - this.sendListeners = Http1ConnectionListenerUtil.singleListenerToList(config.sendListeners); - this.recvListeners = Http1ConnectionListenerUtil.singleListenerToList(config.recvListeners); - } - - /** - * HTTP/1.1 connection provider configuration node. - * - * @param config configuration note to process - * @return updated builder - */ - public Builder config(Config config) { - config.get("max-prologue-length").asInt().ifPresent(value -> maxPrologueLength = value); - config.get("max-headers-size").asInt().ifPresent(value -> maxHeaderSize = value); - config.get("validate-headers").asBoolean().ifPresent(value -> validateHeaders = value); - config.get("validate-path").asBoolean().ifPresent(value -> validatePath = value); - if (config.get("recv-log").asBoolean().orElse(true)) { - addSendListener(new Http1LoggingConnectionListener("send")); - } - if (config.get("send-log").asBoolean().orElse(true)) { - addReceiveListener(new Http1LoggingConnectionListener("recv")); - } - return this; - } - - /** - * Maximal size of received HTTP prologue (GET /path HTTP/1.1). - * - * @param maxPrologueLength maximal size in bytes - * @return updated builder - */ - public Builder maxPrologueLength(int maxPrologueLength) { - this.maxPrologueLength = maxPrologueLength; - return this; - } - - /** - * Maximal size of received headers in bytes. - * - * @param maxHeaderSize maximal header size - * @return updated builder - */ - public Builder maxHeaderSize(int maxHeaderSize) { - this.maxHeaderSize = maxHeaderSize; - return this; - } - - /** - * Whether to validate headers. - * If set to false, any value is accepted, otherwise validates headers + known headers - * are validated by format - * (content length is always validated as it is part of protocol processing (other headers may be validated if - * features use them)). - * - * @param validateHeaders whether to validate headers - * @return updated builder - */ - public Builder validateHeaders(boolean validateHeaders) { - this.validateHeaders = validateHeaders; - return this; - } - - /** - * If set to false, any path is accepted (even containing illegal characters). - * - * @param validatePath whether to validate path - * @return updated builder - */ - public Builder validatePath(boolean validatePath) { - this.validatePath = validatePath; - return this; - } - - /** - * Add a send event listener. - * - * @param listener listener to add - * @return updated builder - */ - public Builder addSendListener(Http1ConnectionListener listener) { - sendListeners.add(listener); - return this; - } - - /** - * Add a receive event listener. - * - * @param listener listener to add - * @return updated builder - */ - public Builder addReceiveListener(Http1ConnectionListener listener) { - recvListeners.add(listener); - return this; - } - - @Override - public Http1Config build() { - return new Http1Config( - maxPrologueLength, - maxHeaderSize, - validateHeaders, - validatePath, - Http1ConnectionListener.create(sendListeners), - Http1ConnectionListener.create(recvListeners) - ); - } - - } + Http1ConnectionListener compositeSendListener(); + /** + * A single receive listener, this value is computed. + * + * @return receive listener + */ + Http1ConnectionListener compositeReceiveListener(); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java index e5fd52a1cdf..535a4ddb0e9 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Connection.java @@ -41,6 +41,7 @@ import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.http.DirectTransportRequest; import io.helidon.nima.webserver.http.HttpRouting; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.webserver.spi.ServerConnection; import static java.lang.System.Logger.Level.TRACE; @@ -53,7 +54,7 @@ public class Http1Connection implements ServerConnection { private static final System.Logger LOGGER = System.getLogger(Http1Connection.class.getName()); private final ConnectionContext ctx; - private final Http1Config config; + private final Http1Config http1Config; private final DataWriter writer; private final DataReader reader; private final Map upgradeProviderMap; @@ -64,6 +65,8 @@ public class Http1Connection implements ServerConnection { private final ContentEncodingContext contentEncodingContext = ContentEncodingContext.create(); private final HttpRouting routing; private final long maxPayloadSize; + private final Http1ConnectionListener recvListener; + private final Http1ConnectionListener sendListener; // overall connection private int requestId; @@ -74,21 +77,23 @@ public class Http1Connection implements ServerConnection { * Create a new connection. * * @param ctx connection context - * @param config connection provider configuration + * @param http1Config connection provider configuration * @param upgradeProviderMap map of upgrade providers (protocol id to provider) */ public Http1Connection(ConnectionContext ctx, - Http1Config config, + Http1Config http1Config, Map upgradeProviderMap) { this.ctx = ctx; this.writer = ctx.dataWriter(); this.reader = ctx.dataReader(); - this.config = config; + this.http1Config = http1Config; this.upgradeProviderMap = upgradeProviderMap; this.canUpgrade = !upgradeProviderMap.isEmpty(); - this.reader.listener(config.recvListeners(), ctx); - this.http1headers = new Http1Headers(reader, config.maxHeadersSize(), config.validateHeaders()); - this.http1prologue = new Http1Prologue(reader, config.maxPrologueLength(), config.validatePath()); + this.recvListener = http1Config.compositeReceiveListener(); + this.sendListener = http1Config.compositeSendListener(); + this.reader.listener(recvListener, ctx); + this.http1headers = new Http1Headers(reader, http1Config.maxHeadersSize(), http1Config.validateHeaders()); + this.http1prologue = new Http1Prologue(reader, http1Config.maxPrologueLength(), http1Config.validatePath()); this.routing = ctx.router().routing(HttpRouting.class, HttpRouting.empty()); this.maxPayloadSize = ctx.maxPayloadSize(); } @@ -100,12 +105,12 @@ public void handle() throws InterruptedException { while (true) { // prologue (first line of request) HttpPrologue prologue = http1prologue.readPrologue(); - config.recvListeners().prologue(ctx, prologue); + recvListener.prologue(ctx, prologue); currentEntitySize = 0; currentEntitySizeRead = 0; WritableHeaders headers = http1headers.readHeaders(prologue); - config.recvListeners().headers(ctx, headers); + recvListener.headers(ctx, headers); if (canUpgrade) { if (headers.contains(Http.Header.UPGRADE)) { @@ -232,7 +237,7 @@ private void route(HttpPrologue prologue, WritableHeaders headers) { if (entity == EntityStyle.NONE) { Http1ServerRequest request = Http1ServerRequest.create(ctx, routing.security(), prologue, headers, requestId); Http1ServerResponse response = new Http1ServerResponse(ctx, - config.sendListeners(), + sendListener, writer, request, !request.headers() @@ -287,7 +292,7 @@ private void route(HttpPrologue prologue, WritableHeaders headers) { entityReadLatch, () -> this.readEntityFromPipeline(prologue, headers)); Http1ServerResponse response = new Http1ServerResponse(ctx, - config.sendListeners(), + sendListener, writer, request, !request.headers() @@ -348,8 +353,8 @@ private void handleRequestException(RequestException e) { buffer.write(message); } - config.sendListeners().headers(ctx, headers); - config.sendListeners().data(ctx, buffer); + sendListener.headers(ctx, headers); + sendListener.data(ctx, buffer); writer.write(buffer); if (response.status() == Http.Status.INTERNAL_SERVER_ERROR_500) { @@ -359,7 +364,7 @@ private void handleRequestException(RequestException e) { // jUnit Http2Config pkg only visible test accessor. Http1Config config() { - return config; + return http1Config; } } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionProvider.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionProvider.java index 07f76446690..97a5a0e3536 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionProvider.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionProvider.java @@ -16,231 +16,143 @@ package io.helidon.nima.webserver.http1; -import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; +import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import io.helidon.common.HelidonServiceLoader; import io.helidon.config.Config; -import io.helidon.nima.webserver.ServerConnectionSelector; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.webserver.spi.ServerConnectionProvider; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; /** * {@link io.helidon.nima.webserver.spi.ServerConnectionProvider} implementation for HTTP/1.1 server connection provider. */ public class Http1ConnectionProvider implements ServerConnectionProvider { - /** HTTP/1.1 server connection provider configuration node name. */ + /** + * HTTP/1.1 server connection provider configuration node name. + */ private static final String CONFIG_NAME = "http_1_1"; - // Config key to HTTP/1.1 connection upgrade providers mapping - private final Map> providersConfigMap; - - private Http1Config config; + // all upgrade providers supported by HTTP/1.1 + private final List upgradeProviders; + private final Http1Config http1Config; - private Http1ConnectionProvider(Http1Config config, List providers) { - this.config = config; - this.providersConfigMap = initUpgradeProvidersConfigMap(providers); + private Http1ConnectionProvider(Builder builder) { + this.upgradeProviders = builder.upgradeProviders(); + this.http1Config = builder.http1Config(); } /** * Create a new instance with default configuration. + * Please use {@link #builder()} to customize this provider. * * @deprecated to be used solely by {@link java.util.ServiceLoader} */ @Deprecated public Http1ConnectionProvider() { - this.config = Http1Config.builder().build(); - this.providersConfigMap = initUpgradeProvidersConfigMap(upgradeProviderBuilder().build()); + this(builder()); } - - // Constructor helper: Build config key to HTTP/1.1 connection upgrading providers mapping - private static Map> initUpgradeProvidersConfigMap( - List factories) { - Map> map = new HashMap<>(factories.size()); - factories.forEach(factory -> addProviderConfigMapping(map, factory)); - return map; - } - - // Constructor helper: Add config key to HTTP/1.1 connection upgrading providers mapping into provided map - private static void addProviderConfigMapping(Map> map, Http1UpgradeProvider provider) { - List providers; - if (map.containsKey(provider.configKey())) { - providers = map.get(provider.configKey()); - } else { - providers = new LinkedList<>(); - map.put(provider.configKey(), providers); - } - providers.add(provider); + /** + * Builder to set up this provider. + * + * @return a new builder + */ + public static Builder builder() { + return new Builder(); } // Returns all config keys of Http1UpgradeProvider instances and this provider. // This method is called just once from WebServer.Builder so let's build the list on the fly. @Override public Iterable configKeys() { - List keys = new ArrayList<>(providersConfigMap.keySet().size() + 1); - keys.addAll(providersConfigMap.keySet()); - keys.add(CONFIG_NAME); - return keys; + Set result = new HashSet<>(); + result.add(CONFIG_NAME); + + result.addAll(upgradeProviders.stream() + .flatMap(it -> it.configKeys().stream()) + .collect(Collectors.toSet())); + + return result; } @Override - public void config(Config config) { - if (CONFIG_NAME.equals(config.name())) { - // Empty node can't overwrite existing local configuration. - if (config.exists()) { - // Initialize builder with existing configuration - this.config = Http1Config.builder(this.config) - // Overwrite values from config node - .config(config) - .build(); - } - // Send configuration nodes to HTTP/1.1 connection upgrading providers including possible empty nodes. + public ServerConnectionSelector create(Function configs) { + Http1Config config; + if (http1Config == null) { + config = DefaultHttp1Config.toBuilder(configs.apply(CONFIG_NAME)).build(); } else { - List providers = providersConfigMap.get(config.name()); - if (providers != null) { - providers.forEach(provider -> provider.config(config)); - } + config = http1Config; } - } - @Override - public ServerConnectionSelector create() { - // Calculate providers count to properly scale HashMap instance - int size = 0; - for (List providers : providersConfigMap.values()) { - size += providers.size(); - } - // Build HTTP/1.1 connection upgrade providers map. - Map selectors = new HashMap<>(size); - for (List providers : providersConfigMap.values()) { - for (Http1UpgradeProvider provider : providers) { - Http1Upgrader selector = provider.create(); - selectors.putIfAbsent(selector.supportedProtocol(), selector); - } + // now create an upgrader for each upgrade provider + var upgraderList = upgradeProviders.stream() + .map(it -> it.create(configs)) + .toList(); + + var upgraders = new HashMap(); + for (Http1Upgrader http1Upgrader : upgraderList) { + // use put if absent, so when we have more than one upgraded for the same protocol, we use the one with higher weight + upgraders.putIfAbsent(http1Upgrader.supportedProtocol(), http1Upgrader); } - // Map passed to connection provider instance must be immutable - return new Http1ConnectionSelector(config, Map.copyOf(selectors)); - } - /** - * Builder to set up this provider. - * - * @return a new builder - */ - public static Builder builder() { - return new Builder(); + return new Http1ConnectionSelector(config, upgraders); } /** * Fluent API builder for {@link Http1ConnectionProvider}. */ public static class Builder implements io.helidon.common.Builder { - - private final Http1Config.Builder configBuilder; - private final UpgradeProviderBuilder upgradeProviderBuilder; + private final HelidonServiceLoader.Builder upgradeProviderServices = HelidonServiceLoader.builder( + ServiceLoader.load(Http1UpgradeProvider.class)); + private Http1Config http1Config; private Builder() { - configBuilder = Http1Config.builder(); - upgradeProviderBuilder = upgradeProviderBuilder(); - } - - /** - * Maximal size of received HTTP prologue (GET /path HTTP/1.1). - * - * @param maxPrologueLength maximal size in bytes - * @return updated builder - */ - public Builder maxPrologueLength(int maxPrologueLength) { - configBuilder.maxPrologueLength(maxPrologueLength); - return this; } - /** - * Maximal size of received headers in bytes. - * - * @param maxHeadersSize maximal header size - * @return updated builder - */ - public Builder maxHeadersSize(int maxHeadersSize) { - configBuilder.maxHeaderSize(maxHeadersSize); - return this; - } - - /** - * Whether to validate headers. - * If set to false, any value is accepted, otherwise validates headers + known headers - * are validated by format - * (content length is always validated as it is part of protocol processing (other headers may be validated if - * features use them)). - * - * @param validateHeaders whether to validate headers - * @return updated builder - */ - public Builder validateHeaders(boolean validateHeaders) { - configBuilder.validateHeaders(validateHeaders); - return this; + @Override + public Http1ConnectionProvider build() { + return new Http1ConnectionProvider(this); } /** - * If set to false, any path is accepted (even containing illegal characters). + * Custom configuration of HTTP/1 connection provider. + * If not defined, it will be configured from config, or defaults would be used. * - * @param validatePath whether to validate path + * @param http1Config HTTP/1 configuration * @return updated builder */ - public Builder validatePath(boolean validatePath) { - configBuilder.validatePath(validatePath); + public Builder http1Config(Http1Config http1Config) { + this.http1Config = http1Config; return this; } /** * Add a configured upgrade provider. This will replace the instance discovered through service loader (if one exists). * - * @param provider add a provider + * @param provider provider to add * @return updated builder */ public Builder addUpgradeProvider(Http1UpgradeProvider provider) { - upgradeProviderBuilder.addUpgradeProvider(provider); + upgradeProviderServices.addService(provider); return this; } - @Override - public Http1ConnectionProvider build() { - return new Http1ConnectionProvider(configBuilder.build(), upgradeFactories()); + private List upgradeProviders() { + return upgradeProviderServices.build().asList(); } - private List upgradeFactories() { - return upgradeProviderBuilder.build(); + // may be null + private Http1Config http1Config() { + return http1Config; } - - } - - private static UpgradeProviderBuilder upgradeProviderBuilder() { - return new UpgradeProviderBuilder(); } - - static class UpgradeProviderBuilder { - - private final HelidonServiceLoader.Builder builder; - - private UpgradeProviderBuilder() { - builder = HelidonServiceLoader.builder(ServiceLoader.load(Http1UpgradeProvider.class)); - } - - UpgradeProviderBuilder addUpgradeProvider(Http1UpgradeProvider provider) { - builder.addService(provider); - return this; - } - - List build() { - return builder.build().asList(); - } - - } - } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionSelector.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionSelector.java index a49c7f30ff6..a3c6d20bc77 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionSelector.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1ConnectionSelector.java @@ -22,8 +22,9 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.Bytes; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.webserver.spi.ServerConnection; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; /** * HTTP/1.1 server connection selector. @@ -32,8 +33,8 @@ public class Http1ConnectionSelector implements ServerConnectionSelector { private static final String PROTOCOL = " HTTP/1.1\r"; // HTTP/1.1 connection upgrade providers - private final Map upgradeProviderMap; private final Http1Config config; + private final Map upgradeProviderMap; // Creates an instance of HTTP/1.1 server connection selector. Http1ConnectionSelector(Http1Config config, Map upgradeProviderMap) { diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1UpgradeProvider.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1UpgradeProvider.java index 7f60edab8ce..633fdadba25 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1UpgradeProvider.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1UpgradeProvider.java @@ -16,12 +16,14 @@ package io.helidon.nima.webserver.http1.spi; +import java.util.Set; +import java.util.function.Function; + import io.helidon.config.Config; -import io.helidon.nima.webserver.http1.Http1Upgrader; /** * {@link java.util.ServiceLoader} provider interface for HTTP/1.1 connection upgrade provider. - * This interface serves as {@link io.helidon.nima.webserver.http1.Http1Upgrader} builder + * This interface serves as {@link Http1Upgrader} builder * which receives requested configuration nodes from the server configuration when server builder * is running. */ @@ -32,22 +34,14 @@ public interface Http1UpgradeProvider { * * @return name of the node to request */ - String configKey(); - - /** - * Provider's configuration reader. - * Node with {@code configKey()} name will be provided. May receive empty config - * when node with specified key is missing. - * - * @param config {@link io.helidon.config.Config} configuration node - */ - void config(Config config); + Set configKeys(); /** - * Creates an instance of server HTTP/1.1 connection upgrade selector. + * Creates an instance of HTTP/HTTP/1.1 connection upgrader. * + * @param config {@link io.helidon.config.Config} configuration function that provides a value for any {@link #configKeys()} * @return new server HTTP/1.1 connection upgrade selector */ - Http1Upgrader create(); + Http1Upgrader create(Function config); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Upgrader.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1Upgrader.java similarity index 88% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Upgrader.java rename to nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1Upgrader.java index 2ff3ac760e6..3f8dda3c4d5 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/Http1Upgrader.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/http1/spi/Http1Upgrader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.helidon.nima.webserver.http1; +package io.helidon.nima.webserver.http1.spi; import io.helidon.common.http.HttpPrologue; import io.helidon.common.http.WritableHeaders; @@ -22,7 +22,7 @@ import io.helidon.nima.webserver.spi.ServerConnection; /** - * HTTP/1.1 connection upgrade. + * HTTP/1.1 connection upgrader. */ public interface Http1Upgrader { /** @@ -39,7 +39,8 @@ public interface Http1Upgrader { * @param ctx connection context * @param prologue http prologue of the upgrade request * @param headers http headers of the upgrade request - * @return a new connection to use instead of the original {@link io.helidon.nima.webserver.http1.Http1Connection} + * @return a new connection to use instead of the original {@link io.helidon.nima.webserver.http1.Http1Connection}, + * or {@code null} if the connection cannot be upgraded */ ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, WritableHeaders headers); diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnection.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnection.java index 6b84d047b02..e3d8dca3811 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnection.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnection.java @@ -22,7 +22,7 @@ public interface ServerConnection { /** * Start handling the connection. Data is provided through - * {@link io.helidon.nima.webserver.ServerConnectionSelector#connection(io.helidon.nima.webserver.ConnectionContext)}. + * {@link ServerConnectionSelector#connection(io.helidon.nima.webserver.ConnectionContext)}. * * @throws InterruptedException to interrupt any waiting state and terminate this connection */ diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionProvider.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionProvider.java index d24d9f7d9e8..f5c507cff99 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionProvider.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionProvider.java @@ -16,17 +16,17 @@ package io.helidon.nima.webserver.spi; +import java.util.function.Function; + import io.helidon.config.Config; -import io.helidon.nima.webserver.ServerConnectionSelector; /** * {@link java.util.ServiceLoader} provider interface for server connection providers. - * This interface serves as {@link io.helidon.nima.webserver.ServerConnectionSelector} builder + * This interface serves as {@link ServerConnectionSelector} builder * which receives requested configuration nodes from the server configuration when server builder * is running. */ public interface ServerConnectionProvider { - /** * Provider's specific configuration nodes names. * @@ -34,20 +34,13 @@ public interface ServerConnectionProvider { */ Iterable configKeys(); - /** - * Provider's configuration reader. - * Node with {@code configKey()} name will be provided. May receive empty config - * when node with specified key is missing. - * - * @param config {@link io.helidon.config.Config} configuration node - */ - void config(Config config); - /** * Creates an instance of server connection selector. * + * @param configs configuration for each {@link #configKeys()}, the config may be empty, but it will be present + * for each value * @return new server connection selector */ - ServerConnectionSelector create(); + ServerConnectionSelector create(Function configs); } diff --git a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConnectionSelector.java b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionSelector.java similarity index 96% rename from nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConnectionSelector.java rename to nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionSelector.java index c7906f4f9d0..b8fcee9874e 100644 --- a/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/ServerConnectionSelector.java +++ b/nima/webserver/webserver/src/main/java/io/helidon/nima/webserver/spi/ServerConnectionSelector.java @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.helidon.nima.webserver; +package io.helidon.nima.webserver.spi; import java.util.Set; import io.helidon.common.buffers.BufferData; -import io.helidon.nima.webserver.spi.ServerConnection; +import io.helidon.nima.webserver.ConnectionContext; /** * Connection selector is given a chance to analyze request bytes and decide whether this is a connection it can accept. diff --git a/nima/webserver/webserver/src/main/java/module-info.java b/nima/webserver/webserver/src/main/java/module-info.java index a399f12bbc8..b6f6bcccfb3 100644 --- a/nima/webserver/webserver/src/main/java/module-info.java +++ b/nima/webserver/webserver/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 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. @@ -37,12 +37,16 @@ requires transitive io.helidon.common.context; requires transitive io.helidon.common.security; requires io.helidon.logging.common; + requires io.helidon.builder; + requires io.helidon.pico.builder.config; requires java.management; requires jakarta.annotation; requires io.helidon.common.uri; + requires static io.helidon.common.features.api; + requires static io.helidon.config.metadata; // provides multiple packages due to intentional cyclic dependency // we want to support HTTP/1.1 by default (we could fully separate it, but the API would be harder to use diff --git a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java index 5e8ba4bfa39..63fee771eb3 100644 --- a/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java +++ b/nima/webserver/webserver/src/test/java/io/helidon/nima/webserver/http1/ConnectionConfigTest.java @@ -30,7 +30,7 @@ import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; import io.helidon.nima.webserver.Routing; -import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import io.helidon.nima.webserver.ServerContext; import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.DirectHandlers; diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java index 14c9a54d30b..2d9ef599afb 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgradeProvider.java @@ -18,18 +18,27 @@ import java.util.HashSet; import java.util.Set; +import java.util.function.Function; import io.helidon.config.Config; -import io.helidon.nima.webserver.http1.Http1Upgrader; import io.helidon.nima.webserver.http1.spi.Http1UpgradeProvider; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; /** * {@link java.util.ServiceLoader} provider implementation for upgrade from HTTP/1.1 to WebSocket. */ public class WsUpgradeProvider implements Http1UpgradeProvider { + /** + * HTTP/2 server connection provider configuration node name. + */ + protected static final String CONFIG_NAME = "websocket"; private final Set origins; + protected WsUpgradeProvider(AbstractBuilder builder) { + this.origins = Set.copyOf(builder.origins()); + } + /** * Create a new instance with default configuration. * @@ -37,47 +46,44 @@ public class WsUpgradeProvider implements Http1UpgradeProvider { */ @Deprecated() public WsUpgradeProvider() { - this(new HashSet<>()); + this(builder()); } - protected WsUpgradeProvider(Set origins) { - this.origins = origins; + /** + * New builder. + * + * @return builder + */ + public static Builder builder() { + return new Builder(); } - /** HTTP/2 server connection provider configuration node name. */ - private static final String CONFIG_NAME = "websocket"; - @Override - public String configKey() { - return CONFIG_NAME; + public Set configKeys() { + return Set.of(CONFIG_NAME); } @Override - public void config(Config config) { - // Accept origins as list of String values from config file - config.get("origins") - .asList(String.class) - .ifPresent(origins::addAll); - } + public Http1Upgrader create(Function config) { + Set usedOrigins; + + if (origins.isEmpty()) { + usedOrigins = config.apply(CONFIG_NAME) + .get("origins") + .asList(String.class) + .map(Set::copyOf) + .orElseGet(Set::of); + } else { + usedOrigins = origins; + } - @Override - public Http1Upgrader create() { - return new WsUpgrader(Set.copyOf(origins)); + return new WsUpgrader(usedOrigins); } protected Set origins() { return origins; } - /** - * New builder. - * - * @return builder - */ - public static Builder builder() { - return new Builder(); - } - /** * Abstract Fluent API builder for {@link WsUpgradeProvider} and child classes. * @@ -119,9 +125,8 @@ private Builder() { @Override public WsUpgradeProvider build() { - return new WsUpgradeProvider(origins()); + return new WsUpgradeProvider(this); } } - } diff --git a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java index eb29fcc3bbb..362d7fe3f9a 100644 --- a/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java +++ b/nima/websocket/webserver/src/main/java/io/helidon/nima/websocket/webserver/WsUpgrader.java @@ -31,17 +31,18 @@ import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http.HeaderName; import io.helidon.common.http.HttpPrologue; +import io.helidon.common.http.NotFoundException; import io.helidon.common.http.RequestException; import io.helidon.common.http.WritableHeaders; import io.helidon.nima.webserver.ConnectionContext; -import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import io.helidon.nima.webserver.spi.ServerConnection; import io.helidon.nima.websocket.WsUpgradeException; import static java.nio.charset.StandardCharsets.US_ASCII; /** - * {@link java.util.ServiceLoader} provider implementation for upgrade from HTTP/1.1 to WebSocket. + * {@link io.helidon.nima.webserver.http1.spi.Http1Upgrader} implementation to upgrade from HTTP/1.1 to WebSocket. */ public class WsUpgrader implements Http1Upgrader { @@ -94,6 +95,7 @@ public class WsUpgrader implements Http1Upgrader { private static final Base64.Decoder B64_DECODER = Base64.getDecoder(); private static final Base64.Encoder B64_ENCODER = Base64.getEncoder(); private static final byte[] HEADERS_SEPARATOR = "\r\n".getBytes(US_ASCII); + static final Headers EMPTY_HEADERS = WritableHeaders.create(); private final Set origins; private final boolean anyOrigin; @@ -133,10 +135,12 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, Wr .build(); } - WebSocket route = ctx.router().routing(WebSocketRouting.class, WebSocketRouting.empty()) - .findRoute(prologue); + WsRoute route; - if (route == null) { + try { + route = ctx.router().routing(WsRouting.class, WsRouting.empty()) + .findRoute(prologue); + } catch (NotFoundException e) { return null; } @@ -178,7 +182,7 @@ public ServerConnection upgrade(ConnectionContext ctx, HttpPrologue prologue, Wr LOGGER.log(Level.TRACE, "Upgraded to websocket version " + version); } - return new WsConnection(ctx, prologue, headers, upgradeHeaders.orElse(null), wsKey, route); + return WsConnection.create(ctx, prologue, upgradeHeaders.orElse(EMPTY_HEADERS), wsKey, route); } protected boolean anyOrigin() { diff --git a/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java b/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java index f6e5df3c4bd..06447de699a 100644 --- a/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java +++ b/nima/websocket/webserver/src/test/java/io/helidon/nima/websocket/webserver/WsUpgradeProviderConfigTest.java @@ -31,13 +31,13 @@ import io.helidon.nima.webserver.ConnectionContext; import io.helidon.nima.webserver.Router; import io.helidon.nima.webserver.Routing; -import io.helidon.nima.webserver.ServerConnectionSelector; +import io.helidon.nima.webserver.spi.ServerConnectionSelector; import io.helidon.nima.webserver.ServerContext; import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.DirectHandlers; import io.helidon.nima.webserver.http1.Http1Connection; import io.helidon.nima.webserver.http1.Http1ConnectionSelector; -import io.helidon.nima.webserver.http1.Http1Upgrader; +import io.helidon.nima.webserver.http1.spi.Http1Upgrader; import org.junit.jupiter.api.Test; @@ -171,7 +171,7 @@ void testUpgraderConfigBuilder() { .addOrigin("bOrigin1") .addOrigin("bOrigin2") .build() - .create(); + .create(it -> Config.empty()); Set origins = upgrader.origins(); assertThat(origins, containsInAnyOrder("bOrigin1", "bOrigin2"));