From 85f52029338363ae6a5c1946e8859a4f43968204 Mon Sep 17 00:00:00 2001 From: Santiago Pericas-Geertsen Date: Wed, 20 Sep 2023 14:58:28 -0400 Subject: [PATCH] [4.x] Modifies Helidon Connector to use WebClient and also support HTTP/2 (#7582) * Modifies Helidon connector to use WebClient and thus support HTTP/1.1 and HTTP/2. Fixes problem in http2 module-info.java. Updates tests to verify HTTP/2 using ALPN and prior knowledge. * - New test for protocol upgrade without TLS - Fixes problem in connector when accessing request properties - Renames test classes for clarity * Cleans up support for Tls and SSLContext. * Fixes import after merge. Signed-off-by: Santiago Pericasgeertsen * Fixes problems with timeouts. Signed-off-by: Santiago Pericasgeertsen * Fixes checkstyle problems. Signed-off-by: Santiago Pericasgeertsen * Enables headers test. Signed-off-by: Santiago Pericasgeertsen * Turns off connection cache but allows enabling this with newly defined property. Minor cleanup in pom file. * Adds a create() method to HelidonConnectorProvider. Updates Javadocs. * Close WebClient's response when Jersey's response is closed. Signed-off-by: Santiago Pericasgeertsen --------- Signed-off-by: Santiago Pericasgeertsen --- jersey/connector/pom.xml | 4 + .../jersey/connector/HelidonConnector.java | 100 ++++++++++++------ .../connector/HelidonConnectorProvider.java | 47 ++++---- .../jersey/connector/HelidonProperties.java | 55 +++++++++- .../connector/src/main/java/module-info.java | 1 + .../helidon/jersey/connector/ConfigTest.java | 8 +- jersey/tests/connector/pom.xml | 5 + ...yConnectorTest.java => ConnectorBase.java} | 57 +++++----- .../jersey/connector/ConnectorHttp1Test.java | 37 +++++++ .../connector/ConnectorHttp2AlpnTest.java | 71 +++++++++++++ .../connector/ConnectorHttp2PriorTest.java | 75 +++++++++++++ .../connector/ConnectorHttp2UpgradeTest.java | 50 +++++++++ .../src/test/resources/certificate.p12 | Bin 0 -> 2568 bytes .../http2/src/main/java/module-info.java | 2 + 14 files changed, 431 insertions(+), 81 deletions(-) rename jersey/tests/connector/src/test/java/io/helidon/jersey/connector/{JerseyConnectorTest.java => ConnectorBase.java} (76%) create mode 100644 jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp1Test.java create mode 100644 jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2AlpnTest.java create mode 100644 jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2PriorTest.java create mode 100644 jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2UpgradeTest.java create mode 100644 jersey/tests/connector/src/test/resources/certificate.p12 diff --git a/jersey/connector/pom.xml b/jersey/connector/pom.xml index d56c2e689c3..233a59e1ee0 100644 --- a/jersey/connector/pom.xml +++ b/jersey/connector/pom.xml @@ -45,6 +45,10 @@ io.helidon.webclient helidon-webclient + + io.helidon.webclient + helidon-webclient-http2 + org.junit.jupiter junit-jupiter-api diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java index 0f1b710f29d..062367a642d 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnector.java @@ -18,6 +18,7 @@ import java.net.URI; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -25,8 +26,6 @@ import java.util.concurrent.Future; import java.util.logging.Logger; -import javax.net.ssl.SSLContext; - import io.helidon.common.LazyValue; import io.helidon.common.Version; import io.helidon.common.tls.Tls; @@ -36,10 +35,12 @@ import io.helidon.http.HeaderNames; import io.helidon.http.Method; import io.helidon.http.media.ReadableEntity; +import io.helidon.webclient.api.HttpClientRequest; +import io.helidon.webclient.api.HttpClientResponse; import io.helidon.webclient.api.Proxy; -import io.helidon.webclient.http1.Http1Client; -import io.helidon.webclient.http1.Http1ClientRequest; -import io.helidon.webclient.http1.Http1ClientResponse; +import io.helidon.webclient.api.WebClient; +import io.helidon.webclient.api.WebClientConfig; +import io.helidon.webclient.spi.ProtocolConfig; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; @@ -50,6 +51,11 @@ import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.util.PropertiesHelper; +import static io.helidon.jersey.connector.HelidonProperties.DEFAULT_HEADERS; +import static io.helidon.jersey.connector.HelidonProperties.PROTOCOL_CONFIGS; +import static io.helidon.jersey.connector.HelidonProperties.PROTOCOL_ID; +import static io.helidon.jersey.connector.HelidonProperties.SHARE_CONNECTION_CACHE; +import static io.helidon.jersey.connector.HelidonProperties.TLS; import static org.glassfish.jersey.client.ClientProperties.CONNECT_TIMEOUT; import static org.glassfish.jersey.client.ClientProperties.FOLLOW_REDIRECTS; import static org.glassfish.jersey.client.ClientProperties.READ_TIMEOUT; @@ -59,6 +65,7 @@ class HelidonConnector implements Connector { static final Logger LOGGER = Logger.getLogger(HelidonConnector.class.getName()); private static final int DEFAULT_TIMEOUT = 10000; + private static final Map EMPTY_MAP_LIST = Map.of("", ""); private static final String HELIDON_VERSION = "Helidon/" + Version.VERSION + " (java " + PropertiesHelper.getSystemProperty("java.runtime.version") + ")"; @@ -67,16 +74,13 @@ class HelidonConnector implements Connector { LazyValue.create(() -> Executors.newThreadPerTaskExecutor( Thread.ofVirtual().name("helidon-connector-", 0).factory())); - private final Client client; - private final Http1Client httpClient; - private Proxy proxy; + private final WebClient webClient; + private final Proxy proxy; HelidonConnector(Client client, Configuration config) { - this.client = client; - // create underlying HTTP client Map properties = config.getProperties(); - var builder = Http1Client.builder(); + var builder = WebClientConfig.builder(); // use config for client builder.config(helidonConfig(config).orElse(Config.empty())); @@ -94,22 +98,49 @@ class HelidonConnector implements Connector { if (properties.containsKey(FOLLOW_REDIRECTS)) { builder.followRedirects(getValue(properties, FOLLOW_REDIRECTS, true)); } - httpClient = builder.build(); + + // prefer Tls over SSLContext + if (properties.containsKey(TLS)) { + builder.tls(getValue(properties, TLS, Tls.class)); + } else if (client.getSslContext() != null) { + builder.tls(Tls.builder().sslContext(client.getSslContext()).build()); + } + + // protocol configs + if (properties.containsKey(PROTOCOL_CONFIGS)) { + List protocolConfigs = + (List) properties.get(PROTOCOL_CONFIGS); + if (protocolConfigs != null) { + builder.addProtocolConfigs(protocolConfigs); + } + } + + // default headers + if (properties.containsKey(DEFAULT_HEADERS)) { + builder.defaultHeadersMap(getValue(properties, DEFAULT_HEADERS, EMPTY_MAP_LIST)); + } + + // connection sharing defaults to false in this connector + if (properties.containsKey(SHARE_CONNECTION_CACHE)) { + builder.shareConnectionCache(getValue(properties, SHARE_CONNECTION_CACHE, false)); + } + + webClient = builder.build(); } /** - * Map a Jersey request to a Helidon HTTP/1.1 request. + * Map a Jersey request to a Helidon HTTP request. * * @param request the request to map * @return the mapped request */ - private Http1ClientRequest mapRequest(ClientRequest request) { + private HttpClientRequest mapRequest(ClientRequest request) { // possibly override proxy in request Proxy requestProxy = ProxyBuilder.createProxy(request).orElse(proxy); // create WebClient request URI uri = request.getUri(); - Http1ClientRequest httpRequest = httpClient + HttpClientRequest httpRequest = webClient .method(Method.create(request.getMethod())) .proxy(requestProxy) .uri(uri); @@ -131,16 +162,18 @@ private Http1ClientRequest mapRequest(ClientRequest request) { httpRequest.header(HeaderNames.create(key), values); }); - // SSL context - SSLContext sslContext = client.getSslContext(); - httpRequest.tls(Tls.builder().sslContext(sslContext).build()); - // request config - if (request.hasProperty(FOLLOW_REDIRECTS)) { - httpRequest.followRedirects(request.resolveProperty(FOLLOW_REDIRECTS, true)); + Boolean followRedirects = request.resolveProperty(FOLLOW_REDIRECTS, Boolean.class); + if (followRedirects != null) { + httpRequest.followRedirects(followRedirects); + } + Integer readTimeout = request.resolveProperty(READ_TIMEOUT, Integer.class); + if (readTimeout != null) { + httpRequest.readTimeout(Duration.ofMillis(readTimeout)); } - if (request.hasProperty(READ_TIMEOUT)) { - httpRequest.readTimeout(Duration.ofMillis(request.resolveProperty(READ_TIMEOUT, DEFAULT_TIMEOUT))); + String protocolId = request.resolveProperty(PROTOCOL_ID, String.class); + if (protocolId != null) { + httpRequest.protocolId(protocolId); } // copy properties @@ -167,8 +200,8 @@ private Http1ClientRequest mapRequest(ClientRequest request) { * @param request the request * @return the mapped response */ - private ClientResponse mapResponse(Http1ClientResponse httpResponse, ClientRequest request) { - ClientResponse response = new ClientResponse(new Response.StatusType() { + private ClientResponse mapResponse(HttpClientResponse httpResponse, ClientRequest request) { + Response.StatusType statusType = new Response.StatusType() { @Override public int getStatusCode() { return httpResponse.status().code(); @@ -183,7 +216,14 @@ public Response.Status.Family getFamily() { public String getReasonPhrase() { return httpResponse.status().reasonPhrase(); } - }, request); + }; + ClientResponse response = new ClientResponse(statusType, request) { + @Override + public void close() { + super.close(); + httpResponse.close(); // closes WebClient's response + } + }; // copy headers for (Header header : httpResponse.headers()) { @@ -211,8 +251,8 @@ public String getReasonPhrase() { */ @Override public ClientResponse apply(ClientRequest request) { - Http1ClientResponse httpResponse; - Http1ClientRequest httpRequest = mapRequest(request); + HttpClientResponse httpResponse; + HttpClientRequest httpRequest = mapRequest(request); if (request.hasEntity()) { httpResponse = httpRequest.outputStream(os -> { @@ -253,8 +293,8 @@ public String getName() { public void close() { } - Http1Client client() { - return httpClient; + WebClient client() { + return webClient; } Proxy proxy() { diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnectorProvider.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnectorProvider.java index 03774491f8b..91447ca0900 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnectorProvider.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonConnectorProvider.java @@ -16,24 +16,31 @@ package io.helidon.jersey.connector; -import java.io.OutputStream; - import jakarta.ws.rs.client.Client; import jakarta.ws.rs.core.Configuration; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.client.spi.ConnectorProvider; /** - * Provider for Helidon WebClient {@link Connector} that utilizes the Helidon HTTP Client to send and receive - * HTTP request and responses. + * A Jersey {@link ConnectorProvider} that uses a {@link io.helidon.webclient.api.WebClient} + * instance to executed HTTP requests on behalf of a Jakarta REST {@link Client}. + *

+ * An instance of this class can be specified during the creation of a {@link Client} + * using the method {@link org.glassfish.jersey.client.ClientConfig#connectorProvider}. + * It is recommended to use the static method {@link #create()} for obtain an + * instance of this class. + *

+ * Configuration of a connector is driven by properties set on a {@link Client} + * instance, including possibly a config tree. There is a combination of Jersey + * and Helidon properties that can be specified for that purpose. Jersey properties + * are defined in class {@link org.glassfish.jersey.client.ClientProperties} and Helidon + * properties are defined in {@link HelidonProperties}. *

- * The following properties are only supported at construction of this class: + * Only the following properties from {@link org.glassfish.jersey.client.ClientProperties} + * are supported: *

    *
  • {@link org.glassfish.jersey.client.ClientProperties#CONNECT_TIMEOUT}
  • *
  • {@link org.glassfish.jersey.client.ClientProperties#FOLLOW_REDIRECTS}
  • - *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_URI}
  • - *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_USERNAME}
  • - *
  • {@link org.glassfish.jersey.client.ClientProperties#PROXY_PASSWORD}
  • *
  • {@link org.glassfish.jersey.client.ClientProperties#READ_TIMEOUT}
  • *
*

@@ -41,24 +48,13 @@ * entity is not read from the response then * {@link org.glassfish.jersey.client.ClientResponse#close()} MUST be called * after processing the response to release connection-based resources. - *

*

- * Client operations are thread safe, the HTTP connection may - * be shared between different threads. - *

+ * Client operations are thread safe, the HTTP connection may be shared between + * different threads. *

* If a response entity is obtained that is an instance of {@link java.io.Closeable} * then the instance MUST be closed after processing the entity to release * connection-based resources. - *

- *

- * This connector uses {@link org.glassfish.jersey.client.ClientProperties#OUTBOUND_CONTENT_LENGTH_BUFFER} to buffer the entity - * written for instance by {@link jakarta.ws.rs.core.StreamingOutput}. Should the buffer be small and - * {@link jakarta.ws.rs.core.StreamingOutput#write(OutputStream)} be called many times, the performance can drop. The Content-Length - * or the Content_Encoding header is set by the underlaying Helidon WebClient regardless of the - * {@link org.glassfish.jersey.client.ClientProperties#OUTBOUND_CONTENT_LENGTH_BUFFER} size, however. - *

- * */ public class HelidonConnectorProvider implements ConnectorProvider { /** @@ -71,4 +67,13 @@ public HelidonConnectorProvider() { public Connector getConnector(Client client, Configuration runtimeConfig) { return new HelidonConnector(client, runtimeConfig); } + + /** + * Create a new instance of {@link HelidonConnectorProvider}. + * + * @return new instance of this class + */ + public static HelidonConnectorProvider create() { + return new HelidonConnectorProvider(); + } } diff --git a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonProperties.java b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonProperties.java index dd64eee86e9..afd5cafd10e 100644 --- a/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonProperties.java +++ b/jersey/connector/src/main/java/io/helidon/jersey/connector/HelidonProperties.java @@ -16,7 +16,12 @@ package io.helidon.jersey.connector; +import java.util.List; +import java.util.Map; + +import io.helidon.common.tls.Tls; import io.helidon.config.Config; +import io.helidon.webclient.api.WebClient; /** * Configuration options specific to the Client API that utilizes {@link HelidonConnector}. @@ -27,8 +32,56 @@ private HelidonProperties() { } /** - * A Helidon {@link Config} instance used to create the corresponding {@link io.helidon.webclient.api.WebClient}. + * Property name to set a {@link Config} instance to by used by underlying {@link WebClient}. * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}. + * + * @see io.helidon.webclient.api.WebClientConfig.Builder#config(io.helidon.common.config.Config) */ public static final String CONFIG = "jersey.connector.helidon.config"; + + /** + * Property name to set a {@link Tls} instance to be used by underlying {@link WebClient}. + * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}. + * + * @see io.helidon.webclient.api.WebClientConfig.Builder#tls(Tls) + */ + public static final String TLS = "jersey.connector.helidon.tls"; + + /** + * Property name to set a {@code List} instance with a list of + * protocol configs to be used by underlying {@link WebClient}. + * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}. + * + * @see io.helidon.webclient.api.WebClientConfig.Builder#protocolConfigs(List) + */ + public static final String PROTOCOL_CONFIGS = "jersey.connector.helidon.protocolConfigs"; + + /** + * Property name to set a {@code Map} instance with a list of + * default headers to be used by underlying {@link WebClient}. + * This property is settable on {@link jakarta.ws.rs.core.Configurable#property(String, Object)}. + * + * @see io.helidon.webclient.api.WebClientConfig.Builder#defaultHeadersMap(Map) + */ + public static final String DEFAULT_HEADERS = "jersey.connector.helidon.defaultHeaders"; + + /** + * Property name to set a protocol ID for each request. You can use this property + * to request an HTTP/2 upgrade from HTTP/1.1 by setting its value to {@code "h2"}. + * When using TLS, Helidon uses negotiation via the ALPN extension instead of this + * property. + * + * @see io.helidon.webclient.api.HttpClientRequest#protocolId(String) + */ + public static final String PROTOCOL_ID = "jersey.connector.helidon.protocolId"; + + /** + * Property name to enable or disable connection caching in the underlying {@link WebClient}. + * The default for the Helidon connector is {@code false}, or no sharing (which is the + * opposite of {@link WebClient}). Set this property to {@code true} to enable connection + * caching. + * + * @see io.helidon.webclient.api.WebClientConfig.Builder#shareConnectionCache(boolean) + */ + public static final String SHARE_CONNECTION_CACHE = "jersey.connector.helidon.shareConnectionCache"; } diff --git a/jersey/connector/src/main/java/module-info.java b/jersey/connector/src/main/java/module-info.java index 16920e66084..bf224a1f102 100644 --- a/jersey/connector/src/main/java/module-info.java +++ b/jersey/connector/src/main/java/module-info.java @@ -24,6 +24,7 @@ requires io.helidon.config; requires io.helidon.webclient; + requires io.helidon.webclient.http2; requires jakarta.ws.rs; requires java.logging; requires jersey.common; diff --git a/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java b/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java index 5f66080e26a..56701f80136 100644 --- a/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java +++ b/jersey/connector/src/test/java/io/helidon/jersey/connector/ConfigTest.java @@ -20,9 +20,9 @@ import io.helidon.config.Config; import io.helidon.config.ConfigSources; -import io.helidon.webclient.api.Proxy; -import io.helidon.webclient.http1.Http1ClientRequest; +import io.helidon.webclient.api.HttpClientRequest; +import io.helidon.webclient.api.Proxy; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import org.glassfish.jersey.client.ClientProperties; @@ -55,7 +55,7 @@ void testConfig() { .property(HelidonProperties.CONFIG, config.get("client")) .build(); HelidonConnector connector = new HelidonConnector(client, client.getConfiguration()); - Http1ClientRequest request = connector.client().get(); + HttpClientRequest request = connector.client().get(); assertThat(request.followRedirects(), is(true)); } @@ -66,7 +66,7 @@ void testConfigPropertyOverride() { .property(ClientProperties.FOLLOW_REDIRECTS, false) // override .build(); HelidonConnector connector = new HelidonConnector(client, client.getConfiguration()); - Http1ClientRequest request = connector.client().get(); + HttpClientRequest request = connector.client().get(); assertThat(request.followRedirects(), is(false)); } diff --git a/jersey/tests/connector/pom.xml b/jersey/tests/connector/pom.xml index 926fca37b40..fd2ca348bb0 100644 --- a/jersey/tests/connector/pom.xml +++ b/jersey/tests/connector/pom.xml @@ -63,5 +63,10 @@ hamcrest-all test
+ + io.helidon.webserver + helidon-webserver-http2 + test + diff --git a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/JerseyConnectorTest.java b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorBase.java similarity index 76% rename from jersey/tests/connector/src/test/java/io/helidon/jersey/connector/JerseyConnectorTest.java rename to jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorBase.java index e072fbca8bf..fbf892c03be 100644 --- a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/JerseyConnectorTest.java +++ b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorBase.java @@ -19,54 +19,56 @@ import java.util.Arrays; import io.helidon.http.Status; -import io.helidon.webserver.WebServer; +import io.helidon.webclient.http2.Http2Client; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; -import io.helidon.webserver.testing.junit5.ServerTest; import io.helidon.webserver.testing.junit5.SetUpRoute; - import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.Response; -import org.glassfish.jersey.client.ClientConfig; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasKey; -/** - * Tests integration of Jakarta REST client with the Helidon connector that uses - * WebClient to execute HTTP requests. - */ -@ServerTest -class JerseyConnectorTest { +class ConnectorBase { + + private String baseURI; + private Client client; + private String protocolId; - private final String baseURI; - private final Client client; - JerseyConnectorTest(WebServer webServer) { - baseURI = "http://localhost:" + webServer.port(); - ClientConfig config = new ClientConfig(); - config.connectorProvider(new HelidonConnectorProvider()); // use Helidon's provider - client = ClientBuilder.newClient(config); + public void baseURI(String baseURI) { + this.baseURI = baseURI; + } + + public void client(Client client) { + this.client = client; + } + + public void protocolId(String protocolId) { + this.protocolId = protocolId; } @SetUpRoute static void routing(HttpRules rules) { - rules.get("/basic/get", JerseyConnectorTest::basicGet) - .post("/basic/post", JerseyConnectorTest::basicPost) - .get("/basic/getquery", JerseyConnectorTest::basicGetQuery) - .get("/basic/headers", JerseyConnectorTest::basicHeaders); + rules.get("/basic/get", ConnectorBase::basicGet) + .post("/basic/post", ConnectorBase::basicPost) + .get("/basic/getquery", ConnectorBase::basicGetQuery) + .get("/basic/headers", ConnectorBase::basicHeaders); } private WebTarget target(String uri) { - return client.target(baseURI).path(uri); + WebTarget webTarget = client.target(baseURI).path(uri); + if (protocolId != null) { + webTarget.property(HelidonProperties.PROTOCOL_ID, Http2Client.PROTOCOL_ID); + } + return webTarget; } static void basicGet(ServerRequest request, ServerResponse response) { @@ -87,7 +89,7 @@ static void basicGetQuery(ServerRequest request, ServerResponse response) { static void basicHeaders(ServerRequest request, ServerResponse response) { request.headers() .stream() - .filter(h -> h.name().startsWith("X-TEST")) + .filter(h -> h.name().startsWith("x-test")) .forEach(response::header); response.status(Status.OK_200).send("ok"); } @@ -122,9 +124,14 @@ public void queryGetTest() { @Test public void testHeaders() { - String[][] headers = new String[][]{{"X-TEST-ONE", "ONE"}, {"X-TEST-TWO", "TWO"}, {"X-TEST-THREE", "THREE"}}; + String[][] headers = new String[][]{ + {"x-test-one", "one"}, + {"x-test-two", "two"}, + {"x-test-three", "three"} + }; MultivaluedHashMap map = new MultivaluedHashMap<>(); Arrays.stream(headers).forEach(a -> map.add(a[0], a[1])); + try (Response response = target("basic").path("headers").request().headers(map).get()) { assertThat(response.getStatus(), is(200)); assertThat(response.readEntity(String.class), is("ok")); diff --git a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp1Test.java b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp1Test.java new file mode 100644 index 00000000000..f190b52e01b --- /dev/null +++ b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp1Test.java @@ -0,0 +1,37 @@ +/* + * 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.jersey.connector; + +import io.helidon.webserver.WebServer; +import io.helidon.webserver.testing.junit5.ServerTest; +import jakarta.ws.rs.client.ClientBuilder; +import org.glassfish.jersey.client.ClientConfig; + +/** + * Tests HTTP/1.1 integration of Jakarta REST client with the Helidon connector that uses + * WebClient to execute HTTP requests. + */ +@ServerTest +class ConnectorHttp1Test extends ConnectorBase { + + ConnectorHttp1Test(WebServer webServer) { + baseURI("http://localhost:" + webServer.port()); + ClientConfig config = new ClientConfig(); + config.connectorProvider(HelidonConnectorProvider.create()); // use Helidon's provider + client(ClientBuilder.newClient(config)); + } +} diff --git a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2AlpnTest.java b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2AlpnTest.java new file mode 100644 index 00000000000..db6a5ff0541 --- /dev/null +++ b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2AlpnTest.java @@ -0,0 +1,71 @@ +/* + * 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.jersey.connector; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.Keys; +import io.helidon.common.tls.Tls; +import io.helidon.webclient.http2.Http2Client; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http2.Http2Config; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import jakarta.ws.rs.client.ClientBuilder; +import org.glassfish.jersey.client.ClientConfig; + +/** + * Tests HTTP/2 integration of Jakarta REST client with the Helidon connector that uses + * WebClient to execute HTTP requests. Uses ALPN extension for negotiation. + */ +@ServerTest +class ConnectorHttp2AlpnTest extends ConnectorBase { + + @SetUpServer + static void setUpServer(WebServerConfig.Builder serverBuilder) { + Keys privateKeyConfig = Keys.builder() + .keystore(keystore -> keystore + .keystore(Resource.create("certificate.p12")) + .keystorePassphrase("helidon")) + .build(); + + Tls tls = Tls.builder() + .privateKey(privateKeyConfig.privateKey().orElseThrow()) + .privateKeyCertChain(privateKeyConfig.certChain()) + .build(); + + serverBuilder.putSocket("https", socketBuilder -> socketBuilder.tls(tls)); + serverBuilder.addProtocol(Http2Config.create()); + } + + ConnectorHttp2AlpnTest(WebServer server) { + int port = server.port("https"); + + Tls tls = Tls.builder() + .trustAll(true) + .addApplicationProtocol(Http2Client.PROTOCOL_ID) // h2 support ALPN + .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE) + .build(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(HelidonConnectorProvider.create()); // use Helidon's provider + config.property(HelidonProperties.TLS, tls); + + client(ClientBuilder.newClient(config)); + baseURI("https://localhost:" + port); + } +} diff --git a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2PriorTest.java b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2PriorTest.java new file mode 100644 index 00000000000..20b1bb54f9a --- /dev/null +++ b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2PriorTest.java @@ -0,0 +1,75 @@ +/* + * 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.jersey.connector; + +import java.util.List; + +import io.helidon.common.configurable.Resource; +import io.helidon.common.pki.Keys; +import io.helidon.common.tls.Tls; +import io.helidon.webclient.http2.Http2ClientProtocolConfig; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http2.Http2Config; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import jakarta.ws.rs.client.ClientBuilder; +import org.glassfish.jersey.client.ClientConfig; + +/** + * Tests HTTP/2 integration of Jakarta REST client with the Helidon connector that uses + * WebClient to execute HTTP requests. Assumes prior knowledge. + */ +@ServerTest +class ConnectorHttp2PriorTest extends ConnectorBase { + + @SetUpServer + static void setUpServer(WebServerConfig.Builder serverBuilder) { + Keys privateKeyConfig = Keys.builder() + .keystore(keystore -> keystore + .keystore(Resource.create("certificate.p12")) + .keystorePassphrase("helidon")) + .build(); + + Tls tls = Tls.builder() + .privateKey(privateKeyConfig.privateKey().orElseThrow()) + .privateKeyCertChain(privateKeyConfig.certChain()) + .build(); + + serverBuilder.putSocket("https", socketBuilder -> socketBuilder.tls(tls)); + serverBuilder.addProtocol(Http2Config.create()); + } + + ConnectorHttp2PriorTest(WebServer server) { + int port = server.port("https"); + + Tls tls = Tls.builder() + .trustAll(true) + .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE) + .build(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(HelidonConnectorProvider.create()); // use Helidon's provider + config.property(HelidonProperties.TLS, tls); + config.property(HelidonProperties.PROTOCOL_CONFIGS, + List.of(Http2ClientProtocolConfig.builder() + .priorKnowledge(true) + .build())); + client(ClientBuilder.newClient(config)); + baseURI("https://localhost:" + port); + } +} diff --git a/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2UpgradeTest.java b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2UpgradeTest.java new file mode 100644 index 00000000000..a76d2ce5815 --- /dev/null +++ b/jersey/tests/connector/src/test/java/io/helidon/jersey/connector/ConnectorHttp2UpgradeTest.java @@ -0,0 +1,50 @@ +/* + * 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.jersey.connector; + +import io.helidon.webclient.http2.Http2Client; +import io.helidon.webserver.WebServer; +import io.helidon.webserver.WebServerConfig; +import io.helidon.webserver.http2.Http2Config; +import io.helidon.webserver.testing.junit5.ServerTest; +import io.helidon.webserver.testing.junit5.SetUpServer; +import jakarta.ws.rs.client.ClientBuilder; +import org.glassfish.jersey.client.ClientConfig; + +/** + * Tests HTTP/2 integration of Jakarta REST client with the Helidon connector that uses + * WebClient to execute HTTP requests. Upgrades connection from HTTP/1.1 to HTTP/2. + */ +@ServerTest +class ConnectorHttp2UpgradeTest extends ConnectorBase { + + @SetUpServer + static void setUpServer(WebServerConfig.Builder serverBuilder) { + serverBuilder.addProtocol(Http2Config.create()); + } + + ConnectorHttp2UpgradeTest(WebServer server) { + int port = server.port(); + + ClientConfig config = new ClientConfig(); + config.connectorProvider(HelidonConnectorProvider.create()); // use Helidon's provider + + client(ClientBuilder.newClient(config)); + baseURI("http://localhost:" + port); + protocolId(Http2Client.PROTOCOL_ID); // HTTP/2 negotiation from 1.1 + } +} diff --git a/jersey/tests/connector/src/test/resources/certificate.p12 b/jersey/tests/connector/src/test/resources/certificate.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b7f6fd1c6c68077c31f1c7eacd060dcf475ddc00 GIT binary patch literal 2568 zcmY+^XHXLe7Qk^5Qm6^ND80mjKtc^l3$XN}p@>GQiqbX&khatyAxN*Xp-S&r!9ouL zQX(A$RJyEm5C!RSICFP<_u@rNX`fPer-B$1g8VZKZ9dI_l6S}|{k{Q-E-me~aQ z7C4sapAlWzFWWz0Bd05s{_{bwb${}Y@MLmcJn}k;)L30tmq&%S(Y5T0Q{-1#4A>mi zR1#~X*nGL3kvZ+$marp0tz@Q(xHnUk67gh`A?9MMmPf1c!73gO{ky~np8zwANRiu)~_tFn%w@LJwGd4Ws3%+Xu7AsVd5e%=gmP6#v!^@K`T=*VLfeOprhyCX2ot1fYae zRN#W?5noxvm|o#iRk~qVYb1Y8d|#UT^LT9idGp;w1iAV%&i;$9t8f3t&+T5ftB6lY z)T4wfCVL*M%ph?!H2a`J;i_Nm`&zn(aJ$t;hjzE17;_57+|6%zUbsoKB&!Ose*%N5 zu}okMbH2DnEUXplbWeXlD4L2HIWS-_p##E$ z@Vomy^j5qMf6MJYqM7f=(MGy8X$Ef86D!Mf%&Pq805bEJK5zB7S2M|{bxZBIv(NKS zs!WE~S~|TW3|2ksrOLeD9YLm`v?3FwX;(4=>mfgKljTbPkWM?*@`GiqI_WtYj-?+e z=`1%8uTJUe_+{vP)b{RX;CsF^n2n8TQ`N}$Fo73nRE7~e2VU%Keqtxz$=AU$4^7pv zYZBJ4=wU*)hZ65A4@>o{OdWklI7d%RRV-fQ-<-EGKX0)63w3AwO`7Bh@hUm zr%o+N#x(^Oq-AUgclAO%QZfrGp-w@O=`=z}5vQt5^b_!F;!3@-shqi><0-nVq(Az{ zU7SBf=5NMi=VKNgk!ec7U+2Pwr)VEVW?{vV1?&C?Wb$X64&@fT$mp`!oqT5A zo`{4C`+i0xudvH>TZpcx~4{p24$f zF<|bGLohPlR27vg3(Zc*FU2C~D2(F*529t-G(&A4RO21*g?MU1*W|zdFLeC~?&>2oEkur8?wv)LgUB^v;{iXZ@1`$K)w23CfO7DUW zp8(r3XPwc!QqVTMe%_V8*}B4dE+3{sH5i)8{gk?2D7)-0vxI5#Ui6=s=(z_aOT@xW z!&_bwo^QHR(hmE~_qMp7Ox-zNMk0#alBQK{oK8$^^t*aq4%qm+O&__uhj#d})hP41 zH_dk?<+h6pK{jz$ZCi#-s%srCKFik-|7qVB%ts&He};dTlbXlRc&V%)SvWu`HD!gw z{<65S3E=ODf*~ruzAPGj;l8Q-BLoh&3*yX}U3;11)$(6-K)xmfaB{bq38e3Oy?hbRI0<(n=p^^C!R; zHKTe?!x;7Thb&qfnglMOOsv3C6JG&Z?CtVv%@e=pxIR=EIkF!b5Ow|V$!8>se-2xJ zaM|EtXHvORG*~q}eZXtLSPtLHBc>QAb~IJp=X@FEhl??wv(X(F1Bx<{@Gk1-=@Pf& zrFR;>g!1s60w1{XxA6Is+x+8ZUKNo7{Asw{je7imLj05i;?<8m;Er6$AC?Nno}5bx zk9S{T!I`Ei`1tHuL|&YwB{p80BM+RJhMJ>;(C@6F4Td+#2DBtHX{GM#6fzvX6Mv&`ath3MHp$^y-`DY5e2DkubaA zlx2iTWIQZh9ldp*Y>;2aj_yzv`=VA62y^o-h1n-zOy);F@`T zkDloj%~Co{lZ)S&8*)UO8#eu@@hZ;$4F}(Zl)T_~xPeDB->OuL3VM9^)YUZ_d zt42!ZbcD^3_9_IXw~RU!P`jpMKU_Egg+_AzS4U?ClXy{lAfDyJ=Z0?&aNKs+N`lAK~U7|M9fEL^ssI17(fyEh;v PNwXcW8IJ_lFF5}NTT7{< literal 0 HcmV?d00001 diff --git a/webclient/http2/src/main/java/module-info.java b/webclient/http2/src/main/java/module-info.java index 742bd71f566..ef56d1bf7ca 100644 --- a/webclient/http2/src/main/java/module-info.java +++ b/webclient/http2/src/main/java/module-info.java @@ -37,4 +37,6 @@ exports io.helidon.webclient.http2; + provides io.helidon.webclient.spi.HttpClientSpiProvider + with io.helidon.webclient.http2.Http2ClientSpiProvider; }