From 371ff8163a3f0c2912c8974e1e57b6cd39f09156 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 20:10:52 +0100 Subject: Add test java source files from jdisc_http_service. --- .../logging/CircularArrayAccessLogKeeperTest.java | 42 + .../yahoo/container/logging/JSONLogTestCase.java | 295 +++++ .../logging/JsonConnectionLogWriterTest.java | 44 + .../container/logging/LogFileHandlerTestCase.java | 208 ++++ .../logging/test/LogFormatterTestCase.java | 27 + .../java/com/yahoo/jdisc/http/CookieTestCase.java | 238 ++++ .../com/yahoo/jdisc/http/HttpHeadersTestCase.java | 17 + .../com/yahoo/jdisc/http/HttpRequestTestCase.java | 206 ++++ .../com/yahoo/jdisc/http/HttpResponseTestCase.java | 139 +++ .../jdisc/http/filter/DiscFilterRequestTest.java | 357 ++++++ .../jdisc/http/filter/DiscFilterResponseTest.java | 113 ++ .../http/filter/EmptyRequestFilterTestCase.java | 48 + .../http/filter/EmptyResponseFilterTestCase.java | 45 + .../jdisc/http/filter/JDiscCookieWrapperTest.java | 29 + .../jdisc/http/filter/RequestViewImplTest.java | 57 + .../jdisc/http/filter/ResponseHeaderFilter.java | 25 + .../filter/SecurityRequestFilterChainTest.java | 145 +++ .../filter/SecurityResponseFilterChainTest.java | 74 ++ .../http/filter/ServletFilterRequestTest.java | 179 +++ .../http/filter/ServletFilterResponseTest.java | 87 ++ .../ConnectorFactoryRegistryModule.java | 54 + .../jdisc/http/guiceModules/ServletModule.java | 24 + .../http/server/jetty/AccessLogRequestLogTest.java | 156 +++ .../http/server/jetty/BlockingQueueRequestLog.java | 24 + .../http/server/jetty/ConnectionThrottlerTest.java | 78 ++ .../http/server/jetty/ConnectorFactoryTest.java | 83 ++ .../jetty/ErrorResponseContentCreatorTest.java | 44 + .../http/server/jetty/ExceptionWrapperTest.java | 51 + .../jdisc/http/server/jetty/FilterTestCase.java | 667 +++++++++++ .../http/server/jetty/HttpRequestFactoryTest.java | 204 ++++ .../jetty/HttpResponseStatisticsCollectorTest.java | 221 ++++ .../server/jetty/HttpServerConformanceTest.java | 847 ++++++++++++++ .../jdisc/http/server/jetty/HttpServerTest.java | 1201 ++++++++++++++++++++ .../http/server/jetty/InMemoryConnectionLog.java | 25 + .../http/server/jetty/InMemoryRequestLog.java | 20 + .../http/server/jetty/JDiscHttpServletTest.java | 80 ++ .../http/server/jetty/MetricConsumerMock.java | 28 + .../jdisc/http/server/jetty/SimpleHttpClient.java | 202 ++++ .../jetty/SslHandshakeFailedListenerTest.java | 42 + .../yahoo/jdisc/http/server/jetty/TestDriver.java | 79 ++ .../yahoo/jdisc/http/server/jetty/TestDrivers.java | 94 ++ .../jetty/servlet/JDiscFilterForServletTest.java | 166 +++ .../jetty/servlet/ServletAccessLoggingTest.java | 64 ++ .../http/server/jetty/servlet/ServletTestBase.java | 132 +++ .../http/ssl/impl/TlsContextBasedProviderTest.java | 71 ++ 45 files changed, 7032 insertions(+) create mode 100644 container-core/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java create mode 100644 container-core/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java create mode 100644 container-core/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java create mode 100644 container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java diff --git a/container-core/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java b/container-core/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java new file mode 100644 index 00000000000..5d9509eb045 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java @@ -0,0 +1,42 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertThat; + +public class CircularArrayAccessLogKeeperTest { + private CircularArrayAccessLogKeeper circularArrayAccessLogKeeper = new CircularArrayAccessLogKeeper(); + + @Test + public void testSizeIsCroppedCorrectly() { + for (int i = 0; i < CircularArrayAccessLogKeeper.SIZE - 1; i++) { + circularArrayAccessLogKeeper.addUri(String.valueOf(i)); + } + assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE -1)); + circularArrayAccessLogKeeper.addUri("foo"); + assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE)); + circularArrayAccessLogKeeper.addUri("bar"); + assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE)); + assertThat(circularArrayAccessLogKeeper.getUris(), hasItems("1", "2", "3", "foo", "bar")); + assertThat(circularArrayAccessLogKeeper.getUris(), not(hasItem("0"))); + } + + @Test + public void testEmpty() { + assertThat(circularArrayAccessLogKeeper.getUris().size(), is(0)); + } + + @Test + public void testSomeItems() { + circularArrayAccessLogKeeper.addUri("a"); + circularArrayAccessLogKeeper.addUri("b"); + circularArrayAccessLogKeeper.addUri("b"); + assertThat(circularArrayAccessLogKeeper.getUris(), contains("a", "b", "b")); + } +} diff --git a/container-core/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java b/container-core/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java new file mode 100644 index 00000000000..cb3d1d0a12f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java @@ -0,0 +1,295 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.yolean.trace.TraceNode; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; + +import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; + + +/** + * @author frodelu + */ +public class JSONLogTestCase { + + private static String ipAddress = "152.200.54.243"; + + private RequestLogEntry.Builder newRequestLogEntry(final String query) { + return newRequestLogEntry(query, new Coverage(100,100,100,0)); + } + private RequestLogEntry.Builder newRequestLogEntry(final String query, Coverage coverage) { + return new RequestLogEntry.Builder() + .rawQuery("query=" + query) + .rawPath("") + .peerAddress(ipAddress) + .httpMethod("GET") + .httpVersion("HTTP/1.1") + .userAgent("Mozilla/4.05 [en] (Win95; I)") + .hitCounts(new HitCounts(0, 10, 1234, 0, 10, coverage)) + .hostString("localhost") + .statusCode(200) + .timestamp(Instant.ofEpochMilli(920880005023L)) + .duration(Duration.ofMillis(122)) + .contentSize(9875) + .localPort(0) + .peerPort(0); + } + + @Test + public void test_json_log_entry() { + RequestLogEntry entry = newRequestLogEntry("test").build(); + + String expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":100,\"documents\":100}" + + "}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + } + @Test + public void test_json_of_trace() { + TraceNode root = new TraceNode("root", 7); + RequestLogEntry entry = newRequestLogEntry("test") + .traceNode(root) + .build(); + + String expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"trace\":{\"timestamp\":0,\"message\":\"root\"}," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":100,\"documents\":100}" + + "}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + } + @Test + public void test_with_keyvalues() { + RequestLogEntry entry = newRequestLogEntry("test") + .addExtraAttribute("singlevalue", "value1") + .addExtraAttribute("multivalue", "value2") + .addExtraAttribute("multivalue", "value3") + .build(); + + String expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":100,\"documents\":100}" + + "}," + + "\"attributes\":{" + + "\"singlevalue\":\"value1\"," + + "\"multivalue\":[\"value2\",\"value3\"]}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + + } + + @Test + public void test_with_remoteaddrport() throws Exception { + RequestLogEntry entry = newRequestLogEntry("test") + .remoteAddress("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") + .build(); + + String expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"remoteaddr\":\"FE80:0000:0000:0000:0202:B3FF:FE1E:8329\"," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":100,\"documents\":100}" + + "}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + + // Add remote port and verify + entry = newRequestLogEntry("test") + .remoteAddress("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") + .remotePort(1234) + .build(); + + expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"remoteaddr\":\"FE80:0000:0000:0000:0202:B3FF:FE1E:8329\"," + + "\"remoteport\":1234," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":100,\"documents\":100}" + + "}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + } + + @Test + public void test_remote_address_same_as_ip_address() throws Exception { + RequestLogEntry entry = newRequestLogEntry("test").build(); + RequestLogEntry entrywithremote = newRequestLogEntry("test") + .remoteAddress(entry.peerAddress().get()) + .build(); + JSONFormatter formatter = new JSONFormatter(); + assertJsonEquals(formatEntry(entry), formatEntry(entrywithremote)); + } + + @Test + public void test_useragent_with_quotes() { + RequestLogEntry entry = new RequestLogEntry.Builder() + .rawQuery("query=test") + .rawPath("") + .peerAddress(ipAddress) + .httpMethod("GET") + .httpVersion("HTTP/1.1") + .userAgent("Mozilla/4.05 [en] (Win95; I; \"Best Browser Ever\")") + .hitCounts(new HitCounts(0, 10, 1234, 0, 10, new Coverage(100, 200, 200, 0))) + .hostString("localhost") + .statusCode(200) + .timestamp(Instant.ofEpochMilli(920880005023L)) + .duration(Duration.ofMillis(122)) + .contentSize(9875) + .localPort(0) + .peerPort(0) + .build(); + + String expectedOutput = + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I; \\\"Best Browser Ever\\\")\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + "\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"non-ideal-state\":true}}" + + "}" + + "}"; + + assertJsonEquals(formatEntry(entry), expectedOutput); + } + + private void verifyCoverage(String coverage, RequestLogEntry entry) { + assertJsonEquals(formatEntry(entry), + "{\"ip\":\"152.200.54.243\"," + + "\"peeraddr\":\"152.200.54.243\"," + + "\"time\":920880005.023," + + "\"duration\":0.122," + + "\"responsesize\":9875," + + "\"code\":200," + + "\"method\":\"GET\"," + + "\"uri\":\"?query=test\"," + + "\"version\":\"HTTP/1.1\"," + + "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + + "\"host\":\"localhost\"," + + "\"scheme\":null," + + "\"localport\":0," + + "\"search\":{" + + "\"totalhits\":1234," + + "\"hits\":0," + + coverage + + "}" + + "}"); + } + + @Test + public void test_with_coverage_degradation() { + verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"non-ideal-state\":true}}", + newRequestLogEntry("test", new Coverage(100,200,200,0)).build()); + verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"match-phase\":true}}", + newRequestLogEntry("test", new Coverage(100,200,200,1)).build()); + verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"timeout\":true}}", + newRequestLogEntry("test", new Coverage(100,200,200,2)).build()); + verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"adaptive-timeout\":true}}", + newRequestLogEntry("test", new Coverage(100,200,200,4)).build()); + } + + private String formatEntry(RequestLogEntry entry) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + new JSONFormatter().write(entry, outputStream); + return outputStream.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java b/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java new file mode 100644 index 00000000000..15118b23f85 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java @@ -0,0 +1,44 @@ +package com.yahoo.container.logging;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +import com.yahoo.test.json.JsonTestHelper; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * @author bjorncs + */ +class JsonConnectionLogWriterTest { + + @Test + void test_serialization() throws IOException { + var id = UUID.randomUUID(); + var instant = Instant.parse("2021-01-13T12:12:12Z"); + ConnectionLogEntry entry = ConnectionLogEntry.builder(id, instant) + .withPeerPort(1234) + .withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure("UNKNOWN", + List.of( + new ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry("javax.net.ssl.SSLHandshakeException", "message"), + new ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry("java.io.IOException", "cause message")))) + .build(); + String expectedJson = "{" + + "\"id\":\""+id.toString()+"\"," + + "\"timestamp\":\"2021-01-13T12:12:12Z\"," + + "\"peerPort\":1234," + + "\"ssl\":{\"handshake-failure\":{\"exception\":[" + + "{\"cause\":\"javax.net.ssl.SSLHandshakeException\",\"message\":\"message\"}," + + "{\"cause\":\"java.io.IOException\",\"message\":\"cause message\"}" + + "],\"type\":\"UNKNOWN\"}}}"; + + JsonConnectionLogWriter writer = new JsonConnectionLogWriter(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + writer.write(entry, out); + String actualJson = out.toString(StandardCharsets.UTF_8); + JsonTestHelper.assertJsonEquals(actualJson, expectedJson); + } +} \ No newline at end of file diff --git a/container-core/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java b/container-core/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java new file mode 100644 index 00000000000..dad8f5e3f90 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java @@ -0,0 +1,208 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.compress.ZstdCompressor; +import com.yahoo.container.logging.LogFileHandler.Compression; +import com.yahoo.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.function.BiFunction; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.zip.GZIPInputStream; + +import static com.yahoo.yolean.Exceptions.uncheck; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotEquals; + +/** + * @author Bob Travis + * @author bjorncs + */ +public class LogFileHandlerTestCase { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testIt() throws IOException { + File root = temporaryFolder.newFolder("logfilehandlertest"); + + String pattern = root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S"; + long[] rTimes = {1000, 2000, 10000}; + LogFileHandler h = new LogFileHandler<>(Compression.NONE, pattern, rTimes, null, 2048, "thread-name", new StringLogWriter()); + long now = System.currentTimeMillis(); + long millisPerDay = 60*60*24*1000; + long tomorrowDays = (now / millisPerDay) +1; + long tomorrowMillis = tomorrowDays * millisPerDay; + + assertThat(tomorrowMillis+1000).isEqualTo(h.logThread.getNextRotationTime(tomorrowMillis)); + assertThat(tomorrowMillis+10000).isEqualTo(h.logThread.getNextRotationTime(tomorrowMillis+3000)); + String message = "test"; + h.publish(message); + h.publish( "another test"); + h.rotateNow(); + h.publish(message); + h.flush(); + h.shutdown(); + } + + @Test + public void testSimpleLogging() throws IOException { + File logFile = temporaryFolder.newFile("testLogFileG1.txt"); + + //create logfilehandler + LogFileHandler h = new LogFileHandler<>(Compression.NONE, logFile.getAbsolutePath(), "0 5 ...", null, 2048, "thread-name", new StringLogWriter()); + + //write log + h.publish("testDeleteFileFirst1"); + h.flush(); + h.shutdown(); + } + + @Test + public void testDeleteFileDuringLogging() throws IOException { + File logFile = temporaryFolder.newFile("testLogFileG2.txt"); + + //create logfilehandler + LogFileHandler h = new LogFileHandler<>(Compression.NONE, logFile.getAbsolutePath(), "0 5 ...", null, 2048, "thread-name", new StringLogWriter()); + + //write log + h.publish("testDeleteFileDuringLogging1"); + h.flush(); + + //delete log file + logFile.delete(); + + //write log again + h.publish("testDeleteFileDuringLogging2"); + h.flush(); + h.shutdown(); + } + + @Test(timeout = /*5 minutes*/300_000) + public void testSymlink() throws IOException, InterruptedException { + File root = temporaryFolder.newFolder("testlogforsymlinkchecking"); + Formatter formatter = new Formatter() { + public String format(LogRecord r) { + DateFormat df = new SimpleDateFormat("yyyy.MM.dd:HH:mm:ss.SSS"); + String timeStamp = df.format(new Date(r.getMillis())); + return ("[" + timeStamp + "]" + " " + formatMessage(r)); + } + }; + LogFileHandler handler = new LogFileHandler<>( + Compression.NONE, root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); + + String message = formatter.format(new LogRecord(Level.INFO, "test")); + handler.publishAndWait(message); + String firstFile = handler.getFileName(); + handler.rotateNow(); + String secondFileName = handler.getFileName(); + assertNotEquals(firstFile, secondFileName); + + String longMessage = formatter.format(new LogRecord(Level.INFO, "string which is way longer than the word test")); + handler.publish(longMessage); + handler.flush(); + assertThat(Files.size(Paths.get(firstFile))).isEqualTo(31); + final long expectedSecondFileLength = 72; + + long symlinkFileLength = Files.size(root.toPath().resolve("symlink")); + assertThat(symlinkFileLength).isEqualTo(expectedSecondFileLength); + handler.shutdown(); + } + + @Test(timeout = /*5 minutes*/300_000) + public void compresses_previous_log_file() throws InterruptedException, IOException { + File root = temporaryFolder.newFolder("compressespreviouslogfile"); + LogFileHandler firstHandler = new LogFileHandler<>( + Compression.ZSTD, root.getAbsolutePath() + "/compressespreviouslogfile.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); + firstHandler.publishAndWait("test"); + firstHandler.shutdown(); + + assertThat(Files.size(Paths.get(firstHandler.getFileName()))).isEqualTo(5); + assertThat(root.toPath().resolve("symlink").toRealPath().toString()).isEqualTo(firstHandler.getFileName()); + + LogFileHandler secondHandler = new LogFileHandler<>( + Compression.ZSTD, root.getAbsolutePath() + "/compressespreviouslogfile.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); + secondHandler.publishAndWait("test"); + secondHandler.rotateNow(); + + assertThat(root.toPath().resolve("symlink").toRealPath().toString()).isEqualTo(secondHandler.getFileName()); + + while (Files.exists(root.toPath().resolve(firstHandler.getFileName()))) Thread.sleep(1); + + assertThat(Files.exists(Paths.get(firstHandler.getFileName() + ".zst"))).isTrue(); + secondHandler.shutdown(); + } + + @Test(timeout = /*5 minutes*/300_000) + public void testcompression_gzip() throws InterruptedException, IOException { + testcompression( + Compression.GZIP, "gz", + (compressedFile, __) -> uncheck(() -> new String(new GZIPInputStream(Files.newInputStream(compressedFile)).readAllBytes()))); + } + + @Test(timeout = /*5 minutes*/300_000) + public void testcompression_zstd() throws InterruptedException, IOException { + testcompression( + Compression.ZSTD, "zst", + (compressedFile, uncompressedSize) -> uncheck(() -> { + ZstdCompressor zstdCompressor = new ZstdCompressor(); + byte[] uncompressedBytes = new byte[uncompressedSize]; + byte[] compressedBytes = Files.readAllBytes(compressedFile); + zstdCompressor.decompress(compressedBytes, 0, compressedBytes.length, uncompressedBytes, 0, uncompressedBytes.length); + return new String(uncompressedBytes); + })); + } + + private void testcompression(Compression compression, + String fileExtension, + BiFunction decompressor) throws IOException, InterruptedException { + File root = temporaryFolder.newFolder("testcompression" + compression.name()); + + LogFileHandler h = new LogFileHandler<>( + compression, root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S%s", new long[]{0}, null, 2048, "thread-name", new StringLogWriter()); + int logEntries = 10000; + for (int i = 0; i < logEntries; i++) { + h.publish("test"); + } + h.flush(); + String f1 = h.getFileName(); + assertThat(f1).startsWith(root.getAbsolutePath() + "/logfilehandlertest."); + File uncompressed = new File(f1); + File compressed = new File(f1 + "." + fileExtension); + assertThat(uncompressed).exists(); + assertThat(compressed).doesNotExist(); + String content = IOUtils.readFile(uncompressed); + assertThat(content).hasLineCount(logEntries); + h.rotateNow(); + while (uncompressed.exists()) { + Thread.sleep(1); + } + assertThat(compressed).exists(); + String uncompressedContent = decompressor.apply(compressed.toPath(), content.getBytes().length); + assertThat(uncompressedContent).isEqualTo(content); + h.shutdown(); + } + + static class StringLogWriter implements LogWriter { + + @Override + public void write(String record, OutputStream outputStream) throws IOException { + outputStream.write(record.getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java b/container-core/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java new file mode 100644 index 00000000000..ecacf95d100 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java @@ -0,0 +1,27 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging.test; + +import com.yahoo.container.logging.LogFormatter; +import org.junit.Test; + +import java.util.Date; + +import static org.junit.Assert.assertEquals; + +/** + * @author Bob Travis + */ +public class LogFormatterTestCase { + + @Test + public void testIt() { + java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC")); + @SuppressWarnings("deprecation") + long time = new Date(103,7,25,13,30,35).getTime(); + String result = LogFormatter.insertDate("test%Y%m%d%H%M%S%x",time); + assertEquals("test20030825133035Aug",result); + result = LogFormatter.insertDate("test%s%T",time); + assertEquals("test000"+time, result); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java new file mode 100644 index 00000000000..dbdce5c704e --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java @@ -0,0 +1,238 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class CookieTestCase { + + @Test + public void requireThatDefaultValuesAreSane() { + Cookie cookie = new Cookie("foo", "bar"); + assertEquals("foo", cookie.getName()); + assertEquals("bar", cookie.getValue()); + assertEquals(null, cookie.getDomain()); + assertEquals(Integer.MIN_VALUE, cookie.getMaxAge(TimeUnit.SECONDS)); + assertEquals(null, cookie.getPath()); + assertEquals(false, cookie.isHttpOnly()); + assertEquals(false, cookie.isSecure()); + } + + @Test + public void requireThatAccessorsWork() { + final Cookie cookie = new Cookie(); + cookie.setName("foo"); + assertEquals("foo", cookie.getName()); + cookie.setName("bar"); + assertEquals("bar", cookie.getName()); + + cookie.setValue("foo"); + assertEquals("foo", cookie.getValue()); + cookie.setValue("bar"); + assertEquals("bar", cookie.getValue()); + + cookie.setDomain("foo"); + assertEquals("foo", cookie.getDomain()); + cookie.setDomain("bar"); + assertEquals("bar", cookie.getDomain()); + + cookie.setPath("foo"); + assertEquals("foo", cookie.getPath()); + cookie.setPath("bar"); + assertEquals("bar", cookie.getPath()); + + cookie.setMaxAge(69, TimeUnit.DAYS); + assertEquals(69, cookie.getMaxAge(TimeUnit.DAYS)); + assertEquals(TimeUnit.DAYS.toHours(69), cookie.getMaxAge(TimeUnit.HOURS)); + + cookie.setSecure(true); + assertTrue(cookie.isSecure()); + cookie.setSecure(false); + assertFalse(cookie.isSecure()); + + cookie.setHttpOnly(true); + assertTrue(cookie.isHttpOnly()); + cookie.setHttpOnly(false); + assertFalse(cookie.isHttpOnly()); + } + + @Test + public void requireThatCopyConstructorWorks() { + final Cookie lhs = newSetCookie("foo"); + final Cookie rhs = new Cookie(lhs); + assertEquals(rhs.getName(), rhs.getName()); + assertEquals(rhs.getValue(), rhs.getValue()); + assertEquals(rhs.getDomain(), rhs.getDomain()); + assertEquals(rhs.getPath(), rhs.getPath()); + assertEquals(rhs.getMaxAge(TimeUnit.MILLISECONDS), rhs.getMaxAge(TimeUnit.MILLISECONDS)); + assertEquals(rhs.isSecure(), rhs.isSecure()); + assertEquals(rhs.isHttpOnly(), rhs.isHttpOnly()); + } + + @Test + public void requireThatHashCodeIsImplemented() { + final Cookie cookie = newCookie("foo"); + assertFalse(cookie.hashCode() == new Cookie().hashCode()); + assertEquals(cookie.hashCode(), cookie.hashCode()); + assertEquals(cookie.hashCode(), new Cookie(cookie).hashCode()); + } + + @Test + public void requireThatEqualsIsImplemented() { + final Cookie cookie = newCookie("foo"); + assertFalse(cookie.equals(new Cookie())); + assertEquals(cookie, cookie); + assertEquals(cookie, new Cookie(cookie)); + } + + @Test + public void requireThatCookieCanBeEncoded() { + assertEncodeCookie( + "foo.name=foo.value", + List.of(newCookie("foo"))); + assertEncodeCookie( + "foo.name=foo.value;bar.name=bar.value", + List.of(newCookie("foo"), newCookie("bar"))); + } + + @Test + public void requireThatSetCookieCanBeEncoded() { + assertEncodeSetCookie( + List.of("foo.name=foo.value; Path=path; Domain=domain; Secure; HttpOnly", + "foo.name=foo.value; Path=path; Domain=domain; Secure; HttpOnly; SameSite=None"), + List.of(newSetCookie("foo"), + newSetCookie("foo").setSameSite(Cookie.SameSite.NONE))); + } + + @Test + public void requireThatCookieCanBeDecoded() { + final Cookie foo = new Cookie(); + foo.setName("foo.name"); + foo.setValue("foo.value"); + assertDecodeCookie(List.of(newCookie("foo")), "foo.name=foo.value"); + + final Cookie bar = new Cookie(); + bar.setName("bar.name"); + bar.setValue("bar.value"); + assertDecodeCookie(List.of(foo, bar),"foo.name=foo.value; bar.name=bar.value"); + } + + @Test + public void requireThatSetCookieCanBeDecoded() { + final Cookie foo = new Cookie(); + foo.setName("foo.name"); + foo.setValue("foo.value"); + foo.setPath("path"); + foo.setDomain("domain"); + foo.setMaxAge(0, TimeUnit.SECONDS); + foo.setSecure(true); + foo.setHttpOnly(true); + assertDecodeSetCookie(foo, "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;"); + + final Cookie bar = new Cookie(); + bar.setName("bar.name"); + bar.setValue("bar.value"); + bar.setPath("path"); + bar.setDomain("domain"); + bar.setMaxAge(0, TimeUnit.SECONDS); + assertDecodeSetCookie(bar, "bar.name=bar.value;Max-Age=0;Path=path;Domain=domain;"); + } + + @Test + public void requireThatCookieDecoderWorksForGenericValidCookies() { + Cookie.fromCookieHeader("Y=v=1&n=8es5opih9ljtk&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + + "us&np=1; T=z=h.nzPBhSP4PBVd5JqacVnIbNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=YAE&sk=DAALShmNQ" + + "vhoZV&ks=EAABsibvMK6ejwn0uUoS4rC9w--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + + "NU5nLS0BYQFZQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAWNOUnZIbEc3ZHZoVHlWZ" + + "0NoXzEwYkxhOVdzcy0Bb2sBWlcwLQF0aXABWUhwTmVDAXp6AWgubnpQQkE3RQ--"); + } + + @Test + public void requireThatCookieDecoderWorksForYInvalidCookies() { + Cookie.fromCookieHeader("Y=v=1&n=77nkr5t7o4nqn&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + + "us&np=1; T=z=05nzPB0NP4PBN/n0gwc1AWGNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=QAE&sk=DAA4R2svo" + + "osjIa&ks=EAAj3nBQFkN4ZmuhqFxJdNoaQ--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + + "NU5nLS0BYQFRQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAUpPalRXOEVsUDZrR3RHT" + + "VZkX29CWk53clJIQS0BdGlwAVlIcE5lQwF6egEwNW56UEJBN0U-"); + } + + @Test + public void requireThatCookieDecoderWorksForYValidCookies() { + Cookie.fromCookieHeader("Y=v=1&n=3767k6te5aj2s&l=1v4u3001uw2ys00q0rw0qrw34q0x5s3u/o&p=030vvit012000000&iz=" + + "&r=pu&lg=en-US,it-IT,it&intl=it&np=1; T=z=m38yPBmLk3PBWvehTPBhBHYNU5OBjQ3NE5ONU5P" + + "NDY0NzU0M0&a=IAE&sk=DAAAx5URYgbhQ6&ks=EAA4rTgdlAGeMQmdYeM_VehGg--~E&d=c2wBTWprNUF" + + "UTXdNems1TWprNE16RXpNREl6TkRneAFhAUlBRQFnAUVJSlNMSzVRM1pWNVNLQVBNRkszQTRaWDZBAXNj" + + "aWQBSUlyZW5paXp4NS4zTUZMMDVlSVhuMjZKYUcwLQFvawFaVzAtAWFsAW1hcmlvYXByZWFAeW1haWwuY" + + "29tAXp6AW0zOHlQQkE3RQF0aXABaXRZOFRE"); + } + + @Test + public void requireThatCookieDecoderWorksForGenericInvalidCookies() { + Cookie.fromCookieHeader("Y=v=1&n=e92s5cq8qbs6h&l=3kdb0f.3@i126be10b.d4j/o&p=m1f2qgmb13000107&r=g5&lg=en-US" + + "&intl=us; T=z=TXp3OBTrQ8OBFMcj3GBpFSyNk83TgY2MjMwN04zMDMw&a=YAE&sk=DAAVfaNwLeISrX" + + "&ks=EAAOeNNgY8c5hV8YzPYmnrW7w--~E&d=c2wBTVRnd09RRXhOVFEzTURrME56UTMBYQFZQUUBZwFMQ" + + "U5NT0Q2UjY2Q0I1STY0R0tKSUdVQVlRRQFvawFaVzAtAXRpcAFMTlRUdkMBenoBVFhwM09CQTdF&af=QU" + + "FBQ0FDQURBd0FCMUNCOUFJQUJBQ0FEQU1IME1nTWhNbiZ0cz0xMzIzMjEwMTk1JnBzPVA1d3NYakh0aVk" + + "2UDMuUGZ6WkdTT2ctLQ--"); + } + + @Test + public void requireMappingBetweenSameSiteAndJettySameSite() { + for (var jdiscSameSite : Cookie.SameSite.values()) { + assertEquals(jdiscSameSite, Cookie.SameSite.fromJettySameSite(jdiscSameSite.jettySameSite())); + } + + for (var jettySameSite : org.eclipse.jetty.http.HttpCookie.SameSite.values()) { + assertEquals(jettySameSite, Cookie.SameSite.fromJettySameSite(jettySameSite).jettySameSite()); + } + } + + private static void assertEncodeCookie(String expectedResult, List cookies) { + String actual = Cookie.toCookieHeader(cookies); + String expectedResult1 = expectedResult; + assertThat(actual, equalTo(expectedResult1)); + } + + private static void assertEncodeSetCookie(List expectedResult, List cookies) { + assertThat(Cookie.toSetCookieHeaders(cookies), containsInAnyOrder(expectedResult.toArray())); + } + + private static void assertDecodeCookie(List expected, String toDecode) { + assertThat(Cookie.fromCookieHeader(toDecode), containsInAnyOrder(expected.toArray())); + } + + private static void assertDecodeSetCookie(final Cookie expected, String toDecode) { + assertThat(Cookie.fromSetCookieHeader(toDecode), equalTo(expected)); + } + + private static Cookie newCookie(final String name) { + final Cookie cookie = new Cookie(); + cookie.setName(name + ".name"); + cookie.setValue(name + ".value"); + return cookie; + } + + private static Cookie newSetCookie(String name) { + final Cookie cookie = new Cookie(); + cookie.setName(name + ".name"); + cookie.setValue(name + ".value"); + cookie.setDomain("domain"); + cookie.setPath("path"); + cookie.setSecure(true); + cookie.setHttpOnly(true); + return cookie; + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java new file mode 100644 index 00000000000..d8ce4a6da0c --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java @@ -0,0 +1,17 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Simon Thoresen Hult + */ +public class HttpHeadersTestCase { + + @Test + public void requireThatHeadersDoNotChange() { + assertEquals("X-JDisc-Disable-Chunking", HttpHeaders.Names.X_DISABLE_CHUNKING); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java new file mode 100644 index 00000000000..a3cb31d5ecb --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java @@ -0,0 +1,206 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.service.CurrentContainer; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Simon Thoresen Hult + */ +public class HttpRequestTestCase { + + @Test + public void requireThatSimpleServerConstructorsUseReasonableDefaults() { + URI uri = URI.create("http://localhost/"); + HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.GET, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); + assertTrue(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + } + + @Test + public void requireThatSimpleClientConstructorsUseReasonableDefaults() { + Request parent = new Request(mockContainer(), URI.create("http://localhost/")); + + URI uri = URI.create("http://remotehost/"); + HttpRequest request = HttpRequest.newClientRequest(parent, uri); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.GET, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + + request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); + assertFalse(request.isServerRequest()); + assertEquals(uri, request.getUri()); + assertEquals(HttpRequest.Method.POST, request.getMethod()); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + } + + @Test + public void requireThatAccessorsWork() { + URI uri = URI.create("http://localhost/path?foo=bar&foo=baz&cox=69"); + InetSocketAddress address = new InetSocketAddress("remotehost", 69); + final HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.GET, + HttpRequest.Version.HTTP_1_1, address, 1L); + assertEquals(uri, request.getUri()); + request.setUri(uri = URI.create("http://remotehost/")); + assertEquals(uri, request.getUri()); + + assertEquals(HttpRequest.Method.GET, request.getMethod()); + request.setMethod(HttpRequest.Method.CONNECT); + assertEquals(HttpRequest.Method.CONNECT, request.getMethod()); + + assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); + request.setVersion(HttpRequest.Version.HTTP_1_0); + assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); + + assertEquals(address, request.getRemoteAddress()); + request.setRemoteAddress(address = new InetSocketAddress("localhost", 96)); + assertEquals(address, request.getRemoteAddress()); + + final URI proxy = URI.create("http://proxyhost/"); + request.setProxyServer(proxy); + assertEquals(proxy, request.getProxyServer()); + + assertNull(request.getConnectionTimeout(TimeUnit.MILLISECONDS)); + request.setConnectionTimeout(1, TimeUnit.SECONDS); + assertEquals(Long.valueOf(1000), request.getConnectionTimeout(TimeUnit.MILLISECONDS)); + + assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); + assertEquals(Collections.singletonList("69"), request.parameters().get("cox")); + request.parameters().put("cox", Arrays.asList("6", "9")); + assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); + assertEquals(Arrays.asList("6", "9"), request.parameters().get("cox")); + + assertEquals(1L, request.getConnectedAt(TimeUnit.MILLISECONDS)); + } + + @Test + public void requireThatHttp10EncodingIsNeverChunked() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.isChunked()); + request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); + assertFalse(request.isChunked()); + } + + @Test + public void requireThatHttp11EncodingIsNotChunkedByDefault() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertFalse(request.isChunked()); + } + + @Test + public void requireThatHttp11EncodingCanBeChunked() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); + assertTrue(request.isChunked()); + } + + @Test + public void requireThatHttp10ConnectionIsAlwaysClose() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.isKeepAlive()); + request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + assertTrue(request.isKeepAlive()); + } + + @Test + public void requireThatHttp11ConnectionIsKeepAliveByDefault() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertTrue(request.isKeepAlive()); + } + + @Test + public void requireThatHttp11ConnectionCanBeClose() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE); + assertFalse(request.isKeepAlive()); + } + + @Test + public void requireThatHttp10NeverHasChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + assertFalse(request.hasChunkedResponse()); + } + + @Test + public void requireThatHttp11HasDefaultChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); + assertTrue(request.hasChunkedResponse()); + } + + @Test + public void requireThatHttp11CanDisableChunkedResponse() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + request.headers().add(com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING, "true"); + assertFalse(request.hasChunkedResponse()); + } + + @Test + public void requireThatCookieHeaderCanBeEncoded() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + final List cookies = Collections.singletonList(new Cookie("foo", "bar")); + request.encodeCookieHeader(cookies); + final List headers = request.headers().get(com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE); + assertEquals(1, headers.size()); + assertEquals(Cookie.toCookieHeader(cookies), headers.get(0)); + } + + @Test + public void requireThatCookieHeaderCanBeDecoded() throws Exception { + final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); + final List cookies = Collections.singletonList(new Cookie("foo", "bar")); + request.encodeCookieHeader(cookies); + assertEquals(cookies, request.decodeCookieHeader()); + } + + private static HttpRequest newRequest(final HttpRequest.Version version) throws Exception { + return HttpRequest.newServerRequest( + mockContainer(), + new URI("http://localhost:1234/status.html"), + HttpRequest.Method.GET, + version); + } + + private static CurrentContainer mockContainer() { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return currentContainer; + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java new file mode 100644 index 00000000000..61499200f3c --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java @@ -0,0 +1,139 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.service.CurrentContainer; +import org.junit.Test; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Simon Thoresen Hult + */ +public class HttpResponseTestCase { + + @Test + public void requireThatAccessorsWork() throws Exception { + final HttpResponse response = newResponse(6, "foo"); + assertEquals(6, response.getStatus()); + assertEquals("foo", response.getMessage()); + assertNull(response.getError()); + assertTrue(response.isChunkedEncodingEnabled()); + + response.setStatus(9); + assertEquals(9, response.getStatus()); + + response.setMessage("bar"); + assertEquals("bar", response.getMessage()); + + final Throwable err = new Throwable(); + response.setError(err); + assertSame(err, response.getError()); + + response.setChunkedEncodingEnabled(false); + assertFalse(response.isChunkedEncodingEnabled()); + } + + @Test + public void requireThatStatusCodesDoNotChange() { + assertEquals(HttpResponse.Status.CREATED, 201); + assertEquals(HttpResponse.Status.ACCEPTED, 202); + assertEquals(HttpResponse.Status.NON_AUTHORITATIVE_INFORMATION, 203); + assertEquals(HttpResponse.Status.NO_CONTENT, 204); + assertEquals(HttpResponse.Status.RESET_CONTENT, 205); + assertEquals(HttpResponse.Status.PARTIAL_CONTENT, 206); + + assertEquals(HttpResponse.Status.MULTIPLE_CHOICES, 300); + assertEquals(HttpResponse.Status.SEE_OTHER, 303); + assertEquals(HttpResponse.Status.NOT_MODIFIED, 304); + assertEquals(HttpResponse.Status.USE_PROXY, 305); + + assertEquals(HttpResponse.Status.PAYMENT_REQUIRED, 402); + assertEquals(HttpResponse.Status.PROXY_AUTHENTICATION_REQUIRED, 407); + assertEquals(HttpResponse.Status.CONFLICT, 409); + assertEquals(HttpResponse.Status.GONE, 410); + assertEquals(HttpResponse.Status.LENGTH_REQUIRED, 411); + assertEquals(HttpResponse.Status.PRECONDITION_FAILED, 412); + assertEquals(HttpResponse.Status.REQUEST_ENTITY_TOO_LARGE, 413); + assertEquals(HttpResponse.Status.REQUEST_URI_TOO_LONG, 414); + assertEquals(HttpResponse.Status.UNSUPPORTED_MEDIA_TYPE, 415); + assertEquals(HttpResponse.Status.REQUEST_RANGE_NOT_SATISFIABLE, 416); + assertEquals(HttpResponse.Status.EXPECTATION_FAILED, 417); + + assertEquals(HttpResponse.Status.BAD_GATEWAY, 502); + assertEquals(HttpResponse.Status.GATEWAY_TIMEOUT, 504); + } + + @Test + public void requireThat5xxIsServerError() { + for (int i = 0; i < 999; ++i) { + assertEquals(i >= 500 && i < 600, HttpResponse.isServerError(new Response(i))); + } + } + + @Test + public void requireThatCookieHeaderCanBeEncoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List cookies = Collections.singletonList(new Cookie("foo", "bar")); + response.encodeSetCookieHeader(cookies); + final List headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); + assertEquals(1, headers.size()); + assertEquals(Cookie.toSetCookieHeaders(cookies), headers); + } + + @Test + public void requireThatMultipleCookieHeadersCanBeEncoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); + response.encodeSetCookieHeader(cookies); + final List headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); + assertEquals(2, headers.size()); + assertEquals(Cookie.toSetCookieHeaders(Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox"))), + headers); + } + + @Test + public void requireThatCookieHeaderCanBeDecoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List cookies = Collections.singletonList(new Cookie("foo", "bar")); + response.encodeSetCookieHeader(cookies); + assertEquals(cookies, response.decodeSetCookieHeader()); + } + + @Test + public void requireThatMultipleCookieHeadersCanBeDecoded() throws Exception { + final HttpResponse response = newResponse(69, "foo"); + final List cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); + response.encodeSetCookieHeader(cookies); + assertEquals(cookies, response.decodeSetCookieHeader()); + } + + private static HttpResponse newResponse(final int status, final String message) throws Exception { + final Request request = HttpRequest.newServerRequest( + mockContainer(), + new URI("http://localhost:1234/status.html"), + HttpRequest.Method.GET, + HttpRequest.Version.HTTP_1_1); + return HttpResponse.newInstance(status, message); + } + + private static CurrentContainer mockContainer() { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return currentContainer; + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java new file mode 100644 index 00000000000..1c05a3f3db2 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java @@ -0,0 +1,357 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Version; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class DiscFilterRequestTest { + + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("example.yahoo.com", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + assertTrue(driver.close()); + return request; + } + + @Test + public void testRequestConstruction(){ + URI uri = URI.create("http://localhost:8080/test?param1=abc"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + httpReq.headers().add("X-Custom-Header", "custom_header"); + List cookies = new ArrayList(); + cookies.add(new Cookie("XYZ", "value")); + cookies.add(new Cookie("ABC", "value")); + httpReq.encodeCookieHeader(cookies); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertSame(request.getParentRequest(), httpReq); + Assert.assertEquals(request.getHeader("X-Custom-Header"),"custom_header"); + Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); + + List c = request.getCookies(); + Assert.assertNotNull(c); + Assert.assertEquals(c.size(), 2); + + Assert.assertEquals(request.getParameter("param1"),"abc"); + Assert.assertNull(request.getParameter("param2")); + Assert.assertEquals(request.getVersion(),Version.HTTP_1_1); + Assert.assertEquals(request.getProtocol(),Version.HTTP_1_1.name()); + Assert.assertNull(request.getRequestedSessionId()); + } + + @Test + public void testRequestConstruction2() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add("some-header", "some-value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + request.addHeader("some-header", "some-value"); + String value = request.getUntreatedHeaders().get("some-header").get(0); + Assert.assertEquals(value,"some-value"); + } + + @Test + public void testRequestAttributes() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr", "some_value"); + + Assert.assertEquals(request.containsAttribute("some_attr"),true); + + Assert.assertEquals(request.getAttribute("some_attr"),"some_value"); + + } + + @Test + public void testGetAttributeNames() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr_1", "some_value1"); + request.setAttribute("some_attr_2", "some_value2"); + + Enumeration e = request.getAttributeNames(); + List attrList = Collections.list(e); + Assert.assertEquals(2, attrList.size()); + Assert.assertEquals(attrList.contains("some_attr_1"), true); + Assert.assertEquals(attrList.contains("some_attr_2"), true); + + } + + @Test + public void testRemoveAttribute() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setAttribute("some_attr", "some_value"); + + Assert.assertEquals(request.containsAttribute("some_attr"),true); + + request.removeAttribute("some_attr"); + + Assert.assertEquals(request.containsAttribute("some_attr"),false); + } + + @Test + public void testGetIntHeader() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(-1, request.getIntHeader("int_header")); + + request.addHeader("int_header", String.valueOf(5)); + + Assert.assertEquals(5, request.getIntHeader("int_header")); + } + + @Test + public void testDateHeader() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + + Assert.assertEquals(-1, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); + + request.addHeader(HttpHeaders.Names.IF_MODIFIED_SINCE, "Sat, 29 Oct 1994 19:43:31 GMT"); + + Assert.assertEquals(783459811000L, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); + } + + @Test + public void testParameterAPIsAsList() { + URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getParameter("param1"),"abc"); + + List values = request.getParameterValuesAsList("param2"); + Assert.assertEquals(values.get(0),"xyz"); + Assert.assertEquals(values.get(1),"pqr"); + + List paramNames = request.getParameterNamesAsList(); + Assert.assertEquals(paramNames.size(), 2); + + } + + @Test + public void testParameterAPI(){ + URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getParameter("param1"),"abc"); + + Enumeration values = request.getParameterValues("param2"); + List valuesList = Collections.list(values); + Assert.assertEquals(valuesList.get(0),"xyz"); + Assert.assertEquals(valuesList.get(1),"pqr"); + + Enumeration paramNames = request.getParameterNames(); + List paramNamesList = Collections.list(paramNames); + Assert.assertEquals(paramNamesList.size(), 2); + } + + @Test + public void testGetHeaderNamesAsList() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); + httpReq.headers().add("header_1", "value1"); + httpReq.headers().add("header_2", "value2"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); + Assert.assertEquals(request.getHeaderNamesAsList().size(), 3); + } + + @Test + public void testGetHeadersAsList() { + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); + Assert.assertEquals(request.getHeaderNamesAsList().size(), 0); + + httpReq.headers().add("header_1", "value1"); + httpReq.headers().add("header_1", "value2"); + + Assert.assertEquals(request.getHeadersAsList("header_1").size(), 2); + } + + @Test + public void testIsMultipart() { + + URI uri = URI.create("http://localhost:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(true,DiscFilterRequest.isMultipart(request)); + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); + + Assert.assertEquals(DiscFilterRequest.isMultipart(null),false); + + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); + } + + @Test + public void testGetRemotePortLocalPort() { + + URI uri = URI.create("http://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + Assert.assertEquals(69, request.getRemotePort()); + Assert.assertEquals(8080, request.getLocalPort()); + + if (request.getRemoteHost() != null) // if we have network + Assert.assertEquals("example.yahoo.com", request.getRemoteHost()); + + request.setRemoteAddr("1.1.1.1"); + + Assert.assertEquals("1.1.1.1",request.getRemoteAddr()); + } + + @Test + public void testCharacterEncoding() throws Exception { + URI uri = URI.create("http://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); + + Assert.assertEquals(request.getCharacterEncoding(), "UTF-8"); + + httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + request = new JdiscFilterRequest(httpReq); + request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html"); + request.setCharacterEncoding("UTF-8"); + + Assert.assertEquals(request.getCharacterEncoding(),"UTF-8"); + + Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); + } + + @Test + public void testSetScheme() throws Exception { + URI uri = URI.create("https://example.yahoo.com:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + + request.setScheme("http", true); + System.out.println(request.getUri().toString()); + Assert.assertEquals(request.getUri().toString(), "http://example.yahoo.com:8080/test"); + } + + @Test + public void testGetServerPort() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.getServerPort(), 80); + + request.setUri(URI.create("https://example.yahoo.com/test")); + Assert.assertEquals(request.getServerPort(), 443); + + } + + @Test + public void testIsSecure() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertEquals(request.isSecure(), false); + + request.setUri(URI.create("https://example.yahoo.com/test")); + Assert.assertEquals(request.isSecure(), true); + + } + + @Test + public void requireThatUnresolvableRemoteAddressesAreSupported() { + URI uri = URI.create("http://doesnotresolve.zzz:8080/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + Assert.assertNull(request.getLocalAddr()); + } + + @Test + public void testGetUntreatedHeaders() { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().add("key1", "value1"); + httpReq.headers().add("key2", Arrays.asList("value1","value2")); + + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + HeaderFields headers = request.getUntreatedHeaders(); + Assert.assertEquals(headers.keySet().size(), 2); + Assert.assertEquals(headers.get("key1").get(0), "value1" ); + Assert.assertEquals(headers.get("key2").get(0), "value1" ); + Assert.assertEquals(headers.get("key2").get(1), "value2" ); + } + + @Test + public void testClearCookies() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.clearCookies(); + Assert.assertNull(request.getHeader(HttpHeaders.Names.COOKIE)); + } + + @Test + public void testGetWrapedCookies() throws Exception { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + JDiscCookieWrapper[] wrappers = request.getWrappedCookies(); + Assert.assertEquals(wrappers.length ,1); + Assert.assertEquals(wrappers[0].getName(), "XYZ"); + Assert.assertEquals(wrappers[0].getValue(), "value"); + } + + @Test + public void testAddCookie() { + URI uri = URI.create("http://example.yahoo.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterRequest request = new JdiscFilterRequest(httpReq); + request.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); + + List cookies = request.getCookies(); + Assert.assertEquals(cookies.size(), 1); + Assert.assertEquals(cookies.get(0).getName(), "name"); + Assert.assertEquals(cookies.get(0).getValue(), "value"); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java new file mode 100644 index 00000000000..b349cb8d803 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java @@ -0,0 +1,113 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class DiscFilterResponseTest { + + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("localhost", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + assertTrue(driver.close()); + return request; + } + + public static HttpResponse newResponse(Request request, int status) { + return HttpResponse.newInstance(status); + } + + @Test + public void testGetSetStatus() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + + Assert.assertEquals(response.getStatus(), HttpResponse.Status.OK); + response.setStatus(HttpResponse.Status.REQUEST_TIMEOUT); + Assert.assertEquals(response.getStatus(), HttpResponse.Status.REQUEST_TIMEOUT); + } + + @Test + public void testAttributes() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + response.setAttribute("attr_1", "value1"); + Assert.assertEquals(response.getAttribute("attr_1"), "value1"); + List list = Collections.list(response.getAttributeNames()); + Assert.assertEquals(list.get(0), "attr_1"); + response.removeAttribute("attr_1"); + Assert.assertNull(response.getAttribute("attr_1")); + } + + @Test + public void testAddHeader() { + HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), + HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); + response.addHeader("header1", "value1"); + Assert.assertEquals(response.getHeader("header1"), "value1"); + } + + @Test + public void testAddCookie() { + URI uri = URI.create("http://example.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); + + List cookies = response.getCookies(); + Assert.assertEquals(cookies.size(),1); + Assert.assertEquals(cookies.get(0).getName(),"name"); + } + + @Test + public void testSetCookie() { + URI uri = URI.create("http://example.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.setCookie("name", "value"); + List cookies = response.getCookies(); + Assert.assertEquals(cookies.size(),1); + Assert.assertEquals(cookies.get(0).getName(),"name"); + + } + + @Test + public void testSetHeader() { + URI uri = URI.create("http://example.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + response.setHeader("name", "value"); + Assert.assertEquals(response.getHeader("name"), "value"); + } + + @Test + public void testGetParentResponse() { + URI uri = URI.create("http://example.com/test"); + HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + HttpResponse httpResp = newResponse(httpReq, 200); + DiscFilterResponse response = new JdiscFilterResponse(httpResp); + Assert.assertSame(response.getParentResponse(), httpResp); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java new file mode 100644 index 00000000000..f4418e74169 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java @@ -0,0 +1,48 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.chain.EmptyRequestFilter; +import com.yahoo.jdisc.service.CurrentContainer; +import org.junit.Test; + +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Simon Thoresen Hult + */ +public class EmptyRequestFilterTestCase { + + @Test + public void requireThatEmptyFilterDoesNothing() throws Exception { + final HttpRequest lhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + final HttpRequest rhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + + EmptyRequestFilter.INSTANCE.filter(rhs, mock(ResponseHandler.class)); + + assertEquals(lhs.headers(), rhs.headers()); + assertEquals(lhs.context(), rhs.context()); + assertEquals(lhs.getTimeout(TimeUnit.MILLISECONDS), rhs.getTimeout(TimeUnit.MILLISECONDS)); + assertEquals(lhs.parameters(), rhs.parameters()); + assertEquals(lhs.getMethod(), rhs.getMethod()); + assertEquals(lhs.getVersion(), rhs.getVersion()); + assertEquals(lhs.getRemoteAddress(), rhs.getRemoteAddress()); + } + + private static HttpRequest newRequest( + final Method method, final String uri, final Version version) { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java new file mode 100644 index 00000000000..e6d7259ea41 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java @@ -0,0 +1,45 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.filter.chain.EmptyResponseFilter; +import com.yahoo.jdisc.service.CurrentContainer; +import org.junit.Test; + +import java.net.URI; + +import static com.yahoo.jdisc.http.HttpRequest.Method; +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Simon Thoresen Hult + */ +public class EmptyResponseFilterTestCase { + + @Test + public void requireThatEmptyFilterDoesNothing() throws Exception { + final HttpRequest request = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); + final HttpResponse lhs = HttpResponse.newInstance(Response.Status.OK); + final HttpResponse rhs = HttpResponse.newInstance(Response.Status.OK); + + EmptyResponseFilter.INSTANCE.filter(lhs, null); + + assertEquals(lhs.headers(), rhs.headers()); + assertEquals(lhs.context(), rhs.context()); + assertEquals(lhs.getError(), rhs.getError()); + assertEquals(lhs.getMessage(), rhs.getMessage()); + } + + private static HttpRequest newRequest(final Method method, final String uri, final Version version) { + final CurrentContainer currentContainer = mock(CurrentContainer.class); + when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); + return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java new file mode 100644 index 00000000000..9948e5bfe7f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java @@ -0,0 +1,29 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +public class JDiscCookieWrapperTest { + + @Test + public void requireThatWrapWorks() { + Cookie cookie = new Cookie("name", "value"); + JDiscCookieWrapper wrapper = JDiscCookieWrapper.wrap(cookie); + + wrapper.setDomain("yahoo.com"); + wrapper.setMaxAge(10); + wrapper.setPath("/path"); + + Assert.assertEquals(wrapper.getName(), cookie.getName()); + Assert.assertEquals(wrapper.getValue(), cookie.getValue()); + Assert.assertEquals(wrapper.getDomain(), cookie.getDomain()); + Assert.assertEquals(wrapper.getMaxAge(), cookie.getMaxAge(TimeUnit.SECONDS)); + Assert.assertEquals(wrapper.getPath(), cookie.getPath()); + Assert.assertEquals(wrapper.getSecure(), cookie.isSecure()); + + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java new file mode 100644 index 00000000000..ec0e0a33d35 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java @@ -0,0 +1,57 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.google.common.collect.Lists; +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.http.filter.SecurityResponseFilterChain.RequestViewImpl; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author gjoranv + */ +public class RequestViewImplTest { + + @Test + public void header_from_the_parent_request_is_available() throws Exception { + final String HEADER = "single-header"; + + HeaderFields parentHeaders = new HeaderFields(); + parentHeaders.add(HEADER, "value"); + + RequestView requestView = newRequestView(parentHeaders); + + assertEquals(requestView.getFirstHeader(HEADER).get(), "value"); + assertEquals(requestView.getHeaders(HEADER).size(), 1); + assertEquals(requestView.getHeaders(HEADER).get(0), "value"); + } + + + @Test + public void multi_value_header_from_the_parent_request_is_available() throws Exception { + final String HEADER = "list-header"; + + HeaderFields parentHeaders = new HeaderFields(); + parentHeaders.add(HEADER, Lists.newArrayList("one", "two")); + + RequestView requestView = newRequestView(parentHeaders); + + assertEquals(requestView.getHeaders(HEADER).size(), 2); + assertEquals(requestView.getHeaders(HEADER).get(0), "one"); + assertEquals(requestView.getHeaders(HEADER).get(1), "two"); + + assertEquals(requestView.getFirstHeader(HEADER).get(), "one"); + } + + private static RequestView newRequestView(HeaderFields parentHeaders) { + Request request = mock(Request.class); + when(request.headers()).thenReturn(parentHeaders); + + return new RequestViewImpl(request); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java new file mode 100644 index 00000000000..3855c3a494b --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java @@ -0,0 +1,25 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; + +/** + * @author Simon Thoresen Hult + */ +public class ResponseHeaderFilter extends AbstractResource implements ResponseFilter { + + private final String key; + private final String val; + + public ResponseHeaderFilter(String key, String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(Response response, Request request) { + response.headers().add(key, val); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java new file mode 100644 index 00000000000..be19313dee2 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java @@ -0,0 +1,145 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author bjorncs + */ +public class SecurityRequestFilterChainTest { + + + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("java.corp.yahoo.com", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + Assert.assertTrue(driver.close()); + return request; + } + + @Test + public void testFilterChainConstruction() { + SecurityRequestFilterChain chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(); + assertEquals(chain.getFilters().size(),0); + + List requestFilters = new ArrayList(); + chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(); + + chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(new RequestHeaderFilter("abc", "xyz"), + new RequestHeaderFilter("pqr", "def")); + + assertEquals(chain instanceof SecurityRequestFilterChain, true); + } + + + @Test + public void testFilterChainRun() { + RequestFilter chain = SecurityRequestFilterChain.newInstance(new RequestHeaderFilter("abc", "xyz"), + new RequestHeaderFilter("pqr", "def")); + + assertEquals(chain instanceof SecurityRequestFilterChain, true); + ResponseHandler handler = newResponseHandler(); + HttpRequest request = newRequest(URI.create("http://test/test"), HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + chain.filter(request, handler); + Assert.assertTrue(request.headers().contains("abc", "xyz")); + Assert.assertTrue(request.headers().contains("pqr", "def")); + } + + @Test + public void testFilterChainResponds() { + RequestFilter chain = SecurityRequestFilterChain.newInstance( + new MyFilter(), + new RequestHeaderFilter("abc", "xyz"), + new RequestHeaderFilter("pqr", "def")); + + assertEquals(chain instanceof SecurityRequestFilterChain, true); + ResponseHandler handler = newResponseHandler(); + HttpRequest request = newRequest(URI.create("http://test/test"), HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + chain.filter(request, handler); + Response response = getResponse(handler); + Assert.assertNotNull(response); + Assert.assertTrue(!request.headers().contains("abc", "xyz")); + Assert.assertTrue(!request.headers().contains("pqr", "def")); + } + + private class RequestHeaderFilter extends AbstractResource implements SecurityRequestFilter { + + private final String key; + private final String val; + + public RequestHeaderFilter(String key, String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(DiscFilterRequest request, ResponseHandler handler) { + request.setHeaders(key, val); + } + } + + private class MyFilter extends AbstractResource implements SecurityRequestFilter { + + @Override + public void filter(DiscFilterRequest request, ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.FORBIDDEN).dispatch(handler); + } + } + + private static ResponseHandler newResponseHandler() { + return new NonWorkingResponseHandler(); + } + + private static Response getResponse(ResponseHandler handler) { + return ((NonWorkingResponseHandler) handler).getResponse(); + } + + private static class NonWorkingResponseHandler implements ResponseHandler { + + private Response response = null; + + @Override + public ContentChannel handleResponse(Response response) { + this.response = response; + return new NonWorkingContentChannel(); + } + + public Response getResponse() { + return response; + } + } + + private static class NonWorkingContentChannel implements ContentChannel { + + @Override + public void close(CompletionHandler handler) { + + } + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + + } + + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java new file mode 100644 index 00000000000..25291de5cc1 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java @@ -0,0 +1,74 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.test.TestDriver; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author bjorncs + */ +public class SecurityResponseFilterChainTest { + private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { + InetSocketAddress address = new InetSocketAddress("java.corp.yahoo.com", 69); + TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); + driver.activateContainer(driver.newContainerBuilder()); + HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); + request.release(); + assertTrue(driver.close()); + return request; + } + + @Test + public void testFilterChainConstruction() { + SecurityResponseFilterChain chain = (SecurityResponseFilterChain)SecurityResponseFilterChain.newInstance(); + assertEquals(chain.getFilters().size(),0); + + chain = (SecurityResponseFilterChain)SecurityResponseFilterChain.newInstance(new ResponseHeaderFilter("abc", "xyz"), + new ResponseHeaderFilter("pqr", "def")); + + assertEquals(chain instanceof SecurityResponseFilterChain, true); + } + + @Test + public void testFilterChainRun() { + URI uri = URI.create("http://localhost:8080/echo"); + HttpRequest request = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); + Response response = HttpResponse.newInstance(Response.Status.OK); + + ResponseFilter chain = SecurityResponseFilterChain.newInstance(new ResponseHeaderFilter("abc", "xyz"), + new ResponseHeaderFilter("pqr", "def")); + chain.filter(response, null); + assertTrue(response.headers().contains("abc", "xyz")); + assertTrue(response.headers().contains("pqr", "def")); + } + + private class ResponseHeaderFilter extends AbstractResource implements SecurityResponseFilter { + + private final String key; + private final String val; + + public ResponseHeaderFilter(String key, String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(DiscFilterResponse response, RequestView request) { + response.setHeaders(key, val); + } + + } + + + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java new file mode 100644 index 00000000000..3052902f174 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java @@ -0,0 +1,179 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import org.eclipse.jetty.server.HttpConnection; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.yahoo.jdisc.http.HttpRequest.Version; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +/** + * Test the parts of the DiscFilterRequest API that are implemented + * by ServletFilterRequest, both directly and indirectly via + * {@link com.yahoo.jdisc.http.servlet.ServletRequest}. + * + * @author gjoranv + * @since 5.27 + */ +public class ServletFilterRequestTest { + + private final String host = "host1"; + private final int port = 8080; + private final String path = "/path1"; + private final String paramName = "param1"; + private final String paramValue = "p1"; + private final String listParamName = "listParam"; + private final String[] listParamValue = new String[]{"1", "2"}; + private final String headerName = "header1"; + private final String headerValue = "h1"; + private final String attributeName = "attribute1"; + private final String attributeValue = "a1"; + + private URI uri; + private DiscFilterRequest filterRequest; + private ServletRequest parentRequest; + + @Before + public void init() throws Exception { + uri = new URI("http", null, host, port, path, paramName + "=" + paramValue, null); + + filterRequest = new ServletFilterRequest(newServletRequest()); + parentRequest = ((ServletFilterRequest)filterRequest).getServletRequest(); + } + + private ServletRequest newServletRequest() throws Exception { + MockHttpServletRequest parent = new MockHttpServletRequest("GET", uri.toString()); + parent.setProtocol(Version.HTTP_1_1.toString()); + parent.setRemoteHost(host); + parent.setRemotePort(port); + parent.setParameter(paramName, paramValue); + parent.setParameter(listParamName, listParamValue); + parent.addHeader(headerName, headerValue); + parent.setAttribute(attributeName, attributeValue); + HttpConnection connection = Mockito.mock(HttpConnection.class); + when(connection.getCreatedTimeStamp()).thenReturn(System.currentTimeMillis()); + parent.setAttribute("org.eclipse.jetty.server.HttpConnection", connection); + return new ServletRequest(parent, uri); + } + + @Test + public void parent_properties_are_propagated_to_disc_filter_request() throws Exception { + assertEquals(filterRequest.getVersion(), Version.HTTP_1_1); + assertEquals(filterRequest.getMethod(), "GET"); + assertEquals(filterRequest.getUri(), uri); + assertEquals(filterRequest.getRemoteHost(), host); + assertEquals(filterRequest.getRemotePort(), port); + assertEquals(filterRequest.getRequestURI(), path); // getRequestUri return only the path by design + + assertEquals(filterRequest.getParameter(paramName), paramValue); + assertEquals(filterRequest.getParameterMap().get(paramName), + Collections.singletonList(paramValue)); + assertEquals(filterRequest.getParameterValuesAsList(listParamName), Arrays.asList(listParamValue)); + + assertEquals(filterRequest.getHeader(headerName), headerValue); + assertEquals(filterRequest.getAttribute(attributeName), attributeValue); + } + + @Test + public void untreatedHeaders_is_populated_from_the_parent_request() { + assertEquals(filterRequest.getUntreatedHeaders().getFirst(headerName), headerValue); + } + + @Test + public void uri_can_be_set() throws Exception { + URI newUri = new URI("http", null, host, port + 1, path, paramName + "=" + paramValue, null); + filterRequest.setUri(newUri); + + assertEquals(filterRequest.getUri(), newUri); + assertEquals(parentRequest.getUri(), newUri); + } + + @Test + public void attributes_can_be_set() throws Exception { + String name = "newAttribute"; + String value = name + "Value"; + filterRequest.setAttribute(name, value); + + assertEquals(filterRequest.getAttribute(name), value); + assertEquals(parentRequest.getAttribute(name), value); + } + + @Test + public void attributes_can_be_removed() { + filterRequest.removeAttribute(attributeName); + + assertEquals(filterRequest.getAttribute(attributeName), null); + assertEquals(parentRequest.getAttribute(attributeName), null); + } + + @Test + public void headers_can_be_set() throws Exception { + String name = "myHeader"; + String value = name + "Value"; + filterRequest.setHeaders(name, value); + + assertEquals(filterRequest.getHeader(name), value); + assertEquals(parentRequest.getHeader(name), value); + } + + @Test + public void headers_can_be_removed() throws Exception { + filterRequest.removeHeaders(headerName); + + assertEquals(filterRequest.getHeader(headerName), null); + assertEquals(parentRequest.getHeader(headerName), null); + } + + @Test + public void headers_can_be_added() { + String value = "h2"; + filterRequest.addHeader(headerName, value); + + List expected = Arrays.asList(headerValue, value); + assertEquals(filterRequest.getHeadersAsList(headerName), expected); + assertEquals(Collections.list(parentRequest.getHeaders(headerName)), expected); + } + + @Test + public void cookies_can_be_added_and_removed() { + Cookie cookie = new Cookie("name", "value"); + filterRequest.addCookie(JDiscCookieWrapper.wrap(cookie)); + + assertEquals(filterRequest.getCookies(), Collections.singletonList(cookie)); + assertEquals(parentRequest.getCookies().length, 1); + + javax.servlet.http.Cookie servletCookie = parentRequest.getCookies()[0]; + assertEquals(servletCookie.getName(), cookie.getName()); + assertEquals(servletCookie.getValue(), cookie.getValue()); + + filterRequest.clearCookies(); + assertTrue(filterRequest.getCookies().isEmpty()); + assertEquals(parentRequest.getCookies().length, 0); + } + + @Test + public void character_encoding_can_be_set() throws Exception { + // ContentType must be non-null before setting character encoding + filterRequest.setHeaders(HttpHeaders.Names.CONTENT_TYPE, ""); + + String encoding = "myEncoding"; + filterRequest.setCharacterEncoding(encoding); + + assertTrue(filterRequest.getCharacterEncoding().contains(encoding)); + assertTrue(parentRequest.getCharacterEncoding().contains(encoding)); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java new file mode 100644 index 00000000000..a2bc2badea3 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java @@ -0,0 +1,87 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletResponse; +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; + +/** + * @author gjoranv + * @since 5.27 + */ +public class ServletFilterResponseTest { + + private final String headerName = "header1"; + private final String headerValue = "h1"; + + private DiscFilterResponse filterResponse; + private ServletResponse parentResponse; + + @Before + public void init() throws Exception { + filterResponse = new ServletFilterResponse(newServletResponse()); + parentResponse = ((ServletFilterResponse)filterResponse).getServletResponse(); + + } + + private ServletResponse newServletResponse() throws Exception { + MockServletResponse parent = new MockServletResponse(); + parent.addHeader(headerName, headerValue); + return new ServletResponse(parent); + } + + + @Test + public void headers_can_be_set() throws Exception { + String name = "myHeader"; + String value = name + "Value"; + filterResponse.setHeaders(name, value); + + assertEquals(filterResponse.getHeader(name), value); + assertEquals(parentResponse.getHeader(name), value); + } + + @Test + public void headers_can_be_added() throws Exception { + String newValue = "h2"; + filterResponse.addHeader(headerName, newValue); + + // The DiscFilterResponse has no getHeaders() + assertEquals(filterResponse.getHeader(headerName), newValue); + + assertEquals(parentResponse.getHeaders(headerName), Arrays.asList(headerValue, newValue)); + } + + @Test + public void headers_can_be_removed() throws Exception { + filterResponse.removeHeaders(headerName); + + assertEquals(filterResponse.getHeader(headerName), null); + assertEquals(parentResponse.getHeader(headerName), null); + } + + @Test + public void set_cookie_overwrites_old_values() { + Cookie to_be_removed = new Cookie("to-be-removed", ""); + Cookie to_keep = new Cookie("to-keep", ""); + filterResponse.setCookie(to_be_removed.getName(), to_be_removed.getValue()); + filterResponse.setCookie(to_keep.getName(), to_keep.getValue()); + + assertEquals(filterResponse.getCookies(), Arrays.asList(to_keep)); + assertEquals(parentResponse.getHeaders(HttpHeaders.Names.SET_COOKIE), Arrays.asList(to_keep.toString())); + } + + + private static class MockServletResponse extends org.eclipse.jetty.server.Response { + private MockServletResponse() { + super(null, null); + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java b/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java new file mode 100644 index 00000000000..cc2a00c08c6 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java @@ -0,0 +1,54 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.guiceModules; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Builder; + +import com.yahoo.jdisc.http.server.jetty.ConnectorFactory; +import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; + +/** + * Guice module for test ConnectorFactories + * + * @author Tony Vaagenes + */ +public class ConnectorFactoryRegistryModule implements Module { + + private final Builder connectorConfigBuilder; + + public ConnectorFactoryRegistryModule(Builder connectorConfigBuilder) { + this.connectorConfigBuilder = connectorConfigBuilder; + } + + public ConnectorFactoryRegistryModule() { + this(new Builder()); + } + + @Provides + public ComponentRegistry connectorFactoryComponentRegistry() { + ComponentRegistry registry = new ComponentRegistry<>(); + registry.register(ComponentId.createAnonymousComponentId("connector-factory"), + new StaticKeyDbConnectorFactory(new ConnectorConfig(connectorConfigBuilder))); + + registry.freeze(); + return registry; + } + + @Override + public void configure(Binder binder) { + } + + private static class StaticKeyDbConnectorFactory extends ConnectorFactory { + + public StaticKeyDbConnectorFactory(ConnectorConfig connectorConfig) { + super(connectorConfig, new ConfiguredSslContextFactoryProvider(connectorConfig)); + } + + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java b/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java new file mode 100644 index 00000000000..dd6511d1f88 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java @@ -0,0 +1,24 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.guiceModules; + +import com.google.inject.Binder; +import com.google.inject.Module; +import com.google.inject.Provides; +import com.yahoo.component.provider.ComponentRegistry; + +import org.eclipse.jetty.servlet.ServletHolder; + +/** + * @author Tony Vaagenes + */ +public class ServletModule implements Module { + @Override + public void configure(Binder binder) { + } + + @Provides + public ComponentRegistry servletHolderComponentRegistry() { + return new ComponentRegistry<>(); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java new file mode 100644 index 00000000000..6370912af48 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java @@ -0,0 +1,156 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Oyvind Bakksjo + * @author bjorncs + */ +public class AccessLogRequestLogTest { + @Test + public void requireThatQueryWithUnquotedSpecialCharactersIsHandled() { + final Request jettyRequest = createRequestMock(); + when(jettyRequest.getRequestURI()).thenReturn("/search/"); + when(jettyRequest.getQueryString()).thenReturn("query=year:>2010"); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + + assertThat(entry.rawPath().get(), is(not(nullValue()))); + assertTrue(entry.rawQuery().isPresent()); + } + + @Test + public void requireThatDoubleQuotingIsNotPerformed() { + final Request jettyRequest = createRequestMock(); + final String path = "/search/"; + when(jettyRequest.getRequestURI()).thenReturn(path); + final String query = "query=year%252010+%3B&customParameter=something"; + when(jettyRequest.getQueryString()).thenReturn(query); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + + assertThat(entry.rawPath().get(), is(path)); + assertThat(entry.rawQuery().get(), is(query)); + + } + + @Test + public void raw_path_and_query_are_set_from_request() { + Request jettyRequest = createRequestMock(); + String rawPath = "//search/"; + when(jettyRequest.getRequestURI()).thenReturn(rawPath); + String rawQuery = "q=%%2"; + when(jettyRequest.getQueryString()).thenReturn(rawQuery); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + assertThat(entry.rawPath().get(), is(rawPath)); + Optional actualRawQuery = entry.rawQuery(); + assertThat(actualRawQuery.isPresent(), is(true)); + assertThat(actualRawQuery.get(), is(rawQuery)); + } + + @Test + public void verify_x_forwarded_for_precedence () { + Request jettyRequest = createRequestMock(); + when(jettyRequest.getRequestURI()).thenReturn("//search/"); + when(jettyRequest.getQueryString()).thenReturn("q=%%2"); + when(jettyRequest.getHeader("x-forwarded-for")).thenReturn("1.2.3.4"); + when(jettyRequest.getHeader("y-ra")).thenReturn("2.3.4.5"); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + assertThat(entry.remoteAddress().get(), is("1.2.3.4")); + } + + @Test + public void verify_x_forwarded_port_precedence () { + Request jettyRequest = createRequestMock(); + when(jettyRequest.getRequestURI()).thenReturn("//search/"); + when(jettyRequest.getQueryString()).thenReturn("q=%%2"); + when(jettyRequest.getHeader("X-Forwarded-Port")).thenReturn("80"); + when(jettyRequest.getHeader("y-rp")).thenReturn("8080"); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + assertThat(entry.remotePort().getAsInt(), is(80)); + } + + @Test + public void defaults_to_peer_port_if_remote_port_header_is_invalid() { + final Request jettyRequest = createRequestMock(); + when(jettyRequest.getRequestURI()).thenReturn("/search/"); + when(jettyRequest.getHeader("X-Forwarded-Port")).thenReturn("8o8o"); + when(jettyRequest.getRemotePort()).thenReturn(80); + + InMemoryRequestLog requestLog = new InMemoryRequestLog(); + doAccessLoggingOfRequest(requestLog, jettyRequest); + RequestLogEntry entry = requestLog.entries().get(0); + assertFalse(entry.remotePort().isPresent()); + assertThat(entry.peerPort().getAsInt(), is(80)); + } + + private void doAccessLoggingOfRequest(RequestLog requestLog, Request jettyRequest) { + ServerConfig.AccessLog config = new ServerConfig.AccessLog( + new ServerConfig.AccessLog.Builder() + .remoteAddressHeaders(List.of("x-forwarded-for", "y-ra")) + .remotePortHeaders(List.of("X-Forwarded-Port", "y-rp"))); + new AccessLogRequestLog(requestLog, config).log(jettyRequest, createResponseMock()); + } + + private static Request createRequestMock() { + JDiscServerConnector serverConnector = mock(JDiscServerConnector.class); + int localPort = 1234; + when(serverConnector.connectorConfig()).thenReturn(new ConnectorConfig(new ConnectorConfig.Builder().listenPort(localPort))); + when(serverConnector.getLocalPort()).thenReturn(localPort); + HttpConnection httpConnection = mock(HttpConnection.class); + when(httpConnection.getConnector()).thenReturn(serverConnector); + Request request = mock(Request.class); + when(request.getMethod()).thenReturn("GET"); + when(request.getRemoteAddr()).thenReturn("localhost"); + when(request.getRemotePort()).thenReturn(12345); + when(request.getProtocol()).thenReturn("HTTP/1.1"); + when(request.getScheme()).thenReturn("http"); + when(request.getTimeStamp()).thenReturn(0L); + when(request.getAttribute(JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY)).thenReturn(new AccessLogEntry()); + when(request.getAttribute("org.eclipse.jetty.server.HttpConnection")).thenReturn(httpConnection); + return request; + } + + private Response createResponseMock() { + Response response = mock(Response.class); + when(response.getHttpChannel()).thenReturn(mock(HttpChannel.class)); + when(response.getCommittedMetaData()).thenReturn(mock(MetaData.Response.class)); + return response; + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java new file mode 100644 index 00000000000..c1a2bea8ac4 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java @@ -0,0 +1,24 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; + +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +/** + * @author bjorncs + */ +class BlockingQueueRequestLog implements RequestLog { + + private final BlockingQueue entries = new LinkedBlockingDeque<>(); + + @Override public void log(RequestLogEntry entry) { entries.offer(entry); } + + RequestLogEntry poll(Duration timeout) throws InterruptedException { + return entries.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java new file mode 100644 index 00000000000..65eb7e1c145 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java @@ -0,0 +1,78 @@ +// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.ConnectorConfig; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.statistic.RateStatistic; +import org.eclipse.jetty.util.thread.Scheduler; +import org.junit.Test; + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +/** + * @author bjorncs + */ +public class ConnectionThrottlerTest { + + @Test + public void throttles_when_any_resource_check_exceeds_configured_threshold() { + Runtime runtime = mock(Runtime.class); + when(runtime.maxMemory()).thenReturn(100l); + RateStatistic rateStatistic = new RateStatistic(1, TimeUnit.HOURS); + MockScheduler scheduler = new MockScheduler(); + ConnectorConfig.Throttling config = new ConnectorConfig.Throttling(new ConnectorConfig.Throttling.Builder() + .maxHeapUtilization(0.8) + .maxAcceptRate(1)); + + AbstractConnector connector = mock(AbstractConnector.class); + + ConnectionThrottler throttler = new ConnectionThrottler(runtime, rateStatistic, scheduler, connector, config); + + // Heap utilization above configured threshold, but connection rate below threshold. + when(runtime.freeMemory()).thenReturn(10l); + when(connector.isAccepting()).thenReturn(true); + throttler.onAccepting(null); + assertNotNull(scheduler.task); + verify(connector).setAccepting(false); + + // Heap utilization below threshold, but connection rate above threshold. + when(runtime.freeMemory()).thenReturn(80l); + rateStatistic.record(); + rateStatistic.record(); // above accept rate limit (2 > 1) + scheduler.task.run(); // run unthrottleIfBelowThresholds() + verify(connector, times(1)).setAccepting(anyBoolean()); // verify setAccepting has not been called any mores times + + // Both heap utilization and accept rate below threshold + when(runtime.freeMemory()).thenReturn(80l); + when(connector.isAccepting()).thenReturn(false); + rateStatistic.reset(); + scheduler.task.run(); // run unthrottleIfBelowThresholds() + verify(connector).setAccepting(true); + + // Both heap utilization and accept rate below threshold + when(connector.isAccepting()).thenReturn(true); + when(runtime.freeMemory()).thenReturn(80l); + rateStatistic.record(); + throttler.onAccepting(null); + verify(connector, times(2)).setAccepting(anyBoolean()); // verify setAccepting has not been called any mores times + } + + private static class MockScheduler extends AbstractLifeCycle implements Scheduler { + Runnable task; + + @Override + public Task schedule(Runnable task, long delay, TimeUnit units) { + this.task = task; + return () -> false; + } + } + +} \ No newline at end of file diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java new file mode 100644 index 00000000000..df794c7ecb8 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java @@ -0,0 +1,83 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * @author Einar M R Rosenvinge + */ +public class ConnectorFactoryTest { + + @Test + public void requireThatServerCanBindChannel() throws Exception { + Server server = new Server(); + try { + ConnectorConfig config = new ConnectorConfig(new ConnectorConfig.Builder()); + ConnectorFactory factory = createConnectorFactory(config); + JettyConnectionLogger connectionLogger = new JettyConnectionLogger( + new ServerConfig.ConnectionLog.Builder().enabled(false).build(), + new VoidConnectionLog()); + JDiscServerConnector connector = + (JDiscServerConnector)factory.createConnector(new DummyMetric(), server, connectionLogger); + server.addConnector(connector); + server.setHandler(new HelloWorldHandler()); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(null, connector.getLocalPort(), false); + SimpleHttpClient.RequestExecutor ex = client.newGet("/blaasdfnb"); + SimpleHttpClient.ResponseValidator val = ex.execute(); + val.expectContent(equalTo("Hello world")); + } finally { + try { + server.stop(); + } catch (Exception e) { + //ignore + } + } + } + + private static ConnectorFactory createConnectorFactory(ConnectorConfig config) { + return new ConnectorFactory(config, new ConfiguredSslContextFactoryProvider(config)); + } + + private static class HelloWorldHandler extends AbstractHandler { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { + response.getWriter().write("Hello world"); + response.getWriter().flush(); + response.getWriter().close(); + baseRequest.setHandled(true); + } + } + + private static class DummyMetric implements Metric { + @Override + public void set(String key, Number val, Context ctx) { } + + @Override + public void add(String key, Number val, Context ctx) { } + + @Override + public Context createContext(Map properties) { + return new DummyContext(); + } + } + + private static class DummyContext implements Metric.Context { + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java new file mode 100644 index 00000000000..d66f22801f7 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java @@ -0,0 +1,44 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + + +import org.junit.Test; + +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; + + +/** + * @author bjorncs + */ +public class ErrorResponseContentCreatorTest { + + @Test + public void response_content_matches_expected_string() { + String expectedHtml = + "\n" + + "\n" + + "\n" + + "Error 200\n" + + "\n" + + "\n" + + "

HTTP ERROR: 200

\n" + + "

Problem accessing http://foo.bar. Reason:\n" + + "

    My custom error message

\n" + + "
\n" + + "\n" + + "\n"; + + ErrorResponseContentCreator c = new ErrorResponseContentCreator(); + byte[] rawContent = c.createErrorContent( + "http://foo.bar", + HttpServletResponse.SC_OK, + Optional.of("My custom error message")); + String actualHtml = new String(rawContent, StandardCharsets.ISO_8859_1); + assertEquals(expectedHtml, actualHtml); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java new file mode 100644 index 00000000000..de8df283afe --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java @@ -0,0 +1,51 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Check basic error message formatting. Do note these tests are sensitive to + * the line numbering in this file. (And that's a feature, not a bug.) + * + * @author Steinar Knutsen + */ +public class ExceptionWrapperTest { + + @Test + public final void requireNoMessageIsOK() { + final Throwable t = new Throwable(); + final ExceptionWrapper e = new ExceptionWrapper(t); + final String expected = "Throwable() at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:19)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireAllWrappedLevelsShowUp() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable("t1", t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:30):" + + " Throwable(\"t1\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:29):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:28)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireMixOfMessageAndNoMessageWorks() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable(t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:43):" + + " Throwable(\"java.lang.Throwable: t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:42):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:41)"; + + assertThat(e.getMessage(), equalTo(expected)); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java new file mode 100644 index 00000000000..a67656dd5ca --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java @@ -0,0 +1,667 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.filter.ResponseHeaderFilter; +import com.yahoo.jdisc.http.filter.chain.RequestFilterChain; +import com.yahoo.jdisc.http.filter.chain.ResponseFilterChain; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Oyvind Bakksjo + * @author bjorncs + */ +public class FilterTestCase { + @Test + public void requireThatRequestFilterIsNotRunOnUnboundPath() throws Exception { + RequestFilterMockBase filter = mock(RequestFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterIsRunOnBoundPath() throws Exception { + final RequestFilter filter = mock(RequestFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChangesAreSeenByRequestHandler() throws Exception { + final RequestFilter filter = new HeaderRequestFilter("foo", "bar"); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + assertThat(requestHandler.getHeaderMap().get("foo").get(0), is("bar")); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterCanRespond() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new RespondForbiddenFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.FORBIDDEN)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatFilterCanHaveNullCompletionHandler() throws Exception { + final int responseStatus = Response.Status.OK; + final String responseMessage = "Excellent"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new NullCompletionHandlerFilter(responseStatus, responseMessage)) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectStatusCode(is(responseStatus)) + .expectContent(is(responseMessage)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterExecutionIsExceptionSafe() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new ThrowingRequestFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsNotRunOnUnboundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filter) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsRunOnBoundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filter) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterChangesAreWrittenToResponse() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new HeaderResponseFilter("foo", "bar")) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterExecutionIsExceptionSafe() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new ThrowingResponseFilter()) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterAndResponseFilterCanBindToSamePath() throws Exception { + final RequestFilter requestFilter = mock(RequestFilterMockBase.class); + final ResponseFilter responseFilter = mock(ResponseFilterMockBase.class); + final String uriPattern = "http://*/*"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", requestFilter) + .addRequestFilterBinding("my-request-filter", uriPattern) + .addResponseFilter("my-response-filter", responseFilter) + .addResponseFilterBinding("my-response-filter", uriPattern) + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(requestFilter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(responseFilter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFromRequestFilterGoesThroughResponseFilter() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new RespondForbiddenFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .addResponseFilter("my-response-filter", new HeaderResponseFilter("foo", "bar")) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectStatusCode(is(Response.Status.FORBIDDEN)) + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChainRetainsFilters() { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + + verify(requestFilter1, never()).refer(); + verify(requestFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(requestFilter1.refer()).thenReturn(reference1); + when(requestFilter2.refer()).thenReturn(reference2); + final RequestFilter chain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + verify(requestFilter1, times(1)).refer(); + verify(requestFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatRequestFilterChainIsRun() throws Exception { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + verify(requestFilter1).filter(isNull(), any(ResponseHandler.class)); + verify(requestFilter2).filter(isNull(), any(ResponseHandler.class)); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalRequest() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = mock(HttpRequest.class); + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same request argument as the chain was, + // in a manner that allows the request object to be wrapped. + final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + verify(requestFilter).filter(requestCaptor.capture(), isNull()); + verify(request, never()).getUri(); + requestCaptor.getValue().getUri(); + verify(request, times(1)).getUri(); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalResponseHandler() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same response handler argument as the chain was, + // in a manner that allows the handler object to be wrapped. + final ArgumentCaptor responseHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + verify(requestFilter).filter(isNull(), responseHandlerCaptor.capture()); + verify(responseHandler, never()).handleResponse(any(Response.class)); + responseHandlerCaptor.getValue().handleResponse(mock(Response.class)); + verify(responseHandler, times(1)).handleResponse(any(Response.class)); + } + + @Test + public void requireThatRequestFilterCanTerminateChain() throws Exception { + final RequestFilter requestFilter1 = new RespondForbiddenFilter(); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + when(responseHandler.handleResponse(any(Response.class))).thenReturn(mock(ContentChannel.class)); + + requestFilterChain.filter(request, responseHandler); + + verify(requestFilter2, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseHandler).handleResponse(responseCaptor.capture()); + assertThat(responseCaptor.getValue().getStatus(), is(Response.Status.FORBIDDEN)); + } + + @Test + public void requireThatResponseFilterChainRetainsFilters() { + final ResponseFilter responseFilter1 = mock(ResponseFilter.class); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + + verify(responseFilter1, never()).refer(); + verify(responseFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(responseFilter1.refer()).thenReturn(reference1); + when(responseFilter2.refer()).thenReturn(reference2); + final ResponseFilter chain = ResponseFilterChain.newInstance(responseFilter1, responseFilter2); + verify(responseFilter1, times(1)).refer(); + verify(responseFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatResponseFilterChainIsRun() { + final ResponseFilter responseFilter1 = new ResponseHeaderFilter("foo", "bar"); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + final int statusCode = Response.Status.BAD_GATEWAY; + final Response response = new Response(statusCode); + final Request request = null; + + ResponseFilterChain.newInstance(responseFilter1, responseFilter2).filter(response, request); + + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseFilter2).filter(responseCaptor.capture(), isNull()); + assertThat(responseCaptor.getValue().getStatus(), is(statusCode)); + assertThat(responseCaptor.getValue().headers().getFirst("foo"), is("bar")); + + assertThat(response.getStatus(), is(statusCode)); + assertThat(response.headers().getFirst("foo"), is("bar")); + } + + @Test + public void requireThatDefaultRequestFilterChainIsRunIfNoOtherFilterChainMatches() throws IOException, InterruptedException { + RequestFilter filterWithBinding = mock(RequestFilter.class); + RequestFilter defaultFilter = mock(RequestFilter.class); + String defaultFilterId = "default-request-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filterWithBinding) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .addRequestFilter(defaultFilterId, defaultFilter) + .setRequestFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(filterWithBinding, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatDefaultResponseFilterChainIsRunIfNoOtherFilterChainMatches() throws IOException, InterruptedException { + ResponseFilter filterWithBinding = mock(ResponseFilter.class); + ResponseFilter defaultFilter = mock(ResponseFilter.class); + String defaultFilterId = "default-response-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filterWithBinding) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .addResponseFilter(defaultFilterId, defaultFilter) + .setResponseFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, times(1)).filter(any(Response.class), any(Request.class)); + verify(filterWithBinding, never()).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterWithBindingMatchHasPrecedenceOverDefaultFilter() throws IOException, InterruptedException { + RequestFilterMockBase filterWithBinding = mock(RequestFilterMockBase.class); + RequestFilterMockBase defaultFilter = mock(RequestFilterMockBase.class); + String defaultFilterId = "default-request-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filterWithBinding) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .addRequestFilter(defaultFilterId, defaultFilter) + .setRequestFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(filterWithBinding).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterWithBindingMatchHasPrecedenceOverDefaultFilter() throws IOException, InterruptedException { + ResponseFilter filterWithBinding = mock(ResponseFilter.class); + ResponseFilter defaultFilter = mock(ResponseFilter.class); + String defaultFilterId = "default-response-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filterWithBinding) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .addResponseFilter(defaultFilterId, defaultFilter) + .setResponseFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, never()).filter(any(Response.class), any(Request.class)); + verify(filterWithBinding, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatMetricAreReported() throws IOException, InterruptedException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", mock(RequestFilter.class)) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + MetricConsumerMock metricConsumerMock = new MetricConsumerMock(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings, metricConsumerMock, false); + + testDriver.client().get("/status.html"); + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(metricConsumerMock.mockitoMock()) + .add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock(), never()) + .add(MetricDefinitions.FILTERING_REQUEST_UNHANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock(), never()) + .add(MetricDefinitions.FILTERING_RESPONSE_HANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock()) + .add(MetricDefinitions.FILTERING_RESPONSE_UNHANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatStrictFilteringRejectsRequestsNotMatchingFilterChains() throws IOException { + RequestFilter filter = mock(RequestFilter.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings, new MetricConsumerMock(), true); + + testDriver.client().get("/unfiltered/") + .expectStatusCode(is(Response.Status.FORBIDDEN)) + .expectContent(containsString("Request did not match any request filter chain")); + verify(filter, never()).filter(any(), any()); + assertThat(testDriver.close(), is(true)); + } + + private static TestDriver newDriver(MyRequestHandler requestHandler, FilterBindings filterBindings) { + return newDriver(requestHandler, filterBindings, new MetricConsumerMock(), false); + } + + private static TestDriver newDriver( + MyRequestHandler requestHandler, + FilterBindings filterBindings, + MetricConsumerMock metricConsumer, + boolean strictFiltering) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newFilterModule(filterBindings, metricConsumer, strictFiltering)); + } + + private static com.google.inject.Module newFilterModule( + FilterBindings filterBindings, MetricConsumerMock metricConsumer, boolean strictFiltering) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + + bind(FilterBindings.class).toInstance(filterBindings); + bind(ServerConfig.class).toInstance(new ServerConfig(new ServerConfig.Builder().strictFiltering(strictFiltering))); + bind(ConnectorConfig.class).toInstance(new ConnectorConfig(new ConnectorConfig.Builder())); + bind(ServletPathsConfig.class).toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ConnectionLog.class).toInstance(new VoidConnectionLog()); + bind(RequestLog.class).toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule(), + metricConsumer.asGuiceModule()); + } + + private static abstract class RequestFilterMockBase extends AbstractResource implements RequestFilter {} + private static abstract class ResponseFilterMockBase extends AbstractResource implements ResponseFilter {} + + private static class MyRequestHandler extends AbstractRequestHandler { + private final CountDownLatch invocationLatch = new CountDownLatch(1); + private final AtomicReference>> headerCopy = new AtomicReference<>(null); + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + try { + headerCopy.set(new HashMap>(request.headers())); + ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler); + return null; + } finally { + invocationLatch.countDown(); + } + } + + public boolean hasBeenInvokedYet() { + return invocationLatch.getCount() == 0L; + } + + public boolean awaitInvocation() throws InterruptedException { + return invocationLatch.await(60, TimeUnit.SECONDS); + } + + public Map> getHeaderMap() { + return headerCopy.get(); + } + } + + private static class RespondForbiddenFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.FORBIDDEN).dispatch(handler); + } + } + + private static class ThrowingRequestFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + throw new RuntimeException(); + } + } + + private static class ThrowingResponseFilter extends AbstractResource implements ResponseFilter { + @Override + public void filter(final Response response, final Request request) { + throw new RuntimeException(); + } + } + + private static class HeaderRequestFilter extends AbstractResource implements RequestFilter { + private final String key; + private final String val; + + public HeaderRequestFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + request.headers().add(key, val); + } + } + + private static class HeaderResponseFilter extends AbstractResource implements ResponseFilter { + private final String key; + private final String val; + + public HeaderResponseFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final Response response, final Request request) { + response.headers().add(key, val); + } + } + + public class NullCompletionHandlerFilter extends AbstractResource implements RequestFilter { + private final int responseStatus; + private final String responseMessage; + + public NullCompletionHandlerFilter(final int responseStatus, final String responseMessage) { + this.responseStatus = responseStatus; + this.responseMessage = responseMessage; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler responseHandler) { + final HttpResponse response = HttpResponse.newInstance(responseStatus); + final ContentChannel channel = responseHandler.handleResponse(response); + final CompletionHandler completionHandler = null; + channel.write(ByteBuffer.wrap(responseMessage.getBytes()), completionHandler); + channel.close(null); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java new file mode 100644 index 00000000000..9c1348004ee --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java @@ -0,0 +1,204 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Key; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.server.HttpConnection; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Steinar Knutsen + * @author bjorncs + */ +public class HttpRequestFactoryTest { + + private static final int LOCAL_PORT = 80; + + @Test + public void testLegalURIs() { + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", null, null)); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertNull(uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "", "")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("http", "host.a1-2-3", "", "")); + assertEquals("http", uri.getScheme()); + assertEquals("host.a1-2-3", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "/:1/../1=.", "")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("/:1/../1=.", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "", "a=/../&?=")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("a=/../&?=", uri.getRawQuery()); + } + } + + @Test + public void testIllegalQuery() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "example.com", "/search", "query=\"contains_quotes\"")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception1() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "?", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception2() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", ".", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception3() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "*", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_unicode_in_query_throws_requestexception() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "example.com", "/search", "query=%c0%ae")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + assertThat(e.getMessage(), equalTo("URL violates RFC 2396: Not valid UTF8! byte C0 in state 0")); + } + } + + @Test + public void request_uri_uses_local_port() { + HttpRequest request = HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("https", "example.com", "/search", "query=value")); + assertEquals(LOCAL_PORT, request.getUri().getPort()); + } + + private static HttpServletRequest createMockRequest(String scheme, String serverName, String path, String queryString) { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpConnection connection = mock(HttpConnection.class); + JDiscServerConnector connector = mock(JDiscServerConnector.class); + when(connector.connectorConfig()).thenReturn(new ConnectorConfig(new ConnectorConfig.Builder().listenPort(LOCAL_PORT))); + when(connector.getLocalPort()).thenReturn(LOCAL_PORT); + when(connection.getCreatedTimeStamp()).thenReturn(System.currentTimeMillis()); + when(connection.getConnector()).thenReturn(connector); + when(request.getAttribute("org.eclipse.jetty.server.HttpConnection")).thenReturn(connection); + when(request.getProtocol()).thenReturn("HTTP/1.1"); + when(request.getScheme()).thenReturn(scheme); + when(request.getServerName()).thenReturn(serverName); + when(request.getRemoteAddr()).thenReturn("127.0.0.1"); + when(request.getRemotePort()).thenReturn(1234); + when(request.getLocalPort()).thenReturn(LOCAL_PORT); + when(request.getMethod()).thenReturn("GET"); + when(request.getQueryString()).thenReturn(queryString); + when(request.getRequestURI()).thenReturn(path); + return request; + } + + private static final class MockContainer implements CurrentContainer { + + @Override + public Container newReference(URI uri) { + return new Container() { + + @Override + public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) { + return null; + } + + @Override + public T getInstance(Key tKey) { + return null; + } + + @Override + public T getInstance(Class tClass) { + return null; + } + + @Override + public ResourceReference refer() { + return References.NOOP_REFERENCE; + } + + @Override + public void release() { + + } + + @Override + public long currentTimeMillis() { + return 0; + } + }; + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java new file mode 100644 index 00000000000..bb92d75bed5 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java @@ -0,0 +1,221 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.server.jetty.HttpResponseStatisticsCollector.StatisticsEntry; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.MetaData.Response; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpTransport; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.Callback; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * @author ollivir + */ +public class HttpResponseStatisticsCollectorTest { + + private Connector connector; + private List monitoringPaths = List.of("/status.html"); + private List searchPaths = List.of("/search"); + private HttpResponseStatisticsCollector collector = new HttpResponseStatisticsCollector(monitoringPaths, searchPaths); + private int httpResponseCode = 500; + + @Test + public void statistics_are_aggregated_by_category() { + testRequest("http", 300, "GET"); + testRequest("http", 301, "GET"); + testRequest("http", 200, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_3XX, 2L); + } + + @Test + public void statistics_are_grouped_by_http_method_and_scheme() { + testRequest("http", 200, "GET"); + testRequest("http", 200, "PUT"); + testRequest("http", 200, "POST"); + testRequest("http", 200, "POST"); + testRequest("http", 404, "GET"); + testRequest("https", 404, "GET"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "http", "PUT", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, 2L); + assertStatisticsEntryPresent(stats, "https", "GET", MetricDefinitions.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "https", "POST", MetricDefinitions.RESPONSES_2XX, 4L); + } + + @Test + public void statistics_include_grouped_and_single_statuscodes() { + testRequest("http", 401, "GET"); + testRequest("http", 404, "GET"); + testRequest("http", 403, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_4XX, 3L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_401, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_403, 1L); + + } + + @Test + public void retrieving_statistics_resets_the_counters() { + testRequest("http", 200, "GET"); + testRequest("http", 200, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 2L); + + testRequest("http", 200, "GET"); + + stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + } + + @Test + public void statistics_include_request_type_dimension() { + testRequest("http", 200, "GET", "/search"); + testRequest("http", 200, "POST", "/search"); + testRequest("http", 200, "POST", "/feed"); + testRequest("http", 200, "GET", "/status.html?foo=bar"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "monitoring", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "read", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, "read", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, "write", 1L); + + testRequest("http", 200, "GET"); + + stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + } + + @Test + public void request_type_can_be_set_explicitly() { + testRequest("http", 200, "GET", "/search", com.yahoo.jdisc.Request.RequestType.WRITE); + + var stats = collector.takeStatistics(); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "write", 1L); + } + + @Before + public void initializeCollector() throws Exception { + Server server = new Server(); + connector = new AbstractConnector(server, null, null, null, 0) { + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException { + } + + @Override + public Object getTransport() { + return null; + } + }; + collector.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + baseRequest.setHandled(true); + baseRequest.getResponse().setStatus(httpResponseCode); + } + }); + server.setHandler(collector); + server.start(); + } + + private Request testRequest(String scheme, int responseCode, String httpMethod) { + return testRequest(scheme, responseCode, httpMethod, "foo/bar"); + } + private Request testRequest(String scheme, int responseCode, String httpMethod, String path) { + return testRequest(scheme, responseCode, httpMethod, path, null); + } + private Request testRequest(String scheme, int responseCode, String httpMethod, String path, + com.yahoo.jdisc.Request.RequestType explicitRequestType) { + HttpChannel channel = new HttpChannel(connector, new HttpConfiguration(), null, new DummyTransport()); + MetaData.Request metaData = new MetaData.Request(httpMethod, new HttpURI(scheme + "://" + path), HttpVersion.HTTP_1_1, new HttpFields()); + Request req = channel.getRequest(); + if (explicitRequestType != null) + req.setAttribute("requestType", explicitRequestType); + req.setMetaData(metaData); + + this.httpResponseCode = responseCode; + channel.handle(); + return req; + } + + private static void assertStatisticsEntryPresent(List result, String scheme, String method, String name, long expectedValue) { + long value = result.stream() + .filter(entry -> entry.method.equals(method) && entry.scheme.equals(scheme) && entry.name.equals(name)) + .mapToLong(entry -> entry.value) + .findAny() + .orElseThrow(() -> new AssertionError(String.format("Not matching entry in result (scheme=%s, method=%s, name=%s)", scheme, method, name))); + assertThat(value, equalTo(expectedValue)); + } + + private static void assertStatisticsEntryWithRequestTypePresent(List result, String scheme, String method, String name, String requestType, long expectedValue) { + long value = result.stream() + .filter(entry -> entry.method.equals(method) && entry.scheme.equals(scheme) && entry.name.equals(name) && entry.requestType.equals(requestType)) + .mapToLong(entry -> entry.value) + .reduce(Long::sum) + .orElseThrow(() -> new AssertionError(String.format("Not matching entry in result (scheme=%s, method=%s, name=%s, type=%s)", scheme, method, name, requestType))); + assertThat(value, equalTo(expectedValue)); + } + + private final class DummyTransport implements HttpTransport { + @Override + public void send(Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) { + callback.succeeded(); + } + + @Override + public boolean isPushSupported() { + return false; + } + + @Override + public boolean isOptimizedForDirectBuffers() { + return false; + } + + @Override + public void push(MetaData.Request request) { + } + + @Override + public void onCompleted() { + } + + @Override + public void abort(Throwable failure) { + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java new file mode 100644 index 00000000000..5659dfc2d3c --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java @@ -0,0 +1,847 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.test.ServerProviderConformanceTest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Simon Thoresen Hult + */ +public class HttpServerConformanceTest extends ServerProviderConformanceTest { + + private static final Logger log = Logger.getLogger(HttpServerConformanceTest.class.getName()); + + private static final String REQUEST_CONTENT = "myRequestContent"; + private static final String RESPONSE_CONTENT = "myResponseContent"; + + @SuppressWarnings("LoggerInitializedWithForeignClass") + private static Logger httpRequestDispatchLogger = Logger.getLogger(HttpRequestDispatch.class.getName()); + private static Level httpRequestDispatchLoggerOriginalLevel; + + /* + * Reduce logging of every stack trace for {@link ServerProviderConformanceTest.ConformanceException} thrown. + * This makes the log more readable and the test faster as well. + */ + @BeforeClass + public static void reduceExcessiveLogging() { + httpRequestDispatchLoggerOriginalLevel = httpRequestDispatchLogger.getLevel(); + httpRequestDispatchLogger.setLevel(Level.SEVERE); + } + + @AfterClass + public static void restoreExcessiveLogging() { + httpRequestDispatchLogger.setLevel(httpRequestDispatchLoggerOriginalLevel); + } + + @AfterClass + public static void reportDiagnostics() { + System.out.println( + "After " + HttpServerConformanceTest.class.getSimpleName() + + ": #threads=" + Thread.getAllStackTraces().size()); + } + + @Override + @Test + public void testContainerNotReadyException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), containsString("Container not ready."))) + .execute(); + } + + @Override + @Test + public void testBindingSetNotFoundException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_NOT_FOUND), containsString("No binding set named 'unknown'."))) + .execute(); + } + + @Override + @Test + public void testNoBindingSetSelectedException() throws Throwable { + final Pattern reasonPattern = Pattern.compile(".*No binding set selected for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), matchesPattern(reasonPattern))) + .execute(); + } + + @Override + @Test + public void testBindingNotFoundException() throws Throwable { + final Pattern contentPattern = Pattern.compile("No binding for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(NOT_FOUND), containsPattern(contentPattern))) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestException() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner() + .expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testResponseWriteCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionExceptionNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + private static Matcher success() { + final Matcher expectedStatusCode = is(OK); + final Matcher expectedReasonPhrase = is("OK"); + final Matcher expectedContent = is(RESPONSE_CONTENT); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher successNoContent() { + final Matcher expectedStatusCode = is(OK); + final Matcher expectedReasonPhrase = is("OK"); + final Matcher expectedContent = is(""); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher serverError() { + final Matcher expectedStatusCode = is(INTERNAL_SERVER_ERROR); + final Matcher expectedReasonPhrase = any(String.class); + final Matcher expectedContent = containsString(ConformanceException.class.getSimpleName()); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher errorWithReason( + final Matcher expectedStatusCode, final Matcher expectedReasonPhrase) { + final Matcher expectedContent = any(String.class); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher responseMatcher( + final Matcher expectedStatusCode, + final Matcher expectedReasonPhrase, + final Matcher expectedContent) { + return new TypeSafeMatcher() { + @Override + public void describeTo(final Description description) { + description.appendText("status code "); + expectedStatusCode.describeTo(description); + description.appendText(", reason "); + expectedReasonPhrase.describeTo(description); + description.appendText(" and content "); + expectedContent.describeTo(description); + } + + @Override + protected void describeMismatchSafely( + final ResponseGist response, final Description mismatchDescription) { + mismatchDescription.appendText(" status code was ").appendValue(response.getStatusCode()) + .appendText(", reason was ").appendValue(response.getReasonPhrase()) + .appendText(" and content was ").appendValue(response.getContent()); + } + + @Override + protected boolean matchesSafely(final ResponseGist response) { + return expectedStatusCode.matches(response.getStatusCode()) + && expectedReasonPhrase.matches(response.getReasonPhrase()) + && expectedContent.matches(response.getContent()); + } + }; + } + + private static class ResponseGist { + private final int statusCode; + private final String content; + private String reasonPhrase; + + public ResponseGist(int statusCode, String reasonPhrase, String content) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() { + return content; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + @Override + public String toString() { + return "ResponseGist {" + + " statusCode=" + statusCode + + " reasonPhrase=" + reasonPhrase + + " content=" + content + + " }"; + } + } + + private class TestRunner implements Adapter> { + + private Matcher expectedResponse = null; + HttpVersion requestVersion; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + void execute() throws Throwable { + requestVersion = HttpVersion.HTTP_1_0; + runTest(this); + + requestVersion = HttpVersion.HTTP_1_1; + runTest(this); + + executorService.shutdown(); + } + + TestRunner expect(final Matcher matcher) { + expectedResponse = matcher; + return this; + } + + @Override + public Module newConfigModule() { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class) + .toInstance(new FilterBindings.Builder().build()); + bind(ServerConfig.class) + .toInstance(new ServerConfig(new ServerConfig.Builder())); + bind(ServletPathsConfig.class) + .toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ConnectionLog.class) + .toInstance(new VoidConnectionLog()); + bind(RequestLog.class) + .toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule()); + } + + @Override + public Class getServerProviderClass() { + return JettyHttpServer.class; + } + + @Override + public ClientProxy newClient(final JettyHttpServer server) throws Throwable { + return new ClientProxy(server.getListenPort(), requestVersion); + } + + @Override + public Future executeRequest( + final ClientProxy client, + final boolean withRequestContent) throws Throwable { + final HttpUriRequest request; + final URI requestUri = URI.create("http://localhost:" + client.listenPort + "/status.html"); + if (!withRequestContent) { + HttpGet httpGet = new HttpGet(requestUri); + httpGet.setProtocolVersion(client.requestVersion); + request = httpGet; + } else { + final HttpPost post = new HttpPost(requestUri); + post.setEntity(new StringEntity(REQUEST_CONTENT, StandardCharsets.UTF_8)); + post.setProtocolVersion(client.requestVersion); + request = post; + } + log.fine(() -> "executorService:" + + " .isShutDown()=" + executorService.isShutdown() + + " .isTerminated()=" + executorService.isTerminated()); + return executorService.submit(() -> client.delegate.execute(request)); + } + + @Override + public Iterable newResponseContent() { + return Collections.singleton(StandardCharsets.UTF_8.encode(RESPONSE_CONTENT)); + } + + @Override + public void validateResponse(final Future responseFuture) throws Throwable { + final HttpResponse response = responseFuture.get(); + final ResponseGist responseGist = new ResponseGist( + response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase(), + EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + assertThat(responseGist, expectedResponse); + } + } + + private static class ClientProxy { + + final HttpClient delegate; + final int listenPort; + final ProtocolVersion requestVersion; + + ClientProxy(final int listenPort, final HttpVersion requestVersion) { + this.delegate = HttpClientBuilder.create().build(); + this.requestVersion = requestVersion; + this.listenPort = listenPort; + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java new file mode 100644 index 00000000000..c00525a3ddc --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -0,0 +1,1201 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; +import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Throttling; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.server.jetty.TestDrivers.TlsClientAuth; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.Pkcs10Csr; +import com.yahoo.security.Pkcs10CsrBuilder; +import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.tls.TlsContext; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.content.StringBody; +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V1; +import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.server.handler.AbstractHandlerContainer; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.math.BigInteger; +import java.net.BindException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.GATEWAY_TIMEOUT; +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static com.yahoo.jdisc.Response.Status.REQUEST_URI_TOO_LONG; +import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED; +import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONNECTION; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONTENT_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING; +import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED; +import static com.yahoo.jdisc.http.HttpHeaders.Values.CLOSE; +import static com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import static com.yahoo.security.KeyAlgorithm.EC; +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Oyvind Bakksjo + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class HttpServerTest { + + private static final Logger log = Logger.getLogger(HttpServerTest.class.getName()); + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Test + public void requireThatServerCanListenToRandomPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + assertNotEquals(0, driver.server().getListenPort()); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanNotListenToBoundPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + try { + TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .listenPort(driver.server().getListenPort()) + ); + } catch (final Throwable t) { + assertThat(t.getCause(), instanceOf(BindException.class)); + } + assertTrue(driver.close()); + } + + @Test + public void requireThatBindingSetNotFoundReturns404() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder() + .developerMode(true), + new ConnectorConfig.Builder(), + newBindingSetSelector("unknown")); + driver.client().get("/status.html") + .expectStatusCode(is(NOT_FOUND)) + .expectContent(containsPattern(Pattern.compile( + Pattern.quote(BindingSetNotFoundException.class.getName()) + + ": No binding set named 'unknown'\\.\n\tat .+", + Pattern.DOTALL | Pattern.MULTILINE))); + assertTrue(driver.close()); + } + + @Test + public void requireThatTooLongInitLineReturns414() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .requestHeaderSize(1)); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_URI_TOO_LONG)); + assertTrue(driver.close()); + } + + @Test + public void requireThatAccessLogIsCalledForRequestRejectedByJetty() throws Exception { + BlockingQueueRequestLog requestLogMock = new BlockingQueueRequestLog(); + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder().requestHeaderSize(1), + binder -> binder.bind(RequestLog.class).toInstance(requestLogMock)); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_URI_TOO_LONG)); + RequestLogEntry entry = requestLogMock.poll(Duration.ofSeconds(30)); + assertEquals(414, entry.statusCode().getAsInt()); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanEcho() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanEchoCompressed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + SimpleHttpClient client = driver.newClient(true); + client.get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanHandleMultipleRequests() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostDoesNotRemoveContentByDefault() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostKeepsContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), false); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostRemovesContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithCharsetSpecifiedWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(X_DISABLE_CHUNKING, "true") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=UTF-8") + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatEmptyFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b&c=d") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatUriParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{a=[b], c=[d]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormAndUriParametersAreMerged() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d1") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("c=d2&e=f") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d1, d2], e=[f]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormCharsetIsHonored() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=ISO-8859-1") + .setBinaryContent(new byte[]{66, (byte) 230, 114, 61, 98, 108, (byte) 229}) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{B\u00e6r=[bl\u00e5]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatUnknownFormCharsetIsTreatedAsBadRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=FLARBA-GARBA-7") + .setContent("a=b") + .execute(); + response.expectStatusCode(is(UNSUPPORTED_MEDIA_TYPE)); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithPercentEncodedContentIsDecoded() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("%20%3D%C3%98=%22%25+") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{ =\u00d8=[\"% ]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithThrowingHandlerIsExceptionSafe() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ThrowingHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b") + .execute(); + response.expectStatusCode(is(INTERNAL_SERVER_ERROR)); + assertTrue(driver.close()); + } + + @Test + public void requireThatMultiPostWorks() throws Exception { + // This is taken from tcpdump of bug 5433352 and reassembled here to see that httpserver passes things on. + final String startTxtContent = "this is a test for POST."; + final String updaterConfContent + = "identifier = updater\n" + + "server_type = gds\n"; + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .setMultipartContent( + newFileBody("", "start.txt", startTxtContent), + newFileBody("", "updater.conf", updaterConfContent)) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString(startTxtContent)) + .expectContent(containsString(updaterConfContent)); + } + + @Test + public void requireThatRequestCookiesAreReceived() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookiePrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(COOKIE, "foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString("[foo=bar]")); + assertTrue(driver.close()); + } + + @Test + public void requireThatSetCookieHeaderIsCorrect() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookieSetterRequestHandler( + new Cookie("foo", "bar") + .setDomain(".localhost") + .setHttpOnly(true) + .setPath("/foopath") + .setSecure(true))); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader("Set-Cookie", + is("foo=bar; Path=/foopath; Domain=.localhost; Secure; HttpOnly")); + assertTrue(driver.close()); + } + + @Test + public void requireThatTimeoutWorks() throws Exception { + final UnresponsiveHandler requestHandler = new UnresponsiveHandler(); + final TestDriver driver = TestDrivers.newInstance(requestHandler); + driver.client().get("/status.html") + .expectStatusCode(is(GATEWAY_TIMEOUT)); + ResponseDispatch.newInstance(OK).dispatch(requestHandler.responseHandler); + assertTrue(driver.close()); + } + + // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2 + // Details in https://github.com/eclipse/jetty.project/issues/1116 + @Test + public void requireThatHeaderWithNullValueIsOmitted() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", null)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader("X-Foo"); + assertTrue(driver.close()); + } + + // Header with empty value is allowed by https://tools.ietf.org/html/rfc7230#section-3.2 + // Details in https://github.com/eclipse/jetty.project/issues/1116 + @Test + public void requireThatHeaderWithEmptyValueIsAllowed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", "")); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader("X-Foo", is("")); + assertTrue(driver.close()); + } + + @Test + public void requireThatNoConnectionHeaderMeansKeepAliveInHttp11KeepAliveDisabled() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler(CONNECTION, CLOSE)); + driver.client().get("/status.html") + .expectHeader(CONNECTION, is(CLOSE)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatConnectionIsClosedAfterXRequests() throws Exception { + final int MAX_KEEPALIVE_REQUESTS = 100; + final TestDriver driver = TestDrivers.newConfiguredInstance(new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder().maxRequestsPerConnection(MAX_KEEPALIVE_REQUESTS)); + for (int i = 0; i < MAX_KEEPALIVE_REQUESTS - 1; i++) { + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader(CONNECTION); + } + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader(CONNECTION, is(CLOSE)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanRespondToSslRequest() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + + final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatTlsClientAuthenticationEnforcerRejectsRequestsForNonWhitelistedPaths() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + + SSLContext trustStoreOnlyCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + + new SimpleHttpClient(trustStoreOnlyCtx, driver.server().getListenPort(), false) + .get("/dummy.html") + .expectStatusCode(is(UNAUTHORIZED)); + + assertTrue(driver.close()); + } + + @Test + public void requireThatTlsClientAuthenticationEnforcerAllowsRequestForWhitelistedPaths() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + + SSLContext trustStoreOnlyCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + + new SimpleHttpClient(trustStoreOnlyCtx, driver.server().getListenPort(), false) + .get("/status.html") + .expectStatusCode(is(OK)); + + assertTrue(driver.close()); + } + + @Test + public void requireThatConnectedAtReturnsNonZero() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ConnectedAtRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectContent(matchesPattern("\\d{13,}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatGzipEncodingRequestsAreAutomaticallyDecompressed() throws Exception { + TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + String requestContent = generateContent('a', 30); + ResponseValidator response = driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setGzipContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseStatsAreCollected() throws Exception { + RequestTypeHandler handler = new RequestTypeHandler(); + TestDriver driver = TestDrivers.newInstance(handler); + HttpResponseStatisticsCollector statisticsCollector = ((AbstractHandlerContainer) driver.server().server().getHandler()) + .getChildHandlerByClass(HttpResponseStatisticsCollector.class); + + { + List stats = statisticsCollector.takeStatistics(); + assertEquals(0, stats.size()); + } + + { + driver.client().newPost("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("http", entry.scheme); + assertEquals("POST", entry.method); + assertEquals("http.status.2xx", entry.name); + assertEquals("write", entry.requestType); + assertEquals(1, entry.value); + } + + { + driver.client().newGet("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("http", entry.scheme); + assertEquals("GET", entry.method); + assertEquals("http.status.2xx", entry.name); + assertEquals("read", entry.requestType); + assertEquals(1, entry.value); + } + + { + handler.setRequestType(Request.RequestType.READ); + driver.client().newPost("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("Handler overrides request type", "read", entry.requestType); + } + + assertTrue(driver.close()); + } + + private HttpResponseStatisticsCollector.StatisticsEntry waitForStatistics(HttpResponseStatisticsCollector + statisticsCollector) { + List entries = Collections.emptyList(); + int tries = 0; + while (entries.isEmpty() && tries < 10000) { + entries = statisticsCollector.takeStatistics(); + if (entries.isEmpty()) + try {Thread.sleep(100); } catch (InterruptedException e) {} + tries++; + } + assertEquals(1, entries.size()); + return entries.get(0); + } + + @Test + public void requireThatConnectionThrottleDoesNotBlockConnectionsBelowThreshold() throws Exception { + TestDriver driver = TestDrivers.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .throttling(new Throttling.Builder() + .enabled(true) + .maxAcceptRate(10) + .maxHeapUtilization(1.0) + .maxConnections(10))); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientIsMissingCertificateOnHandshake() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: bad_certificate"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.MISSING_CLIENT_CERT.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleTlsVersion() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + boolean tlsv11Enabled = List.of(clientCtx.getDefaultSSLParameters().getProtocols()).contains("TLSv1.1"); + assumeTrue("TLSv1.1 must be enabled in installed JDK", tlsv11Enabled); + + assertHttpsRequestTriggersSslHandshakeException(driver, clientCtx, "TLSv1.1", null, "protocol"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_PROTOCOLS.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleCiphers() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "Received fatal alert: handshake_failure"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_CIPHERS.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesInvalidCertificateInHandshake() throws IOException { + Path serverPrivateKeyFile = tmpFolder.newFile().toPath(); + Path serverCertificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(serverPrivateKeyFile, serverCertificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(serverCertificateFile, serverPrivateKeyFile, metricConsumer, connectionLog); + + Path clientPrivateKeyFile = tmpFolder.newFile().toPath(); + Path clientCertificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(clientPrivateKeyFile, clientCertificateFile); + + SSLContext clientCtx = new SslContextBuilder() + .withKeyStore(clientPrivateKeyFile, clientCertificateFile) + .withTrustStore(serverCertificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INVALID_CLIENT_CERT.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesExpiredCertificateInHandshake() throws IOException { + Path rootPrivateKeyFile = tmpFolder.newFile().toPath(); + Path rootCertificateFile = tmpFolder.newFile().toPath(); + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + Instant notAfter = Instant.now().minus(100, ChronoUnit.DAYS); + generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile, privateKeyFile, certificateFile, notAfter); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(rootCertificateFile, rootPrivateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(rootCertificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + + } + + @Test + public void requireThatProxyProtocolIsAcceptedAndActualRemoteAddressStoredInAccessLog() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, false); + + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + sendJettyClientRequest(driver, certificateFile, new V1.Tag(proxiedRemoteAddress, proxiedRemotePort)); + sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, proxiedRemotePort)); + assertTrue(driver.close()); + + assertEquals(2, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, proxiedRemotePort); + Assertions.assertThat(connectionLog.logEntries()).hasSize(2); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, proxiedRemotePort); + } + + @Test + public void requireThatConnectorWithProxyProtocolMixedEnabledAcceptsBothProxyProtocolAndHttps() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, true); + + String proxiedRemoteAddress = "192.168.0.100"; + sendJettyClientRequest(driver, certificateFile, null); + sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, 12345)); + assertTrue(driver.close()); + + assertEquals(2, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), "127.0.0.1", 0); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, 0); + Assertions.assertThat(connectionLog.logEntries()).hasSize(2); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), null, 0); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, 12345); + } + + @Test + public void requireThatJdiscLocalPortPropertyIsNotOverriddenByProxyProtocol() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, connectionLog, /*mixedMode*/false); + + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + String proxyLocalAddress = "10.0.0.10"; + int proxyLocalPort = 23456; + V2.Tag v2Tag = new V2.Tag(V2.Tag.Command.PROXY, null, V2.Tag.Protocol.STREAM, + proxiedRemoteAddress, proxiedRemotePort, proxyLocalAddress, proxyLocalPort, null); + ContentResponse response = sendJettyClientRequest(driver, certificateFile, v2Tag); + assertTrue(driver.close()); + + int clientPort = Integer.parseInt(response.getHeaders().get("Jdisc-Local-Port")); + assertNotEquals(proxyLocalPort, clientPort); + assertNotEquals(proxyLocalPort, connectionLog.logEntries().get(0).localPort().get().intValue()); + } + + @Test + public void requireThatConnectionIsTrackedInConnectionLog() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + Module overrideModule = binder -> binder.bind(ConnectionLog.class).toInstance(connectionLog); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.NEED, overrideModule); + int listenPort = driver.server().getListenPort(); + driver.client().get("/status.html"); + assertTrue(driver.close()); + List logEntries = connectionLog.logEntries(); + Assertions.assertThat(logEntries).hasSize(1); + ConnectionLogEntry logEntry = logEntries.get(0); + assertEquals(4, UUID.fromString(logEntry.id()).version()); + Assertions.assertThat(logEntry.timestamp()).isAfter(Instant.EPOCH); + Assertions.assertThat(logEntry.requests()).hasValue(1L); + Assertions.assertThat(logEntry.responses()).hasValue(1L); + Assertions.assertThat(logEntry.peerAddress()).hasValue("127.0.0.1"); + Assertions.assertThat(logEntry.localAddress()).hasValue("127.0.0.1"); + Assertions.assertThat(logEntry.localPort()).hasValue(listenPort); + Assertions.assertThat(logEntry.httpBytesReceived()).hasValueSatisfying(value -> Assertions.assertThat(value).isPositive()); + Assertions.assertThat(logEntry.httpBytesSent()).hasValueSatisfying(value -> Assertions.assertThat(value).isPositive()); + Assertions.assertThat(logEntry.sslProtocol()).hasValueSatisfying(TlsContext.ALLOWED_PROTOCOLS::contains); + Assertions.assertThat(logEntry.sslPeerSubject()).hasValue("CN=localhost"); + Assertions.assertThat(logEntry.sslCipherSuite()).hasValueSatisfying(cipher -> Assertions.assertThat(cipher).isNotBlank()); + Assertions.assertThat(logEntry.sslSessionId()).hasValueSatisfying(sessionId -> Assertions.assertThat(sessionId).hasSize(64)); + Assertions.assertThat(logEntry.sslPeerNotBefore()).hasValue(Instant.EPOCH); + Assertions.assertThat(logEntry.sslPeerNotAfter()).hasValue(Instant.EPOCH.plus(100_000, ChronoUnit.DAYS)); + } + + private ContentResponse sendJettyClientRequest(TestDriver testDriver, Path certificateFile, Object tag) + throws Exception { + HttpClient client = createJettyHttpClient(certificateFile); + try { + int maxAttempts = 3; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + ContentResponse response = client.newRequest(URI.create("https://localhost:" + testDriver.server().getListenPort() + "/")) + .tag(tag) + .send(); + assertEquals(200, response.getStatus()); + return response; + } catch (ExecutionException e) { + // Retry when the server closes the connection before the TLS handshake is completed. This have been observed in CI. + // We have been unable to reproduce this locally. The cause is therefor currently unknown. + log.log(Level.WARNING, String.format("Attempt %d failed: %s", attempt, e.getMessage()), e); + Thread.sleep(10); + } + } + throw new AssertionError("Failed to send request, see log for details"); + } finally { + client.stop(); + } + } + + // Using Jetty's http client as Apache httpclient does not support the proxy-protocol v1/v2. + private static HttpClient createJettyHttpClient(Path certificateFile) throws Exception { + SslContextFactory.Client clientSslCtxFactory = new SslContextFactory.Client(); + clientSslCtxFactory.setHostnameVerifier(NoopHostnameVerifier.INSTANCE); + clientSslCtxFactory.setSslContext(new SslContextBuilder().withTrustStore(certificateFile).build()); + + HttpClient client = new HttpClient(clientSslCtxFactory); + client.start(); + return client; + } + + private static void assertLogEntryHasRemote(RequestLogEntry entry, String expectedAddress, int expectedPort) { + assertEquals(expectedAddress, entry.peerAddress().get()); + if (expectedPort > 0) { + assertEquals(expectedPort, entry.peerPort().getAsInt()); + } + } + + private static void assertLogEntryHasRemote(ConnectionLogEntry entry, String expectedAddress, int expectedPort) { + if (expectedAddress != null) { + Assertions.assertThat(entry.remoteAddress()).hasValue(expectedAddress); + } else { + Assertions.assertThat(entry.remoteAddress()).isEmpty(); + } + if (expectedPort > 0) { + Assertions.assertThat(entry.remotePort()).hasValue(expectedPort); + } else { + Assertions.assertThat(entry.remotePort()).isEmpty(); + } + } + + private static void assertSslHandshakeFailurePresent( + ConnectionLogEntry entry, Class expectedException, String expectedType) { + Assertions.assertThat(entry.sslHandshakeFailure()).isPresent(); + ConnectionLogEntry.SslHandshakeFailure failure = entry.sslHandshakeFailure().get(); + assertEquals(expectedType, failure.type()); + ExceptionEntry exceptionEntry = failure.exceptionChain().get(0); + assertEquals(expectedException.getName(), exceptionEntry.name()); + } + + private static TestDriver createSslWithProxyProtocolTestDriver( + Path certificateFile, Path privateKeyFile, RequestLog requestLog, + ConnectionLog connectionLog, boolean mixedMode) { + ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() + .proxyProtocol(new ConnectorConfig.ProxyProtocol.Builder() + .enabled(true) + .mixedMode(mixedMode)) + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .privateKeyFile(privateKeyFile.toString()) + .certificateFile(certificateFile.toString()) + .caCertificateFile(certificateFile.toString())); + return TestDrivers.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), + connectorConfig, + binder -> { + binder.bind(RequestLog.class).toInstance(requestLog); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }); + } + + private static TestDriver createSslTestDriver( + Path serverCertificateFile, Path serverPrivateKeyFile, MetricConsumerMock metricConsumer, InMemoryConnectionLog connectionLog) throws IOException { + Module extraModule = binder -> { + binder.bind(MetricConsumer.class).toInstance(metricConsumer.mockitoMock()); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }; + return TestDrivers.newInstanceWithSsl( + new EchoRequestHandler(), serverCertificateFile, serverPrivateKeyFile, TlsClientAuth.NEED, extraModule); + } + + private static void assertHttpsRequestTriggersSslHandshakeException( + TestDriver testDriver, + SSLContext sslContext, + String protocolOverride, + String cipherOverride, + String expectedExceptionSubstring) throws IOException { + List protocols = protocolOverride != null ? List.of(protocolOverride) : null; + List ciphers = cipherOverride != null ? List.of(cipherOverride) : null; + try (var client = new SimpleHttpClient(sslContext, protocols, ciphers, testDriver.server().getListenPort(), false)) { + client.get("/status.html"); + fail("SSLHandshakeException expected"); + } catch (SSLHandshakeException e) { + assertThat(e.getMessage(), containsString(expectedExceptionSubstring)); + } catch (SSLException e) { + // This exception is thrown if Apache httpclient's write thread detects the handshake failure before the read thread. + log.log(Level.WARNING, "Client failed to get a proper TLS handshake response: " + e.getMessage(), e); + // Only ignore a subset of exceptions + assertThat(e.getMessage(), anyOf(containsString("readHandshakeRecord"), containsString("Broken pipe"))); + } + } + + private static void generatePrivateKeyAndCertificate(Path privateKeyFile, Path certificateFile) throws IOException { + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, new X500Principal("CN=localhost"), Instant.EPOCH, Instant.EPOCH.plus(100_000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } + + private static void generatePrivateKeyAndCertificate(Path rootPrivateKeyFile, Path rootCertificateFile, + Path privateKeyFile, Path certificateFile, Instant notAfter) throws IOException { + generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile); + X509Certificate rootCertificate = X509CertificateUtils.fromPem(Files.readString(rootCertificateFile)); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(Files.readString(rootPrivateKeyFile)); + + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=myclient"), keyPair, SHA256_WITH_ECDSA).build(); + X509Certificate certificate = X509CertificateBuilder + .fromCsr(csr, rootCertificate.getSubjectX500Principal(), Instant.EPOCH, notAfter, privateKey, SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } + + private static RequestHandler mockRequestHandler() { + final RequestHandler mockRequestHandler = mock(RequestHandler.class); + when(mockRequestHandler.refer()).thenReturn(References.NOOP_REFERENCE); + return mockRequestHandler; + } + + private static String generateContent(final char c, final int len) { + final StringBuilder ret = new StringBuilder(len); + for (int i = 0; i < len; ++i) { + ret.append(c); + } + return ret.toString(); + } + + private static TestDriver newDriverWithFormPostContentRemoved(RequestHandler requestHandler, + boolean removeFormPostBody) throws Exception { + return TestDrivers.newConfiguredInstance( + requestHandler, + new ServerConfig.Builder() + .removeRawPostBodyForWwwUrlEncodedPost(removeFormPostBody), + new ConnectorConfig.Builder()); + } + + private static FormBodyPart newFileBody(final String parameterName, final String fileName, final String fileContent) { + return new FormBodyPart( + parameterName, + new StringBody(fileContent, ContentType.TEXT_PLAIN) { + @Override + public String getFilename() { + return fileName; + } + + @Override + public String getTransferEncoding() { + return "binary"; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public String getCharset() { + return null; + } + }); + } + + private static class ConnectedAtRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpRequest httpRequest = (HttpRequest)request; + final String connectedAt = String.valueOf(httpRequest.getConnectedAt(TimeUnit.MILLISECONDS)); + final ContentChannel ch = handler.handleResponse(new Response(OK)); + ch.write(ByteBuffer.wrap(connectedAt.getBytes(StandardCharsets.UTF_8)), null); + ch.close(null); + return null; + } + } + + private static class CookieSetterRequestHandler extends AbstractRequestHandler { + + final Cookie cookie; + + CookieSetterRequestHandler(final Cookie cookie) { + this.cookie = cookie; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpResponse response = HttpResponse.newInstance(OK); + response.encodeSetCookieHeader(Collections.singletonList(cookie)); + ResponseDispatch.newInstance(response).dispatch(handler); + return null; + } + } + + private static class CookiePrinterRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final List cookies = new ArrayList<>(((HttpRequest)request).decodeCookieHeader()); + Collections.sort(cookies, new CookieComparator()); + final ContentChannel out = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + out.write(StandardCharsets.UTF_8.encode(cookies.toString()), null); + out.close(null); + return null; + } + } + + private static class ParameterPrinterRequestHandler extends AbstractRequestHandler { + + private static final CompletionHandler NULL_COMPLETION_HANDLER = null; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + Map> parameters = new TreeMap<>(((HttpRequest)request).parameters()); + ContentChannel responseContentChannel = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + responseContentChannel.write(ByteBuffer.wrap(parameters.toString().getBytes(StandardCharsets.UTF_8)), + NULL_COMPLETION_HANDLER); + + // Have the request content written back to the response. + return responseContentChannel; + } + } + + private static class RequestTypeHandler extends AbstractRequestHandler { + + private Request.RequestType requestType = null; + + public void setRequestType(Request.RequestType requestType) { + this.requestType = requestType; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + Response response = new Response(OK); + response.setRequestType(requestType); + return handler.handleResponse(response); + } + } + + private static class ThrowingHandler extends AbstractRequestHandler { + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + throw new RuntimeException("Deliberately thrown exception"); + } + } + + private static class UnresponsiveHandler extends AbstractRequestHandler { + + ResponseHandler responseHandler; + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + request.setTimeout(100, TimeUnit.MILLISECONDS); + responseHandler = handler; + return null; + } + } + + private static class EchoRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + int port = request.getUri().getPort(); + Response response = new Response(OK); + response.headers().put("Jdisc-Local-Port", Integer.toString(port)); + return handler.handleResponse(response); + } + } + + private static class EchoWithHeaderRequestHandler extends AbstractRequestHandler { + + final String headerName; + final String headerValue; + + EchoWithHeaderRequestHandler(final String headerName, final String headerValue) { + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final Response response = new Response(OK); + response.headers().add(headerName, headerValue); + return handler.handleResponse(response); + } + } + + private static Module newBindingSetSelector(final String setName) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(new BindingSetSelector() { + + @Override + public String select(final URI uri) { + return setName; + } + }); + } + }; + } + + private static class CookieComparator implements Comparator { + + @Override + public int compare(final Cookie lhs, final Cookie rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java new file mode 100644 index 00000000000..6d1baf0423f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java @@ -0,0 +1,25 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A {@link ConnectionLog} that aggregates log entries in memory + * + * @author bjorncs + */ +class InMemoryConnectionLog implements ConnectionLog { + + private final List logEntries = new CopyOnWriteArrayList<>(); + + @Override + public void log(ConnectionLogEntry entry) { + logEntries.add(entry); + } + + List logEntries() { return logEntries; } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java new file mode 100644 index 00000000000..b87ec5e8b8b --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java @@ -0,0 +1,20 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author bjorncs + */ +public class InMemoryRequestLog implements RequestLog { + + private final List entries = new CopyOnWriteArrayList<>(); + + @Override public void log(RequestLogEntry entry) { entries.add(entry); } + + List entries() { return entries; } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java new file mode 100644 index 00000000000..230f59cbb34 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java @@ -0,0 +1,80 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpTrace; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; + +import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Simon Thoresen Hult + */ +public class JDiscHttpServletTest { + + @Test + public void requireThatServerRespondsToAllMethods() throws Exception { + final TestDriver driver = TestDrivers.newInstance(newEchoHandler()); + final URI uri = driver.client().newUri("/status.html"); + driver.client().execute(new HttpGet(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPost(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpHead(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPut(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpDelete(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpOptions(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpTrace(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPatch(uri)) + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerResponds405ToUnknownMethods() throws IOException { + TestDriver driver = TestDrivers.newInstance(newEchoHandler()); + final URI uri = driver.client().newUri("/status.html"); + driver.client().execute(new UnknownMethodHttpRequest(uri)) + .expectStatusCode(is(METHOD_NOT_ALLOWED)); + assertThat(driver.close(), is(true)); + } + + private static RequestHandler newEchoHandler() { + return new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + }; + } + + private static class UnknownMethodHttpRequest extends HttpRequestBase { + UnknownMethodHttpRequest(URI uri) { setURI(uri); } + @Override public String getMethod() { return "UNKNOWN_METHOD"; } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java new file mode 100644 index 00000000000..f839d83a800 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java @@ -0,0 +1,28 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Module; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.application.MetricConsumer; + +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author bjorncs + */ +class MetricConsumerMock { + + static final Metric.Context STATIC_CONTEXT = new Metric.Context() {}; + + private final MetricConsumer mockitoMock = mock(MetricConsumer.class); + + MetricConsumerMock() { + when(mockitoMock.createContext(anyMap())).thenReturn(STATIC_CONTEXT); + } + + MetricConsumer mockitoMock() { return mockitoMock; } + Module asGuiceModule() { return binder -> binder.bind(MetricConsumer.class).toInstance(mockitoMock); } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java new file mode 100644 index 00000000000..f1d710bd10f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java @@ -0,0 +1,202 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.entity.GzipCompressingEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; + +/** + * A simple http client for testing + * + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class SimpleHttpClient implements AutoCloseable { + + private final CloseableHttpClient delegate; + private final String scheme; + private final int listenPort; + + public SimpleHttpClient(SSLContext sslContext, int listenPort, boolean useCompression) { + this(sslContext, null, null, listenPort, useCompression); + } + + public SimpleHttpClient(SSLContext sslContext, List enabledProtocols, List enabledCiphers, + int listenPort, boolean useCompression) { + HttpClientBuilder builder = HttpClientBuilder.create(); + if (!useCompression) { + builder.disableContentCompression(); + } + if (sslContext != null) { + SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( + sslContext, + toArray(enabledProtocols), + toArray(enabledCiphers), + new DefaultHostnameVerifier()); + builder.setSSLSocketFactory(sslConnectionFactory); + + Registry registry = RegistryBuilder.create() + .register("https", sslConnectionFactory) + .build(); + builder.setConnectionManager(new BasicHttpClientConnectionManager(registry)); + scheme = "https"; + } else { + scheme = "http"; + } + this.delegate = builder.build(); + this.listenPort = listenPort; + } + + private static String[] toArray(List list) { + return list != null ? list.toArray(new String[0]) : null; + } + + public URI newUri(final String path) { + return URI.create(scheme + "://localhost:" + listenPort + path); + } + + public RequestExecutor newGet(String path) { + return newRequest(new HttpGet(newUri(path))); + } + + public RequestExecutor newPost(String path) { + return newRequest(new HttpPost(newUri(path))); + } + + public RequestExecutor newRequest(HttpUriRequest request) { + return new RequestExecutor().setRequest(request); + } + + public ResponseValidator execute(HttpUriRequest request) throws IOException { + return newRequest(request).execute(); + } + + public ResponseValidator get(String path) throws IOException { + return newGet(path).execute(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + public class RequestExecutor { + + private HttpUriRequest request; + private HttpEntity entity; + + public RequestExecutor setRequest(final HttpUriRequest request) { + this.request = request; + return this; + } + + public RequestExecutor addHeader(final String name, final String value) { + this.request.addHeader(name, value); + return this; + } + + public RequestExecutor setContent(final String content) { + this.entity = new StringEntity(content, StandardCharsets.UTF_8); + return this; + } + + public RequestExecutor setGzipContent(String content) { + this.entity = new GzipCompressingEntity(new StringEntity(content, StandardCharsets.UTF_8)); + return this; + } + + public RequestExecutor setBinaryContent(final byte[] content) { + this.entity = new ByteArrayEntity(content); + return this; + } + + public RequestExecutor setMultipartContent(final FormBodyPart... parts) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + Arrays.stream(parts).forEach(part -> builder.addPart(part.getName(), part.getBody())); + this.entity = builder.build(); + return this; + } + + public ResponseValidator execute() throws IOException { + if (entity != null) { + ((HttpPost)request).setEntity(entity); + } + try (CloseableHttpResponse response = delegate.execute(request)){ + return new ResponseValidator(response); + } + } + } + + public static class ResponseValidator { + + private final HttpResponse response; + private final String content; + + public ResponseValidator(HttpResponse response) throws IOException { + this.response = response; + + HttpEntity entity = response.getEntity(); + this.content = entity == null ? null : EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + + public ResponseValidator expectStatusCode(Matcher matcher) { + MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), matcher); + return this; + } + + public ResponseValidator expectHeader(String headerName, Matcher matcher) { + Header firstHeader = response.getFirstHeader(headerName); + String headerValue = firstHeader != null ? firstHeader.getValue() : null; + MatcherAssert.assertThat(headerValue, matcher); + assertNotNull(firstHeader); + return this; + } + + public ResponseValidator expectNoHeader(String headerName) { + Header firstHeader = response.getFirstHeader(headerName); + assertThat(firstHeader, is(nullValue())); + return this; + } + + public ResponseValidator expectContent(final Matcher matcher) { + MatcherAssert.assertThat(content, matcher); + return this; + } + + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java new file mode 100644 index 00000000000..20f050d715d --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java @@ -0,0 +1,42 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.junit.Test; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author mortent + */ +public class SslHandshakeFailedListenerTest { + + private Metric metrics = mock(Metric.class); + SslHandshakeFailedListener listener = new SslHandshakeFailedListener(metrics, "connector", 1234); + + @Test + public void includes_client_ip_dimension_present_when_peer_available() { + listener.handshakeFailed(handshakeEvent(true), new SSLHandshakeException("Empty server certificate chain")); + verify(metrics).createContext(eq(Map.of("clientIp", "127.0.0.1", "serverName", "connector", "serverPort", 1234))); + } + + @Test + public void does_not_include_client_ip_dimension_present_when_peer_unavailable() { + listener.handshakeFailed(handshakeEvent(false), new SSLHandshakeException("Empty server certificate chain")); + verify(metrics).createContext(eq(Map.of("serverName", "connector", "serverPort", 1234))); + } + + private SslHandshakeListener.Event handshakeEvent(boolean includePeer) { + var sslEngine = mock(SSLEngine.class); + if(includePeer) when(sslEngine.getPeerHost()).thenReturn("127.0.0.1"); + return new SslHandshakeListener.Event(sslEngine); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java new file mode 100644 index 00000000000..875889ed5ce --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java @@ -0,0 +1,79 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Module; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.security.SslContextBuilder; + +import javax.net.ssl.SSLContext; +import java.nio.file.Paths; + +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * This class is based on the class by the same name in the jdisc_http_service module. + * It provides functionality for setting up a jdisc container with an HTTP server and handlers. + * + * @author Simon Thoresen Hult + * @author bakksjo + */ +public class TestDriver { + + private final com.yahoo.jdisc.test.TestDriver driver; + private final JettyHttpServer server; + private final SimpleHttpClient client; + + private TestDriver(com.yahoo.jdisc.test.TestDriver driver, JettyHttpServer server, SimpleHttpClient client) { + this.driver = driver; + this.server = server; + this.client = client; + } + + public static TestDriver newInstance(Class serverClass, + RequestHandler requestHandler, + Module testConfig) { + com.yahoo.jdisc.test.TestDriver driver = + com.yahoo.jdisc.test.TestDriver.newSimpleApplicationInstance(testConfig); + ContainerBuilder builder = driver.newContainerBuilder(); + JettyHttpServer server = builder.getInstance(serverClass); + builder.serverProviders().install(server); + builder.serverBindings().bind("http://*/*", requestHandler); + driver.activateContainer(builder); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(newSslContext(builder), server.getListenPort(), false); + return new TestDriver(driver, server, client); + } + + public boolean close() { + server.close(); + server.release(); + uncheck(client::close); + return driver.close(); + } + + public JettyHttpServer server() { return server; } + + public SimpleHttpClient client() { return client; } + + public SimpleHttpClient newClient(final boolean useCompression) { + return new SimpleHttpClient(newSslContext(), server.getListenPort(), useCompression); + } + + public SSLContext newSslContext() { + return newSslContext(driver.newContainerBuilder()); + } + + private static SSLContext newSslContext(ContainerBuilder builder) { + ConnectorConfig.Ssl sslConfig = builder.getInstance(ConnectorConfig.class).ssl(); + if (!sslConfig.enabled()) return null; + + return new SslContextBuilder() + .withKeyStore(Paths.get(sslConfig.privateKeyFile()), Paths.get(sslConfig.certificateFile())) + .withTrustStore(Paths.get(sslConfig.caCertificateFile())) + .build(); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java new file mode 100644 index 00000000000..7d7530c32e0 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java @@ -0,0 +1,94 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.guiceModules.ServletModule; + +import java.nio.file.Path; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class TestDrivers { + + public static TestDriver newConfiguredInstance(RequestHandler requestHandler, + ServerConfig.Builder serverConfig, + ConnectorConfig.Builder connectorConfig, + Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule(serverConfig, connectorConfig, guiceModules)); + } + + public static TestDriver newInstance(RequestHandler requestHandler, Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder(), + new ConnectorConfig.Builder(), + guiceModules + )); + } + + public enum TlsClientAuth { NEED, WANT } + + public static TestDriver newInstanceWithSsl(RequestHandler requestHandler, + Path certificateFile, + Path privateKeyFile, + TlsClientAuth tlsClientAuth, + Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), + new ConnectorConfig.Builder() + .tlsClientAuthEnforcer( + new ConnectorConfig.TlsClientAuthEnforcer.Builder() + .enable(true) + .pathWhitelist("/status.html")) + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .clientAuth(tlsClientAuth == TlsClientAuth.NEED + ? ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH + : ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH) + .privateKeyFile(privateKeyFile.toString()) + .certificateFile(certificateFile.toString()) + .caCertificateFile(certificateFile.toString())), + guiceModules)); + } + + private static Module newConfigModule(ServerConfig.Builder serverConfig, + ConnectorConfig.Builder connectorConfigBuilder, + Module... guiceModules) { + return Modules.override( + Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(ServletPathsConfig.class).toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ServerConfig.class).toInstance(new ServerConfig(serverConfig)); + bind(ConnectorConfig.class).toInstance(new ConnectorConfig(connectorConfigBuilder)); + bind(FilterBindings.class).toInstance(new FilterBindings.Builder().build()); + bind(ConnectionLog.class).toInstance(new VoidConnectionLog()); + bind(RequestLog.class).toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule(connectorConfigBuilder), + new ServletModule())) + .with(guiceModules); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java new file mode 100644 index 00000000000..16969a47b84 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java @@ -0,0 +1,166 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.server.jetty.FilterBindings; +import com.yahoo.jdisc.http.server.jetty.FilterInvoker; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; + +/** + * @author Tony Vaagenes + * @author bjorncs + */ +public class JDiscFilterForServletTest extends ServletTestBase { + @Test + public void request_filter_can_return_response() throws IOException, InterruptedException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectContent(containsString(TestRequestFilter.responseContent)); + } + + @Test + public void request_can_be_forwarded_through_request_filter_to_servlet() throws IOException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH). + addHeader(TestRequestFilter.BYPASS_FILTER_HEADER, Boolean.TRUE.toString()). + execute(); + + response.expectContent(containsString(TestServlet.RESPONSE_CONTENT)); + } + + @Test + public void response_filter_can_modify_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_sync_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_async_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH). + addHeader(NoContentTestServlet.HEADER_ASYNC, Boolean.TRUE.toString()). + execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + private TestDriver requestFilterTestDriver() throws IOException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new TestRequestFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + return TestDrivers.newInstance(dummyRequestHandler, bindings(filterBindings)); + } + + private TestDriver responseFilterTestDriver() throws IOException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new TestResponseFilter()) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + return TestDrivers.newInstance(dummyRequestHandler, bindings(filterBindings)); + } + + + + private Module bindings(FilterBindings filterBindings) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class).toInstance(filterBindings); + bind(FilterInvoker.class).toInstance(new FilterInvoker() { + @Override + public HttpServletRequest invokeRequestFilterChain( + RequestFilter requestFilter, + URI uri, + HttpServletRequest httpRequest, + ResponseHandler responseHandler) { + TestRequestFilter filter = (TestRequestFilter) requestFilter; + filter.runAsSecurityFilter(httpRequest, responseHandler); + return httpRequest; + } + + @Override + public void invokeResponseFilterChain( + ResponseFilter responseFilter, + URI uri, + HttpServletRequest request, + HttpServletResponse response) { + + TestResponseFilter filter = (TestResponseFilter) responseFilter; + filter.runAsSecurityFilter(request, response); + } + }); + } + }, + guiceModule()); + } + + static class TestRequestFilter extends AbstractResource implements RequestFilter { + static final String simpleName = TestRequestFilter.class.getSimpleName(); + static final String responseContent = "Rejected by " + simpleName; + static final String BYPASS_FILTER_HEADER = "BYPASS_HEADER" + simpleName; + + @Override + public void filter(HttpRequest request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + public void runAsSecurityFilter(HttpServletRequest request, ResponseHandler responseHandler) { + if (Boolean.parseBoolean(request.getHeader(BYPASS_FILTER_HEADER))) + return; + + ContentChannel contentChannel = responseHandler.handleResponse(new Response(500)); + contentChannel.write(ByteBuffer.wrap(responseContent.getBytes(StandardCharsets.UTF_8)), null); + contentChannel.close(null); + } + } + + + static class TestResponseFilter extends AbstractResource implements ResponseFilter { + static final String INVOKED_HEADER = TestResponseFilter.class.getSimpleName() + "_INVOKED_HEADER"; + + @Override + public void filter(Response response, Request request) { + throw new UnsupportedClassVersionError(); + } + + public void runAsSecurityFilter(HttpServletRequest request, HttpServletResponse response) { + response.addHeader(INVOKED_HEADER, Boolean.TRUE.toString()); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java new file mode 100644 index 00000000000..a533a447f6a --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java @@ -0,0 +1,64 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.junit.Test; +import org.mockito.verification.VerificationMode; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +/** + * @author bakksjo + * @author bjorncs + */ +public class ServletAccessLoggingTest extends ServletTestBase { + private static final long MAX_LOG_WAIT_TIME_MILLIS = TimeUnit.SECONDS.toMillis(60); + + @Test + public void accessLogIsInvokedForNonJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + httpGet(testDriver, TestServlet.PATH).execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + @Test + public void accessLogIsInvokedForJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + testDriver.client().newGet("/status.html").execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + private void verifyCallsLog(RequestLog requestLog, final VerificationMode verificationMode) { + verify(requestLog, verificationMode).log(any(RequestLogEntry.class)); + } + + private TestDriver newTestDriver(RequestLog requestLog) throws IOException { + return TestDrivers.newInstance(dummyRequestHandler, bindings(requestLog)); + } + + private Module bindings(RequestLog requestLog) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(RequestLog.class).toInstance(requestLog); + } + }, + guiceModule()); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java new file mode 100644 index 00000000000..54bfe8c026d --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java @@ -0,0 +1,132 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.ServletPathsConfig.Servlets.Builder; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.RequestExecutor; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import org.eclipse.jetty.servlet.ServletHolder; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +/** + * @author Tony Vaagenes + * @author bakksjo + */ +public class ServletTestBase { + + private static class ServletInstance { + final ComponentId componentId; final String path; final HttpServlet instance; + + ServletInstance(ComponentId componentId, String path, HttpServlet instance) { + this.componentId = componentId; + this.path = path; + this.instance = instance; + } + } + + private final List servlets = List.of( + new ServletInstance(TestServlet.ID, TestServlet.PATH, new TestServlet()), + new ServletInstance(NoContentTestServlet.ID, NoContentTestServlet.PATH, new NoContentTestServlet())); + + protected RequestExecutor httpGet(TestDriver testDriver, String path) { + return testDriver.client().newGet("/" + path); + } + + protected ServletPathsConfig createServletPathConfig() { + ServletPathsConfig.Builder configBuilder = new ServletPathsConfig.Builder(); + + servlets.forEach(servlet -> + configBuilder.servlets( + servlet.componentId.stringValue(), + new Builder().path(servlet.path))); + + return new ServletPathsConfig(configBuilder); + } + + protected ComponentRegistry servlets() { + ComponentRegistry result = new ComponentRegistry<>(); + + servlets.forEach(servlet -> + result.register(servlet.componentId, new ServletHolder(servlet.instance))); + + result.freeze(); + return result; + } + + protected Module guiceModule() { + return new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>(){}).toInstance(servlets()); + bind(ServletPathsConfig.class).toInstance(createServletPathConfig()); + } + }; + } + + protected static class TestServlet extends HttpServlet { + static final String PATH = "servlet/test-servlet"; + static final ComponentId ID = ComponentId.fromString("test-servlet"); + static final String RESPONSE_CONTENT = "Response from " + TestServlet.class.getSimpleName(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + PrintWriter writer = response.getWriter(); + writer.write(RESPONSE_CONTENT); + writer.close(); + } + } + + @WebServlet(asyncSupported = true) + protected static class NoContentTestServlet extends HttpServlet { + static final String HEADER_ASYNC = "HEADER_ASYNC"; + + static final String PATH = "servlet/no-content-test-servlet"; + static final ComponentId ID = ComponentId.fromString("no-content-test-servlet"); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (request.getHeader(HEADER_ASYNC) != null) { + asyncGet(request); + } + } + + private void asyncGet(HttpServletRequest request) { + request.startAsync().start(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + log("Interrupted", e); + } finally { + request.getAsyncContext().complete(); + } + }); + } + } + + + protected static final RequestHandler dummyRequestHandler = new AbstractRequestHandler() { + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + }; +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java new file mode 100644 index 00000000000..eb292199ea2 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java @@ -0,0 +1,71 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl.impl; + +import com.yahoo.security.KeyUtils; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.AuthorizationMode; +import com.yahoo.security.tls.DefaultTlsContext; +import com.yahoo.security.tls.HostnameVerification; +import com.yahoo.security.tls.PeerAuthentication; +import com.yahoo.security.tls.TlsContext; +import com.yahoo.security.tls.policy.AuthorizedPeers; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; + +import static com.yahoo.security.KeyAlgorithm.EC; +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author bjorncs + */ +public class TlsContextBasedProviderTest { + + @Test + public void creates_sslcontextfactory_from_tlscontext() { + TlsContext tlsContext = createTlsContext(); + var provider = new SimpleTlsContextBasedProvider(tlsContext); + SslContextFactory sslContextFactory = provider.getInstance("dummyContainerId", 8080); + assertNotNull(sslContextFactory); + assertArrayEquals(tlsContext.parameters().getCipherSuites(), sslContextFactory.getIncludeCipherSuites()); + } + + private static TlsContext createTlsContext() { + KeyPair keyPair = KeyUtils.generateKeypair(EC); + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, + new X500Principal("CN=dummy"), + Instant.EPOCH, + Instant.EPOCH.plus(100000, ChronoUnit.DAYS), + SHA256_WITH_ECDSA, + BigInteger.ONE) + .build(); + return new DefaultTlsContext( + List.of(certificate), keyPair.getPrivate(), List.of(certificate), new AuthorizedPeers(Set.of()), AuthorizationMode.ENFORCE, PeerAuthentication.NEED, HostnameVerification.ENABLED); + } + + private static class SimpleTlsContextBasedProvider extends TlsContextBasedProvider { + final TlsContext tlsContext; + + SimpleTlsContextBasedProvider(TlsContext tlsContext) { + this.tlsContext = tlsContext; + } + + @Override + protected TlsContext getTlsContext(String containerId, int port) { + return tlsContext; + } + + } +} \ No newline at end of file -- cgit v1.2.3