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.
+ *
+ * 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.
+ */
+ Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme);
+
/**
* Set the username used to authenticate with the proxy username.
*
@@ -305,6 +342,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;
@@ -322,6 +360,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;
}
@@ -354,6 +393,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 ff5c87e57038..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,14 +36,17 @@
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;
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.
@@ -87,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;
@@ -99,6 +104,7 @@ private AwaitCloseChannelPoolMap(Builder builder, Function "Closing channel pools");
@@ -293,6 +329,9 @@ public static class Builder {
private ProxyConfiguration proxyConfiguration;
private Boolean useNonBlockingDnsResolver;
+ // testing only
+ private Configuration negotiateAuthConfig;
+
private Builder() {
}
@@ -351,6 +390,12 @@ public Builder useNonBlockingDnsResolver(Boolean useNonBlockingDnsResolver) {
return this;
}
+ @SdkTestInternalApi
+ public Builder negotiateAuthConfig(Configuration negotiateAuthConfig) {
+ this.negotiateAuthConfig = negotiateAuthConfig;
+ return this;
+ }
+
public AwaitCloseChannelPoolMap build() {
return new AwaitCloseChannelPoolMap(this);
}
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..36055cf0b0fe
--- /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.net.URI;
+import java.util.Base64;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+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.notEmpty(username, "username must not be empty");
+ this.password = Validate.notEmpty(password, "password must not be empty");
+ }
+
+ @Override
+ public ProxyAuthScheme scheme() {
+ return ProxyAuthScheme.BASIC;
+ }
+
+ @Override
+ public String generateAuthParams(URI proxyEndpoint) {
+ String authToken = String.format("%s:%s", this.username, this.password);
+ return Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8));
+ }
+}
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java
index cc53ed4da46a..0dafa642d6e4 100644
--- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPool.java
@@ -49,41 +49,30 @@ public class Http1TunnelConnectionPool implements ChannelPool {
private final ChannelPool delegate;
private final SslContext sslContext;
private final URI proxyAddress;
- private final String proxyUser;
- private final String proxyPassword;
+ private final ProxyAuthGenerator proxyAuthGenerator;
private final URI remoteAddress;
private final ChannelPoolHandler handler;
private final InitHandlerSupplier initHandlerSupplier;
private final NettyConfiguration nettyConfiguration;
public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
- URI proxyAddress, String proxyUsername, String proxyPassword,
+ URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator,
URI remoteAddress, ChannelPoolHandler handler, NettyConfiguration nettyConfiguration) {
this(eventLoop, delegate, sslContext,
- proxyAddress, proxyUsername, proxyPassword, remoteAddress, handler,
+ proxyAddress, proxyAuthGenerator, remoteAddress, handler,
ProxyTunnelInitHandler::new, nettyConfiguration);
}
- public Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
- URI proxyAddress, URI remoteAddress, ChannelPoolHandler handler,
- NettyConfiguration nettyConfiguration) {
- this(eventLoop, delegate, sslContext,
- proxyAddress, null, null, remoteAddress, handler,
- ProxyTunnelInitHandler::new, nettyConfiguration);
-
- }
-
@SdkTestInternalApi
Http1TunnelConnectionPool(EventLoop eventLoop, ChannelPool delegate, SslContext sslContext,
- URI proxyAddress, String proxyUser, String proxyPassword, URI remoteAddress,
+ URI proxyAddress, ProxyAuthGenerator proxyAuthGenerator, URI remoteAddress,
ChannelPoolHandler handler, InitHandlerSupplier initHandlerSupplier,
NettyConfiguration nettyConfiguration) {
this.eventLoop = eventLoop;
this.delegate = delegate;
this.sslContext = sslContext;
this.proxyAddress = proxyAddress;
- this.proxyUser = proxyUser;
- this.proxyPassword = proxyPassword;
+ this.proxyAuthGenerator = proxyAuthGenerator;
this.remoteAddress = remoteAddress;
this.handler = handler;
this.initHandlerSupplier = initHandlerSupplier;
@@ -138,7 +127,7 @@ private void setupChannel(Channel ch, Promise acquirePromise) {
if (sslHandler != null) {
ch.pipeline().addLast(sslHandler);
}
- ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyUser, proxyPassword, remoteAddress,
+ ch.pipeline().addLast(initHandlerSupplier.newInitHandler(delegate, proxyAddress, proxyAuthGenerator, remoteAddress,
tunnelEstablishedPromise));
tunnelEstablishedPromise.addListener((Future f) -> {
if (f.isSuccess()) {
@@ -180,7 +169,10 @@ private static boolean isTunnelEstablished(Channel ch) {
@SdkTestInternalApi
@FunctionalInterface
interface InitHandlerSupplier {
- ChannelHandler newInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteAddress,
+ ChannelHandler newInitHandler(ChannelPool sourcePool,
+ URI proxyAddress,
+ ProxyAuthGenerator authGenerator,
+ URI remoteAddress,
Promise tunnelInitFuture);
}
}
diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java
new file mode 100644
index 000000000000..0866073946c2
--- /dev/null
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGenerator.java
@@ -0,0 +1,142 @@
+/*
+ * 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 java.net.URI;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme;
+import software.amazon.awssdk.utils.BinaryUtils;
+
+/**
+ * Auth generator for Kerberos. This does not login/authentication to Kerberos. It expects the ticket cache to be present and
+ * simply reads that to generate the token.
+ */
+@SdkInternalApi
+public class NegotiateProxyAuthGenerator implements ProxyAuthGenerator {
+ // SPNEGO pseudo-mechanism OID. Lets the proxy negotiate Kerberos over HTTP "Negotiate".
+ // See https://www.ietf.org/rfc/rfc4178.txt for more info
+ private static final String OID = "1.3.6.1.5.5.2";
+ private static final String SERVICE_NAME = "HTTP";
+ private final Configuration config;
+
+ public NegotiateProxyAuthGenerator() {
+ this(createDefaultConfig());
+ }
+
+ public NegotiateProxyAuthGenerator(Configuration config) {
+ if (config != null) {
+ this.config = config;
+ } else {
+ this.config = createDefaultConfig();
+ }
+ }
+
+ @Override
+ public ProxyAuthScheme scheme() {
+ return ProxyAuthScheme.NEGOTIATE;
+ }
+
+ @Override
+ public String generateAuthParams(URI proxyEndpoint) {
+ try {
+ Subject subject = getSubject();
+
+ byte[] token = Subject.doAs(subject, (PrivilegedExceptionAction) () -> {
+ GSSContext ctx = createGssContext(getManager(), proxyEndpoint);
+ try {
+ return ctx.initSecContext(new byte[0], 0, 0);
+ } finally {
+ ctx.dispose();
+ }
+ });
+
+ return BinaryUtils.toBase64(token);
+ } catch (PrivilegedActionException 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);
+ }
+ }
+
+ 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 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);
+ }
+ }
+
+ 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 {@code com.sun.security.auth.module.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..eeb84fbdb6f5
--- /dev/null
+++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * 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 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..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
@@ -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,37 +45,64 @@ 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(proxyUsername) && !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();
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());
@@ -151,10 +176,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/ProxyConfigurationTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/ProxyConfigurationTest.java
index 06d57c1aa7d2..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();
@@ -185,7 +247,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/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..b0294ea768c3
--- /dev/null
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/BasicProxyAuthGeneratorTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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 java.net.URI;
+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.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(URI.create("http://amazon.com"))).isEqualTo(expected);
+ }
+
+ private static Stream invalidCtorParams() {
+ return Stream.of(
+ Arguments.of(null, null, "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/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..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;
@@ -73,6 +75,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 +119,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 +131,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 +153,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 +168,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 +178,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 +201,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 +222,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 +235,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 +243,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 +252,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 +261,33 @@ 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);
-
+ String expectedAuthHeader = Base64.getEncoder().encodeToString((PROXY_USER + ":" + PROXY_PASSWORD)
+ .getBytes(StandardCharsets.UTF_8));
+ assertThat(data.authHeader()).isEqualTo(expectedAuthHeader);
}
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..218630a46ac4
--- /dev/null
+++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 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 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;
+
+ @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.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);
+ }
+
+ @Test
+ void generateAuthParams_configValid_successfullyGeneratesToken() {
+ NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config);
+
+ URI proxyEndpoint = URI.create("https://localhost:8192");
+
+ 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");
+ }
+
+}
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..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}.
@@ -95,7 +98,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 +206,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 +223,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);
@@ -238,6 +242,23 @@ public void handledAdded_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);
diff --git a/pom.xml b/pom.xml
index 4714e90d93e6..08510f5e05f4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -151,6 +151,7 @@
1.17.5
1.3.0
1.5.4
+ 2.0.3
3.1.2