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

+ * See javadoc for {@link Krb5LoginModule} for additional info on the configuration options. + */ + private static Configuration createDefaultConfig() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("doNotPrompt", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + } +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java new file mode 100644 index 000000000000..b078500b09ac --- /dev/null +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyAuthGenerator.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.nio.netty.internal; + +import io.netty.handler.codec.http.HttpRequest; +import java.net.URI; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.nio.netty.ProxyAuthScheme; + +/** + * Generates the auth params for an {@code Authorization} HTTP header. + */ +@SdkInternalApi +public interface ProxyAuthGenerator { + /** + * The name of the auth scheme this generator supports. + */ + ProxyAuthScheme scheme(); + + /** + * Generate the auth params for this request. + */ + String generateAuthParams(URI proxyEndpoint); +} diff --git a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java index e6f309afbad3..277ed555dbc5 100644 --- a/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java +++ b/http-clients/netty-nio-client/src/main/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandler.java @@ -28,11 +28,9 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; -import io.netty.util.CharsetUtil; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.URI; -import java.util.Base64; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; @@ -47,32 +45,51 @@ public final class ProxyTunnelInitHandler extends ChannelDuplexHandler { public static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyTunnelInitHandler.class); private final ChannelPool sourcePool; - private final String username; - private final String password; + private final URI proxyAddress; + private final ProxyAuthGenerator authGenerator; private final URI remoteHost; private final Promise initPromise; private final Supplier httpCodecSupplier; public ProxyTunnelInitHandler(ChannelPool sourcePool, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise) { - this(sourcePool, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, proxyUsername, proxyPassword, remoteHost, initPromise, HttpClientCodec::new); } public ProxyTunnelInitHandler(ChannelPool sourcePool, URI remoteHost, Promise initPromise) { - this(sourcePool, null, null, remoteHost, initPromise, HttpClientCodec::new); + this(sourcePool, null, null, null, remoteHost, initPromise, HttpClientCodec::new); } @SdkTestInternalApi - public ProxyTunnelInitHandler(ChannelPool sourcePool, String prosyUsername, String proxyPassword, + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, String proxyUsername, String proxyPassword, URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; this.remoteHost = remoteHost; this.initPromise = initPromise; - this.username = prosyUsername; - this.password = proxyPassword; + if (!StringUtils.isBlank(proxyPassword) && !StringUtils.isBlank(proxyPassword)) { + this.authGenerator = new BasicProxyAuthGenerator(proxyUsername, proxyPassword); + } else { + this.authGenerator = null; + } this.httpCodecSupplier = httpCodecSupplier; } + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise, Supplier httpCodecSupplier) { + this.sourcePool = sourcePool; + this.proxyAddress = proxyAddress; + this.remoteHost = remoteHost; + this.initPromise = initPromise; + this.authGenerator = authGenerator; + this.httpCodecSupplier = httpCodecSupplier; + } + + public ProxyTunnelInitHandler(ChannelPool sourcePool, URI proxyAddress, ProxyAuthGenerator authGenerator, + URI remoteHost, Promise initPromise) { + this(sourcePool, proxyAddress, authGenerator, remoteHost, initPromise, HttpClientCodec::new); + } + @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline pipeline = ctx.pipeline(); @@ -151,10 +168,9 @@ private HttpRequest connectRequest() { Unpooled.EMPTY_BUFFER); request.headers().add(HttpHeaderNames.HOST, uri); - if (!StringUtils.isEmpty(this.username) && !StringUtils.isEmpty(this.password)) { - String authToken = String.format("%s:%s", this.username, this.password); - String authB64 = Base64.getEncoder().encodeToString(authToken.getBytes(CharsetUtil.UTF_8)); - request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, String.format("Basic %s", authB64)); + if (authGenerator != null) { + String auth = String.format("%s %s", authGenerator.scheme().value(), authGenerator.generateAuthParams(proxyAddress)); + request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, auth); } return request; diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java index d43b404f3f5f..9e2ed53cd1e3 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/Http1TunnelConnectionPoolTest.java @@ -73,6 +73,8 @@ public class Http1TunnelConnectionPoolTest { private static final String PROXY_PASSWORD = "mypassword"; + private static final ProxyAuthGenerator basicAuth = new BasicProxyAuthGenerator(PROXY_USER, PROXY_PASSWORD); + @Mock private ChannelPool delegatePool; @@ -115,7 +117,7 @@ public static void teardown() { @Test public void tunnelAlreadyEstablished_doesNotAddInitHandler() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(true); @@ -127,7 +129,7 @@ public void tunnelAlreadyEstablished_doesNotAddInitHandler() { @Test(timeout = 1000) public void tunnelNotEstablished_addsInitHandler() throws InterruptedException { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(mockAttr.get()).thenReturn(false); @@ -149,7 +151,7 @@ public void tunnelInitFails_acquireFutureFails() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -164,7 +166,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); Future acquireFuture = tunnelPool.acquire(); @@ -174,7 +176,7 @@ public void tunnelInitSucceeds_acquireFutureSucceeds() { @Test public void acquireFromDelegatePoolFails_failsFuture() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom"))); @@ -197,7 +199,7 @@ public void sslContextProvided_andProxyUsingHttps_addsSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTPS_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -218,7 +220,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, - HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); @@ -231,7 +233,7 @@ public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { @Test public void release_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS,null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.release(mockChannel); verify(delegatePool).release(eq(mockChannel), any(Promise.class)); } @@ -239,7 +241,7 @@ public void release_releasedToDelegatePool() { @Test public void release_withGivenPromise_releasedToDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); Promise mockPromise = mock(Promise.class); tunnelPool.release(mockChannel, mockPromise); verify(delegatePool).release(eq(mockChannel), eq(mockPromise)); @@ -248,7 +250,7 @@ public void release_withGivenPromise_releasedToDelegatePool() { @Test public void close_closesDelegatePool() { Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); + HTTP_PROXY_ADDRESS, null, REMOTE_ADDRESS, mockHandler, configuration); tunnelPool.close(); verify(delegatePool).close(); } @@ -257,42 +259,32 @@ public void close_closesDelegatePool() { public void proxyAuthProvided_addInitHandler_withAuth(){ TestInitHandlerData data = new TestInitHandlerData(); - Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { + Http1TunnelConnectionPool.InitHandlerSupplier supplier = + (srcPool, proxyEndpoint, proxyAuthGenerator, remoteAddr, initFuture) -> { initFuture.setSuccess(mockChannel); - data.proxyUser(proxyUser); - data.proxyPassword(proxyPassword); + data.authHeader = proxyAuthGenerator.generateAuthParams(proxyEndpoint); return mock(ChannelHandler.class); }; Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, - HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration); + HTTP_PROXY_ADDRESS, basicAuth, REMOTE_ADDRESS, mockHandler, supplier, configuration); tunnelPool.acquire().awaitUninterruptibly(); - assertThat(data.proxyUser()).isEqualTo(PROXY_USER); - assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); - + // assertThat(data.proxyUser()).isEqualTo(PROXY_USER); + // assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); } private static class TestInitHandlerData { - private String proxyUser; - private String proxyPassword; - - public void proxyUser(String proxyUser) { - this.proxyUser = proxyUser; - } - - public String proxyUser() { - return this.proxyUser; - } + private String authHeader; - public void proxyPassword(String proxyPassword) { - this.proxyPassword = proxyPassword; + public void authHeader(String authHeader) { + this.authHeader = authHeader; } - public String proxyPassword(){ - return this.proxyPassword; + public String authHeader() { + return authHeader; } } diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java new file mode 100644 index 000000000000..d7c7a3bc1506 --- /dev/null +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/NegotiateProxyAuthGeneratorTest.java @@ -0,0 +1,110 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.http.nio.netty.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import org.apache.kerby.kerberos.kerb.KrbException; +import org.apache.kerby.kerberos.kerb.client.KrbClient; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.testutils.FileUtils; + +public class NegotiateProxyAuthGeneratorTest { + private static Path tempDir; + private static Path keytabFile; + private static Path ccacheFile; + private static int port; + + private static SimpleKdcServer kdc; + + private static Configuration config; + + @BeforeAll + static void setup() throws IOException, KrbException { + tempDir = Files.createTempDirectory(null); + keytabFile = tempDir.resolve("keytab"); + ccacheFile = tempDir.resolve("ccache"); + + try (Socket freePort = new Socket()) { + freePort.setReuseAddress(true); + freePort.bind(new InetSocketAddress(0)); + port = freePort.getLocalPort(); + } + + kdc = new SimpleKdcServer(); + kdc.setKdcRealm("EXAMPLE.COM"); + kdc.setKdcHost("localhost"); + kdc.setWorkDir(tempDir.toFile()); + kdc.setKdcTcpPort(port); + kdc.init(); + kdc.start(); + + kdc.createPrincipal("alice@EXAMPLE.COM", "alicePassword"); + kdc.createAndExportPrincipals(keytabFile.toFile(), "HTTP/localhost@EXAMPLE.COM"); + + // initialize the ticket cache + KrbClient krbClient = kdc.getKrbClient(); + TgtTicket tgt = krbClient.requestTgt("alice@EXAMPLE.COM", "alicePassword"); + krbClient.storeTicket(tgt, ccacheFile.toFile()); + + // Override config so we look at the testing cache instead of the real system cache + config = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + opts.put("useTicketCache", "true"); + opts.put("ticketCache", ccacheFile.toAbsolutePath().toString()); + opts.put("doNotPrompt", "true"); + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + + } + + @AfterAll + static void teardown() throws KrbException { + kdc.stop(); + FileUtils.cleanUpTestDirectory(tempDir); + } + + @Test + void generateAuthParams_configValid_successfullyGeneratesToken() { + NegotiateProxyAuthGenerator authGenerator = new NegotiateProxyAuthGenerator(config); + + URI proxyEndpoint = URI.create("https://localhost:8192"); + + assertThat(authGenerator.generateAuthParams(proxyEndpoint)).startsWith("YII"); + } + +} diff --git a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java index 9836a953bda9..7828050bef26 100644 --- a/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java +++ b/http-clients/netty-nio-client/src/test/java/software/amazon/awssdk/http/nio/netty/internal/ProxyTunnelInitHandlerTest.java @@ -95,7 +95,8 @@ public void addedToPipeline_addsCodec() { Supplier codecSupplier = () -> codec; when(mockCtx.name()).thenReturn("foo"); - ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, REMOTE_HOST, null, codecSupplier); + ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, null, null, null, REMOTE_HOST, null, + codecSupplier); handler.handlerAdded(mockCtx); verify(mockPipeline).addBefore(eq("foo"), eq(null), eq(codec)); @@ -202,7 +203,7 @@ public void handlerRemoved_removesCodec() { } @Test - public void handledAdded_writesRequest_withoutAuth() { + public void handlerAdded_writesRequest_withoutAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); @@ -219,7 +220,7 @@ public void handledAdded_writesRequest_withoutAuth() { } @Test - public void handledAdded_writesRequest_withAuth() { + public void handlerAdded_writesRequest_withAuth() { Promise promise = GROUP.next().newPromise(); ProxyTunnelInitHandler handler = new ProxyTunnelInitHandler(mockChannelPool, PROXY_USER, PROXY_PASSWORD, REMOTE_HOST, promise); handler.handlerAdded(mockCtx); diff --git a/pom.xml b/pom.xml index 83c359fa7ff5..cee43145ce64 100644 --- a/pom.xml +++ b/pom.xml @@ -152,6 +152,7 @@ 1.17.5 1.3.0 1.5.4 + 2.0.3 3.1.2