1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.http.nio.netty.internal; 17 18 import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 19 import static org.mockito.ArgumentMatchers.any; 20 import static org.mockito.ArgumentMatchers.eq; 21 import static org.mockito.Mockito.mock; 22 import static org.mockito.Mockito.never; 23 import static org.mockito.Mockito.times; 24 import static org.mockito.Mockito.verify; 25 import static org.mockito.Mockito.when; 26 import static software.amazon.awssdk.http.nio.netty.internal.Http1TunnelConnectionPool.TUNNEL_ESTABLISHED_KEY; 27 28 import io.netty.buffer.ByteBufAllocator; 29 import io.netty.channel.Channel; 30 import io.netty.channel.ChannelHandler; 31 import io.netty.channel.ChannelHandlerContext; 32 import io.netty.channel.ChannelId; 33 import io.netty.channel.ChannelPipeline; 34 import io.netty.channel.nio.NioEventLoopGroup; 35 import io.netty.channel.pool.ChannelPool; 36 import io.netty.channel.pool.ChannelPoolHandler; 37 import io.netty.handler.ssl.ApplicationProtocolNegotiator; 38 import io.netty.handler.ssl.SslContext; 39 import io.netty.handler.ssl.SslHandler; 40 import io.netty.util.Attribute; 41 import io.netty.util.concurrent.Future; 42 import io.netty.util.concurrent.Promise; 43 import java.io.IOException; 44 import java.net.URI; 45 import java.util.List; 46 import java.util.concurrent.CountDownLatch; 47 import javax.net.ssl.SSLEngine; 48 import javax.net.ssl.SSLParameters; 49 import javax.net.ssl.SSLSessionContext; 50 import org.junit.AfterClass; 51 import org.junit.Before; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 import org.mockito.ArgumentCaptor; 55 import org.mockito.Mock; 56 import org.mockito.junit.MockitoJUnitRunner; 57 import software.amazon.awssdk.http.SdkHttpConfigurationOption; 58 59 /** 60 * Unit tests for {@link Http1TunnelConnectionPool}. 61 */ 62 @RunWith(MockitoJUnitRunner.class) 63 public class Http1TunnelConnectionPoolTest { 64 private static final NioEventLoopGroup GROUP = new NioEventLoopGroup(1); 65 66 private static final URI HTTP_PROXY_ADDRESS = URI.create("http://localhost:1234"); 67 68 private static final URI HTTPS_PROXY_ADDRESS = URI.create("https://localhost:5678"); 69 70 private static final URI REMOTE_ADDRESS = URI.create("https://s3.amazonaws.com:5678"); 71 72 private static final String PROXY_USER = "myuser"; 73 74 private static final String PROXY_PASSWORD = "mypassword"; 75 76 @Mock 77 private ChannelPool delegatePool; 78 79 @Mock 80 private ChannelPoolHandler mockHandler; 81 82 @Mock 83 public Channel mockChannel; 84 85 @Mock 86 public ChannelPipeline mockPipeline; 87 88 @Mock 89 public Attribute mockAttr; 90 91 @Mock 92 public ChannelHandlerContext mockCtx; 93 94 @Mock 95 public ChannelId mockId; 96 97 public NettyConfiguration configuration; 98 99 @Before methodSetup()100 public void methodSetup() { 101 Future<Channel> channelFuture = GROUP.next().newSucceededFuture(mockChannel); 102 when(delegatePool.acquire(any(Promise.class))).thenReturn(channelFuture); 103 104 when(mockChannel.attr(eq(TUNNEL_ESTABLISHED_KEY))).thenReturn(mockAttr); 105 when(mockChannel.id()).thenReturn(mockId); 106 when(mockChannel.pipeline()).thenReturn(mockPipeline); 107 configuration = new NettyConfiguration(SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS); 108 } 109 110 @AfterClass teardown()111 public static void teardown() { 112 GROUP.shutdownGracefully().awaitUninterruptibly(); 113 } 114 115 @Test tunnelAlreadyEstablished_doesNotAddInitHandler()116 public void tunnelAlreadyEstablished_doesNotAddInitHandler() { 117 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 118 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 119 120 when(mockAttr.get()).thenReturn(true); 121 122 tunnelPool.acquire().awaitUninterruptibly(); 123 124 verify(mockPipeline, never()).addLast(any()); 125 } 126 127 @Test(timeout = 1000) tunnelNotEstablished_addsInitHandler()128 public void tunnelNotEstablished_addsInitHandler() throws InterruptedException { 129 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 130 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 131 132 when(mockAttr.get()).thenReturn(false); 133 134 CountDownLatch latch = new CountDownLatch(1); 135 when(mockPipeline.addLast(any(ChannelHandler.class))).thenAnswer(i -> { 136 latch.countDown(); 137 return mockPipeline; 138 }); 139 tunnelPool.acquire(); 140 latch.await(); 141 verify(mockPipeline, times(1)).addLast(any(ProxyTunnelInitHandler.class)); 142 } 143 144 @Test tunnelInitFails_acquireFutureFails()145 public void tunnelInitFails_acquireFutureFails() { 146 Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { 147 initFuture.setFailure(new IOException("boom")); 148 return mock(ChannelHandler.class); 149 }; 150 151 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 152 HTTP_PROXY_ADDRESS,null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); 153 154 Future<Channel> acquireFuture = tunnelPool.acquire(); 155 156 assertThat(acquireFuture.awaitUninterruptibly().cause()).hasMessage("boom"); 157 } 158 159 @Test tunnelInitSucceeds_acquireFutureSucceeds()160 public void tunnelInitSucceeds_acquireFutureSucceeds() { 161 Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { 162 initFuture.setSuccess(mockChannel); 163 return mock(ChannelHandler.class); 164 }; 165 166 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 167 HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); 168 169 Future<Channel> acquireFuture = tunnelPool.acquire(); 170 171 assertThat(acquireFuture.awaitUninterruptibly().getNow()).isEqualTo(mockChannel); 172 } 173 174 @Test acquireFromDelegatePoolFails_failsFuture()175 public void acquireFromDelegatePoolFails_failsFuture() { 176 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 177 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 178 179 when(delegatePool.acquire(any(Promise.class))).thenReturn(GROUP.next().newFailedFuture(new IOException("boom"))); 180 181 Future<Channel> acquireFuture = tunnelPool.acquire(); 182 183 assertThat(acquireFuture.awaitUninterruptibly().cause()).hasMessage("boom"); 184 } 185 186 @Test sslContextProvided_andProxyUsingHttps_addsSslHandler()187 public void sslContextProvided_andProxyUsingHttps_addsSslHandler() { 188 SslHandler mockSslHandler = mock(SslHandler.class); 189 SSLEngine mockSslEngine = mock(SSLEngine.class); 190 when(mockSslHandler.engine()).thenReturn(mockSslEngine); 191 when(mockSslEngine.getSSLParameters()).thenReturn(mock(SSLParameters.class)); 192 TestSslContext mockSslCtx = new TestSslContext(mockSslHandler); 193 194 Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { 195 initFuture.setSuccess(mockChannel); 196 return mock(ChannelHandler.class); 197 }; 198 199 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, 200 HTTPS_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); 201 202 tunnelPool.acquire().awaitUninterruptibly(); 203 204 ArgumentCaptor<ChannelHandler> handlersCaptor = ArgumentCaptor.forClass(ChannelHandler.class); 205 verify(mockPipeline, times(2)).addLast(handlersCaptor.capture()); 206 207 assertThat(handlersCaptor.getAllValues().get(0)).isEqualTo(mockSslHandler); 208 } 209 210 @Test sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler()211 public void sslContextProvided_andProxyNotUsingHttps_doesNotAddSslHandler() { 212 SslHandler mockSslHandler = mock(SslHandler.class); 213 TestSslContext mockSslCtx = new TestSslContext(mockSslHandler); 214 215 Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { 216 initFuture.setSuccess(mockChannel); 217 return mock(ChannelHandler.class); 218 }; 219 220 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, mockSslCtx, 221 HTTP_PROXY_ADDRESS, null, null, REMOTE_ADDRESS, mockHandler, supplier, configuration); 222 223 tunnelPool.acquire().awaitUninterruptibly(); 224 225 ArgumentCaptor<ChannelHandler> handlersCaptor = ArgumentCaptor.forClass(ChannelHandler.class); 226 verify(mockPipeline).addLast(handlersCaptor.capture()); 227 228 assertThat(handlersCaptor.getAllValues().get(0)).isNotInstanceOf(SslHandler.class); 229 } 230 231 @Test release_releasedToDelegatePool()232 public void release_releasedToDelegatePool() { 233 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 234 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 235 tunnelPool.release(mockChannel); 236 verify(delegatePool).release(eq(mockChannel), any(Promise.class)); 237 } 238 239 @Test release_withGivenPromise_releasedToDelegatePool()240 public void release_withGivenPromise_releasedToDelegatePool() { 241 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 242 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 243 Promise mockPromise = mock(Promise.class); 244 tunnelPool.release(mockChannel, mockPromise); 245 verify(delegatePool).release(eq(mockChannel), eq(mockPromise)); 246 } 247 248 @Test close_closesDelegatePool()249 public void close_closesDelegatePool() { 250 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 251 HTTP_PROXY_ADDRESS, REMOTE_ADDRESS, mockHandler, configuration); 252 tunnelPool.close(); 253 verify(delegatePool).close(); 254 } 255 256 @Test proxyAuthProvided_addInitHandler_withAuth()257 public void proxyAuthProvided_addInitHandler_withAuth(){ 258 TestInitHandlerData data = new TestInitHandlerData(); 259 260 Http1TunnelConnectionPool.InitHandlerSupplier supplier = (srcPool, proxyUser, proxyPassword, remoteAddr, initFuture) -> { 261 initFuture.setSuccess(mockChannel); 262 data.proxyUser(proxyUser); 263 data.proxyPassword(proxyPassword); 264 return mock(ChannelHandler.class); 265 }; 266 267 Http1TunnelConnectionPool tunnelPool = new Http1TunnelConnectionPool(GROUP.next(), delegatePool, null, 268 HTTP_PROXY_ADDRESS, PROXY_USER, PROXY_PASSWORD, REMOTE_ADDRESS, mockHandler, supplier, configuration); 269 270 tunnelPool.acquire().awaitUninterruptibly(); 271 272 assertThat(data.proxyUser()).isEqualTo(PROXY_USER); 273 assertThat(data.proxyPassword()).isEqualTo(PROXY_PASSWORD); 274 275 } 276 277 private static class TestInitHandlerData { 278 279 private String proxyUser; 280 private String proxyPassword; 281 proxyUser(String proxyUser)282 public void proxyUser(String proxyUser) { 283 this.proxyUser = proxyUser; 284 } 285 proxyUser()286 public String proxyUser() { 287 return this.proxyUser; 288 } 289 proxyPassword(String proxyPassword)290 public void proxyPassword(String proxyPassword) { 291 this.proxyPassword = proxyPassword; 292 } 293 proxyPassword()294 public String proxyPassword(){ 295 return this.proxyPassword; 296 } 297 298 } 299 300 private static class TestSslContext extends SslContext { 301 private final SslHandler handler; 302 TestSslContext(SslHandler handler)303 protected TestSslContext(SslHandler handler) { 304 this.handler = handler; 305 } 306 307 @Override isClient()308 public boolean isClient() { 309 return false; 310 } 311 312 @Override cipherSuites()313 public List<String> cipherSuites() { 314 return null; 315 } 316 317 @Override sessionCacheSize()318 public long sessionCacheSize() { 319 return 0; 320 } 321 322 @Override sessionTimeout()323 public long sessionTimeout() { 324 return 0; 325 } 326 327 @Override applicationProtocolNegotiator()328 public ApplicationProtocolNegotiator applicationProtocolNegotiator() { 329 return null; 330 } 331 332 @Override newEngine(ByteBufAllocator alloc)333 public SSLEngine newEngine(ByteBufAllocator alloc) { 334 return null; 335 } 336 337 @Override newEngine(ByteBufAllocator alloc, String peerHost, int peerPort)338 public SSLEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort) { 339 return null; 340 } 341 342 @Override sessionContext()343 public SSLSessionContext sessionContext() { 344 return null; 345 } 346 347 @Override newHandler(ByteBufAllocator alloc, String host, int port, boolean startTls)348 public SslHandler newHandler(ByteBufAllocator alloc, String host, int port, boolean startTls) { 349 return handler; 350 } 351 } 352 } 353