From 2c2d91e56f31b4bbf56a4266b9d1ae249035b6a1 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:54:59 -0700 Subject: [PATCH 01/12] Initial support for Kerberos auth (#7211) * Initial support for Kerberos auth This commit adds - A new enum `ProxyAuthScheme` that enumerates the proxy auth mechanisms supported by Netty - `ProxyAuthGenerator` (internal) that knows how to generate the auth params for its respective auth scheme - `NegotiateProxyAuthGenerator` for Kerberos * wip * Document OID --- bom-internal/pom.xml | 6 + http-clients/netty-nio-client/pom.xml | 5 + .../http/nio/netty/ProxyAuthScheme.java | 45 ++++++ .../internal/AwaitCloseChannelPoolMap.java | 13 +- .../internal/BasicProxyAuthGenerator.java | 43 ++++++ .../internal/Http1TunnelConnectionPool.java | 28 ++-- .../internal/NegotiateProxyAuthGenerator.java | 131 ++++++++++++++++++ .../netty/internal/ProxyAuthGenerator.java | 37 +++++ .../internal/ProxyTunnelInitHandler.java | 42 ++++-- .../Http1TunnelConnectionPoolTest.java | 54 +++----- .../NegotiateProxyAuthGeneratorTest.java | 110 +++++++++++++++ .../internal/ProxyTunnelInitHandlerTest.java | 7 +- pom.xml | 1 + 13 files changed, 456 insertions(+), 66 deletions(-) create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java diff --git a/bom-internal/pom.xml b/bom-internal/pom.xml index e5b037c07c66..78c85e86a17e 100644 --- a/bom-internal/pom.xml +++ b/bom-internal/pom.xml @@ -519,6 +519,12 @@ pom import + + org.apache.kerby + kerb-simplekdc + ${kerb-simplekdc.version} + test + diff --git a/http-clients/netty-nio-client/pom.xml b/http-clients/netty-nio-client/pom.xml index 60139771232f..9988be3c5b18 100644 --- a/http-clients/netty-nio-client/pom.xml +++ b/http-clients/netty-nio-client/pom.xml @@ -233,6 +233,11 @@ jetty-util test + + org.apache.kerby + kerb-simplekdc + test + diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java new file mode 100644 index 000000000000..05719c612c02 --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty; + +import software.amazon.awssdk.annotations.SdkPublicApi; + +/** + * Supported auth schemes for authentication with a proxy. + */ +@SdkPublicApi +public enum ProxyAuthScheme { + /** + * Basic authentication. + */ + BASIC("Basic"), + + /** + * Kerberos authentication. + */ + NEGOTIATE("Negotiate"), + ; + + private final String value; + + ProxyAuthScheme(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index ff5c87e57038..b9f28b6d3a59 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -44,6 +44,7 @@ import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; +import software.amazon.awssdk.utils.StringUtils; /** * Implementation of {@link SdkChannelPoolMap} that awaits channel pools to be closed upon closing. @@ -143,7 +144,7 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { if (shouldUseProxyForHost(key)) { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER); baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext, - proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(), + proxyAddress(key), resolveProxyAuthGenerator(proxyConfiguration), key, pipelineInitializer, configuration); } else { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer); @@ -156,6 +157,16 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { return new SimpleChannelPoolAwareChannelPool(wrappedPool, tcpChannelPool); } + private ProxyAuthGenerator resolveProxyAuthGenerator(ProxyConfiguration proxyConfiguration) { + String username = proxyConfiguration.username(); + String password = proxyConfiguration.password(); + if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password)) { + return new BasicProxyAuthGenerator(username, password); + } + + return null; + } + @Override public void close() { log.trace(null, () -> "Closing channel pools"); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java new file mode 100644 index 000000000000..4d8912994085 --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java @@ -0,0 +1,43 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.CharsetUtil; +import java.net.URI; +import java.util.Base64; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; + +public class BasicProxyAuthGenerator implements ProxyAuthGenerator { + private final String username; + private final String password; + + public BasicProxyAuthGenerator(String username, String password) { + this.username = username; + this.password = password; + } + + @Override + public ProxyAuthScheme scheme() { + return ProxyAuthScheme.BASIC; + } + + @Override + public String generateAuthParams(URI proxyEndpoint) { + String authToken = String.format("%s:%s", this.username, this.password); + return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java index cc53ed4da46a..0dafa642d6e4 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java @@ -49,41 +49,30 @@ public class Http1TunnelConnectionPool implements ChannelPool { private final ChannelPool delegate; private final SslContext sslContext; private final URI proxyAddress; - private final String proxyUser; - private final String proxyPassword; + private final ProxyAuthGenerator proxyAuthGenerator; private final URI remoteAddress; private final ChannelPoolHandler handler; private final InitHandlerSupplier initHandlerSupplier; private final NettyConfiguration nettyConfiguration; public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, String proxyUsername, String proxyPassword, + URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress, ChannelPoolHandler handler, NettyConfiguration nettyConfiguration) { this(eventLoop, delegate, sslContext, - proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler, + proxyAddress, proxyAuthGenerator, remoteAddress, handler, ProxyTunnelInitHandler::new, nettyConfiguration); } - public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler, - NettyConfiguration nettyConfiguration) { - this(eventLoop, delegate, sslContext, - proxyAddress, null, null, remoteAddress, handler, - ProxyTunnelInitHandler::new, nettyConfiguration); - - } - @SdkTestInternalApi Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress, + URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress, ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier, NettyConfiguration nettyConfiguration) { this.eventLoop = eventLoop; this.delegate = delegate; this.sslContext = sslContext; this.proxyAddress = proxyAddress; - this.proxyUser = proxyUser; - this.proxyPassword = proxyPassword; + this.proxyAuthGenerator = proxyAuthGenerator; this.remoteAddress = remoteAddress; this.handler = handler; this.initHandlerSupplier = initHandlerSupplier; @@ -138,7 +127,7 @@ private void setupChannel(Channel ch, Promise acquirePromise) { if (sslHandler != null) { ch.pipeline().addLast(sslHandler); } - ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress, + ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyAddress, proxyAuthGenerator, remoteAddress, tunnelEstablishedPromise)); tunnelEstablishedPromise.addListener((Future f) -> { if (f.isSuccess()) { @@ -180,7 +169,10 @@ private static boolean isTunnelEstablished(Channel ch) { @SdkTestInternalApi @FunctionalInterface interface InitHandlerSupplier { - ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress, + ChannelHandler newInitHandler(ChannelPool sourcePool, + URI proxyAddress, + ProxyAuthGenerator authGenerator, + URI remoteAddress, Promise tunnelInitFuture); } } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java new file mode 100644 index 000000000000..95d6156aa96f --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import com.sun.security.auth.module.Krb5LoginModule; +import io.netty.handler.codec.http.HttpRequest; +import java.net.URI; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.utils.BinaryUtils; + +/** + * Auth generator for Kerberos. This does not login/authentication to Kerberos. It expects the ticket cache to be present and + * simply reads that to generate the token. + */ +@SdkInternalApi +public class NegotiateProxyAuthGenerator implements ProxyAuthGenerator { + // SPNEGO pseudo-mechanism OID. Lets the proxy negotiate Kerberos over HTTP "Negotiate". + // See https://www.ietf.org/rfc/rfc4178.txt for more info + private static final String OID = "1.3.6.1.5.5.2"; + private static final String SERVICE_NAME = "HTTP"; + private final Configuration config; + + public NegotiateProxyAuthGenerator() { + this(createDefaultConfig()); + } + + @SdkTestInternalApi + NegotiateProxyAuthGenerator(Configuration config) { + this.config = config; + } + + @Override + public ProxyAuthScheme scheme() { + return ProxyAuthScheme.NEGOTIATE; + } + + @Override + public String generateAuthParams(URI proxyEndpoint) { + try { + Subject subject = getSubject(); + + byte[] token = Subject.doAs(subject, (PrivilegedExceptionAction) () -> { + GSSContext ctx = createGSSContext(getManager(), proxyEndpoint); + ctx.requestMutualAuth(true); + return ctx.initSecContext(new byte[0], 0, 0); + }); + + return BinaryUtils.toBase64(token); + } catch (PrivilegedActionException e) { + throw new RuntimeException("Unable to generate token", e); + } + } + + private Subject getSubject() { + try { + LoginContext loginContext = new LoginContext("dummy", null, null, config); + loginContext.login(); + return loginContext.getSubject(); + } catch (LoginException e) { + throw new RuntimeException("Unable to perform login", e); + } + } + + private GSSContext createGSSContext(GSSManager manager, URI endpoint) { + try { + String name = String.format("%s@%s", SERVICE_NAME, endpoint.getHost()); + GSSName serverName = manager.createName(name, GSSName.NT_HOSTBASED_SERVICE); + Oid spnegoOid = new Oid(OID); + return manager.createContext(serverName, spnegoOid, null, + GSSContext.DEFAULT_LIFETIME); + } catch (GSSException e) { + throw new RuntimeException("Unable to create GSSContext", e); + } + } + + private static GSSManager getManager() { + return GSSManager.getInstance(); + } + + /** + * Create a generic {@link Configuration} that instructs the Kerberos login module to simply look in the ticket cache, and + * not to prompt for passwords. + *

+ * See javadoc for {@link Krb5LoginModule} for additional info on the configuration options. + */ + private static Configuration createDefaultConfig() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("doNotPrompt", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java new file mode 100644 index 000000000000..b078500b09ac --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import io.netty.handler.codec.http.HttpRequest; +import java.net.URI; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; + +/** + * Generates the auth params for an {@code Authorization} HTTP header. + */ +@SdkInternalApi +public interface ProxyAuthGenerator { + /** + * The name of the auth scheme this generator supports. + */ + ProxyAuthScheme scheme(); + + /** + * Generate the auth params for this request. + */ + String generateAuthParams(URI proxyEndpoint); +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index e6f309afbad3..277ed555dbc5 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -28,11 +28,9 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; -import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; -import java.util.Base64; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -47,32 +45,51 @@ public final class ProxyTunnelInitHandler extends ChannelDuplexHandler { public static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyTunnelInitHandler.class); private final ChannelPool sourcePool; - private final String username; - private final String password; + private final URI proxyAddress; + private final ProxyAuthGenerator authGenerator; private final URI remoteHost; private final Promise initPromise; private final Supplier httpCodecSupplier; public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise) { - this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); } public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise initPromise) { - this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, null, null, remoteHost, initPromise, HttpClientCodec::new); } @SdkTestInternalApi - public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword, + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; this.remoteHost = remoteHost; this.initPromise = initPromise; - this.username = prosyUsername; - this.password = proxyPassword; + if (!StringUtils.isBlank(proxyPassword) && !StringUtils.isBlank(proxyPassword)) { + this.authGenerator = new BasicProxyAuthGenerator(proxyUsername, proxyPassword); + } else { + this.authGenerator = null; + } this.httpCodecSupplier = httpCodecSupplier; } + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { + this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; + this.remoteHost = remoteHost; + this.initPromise = initPromise; + this.authGenerator = authGenerator; + this.httpCodecSupplier = httpCodecSupplier; + } + + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise) { + this(sourcePool, proxyAddress, authGenerator, remoteHost, initPromise, HttpClientCodec::new); + } + @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); @@ -151,10 +168,9 @@ private HttpRequest connectRequest() { Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, uri); - if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) { - String authToken = String.format("%s:%s", this.username, this.password); - String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); - request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64)); + if (authGenerator != null) { + String auth = String.format("%s %s", authGenerator.scheme().value(), authGenerator.generateAuthParams(proxyAddress)); + request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, auth); } return request; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index d43b404f3f5f..9e2ed53cd1e3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -73,6 +73,8 @@ public class Http1TunnelConnectionPoolTest { private static final String PROXY_PASSWORD = "mypassword"; + private static final ProxyAuthGenerator basicAuth = new BasicProxyAuthGenerator(PROXY_USER, PROXY_PASSWORD); + @Mock private ChannelPool delegatePool; @@ -115,7 +117,7 @@ public static void teardown() { @Test public void tunnelAlreadyEstablished_doesNotAddInitHandler() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(true); @@ -127,7 +129,7 @@ public void tunnelAlreadyEstablished_doesNotAddInitHandler() { @Test(timeout = 1000) public void tunnelNotEstablished_addsInitHandler() throws InterruptedException { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(false); @@ -149,7 +151,7 @@ public void tunnelInitFails_acquireFutureFails() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -164,7 +166,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -174,7 +176,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { @Test public void acquireFromDelegatePoolFails_failsFuture() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom"))); @@ -197,7 +199,7 @@ public void sslContextProvided_andProxyUsingHttps_addsSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTPS_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -218,7 +220,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -231,7 +233,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { @Test public void release_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.release(mockChannel); verify(delegatePool).release(eq(mockChannel), any(Promise.class)); } @@ -239,7 +241,7 @@ public void release_releasedToDelegatePool() { @Test public void release_withGivenPromise_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); Promise mockPromise = mock(Promise.class); tunnelPool.release(mockChannel, mockPromise); verify(delegatePool).release(eq(mockChannel), eq(mockPromise)); @@ -248,7 +250,7 @@ public void release_withGivenPromise_releasedToDelegatePool() { @Test public void close_closesDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.close(); verify(delegatePool).close(); } @@ -257,42 +259,32 @@ public void close_closesDelegatePool() { public void proxyAuthProvided_addInitHandler_withAuth(){ TestInitHandlerData data = new TestInitHandlerData(); - Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { + Http1TunnelConnectionPool.InitHandlerSupplier supplier = + (srcPool, proxyEndpoint, proxyAuthGenerator, remoteAddr, initFuture) -> { initFuture.setSuccess(mockChannel); - data.proxyUser(proxyUser); - data.proxyPassword(proxyPassword); + data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint); return mock(ChannelHandler.class); }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, basicAuth, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); - assertThat(data.proxyUser()).isEqualTo(PROXY_USER); - assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); - + // assertThat(data.proxyUser()).isEqualTo(PROXY_USER); + // assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); } private static class TestInitHandlerData { - private String proxyUser; - private String proxyPassword; - - public void proxyUser(String proxyUser) { - this.proxyUser = proxyUser; - } - - public String proxyUser() { - return this.proxyUser; - } + private String authHeader; - public void proxyPassword(String proxyPassword) { - this.proxyPassword = proxyPassword; + public void authHeader(String authHeader) { + this.authHeader = authHeader; } - public String proxyPassword(){ - return this.proxyPassword; + public String authHeader() { + return authHeader; } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java new file mode 100644 index 000000000000..d7c7a3bc1506 --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -0,0 +1,110 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import org.apache.kerby.kerberos.kerb.KrbException; +import org.apache.kerby.kerberos.kerb.client.KrbClient; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.testutils.FileUtils; + +public class NegotiateProxyAuthGeneratorTest { + private static Path tempDir; + private static Path keytabFile; + private static Path ccacheFile; + private static int port; + + private static SimpleKdcServer kdc; + + private static Configuration config; + + @BeforeAll + static void setup() throws IOException, KrbException { + tempDir = Files.createTempDirectory(null); + keytabFile = tempDir.resolve("keytab"); + ccacheFile = tempDir.resolve("ccache"); + + try (Socket freePort = new Socket()) { + freePort.setReuseAddress(true); + freePort.bind(new InetSocketAddress(0)); + port = freePort.getLocalPort(); + } + + kdc = new SimpleKdcServer(); + kdc.setKdcRealm("EXAMPLE.COM"); + kdc.setKdcHost("localhost"); + kdc.setWorkDir(tempDir.toFile()); + kdc.setKdcTcpPort(port); + kdc.init(); + kdc.start(); + + kdc.createPrincipal("alice@EXAMPLE.COM", "alicePassword"); + kdc.createAndExportPrincipals(keytabFile.toFile(), "HTTP/localhost@EXAMPLE.COM"); + + // initialize the ticket cache + KrbClient krbClient = kdc.getKrbClient(); + TgtTicket tgt = krbClient.requestTgt("alice@EXAMPLE.COM", "alicePassword"); + krbClient.storeTicket(tgt, ccacheFile.toFile()); + + // Override config so we look at the testing cache instead of the real system cache + config = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("ticketCache", ccacheFile.toAbsolutePath().toString()); + opts.put("doNotPrompt", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + + } + + @AfterAll + static void teardown() throws KrbException { + kdc.stop(); + FileUtils.cleanUpTestDirectory(tempDir); + } + + @Test + void generateAuthParams_configValid_successfullyGeneratesToken() { + NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config); + + URI proxyEndpoint = URI.create("https://localhost:8192"); + + assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); + } + +} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 9836a953bda9..7828050bef26 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -95,7 +95,8 @@ public void addedToPipeline_addsCodec() { Supplier codecSupplier = () -> codec; when(mockCtx.name()).thenReturn("foo"); - ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, REMOTE_HOST, null, codecSupplier); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, null, REMOTE_HOST, null, + codecSupplier); handler.handlerAdded(mockCtx); verify(mockPipeline).addBefore(eq("foo"), eq(null), eq(codec)); @@ -202,7 +203,7 @@ public void handlerRemoved_removesCodec() { } @Test - public void handledAdded_writesRequest_withoutAuth() { + public void handlerAdded_writesRequest_withoutAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); @@ -219,7 +220,7 @@ public void handledAdded_writesRequest_withoutAuth() { } @Test - public void handledAdded_writesRequest_withAuth() { + public void handlerAdded_writesRequest_withAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, PROXY_USER, PROXY_PASSWORD, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); diff --git a/pom.xml b/pom.xml index 83c359fa7ff5..cee43145ce64 100644 --- a/pom.xml +++ b/pom.xml @@ -152,6 +152,7 @@ 1.17.5 1.3.0 1.5.4 + 2.0.3 3.1.2 From bf5fa3411027010871c785b2b84ff28c8301854d Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:55:58 -0700 Subject: [PATCH 02/12] Revert "wip" (#7215) This reverts commit b20454f5ec9fb0a9873fab01c805bf28c8b0224c. --- .../internal/AwaitCloseChannelPoolMap.java | 13 +---- .../internal/BasicProxyAuthGenerator.java | 43 --------------- .../internal/Http1TunnelConnectionPool.java | 28 ++++++---- .../internal/NegotiateProxyAuthGenerator.java | 5 +- .../netty/internal/ProxyAuthGenerator.java | 5 +- .../internal/ProxyTunnelInitHandler.java | 42 +++++---------- .../Http1TunnelConnectionPoolTest.java | 54 +++++++++++-------- .../NegotiateProxyAuthGeneratorTest.java | 11 ++-- .../internal/ProxyTunnelInitHandlerTest.java | 7 ++- 9 files changed, 78 insertions(+), 130 deletions(-) delete mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index b9f28b6d3a59..ff5c87e57038 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -44,7 +44,6 @@ import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; -import software.amazon.awssdk.utils.StringUtils; /** * Implementation of {@link SdkChannelPoolMap} that awaits channel pools to be closed upon closing. @@ -144,7 +143,7 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { if (shouldUseProxyForHost(key)) { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER); baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext, - proxyAddress(key), resolveProxyAuthGenerator(proxyConfiguration), + proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(), key, pipelineInitializer, configuration); } else { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer); @@ -157,16 +156,6 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { return new SimpleChannelPoolAwareChannelPool(wrappedPool, tcpChannelPool); } - private ProxyAuthGenerator resolveProxyAuthGenerator(ProxyConfiguration proxyConfiguration) { - String username = proxyConfiguration.username(); - String password = proxyConfiguration.password(); - if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password)) { - return new BasicProxyAuthGenerator(username, password); - } - - return null; - } - @Override public void close() { log.trace(null, () -> "Closing channel pools"); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java deleted file mode 100644 index 4d8912994085..000000000000 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; - -import io.netty.handler.codec.http.HttpRequest; -import io.netty.util.CharsetUtil; -import java.net.URI; -import java.util.Base64; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; - -public class BasicProxyAuthGenerator implements ProxyAuthGenerator { - private final String username; - private final String password; - - public BasicProxyAuthGenerator(String username, String password) { - this.username = username; - this.password = password; - } - - @Override - public ProxyAuthScheme scheme() { - return ProxyAuthScheme.BASIC; - } - - @Override - public String generateAuthParams(URI proxyEndpoint) { - String authToken = String.format("%s:%s", this.username, this.password); - return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); - } -} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java index 0dafa642d6e4..cc53ed4da46a 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java @@ -49,30 +49,41 @@ public class Http1TunnelConnectionPool implements ChannelPool { private final ChannelPool delegate; private final SslContext sslContext; private final URI proxyAddress; - private final ProxyAuthGenerator proxyAuthGenerator; + private final String proxyUser; + private final String proxyPassword; private final URI remoteAddress; private final ChannelPoolHandler handler; private final InitHandlerSupplier initHandlerSupplier; private final NettyConfiguration nettyConfiguration; public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, + URI proxyAddress, String proxyUsername, String proxyPassword, URI remoteAddress, ChannelPoolHandler handler, NettyConfiguration nettyConfiguration) { this(eventLoop, delegate, sslContext, - proxyAddress, proxyAuthGenerator, remoteAddress, handler, + proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler, ProxyTunnelInitHandler::new, nettyConfiguration); } + public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, + URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler, + NettyConfiguration nettyConfiguration) { + this(eventLoop, delegate, sslContext, + proxyAddress, null, null, remoteAddress, handler, + ProxyTunnelInitHandler::new, nettyConfiguration); + + } + @SdkTestInternalApi Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress, + URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress, ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier, NettyConfiguration nettyConfiguration) { this.eventLoop = eventLoop; this.delegate = delegate; this.sslContext = sslContext; this.proxyAddress = proxyAddress; - this.proxyAuthGenerator = proxyAuthGenerator; + this.proxyUser = proxyUser; + this.proxyPassword = proxyPassword; this.remoteAddress = remoteAddress; this.handler = handler; this.initHandlerSupplier = initHandlerSupplier; @@ -127,7 +138,7 @@ private void setupChannel(Channel ch, Promise acquirePromise) { if (sslHandler != null) { ch.pipeline().addLast(sslHandler); } - ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyAddress, proxyAuthGenerator, remoteAddress, + ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress, tunnelEstablishedPromise)); tunnelEstablishedPromise.addListener((Future f) -> { if (f.isSuccess()) { @@ -169,10 +180,7 @@ private static boolean isTunnelEstablished(Channel ch) { @SdkTestInternalApi @FunctionalInterface interface InitHandlerSupplier { - ChannelHandler newInitHandler(ChannelPool sourcePool, - URI proxyAddress, - ProxyAuthGenerator authGenerator, - URI remoteAddress, + ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress, Promise tunnelInitFuture); } } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index 95d6156aa96f..c314d5c32ff3 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.http.nio.netty.internal; import com.sun.security.auth.module.Krb5LoginModule; -import io.netty.handler.codec.http.HttpRequest; import java.net.URI; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -65,12 +64,12 @@ public ProxyAuthScheme scheme() { } @Override - public String generateAuthParams(URI proxyEndpoint) { + public String generateAuthParams(SdkHttpRequest request) { try { Subject subject = getSubject(); byte[] token = Subject.doAs(subject, (PrivilegedExceptionAction) () -> { - GSSContext ctx = createGSSContext(getManager(), proxyEndpoint); + GSSContext ctx = createGSSContext(getManager(), request.getUri()); ctx.requestMutualAuth(true); return ctx.initSecContext(new byte[0], 0, 0); }); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java index b078500b09ac..3e0b2569f364 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -15,9 +15,8 @@ package software.amazon.awssdk.http.nio.netty.internal; -import io.netty.handler.codec.http.HttpRequest; -import java.net.URI; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; /** @@ -33,5 +32,5 @@ public interface ProxyAuthGenerator { /** * Generate the auth params for this request. */ - String generateAuthParams(URI proxyEndpoint); + String generateAuthParams(SdkHttpRequest request); } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index 277ed555dbc5..e6f309afbad3 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -28,9 +28,11 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; +import java.util.Base64; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -45,51 +47,32 @@ public final class ProxyTunnelInitHandler extends ChannelDuplexHandler { public static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyTunnelInitHandler.class); private final ChannelPool sourcePool; - private final URI proxyAddress; - private final ProxyAuthGenerator authGenerator; + private final String username; + private final String password; private final URI remoteHost; private final Promise initPromise; private final Supplier httpCodecSupplier; public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise) { - this(sourcePool, null, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); } public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise initPromise) { - this(sourcePool, null, null, null, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new); } @SdkTestInternalApi - public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, String proxyUsername, String proxyPassword, + public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword, URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { this.sourcePool = sourcePool; - this.proxyAddress = proxyAddress; this.remoteHost = remoteHost; this.initPromise = initPromise; - if (!StringUtils.isBlank(proxyPassword) && !StringUtils.isBlank(proxyPassword)) { - this.authGenerator = new BasicProxyAuthGenerator(proxyUsername, proxyPassword); - } else { - this.authGenerator = null; - } + this.username = prosyUsername; + this.password = proxyPassword; this.httpCodecSupplier = httpCodecSupplier; } - public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, - URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { - this.sourcePool = sourcePool; - this.proxyAddress = proxyAddress; - this.remoteHost = remoteHost; - this.initPromise = initPromise; - this.authGenerator = authGenerator; - this.httpCodecSupplier = httpCodecSupplier; - } - - public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, - URI remoteHost, Promise initPromise) { - this(sourcePool, proxyAddress, authGenerator, remoteHost, initPromise, HttpClientCodec::new); - } - @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); @@ -168,9 +151,10 @@ private HttpRequest connectRequest() { Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, uri); - if (authGenerator != null) { - String auth = String.format("%s %s", authGenerator.scheme().value(), authGenerator.generateAuthParams(proxyAddress)); - request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, auth); + if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) { + String authToken = String.format("%s:%s", this.username, this.password); + String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); + request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64)); } return request; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index 9e2ed53cd1e3..d43b404f3f5f 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -73,8 +73,6 @@ public class Http1TunnelConnectionPoolTest { private static final String PROXY_PASSWORD = "mypassword"; - private static final ProxyAuthGenerator basicAuth = new BasicProxyAuthGenerator(PROXY_USER, PROXY_PASSWORD); - @Mock private ChannelPool delegatePool; @@ -117,7 +115,7 @@ public static void teardown() { @Test public void tunnelAlreadyEstablished_doesNotAddInitHandler() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(true); @@ -129,7 +127,7 @@ public void tunnelAlreadyEstablished_doesNotAddInitHandler() { @Test(timeout = 1000) public void tunnelNotEstablished_addsInitHandler() throws InterruptedException { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(false); @@ -151,7 +149,7 @@ public void tunnelInitFails_acquireFutureFails() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -166,7 +164,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -176,7 +174,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { @Test public void acquireFromDelegatePoolFails_failsFuture() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom"))); @@ -199,7 +197,7 @@ public void sslContextProvided_andProxyUsingHttps_addsSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTPS_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -220,7 +218,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -233,7 +231,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { @Test public void release_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.release(mockChannel); verify(delegatePool).release(eq(mockChannel), any(Promise.class)); } @@ -241,7 +239,7 @@ public void release_releasedToDelegatePool() { @Test public void release_withGivenPromise_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); Promise mockPromise = mock(Promise.class); tunnelPool.release(mockChannel, mockPromise); verify(delegatePool).release(eq(mockChannel), eq(mockPromise)); @@ -250,7 +248,7 @@ public void release_withGivenPromise_releasedToDelegatePool() { @Test public void close_closesDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.close(); verify(delegatePool).close(); } @@ -259,32 +257,42 @@ public void close_closesDelegatePool() { public void proxyAuthProvided_addInitHandler_withAuth(){ TestInitHandlerData data = new TestInitHandlerData(); - Http1TunnelConnectionPool.InitHandlerSupplier supplier = - (srcPool, proxyEndpoint, proxyAuthGenerator, remoteAddr, initFuture) -> { + Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { initFuture.setSuccess(mockChannel); - data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint); + data.proxyUser(proxyUser); + data.proxyPassword(proxyPassword); return mock(ChannelHandler.class); }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, basicAuth, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); - // assertThat(data.proxyUser()).isEqualTo(PROXY_USER); - // assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); + assertThat(data.proxyUser()).isEqualTo(PROXY_USER); + assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); + } private static class TestInitHandlerData { - private String authHeader; + private String proxyUser; + private String proxyPassword; + + public void proxyUser(String proxyUser) { + this.proxyUser = proxyUser; + } + + public String proxyUser() { + return this.proxyUser; + } - public void authHeader(String authHeader) { - this.authHeader = authHeader; + public void proxyPassword(String proxyPassword) { + this.proxyPassword = proxyPassword; } - public String authHeader() { - return authHeader; + public String proxyPassword(){ + return this.proxyPassword; } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java index d7c7a3bc1506..839aff7c4be5 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; @@ -34,6 +33,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.testutils.FileUtils; public class NegotiateProxyAuthGeneratorTest { @@ -102,9 +103,13 @@ static void teardown() throws KrbException { void generateAuthParams_configValid_successfullyGeneratesToken() { NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config); - URI proxyEndpoint = URI.create("https://localhost:8192"); + SdkHttpRequest request = SdkHttpRequest.builder() + .protocol("http") + .host("localhost") + .method(SdkHttpMethod.GET) + .build(); - assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); + assertThat(authGenerator.generateAuthParams(request)).startsWith("YII"); } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 7828050bef26..9836a953bda9 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -95,8 +95,7 @@ public void addedToPipeline_addsCodec() { Supplier codecSupplier = () -> codec; when(mockCtx.name()).thenReturn("foo"); - ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, null, REMOTE_HOST, null, - codecSupplier); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, REMOTE_HOST, null, codecSupplier); handler.handlerAdded(mockCtx); verify(mockPipeline).addBefore(eq("foo"), eq(null), eq(codec)); @@ -203,7 +202,7 @@ public void handlerRemoved_removesCodec() { } @Test - public void handlerAdded_writesRequest_withoutAuth() { + public void handledAdded_writesRequest_withoutAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); @@ -220,7 +219,7 @@ public void handlerAdded_writesRequest_withoutAuth() { } @Test - public void handlerAdded_writesRequest_withAuth() { + public void handledAdded_writesRequest_withAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, PROXY_USER, PROXY_PASSWORD, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); From 81f440d127b913e43ff3277dfbc152e7b74099c4 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:29:07 -0700 Subject: [PATCH 03/12] Add basic auth impl (#7220) * Add basic auth impl * Checkstyle and dependency issues --- bom-internal/pom.xml | 14 +++- http-clients/netty-nio-client/pom.xml | 10 +++ .../internal/BasicProxyAuthGenerator.java | 50 +++++++++++++ .../internal/BasicProxyAuthGeneratorTest.java | 72 +++++++++++++++++++ pom.xml | 2 +- 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java create mode 100644 http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java diff --git a/bom-internal/pom.xml b/bom-internal/pom.xml index 78c85e86a17e..de6705135331 100644 --- a/bom-internal/pom.xml +++ b/bom-internal/pom.xml @@ -522,7 +522,19 @@ org.apache.kerby kerb-simplekdc - ${kerb-simplekdc.version} + ${kerby.version} + test + + + org.apache.kerby + kerb-client + ${kerby.version} + test + + + org.apache.kerby + kerb-core + ${kerby.version} test diff --git a/http-clients/netty-nio-client/pom.xml b/http-clients/netty-nio-client/pom.xml index 9988be3c5b18..eba24127eaa6 100644 --- a/http-clients/netty-nio-client/pom.xml +++ b/http-clients/netty-nio-client/pom.xml @@ -238,6 +238,16 @@ kerb-simplekdc test + + org.apache.kerby + kerb-client + test + + + org.apache.kerby + kerb-core + test + diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java new file mode 100644 index 000000000000..4efc98848497 --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import io.netty.util.CharsetUtil; +import java.util.Base64; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.utils.Validate; + +/** + * Auth param generator for Basic proxy authentication. + *

+ * See https://datatracker.ietf.org/doc/html/rfc7617. + */ +@SdkInternalApi +public class BasicProxyAuthGenerator implements ProxyAuthGenerator { + private final String username; + private final String password; + + public BasicProxyAuthGenerator(String username, String password) { + this.username = Validate.notBlank(username, "username must not be blank"); + this.password = Validate.notBlank(password, "password must not be blank"); + } + + @Override + public ProxyAuthScheme scheme() { + return ProxyAuthScheme.BASIC; + } + + @Override + public String generateAuthParams(SdkHttpRequest request) { + String authToken = String.format("%s:%s", this.username, this.password); + return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); + } +} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java new file mode 100644 index 000000000000..ba7d4bf5d4f1 --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; + +public class BasicProxyAuthGeneratorTest { + private static final String USERNAME = "user"; + private static final String PASSWORD = "pass"; + + private final BasicProxyAuthGenerator authGenerator = new BasicProxyAuthGenerator(USERNAME, PASSWORD); + + @ParameterizedTest(name = "username = {0}, password = {1}, expected error = {2}") + @MethodSource("invalidCtorParams") + void ctor_paramsInvalid_throws(String username, String password, String errorMessage) { + assertThatThrownBy(() -> new BasicProxyAuthGenerator(username, password)) + .hasMessageContaining(errorMessage); + } + + @Test + void scheme_returnsCorrectValue() { + assertThat(authGenerator.scheme()).isEqualTo(ProxyAuthScheme.BASIC); + } + + @Test + void generateAuthParams_generatedCorrectly() { + String expected = Base64.getEncoder() + .encodeToString(String.format("%s:%s", USERNAME, PASSWORD) + .getBytes(StandardCharsets.UTF_8)); + + assertThat(authGenerator.generateAuthParams(mock(SdkHttpRequest.class))).isEqualTo(expected); + } + + private static Stream invalidCtorParams() { + return Stream.of( + Arguments.of(null, null, "username"), + Arguments.of("", "", "username"), + Arguments.of(" ", "", "username"), + Arguments.of(" ", " ", "username"), + Arguments.of(null, PASSWORD, "username"), + Arguments.of("", PASSWORD, "username"), + Arguments.of(USERNAME, null, "password"), + Arguments.of(USERNAME, "", "password") + + ); + } +} diff --git a/pom.xml b/pom.xml index cee43145ce64..a6746e66b0cf 100644 --- a/pom.xml +++ b/pom.xml @@ -152,7 +152,7 @@ 1.17.5 1.3.0 1.5.4 - 2.0.3 + 2.0.3 3.1.2 From b2aaf34f5aa8d6872807241a567bb1321c34d334 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:29:13 -0700 Subject: [PATCH 04/12] Switch to AuthGenerator in tunnel pool (#7252) * Switch to AuthGenerator in tunnel pool Use the new AuthGenerator mechanism in the `Http1TunnelConnectionPool` and `AwaitCloseChannelPoolMap` classes. For now, supports only using BASIC auth; Kerberos will be added in a subsequent PR. * Allow empty username, pass Original impl allowed empty (e.g. whitespace) in username and pass for BASIC auth so preserve that behavior. --- .../internal/AwaitCloseChannelPoolMap.java | 13 ++++- .../internal/BasicProxyAuthGenerator.java | 8 +-- .../internal/Http1TunnelConnectionPool.java | 28 ++++------ .../internal/NegotiateProxyAuthGenerator.java | 7 ++- .../netty/internal/ProxyAuthGenerator.java | 4 +- .../internal/ProxyTunnelInitHandler.java | 42 ++++++++++----- .../internal/BasicProxyAuthGeneratorTest.java | 7 +-- .../Http1TunnelConnectionPoolTest.java | 54 ++++++++----------- .../NegotiateProxyAuthGeneratorTest.java | 11 ++-- .../internal/ProxyTunnelInitHandlerTest.java | 7 +-- 10 files changed, 92 insertions(+), 89 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index ff5c87e57038..83665f08b927 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -44,6 +44,7 @@ import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; +import software.amazon.awssdk.utils.StringUtils; /** * Implementation of {@link SdkChannelPoolMap} that awaits channel pools to be closed upon closing. @@ -143,7 +144,7 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { if (shouldUseProxyForHost(key)) { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, NOOP_HANDLER); baseChannelPool = new Http1TunnelConnectionPool(bootstrap.config().group().next(), tcpChannelPool, sslContext, - proxyAddress(key), proxyConfiguration.username(), proxyConfiguration.password(), + proxyAddress(key), resolveProxyAuthGenerator(proxyConfiguration), key, pipelineInitializer, configuration); } else { tcpChannelPool = new BetterSimpleChannelPool(bootstrap, pipelineInitializer); @@ -156,6 +157,16 @@ protected SimpleChannelPoolAwareChannelPool newPool(URI key) { return new SimpleChannelPoolAwareChannelPool(wrappedPool, tcpChannelPool); } + private ProxyAuthGenerator resolveProxyAuthGenerator(ProxyConfiguration proxyConfiguration) { + String username = proxyConfiguration.username(); + String password = proxyConfiguration.password(); + if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { + return new BasicProxyAuthGenerator(username, password); + } + + return null; + } + @Override public void close() { log.trace(null, () -> "Closing channel pools"); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java index 4efc98848497..36055cf0b0fe 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java @@ -16,9 +16,9 @@ package software.amazon.awssdk.http.nio.netty.internal; import io.netty.util.CharsetUtil; +import java.net.URI; import java.util.Base64; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.utils.Validate; @@ -33,8 +33,8 @@ public class BasicProxyAuthGenerator implements ProxyAuthGenerator { private final String password; public BasicProxyAuthGenerator(String username, String password) { - this.username = Validate.notBlank(username, "username must not be blank"); - this.password = Validate.notBlank(password, "password must not be blank"); + this.username = Validate.notEmpty(username, "username must not be empty"); + this.password = Validate.notEmpty(password, "password must not be empty"); } @Override @@ -43,7 +43,7 @@ public ProxyAuthScheme scheme() { } @Override - public String generateAuthParams(SdkHttpRequest request) { + public String generateAuthParams(URI proxyEndpoint) { String authToken = String.format("%s:%s", this.username, this.password); return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java index cc53ed4da46a..0dafa642d6e4 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java @@ -49,41 +49,30 @@ public class Http1TunnelConnectionPool implements ChannelPool { private final ChannelPool delegate; private final SslContext sslContext; private final URI proxyAddress; - private final String proxyUser; - private final String proxyPassword; + private final ProxyAuthGenerator proxyAuthGenerator; private final URI remoteAddress; private final ChannelPoolHandler handler; private final InitHandlerSupplier initHandlerSupplier; private final NettyConfiguration nettyConfiguration; public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, String proxyUsername, String proxyPassword, + URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress, ChannelPoolHandler handler, NettyConfiguration nettyConfiguration) { this(eventLoop, delegate, sslContext, - proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler, + proxyAddress, proxyAuthGenerator, remoteAddress, handler, ProxyTunnelInitHandler::new, nettyConfiguration); } - public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler, - NettyConfiguration nettyConfiguration) { - this(eventLoop, delegate, sslContext, - proxyAddress, null, null, remoteAddress, handler, - ProxyTunnelInitHandler::new, nettyConfiguration); - - } - @SdkTestInternalApi Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext, - URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress, + URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress, ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier, NettyConfiguration nettyConfiguration) { this.eventLoop = eventLoop; this.delegate = delegate; this.sslContext = sslContext; this.proxyAddress = proxyAddress; - this.proxyUser = proxyUser; - this.proxyPassword = proxyPassword; + this.proxyAuthGenerator = proxyAuthGenerator; this.remoteAddress = remoteAddress; this.handler = handler; this.initHandlerSupplier = initHandlerSupplier; @@ -138,7 +127,7 @@ private void setupChannel(Channel ch, Promise acquirePromise) { if (sslHandler != null) { ch.pipeline().addLast(sslHandler); } - ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress, + ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyAddress, proxyAuthGenerator, remoteAddress, tunnelEstablishedPromise)); tunnelEstablishedPromise.addListener((Future f) -> { if (f.isSuccess()) { @@ -180,7 +169,10 @@ private static boolean isTunnelEstablished(Channel ch) { @SdkTestInternalApi @FunctionalInterface interface InitHandlerSupplier { - ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress, + ChannelHandler newInitHandler(ChannelPool sourcePool, + URI proxyAddress, + ProxyAuthGenerator authGenerator, + URI remoteAddress, Promise tunnelInitFuture); } } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index c314d5c32ff3..df76b2a8a8ba 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -33,7 +33,6 @@ import org.ietf.jgss.Oid; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.utils.BinaryUtils; @@ -64,12 +63,12 @@ public ProxyAuthScheme scheme() { } @Override - public String generateAuthParams(SdkHttpRequest request) { + public String generateAuthParams(URI proxyEndpoint) { try { Subject subject = getSubject(); byte[] token = Subject.doAs(subject, (PrivilegedExceptionAction) () -> { - GSSContext ctx = createGSSContext(getManager(), request.getUri()); + GSSContext ctx = createGssContext(getManager(), proxyEndpoint); ctx.requestMutualAuth(true); return ctx.initSecContext(new byte[0], 0, 0); }); @@ -90,7 +89,7 @@ private Subject getSubject() { } } - private GSSContext createGSSContext(GSSManager manager, URI endpoint) { + private GSSContext createGssContext(GSSManager manager, URI endpoint) { try { String name = String.format("%s@%s", SERVICE_NAME, endpoint.getHost()); GSSName serverName = manager.createName(name, GSSName.NT_HOSTBASED_SERVICE); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java index 3e0b2569f364..eeb84fbdb6f5 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -15,8 +15,8 @@ package software.amazon.awssdk.http.nio.netty.internal; +import java.net.URI; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; /** @@ -32,5 +32,5 @@ public interface ProxyAuthGenerator { /** * Generate the auth params for this request. */ - String generateAuthParams(SdkHttpRequest request); + String generateAuthParams(URI proxyEndpoint); } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index e6f309afbad3..277ed555dbc5 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -28,11 +28,9 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; -import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; -import java.util.Base64; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -47,32 +45,51 @@ public final class ProxyTunnelInitHandler extends ChannelDuplexHandler { public static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyTunnelInitHandler.class); private final ChannelPool sourcePool; - private final String username; - private final String password; + private final URI proxyAddress; + private final ProxyAuthGenerator authGenerator; private final URI remoteHost; private final Promise initPromise; private final Supplier httpCodecSupplier; public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise) { - this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); } public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise initPromise) { - this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, null, null, remoteHost, initPromise, HttpClientCodec::new); } @SdkTestInternalApi - public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword, + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; this.remoteHost = remoteHost; this.initPromise = initPromise; - this.username = prosyUsername; - this.password = proxyPassword; + if (!StringUtils.isBlank(proxyPassword) && !StringUtils.isBlank(proxyPassword)) { + this.authGenerator = new BasicProxyAuthGenerator(proxyUsername, proxyPassword); + } else { + this.authGenerator = null; + } this.httpCodecSupplier = httpCodecSupplier; } + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { + this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; + this.remoteHost = remoteHost; + this.initPromise = initPromise; + this.authGenerator = authGenerator; + this.httpCodecSupplier = httpCodecSupplier; + } + + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise) { + this(sourcePool, proxyAddress, authGenerator, remoteHost, initPromise, HttpClientCodec::new); + } + @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); @@ -151,10 +168,9 @@ private HttpRequest connectRequest() { Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, uri); - if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) { - String authToken = String.format("%s:%s", this.username, this.password); - String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); - request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64)); + if (authGenerator != null) { + String auth = String.format("%s %s", authGenerator.scheme().value(), authGenerator.generateAuthParams(proxyAddress)); + request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, auth); } return request; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java index ba7d4bf5d4f1..b0294ea768c3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java @@ -17,8 +17,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.stream.Stream; @@ -26,7 +26,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; public class BasicProxyAuthGeneratorTest { @@ -53,15 +52,13 @@ void generateAuthParams_generatedCorrectly() { .encodeToString(String.format("%s:%s", USERNAME, PASSWORD) .getBytes(StandardCharsets.UTF_8)); - assertThat(authGenerator.generateAuthParams(mock(SdkHttpRequest.class))).isEqualTo(expected); + assertThat(authGenerator.generateAuthParams(URI.create("http://amazon.com"))).isEqualTo(expected); } private static Stream invalidCtorParams() { return Stream.of( Arguments.of(null, null, "username"), Arguments.of("", "", "username"), - Arguments.of(" ", "", "username"), - Arguments.of(" ", " ", "username"), Arguments.of(null, PASSWORD, "username"), Arguments.of("", PASSWORD, "username"), Arguments.of(USERNAME, null, "password"), diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index d43b404f3f5f..9e2ed53cd1e3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -73,6 +73,8 @@ public class Http1TunnelConnectionPoolTest { private static final String PROXY_PASSWORD = "mypassword"; + private static final ProxyAuthGenerator basicAuth = new BasicProxyAuthGenerator(PROXY_USER, PROXY_PASSWORD); + @Mock private ChannelPool delegatePool; @@ -115,7 +117,7 @@ public static void teardown() { @Test public void tunnelAlreadyEstablished_doesNotAddInitHandler() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(true); @@ -127,7 +129,7 @@ public void tunnelAlreadyEstablished_doesNotAddInitHandler() { @Test(timeout = 1000) public void tunnelNotEstablished_addsInitHandler() throws InterruptedException { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(false); @@ -149,7 +151,7 @@ public void tunnelInitFails_acquireFutureFails() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -164,7 +166,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -174,7 +176,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { @Test public void acquireFromDelegatePoolFails_failsFuture() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom"))); @@ -197,7 +199,7 @@ public void sslContextProvided_andProxyUsingHttps_addsSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTPS_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -218,7 +220,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -231,7 +233,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { @Test public void release_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.release(mockChannel); verify(delegatePool).release(eq(mockChannel), any(Promise.class)); } @@ -239,7 +241,7 @@ public void release_releasedToDelegatePool() { @Test public void release_withGivenPromise_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); Promise mockPromise = mock(Promise.class); tunnelPool.release(mockChannel, mockPromise); verify(delegatePool).release(eq(mockChannel), eq(mockPromise)); @@ -248,7 +250,7 @@ public void release_withGivenPromise_releasedToDelegatePool() { @Test public void close_closesDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.close(); verify(delegatePool).close(); } @@ -257,42 +259,32 @@ public void close_closesDelegatePool() { public void proxyAuthProvided_addInitHandler_withAuth(){ TestInitHandlerData data = new TestInitHandlerData(); - Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { + Http1TunnelConnectionPool.InitHandlerSupplier supplier = + (srcPool, proxyEndpoint, proxyAuthGenerator, remoteAddr, initFuture) -> { initFuture.setSuccess(mockChannel); - data.proxyUser(proxyUser); - data.proxyPassword(proxyPassword); + data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint); return mock(ChannelHandler.class); }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, basicAuth, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); - assertThat(data.proxyUser()).isEqualTo(PROXY_USER); - assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); - + // assertThat(data.proxyUser()).isEqualTo(PROXY_USER); + // assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); } private static class TestInitHandlerData { - private String proxyUser; - private String proxyPassword; - - public void proxyUser(String proxyUser) { - this.proxyUser = proxyUser; - } - - public String proxyUser() { - return this.proxyUser; - } + private String authHeader; - public void proxyPassword(String proxyPassword) { - this.proxyPassword = proxyPassword; + public void authHeader(String authHeader) { + this.authHeader = authHeader; } - public String proxyPassword(){ - return this.proxyPassword; + public String authHeader() { + return authHeader; } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java index 839aff7c4be5..d7c7a3bc1506 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; @@ -33,8 +34,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.testutils.FileUtils; public class NegotiateProxyAuthGeneratorTest { @@ -103,13 +102,9 @@ static void teardown() throws KrbException { void generateAuthParams_configValid_successfullyGeneratesToken() { NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config); - SdkHttpRequest request = SdkHttpRequest.builder() - .protocol("http") - .host("localhost") - .method(SdkHttpMethod.GET) - .build(); + URI proxyEndpoint = URI.create("https://localhost:8192"); - assertThat(authGenerator.generateAuthParams(request)).startsWith("YII"); + assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 9836a953bda9..7828050bef26 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -95,7 +95,8 @@ public void addedToPipeline_addsCodec() { Supplier codecSupplier = () -> codec; when(mockCtx.name()).thenReturn("foo"); - ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, REMOTE_HOST, null, codecSupplier); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, null, REMOTE_HOST, null, + codecSupplier); handler.handlerAdded(mockCtx); verify(mockPipeline).addBefore(eq("foo"), eq(null), eq(codec)); @@ -202,7 +203,7 @@ public void handlerRemoved_removesCodec() { } @Test - public void handledAdded_writesRequest_withoutAuth() { + public void handlerAdded_writesRequest_withoutAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); @@ -219,7 +220,7 @@ public void handledAdded_writesRequest_withoutAuth() { } @Test - public void handledAdded_writesRequest_withAuth() { + public void handlerAdded_writesRequest_withAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, PROXY_USER, PROXY_PASSWORD, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); From eea0b886967e4d95b189150c3d1b18d1e762b0fd Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:34:38 -0700 Subject: [PATCH 05/12] Support ProxyAuthScheme (#7260) * Support ProxyAuthScheme This commit Adds a `ProxyAuthScheme` configuration option in `ProxyConfiguration` and adds support for `NEGOTIATE` auth scheme. For backwards compatibility, if username and password are set on the config and the proxy auth scheme is *not* set, the client assumes `BASIC` auth scheme. If `NEGOTIATE` is configured, `username` and `password` are ignored. * Fix test --- .../http/nio/netty/ProxyConfiguration.java | 33 ++++ .../internal/AwaitCloseChannelPoolMap.java | 34 +++++ .../internal/NegotiateProxyAuthGenerator.java | 18 ++- .../internal/ProxyTunnelInitHandler.java | 12 +- .../nio/netty/ProxyConfigurationTest.java | 6 +- .../AwaitCloseChannelPoolMapTest.java | 142 +++++++++++++++--- .../Http1TunnelConnectionPoolTest.java | 7 +- .../NegotiateProxyAuthGeneratorTest.java | 78 ++++++---- .../internal/ProxyTunnelInitHandlerTest.java | 20 +++ 9 files changed, 288 insertions(+), 62 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java index 2c422fceb2b4..0e2592c32d6a 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java @@ -41,6 +41,7 @@ public final class ProxyConfiguration implements ToCopyableBuilder nonProxyHosts; private ProxyConfiguration(BuilderImpl builder) { @@ -56,6 +57,7 @@ private ProxyConfiguration(BuilderImpl builder) { this.port = resolvePort(builder, proxyConfigProvider); this.username = resolveUserName(builder, proxyConfigProvider); this.password = resolvePassword(builder, proxyConfigProvider); + this.proxyAuthScheme = builder.proxyAuthScheme; this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider); } @@ -151,6 +153,13 @@ public Set nonProxyHosts() { return Collections.unmodifiableSet(nonProxyHosts != null ? nonProxyHosts : Collections.emptySet()); } + /** + * @return The auth scheme to use to authenticate with the proxy. + */ + public ProxyAuthScheme proxyAuthScheme() { + return proxyAuthScheme; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -183,6 +192,10 @@ public boolean equals(Object o) { return false; } + if (proxyAuthScheme != null ? !proxyAuthScheme.equals(that.proxyAuthScheme) : that.proxyAuthScheme != null) { + return false; + } + return nonProxyHosts.equals(that.nonProxyHosts); } @@ -195,6 +208,7 @@ public int hashCode() { result = 31 * result + nonProxyHosts.hashCode(); result = 31 * result + (username != null ? username.hashCode() : 0); result = 31 * result + (password != null ? password.hashCode() : 0); + result = 31 * result + (proxyAuthScheme != null ? proxyAuthScheme.hashCode() : 0); return result; } @@ -243,6 +257,17 @@ public interface Builder extends CopyableBuilder { */ Builder nonProxyHosts(Set nonProxyHosts); + /** + * Configure the auth scheme to use to authenticate with the proxy. + *

+ * If unset and {@link #username(String)} and {@link #password(String)} are set, the client will + * assume {@link ProxyAuthScheme#BASIC} auth. + * + * @param proxyAuthScheme The auth scheme. + * @return This object for method chaining. + */ + Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme); + /** * Set the username used to authenticate with the proxy username. * @@ -293,6 +318,7 @@ private static final class BuilderImpl implements Builder { private String scheme = "http"; private String host; private int port = 0; + private ProxyAuthScheme proxyAuthScheme; private String username; private String password; private Set nonProxyHosts; @@ -310,6 +336,7 @@ private BuilderImpl(ProxyConfiguration proxyConfiguration) { this.port = proxyConfiguration.port; this.nonProxyHosts = proxyConfiguration.nonProxyHosts != null ? new HashSet<>(proxyConfiguration.nonProxyHosts) : null; + this.proxyAuthScheme = proxyConfiguration.proxyAuthScheme; this.username = proxyConfiguration.username; this.password = proxyConfiguration.password; } @@ -342,6 +369,12 @@ public Builder nonProxyHosts(Set nonProxyHosts) { return this; } + @Override + public Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme) { + this.proxyAuthScheme = proxyAuthScheme; + return this; + } + @Override public Builder username(String username) { this.username = username; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index 83665f08b927..d9441a2f6ee2 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -36,10 +36,12 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import javax.security.auth.login.Configuration; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.http.Protocol; import software.amazon.awssdk.http.ProtocolNegotiation; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.http.nio.netty.ProxyConfiguration; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; @@ -88,6 +90,8 @@ public void channelCreated(Channel ch) throws Exception { private final SslContextProvider sslContextProvider; private final Boolean useNonBlockingDnsResolver; + private final Configuration negotiateAuthConfig; + private AwaitCloseChannelPoolMap(Builder builder, Function createBootStrapProvider) { this.configuration = builder.configuration; this.protocol = builder.protocol; @@ -100,6 +104,7 @@ private AwaitCloseChannelPoolMap(Builder builder, Function) () -> { GSSContext ctx = createGssContext(getManager(), proxyEndpoint); - ctx.requestMutualAuth(true); - return ctx.initSecContext(new byte[0], 0, 0); + try { + ctx.requestMutualAuth(true); + return ctx.initSecContext(new byte[0], 0, 0); + } finally { + ctx.dispose(); + } }); return BinaryUtils.toBase64(token); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index 277ed555dbc5..aeda02f02e0f 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -67,7 +67,7 @@ public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, String p this.proxyAddress = proxyAddress; this.remoteHost = remoteHost; this.initPromise = initPromise; - if (!StringUtils.isBlank(proxyPassword) && !StringUtils.isBlank(proxyPassword)) { + if (!StringUtils.isBlank(proxyUsername) && !StringUtils.isBlank(proxyPassword)) { this.authGenerator = new BasicProxyAuthGenerator(proxyUsername, proxyPassword); } else { this.authGenerator = null; @@ -94,7 +94,15 @@ public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAut public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); pipeline.addBefore(ctx.name(), null, httpCodecSupplier.get()); - HttpRequest connectRequest = connectRequest(); + + HttpRequest connectRequest; + try { + connectRequest = connectRequest(); + } catch (Throwable t) { + handleConnectRequestFailure(ctx, t); + return; + } + ctx.channel().writeAndFlush(connectRequest).addListener(f -> { if (!f.isSuccess()) { handleConnectRequestFailure(ctx, f.cause()); diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java index 06d57c1aa7d2..36cc02d57c26 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java @@ -185,7 +185,11 @@ private void setRandomValue(Object o, Method setter) throws InvocationTargetExce setter.invoke(o, randomSet()); } else if (Boolean.class.equals(paramClass)) { setter.invoke(o, RNG.nextBoolean()); - } else { + } else if (ProxyAuthScheme.class.equals(paramClass)) { + ProxyAuthScheme authScheme = ProxyAuthScheme.values()[RNG.nextInt(ProxyAuthScheme.values().length)]; + setter.invoke(o, authScheme); + } + else { throw new RuntimeException("Don't know how create random value for type " + paramClass); } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java index a1d4b9781f35..f1b80597d3c4 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java @@ -23,45 +23,83 @@ import static software.amazon.awssdk.http.SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS; import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TLS_KEY_MANAGERS_PROVIDER; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.WireMockServer; import io.netty.channel.Channel; import io.netty.channel.pool.ChannelPool; import io.netty.handler.ssl.SslProvider; -import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import java.net.Socket; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; -import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; import org.apache.commons.lang3.RandomStringUtils; -import org.junit.After; -import org.junit.Rule; -import org.junit.Test; +import org.apache.kerby.kerberos.kerb.client.KrbClient; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import software.amazon.awssdk.http.Protocol; import software.amazon.awssdk.http.ProtocolNegotiation; import software.amazon.awssdk.http.TlsKeyManagersProvider; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.http.nio.netty.ProxyConfiguration; import software.amazon.awssdk.http.nio.netty.RecordingNetworkTrafficListener; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.utils.AttributeMap; public class AwaitCloseChannelPoolMapTest { + private static final String KRB5_PROP = "java.security.krb5.conf"; + private static final RecordingNetworkTrafficListener recorder = new RecordingNetworkTrafficListener(); - private final RecordingNetworkTrafficListener recorder = new RecordingNetworkTrafficListener(); + private static WireMockServer mockProxy; + + private static Path tempDir; + private static Path keytabFile; + private static Path ccacheFile; + private static int port; + + private static SimpleKdcServer kdc; + private static String krb5PropSave; + + private static Configuration negotiateAuthConfig; private AwaitCloseChannelPoolMap channelPoolMap; - @Rule - public WireMockRule mockProxy = new WireMockRule(wireMockConfig() - .dynamicPort() - .networkTrafficListener(recorder)); + @BeforeAll + public static void setup() throws Exception { + mockProxy = new WireMockServer(wireMockConfig().dynamicPort().networkTrafficListener(recorder)); + mockProxy.start(); + + setupMockKerberos(); + } + + @AfterAll + public static void teardown() throws Exception { + if (krb5PropSave != null) { + System.setProperty(KRB5_PROP, krb5PropSave); + } else { + System.clearProperty(KRB5_PROP); + } + mockProxy.stop(); + kdc.stop(); + } - @After + @AfterEach public void methodTeardown() { if (channelPoolMap != null) { channelPoolMap.close(); @@ -71,6 +109,56 @@ public void methodTeardown() { recorder.reset(); } + private static void setupMockKerberos() throws Exception { + tempDir = Files.createTempDirectory(null); + keytabFile = tempDir.resolve("keytab"); + ccacheFile = tempDir.resolve("ccache"); + + try (Socket freePort = new Socket()) { + freePort.setReuseAddress(true); + freePort.bind(new InetSocketAddress(0)); + port = freePort.getLocalPort(); + + kdc = new SimpleKdcServer(); + kdc.setKdcRealm("EXAMPLE.COM"); + kdc.setKdcHost("localhost"); + kdc.setWorkDir(tempDir.toFile()); + kdc.setKdcTcpPort(port); + kdc.setAllowUdp(false); + kdc.init(); + + krb5PropSave = System.getProperty(KRB5_PROP); + + System.setProperty(KRB5_PROP, tempDir.resolve("krb5.conf").toAbsolutePath().toString()); + kdc.start(); + + kdc.createPrincipal("alice@EXAMPLE.COM", "alicePassword"); + kdc.createAndExportPrincipals(keytabFile.toFile(), "HTTP/localhost@EXAMPLE.COM"); + + // initialize the ticket cache + KrbClient krbClient = kdc.getKrbClient(); + TgtTicket tgt = krbClient.requestTgt("alice@EXAMPLE.COM", "alicePassword"); + krbClient.storeTicket(tgt, ccacheFile.toFile()); + + // Override config so we look at the testing cache instead of the real system cache + negotiateAuthConfig = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("ticketCache", ccacheFile.toAbsolutePath().toString()); + opts.put("refreshKrb5Config", "true"); + opts.put("doNotPrompt", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + } + } + @Test public void close_underlyingPoolsShouldBeClosed() { channelPoolMap = AwaitCloseChannelPoolMap.builder() @@ -216,13 +304,16 @@ public void usingProxy_noSchemeGiven_defaultsToHttp() { assertThat(requests).contains("CONNECT some-awesome-service:443"); } - @Test - public void usingProxy_withAuth() { + @ParameterizedTest + @MethodSource("proxyAuthTestParams") + public void usingProxy_authHeaderCorrect(ProxyAuthScheme authScheme, String username, String password, + String proxyAuthHeader) { ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder() .host("localhost") .port(mockProxy.port()) - .username("myuser") - .password("mypassword") + .proxyAuthScheme(authScheme) + .username(username) + .password(password) .build(); channelPoolMap = AwaitCloseChannelPoolMap.builder() @@ -233,6 +324,7 @@ public void usingProxy_withAuth() { .protocol(Protocol.HTTP1_1) .maxStreams(100) .sslProvider(SslProvider.OPENSSL) + .negotiateAuthConfig(negotiateAuthConfig) .build(); SimpleChannelPoolAwareChannelPool simpleChannelPoolAwareChannelPool = channelPoolMap.newPool( @@ -244,9 +336,11 @@ public void usingProxy_withAuth() { assertThat(requests).contains("CONNECT some-awesome-service:443"); - String authB64 = Base64.getEncoder().encodeToString("myuser:mypassword".getBytes(CharsetUtil.UTF_8)); - String authHeaderValue = String.format("Basic %s", authB64); - assertThat(requests).contains(String.format("proxy-authorization: %s", authHeaderValue)); + if (proxyAuthHeader == null) { + assertThat(requests).doesNotContain("proxy-authorization:"); + } else { + assertThat(requests).contains(String.format("proxy-authorization: %s", proxyAuthHeader)); + } } @Test @@ -309,4 +403,14 @@ public void releaseChannel_autoReadEnabled() { assertThat(channel.config().isAutoRead()).isTrue(); } + private static Stream proxyAuthTestParams() { + return Stream.of( + Arguments.of(null, null, null, null), + Arguments.of(null, "user", "pass", "Basic dXNlcjpwYXNz"), + Arguments.of(ProxyAuthScheme.BASIC, "user", "pass", "Basic dXNlcjpwYXNz"), + Arguments.of(ProxyAuthScheme.NEGOTIATE, null, null, "Negotiate YII"), + Arguments.of(ProxyAuthScheme.NEGOTIATE, "user", "pass", "Negotiate YII") + + ); + } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index 9e2ed53cd1e3..b61ac05e0b10 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -42,6 +42,8 @@ import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; import java.util.concurrent.CountDownLatch; import javax.net.ssl.SSLEngine; @@ -271,8 +273,9 @@ public void proxyAuthProvided_addInitHandler_withAuth(){ tunnelPool.acquire().awaitUninterruptibly(); - // assertThat(data.proxyUser()).isEqualTo(PROXY_USER); - // assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); + String expectedAuthHeader = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD) + .getBytes(StandardCharsets.UTF_8)); + assertThat(data.authHeader()).isEqualTo(expectedAuthHeader); } private static class TestInitHandlerData { diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java index d7c7a3bc1506..31b02867b6e7 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -37,12 +37,14 @@ import software.amazon.awssdk.testutils.FileUtils; public class NegotiateProxyAuthGeneratorTest { + private static final String KRB5_PROP = "java.security.krb5.conf"; private static Path tempDir; private static Path keytabFile; private static Path ccacheFile; private static int port; private static SimpleKdcServer kdc; + private static String krb5PropSave; private static Configuration config; @@ -56,44 +58,56 @@ static void setup() throws IOException, KrbException { freePort.setReuseAddress(true); freePort.bind(new InetSocketAddress(0)); port = freePort.getLocalPort(); - } - kdc = new SimpleKdcServer(); - kdc.setKdcRealm("EXAMPLE.COM"); - kdc.setKdcHost("localhost"); - kdc.setWorkDir(tempDir.toFile()); - kdc.setKdcTcpPort(port); - kdc.init(); - kdc.start(); - - kdc.createPrincipal("alice@EXAMPLE.COM", "alicePassword"); - kdc.createAndExportPrincipals(keytabFile.toFile(), "HTTP/localhost@EXAMPLE.COM"); - - // initialize the ticket cache - KrbClient krbClient = kdc.getKrbClient(); - TgtTicket tgt = krbClient.requestTgt("alice@EXAMPLE.COM", "alicePassword"); - krbClient.storeTicket(tgt, ccacheFile.toFile()); - - // Override config so we look at the testing cache instead of the real system cache - config = new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - Map opts = new HashMap<>(); - opts.put("useTicketCache", "true"); - opts.put("ticketCache", ccacheFile.toAbsolutePath().toString()); - opts.put("doNotPrompt", "true"); - return new AppConfigurationEntry[] { - new AppConfigurationEntry( - "com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) - }; - } - }; + kdc = new SimpleKdcServer(); + kdc.setKdcRealm("EXAMPLE.COM"); + kdc.setKdcHost("localhost"); + kdc.setWorkDir(tempDir.toFile()); + kdc.setKdcTcpPort(port); + kdc.setAllowUdp(false); + kdc.init(); + + krb5PropSave = System.getProperty(KRB5_PROP); + + System.setProperty(KRB5_PROP, tempDir.resolve("krb5.conf").toAbsolutePath().toString()); + + kdc.start(); + + kdc.createPrincipal("alice@EXAMPLE.COM", "alicePassword"); + kdc.createAndExportPrincipals(keytabFile.toFile(), "HTTP/localhost@EXAMPLE.COM"); + + // initialize the ticket cache + KrbClient krbClient = kdc.getKrbClient(); + TgtTicket tgt = krbClient.requestTgt("alice@EXAMPLE.COM", "alicePassword"); + krbClient.storeTicket(tgt, ccacheFile.toFile()); + + // Override config so we look at the testing cache instead of the real system cache + config = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("ticketCache", ccacheFile.toAbsolutePath().toString()); + opts.put("doNotPrompt", "true"); + opts.put("refreshKrb5Config", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + } } @AfterAll static void teardown() throws KrbException { + if (krb5PropSave != null) { + System.setProperty(KRB5_PROP, krb5PropSave); + } else { + System.clearProperty(KRB5_PROP); + } kdc.stop(); FileUtils.cleanUpTestDirectory(tempDir); } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 7828050bef26..143cc174701f 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.http.nio.netty.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -45,6 +46,7 @@ import java.io.IOException; import java.net.URI; import java.util.Base64; +import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import org.junit.AfterClass; import org.junit.Before; @@ -53,6 +55,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; /** * Unit tests for {@link ProxyTunnelInitHandler}. @@ -239,6 +242,23 @@ public void handlerAdded_writesRequest_withAuth() { assertThat(requestCaptor.getValue()).isEqualTo(expectedRequest); } + @Test + public void handlerAdded_authParamsGeneratorThrows_failsFuture() { + ProxyAuthGenerator authGenerator = mock(ProxyAuthGenerator.class); + when(authGenerator.scheme()).thenReturn(ProxyAuthScheme.BASIC); + when(authGenerator.generateAuthParams(any(URI.class))).thenThrow(new RuntimeException("auth generator error")); + + Promise promise = GROUP.next().newPromise(); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, URI.create("https://amazon.com"), + authGenerator, + REMOTE_HOST, + promise); + handler.handlerAdded(mockCtx); + + assertThatThrownBy(promise::get).hasMessageContaining("Unable to send CONNECT request to proxy") + .hasRootCauseMessage("auth generator error"); + } + private void successResponse(ProxyTunnelInitHandler handler) { DefaultHttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); handler.channelRead(mockCtx, resp); From 4675fdacd1d4d59108004bf00f90029f2babd0d2 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:17:10 -0700 Subject: [PATCH 06/12] Validate BASIC scheme options at build time (#7277) --- .../http/nio/netty/ProxyConfiguration.java | 12 ++++ .../nio/netty/ProxyConfigurationTest.java | 62 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java index 2d0433b2169b..79697dbf9ebe 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java @@ -59,6 +59,14 @@ private ProxyConfiguration(BuilderImpl builder) { this.password = resolvePassword(builder, proxyConfigProvider); this.proxyAuthScheme = builder.proxyAuthScheme; this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider); + validateProxyAuthConfig(proxyAuthScheme, username, password); + } + + private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, String username, String password) { + if (proxyAuthScheme == ProxyAuthScheme.BASIC + && (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))) { + throw new IllegalArgumentException("username and password must be configured when using BASIC proxy auth"); + } } private static Set resolveNonProxyHosts(BuilderImpl builder, ProxyConfigProvider proxyConfigProvider) { @@ -266,6 +274,10 @@ public interface Builder extends CopyableBuilder { *

* If unset and {@link #username(String)} and {@link #password(String)} are set, the client will * assume {@link ProxyAuthScheme#BASIC} auth. + *

+ * If set to {@link ProxyAuthScheme#BASIC}, {@link #username(String)} and {@link #password(String)} must also be + * configured (directly, or resolved from system properties or environment variables), otherwise + * {@link Builder#build()} throws {@link IllegalArgumentException}. * * @param proxyAuthScheme The auth scheme. * @return This object for method chaining. diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java index 36cc02d57c26..b15b4c951db7 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.http.nio.netty; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -147,6 +148,67 @@ void setNonProxyHostsToNull_createsEmptySet() { assertThat(cfg.nonProxyHosts()).isEmpty(); } + @Test + void build_basicAuthSchemeWithoutCredentials_throws() { + ProxyConfiguration.Builder builder = ProxyConfiguration.builder() + .host("localhost") + .port(8888) + .proxyAuthScheme(ProxyAuthScheme.BASIC); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("username and password must be configured"); + } + + @Test + void build_basicAuthSchemeWithoutPassword_throws() { + ProxyConfiguration.Builder builder = ProxyConfiguration.builder() + .host("localhost") + .port(8888) + .proxyAuthScheme(ProxyAuthScheme.BASIC) + .username("user"); + + assertThatThrownBy(builder::build) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("username and password must be configured"); + } + + @Test + void build_basicAuthSchemeWithCredentials_doesNotThrow() { + ProxyConfiguration cfg = ProxyConfiguration.builder() + .host("localhost") + .port(8888) + .proxyAuthScheme(ProxyAuthScheme.BASIC) + .username("user") + .password("pass") + .build(); + + assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.BASIC); + } + + @Test + void build_basicAuthSchemeWithSystemPropertyCredentials_doesNotThrow() { + setHttpProxyProperties(); + + ProxyConfiguration cfg = ProxyConfiguration.builder() + .proxyAuthScheme(ProxyAuthScheme.BASIC) + .build(); + + assertThat(cfg.username()).isEqualTo(TEST_USER); + assertThat(cfg.password()).isEqualTo(TEST_PASSWORD); + } + + @Test + void build_negotiateAuthSchemeWithoutCredentials_doesNotThrow() { + ProxyConfiguration cfg = ProxyConfiguration.builder() + .host("localhost") + .port(8888) + .proxyAuthScheme(ProxyAuthScheme.NEGOTIATE) + .build(); + + assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.NEGOTIATE); + } + @Test void toBuilderModified_doesNotModifySource() { ProxyConfiguration original = allPropertiesSetConfig(); From d4ebccf993e7382f577d131bb4de10088a93f60f Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:14:16 -0700 Subject: [PATCH 07/12] Improve error message when token gen fails (#7278) --- .../internal/NegotiateProxyAuthGenerator.java | 13 +++++++-- .../NegotiateProxyAuthGeneratorTest.java | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index 7432bf45e197..41cb5a3c37ba 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -81,7 +81,12 @@ public String generateAuthParams(URI proxyEndpoint) { return BinaryUtils.toBase64(token); } catch (PrivilegedActionException e) { - throw new RuntimeException("Unable to generate token", e); + throw new RuntimeException(String.format("Unable to generate SPNEGO token for Negotiate proxy authentication " + + "with '%s@%s'. This can happen when a service ticket for the proxy " + + "cannot be obtained from the KDC, e.g. because the ticket-granting " + + "ticket has expired (renew with 'kinit') or the proxy host does not " + + "match its Kerberos service principal name.", + SERVICE_NAME, proxyEndpoint.getHost()), e); } } @@ -91,7 +96,11 @@ private Subject getSubject() { loginContext.login(); return loginContext.getSubject(); } catch (LoginException e) { - throw new RuntimeException("Unable to perform login", e); + throw new RuntimeException("Unable to perform Kerberos login for Negotiate proxy authentication. This " + + "typically means the Kerberos ticket cache is missing, expired, or not readable. " + + "Ensure a valid ticket-granting ticket exists (e.g., by running 'kinit'), and that " + + "the cache is at the expected location (see the KRB5CCNAME environment variable). " + + "Verify with 'klist'.", e); } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java index 31b02867b6e7..218630a46ac4 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.http.nio.netty.internal; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.IOException; import java.net.InetSocketAddress; @@ -121,4 +122,30 @@ void generateAuthParams_configValid_successfullyGeneratesToken() { assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); } + @Test + void generateAuthParams_ticketCacheMissing_failsWithActionableMessage() { + Configuration missingCacheConfig = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("ticketCache", tempDir.resolve("nonexistent-cache").toAbsolutePath().toString()); + opts.put("doNotPrompt", "true"); + opts.put("refreshKrb5Config", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + + NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(missingCacheConfig); + + assertThatThrownBy(() -> authGenerator.generateAuthParams(URI.create("https://localhost:8192"))) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("kinit") + .hasMessageContaining("ticket cache"); + } + } From bdf60d50e1c31086cfcacb6cd0f0995ba329a223 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:18:00 -0700 Subject: [PATCH 08/12] Remove inert mutual auth request and javadoc-only import (#7281) requestMutualAuth(true) asked for mutual authentication that was never established: the proxy's response token is never consumed, so there is nothing to verify it against. Preemptive single-leg Negotiate cannot verify it either, so the call is dropped rather than wired up. The com.sun.security.auth.module.Krb5LoginModule import existed only to satisfy a javadoc {@link}. Referring to the class by name in {@code} instead keeps the documentation while dropping a compile-time reference to a JDK-implementation-specific class. --- .../http/nio/netty/internal/NegotiateProxyAuthGenerator.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index 41cb5a3c37ba..0866073946c2 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -15,7 +15,6 @@ package software.amazon.awssdk.http.nio.netty.internal; -import com.sun.security.auth.module.Krb5LoginModule; import java.net.URI; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -72,7 +71,6 @@ public String generateAuthParams(URI proxyEndpoint) { byte[] token = Subject.doAs(subject, (PrivilegedExceptionAction) () -> { GSSContext ctx = createGssContext(getManager(), proxyEndpoint); try { - ctx.requestMutualAuth(true); return ctx.initSecContext(new byte[0], 0, 0); } finally { ctx.dispose(); @@ -124,7 +122,7 @@ private static GSSManager getManager() { * Create a generic {@link Configuration} that instructs the Kerberos login module to simply look in the ticket cache, and * not to prompt for passwords. *

- * See javadoc for {@link Krb5LoginModule} for additional info on the configuration options. + * See javadoc for {@code com.sun.security.auth.module.Krb5LoginModule} for additional info on the configuration options. */ private static Configuration createDefaultConfig() { return new Configuration() { From 42be292d5284b6a84edce1c98e49073499f5b036 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Mon, 17 Aug 2026 14:14:38 -0700 Subject: [PATCH 09/12] Improvements for `NEGOTIATE` usage (#7285) * Document the Negotiate proxy auth prerequisites NEGOTIATE depends on ambient, expiring, host-level state rather than on anything the customer passes to the builder, and none of that was documented. Spell out on the enum constant that credentials come from the ticket cache and never from a prompt or keytab, that a missing or expired ticket is not detected at build time and instead fails when a proxy connection is established, that the service principal is derived from the configured proxy host so an IP literal will not work, and that the JDK's GSS and JAAS modules must be present in the runtime image. Also note on the builder setter that username and password are ignored for NEGOTIATE, so credentials left in place while switching schemes are not silently assumed to be in use. * Warn when proxy credentials are ignored by NEGOTIATE NEGOTIATE authenticates from the Kerberos ticket cache, so a username and password configured alongside it are dead configuration. Switching an existing Basic proxy configuration over to NEGOTIATE and leaving the credentials in place therefore looks like it still uses them, with no signal either way. Warn at build() rather than reject, and only when the credentials were set directly on the builder: values resolved from system properties or environment variables may not be under the caller's control, so warning about those would be noise they cannot act on. --- .../http/nio/netty/ProxyAuthScheme.java | 11 ++++-- .../http/nio/netty/ProxyConfiguration.java | 23 ++++++++++++ .../nio/netty/ProxyConfigurationTest.java | 35 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java index 05719c612c02..8d677d6d2900 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java @@ -23,12 +23,19 @@ @SdkPublicApi public enum ProxyAuthScheme { /** - * Basic authentication. + * Basic authentication, as defined by RFC 7617. Requires a + * username and password. */ BASIC("Basic"), /** - * Kerberos authentication. + * Kerberos authentication, using SPNEGO as defined by + * RFC 4559. + *

+ * Credentials are read from the environment Kerberos ticket cache. The client never prompts for a password and never reads a + * keytab, so the environment must already hold a valid ticket-granting ticket, typically obtained by running + * {@code kinit} and verifiable with {@code klist}. The cache location follows the usual Kerberos conventions, including + * the {@code KRB5CCNAME} environment variable. Any username and password configured on the proxy are ignored. */ NEGOTIATE("Negotiate"), ; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java index 79697dbf9ebe..7cf332471660 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java @@ -19,6 +19,7 @@ import java.util.HashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; import software.amazon.awssdk.utils.ProxyConfigProvider; import software.amazon.awssdk.utils.ProxyEnvironmentSetting; import software.amazon.awssdk.utils.ProxySystemSetting; @@ -34,6 +35,8 @@ */ @SdkPublicApi public final class ProxyConfiguration implements ToCopyableBuilder { + private static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyConfiguration.class); + private final Boolean useSystemPropertyValues; private final Boolean useEnvironmentVariablesValues; private final String scheme; @@ -60,6 +63,7 @@ private ProxyConfiguration(BuilderImpl builder) { this.proxyAuthScheme = builder.proxyAuthScheme; this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider); validateProxyAuthConfig(proxyAuthScheme, username, password); + warnOnIgnoredCredentials(builder); } private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, String username, String password) { @@ -69,6 +73,21 @@ private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, Str } } + /** + * NEGOTIATE reads its credentials from the Kerberos ticket cache, so a username and password are dead configuration. Warn + * rather than fail, and only when they were set directly on this builder: values resolved from system properties or + * environment variables may not be under the caller's control, and warning about those would be noise. + */ + private static void warnOnIgnoredCredentials(BuilderImpl builder) { + if (builder.proxyAuthScheme == ProxyAuthScheme.NEGOTIATE + && (builder.username != null || builder.password != null)) { + log.warn(null, () -> "A proxy username and/or password was configured alongside the " + + ProxyAuthScheme.NEGOTIATE + " proxy auth scheme, and will be ignored. " + + ProxyAuthScheme.NEGOTIATE + " authenticates using the Kerberos ticket cache. Configure " + + ProxyAuthScheme.BASIC + " to authenticate with a username and password instead."); + } + } + private static Set resolveNonProxyHosts(BuilderImpl builder, ProxyConfigProvider proxyConfigProvider) { if (builder.nonProxyHosts != null || proxyConfigProvider == null) { return builder.nonProxyHosts; @@ -278,6 +297,10 @@ public interface Builder extends CopyableBuilder { * If set to {@link ProxyAuthScheme#BASIC}, {@link #username(String)} and {@link #password(String)} must also be * configured (directly, or resolved from system properties or environment variables), otherwise * {@link Builder#build()} throws {@link IllegalArgumentException}. + *

+ * If set to {@link ProxyAuthScheme#NEGOTIATE}, credentials come from the Kerberos ticket cache rather than from this + * configuration, and any configured username and password are ignored. See {@link ProxyAuthScheme#NEGOTIATE} for the + * environment it requires and for how a missing or expired ticket surfaces. * * @param proxyAuthScheme The auth scheme. * @return This object for method chaining. diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java index b15b4c951db7..2157676c5b7e 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java @@ -24,9 +24,11 @@ import java.util.Random; import java.util.Set; import java.util.stream.Stream; +import org.apache.logging.log4j.Level; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.testutils.LogCaptor; /** * Tests for {@link ProxyConfiguration}. @@ -209,6 +211,39 @@ void build_negotiateAuthSchemeWithoutCredentials_doesNotThrow() { assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.NEGOTIATE); } + @Test + void build_negotiateAuthSchemeWithCredentials_warnsAndKeepsBuilding() { + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + ProxyConfiguration cfg = ProxyConfiguration.builder() + .host("localhost") + .port(8888) + .proxyAuthScheme(ProxyAuthScheme.NEGOTIATE) + .username(TEST_USER) + .password(TEST_PASSWORD) + .build(); + + assertThat(cfg.proxyAuthScheme()).isEqualTo(ProxyAuthScheme.NEGOTIATE); + assertThat(logCaptor.loggedEvents()).singleElement() + .satisfies(event -> assertThat(event.getMessage().getFormattedMessage()) + .contains("NEGOTIATE") + .contains("will be ignored")); + } + } + + @Test + void build_negotiateAuthSchemeWithSystemPropertyCredentials_doesNotWarn() { + setHttpProxyProperties(); + + try (LogCaptor logCaptor = LogCaptor.create(Level.WARN)) { + ProxyConfiguration.builder() + .proxyAuthScheme(ProxyAuthScheme.NEGOTIATE) + .build(); + + // Credentials the caller did not set here are not their mistake to fix, so warning about them would be noise. + assertThat(logCaptor.loggedEvents()).isEmpty(); + } + } + @Test void toBuilderModified_doesNotModifySource() { ProxyConfiguration original = allPropertiesSetConfig(); From 4d727bfe75c6650432e1721cd4b5426f57c52c64 Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:54:10 -0700 Subject: [PATCH 10/12] Move Kerberos proxy auth off the Netty event loop (#7280) Generating a SPNEGO token performs a JAAS login and may make a blocking TGS request to the KDC. That ran on the Netty event loop during proxy tunnel setup, so a slow or unreachable KDC stalled every other channel assigned to that loop, and SDK timeouts could not unpark the thread. ProxyAuthGenerator now returns a CompletableFuture, so the contract states that generating params may be slow and must not complete on the caller's thread. Basic auth completes inline and stays on the existing synchronous path; the handler only hops threads when the future is not already done. AwaitCloseChannelPoolMap creates the executor the Negotiate generator runs on, and shuts it down when it closes, so the resource is created and released in the same place. It is a single daemon thread, created only when NEGOTIATE is configured: the goal is to keep blocking work off the event loops, not to parallelize it. The generator itself is resolved once per client rather than once per remote host. --- .../internal/AwaitCloseChannelPoolMap.java | 41 +++++++++++++- .../internal/BasicProxyAuthGenerator.java | 6 +- .../internal/NegotiateProxyAuthGenerator.java | 25 +++++++-- .../netty/internal/ProxyAuthGenerator.java | 7 ++- .../internal/ProxyTunnelInitHandler.java | 56 +++++++++++++++++-- .../internal/BasicProxyAuthGeneratorTest.java | 2 +- .../Http1TunnelConnectionPoolTest.java | 2 +- .../NegotiateProxyAuthGeneratorTest.java | 45 +++++++++++++-- .../internal/ProxyTunnelInitHandlerTest.java | 52 ++++++++++++++++- 9 files changed, 211 insertions(+), 25 deletions(-) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index d9441a2f6ee2..d2f74cd8dfa9 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -32,6 +32,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; @@ -47,6 +50,7 @@ import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; import software.amazon.awssdk.utils.StringUtils; +import software.amazon.awssdk.utils.ThreadFactoryBuilder; /** * Implementation of {@link SdkChannelPoolMap} that awaits channel pools to be closed upon closing. @@ -91,6 +95,8 @@ public void channelCreated(Channel ch) throws Exception { private final Boolean useNonBlockingDnsResolver; private final Configuration negotiateAuthConfig; + private final ProxyAuthGenerator proxyAuthGenerator; + private final ExecutorService proxyAuthExecutor; private AwaitCloseChannelPoolMap(Builder builder, Function createBootStrapProvider) { this.configuration = builder.configuration; @@ -105,6 +111,10 @@ private AwaitCloseChannelPoolMap(Builder builder, Function channelPools = pools().values(); super.close(); + if (proxyAuthExecutor != null) { + proxyAuthExecutor.shutdownNow(); + } + try { CompletableFuture.allOf(channelPools.stream() .map(pool -> pool.underlyingSimpleChannelPool().closeFuture()) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java index 36055cf0b0fe..dfd22fc9437e 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java @@ -18,6 +18,7 @@ import io.netty.util.CharsetUtil; import java.net.URI; import java.util.Base64; +import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.utils.Validate; @@ -43,8 +44,9 @@ public ProxyAuthScheme scheme() { } @Override - public String generateAuthParams(URI proxyEndpoint) { + public CompletableFuture generateAuthParams(URI proxyEndpoint) { + // Purely local and cheap, so this completes inline rather than hopping to another thread. String authToken = String.format("%s:%s", this.username, this.password); - return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); + return CompletableFuture.completedFuture(Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8))); } } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index 0866073946c2..67801981df85 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -20,6 +20,8 @@ import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import javax.security.auth.Subject; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; @@ -33,6 +35,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; import software.amazon.awssdk.utils.BinaryUtils; +import software.amazon.awssdk.utils.Validate; /** * Auth generator for Kerberos. This does not login/authentication to Kerberos. It expects the ticket cache to be present and @@ -45,17 +48,20 @@ public class NegotiateProxyAuthGenerator implements ProxyAuthGenerator { private static final String OID = "1.3.6.1.5.5.2"; private static final String SERVICE_NAME = "HTTP"; private final Configuration config; + private final Executor executor; - public NegotiateProxyAuthGenerator() { - this(createDefaultConfig()); - } - - public NegotiateProxyAuthGenerator(Configuration config) { + /** + * @param config The JAAS configuration to log in with, or null to use the default ticket-cache-only configuration. + * @param executor Executor to run the blocking Kerberos work on. Must not be a Netty event loop; see + * {@link #generateAuthParams(URI)}. Its lifecycle is owned by the caller. + */ + public NegotiateProxyAuthGenerator(Configuration config, Executor executor) { if (config != null) { this.config = config; } else { this.config = createDefaultConfig(); } + this.executor = Validate.paramNotNull(executor, "executor"); } @Override @@ -64,7 +70,14 @@ public ProxyAuthScheme scheme() { } @Override - public String generateAuthParams(URI proxyEndpoint) { + public CompletableFuture generateAuthParams(URI proxyEndpoint) { + // Must not run on the caller's thread: the caller is a Netty event loop thread, and the work below reads the ticket + // cache from disk and may make a blocking TGS request to the KDC. Blocking the loop would stall every other channel + // assigned to it. + return CompletableFuture.supplyAsync(() -> generateAuthParamsBlocking(proxyEndpoint), executor); + } + + private String generateAuthParamsBlocking(URI proxyEndpoint) { try { Subject subject = getSubject(); diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java index eeb84fbdb6f5..7924635f0620 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.http.nio.netty.internal; import java.net.URI; +import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; @@ -31,6 +32,10 @@ public interface ProxyAuthGenerator { /** * Generate the auth params for this request. + *

+ * This is asynchronous because generating the params may block - Kerberos, for example, may need to read the ticket cache + * from disk and contact the KDC. Implementations that block MUST complete the returned future from a thread other than the + * caller's; the caller is a Netty event loop thread, and blocking it would stall every other channel assigned to that loop. */ - String generateAuthParams(URI proxyEndpoint); + CompletableFuture generateAuthParams(URI proxyEndpoint); } diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index aeda02f02e0f..db1be11492e3 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -31,6 +31,8 @@ import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -95,9 +97,50 @@ public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); pipeline.addBefore(ctx.name(), null, httpCodecSupplier.get()); + if (authGenerator == null) { + sendConnectRequest(ctx, null); + return; + } + + CompletableFuture authParams; + try { + authParams = authGenerator.generateAuthParams(proxyAddress); + } catch (Throwable t) { + handleConnectRequestFailure(ctx, t); + return; + } + + // Basic auth completes inline, so only pay for a thread hop when the generator is actually asynchronous. When it is, + // the future completes on another thread, so hop back to the event loop before touching the channel or this + // handler's state. + boolean completesInline = authParams.isDone(); + authParams.whenComplete((params, error) -> { + if (completesInline) { + sendConnectRequestWithAuth(ctx, params, error); + } else { + ctx.executor().execute(() -> sendConnectRequestWithAuth(ctx, params, error)); + } + }); + } + + private void sendConnectRequestWithAuth(ChannelHandlerContext ctx, String authParams, Throwable error) { + // The channel may have gone away while we were waiting; whoever completed the promise has already cleaned up. + if (initPromise.isDone()) { + return; + } + + if (error != null) { + handleConnectRequestFailure(ctx, unwrap(error)); + return; + } + + sendConnectRequest(ctx, String.format("%s %s", authGenerator.scheme().value(), authParams)); + } + + private void sendConnectRequest(ChannelHandlerContext ctx, String proxyAuthorization) { HttpRequest connectRequest; try { - connectRequest = connectRequest(); + connectRequest = connectRequest(proxyAuthorization); } catch (Throwable t) { handleConnectRequestFailure(ctx, t); return; @@ -110,6 +153,10 @@ public void handlerAdded(ChannelHandlerContext ctx) { }); } + private static Throwable unwrap(Throwable t) { + return t instanceof CompletionException && t.getCause() != null ? t.getCause() : t; + } + @Override public void handlerRemoved(ChannelHandlerContext ctx) { if (ctx.pipeline().get(HttpClientCodec.class) != null) { @@ -170,15 +217,14 @@ private void closeAndRelease(ChannelHandlerContext ctx) { sourcePool.release(ctx.channel()); } - private HttpRequest connectRequest() { + private HttpRequest connectRequest(String proxyAuthorization) { String uri = getUri(); HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.CONNECT, uri, Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, uri); - if (authGenerator != null) { - String auth = String.format("%s %s", authGenerator.scheme().value(), authGenerator.generateAuthParams(proxyAddress)); - request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, auth); + if (proxyAuthorization != null) { + request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, proxyAuthorization); } return request; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java index b0294ea768c3..b3f904f7e483 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java @@ -52,7 +52,7 @@ void generateAuthParams_generatedCorrectly() { .encodeToString(String.format("%s:%s", USERNAME, PASSWORD) .getBytes(StandardCharsets.UTF_8)); - assertThat(authGenerator.generateAuthParams(URI.create("http://amazon.com"))).isEqualTo(expected); + assertThat(authGenerator.generateAuthParams(URI.create("http://amazon.com")).join()).isEqualTo(expected); } private static Stream invalidCtorParams() { diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index b61ac05e0b10..b06eee6eaadc 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -264,7 +264,7 @@ public void proxyAuthProvided_addInitHandler_withAuth(){ Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyEndpoint, proxyAuthGenerator, remoteAddr, initFuture) -> { initFuture.setSuccess(mockChannel); - data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint); + data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint).join(); return mock(ChannelHandler.class); }; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java index 218630a46ac4..97f2c3c2f4e1 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -26,6 +26,10 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; import org.apache.kerby.kerberos.kerb.KrbException; @@ -39,6 +43,9 @@ public class NegotiateProxyAuthGeneratorTest { private static final String KRB5_PROP = "java.security.krb5.conf"; + private static final String EXECUTOR_THREAD_NAME = "test-proxy-auth"; + private static final ExecutorService executor = + Executors.newSingleThreadExecutor(r -> new Thread(r, EXECUTOR_THREAD_NAME)); private static Path tempDir; private static Path keytabFile; private static Path ccacheFile; @@ -104,6 +111,7 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { @AfterAll static void teardown() throws KrbException { + executor.shutdownNow(); if (krb5PropSave != null) { System.setProperty(KRB5_PROP, krb5PropSave); } else { @@ -115,11 +123,11 @@ static void teardown() throws KrbException { @Test void generateAuthParams_configValid_successfullyGeneratesToken() { - NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config); + NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config, executor); URI proxyEndpoint = URI.create("https://localhost:8192"); - assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); + assertThat(authGenerator.generateAuthParams(proxyEndpoint).join()).startsWith("YII"); } @Test @@ -140,12 +148,39 @@ public AppConfigurationEntry[] getAppConfigurationEntry(String name) { } }; - NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(missingCacheConfig); + NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(missingCacheConfig, executor); - assertThatThrownBy(() -> authGenerator.generateAuthParams(URI.create("https://localhost:8192"))) - .isInstanceOf(RuntimeException.class) + assertThatThrownBy(() -> authGenerator.generateAuthParams(URI.create("https://localhost:8192")).join()) + .isInstanceOf(CompletionException.class) + .hasCauseInstanceOf(RuntimeException.class) .hasMessageContaining("kinit") .hasMessageContaining("ticket cache"); } + @Test + void generateAuthParams_runsOnSuppliedExecutor() { + AtomicReference loginThread = new AtomicReference<>(); + Configuration recordingConfig = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + loginThread.set(Thread.currentThread()); + return config.getAppConfigurationEntry(name); + } + }; + + new NegotiateProxyAuthGenerator(recordingConfig, executor).generateAuthParams(URI.create("https://localhost:8192")) + .join(); + + // The blocking Kerberos work must never run on the caller's thread, which in production is a Netty event loop. + assertThat(loginThread.get()).isNotSameAs(Thread.currentThread()); + assertThat(loginThread.get().getName()).isEqualTo(EXECUTOR_THREAD_NAME); + } + + @Test + void constructor_nullExecutor_throws() { + assertThatThrownBy(() -> new NegotiateProxyAuthGenerator(config, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("executor"); + } + } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 143cc174701f..c0627ccbdef2 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -42,10 +42,12 @@ import io.netty.handler.ssl.SslCloseCompletionEvent; import io.netty.handler.ssl.SslHandler; import io.netty.util.CharsetUtil; +import io.netty.util.concurrent.EventExecutor; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; import java.util.Base64; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import org.junit.AfterClass; @@ -245,7 +247,6 @@ public void handlerAdded_writesRequest_withAuth() { @Test public void handlerAdded_authParamsGeneratorThrows_failsFuture() { ProxyAuthGenerator authGenerator = mock(ProxyAuthGenerator.class); - when(authGenerator.scheme()).thenReturn(ProxyAuthScheme.BASIC); when(authGenerator.generateAuthParams(any(URI.class))).thenThrow(new RuntimeException("auth generator error")); Promise promise = GROUP.next().newPromise(); @@ -259,6 +260,55 @@ public void handlerAdded_authParamsGeneratorThrows_failsFuture() { .hasRootCauseMessage("auth generator error"); } + @Test + public void handlerAdded_authParamsCompleteAsynchronously_writesRequestOnceParamsAvailable() throws Exception { + CompletableFuture authParams = new CompletableFuture<>(); + ProxyAuthGenerator authGenerator = mock(ProxyAuthGenerator.class); + when(authGenerator.scheme()).thenReturn(ProxyAuthScheme.NEGOTIATE); + when(authGenerator.generateAuthParams(any(URI.class))).thenReturn(authParams); + + EventExecutor executor = GROUP.next(); + when(mockCtx.executor()).thenReturn(executor); + + Promise promise = GROUP.next().newPromise(); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, URI.create("https://proxy.com"), + authGenerator, REMOTE_HOST, promise); + handler.handlerAdded(mockCtx); + + // The calling thread must not have waited for the auth params, so nothing is written yet. + verify(mockChannel, never()).writeAndFlush(any()); + + authParams.complete("token"); + // Drain the executor so the queued continuation has run. + executor.submit(() -> { }).get(); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + verify(mockChannel).writeAndFlush(requestCaptor.capture()); + assertThat(requestCaptor.getValue().headers().get(HttpHeaderNames.PROXY_AUTHORIZATION)).isEqualTo("Negotiate token"); + } + + @Test + public void handlerAdded_authParamsFailAsynchronously_failsFuture() throws Exception { + CompletableFuture authParams = new CompletableFuture<>(); + ProxyAuthGenerator authGenerator = mock(ProxyAuthGenerator.class); + when(authGenerator.generateAuthParams(any(URI.class))).thenReturn(authParams); + + EventExecutor executor = GROUP.next(); + when(mockCtx.executor()).thenReturn(executor); + + Promise promise = GROUP.next().newPromise(); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, URI.create("https://proxy.com"), + authGenerator, REMOTE_HOST, promise); + handler.handlerAdded(mockCtx); + + authParams.completeExceptionally(new RuntimeException("auth generator error")); + executor.submit(() -> { }).get(); + + verify(mockChannel, never()).writeAndFlush(any()); + assertThatThrownBy(promise::get).hasMessageContaining("Unable to send CONNECT request to proxy") + .hasRootCauseMessage("auth generator error"); + } + private void successResponse(ProxyTunnelInitHandler handler) { DefaultHttpResponse resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); handler.channelRead(mockCtx, resp); From e3b7bfe504105b711a499301eafb832110d64a66 Mon Sep 17 00:00:00 2001 From: Dongie Agnir Date: Tue, 18 Aug 2026 10:10:19 -0700 Subject: [PATCH 11/12] Add changelog --- .../next-release/feature-NettyNIOHTTPClient-febb09f.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/next-release/feature-NettyNIOHTTPClient-febb09f.json diff --git a/.changes/next-release/feature-NettyNIOHTTPClient-febb09f.json b/.changes/next-release/feature-NettyNIOHTTPClient-febb09f.json new file mode 100644 index 000000000000..64b937ccd63b --- /dev/null +++ b/.changes/next-release/feature-NettyNIOHTTPClient-febb09f.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Netty NIO HTTP Client", + "contributor": "", + "description": "Add support for Kerberos (SPNEGO) proxy authentication via the new `proxyAuthScheme` option on the Netty client's `ProxyConfiguration`. Setting `ProxyAuthScheme.NEGOTIATE` authenticates proxy CONNECT tunnels using the Kerberos ticket cache in the environment; a valid ticket-granting ticket must already exist (for example via `kinit`), and no password or keytab is read. `ProxyAuthScheme.BASIC` may also be set to select Basic authentication explicitly. See [#7033](https://github.com/aws/aws-sdk-java-v2/issues/7033)." +} From 3f693667db788e26fe20030d6f237485f4ccf68d Mon Sep 17 00:00:00 2001 From: Dongie Agnir <261310+dagnir@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:20:49 -0700 Subject: [PATCH 12/12] Move ProxyAuthScheme to SPI package (#7289) Per team review, this makes more sense in the SPI package where it can be shared by other client implementations. --- .../java/software/amazon/awssdk/http}/ProxyAuthScheme.java | 2 +- .../amazon/awssdk/http/nio/netty/ProxyConfiguration.java | 1 + .../http/nio/netty/internal/AwaitCloseChannelPoolMap.java | 2 +- .../http/nio/netty/internal/BasicProxyAuthGenerator.java | 2 +- .../http/nio/netty/internal/NegotiateProxyAuthGenerator.java | 2 +- .../awssdk/http/nio/netty/internal/ProxyAuthGenerator.java | 2 +- .../amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java | 1 + .../http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java | 2 +- .../http/nio/netty/internal/BasicProxyAuthGeneratorTest.java | 2 +- .../http/nio/netty/internal/ProxyTunnelInitHandlerTest.java | 3 +-- 10 files changed, 10 insertions(+), 9 deletions(-) rename {http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty => http-client-spi/src/main/java/software/amazon/awssdk/http}/ProxyAuthScheme.java (97%) diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/ProxyAuthScheme.java similarity index 97% rename from http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java rename to http-client-spi/src/main/java/software/amazon/awssdk/http/ProxyAuthScheme.java index 8d677d6d2900..d4857013b2c4 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyAuthScheme.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/ProxyAuthScheme.java @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.http.nio.netty; +package software.amazon.awssdk.http; import software.amazon.awssdk.annotations.SdkPublicApi; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java index 7cf332471660..5c0b6c54b8a6 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/ProxyConfiguration.java @@ -19,6 +19,7 @@ import java.util.HashSet; import java.util.Set; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger; import software.amazon.awssdk.utils.ProxyConfigProvider; import software.amazon.awssdk.utils.ProxyEnvironmentSetting; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java index d2f74cd8dfa9..f13b5381e3b1 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMap.java @@ -44,7 +44,7 @@ import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.http.Protocol; import software.amazon.awssdk.http.ProtocolNegotiation; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.http.nio.netty.ProxyConfiguration; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; import software.amazon.awssdk.http.nio.netty.internal.http2.HttpOrHttp2ChannelPool; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java index dfd22fc9437e..44a80ab34a4e 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGenerator.java @@ -20,7 +20,7 @@ import java.util.Base64; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.utils.Validate; /** diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java index 67801981df85..b2f5bff08d54 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java @@ -33,7 +33,7 @@ import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.utils.BinaryUtils; import software.amazon.awssdk.utils.Validate; diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java index 7924635f0620..901338bd7eb3 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -18,7 +18,7 @@ import java.net.URI; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; /** * Generates the auth params for an {@code Authorization} HTTP header. diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java index 2157676c5b7e..57f9ec44483a 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.testutils.LogCaptor; /** diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java index f1b80597d3c4..8838e230329e 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/AwaitCloseChannelPoolMapTest.java @@ -56,7 +56,7 @@ import software.amazon.awssdk.http.Protocol; import software.amazon.awssdk.http.ProtocolNegotiation; import software.amazon.awssdk.http.TlsKeyManagersProvider; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; import software.amazon.awssdk.http.nio.netty.ProxyConfiguration; import software.amazon.awssdk.http.nio.netty.RecordingNetworkTrafficListener; import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java index b3f904f7e483..99ea7397bd94 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java @@ -26,7 +26,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; public class BasicProxyAuthGeneratorTest { private static final String USERNAME = "user"; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index c0627ccbdef2..8bc20a165b25 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -48,7 +48,6 @@ import java.net.URI; import java.util.Base64; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.function.Supplier; import org.junit.AfterClass; import org.junit.Before; @@ -57,7 +56,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; +import software.amazon.awssdk.http.ProxyAuthScheme; /** * Unit tests for {@link ProxyTunnelInitHandler}.