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 868d0a81fa6..5ed9b6030ae 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 @@ -46,6 +46,7 @@ import jakarta.ws.rs.core.Response; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.JerseyClient; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.internal.util.PropertiesHelper; @@ -82,7 +83,8 @@ class HelidonConnector implements Connector { var builder = WebClientConfig.builder(); // use config for client - builder.config(helidonConfig(config).orElse(Config.empty())); + Config helidonConfig = helidonConfig(config).orElse(Config.empty()); + builder.config(helidonConfig); // proxy support proxy = ProxyBuilder.createProxy(config).orElse(Proxy.create()); @@ -98,11 +100,18 @@ class HelidonConnector implements Connector { builder.followRedirects(getValue(properties, FOLLOW_REDIRECTS, true)); } - // 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()); + //Whether WebClient TLS has been already set via config + boolean helidonConfigTlsSet = helidonConfig.map(hc -> hc.get("tls").exists()).orElse(false); + boolean isJerseyClient = client instanceof JerseyClient; + //Whether Jersey client has non-default SslContext set. If so, we should honor these settings + boolean jerseyHasDefaultSsl = isJerseyClient && ((JerseyClient) client).isDefaultSslContext(); + + if (!helidonConfigTlsSet || !isJerseyClient || !jerseyHasDefaultSsl) {// 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 diff --git a/tests/integration/pom.xml b/tests/integration/pom.xml index 31377fd7775..128661f8593 100644 --- a/tests/integration/pom.xml +++ b/tests/integration/pom.xml @@ -59,6 +59,7 @@ native-image oidc restclient + restclient-connector security vault zipkin-mp-2.2 diff --git a/tests/integration/restclient-connector/pom.xml b/tests/integration/restclient-connector/pom.xml new file mode 100644 index 00000000000..16c537519dc --- /dev/null +++ b/tests/integration/restclient-connector/pom.xml @@ -0,0 +1,68 @@ + + + + + 4.0.0 + + io.helidon.tests.integration + helidon-tests-integration + 4.0.0-SNAPSHOT + + + helidon-tests-integration-restclient-connector + Helidon Integration Test RestClient Webclient Connector + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.helidon.microprofile.rest-client + helidon-microprofile-rest-client + + + io.smallrye + jandex + runtime + true + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.microprofile.testing + helidon-microprofile-testing-junit5 + test + + + io.helidon.jersey + helidon-jersey-connector + test + + + + \ No newline at end of file diff --git a/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResource.java b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResource.java new file mode 100644 index 00000000000..9d80a04f57b --- /dev/null +++ b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResource.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 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.tests.integration.resclient.connector; + +import java.util.Collections; + +import jakarta.json.Json; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +/** + * A typical greet resource that only handles a single GET for a default message. + */ +@Path("/greet") +public class GreetResource { + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap()); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public JsonObject getDefaultMessage() { + return createResponse("World"); + } + + private JsonObject createResponse(String who) { + String msg = String.format("%s %s!", "Hello", who); + + return JSON.createObjectBuilder() + .add("message", msg) + .build(); + } +} diff --git a/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceClient.java b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceClient.java new file mode 100644 index 00000000000..0ff3f8b3a47 --- /dev/null +++ b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceClient.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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.tests.integration.resclient.connector; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; + +/** + * RestClient interface for a simple greet resource that includes a few FT annotations. + */ +@Path("/greet") +@RegisterProvider(GreetResourceFilter.class) +public interface GreetResourceClient { + + @GET + @Produces(MediaType.APPLICATION_JSON) + JsonObject getDefaultMessage(); + +} diff --git a/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceFilter.java b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceFilter.java new file mode 100644 index 00000000000..ccfaf09bf69 --- /dev/null +++ b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/GreetResourceFilter.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 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.tests.integration.resclient.connector; + +import java.io.IOException; +import java.net.URI; + +import io.helidon.common.context.Contexts; + +import jakarta.ws.rs.client.ClientRequestContext; +import jakarta.ws.rs.client.ClientRequestFilter; + +/** + * A client request filter that replaces port 8080 by the ephemeral port allocated for the + * webserver in each run. This is necessary since {@link GreetResourceClient} uses an annotation + * to specify the base URI, and its value cannot be changed dynamically. + */ +public class GreetResourceFilter implements ClientRequestFilter { + + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + URI uri = requestContext.getUri(); + String fixedUri = uri.toString().replace("8080", extractDynamicPort()); + requestContext.setUri(URI.create(fixedUri)); + } + + private String extractDynamicPort() { + URI uri = Contexts.globalContext().get(getClass(), URI.class).orElseThrow(); + String uriString = uri.toString(); + int k = uriString.lastIndexOf(":"); + int j = uriString.indexOf("/", k); + j = j < 0 ? uriString.length() : j; //Prevent failing if / is missing after the port + return uriString.substring(k + 1, j); + } +} diff --git a/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/package-info.java b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/package-info.java new file mode 100644 index 00000000000..510c7bd1a2b --- /dev/null +++ b/tests/integration/restclient-connector/src/main/java/io/helidon/tests/integration/resclient/connector/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 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.tests.integration.resclient.connector; \ No newline at end of file diff --git a/tests/integration/restclient-connector/src/main/resources/META-INF/beans.xml b/tests/integration/restclient-connector/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..79fe65a8048 --- /dev/null +++ b/tests/integration/restclient-connector/src/main/resources/META-INF/beans.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/tests/integration/restclient-connector/src/test/java/io/helidon/tests/integration/restclient/connector/TlsTest.java b/tests/integration/restclient-connector/src/test/java/io/helidon/tests/integration/restclient/connector/TlsTest.java new file mode 100644 index 00000000000..0cc4add9a47 --- /dev/null +++ b/tests/integration/restclient-connector/src/test/java/io/helidon/tests/integration/restclient/connector/TlsTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 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.tests.integration.restclient.connector; + +import java.io.UncheckedIOException; +import java.net.URI; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +import javax.net.ssl.SSLContext; + +import io.helidon.common.context.Contexts; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.jersey.connector.HelidonProperties; +import io.helidon.microprofile.testing.junit5.Configuration; +import io.helidon.microprofile.testing.junit5.HelidonTest; +import io.helidon.tests.integration.resclient.connector.GreetResourceClient; +import io.helidon.tests.integration.resclient.connector.GreetResourceFilter; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.client.WebTarget; +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@HelidonTest +@Configuration(configSources = "tls-config.properties") +public class TlsTest { + + @Test + void testHelloWorld(WebTarget target) { + Config config = Config.create(ConfigSources.create(Map.of("tls.trust-all", "true"))); + Contexts.globalContext().register(GreetResourceFilter.class, target.getUri()); + + GreetResourceClient client = RestClientBuilder.newBuilder() + .baseUri(URI.create("https://localhost:8080")) + .property(HelidonProperties.CONFIG, config) + .build(GreetResourceClient.class); + + JsonObject defaultMessage = client.getDefaultMessage(); + assertThat(defaultMessage.toString(), is("{\"message\":\"Hello World!\"}")); + } + + @Test + void restClientSslContextPriority(WebTarget target) throws NoSuchAlgorithmException { + Config config = Config.create(ConfigSources.create(Map.of("tls.trust-all", "true"))); + Contexts.globalContext().register(GreetResourceFilter.class, target.getUri()); + + GreetResourceClient client = RestClientBuilder.newBuilder() + .baseUri(URI.create("https://localhost:8080")) + .property(HelidonProperties.CONFIG, config) + .sslContext(SSLContext.getDefault()) + .build(GreetResourceClient.class); + + ProcessingException exception = assertThrows(ProcessingException.class, client::getDefaultMessage); + assertThat(exception.getCause(), instanceOf(UncheckedIOException.class)); + assertThat(exception.getCause().getMessage(), endsWith("Failed to execute SSL handshake")); + } + + +} diff --git a/tests/integration/restclient-connector/src/test/resources/server.p12 b/tests/integration/restclient-connector/src/test/resources/server.p12 new file mode 100644 index 00000000000..c5e409b4433 Binary files /dev/null and b/tests/integration/restclient-connector/src/test/resources/server.p12 differ diff --git a/tests/integration/restclient-connector/src/test/resources/tls-config.properties b/tests/integration/restclient-connector/src/test/resources/tls-config.properties new file mode 100644 index 00000000000..b6342e06215 --- /dev/null +++ b/tests/integration/restclient-connector/src/test/resources/tls-config.properties @@ -0,0 +1,26 @@ +# +# Copyright (c) 2024 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.host=0.0.0.0 + +#Truststore setup +server.tls.trust.keystore.resource.resource-path=server.p12 +server.tls.trust.keystore.passphrase=toChange +server.tls.trust.keystore.trust-store=true + +#Keystore with private key and server certificate +server.tls.private-key.keystore.resource.resource-path=server.p12 +server.tls.private-key.keystore.passphrase=toChange \ No newline at end of file