diff --git a/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc index b3ecf60ebd..3abe720215 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc @@ -216,6 +216,22 @@ Using the following classes: NOTE: This example uses the link:#blocking-aggregated[blocking + aggregated] API, as the TLS/SSL configuration API is the same across all the HTTP APIs. +[#HTTPS-Proxy] +== HTTPS Proxy + +This example demonstrates how a client can reach an origin server through an HTTP `CONNECT` proxy that itself requires +mutual TLS. Two independent TLS configurations are involved: the outer handshake authenticates the client to the proxy, +and the inner handshake performs standard server-auth TLS with the origin, tunneled over the established `CONNECT`. + +Using the following classes: + +- link:{source-root}/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyServer.java[HttpsProxyServer] - Starts an origin HTTPS server and a mutual-TLS `CONNECT` proxy in front of it. +- link:{source-root}/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyClient.java[HttpsProxyClient] - A client that configures mutual TLS to the proxy and server-auth TLS to the origin. +- link:{source-root}/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HandshakeLoggingTransportObserver.java[HandshakeLoggingTransportObserver] - A `TransportObserver` that logs both the proxy and origin TLS handshakes. + +NOTE: This example uses the link:#blocking-aggregated[blocking + aggregated] API, as the TLS/SSL configuration API is +the same across all the HTTP APIs. + [#Observer] == Observer This example demonstrates the following: diff --git a/servicetalk-examples/http/https-proxy/build.gradle b/servicetalk-examples/http/https-proxy/build.gradle new file mode 100644 index 0000000000..c2f2e1fcbc --- /dev/null +++ b/servicetalk-examples/http/https-proxy/build.gradle @@ -0,0 +1,36 @@ +/* + * Copyright © 2026 Apple Inc. and the ServiceTalk project authors + * + * 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. + */ + +apply plugin: "java" +apply from: "../../gradle/idea.gradle" + +dependencies { + implementation platform("io.netty:netty-bom:$nettyVersion") + + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-client-api") + implementation project(":servicetalk-http-netty") + // ProxyTunnel, an in-process mutual-TLS CONNECT proxy, lives in this module's test fixtures. It is used here only + // to make the example self-contained; production code never depends on test fixtures. + implementation testFixtures(project(":servicetalk-http-netty")) + + // This dependency brings self-signed TLS certificates for demonstration purposes. + // In real applications users have to provide their own certificates instead. + implementation project(":servicetalk-test-resources") + + implementation "org.slf4j:slf4j-api:$slf4jVersion" + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" +} diff --git a/servicetalk-examples/http/https-proxy/gradle.lockfile b/servicetalk-examples/http/https-proxy/gradle.lockfile new file mode 100644 index 0000000000..7f7b14a016 --- /dev/null +++ b/servicetalk-examples/http/https-proxy/gradle.lockfile @@ -0,0 +1,46 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +# To regenerate this file, run: ./gradlew :servicetalk-examples:http:servicetalk-examples-http-https-proxy:dependencies --write-locks +com.google.code.findbugs:jsr305:3.0.2=compileClasspath,runtimeClasspath +io.netty:netty-bom:4.2.16.Final=compileClasspath,runtimeClasspath +io.netty:netty-buffer:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-base:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-compression:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-dns:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-http2:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-http:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-marshalling:4.2.16.Final=runtimeClasspath +io.netty:netty-codec-protobuf:4.2.16.Final=runtimeClasspath +io.netty:netty-codec:4.2.16.Final=runtimeClasspath +io.netty:netty-common:4.2.16.Final=runtimeClasspath +io.netty:netty-handler:4.2.16.Final=runtimeClasspath +io.netty:netty-resolver-dns-classes-macos:4.2.16.Final=runtimeClasspath +io.netty:netty-resolver-dns-native-macos:4.2.16.Final=runtimeClasspath +io.netty:netty-resolver-dns:4.2.16.Final=runtimeClasspath +io.netty:netty-resolver:4.2.16.Final=runtimeClasspath +io.netty:netty-tcnative-boringssl-static:2.0.78.Final=runtimeClasspath +io.netty:netty-tcnative-classes:2.0.78.Final=runtimeClasspath +io.netty:netty-transport-classes-epoll:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-classes-io_uring:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-classes-kqueue:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-native-epoll:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-native-io_uring:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-native-kqueue:4.2.16.Final=runtimeClasspath +io.netty:netty-transport-native-unix-common:4.2.16.Final=runtimeClasspath +io.netty:netty-transport:4.2.16.Final=runtimeClasspath +net.bytebuddy:byte-buddy-agent:1.12.19=runtimeClasspath +net.bytebuddy:byte-buddy:1.12.19=runtimeClasspath +org.apache.logging.log4j:log4j-api:2.24.3=runtimeClasspath +org.apache.logging.log4j:log4j-core:2.24.3=runtimeClasspath +org.apache.logging.log4j:log4j-slf4j-impl:2.24.3=runtimeClasspath +org.hamcrest:hamcrest:2.2=compileClasspath,runtimeClasspath +org.jctools:jctools-core:4.0.6=runtimeClasspath +org.junit.jupiter:junit-jupiter-api:5.14.1=runtimeClasspath +org.junit.platform:junit-platform-commons:1.14.1=runtimeClasspath +org.junit:junit-bom:5.14.1=runtimeClasspath +org.mockito:mockito-core:4.11.0=runtimeClasspath +org.objenesis:objenesis:3.3=runtimeClasspath +org.opentest4j:opentest4j:1.3.0=runtimeClasspath +org.slf4j:slf4j-api:1.7.36=compileClasspath,runtimeClasspath +empty=annotationProcessor diff --git a/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HandshakeLoggingTransportObserver.java b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HandshakeLoggingTransportObserver.java new file mode 100644 index 0000000000..6ff8263cfd --- /dev/null +++ b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HandshakeLoggingTransportObserver.java @@ -0,0 +1,84 @@ +/* + * Copyright © 2026 Apple Inc. and the ServiceTalk project authors + * + * 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.servicetalk.examples.http.httpsproxy; + +import io.servicetalk.transport.api.ConnectionObserver; +import io.servicetalk.transport.api.SslConfig; +import io.servicetalk.transport.api.TransportObserver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.net.ssl.SSLSession; + +/** + * Logs the outcome of both TLS handshakes performed when CONNECTing through a TLS proxy: the outer handshake with the + * proxy itself ({@link ConnectionObserver#onProxySecurityHandshake}) and the inner handshake with the origin server + * tunneled over the established CONNECT ({@link ConnectionObserver#onSecurityHandshake}). + */ +final class HandshakeLoggingTransportObserver implements TransportObserver { + + private static final Logger LOGGER = LoggerFactory.getLogger(HandshakeLoggingTransportObserver.class); + + @Override + public ConnectionObserver onNewConnection(@Nullable final Object localAddress, final Object remoteAddress) { + return new ConnectionObserver() { + @Override + public SecurityHandshakeObserver onProxySecurityHandshake(final SslConfig sslConfig) { + LOGGER.info("Proxy TLS handshake starting against {}", remoteAddress); + return new LoggingHandshakeObserver("proxy", remoteAddress); + } + + @Override + public SecurityHandshakeObserver onSecurityHandshake(final SslConfig sslConfig) { + LOGGER.info("Origin TLS handshake starting (tunneled through {})", remoteAddress); + return new LoggingHandshakeObserver("origin", remoteAddress); + } + }; + } + + private static final class LoggingHandshakeObserver implements ConnectionObserver.SecurityHandshakeObserver { + + private final String which; + private final Object remoteAddress; + + LoggingHandshakeObserver(final String which, final Object remoteAddress) { + this.which = which; + this.remoteAddress = remoteAddress; + } + + @Override + public void handshakeComplete(final SSLSession sslSession) { + LOGGER.info("{} TLS handshake complete against {}: protocol={}, cipher={}, peer={}", + which, remoteAddress, sslSession.getProtocol(), sslSession.getCipherSuite(), + peerPrincipal(sslSession)); + } + + @Override + public void handshakeFailed(final Throwable cause) { + LOGGER.warn("{} TLS handshake failed against {}", which, remoteAddress, cause); + } + + private static String peerPrincipal(final SSLSession session) { + try { + return session.getPeerPrincipal().getName(); + } catch (Exception e) { + return ""; + } + } + } +} diff --git a/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyClient.java b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyClient.java new file mode 100644 index 0000000000..c1dbf3a43b --- /dev/null +++ b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyClient.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2026 Apple Inc. and the ServiceTalk project authors + * + * 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.servicetalk.examples.http.httpsproxy; + +import io.servicetalk.client.api.TransportObserverConnectionFactoryFilter; +import io.servicetalk.http.api.BlockingHttpClient; +import io.servicetalk.http.api.HttpResponse; +import io.servicetalk.http.api.ProxyConfigBuilder; +import io.servicetalk.http.netty.HttpClients; +import io.servicetalk.test.resources.DefaultTestCerts; +import io.servicetalk.transport.api.ClientSslConfigBuilder; +import io.servicetalk.transport.api.HostAndPort; + +import static io.servicetalk.examples.http.httpsproxy.HttpsProxyServer.ORIGIN_PORT; +import static io.servicetalk.examples.http.httpsproxy.HttpsProxyServer.PROXY_PORT; +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; + +/** + * A client that reaches an origin server through a mutual-TLS HTTP CONNECT proxy: it presents a client certificate to + * the proxy (outer TLS) and does ordinary server-auth TLS to the origin (inner TLS) over the established tunnel. + *

