aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgjoranv <gv@verizonmedia.com>2021-03-18 20:10:52 +0100
committergjoranv <gv@verizonmedia.com>2021-03-23 22:58:12 +0100
commit371ff8163a3f0c2912c8974e1e57b6cd39f09156 (patch)
tree29018b21f2c19bba146303444bd00d25c07e9e51
parent17349ba3ab25ff89ba449244242f9cacb4846bb1 (diff)
Add test java source files from jdisc_http_service.
-rw-r--r--container-core/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java42
-rw-r--r--container-core/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java295
-rw-r--r--container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java44
-rw-r--r--container-core/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java208
-rw-r--r--container-core/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java27
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java238
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java17
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java206
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java139
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java357
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java113
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java48
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java45
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java29
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java57
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java25
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java145
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java74
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java179
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java87
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java54
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java24
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java156
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java24
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java78
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java83
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java44
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java51
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java667
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java204
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java221
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java847
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java1201
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java25
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java20
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java80
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java28
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java202
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java42
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java79
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java94
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java166
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java64
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java132
-rw-r--r--container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java71
45 files changed, 7032 insertions, 0 deletions
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<Path, Integer, String> decompressor) throws IOException, InterruptedException {
+ File root = temporaryFolder.newFolder("testcompression" + compression.name());
+
+ LogFileHandler<String> 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<String> {
+
+ @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<Cookie> cookies) {
+ String actual = Cookie.toCookieHeader(cookies);
+ String expectedResult1 = expectedResult;
+ assertThat(actual, equalTo(expectedResult1));
+ }
+
+ private static void assertEncodeSetCookie(List<String> expectedResult, List<Cookie> cookies) {
+ assertThat(Cookie.toSetCookieHeaders(cookies), containsInAnyOrder(expectedResult.toArray()));
+ }
+
+ private static void assertDecodeCookie(List<Cookie> 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<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar"));
+ request.encodeCookieHeader(cookies);
+ final List<String> 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<Cookie> 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<Cookie> cookies = Collections.singletonList(new Cookie("foo", "bar"));
+ response.encodeSetCookieHeader(cookies);
+ final List<String> 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<Cookie> cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox"));
+ response.encodeSetCookieHeader(cookies);
+ final List<String> 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<Cookie> 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<Cookie> 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<Cookie> cookies = new ArrayList<Cookie>();
+ 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<Cookie> 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<String> e = request.getAttributeNames();
+ List<String> 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&param2=xyz&param2=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<String> values = request.getParameterValuesAsList("param2");
+ Assert.assertEquals(values.get(0),"xyz");
+ Assert.assertEquals(values.get(1),"pqr");
+
+ List<String> paramNames = request.getParameterNamesAsList();
+ Assert.assertEquals(paramNames.size(), 2);
+
+ }
+
+ @Test
+ public void testParameterAPI(){
+ URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc&param2=xyz&param2=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<String> values = request.getParameterValues("param2");
+ List<String> valuesList = Collections.list(values);
+ Assert.assertEquals(valuesList.get(0),"xyz");
+ Assert.assertEquals(valuesList.get(1),"pqr");
+
+ Enumeration<String> paramNames = request.getParameterNames();
+ List<String> 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<Cookie> 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<String> 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<Cookie> 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<Cookie> 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<SecurityRequestFilter> requestFilters = new ArrayList<SecurityRequestFilter>();
+ 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<String> 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<ConnectorFactory> connectorFactoryComponentRegistry() {
+ ComponentRegistry<ConnectorFactory> 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<ServletHolder> 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<String> 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<RequestLogEntry> 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<String, ?> 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 =
+ "<html>\n" +
+ "<head>\n" +
+ "<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\"/>\n" +
+ "<title>Error 200</title>\n" +
+ "</head>\n" +
+ "<body>\n" +
+ "<h2>HTTP ERROR: 200</h2>\n" +
+ "<p>Problem accessing http://foo.bar. Reason:\n" +
+ "<pre> My custom error message</pre></p>\n" +
+ "<hr/>\n" +
+ "</body>\n" +
+ "</html>\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 <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+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<HttpRequest> 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<ResponseHandler> 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<Response> 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<Response> 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<Map<String, List<String>>> headerCopy = new AtomicReference<>(null);
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ try {
+ headerCopy.set(new HashMap<String, List<String>>(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<String, List<String>> 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> T getInstance(Key<T> tKey) {
+ return null;
+ }
+
+ @Override
+ public <T> T getInstance(Class<T> 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<String> monitoringPaths = List.of("/status.html");
+ private List<String> 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<StatisticsEntry> 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<StatisticsEntry> 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<ResponseGist> success() {
+ final Matcher<Integer> expectedStatusCode = is(OK);
+ final Matcher<String> expectedReasonPhrase = is("OK");
+ final Matcher<String> expectedContent = is(RESPONSE_CONTENT);
+ return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent);
+ }
+
+ private static Matcher<ResponseGist> successNoContent() {
+ final Matcher<Integer> expectedStatusCode = is(OK);
+ final Matcher<String> expectedReasonPhrase = is("OK");
+ final Matcher<String> expectedContent = is("");
+ return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent);
+ }
+
+ private static Matcher<ResponseGist> serverError() {
+ final Matcher<Integer> expectedStatusCode = is(INTERNAL_SERVER_ERROR);
+ final Matcher<String> expectedReasonPhrase = any(String.class);
+ final Matcher<String> expectedContent = containsString(ConformanceException.class.getSimpleName());
+ return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent);
+ }
+
+ private static Matcher<ResponseGist> errorWithReason(
+ final Matcher<Integer> expectedStatusCode, final Matcher<String> expectedReasonPhrase) {
+ final Matcher<String> expectedContent = any(String.class);
+ return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent);
+ }
+
+ private static Matcher<ResponseGist> responseMatcher(
+ final Matcher<Integer> expectedStatusCode,
+ final Matcher<String> expectedReasonPhrase,
+ final Matcher<String> expectedContent) {
+ return new TypeSafeMatcher<ResponseGist>() {
+ @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<JettyHttpServer, ClientProxy, Future<HttpResponse>> {
+
+ private Matcher<ResponseGist> 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<ResponseGist> 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<JettyHttpServer> getServerProviderClass() {
+ return JettyHttpServer.class;
+ }
+
+ @Override
+ public ClientProxy newClient(final JettyHttpServer server) throws Throwable {
+ return new ClientProxy(server.getListenPort(), requestVersion);
+ }
+
+ @Override
+ public Future<HttpResponse> 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<ByteBuffer> newResponseContent() {
+ return Collections.singleton(StandardCharsets.UTF_8.encode(RESPONSE_CONTENT));
+ }
+
+ @Override
+ public void validateResponse(final Future<HttpResponse> 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 &apos;unknown&apos;\\.\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<HttpResponseStatisticsCollector.StatisticsEntry> 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<HttpResponseStatisticsCollector.StatisticsEntry> 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<ConnectionLogEntry> 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<? extends SSLHandshakeException> 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<String> protocols = protocolOverride != null ? List.of(protocolOverride) : null;
+ List<String> 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<Cookie> 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<String, List<String>> 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<Cookie> {
+
+ @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<ConnectionLogEntry> logEntries = new CopyOnWriteArrayList<>();
+
+ @Override
+ public void log(ConnectionLogEntry entry) {
+ logEntries.add(entry);
+ }
+
+ List<ConnectionLogEntry> 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<RequestLogEntry> entries = new CopyOnWriteArrayList<>();
+
+ @Override public void log(RequestLogEntry entry) { entries.add(entry); }
+
+ List<RequestLogEntry> 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<String> enabledProtocols, List<String> 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<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>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<String> 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<Integer> matcher) {
+ MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), matcher);
+ return this;
+ }
+
+ public ResponseValidator expectHeader(String headerName, Matcher<String> 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<String> 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<? extends JettyHttpServer> 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<ServletInstance> 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<ServletHolder> servlets() {
+ ComponentRegistry<ServletHolder> 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<ComponentRegistry<ServletHolder>>(){}).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