) () -> {
+ 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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();