+ * Run {@link HttpsProxyServer} first. + */ +public final class HttpsProxyClient { + + private HttpsProxyClient() { + } + + public static void main(String[] args) throws Exception { + // Note: DefaultTestCerts contains self-signed certificates that may be used only for local testing + // or demonstration purposes. Never use those for real use-cases. + try (BlockingHttpClient client = HttpClients.forSingleAddress("localhost", ORIGIN_PORT) + .proxyConfig(new ProxyConfigBuilder<>(HostAndPort.of("localhost", PROXY_PORT)) + // Outer TLS to the proxy: trust the proxy's certificate and present our own to authenticate. + .sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem) + .keyManager(DefaultTestCerts::loadClientPem, DefaultTestCerts::loadClientKey).build()) + .build()) + // Inner TLS to the origin, tunneled through the CONNECT: standard server authentication. + .sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build()) + // Optional: log both TLS handshakes (proxy and origin) as they complete. + .appendConnectionFactoryFilter(new TransportObserverConnectionFactoryFilter<>( + new HandshakeLoggingTransportObserver())) + .buildBlocking()) { + HttpResponse response = client.request(client.get("/")); + System.out.println(response.toString((name, value) -> value)); + System.out.println(response.payloadBody(textSerializerUtf8())); + } + } +} diff --git a/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyServer.java b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyServer.java new file mode 100644 index 0000000000..aa99ce486e --- /dev/null +++ b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/HttpsProxyServer.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2026 Apple Inc. and the ServiceTalk project authors + * + * 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.servicetalk.examples.http.httpsproxy; + +import io.servicetalk.http.netty.HttpServers; +import io.servicetalk.http.netty.ProxyTunnel; +import io.servicetalk.test.resources.DefaultTestCerts; +import io.servicetalk.transport.api.ServerSslConfigBuilder; + +import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.CertificateFactory; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; + +import static io.servicetalk.http.api.HttpSerializers.textSerializerUtf8; + +/** + * Starts the two server-side components used by this example: an origin HTTPS server and a mutual-TLS + * {@link ProxyTunnel CONNECT proxy} in front of it. Run this before {@link HttpsProxyClient}. + */ +public final class HttpsProxyServer { + + static final int ORIGIN_PORT = 8080; + static final int PROXY_PORT = 8081; + + private static final char[] KEYSTORE_PASSWORD = "changeit".toCharArray(); + + private HttpsProxyServer() { + } + + public static void main(String[] args) throws Exception { + // The proxy terminates the outer TLS handshake (requiring a client certificate) and then blindly tunnels the + // inner, end-to-end TLS session between the client and the origin. ProxyTunnel is a test fixture reused here + // to keep the example self-contained. + final ProxyTunnel proxyTunnel = new ProxyTunnel(); + proxyTunnel.sslContext(proxyMtlsSslContext()); + proxyTunnel.needClientAuth(true); + proxyTunnel.startProxy(PROXY_PORT); + + // A regular origin HTTPS server (server-auth TLS only); the proxy forwards the tunneled bytes here. + HttpServers.forPort(ORIGIN_PORT) + // Note: DefaultTestCerts contains self-signed certificates that may be used only for local testing + // or demonstration purposes. Never use those for real use-cases. + .sslConfig(new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem, DefaultTestCerts::loadServerKey) + .build()) + .listenBlockingAndAwait((ctx, request, responseFactory) -> responseFactory.ok() + .payloadBody("Reached the origin through a mutual-TLS CONNECT proxy!", textSerializerUtf8())) + .awaitShutdown(); + } + + // The proxy's mutual-TLS SSLContext: server identity from the shared "localhost" test certificate, trusting + // client certificates issued by the example client CA. + private static SSLContext proxyMtlsSslContext() throws Exception { + final KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream in = DefaultTestCerts.loadServerP12()) { + keyStore.load(in, KEYSTORE_PASSWORD); + } + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, KEYSTORE_PASSWORD); + + final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + try (InputStream in = DefaultTestCerts.loadClientCAPem()) { + trustStore.setCertificateEntry("client-ca", + CertificateFactory.getInstance("X.509").generateCertificate(in)); + } + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); + return context; + } +} diff --git a/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/package-info.java b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/package-info.java new file mode 100644 index 0000000000..df97a59303 --- /dev/null +++ b/servicetalk-examples/http/https-proxy/src/main/java/io/servicetalk/examples/http/httpsproxy/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2026 Apple Inc. and the ServiceTalk project authors + * + * 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. + */ +/** + * Runnable client demonstrating layered TLS through an HTTPS CONNECT proxy: mutual TLS to the proxy, then a tunneled + * TLS handshake with the origin server. Both handshakes are surfaced through a {@code TransportObserver}. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.examples.http.httpsproxy; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-examples/http/https-proxy/src/main/resources/log4j2.xml b/servicetalk-examples/http/https-proxy/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..076620e5d5 --- /dev/null +++ b/servicetalk-examples/http/https-proxy/src/main/resources/log4j2.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + diff --git a/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/ProxyTunnel.java b/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/ProxyTunnel.java index 7801e2ffe8..f340c62e8d 100644 --- a/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/ProxyTunnel.java +++ b/servicetalk-http-netty/src/testFixtures/java/io/servicetalk/http/netty/ProxyTunnel.java @@ -93,20 +93,31 @@ public void close() throws Exception { } /** - * Starts the proxy server and returns its {@link HostAndPort} to connect to. + * Starts the proxy server on an ephemeral port and returns its {@link HostAndPort} to connect to. * * @return the {@link HostAndPort} to use to connect to the proxy * @throws IOException In case of any I/O exception */ - @SuppressWarnings("PMD.ExceptionAsFlowControl") public HostAndPort startProxy() throws IOException { + return startProxy(0); + } + + /** + * Starts the proxy server on the specified port and returns its {@link HostAndPort} to connect to. + * + * @param port the port to bind to, or {@code 0} to bind an ephemeral port + * @return the {@link HostAndPort} to use to connect to the proxy + * @throws IOException In case of any I/O exception + */ + @SuppressWarnings("PMD.ExceptionAsFlowControl") + public HostAndPort startProxy(final int port) throws IOException { final SSLContext sslCtx = this.sslContext; if (sslCtx == null) { - serverSocket = new ServerSocket(0, 50, getLoopbackAddress()); + serverSocket = new ServerSocket(port, 50, getLoopbackAddress()); } else { // Terminate TLS on the proxy listener. The accepted Socket is an SSLSocket whose handshake is performed // implicitly on first read/write; downstream code is plain InputStream/OutputStream and is unaffected. - serverSocket = sslCtx.getServerSocketFactory().createServerSocket(0, 50, getLoopbackAddress()); + serverSocket = sslCtx.getServerSocketFactory().createServerSocket(port, 50, getLoopbackAddress()); // Client auth is a property of the SSLServerSocket, not the SSLContext, so it must be enabled here. ((SSLServerSocket) serverSocket).setNeedClientAuth(needClientAuth); } @@ -125,7 +136,7 @@ public HostAndPort startProxy() throws IOException { final String authority = initialLine.substring(CONNECT_PREFIX.length(), end); final int colon = authority.indexOf(':'); final String host = authority.substring(0, colon); - final int port = Integer.parseInt(authority.substring(colon + 1)); + final int originPort = Integer.parseInt(authority.substring(colon + 1)); final String protocol = initialLine.substring(end + 1); final HttpHeaders headers = readHeaders(in); @@ -151,7 +162,7 @@ public HostAndPort startProxy() throws IOException { proxyAuthRequired(socket, protocol); return; } - handler.handle(socket, host, port, protocol); + handler.handle(socket, host, originPort, protocol); } catch (Exception e) { LOGGER.debug("Error from proxy socket={}", socket, e); } finally { diff --git a/settings.gradle b/settings.gradle index 5013a4cd0c..e3125a5166 100755 --- a/settings.gradle +++ b/settings.gradle @@ -65,6 +65,7 @@ include "servicetalk-annotations", "servicetalk-examples:grpc:keepalive", "servicetalk-examples:http:helloworld", "servicetalk-examples:http:mutual-tls", + "servicetalk-examples:http:https-proxy", "servicetalk-examples:http:redirects", "servicetalk-examples:http:http2", "servicetalk-examples:http:jaxrs", @@ -170,6 +171,7 @@ project(":servicetalk-examples:http:service-composition").name = "servicetalk-ex project(":servicetalk-examples:http:traffic-resilience").name = "servicetalk-examples-http-traffic-resilience" project(":servicetalk-examples:http:uds").name = "servicetalk-examples-http-uds" project(":servicetalk-examples:http:mutual-tls").name = "servicetalk-examples-http-mutual-tls" +project(":servicetalk-examples:http:https-proxy").name = "servicetalk-examples-http-https-proxy" project(":servicetalk-examples:http:redirects").name = "servicetalk-examples-http-redirects" project(":servicetalk-examples:http:compression").name = "servicetalk-examples-http-compression" project(":servicetalk-examples:http:files").name = "servicetalk-examples-http-files"