/* * 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; import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; /** * A set of tests validating that the functionality implemented by a {@link SdkAsyncHttpClient} for HTTP/1 requests * * This is used by an HTTP plugin implementation by extending this class and implementing the abstract methods to provide this * suite with a testable HTTP client implementation. */ public abstract class SdkAsyncHttpClientH1TestSuite { private Server server; private SdkAsyncHttpClient client; protected abstract SdkAsyncHttpClient setupClient(); @BeforeEach public void setup() throws Exception { server = new Server(); server.init(); this.client = setupClient(); } @AfterEach public void teardown() throws InterruptedException { if (server != null) { server.shutdown(); } if (client != null) { client.close(); } server = null; } @Test public void connectionReceiveServerErrorStatusShouldNotReuseConnection() { server.return500OnFirstRequest = true; server.closeConnection = false; HttpTestUtils.sendGetRequest(server.port(), client).join(); HttpTestUtils.sendGetRequest(server.port(), client).join(); assertThat(server.channels.size()).isEqualTo(2); } @Test public void connectionReceiveOkStatusShouldReuseConnection() throws Exception { server.return500OnFirstRequest = false; server.closeConnection = false; HttpTestUtils.sendGetRequest(server.port(), client).join(); // The request-complete-future does not await the channel-release-future // Wait a small amount to allow the channel release to complete Thread.sleep(100); HttpTestUtils.sendGetRequest(server.port(), client).join(); assertThat(server.channels.size()).isEqualTo(1); } @Test public void connectionReceiveCloseHeaderShouldNotReuseConnection() throws InterruptedException { server.return500OnFirstRequest = false; server.closeConnection = true; HttpTestUtils.sendGetRequest(server.port(), client).join(); Thread.sleep(1000); HttpTestUtils.sendGetRequest(server.port(), client).join(); assertThat(server.channels.size()).isEqualTo(2); } @Test public void headRequestResponsesHaveNoPayload() { byte[] responseData = HttpTestUtils.sendHeadRequest(server.port(), client).join(); // The SDK core differentiates between NO data and ZERO bytes of data. Core expects it to be NO data, not ZERO bytes of // data for head requests. assertThat(responseData).isNull(); } @Test public void naughtyHeaderCharactersDoNotGetToServer() { String naughtyHeader = "foo\r\nbar"; assertThatThrownBy(() -> HttpTestUtils.sendRequest(client, SdkHttpFullRequest.builder() .uri(URI.create("https://localhost:" + server.port())) .method(SdkHttpMethod.POST) .appendHeader("h", naughtyHeader) .build()) .join()) .hasCauseInstanceOf(Exception.class); } @Test public void connectionsArePooledByHostAndPort() throws InterruptedException { HttpTestUtils.sendRequest(client, SdkHttpFullRequest.builder() .uri(URI.create("https://127.0.0.1:" + server.port() + "/foo?foo")) .method(SdkHttpMethod.GET) .build()) .join(); Thread.sleep(1_000); HttpTestUtils.sendRequest(client, SdkHttpFullRequest.builder() .uri(URI.create("https://127.0.0.1:" + server.port() + "/bar?bar")) .method(SdkHttpMethod.GET) .build()) .join(); assertThat(server.channels.size()).isEqualTo(1); } private static class Server extends ChannelInitializer { private static final byte[] CONTENT = "helloworld".getBytes(StandardCharsets.UTF_8); private ServerBootstrap bootstrap; private ServerSocketChannel serverSock; private List channels = new ArrayList<>(); private final NioEventLoopGroup group = new NioEventLoopGroup(); private SslContext sslCtx; private boolean return500OnFirstRequest; private boolean closeConnection; private volatile HttpRequest lastRequestReceived; public void init() throws Exception { SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build(); bootstrap = new ServerBootstrap() .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.DEBUG)) .group(group) .childHandler(this); serverSock = (ServerSocketChannel) bootstrap.bind(0).sync().channel(); } public void shutdown() throws InterruptedException { group.shutdownGracefully().await(); serverSock.close(); } public int port() { return serverSock.localAddress().getPort(); } @Override protected void initChannel(Channel ch) { channels.add(ch); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(sslCtx.newHandler(ch.alloc())); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new BehaviorTestChannelHandler()); } private class BehaviorTestChannelHandler extends ChannelDuplexHandler { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { lastRequestReceived = (HttpRequest) msg; HttpResponseStatus status; if (ctx.channel().equals(channels.get(0)) && return500OnFirstRequest) { status = INTERNAL_SERVER_ERROR; } else { status = OK; } FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.wrappedBuffer(CONTENT)); response.headers() .set(CONTENT_TYPE, TEXT_PLAIN) .setInt(CONTENT_LENGTH, response.content().readableBytes()); if (closeConnection) { response.headers().set(CONNECTION, CLOSE); } ctx.writeAndFlush(response); } } } } }