summaryrefslogtreecommitdiffstats
path: root/jdisc_http_service
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /jdisc_http_service
Publish
Diffstat (limited to 'jdisc_http_service')
-rw-r--r--jdisc_http_service/.gitignore2
-rw-r--r--jdisc_http_service/OWNERS2
-rwxr-xr-xjdisc_http_service/README.sh15
-rw-r--r--jdisc_http_service/docs/class-diagram.graffle1856
-rw-r--r--jdisc_http_service/docs/class-diagram.pngbin0 -> 87451 bytes
-rw-r--r--jdisc_http_service/docs/httpserver.html96
-rw-r--r--jdisc_http_service/pom.xml210
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java26
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java297
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java124
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java316
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java130
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java15
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java28
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java187
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java26
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java99
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java36
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java48
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java124
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java25
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java65
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java121
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java264
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java33
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java37
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java26
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java79
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java159
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java12
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java14
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java79
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java24
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java4
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java4
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java4
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java57
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java142
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java544
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java154
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java42
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java95
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java110
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java67
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java13
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java9
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java32
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java14
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java9
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java95
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java13
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java78
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java8
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java89
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java149
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java85
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java24
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java24
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java55
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java54
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java29
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java5
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java7
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java7
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java30
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java150
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java80
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java68
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java22
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java350
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java59
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java30
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java28
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java266
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java165
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java132
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java190
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java210
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java98
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java39
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java287
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java201
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java372
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java80
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java23
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java256
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java39
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java36
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java286
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java266
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java213
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java32
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java419
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java38
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java3
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java4
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java37
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java23
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java245
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java68
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java5
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java34
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java22
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java88
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java29
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java17
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java4
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java124
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java119
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java70
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java53
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java110
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java146
-rw-r--r--jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def36
-rw-r--r--jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def79
-rw-r--r--jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def23
-rw-r--r--jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def5
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java34
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java72
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java315
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java58
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java19
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java249
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java142
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java230
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java60
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java17
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java108
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java40
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java578
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java42
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java30
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java97
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java148
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java79
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java357
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java115
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java48
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java45
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java35
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java25
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java173
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java87
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java80
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java24
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java63
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java136
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java165
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java51
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java513
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java549
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java818
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java100
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java710
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java63
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java219
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java45
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java84
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java95
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java766
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java102
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java175
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java62
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java123
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java52
-rw-r--r--jdisc_http_service/src/test/resources/ssl_keystore_test.jksbin0 -> 2061 bytes
166 files changed, 20604 insertions, 0 deletions
diff --git a/jdisc_http_service/.gitignore b/jdisc_http_service/.gitignore
new file mode 100644
index 00000000000..3cc25b51fc4
--- /dev/null
+++ b/jdisc_http_service/.gitignore
@@ -0,0 +1,2 @@
+/pom.xml.build
+/target
diff --git a/jdisc_http_service/OWNERS b/jdisc_http_service/OWNERS
new file mode 100644
index 00000000000..5255d2560bb
--- /dev/null
+++ b/jdisc_http_service/OWNERS
@@ -0,0 +1,2 @@
+bakksjo
+gjoranv
diff --git a/jdisc_http_service/README.sh b/jdisc_http_service/README.sh
new file mode 100755
index 00000000000..b17f71e119f
--- /dev/null
+++ b/jdisc_http_service/README.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+CURRENT=$(date -u '+%F %T %Z')
+
+if [ -z ${VERSION} ]; then
+ echo "ERROR: No version number defined";
+ exit 1;
+fi
+
+cat <<EOF
+
+This package provides a ClientProvider and a ServerProvider implementation using HTTP on JDisc.
+
+EOF
diff --git a/jdisc_http_service/docs/class-diagram.graffle b/jdisc_http_service/docs/class-diagram.graffle
new file mode 100644
index 00000000000..938459c6571
--- /dev/null
+++ b/jdisc_http_service/docs/class-diagram.graffle
@@ -0,0 +1,1856 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.7.0.167456</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {558.99999713897705, 783}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2012-06-18 12:41:37 +0000</string>
+ <key>Creator</key>
+ <string>Einar Rosenvinge</string>
+ <key>DisplayScale</key>
+ <string>1.000 cm = 1.000 cm</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{404.66666889190674, 39.999999999999936}, {151.17318725585938, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>114</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 J2SE API}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{404.66666889190674, 18.66666666666665}, {151.17318725585938, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>109</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 Netty API}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>108</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{404.66666666666731, 7.9472862957175039e-08}, {151.17318725585938, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>107</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 jDISC core API}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{328.46341597965198, 296.25884156306495}, {29.333332061767578, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>106</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>105</integer>
+ <key>Offset</key>
+ <real>6.6666665077209473</real>
+ <key>Position</key>
+ <real>0.91559326648712158</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 1}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>52</integer>
+ </dict>
+ <key>ID</key>
+ <integer>105</integer>
+ <key>Points</key>
+ <array>
+ <string>{225.55682373046864, 284.810302734375}</string>
+ <string>{352.43905966196957, 312.07829430296113}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>StickArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>104</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{101.99999999999997, 277.810302734375}, {123.55682373046864, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>104</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 ChannelPipeline}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{166.25436339285309, 474.88842165638926}, {29.333332061767578, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>103</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>102</integer>
+ <key>Offset</key>
+ <real>6.6666665077209473</real>
+ <key>Position</key>
+ <real>0.78203368186950684</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 1}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>89</integer>
+ </dict>
+ <key>ID</key>
+ <integer>102</integer>
+ <key>Points</key>
+ <array>
+ <string>{291.52424638132226, 513.75253787937902}</string>
+ <string>{148.21246360738095, 481.32189037141075}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>StickArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>58</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>95</integer>
+ </dict>
+ <key>ID</key>
+ <integer>96</integer>
+ <key>Points</key>
+ <array>
+ <string>{116.77841269969511, 466.71157835576878}</string>
+ <string>{116.77840998702608, 420.16666668003933}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>89</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{58.945077896118171, 405.66666666666708}, {115.66666412353516, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>95</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 Request}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{68.3333333333334, 467.21157836914102}, {96.890159606933594, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>89</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 HttpRequest}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>88</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>86</integer>
+ </dict>
+ <key>ID</key>
+ <integer>87</integer>
+ <key>Points</key>
+ <array>
+ <string>{350.25724339020428, 513.73666184812191}</string>
+ <string>{432.54926154576208, 492.26520134230651}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>58</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{429.24670918782579, 464.13918876647955}, {115.08661651611328, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>86</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 &lt;&lt;interface&gt;&gt;\
+ResponseHandler}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>85</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>80</integer>
+ </dict>
+ <key>ID</key>
+ <integer>81</integer>
+ <key>Points</key>
+ <array>
+ <string>{388.98464357649044, 311.73041212162735}</string>
+ <string>{421.12154403577119, 242.99627753494852}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>52</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{298.33331664403278, 228.54334004720087}, {246, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>80</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 IdleStateAwareChannelUpstreamHandler}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>10</integer>
+ </dict>
+ <key>ID</key>
+ <integer>27</integer>
+ <key>Points</key>
+ <array>
+ <string>{129.27841644847507, 130.04334003445351}</string>
+ <string>{129.27841313680011, 80.666667938232422}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>69</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{71.445081075032547, 66.666667938232422}, {115.66666412353516, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>10</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 AbstractResource}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>78</integer>
+ </dict>
+ <key>ID</key>
+ <integer>79</integer>
+ <key>Points</key>
+ <array>
+ <string>{185.26276724928567, 130.48075966792658}</string>
+ <string>{297.8372493866778, 116.27917789513803}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>69</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{298.3333059188999, 92.666666666666913}, {151.17318725585938, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>78</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 &lt;&lt;interface&gt;&gt;\
+ChannelPipelineFactory}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>77</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>75</integer>
+ </dict>
+ <key>ID</key>
+ <integer>76</integer>
+ <key>Points</key>
+ <array>
+ <string>{402.97819417613408, 311.99312587103537}</string>
+ <string>{465.1473485992222, 286.41782928501391}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>52</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{454.91339111328136, 258.22751967112259}, {89.419929504394531, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>75</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 &lt;&lt;interface&gt;&gt;\
+Runnable}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>74</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>34</integer>
+ </dict>
+ <key>ID</key>
+ <integer>9</integer>
+ <key>Points</key>
+ <array>
+ <string>{143.15706944536032, 130.31230832708621}</string>
+ <string>{250.79491585172687, 74.231022194055214}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>Arrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>Pattern</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>69</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66667683919275, 45.999999999999929}, {102.88706970214844, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>34</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 &lt;&lt;interface&gt;&gt;\
+ServerProvider}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>ID</key>
+ <integer>33</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{344.6666666666668, 438.85001627604163}, {17, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>YES</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>63</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 1}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>Wrap</key>
+ <string>NO</string>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{308.27841186523432, 496.00000000000023}, {29.333332061767578, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>62</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 1..*}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>58</integer>
+ </dict>
+ <key>ID</key>
+ <integer>61</integer>
+ <key>Points</key>
+ <array>
+ <string>{364.20702685721608, 438.63027009978003}</string>
+ <string>{326.6915789284576, 513.41623846111793}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>0</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>54</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666603088379, 513.86289469401004}, {192.55682373046875, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>58</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 RequestContext}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666603088379, 527.86289469401004}, {192.55682373046875, 42}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>59</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 HttpRequest request\
+ContentChannel requestContent\
+ContentChannel responseContent}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666603088379, 569.86289469401004}, {192.55682373046875, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>60</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 connect()\
+handleResponse(Response)}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>58</integer>
+ <integer>59</integer>
+ <integer>60</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>57</integer>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666666666663, 312.183349609375}, {317.66665649414062, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>52</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 ChannelContext}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666666666652, 326.183349609375}, {317.66665649414062, 28}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>53</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 LinkedBlockingQueue&lt;ResponsePart&gt; responseOutputs\
+Channel serverChannel}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{226.66666666666663, 354.183349609375}, {317.66665649414062, 84}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>54</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 channelConnected()\
+messageReceived()\
+exceptionCaught()\
+channelDisconnected()\
+channelIdle()\
+run()}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>52</integer>
+ <integer>53</integer>
+ <integer>54</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>51</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{158.97158196265923, 262.43389980796718}, {29.333332061767578, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>47</integer>
+ <key>Line</key>
+ <dict>
+ <key>ID</key>
+ <integer>16</integer>
+ <key>Offset</key>
+ <real>12.666667938232422</real>
+ <key>Position</key>
+ <real>0.93361091613769531</real>
+ <key>RotationType</key>
+ <integer>0</integer>
+ </dict>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 0..*}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>104</integer>
+ </dict>
+ <key>ID</key>
+ <integer>16</integer>
+ <key>Points</key>
+ <array>
+ <string>{144.7864237041758, 242.98231480726633}</string>
+ <string>{163.77841186523429, 277.810302734375}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>StickArrow</string>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>1</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>71</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>TableGroup</string>
+ <key>Graphics</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{33.000005086262888, 130.54334004720062}, {192.55682373046875, 14}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>69</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc
+
+\f0\b\fs24 \cf0 HttpServer}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{33.000005086262888, 144.54334004720062}, {192.55682373046875, 42}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>70</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 Channel serverChannel\
+Executor channelWorkerExecutor\
+HttpServerConfig config}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{33.000005086262888, 186.54334004720062}, {192.55682373046875, 56}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FitText</key>
+ <string>Vertical</string>
+ <key>Flow</key>
+ <string>Resize</string>
+ <key>ID</key>
+ <integer>71</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>GradientCenter</key>
+ <string>{-0.29411799999999999, -0.264706}</string>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>0</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf470
+{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
+
+\f0\fs24 \cf0 start()\
+close()\
+destroy()\
+getPipeline()}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GridH</key>
+ <array>
+ <integer>69</integer>
+ <integer>70</integer>
+ <integer>71</integer>
+ <array/>
+ </array>
+ <key>ID</key>
+ <integer>68</integer>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict/>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>1</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Layer 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2012-06-19 09:20:43 +0000</string>
+ <key>Modifier</key>
+ <string>Einar Rosenvinge</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{594.99999713897705, 842}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Canvas 1</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array>
+ <dict>
+ <key>name</key>
+ <string>Canvas 1</string>
+ </dict>
+ </array>
+ <key>Frame</key>
+ <string>{{246, 375}, {1064, 803}}</string>
+ <key>ListView</key>
+ <true/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <false/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{-25, 0}, {610, 442.66666666666669}}</string>
+ <key>Zoom</key>
+ <real>1.5</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Canvas 1</string>
+ <real>1.5</real>
+ <real>0.25</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/jdisc_http_service/docs/class-diagram.png b/jdisc_http_service/docs/class-diagram.png
new file mode 100644
index 00000000000..ebccfd75bf9
--- /dev/null
+++ b/jdisc_http_service/docs/class-diagram.png
Binary files differ
diff --git a/jdisc_http_service/docs/httpserver.html b/jdisc_http_service/docs/httpserver.html
new file mode 100644
index 00000000000..66afeb687fe
--- /dev/null
+++ b/jdisc_http_service/docs/httpserver.html
@@ -0,0 +1,96 @@
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+ <title>HTTP Server Architecture</title>
+ <style type="text/css">
+ body {
+ font: 13px/1.231 arial,helvetica,clean,sans-serif;
+ *font-size: small;
+ *font: x-small;
+ }
+ select,input,button,textarea {
+ font: 99% arial,helvetica,clean,sans-serif;
+ }
+ table{
+ font-size: inherit;
+ font: 100%;
+ }
+ pre,code,kbd,samp,tt {
+ font-family: monospace;
+ *font-size: 108%;
+ line-height: 100%;
+ }
+ </style>
+</head>
+<body>
+<p>The HTTP server is started by calling <code>HttpServer.start()</code>, which in turn calls <code>ServerBootstrap.bind()</code>
+ provided by
+ Netty.</p>
+
+<img src="class-diagram.png" alt="Class diagram">
+
+<p>Since our HttpServer implements <code>ChannelPipelineFactory</code> (provided by jetty), its
+ <code>getPipeline()</code> method is called for every new channel that is connected. There is hence a one-to-many
+ relationship between a <code>HttpServer</code> and a pipeline (and a one-to-one relationship between an actual
+ channel and a pipeline).</p>
+
+<p>The pipeline is responsible for decoding (and possibly deflating etc.) every new request that is received on a
+ channel. The final element in the pipeline is a <code>ChannelContext</code>, which is the jDISC class for handling
+ requests on a channel.</p>
+
+<p>The <code>ChannelContext</code> implements <code>SimpleChannelUpstreamHandler</code> (provided by Jetty), which has
+ simple callback methods for various event types. <br/>Examples:</p>
+
+<ul>
+ <li><code>channelConnected()</code></li>
+ <li><code>channelDisconnected()</code></li>
+ <li><code>messageReceived()</code></li>
+</ul>
+
+<p>Since <code>ChannelContext</code> supports HTTP keep-alive and HTTP pipelining, it needs to keep track of multiple
+ requests made on the channel, and their order.</p>
+
+<p>In <code>messageReceived()</code> it will:</p>
+<ul>
+ <li>Determine if the element received is a new HTTP request, or a chunk belonging to the previous one.</li>
+ <li>If it's a request, create a DISC <code>Request</code> object for it, and call <code>Request.connect()</code>,
+ which will in turn give it to the actual application, through the use of
+ <code>RequestHandler.handleRequest()</code>.
+ </li>
+ <li>If it's a chunk, fetch the previously added <code>RequestContext</code>, and use it to write the data received
+ into the <code>ContentChannel</code>.
+ </li>
+</ul>
+
+<p><code>RequestContext</code> keeps track of a request and its input and output <code>ContentChannel</code>s, and
+ related objects. Since <code>RequestContext</code> is a <code>ResponseHandler</code>, it is responsible for
+ instantiating and returning a <code>ContentChannel</code> when an application calls <code>handleResponse()</code>.
+ Two types are supported, one that supports HTTP response chunking, and one that does not. The type used is chosen
+ automatically based on HTTP version, headers etc.</p>
+
+<p>Since the jDISC API is fully asynchronous, operations can occur in any order. This is very extensively tested in the
+ HTTP server implementation. For instance, an application (<code>RequestHandler</code>) may choose to respond and
+ close the output <code>ContentChannel</code> immediately upon receiving the request, before the body of the request
+ has been written into the input <code>ContentChannel</code> of the <code>RequestHandler</code>. All such cases are
+ tested and properly handled.</p>
+
+<p>As one can see from the illustration, <code>ChannelContext</code> is also a <code>Runnable</code>, i.e. it keeps one
+ thread per channel. The HTTP server has two modes of operation, <code>optimizeForPipeline</code> <code>true</code>
+ or <code>false</code> in <code>HttpServerConfig</code>.</p>
+
+<p>If <code>optimizeForPipeline</code> is set to <code>true</code>, response chunks are enqueued on a blocking queue in
+ <code>ChannelContext</code> when <code>ContentChannel.write()</code> is called. The <code>ChannelContext</code>
+ thread is responsible for actually writing them, and closing the channel when appropriate. Since the HTTP server
+ supports pipelining, and writes from an application may occur in any order, special care is taken to write response
+ chunks in the correct order.</p>
+
+<p>If <code>optimizeForPipeline</code> is set to <code>false</code>, a call to <code>ContentChannel.write()</code> will
+ lead to an actual write on the wire, iff. the given chunk to be written is the next in line. Otherwise this is a
+ no-op. This also means that a <code>ContentChannel.write()</code> may lead to a cascade of writes that have been
+ enqueued since they were out-of-order when their <code>write()</code> was called. The <code>ChannelContext</code>
+ thread still takes care of channel closing in most cases.</p>
+</body>
+</html>
diff --git a/jdisc_http_service/pom.xml b/jdisc_http_service/pom.xml
new file mode 100644
index 00000000000..15518d20c62
--- /dev/null
+++ b/jdisc_http_service/pom.xml
@@ -0,0 +1,210 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>jdisc_http_service</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>container-plugin</packaging>
+ <name>${project.artifactId}</name>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>provided</scope>
+ <classifier>no_aop</classifier>
+ </dependency>
+ <dependency>
+ <groupId>com.ning</groupId>
+ <artifactId>async-http-client</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>commons-pool</groupId>
+ <artifactId>commons-pool</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpmime</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.cthul</groupId>
+ <artifactId>cthul-matchers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>jdisc_jetty</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.grizzly</groupId>
+ <artifactId>grizzly-websockets</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testng</groupId>
+ <artifactId>testng</artifactId>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-lib</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>defaults</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>jdisc_core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-accesslogging</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <version>13.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <testNGArtifactName>org.testng:testng</testNGArtifactName>
+ <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile>
+ </configuration>
+ <executions>
+ <execution>
+ <id>default-test</id>
+ <phase>test</phase>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <testNGArtifactName>org.testng:testng</testNGArtifactName>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <compilerArgs>
+ <arg>-Xlint:rawtypes</arg>
+ <arg>-Xlint:unchecked</arg>
+ <arg>-Xlint:deprecation</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <discPreInstallBundle>
+ asm-debug-all-${asm-debug-all.version}.jar,
+ javax.servlet-api-3.1.0.jar,
+ jetty-continuation-${jetty.version}.jar,
+ jetty-http-${jetty.version}.jar,
+ jetty-io-${jetty.version}.jar,
+ jetty-security-${jetty.version}.jar,
+ jetty-server-${jetty.version}.jar,
+ jetty-servlet-${jetty.version}.jar,
+ jetty-servlets-${jetty.version}.jar,
+ jetty-util-${jetty.version}.jar,
+ org.apache.aries.spifly.dynamic.bundle-${aries.spifly.version}.jar,
+ org.apache.aries.util-${aries.util.version}.jar,
+ websocket-api-${jetty.version}.jar,
+ websocket-client-${jetty.version}.jar,
+ websocket-common-${jetty.version}.jar,
+ websocket-server-${jetty.version}.jar,
+ websocket-servlet-${jetty.version}.jar,
+ component-jar-with-dependencies.jar
+ </discPreInstallBundle>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java
new file mode 100644
index 00000000000..156215cf22b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+/**
+ * A store of certificates. An implementation can be plugged in to provide certificates to components who use it.
+ *
+ * @author bratseth
+ */
+public interface CertificateStore {
+
+ /** Returns a certificate for a given appid, using the default TTL and retry time */
+ default String getCertificate(String appid) { return getCertificate(appid, 0L, 0L); }
+
+ /** Returns a certificate for a given appid, using a TTL and default retry time */
+ default String getCertificate(String appid, long ttl) { return getCertificate(appid, ttl, 0L); }
+
+ /**
+ * Returns a certificate for a given appid, using a TTL and default retry time
+ *
+ * @param ttl certificate TTL in ms. Use the default TTL if set to 0
+ * @param retry if no certificate is found, allow access to cert DB again in
+ * "retry" ms. Use the default retry time if set to 0.
+ */
+ String getCertificate(String appid, long ttl, long retry);
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java
new file mode 100644
index 00000000000..874bf35021b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java
@@ -0,0 +1,297 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import org.jboss.netty.handler.codec.http.CookieDecoder;
+import org.jboss.netty.handler.codec.http.CookieEncoder;
+import org.jboss.netty.handler.codec.http.DefaultCookie;
+
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class Cookie {
+
+ private final Set<Integer> ports = new HashSet<>();
+ private String name;
+ private String value;
+ private String domain;
+ private String path;
+ private String comment;
+ private String commentUrl;
+ private long maxAgeMillis = TimeUnit.SECONDS.toMillis(Integer.MIN_VALUE);
+ private int version;
+ private boolean secure;
+ private boolean httpOnly;
+ private boolean discard;
+
+ public Cookie() {
+ }
+
+ public Cookie(Cookie cookie) {
+ ports.addAll(cookie.ports);
+ name = cookie.name;
+ value = cookie.value;
+ domain = cookie.domain;
+ path = cookie.path;
+ comment = cookie.comment;
+ commentUrl = cookie.commentUrl;
+ maxAgeMillis = cookie.maxAgeMillis;
+ version = cookie.version;
+ secure = cookie.secure;
+ httpOnly = cookie.httpOnly;
+ discard = cookie.discard;
+ }
+
+ public Cookie(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Cookie setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Cookie setValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public Cookie setDomain(String domain) {
+ this.domain = domain;
+ return this;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public Cookie setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public Cookie setComment(String comment) {
+ this.comment = comment;
+ return this;
+ }
+
+ public String getCommentURL() {
+ return getCommentUrl();
+ }
+
+ public Cookie setCommentURL(String commentUrl) {
+ return setCommentUrl(commentUrl);
+ }
+
+ public String getCommentUrl() {
+ return commentUrl;
+ }
+
+ public Cookie setCommentUrl(String commentUrl) {
+ this.commentUrl = commentUrl;
+ return this;
+ }
+
+ public int getMaxAge(TimeUnit unit) {
+ return (int)unit.convert(maxAgeMillis, TimeUnit.MILLISECONDS);
+ }
+
+ public Cookie setMaxAge(int maxAge, TimeUnit unit) {
+ this.maxAgeMillis = unit.toMillis(maxAge);
+ return this;
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public Cookie setVersion(int version) {
+ this.version = version;
+ return this;
+ }
+
+ public boolean isSecure() {
+ return secure;
+ }
+
+ public Cookie setSecure(boolean secure) {
+ this.secure = secure;
+ return this;
+ }
+
+ public boolean isHttpOnly() {
+ return httpOnly;
+ }
+
+ public Cookie setHttpOnly(boolean httpOnly) {
+ this.httpOnly = httpOnly;
+ return this;
+ }
+
+ public boolean isDiscard() {
+ return discard;
+ }
+
+ public Cookie setDiscard(boolean discard) {
+ this.discard = discard;
+ return this;
+ }
+
+ public Set<Integer> ports() {
+ return ports;
+ }
+
+ @Override
+ public int hashCode() {
+ return ports.hashCode() + hashCode(name) + hashCode(value) + hashCode(domain) + hashCode(path) +
+ hashCode(comment) + hashCode(commentUrl) + Long.valueOf(maxAgeMillis).hashCode() +
+ Integer.valueOf(version).hashCode() + Boolean.valueOf(secure).hashCode() +
+ Boolean.valueOf(httpOnly).hashCode() + Boolean.valueOf(discard).hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Cookie)) {
+ return false;
+ }
+ Cookie rhs = (Cookie)obj;
+ if (!ports.equals(rhs.ports)) {
+ return false;
+ }
+ if (!equals(name, rhs.name)) {
+ return false;
+ }
+ if (!equals(value, rhs.value)) {
+ return false;
+ }
+ if (!equals(domain, rhs.domain)) {
+ return false;
+ }
+ if (!equals(path, rhs.path)) {
+ return false;
+ }
+ if (!equals(comment, rhs.comment)) {
+ return false;
+ }
+ if (!equals(commentUrl, rhs.commentUrl)) {
+ return false;
+ }
+ if (maxAgeMillis != rhs.maxAgeMillis) {
+ return false;
+ }
+ if (version != rhs.version) {
+ return false;
+ }
+ if (secure != rhs.secure) {
+ return false;
+ }
+ if (httpOnly != rhs.httpOnly) {
+ return false;
+ }
+ if (discard != rhs.discard) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder ret = new StringBuilder();
+ ret.append(name).append("=").append(value);
+ return ret.toString();
+ }
+
+ public static String toCookieHeader(Iterable<? extends Cookie> cookies) {
+ return encodeCookies(cookies, false);
+ }
+
+ public static List<Cookie> fromCookieHeader(String headerVal) {
+ return decodeCookies(headerVal);
+ }
+
+ public static String toSetCookieHeader(Iterable<? extends Cookie> cookies) {
+ return encodeCookies(cookies, true);
+ }
+
+ public static List<Cookie> fromSetCookieHeader(String headerVal) {
+ return decodeCookies(headerVal);
+ }
+
+ private static String encodeCookies(Iterable<? extends Cookie> cookies, boolean server) {
+ CookieEncoder encoder = new org.jboss.netty.handler.codec.http.CookieEncoder(server);
+ for (Cookie cookie : cookies) {
+ org.jboss.netty.handler.codec.http.Cookie nettyCookie =
+ new DefaultCookie(String.valueOf(cookie.getName()), String.valueOf(cookie.getValue()));
+ nettyCookie.setComment(cookie.getComment());
+ nettyCookie.setCommentUrl(cookie.getCommentUrl());
+ nettyCookie.setDiscard(cookie.isDiscard());
+ nettyCookie.setDomain(cookie.getDomain());
+ nettyCookie.setHttpOnly(cookie.isHttpOnly());
+ nettyCookie.setMaxAge(cookie.getMaxAge(TimeUnit.SECONDS));
+ nettyCookie.setPath(cookie.getPath());
+ nettyCookie.setSecure(cookie.isSecure());
+ nettyCookie.setVersion(cookie.getVersion());
+ nettyCookie.setPorts(cookie.ports());
+ encoder.addCookie(nettyCookie);
+ }
+ return encoder.encode();
+ }
+
+ private static List<Cookie> decodeCookies(String str) {
+ CookieDecoder decoder = new CookieDecoder();
+ List<Cookie> ret = new LinkedList<>();
+ for (org.jboss.netty.handler.codec.http.Cookie nettyCookie : decoder.decode(str)) {
+ Cookie cookie = new Cookie();
+ cookie.setName(nettyCookie.getName());
+ cookie.setValue(nettyCookie.getValue());
+ cookie.setComment(nettyCookie.getComment());
+ cookie.setCommentUrl(nettyCookie.getCommentUrl());
+ cookie.setDiscard(nettyCookie.isDiscard());
+ cookie.setDomain(nettyCookie.getDomain());
+ cookie.setHttpOnly(nettyCookie.isHttpOnly());
+ cookie.setMaxAge(nettyCookie.getMaxAge(), TimeUnit.SECONDS);
+ cookie.setPath(nettyCookie.getPath());
+ cookie.setSecure(nettyCookie.isSecure());
+ cookie.setVersion(nettyCookie.getVersion());
+ cookie.ports().addAll(nettyCookie.getPorts());
+ ret.add(cookie);
+ }
+ return ret;
+ }
+
+ private static int hashCode(Object obj) {
+ if (obj == null) {
+ return 0;
+ }
+ return obj.hashCode();
+ }
+
+ private static boolean equals(Object lhs, Object rhs) {
+ if (lhs == null || rhs == null) {
+ return lhs == rhs;
+ }
+ return lhs.equals(rhs);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java
new file mode 100644
index 00000000000..0cc13394f99
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+/**
+ * @author <a href="mailto:anirudha@yahoo-inc.com">Anirudha Khanna</a>
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class HttpHeaders {
+
+ public static final class Names {
+
+ public static final String ACCEPT = "Accept";
+ public static final String ACCEPT_CHARSET = "Accept-Charset";
+ public static final String ACCEPT_ENCODING = "Accept-Encoding";
+ public static final String ACCEPT_LANGUAGE = "Accept-Language";
+ public static final String ACCEPT_RANGES = "Accept-Ranges";
+ public static final String ACCEPT_PATCH = "Accept-Patch";
+ public static final String AGE = "Age";
+ public static final String ALLOW = "Allow";
+ public static final String AUTHORIZATION = "Authorization";
+ public static final String CACHE_CONTROL = "Cache-Control";
+ public static final String CONNECTION = "Connection";
+ public static final String CONTENT_BASE = "Content-Base";
+ public static final String CONTENT_ENCODING = "Content-Encoding";
+ public static final String CONTENT_LANGUAGE = "Content-Language";
+ public static final String CONTENT_LENGTH = "Content-Length";
+ public static final String CONTENT_LOCATION = "Content-Location";
+ public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String CONTENT_MD5 = "Content-MD5";
+ public static final String CONTENT_RANGE = "Content-Range";
+ public static final String CONTENT_TYPE = "Content-Type";
+ public static final String COOKIE = "Cookie";
+ public static final String DATE = "Date";
+ public static final String ETAG = "ETag";
+ public static final String EXPECT = "Expect";
+ public static final String EXPIRES = "Expires";
+ public static final String FROM = "From";
+ public static final String HOST = "Host";
+ public static final String IF_MATCH = "If-Match";
+ public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
+ public static final String IF_NONE_MATCH = "If-None-Match";
+ public static final String IF_RANGE = "If-Range";
+ public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
+ public static final String LAST_MODIFIED = "Last-Modified";
+ public static final String LOCATION = "Location";
+ public static final String MAX_FORWARDS = "Max-Forwards";
+ public static final String ORIGIN = "Origin";
+ public static final String PRAGMA = "Pragma";
+ public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
+ public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
+ public static final String RANGE = "Range";
+ public static final String REFERER = "Referer";
+ public static final String RETRY_AFTER = "Retry-After";
+ public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1";
+ public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2";
+ public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location";
+ public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin";
+ public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
+ public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
+ public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
+ public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept";
+ public static final String SERVER = "Server";
+ public static final String SET_COOKIE = "Set-Cookie";
+ public static final String SET_COOKIE2 = "Set-Cookie2";
+ public static final String TE = "TE";
+ public static final String TRAILER = "Trailer";
+ public static final String TRANSFER_ENCODING = "Transfer-Encoding";
+ public static final String UPGRADE = "Upgrade";
+ public static final String USER_AGENT = "User-Agent";
+ public static final String VARY = "Vary";
+ public static final String VIA = "Via";
+ public static final String WARNING = "Warning";
+ public static final String WEBSOCKET_LOCATION = "WebSocket-Location";
+ public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin";
+ public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol";
+ public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ public static final String X_DISABLE_CHUNKING = "X-JDisc-Disable-Chunking";
+ public static final String X_ENABLE_TRACE_ID = "X-JDisc-Enable-TraceId";
+ public static final String X_TRACE_ID = "X-JDisc-TraceId";
+ public static final String X_YAHOO_SERVING_HOST = "X-Yahoo-Serving-Host";
+
+ private Names() {
+ // hide
+ }
+ }
+
+ public static final class Values {
+
+ public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
+ public static final String BASE64 = "base64";
+ public static final String BINARY = "binary";
+ public static final String BYTES = "bytes";
+ public static final String CHARSET = "charset";
+ public static final String CHUNKED = "chunked";
+ public static final String CLOSE = "close";
+ public static final String COMPRESS = "compress";
+ public static final String CONTINUE = "100-continue";
+ public static final String DEFLATE = "deflate";
+ public static final String GZIP = "gzip";
+ public static final String IDENTITY = "identity";
+ public static final String KEEP_ALIVE = "keep-alive";
+ public static final String MAX_AGE = "max-age";
+ public static final String MAX_STALE = "max-stale";
+ public static final String MIN_FRESH = "min-fresh";
+ public static final String MUST_REVALIDATE = "must-revalidate";
+ public static final String NO_CACHE = "no-cache";
+ public static final String NO_STORE = "no-store";
+ public static final String NO_TRANSFORM = "no-transform";
+ public static final String NONE = "none";
+ public static final String ONLY_IF_CACHED = "only-if-cached";
+ public static final String PRIVATE = "private";
+ public static final String PROXY_REVALIDATE = "proxy-revalidate";
+ public static final String PUBLIC = "public";
+ public static final String QUOTED_PRINTABLE = "quoted-printable";
+ public static final String S_MAXAGE = "s-maxage";
+ public static final String TRAILERS = "trailers";
+ public static final String UPGRADE = "Upgrade";
+ public static final String WEBSOCKET = "WebSocket";
+
+ private Values() {
+ // hide
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java
new file mode 100644
index 00000000000..580f83ca5a8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java
@@ -0,0 +1,316 @@
+// Copyright 2016 Yahoo Inc. 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.HeaderFields;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.jboss.netty.handler.codec.http.QueryStringDecoder;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A HTTP request.
+ *
+ * @author <a href="mailto:anirudha@yahoo-inc.com">Anirudha Khanna</a>
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class HttpRequest extends Request implements ServletOrJdiscHttpRequest {
+
+ public enum Method {
+ OPTIONS,
+ GET,
+ HEAD,
+ POST,
+ PUT,
+ PATCH,
+ DELETE,
+ TRACE,
+ CONNECT
+ }
+
+ public enum Version {
+ HTTP_1_0("HTTP/1.0"),
+ HTTP_1_1("HTTP/1.1");
+
+ private final String str;
+
+ private Version(String str) {
+ this.str = str;
+ }
+
+ @Override
+ public String toString() {
+ return str;
+ }
+
+ public static Version fromString(String str) {
+ for (Version version : values()) {
+ if (version.str.equals(str)) {
+ return version;
+ }
+ }
+ throw new IllegalArgumentException(str);
+ }
+ }
+
+ private final HeaderFields trailers = new HeaderFields();
+ private final Map<String, List<String>> parameters = new HashMap<>();
+ private final long connectedAt;
+ private Method method;
+ private Version version;
+ private SocketAddress remoteAddress;
+ private URI proxyServer;
+ private Long connectionTimeout;
+
+ protected HttpRequest(CurrentContainer container, URI uri, Method method, Version version,
+ SocketAddress remoteAddress, Long connectedAtMillis)
+ {
+ super(container, uri);
+ try {
+ this.method = method;
+ this.version = version;
+ this.remoteAddress = remoteAddress;
+ this.parameters.putAll(new QueryStringDecoder(uri.toString(), true).getParameters());
+ if (connectedAtMillis != null) {
+ this.connectedAt = connectedAtMillis;
+ } else {
+ this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
+ }
+ } catch (RuntimeException e) {
+ release();
+ throw e;
+ }
+ }
+
+ private HttpRequest(Request parent, URI uri, Method method, Version version) {
+ super(parent, uri);
+ try {
+ this.method = method;
+ this.version = version;
+ this.remoteAddress = null;
+ this.parameters.putAll(new QueryStringDecoder(uri.toString(), true).getParameters());
+ this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
+ } catch (RuntimeException e) {
+ release();
+ throw e;
+ }
+ }
+
+ public Method getMethod() {
+ return method;
+ }
+
+ public void setMethod(Method method) {
+ this.method = method;
+ }
+
+ public Version getVersion() {
+ return version;
+ }
+
+ @Override
+ public String getRemoteHostAddress() {
+ if (remoteAddress instanceof InetSocketAddress)
+ return ((InetSocketAddress) remoteAddress).getAddress().getHostAddress();
+ else
+ throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
+ }
+
+ @Override
+ public String getRemoteHostName() {
+ if (remoteAddress instanceof InetSocketAddress) {
+ InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress();
+ if (remoteInetAddress == null) return null; // not resolved; we have no network
+ return remoteInetAddress.getHostName();
+ }
+ else {
+ throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
+ }
+ }
+
+ @Override
+ public int getRemotePort() {
+ if (remoteAddress instanceof InetSocketAddress)
+ return ((InetSocketAddress) remoteAddress).getPort();
+ else
+ throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
+ }
+
+ public void setVersion(Version version) {
+ this.version = version;
+ }
+
+ public SocketAddress getRemoteAddress() {
+ return remoteAddress;
+ }
+
+ public void setRemoteAddress(SocketAddress remoteAddress) {
+ this.remoteAddress = remoteAddress;
+ }
+
+ public URI getProxyServer() {
+ return proxyServer;
+ }
+
+ public void setProxyServer(URI proxyServer) {
+ this.proxyServer = proxyServer;
+ }
+
+ /**
+ * <p>For server requests, this returns the timestamp of when the underlying HTTP channel was connected.
+ * This is whatever value was returned by {@link
+ * com.yahoo.jdisc.Timer#currentTimeMillis()} at the time.</p>
+ *
+ * <p>For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.</p>
+ *
+ * @param unit the unit to return the time in
+ * @return the timestamp of when the underlying HTTP channel was connected, or request creation time
+ */
+ public long getConnectedAt(TimeUnit unit) {
+ return unit.convert(connectedAt, TimeUnit.MILLISECONDS);
+ }
+
+ public Long getConnectionTimeout(TimeUnit unit) {
+ if (connectionTimeout == null) {
+ return null;
+ }
+ return unit.convert(connectionTimeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * <p>Sets the allocated time that this HttpRequest is allowed to spend trying to connect to a remote host. This has
+ * no effect on an HttpRequest received by a {@link RequestHandler}. If no connection timeout is assigned to an
+ * HttpRequest, it defaults the connection-timeout in the corresponding {@link
+ * com.yahoo.jdisc.http.client.HttpClientConfig}.</p>
+ *
+ * <p><b>NOTE:</b> Where {@link Request#setTimeout(long, TimeUnit)} sets the expiration time between calling a
+ * RequestHandler and a {@link ResponseHandler}, this method sets the expiration time of the connect-operation as
+ * performed by the {@link com.yahoo.jdisc.http.client.HttpClient}.</p>
+ *
+ * @param timeout The allocated amount of time.
+ * @param unit The time unit of the <em>timeout</em> argument.
+ */
+ public void setConnectionTimeout(long timeout, TimeUnit unit) {
+ this.connectionTimeout = unit.toMillis(timeout);
+ }
+
+ public Map<String, List<String>> parameters() {
+ return parameters;
+ }
+
+ @Override
+ public void copyHeaders(HeaderFields target) {
+ target.addAll(headers());
+ }
+
+ public List<Cookie> decodeCookieHeader() {
+ List<String> cookies = headers().get(HttpHeaders.Names.COOKIE);
+ if (cookies == null) {
+ return Collections.emptyList();
+ }
+ List<Cookie> ret = new LinkedList<>();
+ for (String cookie : cookies) {
+ ret.addAll(Cookie.fromCookieHeader(cookie));
+ }
+ return ret;
+ }
+
+ public void encodeCookieHeader(List<Cookie> cookies) {
+ headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies));
+ }
+
+ /**
+ * <p>Returns the set of trailer header fields of this HttpRequest. These are typically meta-data that should have
+ * been part of {@link #headers()}, but were not available prior to calling {@link #connect(ResponseHandler)}. You
+ * must NOT WRITE to these headers AFTER calling {@link ContentChannel#close(CompletionHandler)}, and you must NOT
+ * READ from these headers BEFORE {@link ContentChannel#close(CompletionHandler)} has been called.</p>
+ *
+ * <p><b>NOTE:</b> These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to
+ * prevent concurrency issues such as ConcurrentModificationExceptions.</p>
+ *
+ * @return The trailer headers of this HttpRequest.
+ */
+ public HeaderFields trailers() {
+ return trailers;
+ }
+
+ /**
+ * Returns whether this request was <em>explicitly</em> chunked from the client.&nbsp;NOTE that there are cases
+ * where the underlying HTTP server library (Netty for the time being) will read the request in a chunked manner. An
+ * application MUST wait for {@link com.yahoo.jdisc.handler.ContentChannel#close(com.yahoo.jdisc.handler.CompletionHandler)}
+ * before it can actually know that it has received the entire request.
+ *
+ * @return true if this request was chunked from the client.
+ */
+ public boolean isChunked() {
+ return version == Version.HTTP_1_1 &&
+ headers().containsIgnoreCase(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
+ }
+
+ public boolean hasChunkedResponse() {
+ return version == Version.HTTP_1_1 &&
+ !headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING);
+ }
+
+ public boolean isKeepAlive() {
+ if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)) {
+ return true;
+ }
+ if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)) {
+ return false;
+ }
+ return version == Version.HTTP_1_1;
+ }
+
+ public static HttpRequest newServerRequest(CurrentContainer container, URI uri) {
+ return newServerRequest(container, uri, Method.GET);
+ }
+
+ public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method) {
+ return newServerRequest(container, uri, method, Version.HTTP_1_1);
+ }
+
+ public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version) {
+ return newServerRequest(container, uri, method, version, null);
+ }
+
+ @SuppressWarnings("deprecation")
+ public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
+ SocketAddress remoteAddress) {
+ return new HttpRequest(container, uri, method, version, remoteAddress, null);
+ }
+
+ public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
+ SocketAddress remoteAddress, long connectedAtMillis)
+ {
+ return new HttpRequest(container, uri, method, version, remoteAddress, connectedAtMillis);
+ }
+
+ public static HttpRequest newClientRequest(Request parent, URI uri) {
+ return newClientRequest(parent, uri, Method.GET);
+ }
+
+ public static HttpRequest newClientRequest(Request parent, URI uri, Method method) {
+ return newClientRequest(parent, uri, method, Version.HTTP_1_1);
+ }
+
+ @SuppressWarnings("deprecation")
+ public static HttpRequest newClientRequest(Request parent, URI uri, Method method, Version version) {
+ return new HttpRequest(parent, uri, method, version);
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java
new file mode 100644
index 00000000000..5cd8dec0af9
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java
@@ -0,0 +1,130 @@
+// Copyright 2016 Yahoo Inc. 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.HeaderFields;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A HTTP response.
+ *
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class HttpResponse extends Response implements ServletOrJdiscHttpResponse {
+
+ private final HeaderFields trailers = new HeaderFields();
+ private final StringBuffer accessLogExtra = new StringBuffer();
+ private boolean chunkedEncodingEnabled = true;
+ private String message;
+ private final Request request;
+
+ public interface Status extends Response.Status {
+
+ int REQUEST_ENTITY_TOO_LARGE = REQUEST_TOO_LONG;
+ int REQUEST_RANGE_NOT_SATISFIABLE = REQUESTED_RANGE_NOT_SATISFIABLE;
+ }
+
+ protected HttpResponse(Request request, int status, String message, Throwable error) {
+ super(status, error);
+ this.message = message;
+ this.request = request;
+ }
+
+ public boolean isChunkedEncodingEnabled() {
+ if (headers().contains(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED)) {
+ return true;
+ }
+ if (headers().containsKey(HttpHeaders.Names.CONTENT_LENGTH)) {
+ return false;
+ }
+ return chunkedEncodingEnabled;
+ }
+
+ public void setChunkedEncodingEnabled(boolean chunkedEncodingEnabled) {
+ this.chunkedEncodingEnabled = chunkedEncodingEnabled;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public void copyHeaders(HeaderFields target) {
+ target.addAll(headers());
+ }
+
+ public List<Cookie> decodeSetCookieHeader() {
+ List<String> cookies = headers().get(HttpHeaders.Names.SET_COOKIE);
+ if (cookies == null) {
+ return Collections.emptyList();
+ }
+ List<Cookie> ret = new LinkedList<>();
+ for (String cookie : cookies) {
+ ret.addAll(Cookie.fromSetCookieHeader(cookie));
+ }
+ return ret;
+ }
+
+ public void encodeSetCookieHeader(List<Cookie> cookies) {
+ headers().remove(HttpHeaders.Names.SET_COOKIE);
+ for (Cookie cookie : cookies) {
+ headers().add(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeader(Arrays.asList(cookie)));
+ }
+ }
+
+ /**
+ * <p>Returns the set of trailer header fields of this HttpResponse. These are typically meta-data that should have
+ * been part of {@link #headers()}, but were not available prior to calling {@link
+ * ResponseHandler#handleResponse(Response)}. You must NOT WRITE to these headers AFTER calling {@link
+ * ContentChannel#close(CompletionHandler)}, and you must NOT READ from these headers BEFORE {@link
+ * ContentChannel#close(CompletionHandler)} has been called.</p>
+ *
+ * <p><b>NOTE:</b> These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to
+ * prevent concurrency issues such as ConcurrentModificationExceptions.</p>
+ *
+ * @return The trailer headers of this HttpRequest.
+ */
+ public HeaderFields trailers() {
+ return trailers;
+ }
+
+ public static boolean isServerError(Response response) {
+ return (response.getStatus() >= 500) && (response.getStatus() < 600);
+ }
+
+ public static HttpResponse newInstance(int status) {
+ return new HttpResponse(null, status, null, null);
+ }
+
+ public static HttpResponse newInstance(int status, String message) {
+ return new HttpResponse(null, status, message, null);
+ }
+
+ public static HttpResponse newError(Request request, int status, Throwable error) {
+ return new HttpResponse(request, status, formatMessage(error), error);
+ }
+
+ public static HttpResponse newInternalServerError(Request request, Throwable error) {
+ return new HttpResponse(request, Status.INTERNAL_SERVER_ERROR, formatMessage(error), error);
+ }
+
+ private static String formatMessage(Throwable t) {
+ String msg = t.getMessage();
+ return msg != null ? msg : t.toString();
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java
new file mode 100644
index 00000000000..a3ef08df486
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+/**
+ * An abstraction of a secret store for e.g passwords.
+ * Implementations can be plugged in to provide passwords for various keys.
+ *
+ * @author bratseth
+ */
+public interface SecretStore {
+
+ /** Returns the secret for this key */
+ String getSecret(String key);
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java
new file mode 100644
index 00000000000..8a22b67b297
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/WebSocketRequest.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.net.SocketAddress;
+import java.net.URI;
+
+/**
+ * Represents a WebSocket request.
+ *
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+@Beta
+public class WebSocketRequest extends HttpRequest {
+
+ @SuppressWarnings("deprecation")
+ protected WebSocketRequest(CurrentContainer current, URI uri, Method method, Version version,
+ SocketAddress remoteAddress) {
+ super(current, uri, method, version, remoteAddress, null);
+ }
+
+ public static WebSocketRequest newServerRequest(CurrentContainer current, URI uri, Method method, Version version,
+ SocketAddress remoteAddress) {
+ return new WebSocketRequest(current, uri, method, version, remoteAddress);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java
new file mode 100644
index 00000000000..19f65633419
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/AsyncResponseHandler.java
@@ -0,0 +1,187 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHandler;
+import com.ning.http.client.FluentCaseInsensitiveStringsMap;
+import com.ning.http.client.HttpResponseBodyPart;
+import com.ning.http.client.HttpResponseHeaders;
+import com.ning.http.client.HttpResponseStatus;
+import com.ning.http.client.Response;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Timer;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpResponse;
+
+import java.net.ConnectException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ * @since 2.0
+ */
+class AsyncResponseHandler implements AsyncHandler<Response> {
+
+ private final CompletionHandler abortionHandler = new AbortionHandler();
+ private final Request request;
+ private final ResponseHandler responseHandler;
+ private final Metric metric;
+ private final Metric.Context metricCtx;
+ private final Timer timer;
+ private int statusCode;
+ private String statusText;
+ private ContentChannel content;
+ private boolean aborted = false;
+ private long requestCreationTime;
+ private long transferStartTime;
+
+ public AsyncResponseHandler(Request request, ResponseHandler responseHandler, Metric metric,
+ Metric.Context metricCtx)
+ {
+ this.request = request;
+ this.responseHandler = responseHandler;
+ this.metric = metric;
+ this.metricCtx = metricCtx;
+ this.timer = request.container().getInstance(Timer.class);
+ metric.add(HttpClient.Metrics.NUM_REQUESTS, 1, metricCtx);
+ this.requestCreationTime = timer.currentTimeMillis();
+ }
+
+ @Override
+ public void onThrowable(Throwable t) {
+ abort(t);
+ }
+
+ @Override
+ public STATE onStatusReceived(HttpResponseStatus status) throws Exception {
+ if (aborted) {
+ return STATE.ABORT;
+ }
+ long latency = timer.currentTimeMillis() - request.creationTime(TimeUnit.MILLISECONDS);
+ metric.set(HttpClient.Metrics.REQUEST_LATENCY, latency, metricCtx);
+ metric.add(HttpClient.Metrics.NUM_RESPONSES, 1, metricCtx);
+ statusCode = status.getStatusCode();
+ statusText = status.getStatusText();
+
+ metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, ((Integer.SIZE)/8) + statusText.getBytes().length, metricCtx); // status code is an integer
+ return STATE.CONTINUE;
+ }
+
+ @Override
+ public STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception {
+ this.transferStartTime = timer.currentTimeMillis();
+
+ if (aborted) {
+ return STATE.ABORT;
+ }
+ HttpResponse response = HttpResponse.newInstance(statusCode, statusText);
+
+ FluentCaseInsensitiveStringsMap headerMap = headers.getHeaders();
+ response.headers().addAll(headerMap);
+ content = responseHandler.handleResponse(response);
+
+ metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, headerMap.size(), metricCtx);
+
+ return STATE.CONTINUE;
+ }
+
+ @Override
+ public STATE onBodyPartReceived(HttpResponseBodyPart part) throws Exception {
+ if (aborted) {
+ return STATE.ABORT;
+ }
+ metric.add(HttpClient.Metrics.NUM_BYTES_RECEIVED, part.getBodyPartBytes().length, metricCtx);
+
+ content.write(part.getBodyByteBuffer(), abortionHandler);
+ return STATE.CONTINUE;
+ }
+
+ @Override
+ public Response onCompleted() throws Exception {
+ long now = timer.currentTimeMillis();
+ metric.set(HttpClient.Metrics.TRANSFER_LATENCY, now - transferStartTime, metricCtx);
+ metric.set(HttpClient.Metrics.TOTAL_LATENCY, now - requestCreationTime, metricCtx);
+
+ if (aborted) {
+ return null;
+ }
+ content.close(abortionHandler);
+ return EmptyResponse.INSTANCE;
+ }
+
+ /**
+ * Returns the original request associated with this handler. Note: It is the caller's responsibility to ensure
+ * that the request is properly retained and released.
+ */
+ public Request getRequest() {
+ return request;
+ }
+
+ private void abort(Throwable t) {
+ if (aborted) {
+ return;
+ }
+ aborted = true;
+ updateErrorMetric(t);
+ if (content == null) {
+ dispatchErrorResponse(t);
+ }
+ if (content != null) {
+ terminateContent();
+ }
+ }
+
+ private void updateErrorMetric(Throwable t) {
+ try {
+ if (t instanceof ConnectException) {
+ metric.add(HttpClient.Metrics.CONNECTION_EXCEPTIONS, 1, metricCtx);
+ } else if (t instanceof TimeoutException) {
+ metric.add(HttpClient.Metrics.TIMEOUT_EXCEPTIONS, 1, metricCtx);
+ } else {
+ metric.add(HttpClient.Metrics.OTHER_EXCEPTIONS, 1, metricCtx);
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private void dispatchErrorResponse(Throwable t) {
+ int status;
+ if (t instanceof ConnectException) {
+ status = com.yahoo.jdisc.Response.Status.SERVICE_UNAVAILABLE;
+ } else if (t instanceof TimeoutException) {
+ status = com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT;
+ } else {
+ status = com.yahoo.jdisc.Response.Status.BAD_REQUEST;
+ }
+ try {
+ content = responseHandler.handleResponse(HttpResponse.newError(request, status, t));
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private void terminateContent() {
+ try {
+ content.close(null);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private class AbortionHandler implements CompletionHandler {
+
+ @Override
+ public void completed() {
+
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ abort(t);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java
new file mode 100644
index 00000000000..4b4b48dd05b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequest.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+final class BufferedRequest {
+
+ private BufferedRequest() {
+ // hide
+ }
+
+ public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method,
+ ResponseHandler handler, Metric metric, Metric.Context ctx)
+ {
+ return new BufferedRequestContent(ningClient, request, method,
+ new AsyncResponseHandler(request, handler, metric, ctx));
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java
new file mode 100644
index 00000000000..c09dcef98f4
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/BufferedRequestContent.java
@@ -0,0 +1,99 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.RequestBuilder;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.core.HeaderFieldsUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ * @since 2.0
+ */
+class BufferedRequestContent implements ContentChannel {
+
+ private final AsyncHttpClient client;
+ private final AsyncResponseHandler handler;
+ private final Request request;
+ private final HttpRequest.Method method;
+ private final List<CompletionHandler> writeCompletions = new LinkedList<>();
+ private final Object contentLock = new Object();
+ private ByteArrayOutputStream content = new ByteArrayOutputStream();
+
+ public BufferedRequestContent(AsyncHttpClient client, Request request, HttpRequest.Method method,
+ AsyncResponseHandler handler) {
+ this.client = client;
+ this.request = request;
+ this.method = method;
+ this.handler = handler;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler writeCompletion) {
+ Objects.requireNonNull(buf, "buf");
+ synchronized (contentLock) {
+ if (content == null) {
+ throw new IllegalStateException("ContentChannel closed.");
+ }
+ for (int i = 0, len = buf.remaining(); i < len; ++i) {
+ content.write(buf.get());
+ }
+ if (writeCompletion != null) {
+ writeCompletions.add(writeCompletion);
+ }
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler closeCompletion) {
+ byte[] content;
+ synchronized (contentLock) {
+ content = this.content.toByteArray();
+ this.content = null;
+ }
+ try {
+ executeRequest(content);
+ for (CompletionHandler writeCompletion : writeCompletions) {
+ writeCompletion.completed();
+ }
+ if (closeCompletion != null) {
+ closeCompletion.completed();
+ }
+ } catch (Exception e) {
+ for (CompletionHandler writeCompletion : writeCompletions) {
+ tryFail(writeCompletion, e);
+ }
+ if (closeCompletion != null) {
+ tryFail(closeCompletion, e);
+ }
+ }
+ }
+
+ private void tryFail(CompletionHandler handler, Throwable t) {
+ try {
+ handler.failed(t);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private void executeRequest(final byte[] body) throws IOException {
+ RequestBuilder builder = RequestBuilderFactory.newInstance(request, method);
+ HeaderFieldsUtil.copyTrailers(request, builder);
+ if (body.length > 0) {
+ builder.setContentLength(body.length);
+ builder.setBody(body);
+ }
+ client.executeRequest(builder.build(), handler);
+ }
+} \ No newline at end of file
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java
new file mode 100644
index 00000000000..6da40f3c443
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequest.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.RequestBuilder;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+final class ChunkedRequest {
+
+ private ChunkedRequest() {
+ // hide
+ }
+
+ public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method,
+ ResponseHandler handler, Metric metric, Metric.Context ctx)
+ {
+ RequestBuilder builder = RequestBuilderFactory.newInstance(request, method);
+ ChunkedRequestContent content = new ChunkedRequestContent(request);
+ builder.setBody(content);
+ try {
+ ningClient.executeRequest(builder.build(), new AsyncResponseHandler(request, handler, metric, ctx));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return content;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java
new file mode 100644
index 00000000000..9142910a91d
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestBody.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.Body;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class ChunkedRequestBody implements Body {
+
+ private final ChunkedRequestContent content;
+ private ByteBuffer currentBuf;
+
+ public ChunkedRequestBody(ChunkedRequestContent content) {
+ this.content = content;
+ }
+
+ @Override
+ public long getContentLength() {
+ return -1; // unknown
+ }
+
+ @Override
+ public long read(ByteBuffer dst) throws IOException {
+ if (content.isEndOfInput()) {
+ return -1;
+ }
+ if (currentBuf == null || currentBuf.remaining() == 0) {
+ currentBuf = content.nextChunk();
+ }
+ if (currentBuf == null) {
+ return 0;
+ }
+ int len = Math.min(currentBuf.remaining(), dst.remaining());
+ for (int i = 0; i < len; ++i) {
+ dst.put(currentBuf.get());
+ }
+ return len;
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java
new file mode 100644
index 00000000000..265315d3eb8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ChunkedRequestContent.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.Body;
+import com.ning.http.client.BodyGenerator;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.http.core.HeaderFieldsUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedList;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class ChunkedRequestContent implements BodyGenerator, ContentChannel {
+
+ private static final byte[] LAST_CHUNK = "0\r\n".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CRLF_BYTES = "\r\n".getBytes(StandardCharsets.UTF_8);
+ private final AtomicReference<ChunkedRequestBody> body = new AtomicReference<>(new ChunkedRequestBody(this));
+ private final AtomicBoolean writerClosed = new AtomicBoolean(false);
+ private final Queue<Entry> writeQueue = new ConcurrentLinkedQueue<>();
+ private final Queue<ByteBuffer> readQueue = new LinkedList<>();
+ private final Request request;
+ private boolean readerClosed = false;
+
+ public ChunkedRequestContent(Request request) {
+ this.request = request;
+ }
+
+ @Override
+ public Body createBody() throws IOException {
+ // this is called by Netty, and presumably has to be thread-safe since Netty assigns thread by connection --
+ // retries are necessarily done using new connections
+ Body body = this.body.getAndSet(null);
+ if (body == null) {
+ throw new UnsupportedOperationException("ChunkedRequestContent does not support retries.");
+ }
+ return body;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ // this can be called by any JDisc thread, and needs to be thread-safe
+ Objects.requireNonNull(buf, "buf");
+ if (writerClosed.get()) {
+ throw new IllegalStateException("ChunkedRequestContent is closed.");
+ }
+ writeQueue.add(new Entry(buf, handler));
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ // this can be called by any JDisc thread, and needs to be thread-safe
+ if (writerClosed.getAndSet(true)) {
+ throw new IllegalStateException("ChunkedRequestContent already closed.");
+ }
+ writeQueue.add(new Entry(null, handler));
+ }
+
+ public ByteBuffer nextChunk() {
+ // this method is only called by the ChunkedRequestBody, which in turns is only called by the thread assigned to
+ // the underlying Netty connection -- it does not need to be thread-safe
+ if (!readQueue.isEmpty()) {
+ ByteBuffer buf = readQueue.poll();
+ if (buf == null) {
+ readerClosed = true;
+ }
+ return buf;
+ }
+ if (writeQueue.isEmpty()) {
+ return null;
+ }
+ Entry entry = writeQueue.poll();
+ try {
+ entry.handler.completed();
+ } catch (Exception e) {
+ // TODO: fail and close write queue
+ // TODO: rethrow e to make ning abort request
+ }
+ if (entry.buf != null) {
+ readQueue.add(ByteBuffer.wrap(Integer.toHexString(entry.buf.remaining()).getBytes(StandardCharsets.UTF_8)));
+ readQueue.add(ByteBuffer.wrap(CRLF_BYTES));
+ readQueue.add(entry.buf);
+ readQueue.add(ByteBuffer.wrap(CRLF_BYTES));
+ } else {
+ readQueue.add(ByteBuffer.wrap(LAST_CHUNK));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HeaderFieldsUtil.copyTrailers(request, out);
+ byte[] buf = out.toByteArray();
+ if (buf.length > 0) {
+ readQueue.add(ByteBuffer.wrap(buf));
+ }
+ readQueue.add(ByteBuffer.wrap(CRLF_BYTES));
+ readQueue.add(null);
+ }
+ return readQueue.poll();
+ }
+
+ public boolean isEndOfInput() {
+ // only called by the assigned Netty thread, does not need to be thread-safe
+ return readerClosed;
+ }
+
+ private static class Entry {
+
+ final ByteBuffer buf;
+ final CompletionHandler handler;
+
+ Entry(ByteBuffer buf, CompletionHandler handler) {
+ this.buf = buf;
+ this.handler = handler;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java
new file mode 100644
index 00000000000..30a3809e30a
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequest.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+final class EmptyRequest {
+
+ private EmptyRequest() {
+ // hide
+ }
+
+ public static ContentChannel executeRequest(AsyncHttpClient ningClient, Request request, HttpRequest.Method method,
+ ResponseHandler handler, Metric metric, Metric.Context ctx) {
+ return new EmptyRequestContent(ningClient, request, method,
+ new AsyncResponseHandler(request, handler, metric, ctx));
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java
new file mode 100644
index 00000000000..fc29bc9da6e
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyRequestContent.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.RequestBuilder;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.core.HeaderFieldsUtil;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class EmptyRequestContent implements ContentChannel {
+
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+ private final AsyncHttpClient client;
+ private final AsyncResponseHandler handler;
+ private final Request request;
+ private final HttpRequest.Method method;
+
+ public EmptyRequestContent(AsyncHttpClient client, Request request, HttpRequest.Method method,
+ AsyncResponseHandler handler) {
+ this.client = client;
+ this.request = request;
+ this.method = method;
+ this.handler = handler;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ throw new UnsupportedOperationException("Request does not support a message-body.");
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (closed.getAndSet(true)) {
+ if (handler != null) {
+ handler.completed();
+ }
+ return;
+ }
+ try {
+ executeRequest();
+ handler.completed();
+ } catch (Exception e) {
+ try {
+ handler.failed(e);
+ } catch (Exception f) {
+ // ignore
+ }
+ }
+ }
+
+ private void executeRequest() throws IOException {
+ RequestBuilder builder = RequestBuilderFactory.newInstance(request, method);
+ HeaderFieldsUtil.copyTrailers(request, builder);
+ client.executeRequest(builder.build(), handler);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java
new file mode 100644
index 00000000000..19e9190a32b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/EmptyResponse.java
@@ -0,0 +1,121 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.Cookie;
+import com.ning.http.client.FluentCaseInsensitiveStringsMap;
+import com.ning.http.client.Response;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ * @since 2.0
+ */
+final class EmptyResponse implements Response {
+
+ public static final EmptyResponse INSTANCE = new EmptyResponse();
+
+ private EmptyResponse() {
+ // hide
+ }
+
+ @Override
+ public int getStatusCode() {
+ return 0;
+ }
+
+ @Override
+ public String getStatusText() {
+ return null;
+ }
+
+ @Override
+ public ByteBuffer getResponseBodyAsByteBuffer() {
+ return ByteBuffer.allocate(0);
+ }
+
+ @Override
+ public byte[] getResponseBodyAsBytes() throws IOException {
+ return new byte[0];
+ }
+
+ @Override
+ public InputStream getResponseBodyAsStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getResponseBodyExcerpt(int maxLength, String charset) throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getResponseBody(String charset) throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getResponseBodyExcerpt(int maxLength) throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getResponseBody() throws IOException {
+ return null;
+ }
+
+ @Override
+ public URI getUri() throws MalformedURLException {
+ return null;
+ }
+
+ @Override
+ public String getContentType() {
+ return null;
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return null;
+ }
+
+ @Override
+ public List<String> getHeaders(String name) {
+ return null;
+ }
+
+ @Override
+ public FluentCaseInsensitiveStringsMap getHeaders() {
+ return null;
+ }
+
+ @Override
+ public boolean isRedirected() {
+ return false;
+ }
+
+ @Override
+ public List<Cookie> getCookies() {
+ return null;
+ }
+
+ @Override
+ public boolean hasResponseStatus() {
+ return false;
+ }
+
+ @Override
+ public boolean hasResponseHeaders() {
+ return false;
+ }
+
+ @Override
+ public boolean hasResponseBody() {
+ return false;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java
new file mode 100644
index 00000000000..495fd303ad2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/HttpClient.java
@@ -0,0 +1,264 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.google.inject.Inject;
+import com.ning.http.client.AsyncHandler;
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.AsyncHttpClientConfig;
+import com.ning.http.client.filter.FilterContext;
+import com.ning.http.client.filter.FilterException;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.SecretStore;
+import com.yahoo.jdisc.http.client.filter.ResponseFilter;
+import com.yahoo.jdisc.http.client.filter.core.ResponseFilterBridge;
+import com.yahoo.jdisc.http.ssl.JKSKeyStore;
+import com.yahoo.jdisc.http.ssl.SslContextFactory;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+import com.yahoo.jdisc.service.AbstractClientProvider;
+import com.yahoo.vespa.defaults.Defaults;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HttpClient extends AbstractClientProvider {
+
+ public interface Metrics {
+
+ String NUM_REQUESTS = "clientRequests";
+ String NUM_RESPONSES = "clientResponses";
+ String REQUEST_LATENCY = "clientRequestLatency";
+ String CONNECTION_EXCEPTIONS = "clientConnectExceptions";
+ String TIMEOUT_EXCEPTIONS = "clientTimeoutExceptions";
+ String OTHER_EXCEPTIONS = "clientOtherExceptions";
+ String NUM_BYTES_RECEIVED = "ClientBytesReceived";
+ String NUM_BYTES_SENT = "ClientBytesSent";
+ String TOTAL_LATENCY = "ClientTotalResponseLatency";
+ String TRANSFER_LATENCY = "ClientDataTransferLatency";
+ }
+
+ private static final String WEBSOCKET = "ws";
+ private static final String HTTP = "http";
+ private static final String HTTPS = "https";
+
+ private final ConcurrentMap<String, Metric.Context> metricCtx = new ConcurrentHashMap<>();
+ private final Object metricCtxLock = new Object();
+ private final AsyncHttpClient ningClient;
+ private final Metric metric;
+ private final boolean chunkedEncodingEnabled;
+
+ protected HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric,
+ HostnameVerifier hostnameVerifier, SSLContext sslContext,
+ List<ResponseFilter> responseFilters) {
+ this.ningClient = newNingClient(config, threadFactory, hostnameVerifier, sslContext, responseFilters);
+ this.metric = metric;
+ this.chunkedEncodingEnabled = config.chunkedEncodingEnabled();
+ }
+
+ /** Create a client which cannot look up secrets for use in requests */
+ public HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric,
+ HostnameVerifier hostnameVerifier, List<ResponseFilter> responseFilters) {
+ this(config, threadFactory, metric, hostnameVerifier, resolveSslContext(config.ssl(), new ThrowingSecretStore()), responseFilters);
+ }
+
+ @Inject
+ public HttpClient(HttpClientConfig config, ThreadFactory threadFactory, Metric metric,
+ HostnameVerifier hostnameVerifier, List<ResponseFilter> responseFilters, SecretStore secretStore) {
+ this(config, threadFactory, metric, hostnameVerifier, resolveSslContext(config.ssl(), secretStore), responseFilters);
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ Metric.Context ctx = newMetricContext(request.getUri());
+ String uriScheme = request.getUri().getScheme();
+
+ switch (uriScheme) {
+ case WEBSOCKET:
+ return WebSocketClientRequest.executeRequest(ningClient, request, handler, metric, ctx);
+ case HTTP:
+ case HTTPS:
+ HttpRequest.Method method = resolveMethod(request);
+ metric.add(Metrics.NUM_BYTES_SENT, request.headers().size(), ctx);
+
+ if (!hasMessageBody(method)) {
+ return EmptyRequest.executeRequest(ningClient, request, method, handler, metric, ctx);
+ }
+ if (isChunkedEncodingEnabled(request, method)) {
+ return ChunkedRequest.executeRequest(ningClient, request, method, handler, metric, ctx);
+ }
+ return BufferedRequest.executeRequest(ningClient, request, method, handler, metric, ctx);
+ default:
+ throw new UnsupportedOperationException("Unknown protocol: " + uriScheme);
+ }
+ }
+
+ @Override
+ protected void destroy() {
+ ningClient.close();
+ }
+
+ private HttpRequest.Method resolveMethod(Request request) {
+ if (request instanceof HttpRequest) {
+ return ((HttpRequest)request).getMethod();
+ }
+ return HttpRequest.Method.POST;
+ }
+
+ private boolean hasMessageBody(HttpRequest.Method method) {
+ return method != HttpRequest.Method.TRACE;
+ }
+
+ private boolean isChunkedEncodingEnabled(Request request, HttpRequest.Method method) {
+ if (!chunkedEncodingEnabled) {
+ return false;
+ }
+ if (method == HttpRequest.Method.GET || method == HttpRequest.Method.HEAD) {
+ return false;
+ }
+ if (request.headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING)) {
+ return false;
+ }
+ if (request.headers().containsKey(HttpHeaders.Names.CONTENT_LENGTH)) {
+ return false;
+ }
+ if (request instanceof HttpRequest && ((HttpRequest)request).getVersion() == HttpRequest.Version.HTTP_1_0) {
+ return false;
+ }
+ return true;
+ }
+
+ private Metric.Context newMetricContext(URI uri) {
+ String key = uri.getScheme() + "://" + uri.getHost() + (uri.getPort() != -1 ? ":" + uri.getPort() : "");
+ Metric.Context ctx = metricCtx.get(key);
+ if (ctx == null) {
+ synchronized (metricCtxLock) {
+ ctx = metricCtx.get(key);
+ if (ctx == null) {
+ Map<String, Object> props = new HashMap<>();
+ props.put("requestUri", key);
+
+ ctx = metric.createContext(props);
+ if (ctx == null) {
+ ctx = NullContext.INSTANCE;
+ }
+ metricCtx.put(key, ctx);
+ }
+ }
+ }
+ if (ctx == NullContext.INSTANCE) {
+ return null;
+ }
+ return ctx;
+ }
+
+ private static SSLContext resolveSslContext(HttpClientConfig.Ssl config, SecretStore secretStore) {
+ if (!config.enabled()) {
+ return null;
+ }
+ SslKeyStore keyStore = new JKSKeyStore(Paths.get(Defaults.getDefaults().underVespaHome(config.keyStorePath())));
+ SslKeyStore trustStore = new JKSKeyStore(Paths.get(Defaults.getDefaults().underVespaHome(config.trustStorePath())));
+
+ String password = secretStore.getSecret(config.keyDBKey());
+ keyStore.setKeyStorePassword(password);
+ trustStore.setKeyStorePassword(password);
+ SslContextFactory sslContextFactory = SslContextFactory.newInstance(
+ config.algorithm(),
+ config.protocol(),
+ keyStore,
+ trustStore);
+ return sslContextFactory.getServerSSLContext();
+ }
+
+
+ @SuppressWarnings("deprecation")
+ private static AsyncHttpClient newNingClient(HttpClientConfig config, ThreadFactory threadFactory,
+ HostnameVerifier hostnameVerifier, SSLContext sslContext,
+ List<ResponseFilter> responseFilters) {
+ AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder();
+ builder.setAllowPoolingConnection(config.connectionPoolEnabled());
+ builder.setAllowSslConnectionPool(config.sslConnectionPoolEnabled());
+ builder.setCompressionEnabled(config.compressionEnabled());
+ builder.setConnectionTimeoutInMs((int)(config.connectionTimeout() * 1000));
+ builder.setExecutorService(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2,
+ threadFactory));
+ builder.setFollowRedirects(config.followRedirects());
+ builder.setHostnameVerifier(hostnameVerifier);
+ builder.setIOThreadMultiplier(2);
+ builder.setIdleConnectionInPoolTimeoutInMs((int)(config.idleConnectionInPoolTimeout() * 1000));
+ builder.setIdleConnectionTimeoutInMs((int)(config.idleConnectionTimeout() * 1000));
+ builder.setMaxRequestRetry(config.chunkedEncodingEnabled() ? 0 : config.maxNumRetries());
+ builder.setMaximumConnectionsPerHost(config.maxNumConnectionsPerHost());
+ builder.setMaximumConnectionsTotal(config.maxNumConnections());
+ builder.setMaximumNumberOfRedirects(config.maxNumRedirects());
+ if (!config.proxyServer().isEmpty()) {
+ builder.setProxyServer(ProxyServerFactory.newInstance(URI.create(config.proxyServer())));
+ }
+ builder.setRemoveQueryParamsOnRedirect(config.removeQueryParamsOnRedirect());
+ builder.setRequestCompressionLevel(config.compressionLevel());
+ builder.setRequestTimeoutInMs((int)(config.requestTimeout() * 1000));
+ builder.setSSLContext(sslContext);
+ builder.setUseProxyProperties(config.useProxyProperties());
+ builder.setUseRawUrl(config.useRawUri());
+ builder.setUserAgent(config.userAgent());
+ builder.setWebSocketIdleTimeoutInMs((int)(config.idleWebSocketTimeout() * 1000));
+
+ for (final ResponseFilter responseFilter : responseFilters) {
+ builder.addResponseFilter(new com.ning.http.client.filter.ResponseFilter() {
+ @Override
+ @SuppressWarnings("rawtypes")
+ public FilterContext filter(FilterContext filterContext) throws FilterException {
+ /*
+ * TODO: returned ResponseFilterContext is ignored right now.
+ * For now, we return the input filterContext until there is a need for custom filterContext
+ * (which will complicate the code quite a bit since we are abstracting the Ning client)
+ */
+ Request request = null;
+ AsyncHandler<?> handler = filterContext.getAsyncHandler();
+ if (handler instanceof AsyncResponseHandler) {
+ request = ((AsyncResponseHandler)handler).getRequest();
+ }
+ try {
+ // We do not retain the request here since this is executed before the response handler
+ responseFilter.filter(ResponseFilterBridge.toResponseFilterContext(filterContext, request));
+ } catch (com.yahoo.jdisc.http.client.filter.FilterException e) {
+ throw new FilterException(e.getMessage());
+ }
+ return filterContext;
+ }
+ }
+ );
+ }
+ return new AsyncHttpClient(builder.build());
+ }
+
+ private static class NullContext implements Metric.Context {
+
+ static final NullContext INSTANCE = new NullContext();
+ }
+
+ private static final class ThrowingSecretStore implements SecretStore {
+
+ @Override
+ public String getSecret(String key) {
+ throw new UnsupportedOperationException("A secret store is not available");
+ }
+
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java
new file mode 100644
index 00000000000..37c7ce2ac67
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/ProxyServerFactory.java
@@ -0,0 +1,33 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.ProxyServer;
+
+import java.net.URI;
+import java.util.Locale;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ * @since 2.0
+ */
+final class ProxyServerFactory {
+
+ private ProxyServerFactory() {
+ // hide
+ }
+
+ public static ProxyServer newInstance(URI uri) {
+ if (uri == null) {
+ return null;
+ }
+ String userInfo = uri.getUserInfo();
+ String username = null, password = null;
+ if (userInfo != null) {
+ String[] arr = userInfo.split(":", 2);
+ username = arr[0];
+ password = arr.length > 1 ? arr[1] : null;
+ }
+ return new ProxyServer(ProxyServer.Protocol.valueOf(uri.getScheme().toUpperCase(Locale.US)),
+ uri.getHost(), uri.getPort(), username, password);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java
new file mode 100644
index 00000000000..b304ba8a1b2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/RequestBuilderFactory.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.RequestBuilder;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.core.HeaderFieldsUtil;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+final class RequestBuilderFactory {
+
+ private RequestBuilderFactory() {
+ // hide
+ }
+
+ public static RequestBuilder newInstance(Request request, HttpRequest.Method method) {
+ RequestBuilder builder = new RequestBuilder();
+ if (request instanceof HttpRequest) {
+ HttpRequest httpRequest = (HttpRequest)request;
+ builder.setProxyServer(ProxyServerFactory.newInstance(httpRequest.getProxyServer()));
+
+ Long timeout = httpRequest.getConnectionTimeout(TimeUnit.MILLISECONDS);
+ if (timeout != null) {
+ // TODO: Uncomment the next line once ticket 5536510 has been resolved.
+ // builder.setConnectTimeout(timeout);
+ }
+ }
+ builder.setMethod(method.name());
+ builder.setUrl(request.getUri().toString());
+ HeaderFieldsUtil.copyHeaders(request, builder);
+ return builder;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java
new file mode 100644
index 00000000000..9df75e93b92
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketClientRequest.java
@@ -0,0 +1,26 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.websocket.WebSocketUpgradeHandler;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+final class WebSocketClientRequest {
+
+ private WebSocketClientRequest() {
+ // hide
+ }
+
+ public static ContentChannel executeRequest(AsyncHttpClient client, Request request,
+ ResponseHandler responseHandler, Metric metric, Metric.Context ctx) {
+ return new WebSocketContent(client, request, new WebSocketUpgradeHandler.Builder()
+ .addWebSocketListener(new WebSocketHandler(request, responseHandler, metric, ctx))
+ .build());
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java
new file mode 100644
index 00000000000..4331620513d
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketContent.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.RequestBuilder;
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketUpgradeHandler;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * A content channel for interfacing with the web socket client. It accumulates the request data
+ * before dispatching it to the remote endpoint.
+ *
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+class WebSocketContent implements ContentChannel {
+
+ private final AsyncHttpClient client;
+ private final Request request;
+ private final WebSocketUpgradeHandler handler;
+ private final Object wsLock = new Object();
+ private WebSocket websocket;
+
+ WebSocketContent(AsyncHttpClient client, Request request, WebSocketUpgradeHandler handler) {
+ this.client = client;
+ this.request = request;
+ this.handler = handler;
+ this.websocket = null;
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ Objects.requireNonNull(buf, "buf");
+
+ try {
+ executeRequest(buf.array());
+ if (handler != null) {
+ handler.completed();
+ }
+ } catch (Exception e) {
+ if (websocket != null) {
+ websocket.close();
+ }
+
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ if (websocket != null) {
+ websocket.close();
+ }
+
+ if (handler != null) {
+ handler.completed();
+ }
+ }
+
+ private void executeRequest(final byte[] content) throws Exception {
+ RequestBuilder builder = new RequestBuilder();
+ builder.setUrl(request.getUri().toString());
+
+ synchronized (wsLock) {
+ if (websocket == null) {
+ websocket = client.executeRequest(builder.build(), handler).get();
+ }
+ }
+
+ if (websocket.isOpen()) {
+ websocket.sendMessage(content);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java
new file mode 100644
index 00000000000..9b1540881eb
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/WebSocketHandler.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketByteListener;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpResponse;
+
+import java.net.ConnectException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+class WebSocketHandler implements WebSocketByteListener {
+
+ private final CompletionHandler abortOnFailure = new AbortOnFailure();
+ private final Metric metric;
+ private final Metric.Context metricCtx;
+ private final Request request;
+ private final ResponseHandler responseHandler;
+ private ContentChannel content;
+ private boolean aborted = false;
+
+ public WebSocketHandler(Request request, ResponseHandler responseHandler, Metric metric, Metric.Context ctx) {
+ this.request = request;
+ this.responseHandler = responseHandler;
+ this.metric = metric;
+ this.metricCtx = ctx;
+ }
+
+ @Override
+ public synchronized void onOpen(WebSocket webSocket) {
+ // ignore, open on first fragment to allow failures to propagate
+ }
+
+ @Override
+ public synchronized void onMessage(byte[] bytes) {
+ if (aborted) {
+ return;
+ }
+ if (content == null) {
+ dispatchResponse();
+ }
+ // need to copy the bytes into a new buffer since there is no declared ownership of the array
+ content.write((ByteBuffer)ByteBuffer.allocate(bytes.length).put(bytes).flip(), abortOnFailure);
+ }
+
+ @Override
+ public synchronized void onFragment(byte[] bytes, boolean last) {
+ // ignore, write messages instead
+ }
+
+ @Override
+ public synchronized void onClose(WebSocket webSocket) {
+ if (aborted) {
+ return;
+ }
+ if (content == null) {
+ dispatchResponse();
+ }
+ content.close(abortOnFailure);
+ }
+
+ @Override
+ public synchronized void onError(Throwable t) {
+ abort(t);
+ }
+
+ private void dispatchResponse() {
+ content = responseHandler.handleResponse(HttpResponse.newInstance(Response.Status.OK));
+ }
+
+ private synchronized void abort(Throwable t) {
+ if (aborted) {
+ return;
+ }
+ aborted = true;
+ updateErrorMetric(t);
+ if (content == null) {
+ dispatchErrorResponse(t);
+ }
+ if (content != null) {
+ terminateContent();
+ }
+ }
+
+ private void updateErrorMetric(Throwable t) {
+ try {
+ if (t instanceof ConnectException) {
+ metric.add(HttpClient.Metrics.CONNECTION_EXCEPTIONS, 1, metricCtx);
+ } else if (t instanceof TimeoutException) {
+ metric.add(HttpClient.Metrics.TIMEOUT_EXCEPTIONS, 1, metricCtx);
+ } else {
+ metric.add(HttpClient.Metrics.OTHER_EXCEPTIONS, 1, metricCtx);
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private void dispatchErrorResponse(Throwable t) {
+ int status;
+ if (t instanceof ConnectException) {
+ status = com.yahoo.jdisc.Response.Status.SERVICE_UNAVAILABLE;
+ } else if (t instanceof TimeoutException) {
+ status = com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT;
+ } else {
+ status = com.yahoo.jdisc.Response.Status.BAD_REQUEST;
+ }
+ try {
+ content = responseHandler.handleResponse(HttpResponse.newError(request, status, t));
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private void terminateContent() {
+ try {
+ content.close(IgnoreFailure.INSTANCE);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ private class AbortOnFailure implements CompletionHandler {
+
+ @Override
+ public void completed() {
+
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ abort(t);
+ }
+ }
+
+ private static class IgnoreFailure implements CompletionHandler {
+
+ final static IgnoreFailure INSTANCE = new IgnoreFailure();
+
+ @Override
+ public void completed() {
+
+ }
+
+ @Override
+ public void failed(Throwable t) {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java
new file mode 100644
index 00000000000..b9cb5c3fac3
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/FilterException.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client.filter;
+
+/**
+ * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a>
+ */
+public class FilterException extends Exception {
+
+ public FilterException(String msg) {
+ super(msg);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java
new file mode 100644
index 00000000000..5fffc8312d7
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client.filter;
+
+/**
+ * This interface can be implemented to define custom behavior that gets invoked before the response bytes are processed.
+ * Authorization, proxy authentication and redirects processing all happen after the filters get executed.
+ *
+ * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a>
+ */
+public interface ResponseFilter {
+
+ public ResponseFilterContext filter(ResponseFilterContext filterContext) throws FilterException;
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java
new file mode 100644
index 00000000000..4f956220398
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/ResponseFilterContext.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client.filter;
+
+import com.google.common.collect.ImmutableMap;
+import com.ning.http.client.FluentCaseInsensitiveStringsMap;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a>
+ */
+public class ResponseFilterContext {
+
+ private final FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap();
+ private final Map<String, Object> requestContext;
+ private int statusCode;
+ private URI uri;
+
+ private ResponseFilterContext(Builder builder) {
+ this.statusCode = builder.statusCode;
+ this.uri = builder.uri;
+ this.headers.putAll(builder.headers);
+ requestContext = ImmutableMap.copyOf(builder.requestContext);
+ }
+
+ public URI getRequestURI() {
+ return uri;
+ }
+
+ public Map<String, Object> getRequestContext() { return requestContext; }
+
+ public String getResponseFirstHeader(String key) {
+ return headers.getFirstValue(key);
+ }
+
+ public int getResponseStatusCode() {
+ return statusCode;
+ }
+
+ public static class Builder {
+
+ private final FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap();
+ private final Map<String, Object> requestContext = new HashMap<>();
+ private int statusCode;
+ private URI uri;
+
+ public Builder() {
+ }
+
+ public Builder statusCode(int statusCode) {
+ this.statusCode = statusCode;
+ return this;
+ }
+
+ public Builder headers(FluentCaseInsensitiveStringsMap headers) {
+ this.headers.putAll(headers);
+ return this;
+ }
+
+ public Builder uri(URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ public Builder requestContext(Map<String, Object> requestContext) {
+ this.requestContext.putAll(requestContext);
+ return this;
+ }
+
+ public ResponseFilterContext build() {
+ return new ResponseFilterContext(this);
+ }
+
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java
new file mode 100644
index 00000000000..6d895ad0f93
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridge.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client.filter.core;
+
+import com.ning.http.client.filter.FilterContext;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.http.client.filter.ResponseFilterContext;
+
+import java.util.Collections;
+
+/**
+ * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a>
+ */
+public class ResponseFilterBridge {
+
+ public static ResponseFilterContext toResponseFilterContext(FilterContext<?> filterContext, Request request) {
+ return new ResponseFilterContext.Builder()
+ .uri(filterContext.getRequest().getURI())
+ .statusCode(filterContext.getResponseStatus().getStatusCode())
+ .headers(filterContext.getResponseHeaders().getHeaders())
+ .requestContext(request == null ? Collections.<String, Object>emptyMap() : request.context())
+ .build();
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java
new file mode 100644
index 00000000000..4ce70c28623
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/filter/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.client.filter;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java
new file mode 100644
index 00000000000..5d5ec2c1ab8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/client/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.client;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java
new file mode 100644
index 00000000000..7132cee91c0
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.cloud;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java
new file mode 100644
index 00000000000..7f85c8f9c2d
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/CompletionHandlers.java
@@ -0,0 +1,57 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.core;
+
+import com.yahoo.jdisc.handler.CompletionHandler;
+
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class CompletionHandlers {
+
+ public static void tryComplete(CompletionHandler handler) {
+ if (handler == null) {
+ return;
+ }
+ try {
+ handler.completed();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ public static void tryFail(CompletionHandler handler, Throwable t) {
+ if (handler == null) {
+ return;
+ }
+ try {
+ handler.failed(t);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ public static CompletionHandler wrap(CompletionHandler... handlers) {
+ return wrap(Arrays.asList(handlers));
+ }
+
+ public static CompletionHandler wrap(final Iterable<CompletionHandler> handlers) {
+ return new CompletionHandler() {
+
+ @Override
+ public void completed() {
+ for (CompletionHandler handler : handlers) {
+ tryComplete(handler);
+ }
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ for (CompletionHandler handler : handlers) {
+ tryFail(handler, t);
+ }
+ }
+ };
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java
new file mode 100644
index 00000000000..065276962f7
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/core/HeaderFieldsUtil.java
@@ -0,0 +1,142 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.core;
+
+import com.ning.http.client.RequestBuilder;
+import com.yahoo.jdisc.HeaderFields;
+import org.jboss.netty.handler.codec.http.HttpChunkTrailer;
+import org.jboss.netty.handler.codec.http.HttpHeaders;
+import org.jboss.netty.handler.codec.http.HttpMessage;
+import org.jboss.netty.handler.codec.http.HttpResponse;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HeaderFieldsUtil {
+
+ private static final byte[] DELIM_BYTES = ": ".getBytes(StandardCharsets.UTF_8);
+ private static final byte[] CRLF_BYTES = "\r\n".getBytes(StandardCharsets.UTF_8);
+ private static final Set<String> IGNORED_HEADERS = new HashSet<>(Arrays.asList(
+ HttpHeaders.Names.CONTENT_LENGTH,
+ HttpHeaders.Names.TRANSFER_ENCODING));
+
+ public static void copyHeaders(com.yahoo.jdisc.Response src, HttpResponse dst) {
+ copyHeaderFields(src.headers(), newSimpleHeaders(dst));
+ }
+
+ public static void copyHeaders(com.yahoo.jdisc.Request src, RequestBuilder dst) {
+ copyHeaderFields(src.headers(), newSimpleHeaders(dst));
+ }
+
+ public static void copyTrailers(com.yahoo.jdisc.Response src, HttpResponse dst) {
+ copyTrailers(src, newSimpleHeaders(dst));
+ }
+
+ public static void copyTrailers(com.yahoo.jdisc.Response src, HttpChunkTrailer dst) {
+ copyTrailers(src, newSimpleHeaders(dst));
+ }
+
+ public static void copyTrailers(com.yahoo.jdisc.Request src, RequestBuilder dst) {
+ copyTrailers(src, newSimpleHeaders(dst));
+ }
+
+ public static void copyTrailers(com.yahoo.jdisc.Request src, ByteArrayOutputStream dst) {
+ copyTrailers(src, newSimpleHeaders(dst));
+ }
+
+ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
+ public static void copyTrailers(com.yahoo.jdisc.Request src, SimpleHeaders dst) {
+ if (!(src instanceof com.yahoo.jdisc.http.HttpRequest)) {
+ return;
+ }
+ final HeaderFields trailers = ((com.yahoo.jdisc.http.HttpRequest)src).trailers();
+ synchronized (trailers) {
+ copyHeaderFields(trailers, dst);
+ }
+ }
+
+ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
+ public static void copyTrailers(com.yahoo.jdisc.Response src, SimpleHeaders dst) {
+ if (!(src instanceof com.yahoo.jdisc.http.HttpResponse)) {
+ return;
+ }
+ final HeaderFields trailers = ((com.yahoo.jdisc.http.HttpResponse)src).trailers();
+ synchronized (trailers) {
+ copyHeaderFields(trailers, dst);
+ }
+ }
+
+ private static void copyHeaderFields(HeaderFields src, SimpleHeaders dst) {
+ for (Map.Entry<String, List<String>> entry : src.entrySet()) {
+ String key = entry.getKey();
+ if (key != null && !IGNORED_HEADERS.contains(key)) {
+ if (entry.getValue() == null) {
+ dst.addHeader(key, "");
+ continue;
+ }
+ for (String value : entry.getValue()) {
+ dst.addHeader(key, value != null ? value : "");
+ }
+ }
+ }
+ }
+
+ private static SimpleHeaders newSimpleHeaders(final RequestBuilder dst) {
+ return new SimpleHeaders() {
+
+ @Override
+ public void addHeader(String name, String value) {
+ dst.addHeader(name, value);
+ }
+ };
+ }
+
+ private static SimpleHeaders newSimpleHeaders(final ByteArrayOutputStream dst) {
+ return new SimpleHeaders() {
+
+ @Override
+ public void addHeader(String name, String value) {
+ safeWrite(name.getBytes(StandardCharsets.UTF_8));
+ safeWrite(DELIM_BYTES);
+ safeWrite(value.getBytes(StandardCharsets.UTF_8));
+ safeWrite(CRLF_BYTES);
+ }
+
+ void safeWrite(byte[] buf) {
+ dst.write(buf, 0, buf.length);
+ }
+ };
+ }
+
+ private static SimpleHeaders newSimpleHeaders(final HttpMessage dst) {
+ return new SimpleHeaders() {
+
+ @Override
+ public void addHeader(String name, String value) {
+ dst.addHeader(name, value);
+ }
+ };
+ }
+
+ private static SimpleHeaders newSimpleHeaders(final HttpChunkTrailer dst) {
+ return new SimpleHeaders() {
+
+ @Override
+ public void addHeader(String name, String value) {
+ dst.addHeader(name, value);
+ }
+ };
+ }
+
+ private static interface SimpleHeaders {
+
+ public void addHeader(String name, String value);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java
new file mode 100644
index 00000000000..649bd2cf517
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java
@@ -0,0 +1,544 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.security.Principal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+
+import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest;
+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;
+
+/**
+ * The Request class on which all filters will operate upon.
+ * <p>
+ * This class was made abstract from 5.27. Test cases that need a concrete
+ * instance should create a {@link JdiscFilterRequest}.
+ */
+
+public abstract class DiscFilterRequest {
+
+ protected static final String HTTPS_PREFIX = "https";
+ protected static final int DEFAULT_HTTP_PORT = 80;
+ protected static final int DEFAULT_HTTPS_PORT = 443;
+
+ private final ServletOrJdiscHttpRequest parent;
+ protected final InetSocketAddress localAddress;
+ protected final Map<String, List<String>> untreatedParams;
+ private final HeaderFields untreatedHeaders;
+ private List<Cookie> untreatedCookies = null;
+ private Principal userPrincipal = null;
+ private String remoteUser = null;
+ private String[] roles = null;
+ private boolean overrideIsUserInRole = false;
+
+ public DiscFilterRequest(ServletOrJdiscHttpRequest parent) {
+ this.parent = parent;
+
+ //save untreated headers from parent
+ untreatedHeaders = new HeaderFields();
+ parent.copyHeaders(untreatedHeaders);
+
+ untreatedParams = new HashMap<>(parent.parameters());
+
+ int port = parent.getUri().getPort();
+ if(port < 0) {
+ port = 0;
+ }
+ localAddress = new InetSocketAddress(parent.getUri().getHost(), port);
+ }
+
+ public abstract String getMethod();
+
+ public Version getVersion() {
+ return parent.getVersion();
+ }
+
+ public URI getUri() {
+ return parent.getUri();
+ }
+
+ public abstract void setUri(URI uri);
+
+ public HttpRequest getParentRequest() {
+ throw new UnsupportedOperationException(
+ "getParentRequest is not supported for " + parent.getClass().getName());
+ }
+
+ /**
+ * Returns the Internet Protocol (IP) address of the client
+ * or last proxy that sent the request.
+ */
+ public String getRemoteAddr() {
+ return parent.getRemoteHostAddress();
+ }
+
+ /**
+ * Set the IP address of the remote client associated with this Request.
+ */
+ public void setRemoteAddr(String remoteIpAddress) {
+ InetSocketAddress remoteAddress = new InetSocketAddress(remoteIpAddress, this.getRemotePort());
+ parent.setRemoteAddress(remoteAddress);
+ }
+
+ /**
+ * Returns the Internet Protocol (IP) address of the interface
+ * on which the request was received.
+ */
+ public String getLocalAddr() {
+ if (null == localAddress.getAddress()) {
+ return null;
+ }
+ return localAddress.getAddress().getHostAddress();
+ }
+
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(parent.context().keySet());
+ }
+
+ public Object getAttribute(String name) {
+ return parent.context().get(name);
+ }
+
+ public void setAttribute(String name, Object value) {
+ parent.context().put(name, value);
+ }
+
+ public boolean containsAttribute(String name) {
+ return parent.context().containsKey(name);
+ }
+
+ public void removeAttribute(String name) {
+ parent.context().remove(name);
+ }
+
+ public abstract String getParameter(String name);
+
+ public abstract Enumeration<String> getParameterNames();
+
+ public List<String> getParameterNamesAsList() {
+ return new ArrayList<String>(parent.parameters().keySet());
+ }
+
+ public Enumeration<String> getParameterValues(String name) {
+ return Collections.enumeration(parent.parameters().get(name));
+ }
+
+ public List<String> getParameterValuesAsList(String name) {
+ return parent.parameters().get(name);
+ }
+
+ public Map<String,List<String>> getParameterMap() {
+ return parent.parameters();
+ }
+
+
+ /**
+ * Returns the hostName of remoteHost, or null if none
+ */
+ public String getRemoteHost() {
+ return parent.getRemoteHostName();
+ }
+
+ /**
+ * Returns the Internet Protocol (IP) port number of
+ * the interface on which the request was received.
+ */
+ public int getLocalPort() {
+ return localAddress.getPort();
+ }
+
+ /**
+ * Returns the port of remote host
+ */
+ public int getRemotePort() {
+ return parent.getRemotePort();
+ }
+
+ /**
+ * Returns a unmodifiable map of untreatedParameters from the
+ * parent request.
+ */
+ public Map<String, List<String>> getUntreatedParams() {
+ return Collections.unmodifiableMap(untreatedParams);
+ }
+
+
+ /**
+ * Returns the untreatedHeaders from
+ * parent request
+ */
+ public HeaderFields getUntreatedHeaders() {
+ return untreatedHeaders;
+ }
+
+ /**
+ * Returns the untreatedCookies from
+ * parent request
+ */
+ public List<Cookie> getUntreatedCookies() {
+ if(untreatedCookies == null) {
+ this.untreatedCookies = parent.decodeCookieHeader();
+ }
+ return Collections.unmodifiableList(untreatedCookies);
+ }
+
+ /**
+ * Sets a header with the given name and value.
+ * If the header had already been set, the new value overwrites the previous one.
+ */
+ public abstract void addHeader(String name, String value);
+
+ public long getDateHeader(String name) {
+ String value = getHeader(name);
+ if (value == null)
+ return -1L;
+
+ Date date = null;
+ for (int i = 0; (date == null) && (i < formats.length); i++) {
+ try {
+ date = formats[i].parse(value);
+ } catch (ParseException e) {
+ }
+ }
+ if (date == null) {
+ return -1L;
+ }
+
+ return date.getTime();
+ }
+
+ public abstract String getHeader(String name);
+
+ public abstract Enumeration<String> getHeaderNames();
+
+ public abstract List<String> getHeaderNamesAsList();
+
+ public abstract Enumeration<String> getHeaders(String name);
+
+ public abstract List<String> getHeadersAsList(String name);
+
+ public abstract void removeHeaders(String name);
+
+ /**
+ * Sets a header with the given name and value.
+ * If the header had already been set, the new value overwrites the previous one.
+ *
+ */
+ public abstract void setHeaders(String name, String value);
+
+ /**
+ * Sets a header with the given name and value.
+ * If the header had already been set, the new value overwrites the previous one.
+ *
+ */
+ public abstract void setHeaders(String name, List<String> values);
+
+ public int getIntHeader(String name) {
+ String value = getHeader(name);
+ if (value == null) {
+ return -1;
+ } else {
+ return Integer.parseInt(value);
+ }
+ }
+
+
+ public List<Cookie> getCookies() {
+ return parent.decodeCookieHeader();
+ }
+
+ public void setCookies(List<Cookie> cookies) {
+ parent.encodeCookieHeader(cookies);
+ }
+
+ public String getProtocol() {
+ return getVersion().name();
+ }
+
+ /**
+ * Returns the query string that is contained in the request URL.
+ * Returns the undecoded value uri.getRawQuery()
+ */
+ public String getQueryString() {
+ return getUri().getRawQuery();
+ }
+
+ /**
+ * Returns the login of the user making this request,
+ * if the user has been authenticated, or null if the user has not been authenticated.
+ */
+ public String getRemoteUser() {
+ return remoteUser;
+ }
+
+ public String getRequestURI() {
+ return getUri().getRawPath();
+ }
+
+ public String getRequestedSessionId() {
+ return null;
+ }
+
+ public String getScheme() {
+ return getUri().getScheme();
+ }
+
+ public void setScheme(String scheme, boolean isSecure) {
+ String uri = getUri().toString();
+ String arr [] = uri.split("://");
+ URI newUri = URI.create(scheme + "://" + arr[1]);
+ setUri(newUri);
+ }
+
+ public String getServerName() {
+ return getUri().getHost();
+ }
+
+ public int getServerPort() {
+ int port = getUri().getPort();
+ if(port == -1) {
+ if(isSecure()) {
+ port = DEFAULT_HTTPS_PORT;
+ }
+ else {
+ port = DEFAULT_HTTP_PORT;
+ }
+ }
+
+ return port;
+ }
+
+ public Principal getUserPrincipal() {
+ return userPrincipal;
+ }
+
+ public boolean isSecure() {
+ if(getScheme().equalsIgnoreCase(HTTPS_PREFIX)) {
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Returns a boolean indicating whether the authenticated user
+ * is included in the specified logical "role".
+ */
+ public boolean isUserInRole(String role) {
+ if(overrideIsUserInRole) {
+ if(roles != null) {
+ for (String role1 : roles) {
+ if (role1 != null && role1.trim().length() > 0) {
+ String userRole = role1.trim();
+ if (userRole.equals(role)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ else {
+ return false;
+ }
+ }
+
+ public void setOverrideIsUserInRole(boolean overrideIsUserInRole) {
+ this.overrideIsUserInRole = overrideIsUserInRole;
+ }
+
+ public void setRemoteHost(String remoteAddr) { }
+
+ public void setRemoteUser(String remoteUser) {
+ this.remoteUser = remoteUser;
+ }
+
+ public void setUserPrincipal(Principal principal) {
+ this.userPrincipal = principal;
+ }
+
+ public void setUserRoles(String[] roles) {
+ this.roles = roles;
+ }
+
+ /**
+ * Returns the content-type for the request
+ */
+ public String getContentType() {
+ return getHeader(HttpHeaders.Names.CONTENT_TYPE);
+ }
+
+
+ /**
+ * Get character encoding
+ */
+ public String getCharacterEncoding() {
+ return getCharsetFromContentType(this.getContentType());
+ }
+
+ /**
+ * Set character encoding
+ */
+ public void setCharacterEncoding(String encoding) {
+ String charEncoding = setCharsetFromContentType(this.getContentType(), encoding);
+ if(charEncoding != null && !charEncoding.isEmpty()) {
+ removeHeaders(HttpHeaders.Names.CONTENT_TYPE);
+ setHeaders(HttpHeaders.Names.CONTENT_TYPE, charEncoding);
+ }
+ }
+
+ /**
+ * Can be called multiple times to add Cookies
+ */
+ public void addCookie(JDiscCookieWrapper cookie) {
+ if(cookie != null) {
+ List<Cookie> cookies = new ArrayList<Cookie>();
+ //Get current set of cookies first
+ List<Cookie> c = getCookies();
+ if(c != null && !c.isEmpty()) {
+ cookies.addAll(c);
+ }
+ cookies.add(cookie.getCookie());
+ setCookies(cookies);
+ }
+ }
+
+ public abstract void clearCookies();
+
+ public JDiscCookieWrapper[] getWrappedCookies() {
+ List<Cookie> cookies = getCookies();
+ if(cookies == null) {
+ return null;
+ }
+ List<JDiscCookieWrapper> cookieWrapper = new ArrayList<>(cookies.size());
+ for(Cookie cookie : cookies) {
+ cookieWrapper.add(JDiscCookieWrapper.wrap(cookie));
+ }
+
+ return cookieWrapper.toArray(new JDiscCookieWrapper[cookieWrapper.size()]);
+ }
+
+ private String setCharsetFromContentType(String contentType,String charset) {
+ String newContentType = "";
+ if (contentType == null)
+ return (null);
+ int start = contentType.indexOf("charset=");
+ if (start < 0) {
+ //No charset present:
+ newContentType = contentType + ";charset=" + charset;
+ return newContentType;
+ }
+ String encoding = contentType.substring(start + 8);
+ int end = encoding.indexOf(';');
+ if (end >= 0) {
+ newContentType = contentType.substring(0,start);
+ newContentType = newContentType + "charset=" + charset;
+ newContentType = newContentType + encoding.substring(end,encoding.length());
+ }
+ else {
+ newContentType = contentType.substring(0,start);
+ newContentType = newContentType + "charset=" + charset;
+ }
+
+ return (newContentType.trim());
+
+ }
+
+ private String getCharsetFromContentType(String contentType) {
+
+ if (contentType == null)
+ return (null);
+ int start = contentType.indexOf("charset=");
+ if (start < 0)
+ return (null);
+ String encoding = contentType.substring(start + 8);
+ int end = encoding.indexOf(';');
+ if (end >= 0)
+ encoding = encoding.substring(0, end);
+ encoding = encoding.trim();
+ if ((encoding.length() > 2) && (encoding.startsWith("\""))
+ && (encoding.endsWith("\"")))
+ encoding = encoding.substring(1, encoding.length() - 1);
+ return (encoding.trim());
+
+ }
+
+ public static boolean isMultipart(DiscFilterRequest request) {
+ if (request == null) {
+ return false;
+ }
+
+ String contentType = request.getContentType();
+
+ if (contentType == null) {
+ return false;
+ }
+
+ String[] parts = Pattern.compile(";").split(contentType);
+ if (parts.length == 0) {
+ return false;
+ }
+
+ for (String part : parts) {
+ if ("multipart/form-data".equals(part)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected static ThreadLocalSimpleDateFormat formats[] = {
+ new ThreadLocalSimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz",
+ Locale.US),
+ new ThreadLocalSimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz",
+ Locale.US),
+ new ThreadLocalSimpleDateFormat("EEE MMMM d HH:mm:ss yyyy",
+ Locale.US) };
+
+ /**
+ * The set of SimpleDateFormat formats to use in getDateHeader().
+ *
+ * Notice that because SimpleDateFormat is not thread-safe, we can't declare
+ * formats[] as a static variable.
+ */
+ protected static final class ThreadLocalSimpleDateFormat extends
+ ThreadLocal<SimpleDateFormat> {
+ private final String format;
+ private final Locale locale;
+
+ public ThreadLocalSimpleDateFormat(String format, Locale locale) {
+ super();
+ this.format = format;
+ this.locale = locale;
+ }
+
+ // @see java.lang.ThreadLocal#initialValue()
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return new SimpleDateFormat(format, locale);
+ }
+
+ public Date parse(String value) throws ParseException {
+ return get().parse(value);
+ }
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java
new file mode 100644
index 00000000000..84baf5c1177
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java
@@ -0,0 +1,154 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.http.Cookie;
+
+
+import com.yahoo.jdisc.http.HttpResponse;
+
+/**
+ * This class was made abstract from 5.27. Test cases that need
+ * a concrete instance should create a {@link JdiscFilterResponse}.
+ *
+ * @author tejalk
+ */
+public abstract class DiscFilterResponse {
+
+ private final ServletOrJdiscHttpResponse parent;
+ private final HeaderFields untreatedHeaders;
+ private final List<Cookie> untreatedCookies;
+
+ public DiscFilterResponse(ServletOrJdiscHttpResponse parent) {
+ this.parent = parent;
+
+ this.untreatedHeaders = new HeaderFields();
+ parent.copyHeaders(untreatedHeaders);
+
+ this.untreatedCookies = getCookies();
+ }
+
+ /* Attributes on the response are only used for unit testing.
+ * There is no such thing as 'attributes' in the underlying response. */
+
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(parent.context().keySet());
+ }
+
+ public Object getAttribute(String name) {
+ return parent.context().get(name);
+ }
+
+ public void setAttribute(String name, Object value) {
+ parent.context().put(name, value);
+ }
+
+ public void removeAttribute(String name) {
+ parent.context().remove(name);
+ }
+
+ /**
+ * Returns the untreatedHeaders from the parent request
+ */
+ public HeaderFields getUntreatedHeaders() {
+ return untreatedHeaders;
+ }
+
+ /**
+ * Returns the untreatedCookies from the parent request
+ */
+ public List<Cookie> getUntreatedCookies() {
+ return untreatedCookies;
+ }
+
+ /**
+ * Sets a header with the given name and value.
+ * <p>
+ * If the header had already been set, the new value overwrites the previous one.
+ */
+ public abstract void setHeader(String name, String value);
+
+ public abstract void removeHeaders(String name);
+
+ /**
+ * Sets a header with the given name and value.
+ * <p>
+ * If the header had already been set, the new value overwrites the previous one.
+ */
+ public abstract void setHeaders(String name, String value);
+
+ /**
+ * Sets a header with the given name and value.
+ * <p>
+ * If the header had already been set, the new value overwrites the previous one.
+ */
+ public abstract void setHeaders(String name, List<String> values);
+
+ /**
+ * Adds a header with the given name and value
+ * @see com.yahoo.jdisc.HeaderFields#add
+ */
+ public abstract void addHeader(String name, String value);
+
+ public abstract String getHeader(String name);
+
+ public List<Cookie> getCookies() {
+ return parent.decodeSetCookieHeader();
+ }
+
+ public abstract void setCookies(List<Cookie> cookies);
+
+ public int getStatus() {
+ return parent.getStatus();
+ }
+
+ public abstract void setStatus(int status);
+
+ /**
+ * Return the parent HttpResponse
+ */
+ public HttpResponse getParentResponse() {
+ if (parent instanceof HttpResponse)
+ return (HttpResponse)parent;
+ throw new UnsupportedOperationException(
+ "getParentResponse is not supported for " + parent.getClass().getName());
+ }
+
+ public void addCookie(JDiscCookieWrapper cookie) {
+ if(cookie != null) {
+ List<Cookie> cookies = new ArrayList<>();
+ //Get current set of cookies first
+ List<Cookie> c = getCookies();
+ if((c != null) && (! c.isEmpty())) {
+ cookies.addAll(c);
+ }
+ cookies.add(cookie.getCookie());
+ setCookies(cookies);
+ }
+ }
+
+ /**
+ * This method does not actually send the response as it
+ * does not have access to responseHandler but
+ * just sets the status. The methodName is misleading
+ * for historical reasons.
+ */
+ public void sendError(int errorCode) throws IOException {
+ setStatus(errorCode);
+ }
+
+ public void setCookie(String name, String value) {
+ Cookie cookie = new Cookie(name, value);
+ setCookies(Arrays.asList(cookie));
+ }
+
+ }
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java
new file mode 100644
index 00000000000..b01253536b6
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.util.Collection;
+
+/**
+ * Legacy filter config. Prefer to use a regular stringly typed config class for new filters.
+ *
+ * @author tejalk
+ */
+public interface FilterConfig {
+
+ /** Returns the filter-name of this filter */
+ String getFilterName();
+
+ /** Returns the filter-class of this filter */
+ String getFilterClass();
+
+ /**
+ * Returns a String containing the value of the
+ * named initialization parameter, or null if
+ * the parameter does not exist.
+ *
+ * @param name a String specifying the name of the initialization parameter
+ * @return a String containing the value of the initialization parameter
+ */
+ String getInitParameter(String name);
+
+ /**
+ * Returns the boolean value of the init parameter. If not present returns default value
+ *
+ * @return boolean value of init parameter
+ */
+ boolean getBooleanInitParameter(String name, boolean defaultValue);
+
+ /**
+ * Returns the names of the filter's initialization parameters as an Collection of String objects,
+ * or an empty Collection if the filter has no initialization parameters.
+ */
+ Collection<String> getInitParameterNames();
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java
new file mode 100644
index 00000000000..c9765b648d2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.util.concurrent.TimeUnit;
+
+import com.yahoo.jdisc.http.Cookie;
+
+/**
+ * Wrapper of Cookie.
+ *
+ * @author tejalk
+ *
+ */
+public class JDiscCookieWrapper {
+
+ private Cookie cookie;
+
+ protected JDiscCookieWrapper(Cookie cookie) {
+ this.cookie = cookie;
+ }
+
+ public static JDiscCookieWrapper wrap(Cookie cookie) {
+ return new JDiscCookieWrapper(cookie);
+ }
+
+ public String getComment() {
+ return cookie.getComment();
+ }
+
+ public String getDomain() {
+ return cookie.getDomain();
+ }
+
+ public int getMaxAge() {
+ return cookie.getMaxAge(TimeUnit.SECONDS);
+ }
+
+ public String getName() {
+ return cookie.getName();
+ }
+
+ public String getPath() {
+ return cookie.getPath();
+ }
+
+ public boolean getSecure() {
+ return cookie.isSecure();
+ }
+
+ public String getValue() {
+ return cookie.getValue();
+ }
+
+ public int getVersion() {
+ return cookie.getVersion();
+ }
+
+ public void setComment(String purpose) {
+ cookie.setComment(purpose);
+ }
+
+ public void setDomain(String pattern) {
+ cookie.setDomain(pattern);
+ }
+
+ public void setMaxAge(int expiry) {
+ cookie.setMaxAge(expiry, TimeUnit.SECONDS);
+ }
+
+ public void setPath(String uri) {
+ cookie.setPath(uri);
+ }
+
+ public void setSecure(boolean flag) {
+ cookie.setSecure(flag);
+ }
+
+ public void setValue(String newValue) {
+ cookie.setValue(newValue);
+ }
+
+ public void setVersion(int version) {
+ cookie.setVersion(version);
+ }
+
+ /**
+ * Return com.yahoo.jdisc.http.Cookie
+ *
+ * @return - cookie
+ */
+ public Cookie getCookie() {
+ return cookie;
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java
new file mode 100644
index 00000000000..69de16a50c9
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. 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.HttpHeaders;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * JDisc implementation of a filter request.
+ *
+ * @since 5.27
+ */
+public class JdiscFilterRequest extends DiscFilterRequest {
+
+ private final HttpRequest parent;
+
+ public JdiscFilterRequest(HttpRequest parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ public HttpRequest getParentRequest() {
+ return parent;
+ }
+
+ public void setUri(URI uri) {
+ parent.setUri(uri);
+ }
+
+ @Override
+ public String getMethod() {
+ return parent.getMethod().name();
+ }
+
+ @Override
+ public String getParameter(String name) {
+ if(parent.parameters().containsKey(name)) {
+ return parent.parameters().get(name).get(0);
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(parent.parameters().keySet());
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ parent.headers().add(name, value);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ List<String> values = parent.headers().get(name);
+ if (values == null || values.isEmpty()) {
+ return null;
+ }
+ return values.get(values.size() - 1);
+ }
+
+ public Enumeration<String> getHeaderNames() {
+ return Collections.enumeration(parent.headers().keySet());
+ }
+
+ public List<String> getHeaderNamesAsList() {
+ return new ArrayList<String>(parent.headers().keySet());
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ return Collections.enumeration(getHeadersAsList(name));
+ }
+
+ public List<String> getHeadersAsList(String name) {
+ List<String> values = parent.headers().get(name);
+ if(values == null) {
+ return Collections.<String>emptyList();
+ }
+ return parent.headers().get(name);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ parent.headers().remove(name);
+ }
+
+ @Override
+ public void setHeaders(String name, String value) {
+ parent.headers().put(name, value);
+ }
+
+ @Override
+ public void setHeaders(String name, List<String> values) {
+ parent.headers().put(name, values);
+ }
+
+ @Override
+ public void clearCookies() {
+ parent.headers().remove(HttpHeaders.Names.COOKIE);
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java
new file mode 100644
index 00000000000..6d2a87cfa53
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java
@@ -0,0 +1,67 @@
+// Copyright 2016 Yahoo Inc. 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.HttpResponse;
+
+import java.util.List;
+
+/**
+ * JDisc implementation of a filter request.
+ *
+ * @since 5.27
+ */
+public class JdiscFilterResponse extends DiscFilterResponse {
+
+ private final HttpResponse parent;
+
+ public JdiscFilterResponse(HttpResponse parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ @Override
+ public void setStatus(int status) {
+ parent.setStatus(status);
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ parent.headers().put(name, value);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ parent.headers().remove(name);
+ }
+
+ @Override
+ public void setHeaders(String name, String value) {
+ parent.headers().put(name, value);
+ }
+
+ @Override
+ public void setHeaders(String name, List<String> values) {
+ parent.headers().put(name, values);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ parent.headers().add(name, value);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ List<String> values = parent.headers().get(name);
+ if (values == null || values.isEmpty()) {
+ return null;
+ }
+ return values.get(values.size() - 1);
+ }
+
+ @Override
+ public void setCookies(List<Cookie> cookies) {
+ parent.encodeSetCookieHeader(cookies);
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java
new file mode 100644
index 00000000000..8202ef0e693
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. 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.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public interface RequestFilter extends com.yahoo.jdisc.SharedResource, RequestFilterBase {
+
+ public void filter(HttpRequest request, ResponseHandler handler);
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java
new file mode 100644
index 00000000000..47a41dfd6bc
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java
@@ -0,0 +1,9 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+/**
+ * @author gjoranv
+ * @since 2.4
+ */
+public interface RequestFilterBase {
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java
new file mode 100644
index 00000000000..f03a16f0bf0
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. 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.HttpRequest.Method;
+
+import java.net.URI;
+import java.util.Optional;
+
+/**
+ * Read-only view of the request for use by SecurityResponseFilters.
+ *
+ * @author tonytv
+ */
+public interface RequestView {
+
+ /**
+ * Returns a named attribute.
+ *
+ * @see <a href="http://docs.oracle.com/javaee/7/api/javax/servlet/ServletRequest.html#getAttribute%28java.lang.String%29">javax.servlet.ServletRequest.getAttribute(java.lang.String)</a>
+ * @see com.yahoo.jdisc.Request#context()
+ * @return the named data associated with the request that are private to this runtime (not exposed to the client)
+ */
+ public Object getAttribute(String name);
+
+ /**
+ * Returns the Http method. Only present if the underlying request has http-like semantics.
+ */
+ public Optional<Method> getMethod();
+
+ public URI getUri();
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java
new file mode 100644
index 00000000000..244ae056c33
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java
@@ -0,0 +1,14 @@
+// Copyright 2016 Yahoo Inc. 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.Response;
+import com.yahoo.jdisc.SharedResource;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public interface ResponseFilter extends SharedResource, ResponseFilterBase {
+
+ public void filter(Response response, Request request);
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java
new file mode 100644
index 00000000000..c9bd1c8de67
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java
@@ -0,0 +1,9 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+/**
+ * @author gjoranv
+ * @since 2.4
+ */
+public interface ResponseFilterBase {
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java
new file mode 100644
index 00000000000..52e05484afc
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. 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.annotations.Beta;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.jdisc.http.servlet.ServletRequest;
+
+import com.yahoo.jdisc.http.servlet.ServletResponse;
+import com.yahoo.jdisc.http.server.jetty.FilterInvoker;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+import java.util.Optional;
+
+/**
+ * Only intended for internal vespa use.
+ *
+ * Runs JDisc security filter without using JDisc request/response.
+ * Only intended to be used in a servlet context, as the error messages are tailored for that.
+ *
+ * Assumes that SecurityResponseFilters mutate DiscFilterResponse in the thread they are invoked from.
+ *
+ * @author tonytv
+ */
+@Beta
+public class SecurityFilterInvoker implements FilterInvoker {
+
+ /**
+ * Returns the servlet request to be used in any servlets invoked after this.
+ */
+ @Override
+ public HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain,
+ URI uri, HttpServletRequest httpRequest,
+ ResponseHandler responseHandler) {
+
+ SecurityRequestFilterChain securityChain = cast(SecurityRequestFilterChain.class, requestFilterChain).
+ orElseThrow(SecurityFilterInvoker::newUnsupportedOperationException);
+
+ ServletRequest wrappedRequest = new ServletRequest(httpRequest, uri);
+ securityChain.filter(new ServletFilterRequest(wrappedRequest), responseHandler);
+ return wrappedRequest;
+ }
+
+ @Override
+ public void invokeResponseFilterChain(
+ ResponseFilter responseFilterChain,
+ URI uri,
+ HttpServletRequest request,
+ HttpServletResponse response) {
+
+ SecurityResponseFilterChain securityChain = cast(SecurityResponseFilterChain.class, responseFilterChain).
+ orElseThrow(SecurityFilterInvoker::newUnsupportedOperationException);
+
+ ServletFilterResponse wrappedResponse = new ServletFilterResponse(new ServletResponse(response));
+ securityChain.filter(new ServletRequestView(uri, request), wrappedResponse);
+ }
+
+ private static UnsupportedOperationException newUnsupportedOperationException() {
+ return new UnsupportedOperationException(
+ "Filter type not supported. If a request is handled by servlets or jax-rs, then any filters invoked for that request must be security filters.");
+ }
+
+ private <T> Optional<T> cast(Class<T> securityFilterChainClass, Object filter) {
+ return (securityFilterChainClass.isInstance(filter))?
+ Optional.of(securityFilterChainClass.cast(filter)):
+ Optional.empty();
+ }
+
+ private static class ServletRequestView implements RequestView {
+ private final HttpServletRequest request;
+ private final URI uri;
+
+ public ServletRequestView(URI uri, HttpServletRequest request) {
+ this.request = request;
+ this.uri = uri;
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return request.getAttribute(name);
+ }
+
+ @Override
+ public Optional<Method> getMethod() {
+ return Optional.of(Method.valueOf(request.getMethod()));
+ }
+
+ @Override
+ public URI getUri() {
+ return uri;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java
new file mode 100644
index 00000000000..77ee10111be
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. 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.handler.ResponseHandler;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface SecurityRequestFilter extends RequestFilterBase {
+
+ void filter(DiscFilterRequest request, ResponseHandler handler);
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java
new file mode 100644
index 00000000000..d6c5629d6c1
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Yahoo Inc. 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.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of TypedFilterChain for DiscFilterRequest
+ *
+ * @author tejalk
+ *
+ */
+public final class SecurityRequestFilterChain extends AbstractResource implements RequestFilter {
+
+ private final List<SecurityRequestFilter> filters = new ArrayList<SecurityRequestFilter>();
+
+ private SecurityRequestFilterChain(Iterable<? extends SecurityRequestFilter> filters) {
+ for (SecurityRequestFilter filter : filters) {
+ this.filters.add(filter);
+ }
+ }
+
+ @Override
+ public void filter(HttpRequest request, ResponseHandler responseHandler) {
+ DiscFilterRequest discFilterRequest = new JdiscFilterRequest(request);
+ filter(discFilterRequest, responseHandler);
+ }
+
+ public void filter(DiscFilterRequest request, ResponseHandler responseHandler) {
+ ResponseHandlerGuard guard = new ResponseHandlerGuard(responseHandler);
+ for (int i = 0, len = filters.size(); i < len && !guard.isDone(); ++i) {
+ filters.get(i).filter(request, guard);
+ }
+ }
+
+ public static RequestFilter newInstance(SecurityRequestFilter... filters) {
+ return newInstance(Arrays.asList(filters));
+ }
+
+ public static RequestFilter newInstance(List<? extends SecurityRequestFilter> filters) {
+ return new SecurityRequestFilterChain(filters);
+ }
+
+ private static class ResponseHandlerGuard implements ResponseHandler {
+
+ private final ResponseHandler responseHandler;
+ private boolean done = false;
+
+ public ResponseHandlerGuard(ResponseHandler handler) {
+ this.responseHandler = handler;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ done = true;
+ return responseHandler.handleResponse(response);
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+ }
+
+ /** Returns an unmodifiable viuew of the filters in this */
+ public List<SecurityRequestFilter> getFilters() {
+ return Collections.unmodifiableList(filters);
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java
new file mode 100644
index 00000000000..e4acb3f1c89
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+public interface SecurityResponseFilter extends ResponseFilterBase {
+
+ void filter(DiscFilterResponse response, RequestView request);
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java
new file mode 100644
index 00000000000..6ac68cee894
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java
@@ -0,0 +1,89 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+
+/**
+ * Implementation of TypedFilterChain for DiscFilterResponse
+ * @author tejalk
+ *
+ */
+public class SecurityResponseFilterChain extends AbstractResource implements ResponseFilter {
+
+ private final List<SecurityResponseFilter> filters = new ArrayList<>();
+
+ private SecurityResponseFilterChain(Iterable<? extends SecurityResponseFilter> filters) {
+ for (SecurityResponseFilter filter : filters) {
+ this.filters.add(filter);
+ }
+ }
+
+ @Override
+ public void filter(Response response, Request request) {
+ if(response instanceof HttpResponse) {
+ DiscFilterResponse discFilterResponse = new JdiscFilterResponse((HttpResponse)response);
+ RequestView requestView = new RequestViewImpl(request);
+ filter(requestView, discFilterResponse);
+ }
+
+ }
+
+ public void filter(RequestView requestView, DiscFilterResponse response) {
+ for (SecurityResponseFilter filter : filters) {
+ filter.filter(response, requestView);
+ }
+ }
+
+ public static ResponseFilter newInstance(SecurityResponseFilter... filters) {
+ return newInstance(Arrays.asList(filters));
+ }
+
+ public static ResponseFilter newInstance(List<? extends SecurityResponseFilter> filters) {
+ return new SecurityResponseFilterChain(filters);
+ }
+
+ /** Returns an unmodifiable view of the filters in this */
+ public List<SecurityResponseFilter> getFilters() {
+ return Collections.unmodifiableList(filters);
+ }
+
+ private static class RequestViewImpl implements RequestView {
+
+ private final Request request;
+ private final Optional<HttpRequest.Method> method;
+
+ public RequestViewImpl(Request request) {
+ this.request = request;
+ method = request instanceof HttpRequest ?
+ Optional.of(((HttpRequest) request).getMethod()):
+ Optional.empty();
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return request.context().get(name);
+ }
+
+ @Override
+ public Optional<HttpRequest.Method> getMethod() {
+ return method;
+ }
+
+ @Override
+ public URI getUri() {
+ return request.getUri();
+ }
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java
new file mode 100644
index 00000000000..8b5e91e0ad6
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java
@@ -0,0 +1,149 @@
+// Copyright 2016 Yahoo Inc. 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.HttpHeaders;
+import com.yahoo.jdisc.http.servlet.ServletRequest;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Servlet implementation for JDisc filter requests.
+ *
+ * @since 5.27
+ */
+class ServletFilterRequest extends DiscFilterRequest {
+
+ private final ServletRequest parent;
+
+ public ServletFilterRequest(ServletRequest parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ ServletRequest getServletRequest() {
+ return parent;
+ }
+
+ public void setUri(URI uri) {
+ parent.setUri(uri);
+ }
+
+ @Override
+ public String getMethod() {
+ return parent.getRequest().getMethod();
+ }
+
+ @Override
+ public void setRemoteAddr(String remoteIpAddress) {
+ throw new UnsupportedOperationException(
+ "Setting remote address is not supported for " + this.getClass().getName());
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames() {
+ Set<String> names = new HashSet<>(Collections.list(super.getAttributeNames()));
+ names.addAll(Collections.list(parent.getRequest().getAttributeNames()));
+ return Collections.enumeration(names);
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ Object jdiscAttribute = super.getAttribute(name);
+ return jdiscAttribute != null ?
+ jdiscAttribute :
+ parent.getRequest().getAttribute(name);
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ super.setAttribute(name, value);
+ parent.getRequest().setAttribute(name, value);
+ }
+
+ @Override
+ public boolean containsAttribute(String name) {
+ return super.containsAttribute(name)
+ || parent.getRequest().getAttribute(name) != null;
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ super.removeAttribute(name);
+ parent.getRequest().removeAttribute(name);
+ }
+
+ @Override
+ public String getParameter(String name) {
+ return parent.getParameter(name);
+ }
+
+ @Override
+ public Enumeration<String> getParameterNames() {
+ return parent.getParameterNames();
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ parent.addHeader(name, value);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return parent.getHeader(name);
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames() {
+ return parent.getHeaderNames();
+ }
+
+ public List<String> getHeaderNamesAsList() {
+ return Collections.list(getHeaderNames());
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ return parent.getHeaders(name);
+ }
+
+ @Override
+ public List<String> getHeadersAsList(String name) {
+ return Collections.list(getHeaders(name));
+ }
+
+ @Override
+ public void setHeaders(String name, String value) {
+ parent.setHeaders(name, value);
+ }
+
+ @Override
+ public void setHeaders(String name, List<String> values) {
+ parent.setHeaders(name, values);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ parent.removeHeaders(name);
+ }
+
+ @Override
+ public void clearCookies() {
+ parent.removeHeaders(HttpHeaders.Names.COOKIE);
+ }
+
+ @Override
+ public void setCharacterEncoding(String encoding) {
+ super.setCharacterEncoding(encoding);
+ try {
+ parent.setCharacterEncoding(encoding);
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Encoding not supported: " + encoding, e);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java
new file mode 100644
index 00000000000..13f3eb828cd
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. 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.Iterables;
+import com.yahoo.jdisc.http.Cookie;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.servlet.ServletResponse;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Servlet implementation for JDisc filter responses.
+ *
+ * @since 5.27
+ */
+class ServletFilterResponse extends DiscFilterResponse {
+
+ private final ServletResponse parent;
+
+ public ServletFilterResponse(ServletResponse parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ ServletResponse getServletResponse() {
+ return parent;
+ }
+
+ public void setStatus(int status) {
+ parent.setStatus(status);
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ parent.setHeader(name, value);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ HttpServletResponse parentResponse = parent.getResponse();
+ if (parentResponse instanceof org.eclipse.jetty.server.Response) {
+ org.eclipse.jetty.server.Response jettyResponse = (org.eclipse.jetty.server.Response)parentResponse;
+ jettyResponse.getHttpFields().remove(name);
+ } else {
+ throw new UnsupportedOperationException(
+ "Cannot remove headers for response of type " + parentResponse.getClass().getName());
+ }
+ }
+
+ // Why have a setHeaders that takes a single string?
+ @Override
+ public void setHeaders(String name, String value) {
+ parent.setHeader(name, value);
+ }
+
+ @Override
+ public void setHeaders(String name, List<String> values) {
+ for (String value : values)
+ parent.addHeader(name, value);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ parent.addHeader(name, value);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ Collection<String> headers = parent.getHeaders(name);
+ return headers.isEmpty()
+ ? null
+ : Iterables.getLast(headers);
+ }
+
+ @Override
+ public void setCookies(List<Cookie> cookies) {
+ removeHeaders(HttpHeaders.Names.SET_COOKIE);
+ for (Cookie cookie : cookies) {
+ addHeader(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeader(Arrays.asList(cookie)));
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java
new file mode 100644
index 00000000000..9cb103f0c6b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public final class EmptyRequestFilter extends NoopSharedResource implements RequestFilter {
+
+ public static final RequestFilter INSTANCE = new EmptyRequestFilter();
+
+ private EmptyRequestFilter() {
+ // hide
+ }
+
+ @Override
+ public void filter(HttpRequest request, ResponseHandler handler) {
+
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java
new file mode 100644
index 00000000000..7c09e605b46
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.jdisc.NoopSharedResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public final class EmptyResponseFilter extends NoopSharedResource implements ResponseFilter {
+
+ public static final ResponseFilter INSTANCE = new EmptyResponseFilter();
+
+ private EmptyResponseFilter() {
+ // hide
+ }
+
+ @Override
+ public void filter(Response response, Request request) {
+
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java
new file mode 100644
index 00000000000..76ab390f259
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java
@@ -0,0 +1,55 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.application.ResourcePool;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public final class RequestFilterChain extends AbstractResource implements RequestFilter {
+
+ private final List<RequestFilter> filters = new ArrayList<>();
+ private final ResourcePool filterReferences = new ResourcePool();
+
+ private RequestFilterChain(Iterable<? extends RequestFilter> filters) {
+ for (RequestFilter filter : filters) {
+ this.filters.add(filter);
+ filterReferences.retain(filter);
+ }
+ }
+
+ @Override
+ public void filter(HttpRequest request, ResponseHandler responseHandler) {
+ ResponseHandlerGuard guard = new ResponseHandlerGuard(responseHandler);
+ for (int i = 0, len = filters.size(); i < len && !guard.isDone(); ++i) {
+ filters.get(i).filter(request, guard);
+ }
+ }
+
+ @Override
+ protected void destroy() {
+ filterReferences.release();
+ }
+
+ public static RequestFilter newInstance(RequestFilter... filters) {
+ return newInstance(Arrays.asList(filters));
+ }
+
+ public static RequestFilter newInstance(List<? extends RequestFilter> filters) {
+ if (filters.size() == 0) {
+ return EmptyRequestFilter.INSTANCE;
+ }
+ if (filters.size() == 1) {
+ return filters.get(0);
+ }
+ return new RequestFilterChain(filters);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java
new file mode 100644
index 00000000000..1433b98006f
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.jdisc.AbstractResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ResourcePool;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public final class ResponseFilterChain extends AbstractResource implements ResponseFilter {
+
+ private final List<ResponseFilter> filters = new ArrayList<>();
+ private final ResourcePool filterReferences = new ResourcePool();
+
+ private ResponseFilterChain(Iterable<? extends ResponseFilter> filters) {
+ for (ResponseFilter filter : filters) {
+ this.filters.add(filter);
+ filterReferences.retain(filter);
+ }
+ }
+
+ @Override
+ public void filter(Response response, Request request) {
+ for (ResponseFilter filter : filters) {
+ filter.filter(response, request);
+ }
+ }
+
+ @Override
+ protected void destroy() {
+ filterReferences.release();
+ }
+
+ public static ResponseFilter newInstance(ResponseFilter... filters) {
+ return newInstance(Arrays.asList(filters));
+ }
+
+ public static ResponseFilter newInstance(List<? extends ResponseFilter> filters) {
+ if (filters.size() == 0) {
+ return EmptyResponseFilter.INSTANCE;
+ }
+ if (filters.size() == 1) {
+ return filters.get(0);
+ }
+ return new ResponseFilterChain(filters);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java
new file mode 100644
index 00000000000..5194cafd527
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+final class ResponseHandlerGuard implements ResponseHandler {
+
+ private final ResponseHandler responseHandler;
+ private boolean done = false;
+
+ public ResponseHandlerGuard(ResponseHandler handler) {
+ this.responseHandler = handler;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ done = true;
+ return responseHandler.handleResponse(response);
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java
new file mode 100644
index 00000000000..c37c4a3047b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.filter.chain;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java
new file mode 100644
index 00000000000..551ad0aad87
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@PublicApi
+@ExportPackage
+package com.yahoo.jdisc.http.filter;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java
new file mode 100644
index 00000000000..d4b2709e47b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@PublicApi
+@ExportPackage
+package com.yahoo.jdisc.http;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java
new file mode 100644
index 00000000000..cc3c4efc913
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/FilterBindings.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server;
+
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class FilterBindings {
+
+ private final BindingRepository<RequestFilter> requestFilters;
+ private final BindingRepository<ResponseFilter> responseFilters;
+
+ public FilterBindings(BindingRepository<RequestFilter> requestFilters,
+ BindingRepository<ResponseFilter> responseFilters) {
+ this.requestFilters = requestFilters;
+ this.responseFilters = responseFilters;
+ }
+
+ public BindingRepository<RequestFilter> getRequestFilters() {
+ return requestFilters;
+ }
+
+ public BindingRepository<ResponseFilter> getResponseFilters() {
+ return responseFilters;
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java
new file mode 100644
index 00000000000..1049c2eed61
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java
@@ -0,0 +1,150 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Objects;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.container.logging.AccessLogEntry;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+import javax.servlet.http.HttpServletRequest;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class is a bridge between Jetty's {@link org.eclipse.jetty.server.handler.RequestLogHandler}
+ * and our own configurable access logging in different formats provided by {@link AccessLog}.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class AccessLogRequestLog extends AbstractLifeCycle implements RequestLog {
+
+ private static final Logger logger = Logger.getLogger(AccessLogRequestLog.class.getName());
+
+ private static final String HEADER_NAME_Y_RA = "y-ra";
+ private static final String HEADER_NAME_Y_RP = "y-rp";
+ private static final String HEADER_NAME_YAHOOREMOTEIP = "yahooremoteip";
+ private static final String HEADER_NAME_X_FORWARDED_FOR = "x-forwarded-for";
+ private static final String HEADER_NAME_CLIENT_IP = "client-ip";
+
+ private final AccessLog accessLog;
+
+ public AccessLogRequestLog(final AccessLog accessLog) {
+ this.accessLog = accessLog;
+ }
+
+ @Override
+ public void log(final Request request, final Response response) {
+ final AccessLogEntry accessLogEntryFromServletRequest = (AccessLogEntry) request.getAttribute(
+ JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY);
+ final AccessLogEntry accessLogEntry;
+ if (accessLogEntryFromServletRequest != null) {
+ accessLogEntry = accessLogEntryFromServletRequest;
+ } else {
+ accessLogEntry = new AccessLogEntry();
+ populateAccessLogEntryFromHttpServletRequest(request, accessLogEntry);
+ }
+
+ final long startTime = request.getTimeStamp();
+ final long endTime = System.currentTimeMillis();
+ accessLogEntry.setTimeStamp(startTime);
+ accessLogEntry.setDurationBetweenRequestResponse(endTime - startTime);
+ accessLogEntry.setReturnedContentSize(response.getContentCount());
+ accessLogEntry.setStatusCode(response.getStatus());
+
+ accessLog.log(accessLogEntry);
+ }
+
+ /*
+ * Collecting all log entry population based on extracting information from HttpServletRequest in one method
+ * means that this may easily be moved to another location, e.g. if we want to populate this at instantiation
+ * time rather than at logging time. We may, for example, want to set things such as http headers and ip
+ * addresses up-front and make it illegal for request handlers to modify these later.
+ */
+ public static void populateAccessLogEntryFromHttpServletRequest(
+ final HttpServletRequest request,
+ final AccessLogEntry accessLogEntry) {
+ final String quotedPath = request.getRequestURI();
+ final String quotedQuery = request.getQueryString();
+ try {
+ final StringBuilder uriBuffer = new StringBuilder();
+ uriBuffer.append(quotedPath);
+ if (quotedQuery != null) {
+ uriBuffer.append('?').append(quotedQuery);
+ }
+ final URI uri = new URI(uriBuffer.toString());
+ accessLogEntry.setURI(uri);
+ } catch (URISyntaxException e) {
+ setUriFromMalformedInput(accessLogEntry, quotedPath, quotedQuery);
+ }
+
+ final String remoteAddress = getRemoteAddress(request);
+ final int remotePort = getRemotePort(request);
+ final String peerAddress = request.getRemoteAddr();
+ final int peerPort = request.getRemotePort();
+
+ accessLogEntry.setUserAgent(request.getHeader("User-Agent"));
+ accessLogEntry.setHttpMethod(request.getMethod());
+ accessLogEntry.setHostString(request.getHeader("Host"));
+ accessLogEntry.setReferer(request.getHeader("Referer"));
+ accessLogEntry.setIpV4Address(peerAddress);
+ accessLogEntry.setRemoteAddress(remoteAddress);
+ accessLogEntry.setRemotePort(remotePort);
+ if (!Objects.equal(remoteAddress, peerAddress)) {
+ accessLogEntry.setPeerAddress(peerAddress);
+ }
+ if (remotePort != peerPort) {
+ accessLogEntry.setPeerPort(peerPort);
+ }
+ accessLogEntry.setHttpVersion(request.getProtocol());
+ }
+
+ private static String getRemoteAddress(final HttpServletRequest request) {
+ return Alternative.preferred(request.getHeader(HEADER_NAME_Y_RA))
+ .alternatively(() -> request.getHeader(HEADER_NAME_YAHOOREMOTEIP))
+ .alternatively(() -> request.getHeader(HEADER_NAME_X_FORWARDED_FOR))
+ .alternatively(() -> request.getHeader(HEADER_NAME_CLIENT_IP))
+ .orElseGet(request::getRemoteAddr);
+ }
+
+ private static int getRemotePort(final HttpServletRequest request) {
+ return Optional.ofNullable(request.getHeader(HEADER_NAME_Y_RP))
+ .map(Integer::valueOf)
+ .orElseGet(request::getRemotePort);
+ }
+
+ private static void setUriFromMalformedInput(final AccessLogEntry accessLogEntry, final String quotedPath, final String quotedQuery) {
+ try {
+ final String scheme = null;
+ final String authority = null;
+ final String fragment = null;
+ final URI uri = new URI(scheme, authority, unquote(quotedPath), unquote(quotedQuery), fragment);
+ accessLogEntry.setURI(uri);
+ } catch (URISyntaxException e) {
+ // I have no idea how this can happen here now...
+ logger.log(Level.WARNING, "Could not convert String URI to URI object", e);
+ }
+ }
+
+ private static String unquote(final String quotedQuery) {
+ if (quotedQuery == null) {
+ return null;
+ }
+ try {
+ // inconsistent handling of semi-colon added here...
+ return URLDecoder.decode(quotedQuery, StandardCharsets.UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ return quotedQuery;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java
new file mode 100644
index 00000000000..2d9e0558455
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.yahoo.container.logging.AccessLogEntry;
+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 com.yahoo.jdisc.http.HttpRequest;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A wrapper RequestHandler that enables access logging. By wrapping the request handler, we are able to wrap the
+ * response handler as well. Hence, we can populate the access log entry with information from both the request
+ * and the response. This wrapper also adds the access log entry to the request context, so that request handlers
+ * may add information to it.
+ *
+ * Does not otherwise interfere with the request processing of the delegate request handler.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ * $Id$
+ */
+public class AccessLoggingRequestHandler extends AbstractRequestHandler {
+ public static final String CONTEXT_KEY_ACCESS_LOG_ENTRY
+ = AccessLoggingRequestHandler.class.getName() + "_access-log-entry";
+
+ public static Optional<AccessLogEntry> getAccessLogEntry(final HttpRequest jdiscRequest) {
+ final Map<String, Object> requestContextMap = jdiscRequest.context();
+ return getAccessLogEntry(requestContextMap);
+ }
+
+ public static Optional<AccessLogEntry> getAccessLogEntry(final Map<String, Object> requestContextMap) {
+ return Optional.ofNullable(
+ (AccessLogEntry) requestContextMap.get(CONTEXT_KEY_ACCESS_LOG_ENTRY));
+ }
+
+ private final RequestHandler delegate;
+ private final AccessLogEntry accessLogEntry;
+
+ public AccessLoggingRequestHandler(
+ final RequestHandler delegateRequestHandler,
+ final AccessLogEntry accessLogEntry) {
+ this.delegate = delegateRequestHandler;
+ this.accessLogEntry = accessLogEntry;
+ }
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request);
+ final HttpRequest httpRequest = (HttpRequest) request;
+ httpRequest.context().put(CONTEXT_KEY_ACCESS_LOG_ENTRY, accessLogEntry);
+ final ResponseHandler accessLoggingResponseHandler = new AccessLoggingResponseHandler(handler, accessLogEntry);
+ final ContentChannel requestContentChannel = delegate.handleRequest(request, accessLoggingResponseHandler);
+ return requestContentChannel;
+ }
+
+ private static class AccessLoggingResponseHandler implements ResponseHandler {
+ private final ResponseHandler delegateHandler;
+ private final AccessLogEntry accessLogEntry;
+
+ public AccessLoggingResponseHandler(
+ final ResponseHandler delegateHandler,
+ final AccessLogEntry accessLogEntry) {
+ this.delegateHandler = delegateHandler;
+ this.accessLogEntry = accessLogEntry;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return delegateHandler.handleResponse(response);
+ }
+
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java
new file mode 100644
index 00000000000..267a53033ac
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Alternative.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Simple monad class, like Optional but with support for chaining alternatives in preferred order.
+ *
+ * Holds a current value (immutably), but if the current value is null provides an easy way to obtain an instance
+ * with another value, ad infinitum.
+ *
+ * Instances of this class are immutable and thread-safe.
+ *
+ * @author bakksjo
+ */
+public class Alternative<T> {
+ private final T value;
+
+ private Alternative(final T value) {
+ this.value = value;
+ }
+
+ /**
+ * Creates an instance with the supplied value.
+ */
+ public static <T> Alternative<T> preferred(final T value) {
+ return new Alternative<>(value);
+ }
+
+ /**
+ * Returns itself (unchanged) iff current value != null,
+ * otherwise returns a new instance with the value supplied by the supplier.
+ */
+ public Alternative<T> alternatively(final Supplier<? extends T> supplier) {
+ if (value != null) {
+ return this;
+ }
+
+ return new Alternative<>(supplier.get());
+ }
+
+ /**
+ * Returns the held value iff != null, otherwise invokes the supplier and returns its value.
+ */
+ public T orElseGet(final Supplier<? extends T> supplier) {
+ if (value != null) {
+ return value;
+ }
+ return supplier.get();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!(o instanceof Alternative<?>)) {
+ return false;
+ }
+
+ final Alternative<?> other = (Alternative<?>) o;
+
+ return Objects.equals(value, other.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java
new file mode 100644
index 00000000000..8d974639f47
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import java.io.IOException;
+
+/**
+ * Interface for async listeners only interested in onComplete.
+ * @author tonytv
+ */
+@FunctionalInterface
+interface AsyncCompleteListener extends AsyncListener {
+ @Override
+ default void onTimeout(AsyncEvent event) throws IOException {}
+
+ @Override
+ default void onError(AsyncEvent event) throws IOException {}
+
+ @Override
+ default void onStartAsync(AsyncEvent event) throws IOException {}
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java
new file mode 100644
index 00000000000..874d9ab7173
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java
@@ -0,0 +1,350 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.ConnectorConfig.Ssl;
+import com.yahoo.jdisc.http.ConnectorConfig.Ssl.PemKeyStore;
+import com.yahoo.jdisc.http.SecretStore;
+import com.yahoo.jdisc.http.ssl.ReaderForPath;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.ConnectorStatistics;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import javax.servlet.ServletRequest;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Field;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.google.common.io.Closeables.closeQuietly;
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.JKS;
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM;
+import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.21.0
+ */
+public class ConnectorFactory {
+
+ private final static Logger log = Logger.getLogger(ConnectorFactory.class.getName());
+ private final ConnectorConfig connectorConfig;
+ private final SslKeyStoreFactory sslKeyStoreFactory;
+ private final SecretStore secretStore;
+
+ @Inject
+ public ConnectorFactory(ConnectorConfig connectorConfig, SslKeyStoreFactory sslKeyStoreFactory, SecretStore secretStore) {
+ this.connectorConfig = connectorConfig;
+ this.sslKeyStoreFactory = sslKeyStoreFactory;
+ this.secretStore = secretStore;
+
+ if (connectorConfig.ssl().enabled())
+ validateSslConfig(connectorConfig);
+ }
+
+ // TODO: can be removed when we have dedicated SSL config in services.xml
+ private static void validateSslConfig(ConnectorConfig config) {
+ ConnectorConfig.Ssl ssl = config.ssl();
+
+ if (ssl.keyStoreType() == JKS) {
+ if (! ssl.pemKeyStore().keyPath().isEmpty()
+ || ! ssl.pemKeyStore().certificatePath().isEmpty())
+ throw new IllegalArgumentException(
+ "Setting pemKeyStore attributes does not make sense when keyStoreType==JKS.");
+ }
+ if (ssl.keyStoreType() == PEM) {
+ if (! ssl.keyStorePath().isEmpty())
+ throw new IllegalArgumentException(
+ "Setting keyStorePath does not make sense when keyStoreType==PEM");
+ }
+ }
+
+ public ConnectorConfig getConnectorConfig() {
+ return connectorConfig;
+ }
+
+ public ServerConnector createConnector(final Metric metric, final Server server, final ServerSocketChannel ch, Map<Path, FileChannel> keyStoreChannels) {
+ final ServerConnector connector;
+ if (connectorConfig.ssl().enabled()) {
+ connector = new JDiscServerConnector(connectorConfig, metric, server, ch,
+ newSslConnectionFactory(keyStoreChannels),
+ newHttpConnectionFactory());
+ } else {
+ connector = new JDiscServerConnector(connectorConfig, metric, server, ch,
+ newHttpConnectionFactory());
+ }
+ connector.setPort(connectorConfig.listenPort());
+ connector.setName(connectorConfig.name());
+ connector.setAcceptQueueSize(connectorConfig.acceptQueueSize());
+ connector.setReuseAddress(connectorConfig.reuseAddress());
+ connector.setSoLingerTime(connectorConfig.soLingerTime());
+ connector.setIdleTimeout((long)(connectorConfig.idleTimeout() * 1000.0));
+ connector.setStopTimeout((long)(connectorConfig.stopTimeout() * 1000.0));
+ return connector;
+ }
+
+ private HttpConnectionFactory newHttpConnectionFactory() {
+ final HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setSendDateHeader(true);
+ httpConfig.setSendServerVersion(false);
+ httpConfig.setSendXPoweredBy(false);
+ httpConfig.setHeaderCacheSize(connectorConfig.headerCacheSize());
+ httpConfig.setOutputBufferSize(connectorConfig.outputBufferSize());
+ httpConfig.setRequestHeaderSize(connectorConfig.requestHeaderSize());
+ httpConfig.setResponseHeaderSize(connectorConfig.responseHeaderSize());
+ if (connectorConfig.ssl().enabled()) {
+ httpConfig.addCustomizer(new SecureRequestCustomizer());
+ }
+ return new HttpConnectionFactory(httpConfig);
+ }
+
+ //TODO: does not support loading non-yahoo readable JKS key stores.
+ private SslConnectionFactory newSslConnectionFactory(Map<Path, FileChannel> keyStoreChannels) {
+ Ssl sslConfig = connectorConfig.ssl();
+
+ final SslContextFactory factory = new SslContextFactory();
+ if (!sslConfig.excludeProtocol().isEmpty()) {
+ final String[] prots = new String[sslConfig.excludeProtocol().size()];
+ for (int i = 0; i < prots.length; i++) {
+ prots[i] = sslConfig.excludeProtocol(i).name();
+ }
+ factory.setExcludeProtocols(prots);
+ }
+ if (!sslConfig.includeProtocol().isEmpty()) {
+ final String[] prots = new String[sslConfig.includeProtocol().size()];
+ for (int i = 0; i < prots.length; i++) {
+ prots[i] = sslConfig.includeProtocol(i).name();
+ }
+ factory.setIncludeProtocols(prots);
+ }
+ if (!sslConfig.excludeCipherSuite().isEmpty()) {
+ final String[] ciphs = new String[sslConfig.excludeCipherSuite().size()];
+ for (int i = 0; i < ciphs.length; i++) {
+ ciphs[i] = sslConfig.excludeCipherSuite(i).name();
+ }
+ factory.setExcludeCipherSuites(ciphs);
+
+ }
+ if (!sslConfig.includeCipherSuite().isEmpty()) {
+ final String[] ciphs = new String[sslConfig.includeCipherSuite().size()];
+ for (int i = 0; i < ciphs.length; i++) {
+ ciphs[i] = sslConfig.includeCipherSuite(i).name();
+ }
+ factory.setIncludeCipherSuites(ciphs);
+
+ }
+
+
+ Optional<String> password = Optional.of(sslConfig.keyDbKey()).
+ filter(key -> !key.isEmpty()).map(secretStore::getSecret);
+
+ switch (sslConfig.keyStoreType()) {
+ case PEM:
+ factory.setKeyStore(getKeyStore(sslConfig.pemKeyStore(), keyStoreChannels));
+ if (password.isPresent()) {
+ log.warning("Encrypted PEM key stores are not supported.");
+ }
+ break;
+ case JKS:
+ factory.setKeyStorePath(sslConfig.keyStorePath());
+ factory.setKeyStoreType(sslConfig.keyStoreType().toString());
+ factory.setKeyStorePassword(password.orElseThrow(passwordRequiredForJKSKeyStore("key")));
+ break;
+ }
+
+ if (!sslConfig.trustStorePath().isEmpty()) {
+ factory.setTrustStorePath(sslConfig.trustStorePath());
+ factory.setTrustStoreType(sslConfig.trustStoreType().toString());
+ factory.setTrustStorePassword(password.orElseThrow(passwordRequiredForJKSKeyStore("trust")));
+ }
+
+ factory.setSslKeyManagerFactoryAlgorithm(sslConfig.sslKeyManagerFactoryAlgorithm());
+ factory.setProtocol(sslConfig.protocol());
+ return new SslConnectionFactory(factory, HttpVersion.HTTP_1_1.asString());
+ }
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ private Supplier<RuntimeException> passwordRequiredForJKSKeyStore(String type) {
+ return () -> new RuntimeException(String.format("Password is required for JKS %s store", type));
+ }
+
+ private KeyStore getKeyStore(PemKeyStore pemKeyStore, Map<Path, FileChannel> keyStoreChannels) {
+ Preconditions.checkArgument(!pemKeyStore.certificatePath().isEmpty(), "Missing certificate path.");
+ Preconditions.checkArgument(!pemKeyStore.keyPath().isEmpty(), "Missing key path.");
+
+ class KeyStoreReaderForPath implements AutoCloseable {
+ private final Optional<FileChannel> channel;
+ public final ReaderForPath readerForPath;
+
+
+ KeyStoreReaderForPath(String pathString) {
+ Path path = Paths.get(pathString);
+ channel = Optional.ofNullable(keyStoreChannels.get(path));
+ readerForPath = new ReaderForPath(
+ channel.map(this::getReader).orElseGet(() -> getReader(path)),
+ path);
+ }
+
+ private Reader getReader(FileChannel channel) {
+ try {
+ channel.position(0);
+ return Channels.newReader(channel, StandardCharsets.UTF_8.newDecoder(), -1);
+ } catch (IOException e) {
+ throw throwUnchecked(e);
+ }
+
+ }
+
+ private Reader getReader(Path path) {
+ try {
+ return Files.newBufferedReader(path);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed opening " + path, e);
+ }
+ }
+
+ @Override
+ public void close() {
+ //channels are reused
+ if (!channel.isPresent()) {
+ closeQuietly(readerForPath.reader);
+ }
+ }
+ }
+
+ try (KeyStoreReaderForPath certificateReader = new KeyStoreReaderForPath(pemKeyStore.certificatePath());
+ KeyStoreReaderForPath keyReader = new KeyStoreReaderForPath(pemKeyStore.keyPath())) {
+ SslKeyStore keyStore = sslKeyStoreFactory.createKeyStore(certificateReader.readerForPath,
+ keyReader.readerForPath);
+ return keyStore.loadJavaKeyStore();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed setting up key store for " + pemKeyStore.keyPath() + ", " + pemKeyStore.certificatePath(), e);
+ }
+ }
+
+ public static class JDiscServerConnector extends ServerConnector {
+ public static final String REQUEST_ATTRIBUTE = JDiscServerConnector.class.getName();
+ private final static Logger log = Logger.getLogger(JDiscServerConnector.class.getName());
+ private final Metric.Context metricCtx;
+ private final ConnectorStatistics statistics;
+ private final boolean tcpKeepAlive;
+ private final boolean tcpNoDelay;
+ private final ServerSocketChannel channelOpenedByActivator;
+
+ private JDiscServerConnector(
+ final ConnectorConfig config,
+ final Metric metric,
+ final Server server,
+ final ServerSocketChannel channelOpenedByActivator,
+ final ConnectionFactory... factories) {
+ super(server, factories);
+ this.channelOpenedByActivator = channelOpenedByActivator;
+ this.tcpKeepAlive = config.tcpKeepAliveEnabled();
+ this.tcpNoDelay = config.tcpNoDelay();
+ this.metricCtx = createMetricContext(config, metric);
+
+ this.statistics = new ConnectorStatistics();
+ addBean(statistics);
+ }
+
+ private Metric.Context createMetricContext(ConnectorConfig config, Metric metric) {
+ Map<String, Object> props = new TreeMap<>();
+ props.put(JettyHttpServer.Metrics.NAME_DIMENSION, config.name());
+ props.put(JettyHttpServer.Metrics.PORT_DIMENSION, config.listenPort());
+ return metric.createContext(props);
+ }
+
+ @Override
+ protected void configure(final Socket socket) {
+ super.configure(socket);
+ try {
+ socket.setKeepAlive(tcpKeepAlive);
+ socket.setTcpNoDelay(tcpNoDelay);
+ } catch (final SocketException ignored) {
+
+ }
+ }
+
+ @Override
+ public void open() throws IOException {
+ if (channelOpenedByActivator == null) {
+ log.log(Level.INFO, "No channel set by activator, opening channel ourselves.");
+ try {
+ super.open();
+ } catch (RuntimeException e) {
+ log.log(Level.SEVERE, "failed org.eclipse.jetty.server.Server open() with port "+getPort());
+ throw e;
+ }
+ return;
+ }
+ log.log(Level.INFO, "Using channel set by activator: " + channelOpenedByActivator);
+
+ channelOpenedByActivator.socket().setReuseAddress(getReuseAddress());
+ int localPort = channelOpenedByActivator.socket().getLocalPort();
+ try {
+ uglySetLocalPort(localPort);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException("Could not set local port.", e);
+ }
+ if (localPort <= 0) {
+ throw new IOException("Server channel not bound");
+ }
+ addBean(channelOpenedByActivator);
+ channelOpenedByActivator.configureBlocking(true);
+ addBean(channelOpenedByActivator);
+
+ try {
+ uglySetChannel(channelOpenedByActivator);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException("Could not set server channel.", e);
+ }
+ }
+
+ private void uglySetLocalPort(int localPort) throws NoSuchFieldException, IllegalAccessException {
+ Field localPortField = ServerConnector.class.getDeclaredField("_localPort");
+ localPortField.setAccessible(true);
+ localPortField.set(this, localPort);
+ }
+
+ private void uglySetChannel(ServerSocketChannel channelOpenedByActivator) throws NoSuchFieldException, IllegalAccessException {
+ Field acceptChannelField = ServerConnector.class.getDeclaredField("_acceptChannel");
+ acceptChannelField.setAccessible(true);
+ acceptChannelField.set(this, channelOpenedByActivator);
+ }
+
+ public ConnectorStatistics getStatistics() { return statistics; }
+
+ public Metric.Context getMetricContext() { return metricCtx; }
+
+ public static JDiscServerConnector fromRequest(ServletRequest request) {
+ return (JDiscServerConnector)request.getAttribute(REQUEST_ATTRIBUTE);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java
new file mode 100644
index 00000000000..2b28d866f2f
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+/**
+ * A wrapper to make exceptions leaking into Jetty easier to track. Jetty
+ * swallows all information about where an exception was thrown, so this wrapper
+ * ensures some extra information is automatically added to the contents of
+ * getMessage().
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class ExceptionWrapper extends RuntimeException {
+ private final String message;
+
+ /**
+ * Update if serializable contents are added.
+ */
+ private static final long serialVersionUID = 1L;
+
+ public ExceptionWrapper(Throwable t) {
+ super(t);
+ this.message = formatMessage(t);
+ }
+
+ // If calling methods from the constructor, it makes life easier if the
+ // methods are static...
+ private static String formatMessage(final Throwable t) {
+ StringBuilder b = new StringBuilder();
+ Throwable cause = t;
+ while (cause != null) {
+ StackTraceElement[] trace = cause.getStackTrace();
+ String currentMsg = cause.getMessage();
+
+ if (b.length() > 0) {
+ b.append(": ");
+ }
+ b.append(t.getClass().getSimpleName()).append('(');
+ if (currentMsg != null) {
+ b.append('"').append(currentMsg).append('"');
+ }
+ b.append(')');
+ if (trace.length > 0) {
+ b.append(" at ").append(trace[0].getClassName()).append('(');
+ if (trace[0].getFileName() != null) {
+ b.append(trace[0].getFileName()).append(':')
+ .append(trace[0].getLineNumber());
+ }
+ b.append(')');
+ }
+ cause = cause.getCause();
+ }
+ return b.toString();
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java
new file mode 100644
index 00000000000..3c7908356d4
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/Exceptions.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+/**
+ * Utility methods for exceptions
+ *
+ * @author tonytv
+ */
+public class Exceptions {
+
+ /**
+ * Allows treating checked exceptions as unchecked.
+ * Usage:
+ * throw throwUnchecked(e);
+ * The reason for the return type is to allow writing throw at the call site
+ * instead of just calling throwUnchecked. Just calling throwUnchecked
+ * means that the java compiler won't know that the statement will throw an exception,
+ * and will therefore complain on things such e.g. missing return value.
+ */
+ public static RuntimeException throwUnchecked(Throwable e) {
+ throwUncheckedImpl(e);
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T extends Throwable> void throwUncheckedImpl(Throwable t) throws T {
+ throw (T)t;
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java
new file mode 100644
index 00000000000..ef8698ff4f1
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. 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.ImplementedBy;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+
+/**
+ * Separate interface since DiscFilterRequest/Response and Security filter chains are not accessible in this bundle
+ */
+@ImplementedBy(UnsupportedFilterInvoker.class)
+public interface FilterInvoker {
+ HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain,
+ URI uri,
+ HttpServletRequest httpRequest,
+ ResponseHandler responseHandler);
+
+ void invokeResponseFilterChain(
+ ResponseFilter responseFilterChain,
+ URI uri,
+ HttpServletRequest request,
+ HttpServletResponse response);
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java
new file mode 100644
index 00000000000..d787b7294b2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java
@@ -0,0 +1,266 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+/**
+ * Invokes the response filter the first time anything is output to the underlying PrintWriter.
+ * The filter must be invoked before the first output call since this might cause the response
+ * to be committed, i.e. locked and potentially put on the wire.
+ * Any changes to the response after it has been committed might be ignored or cause exceptions.
+ * @author tonytv
+ */
+final class FilterInvokingPrintWriter extends PrintWriter {
+ private final PrintWriter delegate;
+ private final OneTimeRunnable filterInvoker;
+
+ public FilterInvokingPrintWriter(PrintWriter delegate, OneTimeRunnable filterInvoker) {
+ /* The PrintWriter class both
+ * 1) exposes new methods, the PrintWriter "interface"
+ * 2) implements PrintWriter and Writer methods that does some extra things before calling down to the writer methods.
+ * If super was invoked with the delegate PrintWriter, the superclass would behave as a PrintWriter(PrintWriter),
+ * i.e. the extra things in 2. would be done twice.
+ * To avoid this, all the methods of PrintWriter are overridden with versions that forward directly to the underlying delegate
+ * instead of going through super.
+ * The super class is initialized with a non-functioning writer to catch mistakenly non-overridden methods.
+ */
+ super(new Writer() {
+ @Override
+ public void write(char[] cbuf, int off, int len) throws IOException {
+ throwAssertionError();
+ }
+
+ private void throwAssertionError() {
+ throw new AssertionError(FilterInvokingPrintWriter.class.getName() + " failed to delegate to the underlying writer");
+ }
+
+ @Override
+ public void flush() throws IOException {
+ throwAssertionError();
+ }
+
+ @Override
+ public void close() throws IOException {
+ throwAssertionError();
+ }
+ });
+
+ this.delegate = delegate;
+ this.filterInvoker = filterInvoker;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getName() + " (" + super.toString() + ")";
+ }
+
+ private void runFilterIfFirstInvocation() {
+ filterInvoker.runIfFirstInvocation();
+ }
+
+ @Override
+ public void flush() {
+ runFilterIfFirstInvocation();
+ delegate.flush();
+ }
+
+ @Override
+ public void close() {
+ runFilterIfFirstInvocation();
+ delegate.close();
+ }
+
+ @Override
+ public boolean checkError() {
+ return delegate.checkError();
+ }
+
+ @Override
+ public void write(int c) {
+ runFilterIfFirstInvocation();
+ delegate.write(c);
+ }
+
+ @Override
+ public void write(char[] buf, int off, int len) {
+ runFilterIfFirstInvocation();
+ delegate.write(buf, off, len);
+ }
+
+ @Override
+ public void write(char[] buf) {
+ runFilterIfFirstInvocation();
+ delegate.write(buf);
+ }
+
+ @Override
+ public void write(String s, int off, int len) {
+ runFilterIfFirstInvocation();
+ delegate.write(s, off, len);
+ }
+
+ @Override
+ public void write(String s) {
+ runFilterIfFirstInvocation();
+ delegate.write(s);
+ }
+
+ @Override
+ public void print(boolean b) {
+ runFilterIfFirstInvocation();
+ delegate.print(b);
+ }
+
+ @Override
+ public void print(char c) {
+ runFilterIfFirstInvocation();
+ delegate.print(c);
+ }
+
+ @Override
+ public void print(int i) {
+ runFilterIfFirstInvocation();
+ delegate.print(i);
+ }
+
+ @Override
+ public void print(long l) {
+ runFilterIfFirstInvocation();
+ delegate.print(l);
+ }
+
+ @Override
+ public void print(float f) {
+ runFilterIfFirstInvocation();
+ delegate.print(f);
+ }
+
+ @Override
+ public void print(double d) {
+ runFilterIfFirstInvocation();
+ delegate.print(d);
+ }
+
+ @Override
+ public void print(char[] s) {
+ runFilterIfFirstInvocation();
+ delegate.print(s);
+ }
+
+ @Override
+ public void print(String s) {
+ runFilterIfFirstInvocation();
+ delegate.print(s);
+ }
+
+ @Override
+ public void print(Object obj) {
+ runFilterIfFirstInvocation();
+ delegate.print(obj);
+ }
+
+ @Override
+ public void println() {
+ runFilterIfFirstInvocation();
+ delegate.println();
+ }
+
+ @Override
+ public void println(boolean x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(char x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(int x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(long x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(float x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(double x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(char[] x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(String x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public void println(Object x) {
+ runFilterIfFirstInvocation();
+ delegate.println(x);
+ }
+
+ @Override
+ public PrintWriter printf(String format, Object... args) {
+ runFilterIfFirstInvocation();
+ return delegate.printf(format, args);
+ }
+
+ @Override
+ public PrintWriter printf(Locale l, String format, Object... args) {
+ runFilterIfFirstInvocation();
+ return delegate.printf(l, format, args);
+ }
+
+ @Override
+ public PrintWriter format(String format, Object... args) {
+ runFilterIfFirstInvocation();
+ return delegate.format(format, args);
+ }
+
+ @Override
+ public PrintWriter format(Locale l, String format, Object... args) {
+ runFilterIfFirstInvocation();
+ return delegate.format(l, format, args);
+ }
+
+ @Override
+ public PrintWriter append(CharSequence csq) {
+ runFilterIfFirstInvocation();
+ return delegate.append(csq);
+ }
+
+ @Override
+ public PrintWriter append(CharSequence csq, int start, int end) {
+ runFilterIfFirstInvocation();
+ return delegate.append(csq, start, end);
+ }
+
+ @Override
+ public PrintWriter append(char c) {
+ runFilterIfFirstInvocation();
+ return delegate.append(c);
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java
new file mode 100644
index 00000000000..6a36dbfc6b6
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import java.io.IOException;
+
+/**
+ * Invokes the response filter the first time anything is output to the underlying ServletOutputStream.
+ * The filter must be invoked before the first output call since this might cause the response
+ * to be committed, i.e. locked and potentially put on the wire.
+ * Any changes to the response after it has been committed might be ignored or cause exceptions.
+ *
+ * @author tonytv
+ */
+class FilterInvokingServletOutputStream extends ServletOutputStream {
+ private final ServletOutputStream delegate;
+ private final OneTimeRunnable filterInvoker;
+
+ public FilterInvokingServletOutputStream(ServletOutputStream delegate, OneTimeRunnable filterInvoker) {
+ this.delegate = delegate;
+ this.filterInvoker = filterInvoker;
+ }
+
+ @Override
+ public boolean isReady() {
+ return delegate.isReady();
+ }
+
+ @Override
+ public void setWriteListener(WriteListener writeListener) {
+ delegate.setWriteListener(writeListener);
+ }
+
+
+ private void runFilterIfFirstInvocation() {
+ filterInvoker.runIfFirstInvocation();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.write(b);
+ }
+
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.write(b);
+ }
+
+ @Override
+ public void print(String s) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(s);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.write(b, off, len);
+ }
+
+ @Override
+ public void print(boolean b) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(b);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.flush();
+ }
+
+ @Override
+ public void print(char c) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(c);
+ }
+
+ @Override
+ public void close() throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.close();
+ }
+
+ @Override
+ public void print(int i) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(i);
+ }
+
+ @Override
+ public void print(long l) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(l);
+ }
+
+ @Override
+ public void print(float f) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(f);
+ }
+
+ @Override
+ public void print(double d) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.print(d);
+ }
+
+ @Override
+ public void println() throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println();
+ }
+
+ @Override
+ public void println(String s) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(s);
+ }
+
+ @Override
+ public void println(boolean b) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(b);
+ }
+
+ @Override
+ public void println(char c) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(c);
+ }
+
+ @Override
+ public void println(int i) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(i);
+ }
+
+ @Override
+ public void println(long l) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(l);
+ }
+
+ @Override
+ public void println(float f) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(f);
+ }
+
+ @Override
+ public void println(double d) throws IOException {
+ runFilterIfFirstInvocation();
+ delegate.println(d);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getCanonicalName() + " (" + delegate.toString() + ")";
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java
new file mode 100644
index 00000000000..b8073bc6ab5
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java
@@ -0,0 +1,132 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingSet;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDeniedException;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.core.CompletionHandlers;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Request handler that invokes request and response filters in addition to the bound request handler.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ * $Id$
+ */
+class FilteringRequestHandler extends AbstractRequestHandler {
+ private static final ContentChannel COMPLETING_CONTENT_CHANNEL = new ContentChannel() {
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ CompletionHandlers.tryComplete(handler);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ CompletionHandlers.tryComplete(handler);
+ }
+ };
+
+ private final BindingSet<RequestFilter> requestFilters;
+ private final BindingSet<ResponseFilter> responseFilters;
+
+ public FilteringRequestHandler(
+ final BindingSet<RequestFilter> requestFilters,
+ final BindingSet<ResponseFilter> responseFilters) {
+ this.requestFilters = requestFilters;
+ this.responseFilters = responseFilters;
+ }
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler originalResponseHandler) {
+ Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request);
+ Objects.requireNonNull(originalResponseHandler, "responseHandler");
+
+ final RequestFilter requestFilter = requestFilters.resolve(request.getUri());
+ final ResponseFilter responseFilter = responseFilters.resolve(request.getUri());
+ // Not using request.connect() here - it adds logic for error handling that we'd rather leave to the framework.
+ final RequestHandler resolvedRequestHandler = request.container().resolveHandler(request);
+
+ if (resolvedRequestHandler == null) {
+ throw new BindingNotFoundException(request.getUri());
+ }
+
+ final RequestHandler requestHandler = new ReferenceCountingRequestHandler(resolvedRequestHandler);
+
+ final ResponseHandler responseHandler;
+ if (responseFilter != null) {
+ responseHandler = new FilteringResponseHandler(originalResponseHandler, responseFilter, request);
+ } else {
+ responseHandler = originalResponseHandler;
+ }
+
+ if (requestFilter != null) {
+ final InterceptingResponseHandler interceptingResponseHandler
+ = new InterceptingResponseHandler(responseHandler);
+ requestFilter.filter(HttpRequest.class.cast(request), interceptingResponseHandler);
+ if (interceptingResponseHandler.hasProducedResponse()) {
+ return COMPLETING_CONTENT_CHANNEL;
+ }
+ }
+
+ final ContentChannel contentChannel = requestHandler.handleRequest(request, responseHandler);
+ if (contentChannel == null) {
+ throw new RequestDeniedException(request);
+ }
+ return contentChannel;
+ }
+
+ private static class FilteringResponseHandler implements ResponseHandler {
+ private final ResponseHandler delegate;
+ private final ResponseFilter responseFilter;
+ private final Request request;
+
+ public FilteringResponseHandler(
+ final ResponseHandler delegate,
+ final ResponseFilter responseFilter,
+ final Request request) {
+ this.delegate = Objects.requireNonNull(delegate);
+ this.responseFilter = Objects.requireNonNull(responseFilter);
+ this.request = request;
+ }
+
+ @Override
+ public ContentChannel handleResponse(final Response response) {
+ responseFilter.filter(response, request);
+ return delegate.handleResponse(response);
+ }
+ }
+
+ private static class InterceptingResponseHandler implements ResponseHandler {
+ private final ResponseHandler delegate;
+ private AtomicBoolean hasResponded = new AtomicBoolean(false);
+
+ public InterceptingResponseHandler(final ResponseHandler delegate) {
+ this.delegate = Objects.requireNonNull(delegate);
+ }
+
+ @Override
+ public ContentChannel handleResponse(final Response response) {
+ final ContentChannel content = delegate.handleResponse(response);
+ hasResponded.set(true);
+ return content;
+ }
+
+ public boolean hasProducedResponse() {
+ return hasResponded.get();
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
new file mode 100644
index 00000000000..b0f336e876c
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
@@ -0,0 +1,190 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+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.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE;
+
+/**
+ * Request handler that wraps POST requests of application/x-www-form-urlencoded data.
+ *
+ * The wrapper defers invocation of the "real" request handler until it has read the request content (body),
+ * parsed the form parameters and merged them into the request's parameters.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ * $Id$
+ */
+class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel {
+ private static final CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() {
+ @Override public void completed() {}
+ @Override public void failed(final Throwable t) {}
+ };
+
+ private final ByteArrayOutputStream accumulatedRequestContent = new ByteArrayOutputStream();
+ private final RequestHandler delegateHandler;
+ private final String contentCharsetName;
+ private final boolean removeBody;
+
+ private Charset contentCharset;
+ private HttpRequest request;
+ private ResourceReference requestReference;
+ private ResponseHandler responseHandler;
+
+ /**
+ * @param delegateHandler the "real" request handler that this handler wraps
+ * @param contentCharsetName name of the charset to use when interpreting the content data
+ */
+ public FormPostRequestHandler(
+ final RequestHandler delegateHandler,
+ final String contentCharsetName,
+ final boolean removeBody) {
+ this.delegateHandler = Objects.requireNonNull(delegateHandler);
+ this.contentCharsetName = Objects.requireNonNull(contentCharsetName);
+ this.removeBody = removeBody;
+ }
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler responseHandler) {
+ Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request);
+ Objects.requireNonNull(responseHandler, "responseHandler");
+
+ this.contentCharset = getCharsetByName(contentCharsetName);
+ this.responseHandler = responseHandler;
+ this.request = (HttpRequest) request;
+ this.requestReference = request.refer();
+
+ return this;
+ }
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler completionHandler) {
+ assert buf.hasArray();
+ accumulatedRequestContent.write(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
+ completionHandler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler completionHandler) {
+ try (final ResourceReference ref = requestReference) {
+ final byte[] requestContentBytes = accumulatedRequestContent.toByteArray();
+ final String content = new String(requestContentBytes, contentCharset);
+ completionHandler.completed();
+ final Map<String, List<String>> parameterMap = parseFormParameters(content);
+ mergeParameters(parameterMap, request.parameters());
+ final ContentChannel contentChannel = delegateHandler.handleRequest(request, responseHandler);
+ if (contentChannel != null) {
+ if (!removeBody) {
+ final ByteBuffer byteBuffer = ByteBuffer.wrap(requestContentBytes);
+ contentChannel.write(byteBuffer, NOOP_COMPLETION_HANDLER);
+ }
+ contentChannel.close(NOOP_COMPLETION_HANDLER);
+ }
+ }
+ }
+
+ /**
+ * Looks up a Charset given a charset name.
+ *
+ * @param charsetName the name of the charset to look up
+ * @return a valid Charset for the charset name (never returns null)
+ * @throws RequestException if the charset name is invalid or unsupported
+ */
+ private static Charset getCharsetByName(final String charsetName) throws RequestException {
+ try {
+ final Charset charset = Charset.forName(charsetName);
+ if (charset == null) {
+ throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName);
+ }
+ return charset;
+ } catch (final IllegalCharsetNameException |UnsupportedCharsetException e) {
+ throw new RequestException(UNSUPPORTED_MEDIA_TYPE, "Unsupported charset " + charsetName, e);
+ }
+ }
+
+ /**
+ * Parses application/x-www-form-urlencoded data into a map of parameters.
+ *
+ * @param formContent raw form content data (body)
+ * @return map of decoded parameters
+ */
+ private static Map<String, List<String>> parseFormParameters(final String formContent) {
+ if (formContent.isEmpty()) {
+ return Collections.emptyMap();
+ }
+
+ final Map<String, List<String>> parameterMap = new HashMap<>();
+ final String[] params = formContent.split("&");
+ for (final String param : params) {
+ final String[] parts = param.split("=");
+ final String paramName = urlDecode(parts[0]);
+ final String paramValue = parts.length > 1 ? urlDecode(parts[1]) : "";
+ List<String> currentValues = parameterMap.get(paramName);
+ if (currentValues == null) {
+ currentValues = new LinkedList<>();
+ parameterMap.put(paramName, currentValues);
+ }
+ currentValues.add(paramValue);
+ }
+ return parameterMap;
+ }
+
+ /**
+ * Percent-decoding method that doesn't throw.
+ *
+ * @param encoded percent-encoded data
+ * @return decoded data
+ */
+ private static String urlDecode(final String encoded) {
+ try {
+ // Regardless of the charset used to transfer the request body,
+ // all percent-escaping of non-ascii characters should use UTF-8 code points.
+ return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
+ } catch (final UnsupportedEncodingException e) {
+ // Unfortunately, there is no URLDecoder.decode() method that takes a Charset, so we have to deal
+ // with this exception.
+ throw new IllegalStateException("Whoa, JVM doesn't support UTF-8 today.", e);
+ }
+ }
+
+ /**
+ * Merges source parameters into a destination map.
+ *
+ * @param source containing the parameters to copy into the destination
+ * @param destination receiver of parameters, possibly already containing data
+ */
+ private static void mergeParameters(
+ final Map<String,List<String>> source,
+ final Map<String,List<String>> destination) {
+ for (Map.Entry<String, List<String>> entry : source.entrySet()) {
+ final List<String> destinationValues = destination.get(entry.getKey());
+ if (destinationValues != null) {
+ destinationValues.addAll(entry.getValue());
+ } else {
+ destination.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java
new file mode 100644
index 00000000000..e9aba0cb6c9
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java
@@ -0,0 +1,210 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.Metric.Context;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.OverloadException;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED;
+import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class HttpRequestDispatch {
+ private static final Logger log = Logger.getLogger(HttpRequestDispatch.class.getName());
+
+ private final static String CHARSET_ANNOTATION = ";charset=";
+
+ private final JDiscContext jDiscContext;
+ private final AsyncContext async;
+ private final HttpServletRequest servletRequest;
+
+ private final ServletResponseController servletResponseController;
+ private final RequestHandler requestHandler;
+ private final MetricReporter metricReporter;
+
+ public HttpRequestDispatch(
+ final JDiscContext jDiscContext,
+ final AccessLogEntry accessLogEntry,
+ final Context metricContext,
+ final HttpServletRequest servletRequest,
+ final HttpServletResponse servletResponse) throws IOException {
+ this.jDiscContext = jDiscContext;
+
+ requestHandler = newRequestHandler(jDiscContext, accessLogEntry, servletRequest);
+
+ this.metricReporter = new MetricReporter(jDiscContext.metric, metricContext,
+ ((org.eclipse.jetty.server.Request) servletRequest).getTimeStamp());
+ this.servletRequest = servletRequest;
+
+ this.servletResponseController = new ServletResponseController(
+ servletResponse,
+ jDiscContext.janitor,
+ metricReporter,
+ jDiscContext.developerMode());
+
+ this.async = servletRequest.startAsync();
+ async.setTimeout(0);
+ }
+
+ public void dispatch() throws IOException {
+ final ServletRequestReader servletRequestReader;
+ try {
+ servletRequestReader = handleRequest();
+ } catch (Throwable throwable) {
+ servletResponseController.trySendError(throwable);
+ servletResponseController.finishedFuture().whenComplete((result, exception) ->
+ completeRequestCallback.accept(null, throwable));
+ return;
+ }
+
+ try {
+ onError(servletRequestReader.finishedFuture,
+ servletResponseController::trySendError);
+
+ onError(servletResponseController.finishedFuture(),
+ servletRequestReader::onError);
+
+ CompletableFuture.allOf(servletRequestReader.finishedFuture, servletResponseController.finishedFuture())
+ .whenComplete(completeRequestCallback);
+ } catch (Throwable throwable) {
+ log.log(Level.WARNING, "Failed registering finished listeners.", throwable);
+ }
+ }
+
+ private BiConsumer<Void, Throwable> completeRequestCallback;
+ {
+ AtomicBoolean completeRequestCalled = new AtomicBoolean(false);
+ HttpRequestDispatch parent = this; //used to avoid binding uninitialized variables
+
+ completeRequestCallback = (result, error) -> {
+ boolean reportedError = false;
+
+ if (error != null) {
+ if (!(error instanceof OverloadException || error instanceof BindingNotFoundException)) {
+ log.log(Level.WARNING, "Request failed: " + parent.servletRequest.getRequestURI(), error);
+ }
+ reportedError = true;
+ parent.metricReporter.failedResponse();
+ } else {
+ parent.metricReporter.successfulResponse();
+ }
+
+
+ boolean alreadyCalled = completeRequestCalled.getAndSet(true);
+ if (alreadyCalled) {
+ AssertionError e = new AssertionError("completeRequest called more than once");
+ log.log(Level.WARNING, "Assertion failed.", e);
+ throw e;
+ }
+
+ try {
+ parent.async.complete();
+ log.finest(() -> "Request completed successfully: " + parent.servletRequest.getRequestURI());
+ } catch (Throwable throwable) {
+ Level level = reportedError ? Level.FINE: Level.WARNING;
+ log.log(level, "async.complete failed", throwable);
+ }
+ };
+ }
+
+ private ServletRequestReader handleRequest() throws IOException {
+ HttpRequest jdiscRequest = HttpRequestFactory.newJDiscRequest(jDiscContext.container, servletRequest);
+ final ContentChannel requestContentChannel;
+
+ try (ResourceReference ref = References.fromResource(jdiscRequest)) {
+ HttpRequestFactory.copyHeaders(servletRequest, jdiscRequest);
+ requestContentChannel = requestHandler.handleRequest(jdiscRequest, servletResponseController.responseHandler);
+ }
+
+ ServletInputStream servletInputStream = servletRequest.getInputStream();
+
+ ServletRequestReader servletRequestReader =
+ new ServletRequestReader(
+ servletInputStream,
+ requestContentChannel,
+ jDiscContext.janitor,
+ metricReporter);
+
+ servletInputStream.setReadListener(servletRequestReader);
+ return servletRequestReader;
+ }
+
+ private static void onError(CompletableFuture<?> future, Consumer<Throwable> errorHandler) {
+ future.whenComplete((result, exception) -> {
+ if (exception != null) {
+ errorHandler.accept(exception);
+ }
+ });
+ }
+
+ ContentChannel handleRequestFilterResponse(Response response) {
+ try {
+ servletRequest.getInputStream().close();
+ ContentChannel responseContentChannel = servletResponseController.responseHandler.handleResponse(response);
+ servletResponseController.finishedFuture().whenComplete(completeRequestCallback);
+ return responseContentChannel;
+ } catch (IOException e) {
+ throw throwUnchecked(e);
+ }
+ }
+
+
+ private static RequestHandler newRequestHandler(
+ final JDiscContext context,
+ final AccessLogEntry accessLogEntry,
+ final HttpServletRequest servletRequest) {
+ final RequestHandler requestHandler = wrapHandlerIfFormPost(
+ new FilteringRequestHandler(context.requestFilters, context.responseFilters),
+ servletRequest, context.serverConfig.removeRawPostBodyForWwwUrlEncodedPost());
+
+ return new AccessLoggingRequestHandler(requestHandler, accessLogEntry);
+ }
+
+ private static RequestHandler wrapHandlerIfFormPost(
+ final RequestHandler requestHandler,
+ final HttpServletRequest servletRequest,
+ final boolean removeBodyForFormPost) {
+ if (!servletRequest.getMethod().equals("POST")) {
+ return requestHandler;
+ }
+ final String contentType = servletRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE);
+ if (contentType == null) {
+ return requestHandler;
+ }
+ if (!contentType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED)) {
+ return requestHandler;
+ }
+ return new FormPostRequestHandler(requestHandler, getCharsetName(contentType), removeBodyForFormPost);
+ }
+
+ private static String getCharsetName(final String contentType) {
+ if (!contentType.startsWith(CHARSET_ANNOTATION, APPLICATION_X_WWW_FORM_URLENCODED.length())) {
+ return StandardCharsets.UTF_8.name();
+ }
+ return contentType.substring(APPLICATION_X_WWW_FORM_URLENCODED.length() + CHARSET_ANNOTATION.length());
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java
new file mode 100644
index 00000000000..f1c36ffa80f
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. 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.Response;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import javax.servlet.http.HttpServletRequest;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Enumeration;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class HttpRequestFactory {
+ public static HttpRequest newJDiscRequest(final CurrentContainer container,
+ final HttpServletRequest servletRequest) {
+ return HttpRequest.newServerRequest(
+ container,
+ getUri(servletRequest),
+ HttpRequest.Method.valueOf(servletRequest.getMethod()),
+ HttpRequest.Version.fromString(servletRequest.getProtocol()),
+ new InetSocketAddress(servletRequest.getRemoteAddr(), servletRequest.getRemotePort()));
+ }
+
+ public static URI getUri(HttpServletRequest servletRequest) {
+ String query = extraQuote(servletRequest.getQueryString());
+ try {
+ return URI.create(servletRequest.getRequestURL() + (query != null ? '?' + query : ""));
+ } catch (IllegalArgumentException e) {
+ throw new RequestException(Response.Status.BAD_REQUEST, "Query violates RFC 2396", e);
+ }
+ }
+
+ public static void copyHeaders(final HttpServletRequest from,
+ final HttpRequest to) {
+ for (final Enumeration<String> it = from.getHeaderNames(); it.hasMoreElements(); ) {
+ final String key = it.nextElement();
+ for (final Enumeration<String> value = from.getHeaders(key); value.hasMoreElements(); ) {
+ to.headers().add(key, value.nextElement());
+ }
+ }
+ }
+
+ private static String extraQuote(String queryString) {
+ // TODO this is just a stopgap measure, we need some sort of sane URI builder, do we have one?
+ String washed = null;
+ if (queryString == null) {
+ return null;
+ }
+
+ int toAndIncluding = -1;
+ for (int i = 0; i < queryString.length(); ++i) {
+ if (quote(queryString.charAt(i)) != null) {
+ break;
+ }
+ toAndIncluding = i;
+ }
+
+ if (toAndIncluding != (queryString.length() - 1)) {
+ StringBuilder w = new StringBuilder(queryString.substring(0, toAndIncluding + 1));
+ for (int i = toAndIncluding + 1; i < queryString.length(); ++i) {
+ String s = quote(queryString.charAt(i));
+ if (s == null) {
+ w.append(queryString.charAt(i));
+ } else {
+ w.append(s);
+ }
+ }
+ washed = w.toString();
+ } else {
+ washed = queryString;
+ }
+ return washed;
+ }
+
+ private static String quote(char c) {
+ switch(c) {
+ case '\\':
+ return "%5C";
+ case '^':
+ return "%5E";
+ case '{':
+ return "%7B";
+ case '|':
+ return "%7C";
+ case '}':
+ return "%7D";
+ default:
+ return null;
+ }
+
+ }
+
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java
new file mode 100644
index 00000000000..1f7adbde329
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. 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.application.BindingSet;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.util.concurrent.Executor;
+
+public class JDiscContext {
+ final BindingSet<RequestFilter> requestFilters;
+ final BindingSet<ResponseFilter> responseFilters;
+ final CurrentContainer container;
+ final Executor janitor;
+ final Metric metric;
+ final ServerConfig serverConfig;
+
+ public JDiscContext(BindingSet<RequestFilter> requestFilters,
+ BindingSet<ResponseFilter> responseFilters,
+ CurrentContainer container,
+ Executor janitor,
+ Metric metric,
+ ServerConfig serverConfig) {
+
+ this.requestFilters = requestFilters;
+ this.responseFilters = responseFilters;
+ this.container = container;
+ this.janitor = janitor;
+ this.metric = metric;
+ this.serverConfig = serverConfig;
+ }
+
+ public boolean developerMode() {
+ return serverConfig.developerMode();
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java
new file mode 100644
index 00000000000..546f59f53f2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java
@@ -0,0 +1,287 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncListener;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector;
+import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked;
+
+/**
+ * Runs JDisc security filters for Servlets
+ * This component is split in two due to external dependencies:
+ * 1) JDiscFilterInvokerFilter, which uses package private methods to support JDisc APIs
+ * 2) SecurityFilterInvoker, which uses Security filter classes and therefore must reside in jdisc_http_filters
+ *
+ * @author tonytv
+ */
+class JDiscFilterInvokerFilter implements Filter {
+ private final JDiscContext jDiscContext;
+ private final FilterInvoker filterInvoker;
+
+ public JDiscFilterInvokerFilter(JDiscContext jDiscContext,
+ FilterInvoker filterInvoker) {
+ this.jDiscContext = jDiscContext;
+ this.filterInvoker = filterInvoker;
+ }
+
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {}
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpRequest = (HttpServletRequest)request;
+ HttpServletResponse httpResponse = (HttpServletResponse)response;
+
+ URI uri = HttpRequestFactory.getUri(httpRequest);
+
+ AtomicReference<Boolean> responseReturned = new AtomicReference<>(null);
+
+ HttpServletRequest newRequest = runRequestFilterWithMatchingBinding(responseReturned, uri, httpRequest, httpResponse);
+ assert newRequest != null;
+ responseReturned.compareAndSet(null, false);
+
+ if (!responseReturned.get()) {
+ runChainAndResponseFilters(uri, newRequest, httpResponse, chain);
+ }
+ }
+
+ private void runChainAndResponseFilters(URI uri, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+ Optional<OneTimeRunnable> responseFilterInvoker =
+ Optional.ofNullable(jDiscContext.responseFilters.resolve(uri))
+ .map(responseFilter ->
+ new OneTimeRunnable(() ->
+ filterInvoker.invokeResponseFilterChain(responseFilter, uri, request, response)));
+
+
+ HttpServletResponse responseForServlet = responseFilterInvoker
+ .<HttpServletResponse>map(invoker ->
+ new FilterInvokingResponseWrapper(response, invoker))
+ .orElse(response);
+
+ HttpServletRequest requestForServlet = responseFilterInvoker
+ .<HttpServletRequest>map(invoker ->
+ new FilterInvokingRequestWrapper(request, invoker, responseForServlet))
+ .orElse(request);
+
+ chain.doFilter(requestForServlet, responseForServlet);
+
+ responseFilterInvoker.ifPresent(invoker -> {
+ boolean requestHandledSynchronously = !request.isAsyncStarted();
+
+ if (requestHandledSynchronously) {
+ invoker.runIfFirstInvocation();
+ }
+ // For async requests, response filters will be invoked on AsyncContext.complete().
+ });
+ }
+
+ private HttpServletRequest runRequestFilterWithMatchingBinding(AtomicReference<Boolean> responseReturned, URI uri, HttpServletRequest request, HttpServletResponse response) throws IOException {
+ try {
+ RequestFilter requestFilter = jDiscContext.requestFilters.resolve(uri);
+ if (requestFilter == null)
+ return request;
+
+ ResponseHandler responseHandler = createResponseHandler(responseReturned, request, response);
+ return filterInvoker.invokeRequestFilterChain(requestFilter, uri, request, responseHandler);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed running request filter chain for uri " + uri, e);
+ }
+ }
+
+ private ResponseHandler createResponseHandler(AtomicReference<Boolean> responseReturned, HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+ return jdiscResponse -> {
+ boolean oldValueWasNull = responseReturned.compareAndSet(null, true);
+ if (!oldValueWasNull)
+ throw new RuntimeException("Can't return response from filter asynchronously");
+
+ HttpRequestDispatch requestDispatch = createRequestDispatch(httpRequest, httpResponse);
+ return requestDispatch.handleRequestFilterResponse(jdiscResponse);
+ };
+ }
+
+ private HttpRequestDispatch createRequestDispatch(HttpServletRequest request, HttpServletResponse response) {
+ try {
+ final AccessLogEntry accessLogEntry = null; // Not used in this context.
+ return new HttpRequestDispatch(jDiscContext,
+ accessLogEntry,
+ getConnector(request).getMetricContext(),
+ request, response);
+ } catch (IOException e) {
+ throw throwUnchecked(e);
+ }
+ }
+
+ @Override
+ public void destroy() {}
+
+ // ServletRequest wrapper that is necessary because we need to wrap AsyncContext.
+ private static class FilterInvokingRequestWrapper extends HttpServletRequestWrapper {
+ private final OneTimeRunnable filterInvoker;
+ private final HttpServletResponse servletResponse;
+
+ public FilterInvokingRequestWrapper(
+ HttpServletRequest request,
+ OneTimeRunnable filterInvoker,
+ HttpServletResponse servletResponse) {
+ super(request);
+ this.filterInvoker = filterInvoker;
+ this.servletResponse = servletResponse;
+ }
+
+ @Override
+ public AsyncContext startAsync() {
+ final AsyncContext asyncContext = super.startAsync();
+ return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, servletResponse);
+ }
+
+ @Override
+ public AsyncContext startAsync(
+ final ServletRequest wrappedRequest,
+ final ServletResponse wrappedResponse) {
+ // According to the documentation, the passed request/response parameters here must either
+ // _be_ or _wrap_ the original request/response objects passed to the servlet - which are
+ // our wrappers, so no need to wrap again - we can use the user-supplied objects.
+ final AsyncContext asyncContext = super.startAsync(wrappedRequest, wrappedResponse);
+ return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, wrappedResponse);
+ }
+
+ @Override
+ public AsyncContext getAsyncContext() {
+ final AsyncContext asyncContext = super.getAsyncContext();
+ return new FilterInvokingAsyncContext(asyncContext, filterInvoker, this, servletResponse);
+ }
+ }
+
+ // AsyncContext wrapper that is necessary for two reasons:
+ // 1) Run response filters when AsyncContext.complete() is called.
+ // 2) Eliminate paths where application code can get its hands on un-wrapped response object, circumventing
+ // running of response filters.
+ private static class FilterInvokingAsyncContext implements AsyncContext {
+ private final AsyncContext delegate;
+ private final OneTimeRunnable filterInvoker;
+ private final ServletRequest servletRequest;
+ private final ServletResponse servletResponse;
+
+ public FilterInvokingAsyncContext(
+ AsyncContext delegate,
+ OneTimeRunnable filterInvoker,
+ ServletRequest servletRequest,
+ ServletResponse servletResponse) {
+ this.delegate = delegate;
+ this.filterInvoker = filterInvoker;
+ this.servletRequest = servletRequest;
+ this.servletResponse = servletResponse;
+ }
+
+ @Override
+ public ServletRequest getRequest() {
+ return servletRequest;
+ }
+
+ @Override
+ public ServletResponse getResponse() {
+ return servletResponse;
+ }
+
+ @Override
+ public boolean hasOriginalRequestAndResponse() {
+ return delegate.hasOriginalRequestAndResponse();
+ }
+
+ @Override
+ public void dispatch() {
+ delegate.dispatch();
+ }
+
+ @Override
+ public void dispatch(String s) {
+ delegate.dispatch(s);
+ }
+
+ @Override
+ public void dispatch(ServletContext servletContext, String s) {
+ delegate.dispatch(servletContext, s);
+ }
+
+ @Override
+ public void complete() {
+ // Completing may commit the response, so this is the last chance to run response filters.
+ filterInvoker.runIfFirstInvocation();
+ delegate.complete();
+ }
+
+ @Override
+ public void start(Runnable runnable) {
+ delegate.start(runnable);
+ }
+
+ @Override
+ public void addListener(AsyncListener asyncListener) {
+ delegate.addListener(asyncListener);
+ }
+
+ @Override
+ public void addListener(AsyncListener asyncListener, ServletRequest servletRequest, ServletResponse servletResponse) {
+ delegate.addListener(asyncListener, servletRequest, servletResponse);
+ }
+
+ @Override
+ public <T extends AsyncListener> T createListener(Class<T> aClass) throws ServletException {
+ return delegate.createListener(aClass);
+ }
+
+ @Override
+ public void setTimeout(long l) {
+ delegate.setTimeout(l);
+ }
+
+ @Override
+ public long getTimeout() {
+ return delegate.getTimeout();
+ }
+ }
+
+ private static class FilterInvokingResponseWrapper extends HttpServletResponseWrapper {
+ private final OneTimeRunnable filterInvoker;
+
+ public FilterInvokingResponseWrapper(HttpServletResponse response, OneTimeRunnable filterInvoker) {
+ super(response);
+ this.filterInvoker = filterInvoker;
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ ServletOutputStream delegate = super.getOutputStream();
+ return new FilterInvokingServletOutputStream(delegate, filterInvoker);
+ }
+
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ PrintWriter delegate = super.getWriter();
+ return new FilterInvokingPrintWriter(delegate, filterInvoker);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java
new file mode 100644
index 00000000000..e1f3581a1ca
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java
@@ -0,0 +1,201 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.Metric;
+import com.yahoo.jdisc.handler.OverloadException;
+
+import org.eclipse.jetty.server.HttpConnection;
+import org.eclipse.jetty.websocket.server.WebSocketServerFactory;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
+import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
+import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+import static com.yahoo.jdisc.http.server.jetty.ConnectorFactory.JDiscServerConnector;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+@WebServlet(asyncSupported = true, description = "Bridge between Servlet and JDisc APIs")
+class JDiscHttpServlet extends WebSocketServlet {
+ public static final String ATTRIBUTE_NAME_ACCESS_LOG_ENTRY
+ = JDiscHttpServlet.class.getName() + "_access-log-entry";
+
+ private final static Logger log = Logger.getLogger(JDiscHttpServlet.class.getName());
+ private final JDiscContext context;
+
+ public JDiscHttpServlet(JDiscContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public void init() throws ServletException {
+ // The parent class of this loads the WebSocketServerFactory class using Class.forName() in the current thread's
+ // context class loader. To make sure that the class is available when running on OSGi, we configure it
+ // explicitly. This also has the required side-effect of generating the appropriate Import-Package statement in
+ // our OSGi bundle's manifest.
+ Thread.currentThread().setContextClassLoader(WebSocketServerFactory.class.getClassLoader());
+ super.init();
+ }
+
+ @Override
+ protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doHead(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doPut(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doDelete(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doOptions(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ protected void doTrace(final HttpServletRequest request, final HttpServletResponse response)
+ throws ServletException, IOException {
+ dispatchHttpRequest(request, response);
+ }
+
+ @Override
+ public void configure(final WebSocketServletFactory factory) {
+ dispatchWebSocketRequest(factory);
+ }
+
+ private static final Set<String> JETTY_UNSUPPORTED_METHODS = new HashSet<>(Arrays.asList(
+ "PATCH"));
+
+ /**
+ * Override to set connector attribute before the request becomes an upgrade request in the web socket case.
+ * (After the upgrade, the HttpConnection is no longer available.)
+ */
+ @Override
+ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+ request.setAttribute(JDiscServerConnector.REQUEST_ATTRIBUTE, getConnector(request));
+
+ Metric.Context metricContext = getMetricContext(request);
+ context.metric.add(JettyHttpServer.Metrics.NUM_REQUESTS, 1, metricContext);
+ context.metric.add(JettyHttpServer.Metrics.JDISC_HTTP_REQUESTS, 1, metricContext);
+ context.metric.add(JettyHttpServer.Metrics.MANHATTAN_NUM_REQUESTS, 1, metricContext);
+
+ if (JETTY_UNSUPPORTED_METHODS.contains(request.getMethod().toUpperCase())) {
+ dispatchHttpRequest(request, response);
+ } else {
+ super.service(request, response);
+ }
+ }
+
+ static JDiscServerConnector getConnector(HttpServletRequest request) {
+ HttpConnection connection = (HttpConnection)request.getAttribute("org.eclipse.jetty.server.HttpConnection");
+ return (JDiscServerConnector)connection.getConnector();
+ }
+
+ private void dispatchHttpRequest(final HttpServletRequest request,
+ final HttpServletResponse response) throws IOException {
+ final AccessLogEntry accessLogEntry = new AccessLogEntry();
+ AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(request, accessLogEntry);
+ request.setAttribute(ATTRIBUTE_NAME_ACCESS_LOG_ENTRY, accessLogEntry);
+ try {
+ switch (request.getDispatcherType()) {
+ case REQUEST:
+ new HttpRequestDispatch(context,
+ accessLogEntry,
+ getMetricContext(request),
+ request, response).dispatch();
+ break;
+ default:
+ if (log.isLoggable(Level.INFO)) {
+ log.info("Unexpected " + request.getDispatcherType() + "; "
+ + formatAttributes(request));
+ }
+ break;
+ }
+ } catch (OverloadException e) {
+ // nop
+ } catch (RuntimeException e) {
+ throw new ExceptionWrapper(e);
+ }
+ }
+
+ private void dispatchWebSocketRequest(final WebSocketServletFactory factory) {
+ try {
+ // any configuration of the websocket factory goes here
+ factory.setCreator(new WebSocketCreator() {
+
+ @Override
+ public Object createWebSocket(
+ final ServletUpgradeRequest request,
+ final ServletUpgradeResponse response) {
+
+ if (true) {
+ log.warning("WebSocket is currently not supported for JDisc RequestHandlers when running on Jetty.");
+ return null;
+ }
+ return new WebSocketRequestDispatch(context.container, context.janitor, context.metric,
+ getMetricContext(request.getHttpServletRequest()))
+ .dispatch(request, response);
+ }
+ });
+ } catch (RuntimeException e) {
+ throw new ExceptionWrapper(e);
+ }
+ }
+
+ private static Metric.Context getMetricContext(ServletRequest request) {
+ return JDiscServerConnector.fromRequest(request)
+ .getMetricContext();
+ }
+
+ private static String formatAttributes(final HttpServletRequest request) {
+ final StringBuilder out = new StringBuilder();
+ out.append("attributes = {");
+ for (Enumeration<String> names = request.getAttributeNames(); names.hasMoreElements(); ) {
+ String name = names.nextElement();
+ out.append(" '").append(name).append("' = '").append(request.getAttribute(name)).append("'");
+ if (names.hasMoreElements()) {
+ out.append(",");
+ }
+ }
+ out.append(" }");
+ return out.toString();
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java
new file mode 100644
index 00000000000..abebf109fc2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java
@@ -0,0 +1,372 @@
+// Copyright 2016 Yahoo Inc. 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.common.annotations.Beta;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.inject.Inject;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.application.OsgiFramework;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.ServletPathsConfig;
+import com.yahoo.jdisc.http.server.FilterBindings;
+import com.yahoo.jdisc.service.AbstractServerProvider;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.ConnectorStatistics;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandlerContainer;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.server.handler.gzip.GzipHandler;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+import javax.servlet.DispatcherType;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.jdisc.http.server.jetty.ConnectorFactory.JDiscServerConnector;
+import static com.yahoo.jdisc.http.server.jetty.Exceptions.throwUnchecked;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+@Beta
+public class JettyHttpServer extends AbstractServerProvider {
+
+ public interface Metrics {
+ final String NAME_DIMENSION = "serverName";
+ final String PORT_DIMENSION = "serverPort";
+
+ final String NUM_ACTIVE_REQUESTS = "serverNumActiveRequests";
+ final String NUM_OPEN_CONNECTIONS = "serverNumOpenConnections";
+ final String NUM_CONNECTIONS_OPEN_MAX = "serverConnectionsOpenMax";
+ final String CONNECTION_DURATION_MAX = "serverConnectionDurationMax";
+ final String CONNECTION_DURATION_MEAN = "serverConnectionDurationMean";
+ final String CONNECTION_DURATION_STD_DEV = "serverConnectionDurationStdDev";
+
+ final String NUM_BYTES_RECEIVED = "serverBytesReceived";
+ final String NUM_BYTES_SENT = "serverBytesSent";
+ final String MANHATTAN_NUM_BYTES_RECEIVED = "http.in.bytes";
+ final String MANHATTAN_NUM_BYTES_SENT = "http.out.bytes";
+
+ final String NUM_CONNECTIONS = "serverNumConnections";
+ final String NUM_CONNECTIONS_IDLE = "serverNumConnectionsIdle";
+ final String NUM_UNEXPECTED_DISCONNECTS = "serverNumUnexpectedDisconnects";
+
+ /* For historical reasons, these are all aliases for the same metric. 'jdisc.http' should ideally be the only one. */
+ final String JDISC_HTTP_REQUESTS = "jdisc.http.requests";
+ final String NUM_REQUESTS = "serverNumRequests";
+ final String MANHATTAN_NUM_REQUESTS = "http.requests";
+
+ final String NUM_SUCCESSFUL_RESPONSES = "serverNumSuccessfulResponses";
+ final String NUM_FAILED_RESPONSES = "serverNumFailedResponses";
+ final String NUM_SUCCESSFUL_WRITES = "serverNumSuccessfulResponseWrites";
+ final String NUM_FAILED_WRITES = "serverNumFailedResponseWrites";
+
+ final String NETWORK_LATENCY = "serverNetworkLatency";
+ final String TOTAL_SUCCESSFUL_LATENCY = "serverTotalSuccessfulResponseLatency";
+ final String MANHATTAN_TOTAL_SUCCESSFUL_LATENCY = "http.latency";
+ final String TOTAL_FAILED_LATENCY = "serverTotalFailedResponseLatency";
+ final String TIME_TO_FIRST_BYTE = "serverTimeToFirstByte";
+ final String MANHATTAN_TIME_TO_FIRST_BYTE = "http.out.firstbytetime";
+
+ final String RESPONSES_1XX = "http.status.1xx";
+ final String RESPONSES_2XX = "http.status.2xx";
+ final String RESPONSES_3XX = "http.status.3xx";
+ final String RESPONSES_4XX = "http.status.4xx";
+ final String RESPONSES_5XX = "http.status.5xx";
+
+ final String STARTED_MILLIS = "serverStartedMillis";
+ final String MANHATTAN_STARTED_MILLIS = "proc.uptime";
+ }
+
+ private final static Logger log = Logger.getLogger(JettyHttpServer.class.getName());
+ private final long timeStarted = System.currentTimeMillis();
+ private final ExecutorService janitor;
+ private final ScheduledExecutorService metricReporterExecutor;
+ private final Metric metric;
+ private final Server server;
+
+ @Inject
+ public JettyHttpServer(
+ final CurrentContainer container,
+ final Metric metric,
+ final ServerConfig serverConfig,
+ final ServletPathsConfig servletPathsConfig,
+ final ThreadFactory threadFactory,
+ final FilterBindings filterBindings,
+ final ComponentRegistry<ConnectorFactory> connectorFactories,
+ final ComponentRegistry<ServletHolder> servletHolders,
+ final OsgiFramework osgiFramework,
+ final FilterInvoker filterInvoker,
+ final AccessLog accessLog) {
+ super(container);
+ if (connectorFactories.allComponents().isEmpty()) {
+ throw new IllegalArgumentException("No connectors configured.");
+ }
+ this.metric = metric;
+
+ server = new Server();
+ ((QueuedThreadPool)server.getThreadPool()).setMaxThreads(serverConfig.maxWorkerThreads());
+
+ Map<Path, FileChannel> keyStoreChannels = getKeyStoreFileChannels(osgiFramework.bundleContext());
+
+ for (ConnectorFactory connectorFactory : connectorFactories.allComponents()) {
+ ServerSocketChannel preBoundChannel = getChannelFromServiceLayer(connectorFactory.getConnectorConfig().listenPort(), osgiFramework.bundleContext());
+ server.addConnector(connectorFactory.createConnector(metric, server, preBoundChannel, keyStoreChannels));
+ }
+
+ janitor = newJanitor(threadFactory);
+
+ JDiscContext jDiscContext = new JDiscContext(
+ filterBindings.getRequestFilters().activate(),
+ filterBindings.getResponseFilters().activate(),
+ container,
+ janitor,
+ metric,
+ serverConfig);
+
+ ServletHolder jdiscServlet = new ServletHolder(new JDiscHttpServlet(jDiscContext));
+ FilterHolder jDiscFilterInvokerFilter = new FilterHolder(new JDiscFilterInvokerFilter(jDiscContext, filterInvoker));
+
+ final RequestLog requestLog = new AccessLogRequestLog(accessLog);
+
+ server.setHandler(
+ getHandlerCollection(
+ serverConfig,
+ servletPathsConfig,
+ jdiscServlet,
+ servletHolders,
+ jDiscFilterInvokerFilter,
+ requestLog));
+
+ final int numMetricReporterThreads = 1;
+ metricReporterExecutor = Executors.newScheduledThreadPool(
+ numMetricReporterThreads,
+ new ThreadFactoryBuilder()
+ .setDaemon(true)
+ .setNameFormat(JettyHttpServer.class.getName() + "-MetricReporter-%d")
+ .setThreadFactory(threadFactory)
+ .build()
+ );
+ metricReporterExecutor.scheduleAtFixedRate(new MetricTask(), 0, 2, TimeUnit.SECONDS);
+ }
+
+ private HandlerCollection getHandlerCollection(
+ ServerConfig serverConfig,
+ ServletPathsConfig servletPathsConfig,
+ ServletHolder jdiscServlet,
+ ComponentRegistry<ServletHolder> servletHolders,
+ FilterHolder jDiscFilterInvokerFilter,
+ RequestLog requestLog) {
+
+ ServletContextHandler servletContextHandler = createServletContextHandler();
+
+ servletHolders.allComponentsById().forEach((id, servlet) -> {
+ String path = getServletPath(servletPathsConfig, id);
+ servletContextHandler.addServlet(servlet, path);
+ servletContextHandler.addFilter(jDiscFilterInvokerFilter, path, EnumSet.allOf(DispatcherType.class));
+ });
+
+ servletContextHandler.addServlet(jdiscServlet, "/*");
+
+ final GzipHandler gzipHandler = newGzipHandler(serverConfig);
+ gzipHandler.setHandler(servletContextHandler);
+
+ final StatisticsHandler statisticsHandler = newStatisticsHandler();
+ statisticsHandler.setHandler(gzipHandler);
+
+ final RequestLogHandler requestLogHandler = new RequestLogHandler();
+ requestLogHandler.setRequestLog(requestLog);
+
+ HandlerCollection handlerCollection = new HandlerCollection();
+ handlerCollection.setHandlers(new Handler[]{statisticsHandler, requestLogHandler});
+ return handlerCollection;
+ }
+
+ private static String getServletPath(ServletPathsConfig servletPathsConfig, ComponentId id) {
+ return "/" + servletPathsConfig.servlets(id.stringValue()).path();
+ }
+
+ // Ugly trick to get generic type literal.
+ @SuppressWarnings("unchecked")
+ private static final Class<Map<?, ?>> mapClass = (Class<Map<?, ?>>) (Object) Map.class;
+
+ private Map<Path, FileChannel> getKeyStoreFileChannels(BundleContext bundleContext) {
+ try {
+ Collection<ServiceReference<Map<?, ?>>> serviceReferences = bundleContext.getServiceReferences(mapClass,
+ "(role=com.yahoo.container.standalone.StandaloneContainerActivator.KeyStoreFileChannels)");
+
+ if (serviceReferences == null || serviceReferences.isEmpty())
+ return Collections.emptyMap();
+
+ if (serviceReferences.size() != 1)
+ throw new IllegalStateException("Multiple KeyStoreFileChannels registered");
+
+ return getKeyStoreFileChannels(bundleContext, serviceReferences.iterator().next());
+ } catch (InvalidSyntaxException e) {
+ throw throwUnchecked(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<Path, FileChannel> getKeyStoreFileChannels(BundleContext bundleContext, ServiceReference<Map<?, ?>> keyStoreFileChannelReference) {
+ Map<?, ?> fileChannelMap = bundleContext.getService(keyStoreFileChannelReference);
+ try {
+ if (fileChannelMap == null)
+ return Collections.emptyMap();
+
+ Map<Path, FileChannel> result = (Map<Path, FileChannel>) fileChannelMap;
+ log.fine("Using file channel for " + result.keySet());
+ return result;
+ } finally {
+ //if we change this to be anything other than a simple map, we should hold the reference as long as the object is in use.
+ bundleContext.ungetService(keyStoreFileChannelReference);
+ }
+ }
+
+ private ServletContextHandler createServletContextHandler() {
+ ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS);
+ servletContextHandler.setContextPath("/");
+ return servletContextHandler;
+ }
+
+ private ServerSocketChannel getChannelFromServiceLayer(int listenPort, BundleContext bundleContext) {
+ log.log(Level.FINE, "Retrieving channel for port " + listenPort + " from " + bundleContext.getClass().getName());
+ Collection<ServiceReference<ServerSocketChannel>> refs;
+ final String filter = "(port=" + listenPort + ")";
+ try {
+ refs = bundleContext.getServiceReferences(ServerSocketChannel.class, filter);
+ } catch (InvalidSyntaxException e) {
+ throw new IllegalStateException("OSGi framework rejected filter " + filter, e);
+ }
+ if (refs.isEmpty()) {
+ return null;
+ }
+ if (refs.size() != 1) {
+ throw new IllegalStateException("Got more than one service reference for " + ServerSocketChannel.class + " port " + listenPort + ".");
+ }
+ ServiceReference<ServerSocketChannel> ref = refs.iterator().next();
+ return bundleContext.getService(ref);
+ }
+
+ private static ExecutorService newJanitor(final ThreadFactory factory) {
+ final int threadPoolSize = Runtime.getRuntime().availableProcessors();
+ log.info("Creating janitor executor with " + threadPoolSize + " threads");
+ return Executors.newFixedThreadPool(
+ threadPoolSize,
+ new ThreadFactoryBuilder()
+ .setDaemon(true)
+ .setNameFormat(JettyHttpServer.class.getName() + "-Janitor-%d")
+ .setThreadFactory(factory)
+ .build()
+ );
+ }
+
+ @Override
+ public void start() {
+ try {
+ server.start();
+ } catch (final Exception e) {
+ throw new RuntimeException("Failed to start server.", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ try {
+ server.stop();
+ } catch (final Exception e) {
+ log.log(Level.SEVERE, "Server shutdown threw an unexpected exception.", e);
+ }
+
+ metricReporterExecutor.shutdown();
+ janitor.shutdown();
+ }
+
+ public int getListenPort() {
+ return ((ServerConnector)server.getConnectors()[0]).getLocalPort();
+ }
+
+ private class MetricTask implements Runnable {
+ @Override
+ public void run() {
+ StatisticsHandler statisticsHandler = ((AbstractHandlerContainer)server.getHandler())
+ .getChildHandlerByClass(StatisticsHandler.class);
+ if (statisticsHandler == null)
+ return;
+
+ setServerMetrics(statisticsHandler);
+
+ for (Connector connector : server.getConnectors()) {
+ setConnectorMetrics((JDiscServerConnector)connector);
+ }
+ }
+
+ }
+
+ private void setServerMetrics(StatisticsHandler statistics) {
+ long timeSinceStarted = System.currentTimeMillis() - timeStarted;
+ metric.set(Metrics.STARTED_MILLIS, timeSinceStarted, null);
+ metric.set(Metrics.MANHATTAN_STARTED_MILLIS, timeSinceStarted, null);
+
+ metric.add(Metrics.RESPONSES_1XX, statistics.getResponses1xx(), null);
+ metric.add(Metrics.RESPONSES_2XX, statistics.getResponses2xx(), null);
+ metric.add(Metrics.RESPONSES_3XX, statistics.getResponses3xx(), null);
+ metric.add(Metrics.RESPONSES_4XX, statistics.getResponses4xx(), null);
+ metric.add(Metrics.RESPONSES_5XX, statistics.getResponses5xx(), null);
+
+ // Reset to only add the diff for count metrics.
+ // (The alternative to reset would be to preserve the previous value, and only add the diff.)
+ statistics.statsReset();
+ }
+
+ private void setConnectorMetrics(JDiscServerConnector connector) {
+ ConnectorStatistics statistics = connector.getStatistics();
+ metric.set(Metrics.NUM_CONNECTIONS, statistics.getConnections(), connector.getMetricContext());
+ metric.set(Metrics.NUM_OPEN_CONNECTIONS, statistics.getConnectionsOpen(), connector.getMetricContext());
+ metric.set(Metrics.NUM_CONNECTIONS_OPEN_MAX, statistics.getConnectionsOpenMax(), connector.getMetricContext());
+ metric.set(Metrics.CONNECTION_DURATION_MAX, statistics.getConnectionDurationMax(), connector.getMetricContext());
+ metric.set(Metrics.CONNECTION_DURATION_MEAN, statistics.getConnectionDurationMean(), connector.getMetricContext());
+ metric.set(Metrics.CONNECTION_DURATION_STD_DEV, statistics.getConnectionDurationStdDev(), connector.getMetricContext());
+ }
+
+ private StatisticsHandler newStatisticsHandler() {
+ StatisticsHandler statisticsHandler = new StatisticsHandler();
+ statisticsHandler.statsReset();
+ return statisticsHandler;
+ }
+
+ private GzipHandler newGzipHandler(ServerConfig serverConfig) {
+ final GzipHandler gzipHandler = new GzipHandler();
+ gzipHandler.setCompressionLevel(serverConfig.responseCompressionLevel());
+ gzipHandler.setCheckGzExists(false);
+ gzipHandler.setIncludedMethods("GET", "POST");
+ return gzipHandler;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java
new file mode 100644
index 00000000000..518c9f92ea8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricReporter.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. 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.Metric.Context;
+
+import com.yahoo.jdisc.http.server.jetty.JettyHttpServer.Metrics;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+/**
+ * Responsible for metric reporting for JDisc http and web socket request handler support.
+ * @author tonytv
+ */
+public class MetricReporter {
+ private final Metric metric;
+ private final @Nullable Context context;
+
+ private final long requestStartTime;
+
+ //TODO: rename
+ private final AtomicBoolean firstSetOfTimeToFirstByte = new AtomicBoolean(true);
+
+
+ public MetricReporter(Metric metric, @Nullable Context context, long requestStartTime) {
+ this.metric = metric;
+ this.context = context;
+ this.requestStartTime = requestStartTime;
+ }
+
+ public void successfulWrite(int numBytes) {
+ setTimeToFirstByteFirstTime();
+
+ metric.add(Metrics.NUM_SUCCESSFUL_WRITES, 1, context);
+ metric.set(Metrics.NUM_BYTES_SENT, numBytes, context);
+ metric.set(Metrics.MANHATTAN_NUM_BYTES_SENT, numBytes, context);
+ }
+
+ private void setTimeToFirstByteFirstTime() {
+ boolean isFirstWrite = firstSetOfTimeToFirstByte.getAndSet(false);
+ if (isFirstWrite) {
+ long timeToFirstByte = getRequestLatency();
+ metric.set(Metrics.TIME_TO_FIRST_BYTE, timeToFirstByte, context);
+ metric.set(Metrics.MANHATTAN_TIME_TO_FIRST_BYTE, timeToFirstByte, context);
+ }
+ }
+
+ public void failedWrite() {
+ metric.add(Metrics.NUM_FAILED_WRITES, 1, context);
+ }
+
+ public void successfulResponse() {
+ setTimeToFirstByteFirstTime();
+
+ long requestLatency = getRequestLatency();
+
+ metric.set(Metrics.TOTAL_SUCCESSFUL_LATENCY, requestLatency, context);
+ metric.set(Metrics.MANHATTAN_TOTAL_SUCCESSFUL_LATENCY, requestLatency, context);
+
+ metric.add(Metrics.NUM_SUCCESSFUL_RESPONSES, 1, context);
+ }
+
+ public void failedResponse() {
+ setTimeToFirstByteFirstTime();
+
+ metric.set(Metrics.TOTAL_FAILED_LATENCY, getRequestLatency(), context);
+ metric.add(Metrics.NUM_FAILED_RESPONSES, 1, context);
+ }
+
+ public void successfulRead(int bytes_received) {
+ metric.set(JettyHttpServer.Metrics.NUM_BYTES_RECEIVED, bytes_received, context);
+ metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_RECEIVED, bytes_received, context);
+ }
+
+ private long getRequestLatency() {
+ return System.currentTimeMillis() - requestStartTime;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java
new file mode 100644
index 00000000000..1d6d7a55b69
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * @author tonytv
+ */
+public class OneTimeRunnable {
+ private final Runnable runnable;
+ private final AtomicBoolean hasRun = new AtomicBoolean(false);
+
+ public OneTimeRunnable(Runnable runnable) {
+ this.runnable = runnable;
+ }
+
+ public void runIfFirstInvocation() {
+ boolean previous = hasRun.getAndSet(true);
+ if (!previous) {
+ runnable.run();
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java
new file mode 100644
index 00000000000..d8012880694
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java
@@ -0,0 +1,256 @@
+// Copyright 2016 Yahoo Inc. 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.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.SharedResource;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.NullContent;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class wraps a request handler and does reference counting on the request for every object that depends on the
+ * request, such as the response handler, content channels and completion handlers. This ensures that requests (and
+ * hence the current container) will be referenced until the end of the request handling - even with async handling in
+ * non-framework threads - without requiring the application to handle this tedious work.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+class ReferenceCountingRequestHandler implements RequestHandler {
+
+ private static final Logger log = Logger.getLogger(ReferenceCountingRequestHandler.class.getName());
+
+ final RequestHandler delegate;
+
+ ReferenceCountingRequestHandler(RequestHandler delegate) {
+ Objects.requireNonNull(delegate, "delegate");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler responseHandler) {
+ try (final ResourceReference requestReference = request.refer()) {
+ ContentChannel contentChannel;
+ final ReferenceCountingResponseHandler referenceCountingResponseHandler
+ = new ReferenceCountingResponseHandler(request, new NullContentResponseHandler(responseHandler));
+ try {
+ contentChannel = delegate.handleRequest(request, referenceCountingResponseHandler);
+ Objects.requireNonNull(contentChannel, "contentChannel");
+ } catch (Throwable t) {
+ try {
+ // The response handler might never be invoked, due to the exception thrown from handleRequest().
+ referenceCountingResponseHandler.unrefer();
+ } catch (Throwable thrownFromUnrefer) {
+ log.log(Level.WARNING, "Unexpected problem", thrownFromUnrefer);
+ }
+ throw t;
+ }
+ return new ReferenceCountingContentChannel(request, contentChannel);
+ }
+ }
+
+ @Override
+ public void handleTimeout(Request request, ResponseHandler responseHandler) {
+ delegate.handleTimeout(request, new NullContentResponseHandler(responseHandler));
+ }
+
+ @Override
+ public ResourceReference refer() {
+ return delegate.refer();
+ }
+
+ @Override
+ public void release() {
+ delegate.release();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ private static class ReferenceCountingResponseHandler implements ResponseHandler {
+
+ final SharedResource request;
+ final ResourceReference requestReference;
+ final ResponseHandler delegate;
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ ReferenceCountingResponseHandler(SharedResource request, ResponseHandler delegate) {
+ Objects.requireNonNull(request, "request");
+ Objects.requireNonNull(delegate, "delegate");
+ this.request = request;
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ if (closed.getAndSet(true)) {
+ throw new IllegalStateException(delegate + " is already called.");
+ }
+ try (final ResourceReference ref = requestReference) {
+ ContentChannel contentChannel = delegate.handleResponse(response);
+ Objects.requireNonNull(contentChannel, "contentChannel");
+ return new ReferenceCountingContentChannel(request, contentChannel);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ /**
+ * Close the reference that is normally closed by {@link #handleResponse(Response)}.
+ *
+ * This is to be used in error situations, where handleResponse() may not be invoked.
+ */
+ public void unrefer() {
+ if (closed.getAndSet(true)) {
+ // This simply means that handleResponse() has been run, in which case we are
+ // guaranteed that the reference is closed.
+ return;
+ }
+ requestReference.close();
+ }
+ }
+
+ private static class ReferenceCountingContentChannel implements ContentChannel {
+
+ final SharedResource request;
+ final ResourceReference requestReference;
+ final ContentChannel delegate;
+
+ ReferenceCountingContentChannel(SharedResource request, ContentChannel delegate) {
+ Objects.requireNonNull(request, "request");
+ Objects.requireNonNull(delegate, "delegate");
+ this.request = request;
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler completionHandler) {
+ final CompletionHandler referenceCountingCompletionHandler
+ = new ReferenceCountingCompletionHandler(request, completionHandler);
+ try {
+ delegate.write(buf, referenceCountingCompletionHandler);
+ } catch (Throwable t) {
+ try {
+ referenceCountingCompletionHandler.failed(t);
+ } catch (AlreadyCompletedException ignored) {
+ } catch (Throwable failFailure) {
+ log.log(Level.WARNING, "Failure during call to CompletionHandler.failed()", failFailure);
+ }
+ throw t;
+ }
+ }
+
+ @Override
+ public void close(CompletionHandler completionHandler) {
+ final CompletionHandler referenceCountingCompletionHandler
+ = new ReferenceCountingCompletionHandler(request, completionHandler);
+ try (final ResourceReference ref = requestReference) {
+ delegate.close(referenceCountingCompletionHandler);
+ } catch (Throwable t) {
+ try {
+ referenceCountingCompletionHandler.failed(t);
+ } catch (AlreadyCompletedException ignored) {
+ } catch (Throwable failFailure) {
+ log.log(Level.WARNING, "Failure during call to CompletionHandler.failed()", failFailure);
+ }
+ throw t;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+
+ private static class AlreadyCompletedException extends IllegalStateException {
+ public AlreadyCompletedException(final CompletionHandler completionHandler) {
+ super(completionHandler + " is already called.");
+ }
+ }
+
+ private static class ReferenceCountingCompletionHandler implements CompletionHandler {
+
+ final ResourceReference requestReference;
+ final CompletionHandler delegate;
+ final AtomicBoolean closed = new AtomicBoolean(false);
+
+ public ReferenceCountingCompletionHandler(SharedResource request, CompletionHandler delegate) {
+ this.delegate = delegate;
+ this.requestReference = request.refer();
+ }
+
+ @Override
+ public void completed() {
+ if (closed.getAndSet(true)) {
+ throw new AlreadyCompletedException(delegate);
+ }
+ try {
+ if (delegate != null) {
+ delegate.completed();
+ }
+ } finally {
+ requestReference.close();
+ }
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ if (closed.getAndSet(true)) {
+ throw new AlreadyCompletedException(delegate);
+ }
+ try (final ResourceReference ref = requestReference) {
+ if (delegate != null) {
+ delegate.failed(t);
+ } else {
+ log.log(Level.WARNING, "Uncaught completion failure.", t);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(delegate);
+ }
+ }
+
+ private static class NullContentResponseHandler implements ResponseHandler {
+
+ final ResponseHandler delegate;
+
+ NullContentResponseHandler(ResponseHandler delegate) {
+ Objects.requireNonNull(delegate, "delegate");
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ ContentChannel contentChannel = delegate.handleResponse(response);
+ if (contentChannel == null) {
+ contentChannel = NullContent.INSTANCE;
+ }
+ return contentChannel;
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java
new file mode 100644
index 00000000000..cbcbd278bf8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+/**
+ * This exception may be thrown from a request handler to fail a request with a given response code and message.
+ * It is given some special treatment in {@link ServletResponseController}.
+ *
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+class RequestException extends RuntimeException {
+
+ private final int responseStatus;
+
+ /**
+ * @param responseStatus the response code to use for the http response
+ * @param message exception message
+ * @param cause chained throwable
+ */
+ public RequestException(final int responseStatus, final String message, final Throwable cause) {
+ super(message, cause);
+ this.responseStatus = responseStatus;
+ }
+
+ /**
+ * @param responseStatus the response code to use for the http response
+ * @param message exception message
+ */
+ public RequestException(final int responseStatus, final String message) {
+ super(message);
+ this.responseStatus = responseStatus;
+ }
+
+ /**
+ * Returns the response code to use for the http response.
+ */
+ public int getResponseStatus() {
+ return responseStatus;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java
new file mode 100644
index 00000000000..58cdd7a331e
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ResponseContentPart.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. 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.handler.CompletionHandler;
+
+import java.nio.ByteBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author tonytv
+ * @author simon
+ */
+class ResponseContentPart {
+ private static final Logger log = Logger.getLogger(ResponseContentPart.class.getName());
+
+ final ByteBuffer buf;
+ final CompletionHandler handler;
+
+ ResponseContentPart(final ByteBuffer buf, final CompletionHandler handler) {
+ this.buf = (buf != null) ? buf : ByteBuffer.allocate(0);
+ this.handler = (handler != null) ? handler: DEFAULT_COMPLETION_HANDLER;
+ }
+
+ private static final CompletionHandler DEFAULT_COMPLETION_HANDLER = new CompletionHandler() {
+ @Override
+ public void completed() {
+ log.log(Level.FINE, "DefaultCompletionHandler: Operation completed");
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ log.log(Level.FINE, "DefaultCompletionHandler: Operation failed", t);
+ }
+ };
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java
new file mode 100644
index 00000000000..271805765c2
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java
@@ -0,0 +1,286 @@
+// Copyright 2016 Yahoo Inc. 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.handler.CompletionHandler;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author tonytv
+ */
+public class ServletOutputStreamWriter {
+ /** Rules:
+ * 1) Don't modify the output stream without isReady returning true (write/flush/close).
+ * Multiple modification calls without interleaving isReady calls are not allowed.
+ * 2) If isReady returned false, no other calls should be made until the write listener is invoked.
+ * 3) If the write listener sees isReady == false, it must not do any modifications before its next invocation.
+ */
+
+
+ private enum State {
+ NOT_STARTED,
+ WAITING_FOR_WRITE_POSSIBLE_CALLBACK,
+ WAITING_FOR_BUFFER,
+ WRITING_BUFFERS,
+ FINISHED_OR_ERROR
+ }
+
+ private static final Logger log = Logger.getLogger(ServletOutputStreamWriter.class.getName());
+
+ private static final ByteBuffer CLOSE_STREAM_BUFFER = ByteBuffer.allocate(0);
+
+ private final Object monitor = new Object();
+
+ @GuardedBy("monitor")
+ private State state = State.NOT_STARTED;
+
+ @GuardedBy("state")
+ private final ServletOutputStream outputStream;
+ private final Executor executor;
+
+ @GuardedBy("monitor")
+ private final Deque<ResponseContentPart> responseContentQueue = new ArrayDeque<>();
+
+ private final MetricReporter metricReporter;
+
+ /**
+ * When this future completes there will be no more calls against the servlet output stream or servlet response.
+ * The framework is still allowed to invoke us though.
+ *
+ * The future might complete in the servlet framework thread, user thread or executor thread.
+ */
+ final CompletableFuture<Void> finishedFuture = new CompletableFuture<>();
+
+
+ public ServletOutputStreamWriter(ServletOutputStream outputStream, Executor executor, MetricReporter metricReporter) {
+ this.outputStream = outputStream;
+ this.executor = executor;
+ this.metricReporter = metricReporter;
+ }
+
+ public void setSendingError() {
+ synchronized (monitor) {
+ assertStateIs(state, State.NOT_STARTED);
+ state = State.FINISHED_OR_ERROR;
+ }
+ }
+
+ public void writeBuffer(ByteBuffer buf, CompletionHandler handler) {
+ boolean thisThreadShouldWrite = false;
+
+ synchronized (monitor) {
+ if (state == State.FINISHED_OR_ERROR) {
+ if (handler != null) {
+ executor.execute(() -> handler.failed(new IllegalStateException("ContentChannel already closed.")));
+ }
+ return;
+ }
+
+ responseContentQueue.addLast(new ResponseContentPart(buf, handler));
+ switch (state) {
+ case NOT_STARTED:
+ state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK;
+ outputStream.setWriteListener(writeListener);
+ break;
+ case WAITING_FOR_WRITE_POSSIBLE_CALLBACK:
+ case WRITING_BUFFERS:
+ break;
+ case WAITING_FOR_BUFFER:
+ thisThreadShouldWrite = true;
+ state = State.WRITING_BUFFERS;
+ break;
+ default:
+ throw new IllegalStateException("Invalid state " + state);
+ }
+ }
+
+ if (thisThreadShouldWrite) {
+ writeBuffersInQueueToOutputStream();
+ }
+ }
+
+ public void close(CompletionHandler handler) {
+ writeBuffer(CLOSE_STREAM_BUFFER, handler);
+ }
+
+ private void writeBuffersInQueueToOutputStream() {
+ boolean lastOperationWasFlush = false;
+
+ while (true) {
+ ResponseContentPart contentPart;
+
+ synchronized (monitor) {
+ if (state == State.FINISHED_OR_ERROR) {
+ return;
+ }
+
+ assertStateIs(state, State.WRITING_BUFFERS);
+
+ if (!outputStream.isReady()) {
+ state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK;
+ return;
+ }
+
+ contentPart = responseContentQueue.pollFirst();
+
+ if (contentPart == null && lastOperationWasFlush) {
+ state = State.WAITING_FOR_BUFFER;
+ return;
+ }
+ }
+
+ try {
+ boolean isFlush = contentPart == null;
+ if (isFlush) {
+ outputStream.flush();
+ lastOperationWasFlush = true;
+ continue;
+ }
+ lastOperationWasFlush = false;
+
+ if (contentPart.buf == CLOSE_STREAM_BUFFER) {
+ closeOutputStream(contentPart.handler);
+ setFinished(Optional.empty());
+ } else {
+ writeBufferToOutputStream(contentPart);
+ }
+ } catch (Throwable e) {
+ setFinished(Optional.of(e));
+ }
+ }
+ }
+
+ private void setFinished(Optional<Throwable> e) {
+ synchronized (monitor) {
+ state = State.FINISHED_OR_ERROR;
+ if (!responseContentQueue.isEmpty()) {
+ failAllParts_holdingLock(e.orElse(new IllegalStateException("ContentChannel closed.")));
+ }
+ }
+
+ assert !Thread.holdsLock(monitor);
+ if (e.isPresent()) {
+ finishedFuture.completeExceptionally(e.get());
+ } else {
+ finishedFuture.complete(null);
+ }
+ }
+
+ private void failAllParts_holdingLock(Throwable e) {
+ assert Thread.holdsLock(monitor);
+
+ ArrayList<ResponseContentPart> failedParts = new ArrayList<>(responseContentQueue);
+ responseContentQueue.clear();
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ RuntimeException failReason = new RuntimeException("Failing due to earlier ServletOutputStream write failure", e);
+
+ Consumer<ResponseContentPart> failCompletionHandler = responseContentPart ->
+ runCompletionHandler_logOnExceptions(
+ () -> responseContentPart.handler.failed(failReason));
+
+ executor.execute(
+ () -> failedParts.forEach(failCompletionHandler));
+ }
+
+ private void closeOutputStream(CompletionHandler handler) throws Exception {
+ callCompletionHandlerWhenDone(handler, () -> {
+ outputStream.close();
+ return null;
+ });
+ }
+
+ private void writeBufferToOutputStream(ResponseContentPart contentPart) throws Throwable {
+ callCompletionHandlerWhenDone(contentPart.handler, () -> {
+ ByteBuffer buffer = contentPart.buf;
+ final int bytesToSend = buffer.remaining();
+ try {
+ if (buffer.hasArray()) {
+ outputStream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining());
+ } else {
+ final byte[] array = new byte[buffer.remaining()];
+ buffer.get(array);
+ outputStream.write(array);
+ }
+ metricReporter.successfulWrite(bytesToSend);
+ } catch (Throwable throwable) {
+ metricReporter.failedWrite();
+ throw throwable;
+ }
+
+ return null;
+ });
+ }
+
+ //Using Callable<Void> instead of Runnable since Callable supports throwing exceptions.
+ private void callCompletionHandlerWhenDone(CompletionHandler handler, Callable<Void> callable) throws Exception {
+ try {
+ callable.call();
+ } catch (Throwable e) {
+ assert !Thread.holdsLock(monitor);
+ runCompletionHandler_logOnExceptions(
+ () -> handler.failed(e));
+ throw e;
+ }
+
+ assert !Thread.holdsLock(monitor);
+ handler.completed(); //Might throw an exception, handling in the enclosing scope.
+ }
+
+ private void runCompletionHandler_logOnExceptions(Runnable runnable) {
+ assert !Thread.holdsLock(monitor);
+ try {
+ runnable.run();
+ } catch (Throwable e) {
+ log.log(Level.WARNING, "Unexpected exception from CompletionHandler.", e);
+ }
+ }
+
+ private void assertStateIs(State currentState, State expectedState) {
+ if (currentState != expectedState) {
+ AssertionError error = new AssertionError("Expected state " + expectedState + ", got state " + currentState);
+ log.log(Level.WARNING, "Assertion failed.", error);
+ throw error;
+ }
+ }
+
+ public void fail(Throwable t) {
+ setFinished(Optional.of(t));
+ }
+
+ private final WriteListener writeListener = new WriteListener() {
+ @Override
+ public void onWritePossible() throws IOException {
+ synchronized (monitor) {
+ if (state == State.FINISHED_OR_ERROR) {
+ return;
+ }
+
+ assertStateIs(state, State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK);
+ state = State.WRITING_BUFFERS;
+ }
+
+ writeBuffersInQueueToOutputStream();
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ setFinished(Optional.of(t));
+ }
+ };
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java
new file mode 100644
index 00000000000..5bea01bd104
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java
@@ -0,0 +1,266 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Finished when either
+ * 1) There was an error
+ * 2) There is no more data AND the number of pending completion handler invocations is 0
+ *
+ * Stops reading when a failure has happened.
+ *
+ * The reason for not waiting for pending completions in error situations
+ * is that if the error is reported through the finishedFuture,
+ * error reporting might be async.
+ * Since we have tests that first reports errors and then closes the response content,
+ * it's important that errors are delivered synchronously.
+ */
+class ServletRequestReader implements ReadListener {
+ private enum State {
+ READING, ALL_DATA_READ, REQUEST_CONTENT_CLOSED
+ }
+
+ private static final Logger log = Logger.getLogger(ServletRequestReader.class.getName());
+
+ private static final int MIN_BUFFER_SIZE_BYTES = 1024;
+
+ private final Object monitor = new Object();
+
+ private final ServletInputStream servletInputStream;
+ private final ContentChannel requestContentChannel;
+
+ private final Executor executor;
+ private final MetricReporter metricReporter;
+
+ /**
+ * Rules:
+ * 1. If state != State.READING, then numberOfOutstandingUserCalls must not increase
+ * 2. The _first time_ (finishedFuture is completed OR all data is read) AND numberOfOutstandingUserCalls == 0,
+ * the request content channel should be closed
+ * 3. finishedFuture must not be completed when holding the monitor
+ * 4. completing finishedFuture with an exception must be done synchronously
+ * to prioritize failures being transported to the response.
+ * 5. All completion handlers (both for write and complete) must not be
+ * called from a user (request handler) owned thread
+ * (i.e. when being called from user code, don't call back into user code.)
+ */
+ @GuardedBy("monitor")
+ private State state = State.READING;
+
+ /**
+ * Number of calls that we're waiting for from user code.
+ * There are two classes of such calls:
+ * 1) calls to requestContentChannel.write that we're waiting for to complete
+ * 2) completion handlers given to requestContentChannel.write that the user must call.
+ *
+ * As long as we're waiting for such calls, we're not allowed to:
+ * - close the request content channel (currently only required by tests)
+ * - complete the finished future non-exceptionally,
+ * since then we would not be able to report writeCompletionHandler.failed(exception) calls
+ */
+ @GuardedBy("monitor")
+ private int numberOfOutstandingUserCalls = 0;
+
+ /**
+ * When this future completes there will be no more calls against the servlet input stream.
+ * The framework is still allowed to invoke us though.
+ *
+ * The future might complete in the servlet framework thread, user thread or executor thread.
+ *
+ * All completions of finishedFuture, except those done when closing the request content channel,
+ * must be followed by calls to either onAllDataRead or decreasePendingAndCloseRequestContentChannelConditionally.
+ * Those two functions will ensure that the request content channel is closed at the right time.
+ * If calls to those methods does not close the request content channel immediately,
+ * there is some outstanding completion callback that will later come in and complete the request.
+ */
+ final CompletableFuture<Void> finishedFuture = new CompletableFuture<>();
+
+ public ServletRequestReader(
+ ServletInputStream servletInputStream,
+ ContentChannel requestContentChannel,
+ Executor executor,
+ MetricReporter metricReporter) {
+
+ Preconditions.checkNotNull(servletInputStream);
+ Preconditions.checkNotNull(requestContentChannel);
+ Preconditions.checkNotNull(executor);
+ Preconditions.checkNotNull(metricReporter);
+
+ this.servletInputStream = servletInputStream;
+ this.requestContentChannel = requestContentChannel;
+ this.executor = executor;
+ this.metricReporter = metricReporter;
+ }
+
+ @Override
+ public void onDataAvailable() throws IOException {
+ while (servletInputStream.isReady()) {
+ final int estimatedNumBytesAvailable = servletInputStream.available();
+ final int bufferSizeBytes = Math.max(estimatedNumBytesAvailable, MIN_BUFFER_SIZE_BYTES);
+ final byte[] buffer = new byte[bufferSizeBytes];
+ final int numBytesRead = servletInputStream.read(buffer);
+ if (numBytesRead < 0) {
+ // End of stream; there should be no more data available, ever.
+ return;
+ }
+ writeRequestContent(ByteBuffer.wrap(buffer, 0, numBytesRead));
+ }
+ }
+
+ private void writeRequestContent(final ByteBuffer buf) {
+ synchronized (monitor) {
+ if (state != State.READING) {
+ //We have a failure, so no point in giving the buffer to the user.
+ assert finishedFuture.isCompletedExceptionally();
+ return;
+ }
+ //wait for both
+ // - requestContentChannel.write to finish
+ // - the write completion handler to be called
+ numberOfOutstandingUserCalls += 2;
+ }
+ try {
+ requestContentChannel.write(buf, writeCompletionHandler);
+
+ int bytesReceived = buf.remaining();
+ metricReporter.successfulRead(bytesReceived);
+ } catch (final Throwable t) {
+ finishedFuture.completeExceptionally(t);
+ } finally {
+ //decrease due to this method completing.
+ decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally();
+ }
+ }
+
+ private void decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally() {
+ final boolean shouldCloseRequestContentChannel;
+
+ synchronized (monitor) {
+ assertStateNotEquals(state, State.REQUEST_CONTENT_CLOSED);
+
+
+ numberOfOutstandingUserCalls -= 1;
+
+ shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0 &&
+ (finishedFuture.isDone() || state == State.ALL_DATA_READ);
+
+ if (shouldCloseRequestContentChannel) {
+ state = State.REQUEST_CONTENT_CLOSED;
+ }
+ }
+
+ if (shouldCloseRequestContentChannel) {
+ executor.execute(this::closeCompletionHandler_noThrow);
+ }
+ }
+
+ private void assertStateNotEquals(State state, State notExpectedState) {
+ if (state == notExpectedState) {
+ AssertionError e = new AssertionError("State should not be " + notExpectedState);
+ log.log(Level.WARNING,
+ "Assertion failed. " +
+ "numberOfOutstandingUserCalls = " + numberOfOutstandingUserCalls +
+ ", isDone = " + finishedFuture.isDone(),
+ e);
+ throw e;
+ }
+ }
+
+ @Override
+ public void onAllDataRead() {
+ doneReading();
+ }
+
+ private void doneReading() {
+ final boolean shouldCloseRequestContentChannel;
+
+ synchronized (monitor) {
+ if (state != State.READING) {
+ return;
+ }
+
+ state = State.ALL_DATA_READ;
+
+ shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0;
+ if (shouldCloseRequestContentChannel) {
+ state = State.REQUEST_CONTENT_CLOSED;
+ }
+ }
+
+ if (shouldCloseRequestContentChannel) {
+ closeCompletionHandler_noThrow();
+ }
+ }
+
+ private void closeCompletionHandler_noThrow() {
+ //Cannot complete finishedFuture directly in completed(), as any exceptions after this fact will be ignored.
+ // E.g.
+ // close(CompletionHandler completionHandler) {
+ // completionHandler.completed();
+ // throw new RuntimeException
+ // }
+
+ CompletableFuture<Void> completedCalledFuture = new CompletableFuture<>();
+
+ CompletionHandler closeCompletionHandler = new CompletionHandler() {
+ @Override
+ public void completed() {
+ completedCalledFuture.complete(null);
+ }
+
+ @Override
+ public void failed(final Throwable t) {
+ finishedFuture.completeExceptionally(t);
+ }
+ };
+
+ try {
+ requestContentChannel.close(closeCompletionHandler);
+ //if close did not cause an exception,
+ // is it safe to pipe the result of the completionHandlerInvokedFuture into finishedFuture
+ completedCalledFuture.whenComplete(this::setFinishedFuture);
+ } catch (final Throwable t) {
+ finishedFuture.completeExceptionally(t);
+ }
+ }
+
+ private void setFinishedFuture(Void result, Throwable throwable) {
+ if (throwable != null) {
+ finishedFuture.completeExceptionally(throwable);
+ } else {
+ finishedFuture.complete(null);
+ }
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ finishedFuture.completeExceptionally(t);
+ doneReading();
+ }
+
+ private final CompletionHandler writeCompletionHandler = new CompletionHandler() {
+ @Override
+ public void completed() {
+ decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally();
+ }
+
+ @Override
+ public void failed(final Throwable t) {
+ finishedFuture.completeExceptionally(t);
+ decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally();
+ }
+ };
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java
new file mode 100644
index 00000000000..b0781c402d5
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java
@@ -0,0 +1,213 @@
+// Copyright 2016 Yahoo Inc. 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.Response;
+import com.yahoo.jdisc.handler.BindingNotFoundException;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.service.BindingSetNotFoundException;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author tonytv
+ */
+public class ServletResponseController {
+ private static Logger log = Logger.getLogger(ServletResponseController.class.getName());
+
+ /**
+ * The servlet spec does not require (Http)ServletResponse nor ServletOutputStream to be thread-safe. Therefore,
+ * we must provide our own synchronization, since we may attempt to access these objects simultaneously from
+ * different threads. (The typical cause of this is when one thread is writing a response while another thread
+ * throws an exception, causing the request to fail with an error response).
+ */
+ private final Object monitor = new Object();
+
+ //servletResponse must not be modified after the response has been committed.
+ private final HttpServletResponse servletResponse;
+ private final boolean developerMode;
+
+ //all calls to the servletOutputStreamWriter must hold the monitor first to ensure visibility of servletResponse changes.
+ private final ServletOutputStreamWriter servletOutputStreamWriter;
+
+ @GuardedBy("monitor")
+ private boolean responseCommitted = false;
+
+
+ public ServletResponseController(
+ HttpServletResponse servletResponse,
+ Executor executor,
+ MetricReporter metricReporter,
+ boolean developerMode) throws IOException {
+
+ this.servletResponse = servletResponse;
+ this.developerMode = developerMode;
+ this.servletOutputStreamWriter =
+ new ServletOutputStreamWriter(servletResponse.getOutputStream(), executor, metricReporter);
+ }
+
+
+ private static int getStatusCode(Throwable t) {
+ if (t instanceof BindingNotFoundException) {
+ return HttpServletResponse.SC_NOT_FOUND;
+ } else if (t instanceof BindingSetNotFoundException) {
+ return HttpServletResponse.SC_NOT_FOUND;
+ } else if (t instanceof RequestException) {
+ return ((RequestException)t).getResponseStatus();
+ } else {
+ return HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+ }
+ }
+
+ private static String getReasonPhrase(Throwable t, boolean developerMode) {
+ if (developerMode) {
+ final StringWriter out = new StringWriter();
+ t.printStackTrace(new PrintWriter(out));
+ return out.toString();
+ } else if (t.getMessage() != null) {
+ return t.getMessage();
+ } else {
+ return t.toString();
+ }
+ }
+
+
+ public void trySendError(Throwable t) {
+ final boolean responseWasCommitted;
+
+ synchronized (monitor) {
+ responseWasCommitted = responseCommitted;
+
+ if (!responseCommitted) {
+ responseCommitted = true;
+ servletOutputStreamWriter.setSendingError();
+ }
+ }
+
+ //Must be evaluated after state transition for test purposes(See ConformanceTestException)
+ //Done outside the monitor since it causes a callback in tests.
+ String reasonPhrase = getReasonPhrase(t, developerMode);
+ int statusCode = getStatusCode(t);
+
+ if (responseWasCommitted) {
+
+ RuntimeException exceptionWithStackTrace = new RuntimeException(t);
+ log.log(Level.FINE, "Response already committed, can't change response code", exceptionWithStackTrace);
+ // TODO: should always have failed here, but that breaks test assumptions. Doing soft close instead.
+ //assert !Thread.holdsLock(monitor);
+ //servletOutputStreamWriter.fail(t);
+ servletOutputStreamWriter.close(null);
+ return;
+ }
+
+ try {
+ servletResponse.sendError(
+ statusCode,
+ reasonPhrase);
+ finishedFuture().complete(null);
+ } catch (Throwable e) {
+ servletOutputStreamWriter.fail(t);
+ }
+ }
+
+ /**
+ * When this future completes there will be no more calls against the servlet output stream or servlet response.
+ * The framework is still allowed to invoke us though.
+ *
+ * The future might complete in the servlet framework thread, user thread or executor thread.
+ */
+ public CompletableFuture<Void> finishedFuture() {
+ return servletOutputStreamWriter.finishedFuture;
+ }
+
+ private void setResponse(Response jdiscResponse) {
+ synchronized (monitor) {
+ if (responseCommitted) {
+ log.log(Level.FINE,
+ jdiscResponse.getError(),
+ () -> "Response already committed, can't change response code. " +
+ "From: " + servletResponse.getStatus() + ", To: " + jdiscResponse.getStatus());
+
+ //TODO: should throw an exception here, but this breaks unit tests.
+ //The failures will now instead happen when writing buffers.
+ servletOutputStreamWriter.close(null);
+ return;
+ }
+
+ setStatus_holdingLock(jdiscResponse, servletResponse);
+ setHeaders_holdingLock(jdiscResponse, servletResponse);
+ }
+ }
+
+ private static void setHeaders_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) {
+ for (final Map.Entry<String, String> entry : jdiscResponse.headers().entries()) {
+ final String value = entry.getValue();
+ servletResponse.addHeader(entry.getKey(), value != null ? value : "");
+ }
+
+ if (servletResponse.getContentType() == null) {
+ servletResponse.setContentType("text/plain;charset=utf-8");
+ }
+ }
+
+ private static void setStatus_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) {
+ if (jdiscResponse instanceof HttpResponse) {
+ servletResponse.setStatus(jdiscResponse.getStatus(), ((HttpResponse) jdiscResponse).getMessage());
+ } else {
+ Optional<String> errorMessage = getErrorMessage(jdiscResponse);
+ if (errorMessage.isPresent()) {
+ servletResponse.setStatus(jdiscResponse.getStatus(), errorMessage.get());
+ } else {
+ servletResponse.setStatus(jdiscResponse.getStatus());
+ }
+ }
+ }
+
+ private static Optional<String> getErrorMessage(Response jdiscResponse) {
+ return Optional.ofNullable(jdiscResponse.getError()).flatMap(
+ error -> Optional.ofNullable(error.getMessage()));
+ }
+
+
+ private void commitResponse() {
+ synchronized (monitor) {
+ responseCommitted = true;
+ }
+ }
+
+ public final ResponseHandler responseHandler = new ResponseHandler() {
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ setResponse(response);
+ return responseContentChannel;
+ }
+ };
+
+ public final ContentChannel responseContentChannel = new ContentChannel() {
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ commitResponse();
+ servletOutputStreamWriter.writeBuffer(buf, handler);
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ commitResponse();
+ servletOutputStreamWriter.close(handler);
+ }
+ };
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java
new file mode 100644
index 00000000000..1a34a3b81c3
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java
@@ -0,0 +1,32 @@
+// Copyright 2016 Yahoo Inc. 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.handler.ResponseHandler;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+
+/**
+ * @author tonytv
+ */
+public class UnsupportedFilterInvoker implements FilterInvoker {
+ @Override
+ public HttpServletRequest invokeRequestFilterChain(RequestFilter requestFilterChain,
+ URI uri,
+ HttpServletRequest httpRequest,
+ ResponseHandler responseHandler) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void invokeResponseFilterChain(
+ ResponseFilter responseFilterChain,
+ URI uri,
+ HttpServletRequest request,
+ HttpServletResponse response) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java
new file mode 100644
index 00000000000..8c15582c80e
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestDispatch.java
@@ -0,0 +1,419 @@
+// Copyright 2016 Yahoo Inc. 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.common.base.Preconditions;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.References;
+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.RequestHandler;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.StatusCode;
+import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+
+import javax.annotation.concurrent.GuardedBy;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.17.0
+ */
+class WebSocketRequestDispatch extends WebSocketAdapter {
+
+ private final static Logger log = Logger.getLogger(WebSocketRequestDispatch.class.getName());
+ private final static ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
+
+ private final AtomicReference<Object> responseRef = new AtomicReference<>();
+ private final CurrentContainer container;
+ private final Executor janitor;
+ private final RequestHandler requestHandler;
+ private final Metric metric;
+ private final Metric.Context metricCtx;
+ private final Object lock = new Object();
+ private final CompletionHandler failureHandlingCompletionHandler = new CompletionHandler() {
+ @Override
+ public void completed() {
+ }
+
+ @Override
+ public void failed(final Throwable t) {
+ synchronized (lock) {
+ fail_holdingLock(t);
+ }
+ }
+ };
+
+ @GuardedBy("lock")
+ private final Deque<ResponseContentPart> responseContentQueue = new ArrayDeque<>();
+ @GuardedBy("lock")
+ private ContentChannel requestContent;
+ @GuardedBy("lock")
+ private Throwable failure;
+ @GuardedBy("lock")
+ private boolean writingResponse = false;
+ @GuardedBy("lock")
+ private boolean connected;
+
+ public WebSocketRequestDispatch(
+ final CurrentContainer container,
+ final Executor janitor,
+ final Metric metric,
+ final Metric.Context metricCtx) {
+ Objects.requireNonNull(janitor, "janitor");
+ Objects.requireNonNull(metric, "metric");
+ this.container = container;
+ this.requestHandler = new AbstractRequestHandler() {
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ return request.connect(handler);
+ }
+ };
+ this.janitor = janitor;
+ this.metric = metric;
+ this.metricCtx = metricCtx;
+ }
+
+ public WebSocketRequestDispatch dispatch(final ServletUpgradeRequest servletRequest,
+ final ServletUpgradeResponse servletResponse) {
+ final HttpRequest jdiscRequest = WebSocketRequestFactory.newJDiscRequest(container, servletRequest);
+ try (final ResourceReference ref = References.fromResource(jdiscRequest)) {
+ WebSocketRequestFactory.copyHeaders(servletRequest, jdiscRequest);
+ dispatchRequestWithoutThrowing(jdiscRequest);
+ }
+ final Response jdiscResponse = (Response)responseRef.getAndSet(new Object());
+ if (jdiscResponse != null) {
+ log.finer("Applying sync " + jdiscResponse.getStatus() + " response to websocket response.");
+ servletResponse.setStatus(jdiscResponse.getStatus());
+ WebSocketRequestFactory.copyHeaders(jdiscResponse, servletResponse);
+ }
+ return this;
+ }
+
+ @Override
+ public void onWebSocketBinary(final byte[] arr, final int off, final int len) {
+ writeRequestContentWithoutThrowing(ByteBuffer.wrap(arr, off, len));
+ }
+
+ @Override
+ public void onWebSocketText(final String message) {
+ writeRequestContentWithoutThrowing(StandardCharsets.UTF_8.encode(message));
+ }
+
+ @Override
+ public void onWebSocketConnect(final Session session) {
+ super.onWebSocketConnect(session);
+ synchronized (lock) {
+ connected = true;
+ if (writingResponse) {
+ return;
+ }
+ writingResponse = true;
+ }
+ writeNextResponseContent();
+ }
+
+ /**
+ * This is ALWAYS called.
+ * ...if the remote side closes the connection
+ * ...if we c*ck up ourselves and throw an exception out of onWebSocketBinary() or onWebSocketText(),
+ * Jetty calls Session.close on our behalf (later followed by a call to onWebSocketError)
+ *
+ * TODO: Test below
+ * ...and also whenever we call Session.close() ourselves??
+ *
+ * @param statusCode The {@link StatusCode} of the close.
+ * @param reason The reason text for the close.
+ */
+ @Override
+ public void onWebSocketClose(final int statusCode, final String reason) {
+ super.onWebSocketClose(statusCode, reason);
+ final ContentChannel requestContentChannel;
+ synchronized (lock) {
+ Preconditions.checkState(requestContent != null || failure != null,
+ "requestContent should be non-null if we haven't had a failure");
+ if (requestContent == null) {
+ return;
+ }
+ if (failure != null) {
+ // Request content will be closed as a result of the failure handling.
+ return;
+ }
+ requestContentChannel = requestContent;
+ requestContent = null;
+ }
+ try {
+ requestContentChannel.close(failureHandlingCompletionHandler);
+ } catch (final Throwable t) {
+ fail(t);
+ }
+ }
+
+ /**
+ * <p>No need to call Session.close() here, that has been done or will be done by Jetty.</p>
+ *
+ * @param t The cause of the error.
+ */
+ @Override
+ public void onWebSocketError(final Throwable t) {
+ fail(t);
+ }
+
+ private void dispatchRequestWithoutThrowing(final Request request) {
+ final ContentChannel returnedContentChannel;
+ try {
+ returnedContentChannel = requestHandler.handleRequest(request, new GatedResponseHandler());
+ } catch (final Throwable t) {
+ fail(t);
+ throw new IllegalStateException(t);
+ }
+ synchronized (lock) {
+ Preconditions.checkState(requestContent == null, "requestContent should be null");
+ if (failure != null) {
+ // This means that request.connect() caused a synchronous failure. in this case
+ // the cleanup happened before requestContent was assigned, so we must clean it explicitly here
+ closeLater(returnedContentChannel);
+ throw new IllegalStateException(failure);
+ }
+ requestContent = returnedContentChannel;
+ }
+ }
+
+ private void writeRequestContentWithoutThrowing(final ByteBuffer buf) {
+ int bytes_received = buf.remaining();
+ metric.set(JettyHttpServer.Metrics.NUM_BYTES_RECEIVED, bytes_received, metricCtx);
+ metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_RECEIVED, bytes_received, metricCtx);
+ final ContentChannel requestContentChannel;
+ synchronized (lock) {
+ Preconditions.checkState(requestContent != null, "requestContent should be non-null");
+ if (failure != null) {
+ return;
+ }
+ requestContentChannel = requestContent;
+ }
+ try {
+ requestContentChannel.write(buf, failureHandlingCompletionHandler);
+ } catch (final Throwable t) {
+ fail(t);
+ }
+ }
+
+ private void fail(final Throwable t) {
+ synchronized (lock) {
+ fail_holdingLock(t);
+ }
+ }
+
+ private void tryWriteResponseContent(final ByteBuffer buf, final CompletionHandler handler) {
+ synchronized (lock) {
+ if (failure != null) {
+ failLater(handler, failure);
+ return;
+ }
+ responseContentQueue.addLast(new ResponseContentPart(buf, handler));
+ if (writingResponse) {
+ return;
+ }
+ writingResponse = true;
+ }
+ writeNextResponseContent();
+ }
+
+ private void writeNextResponseContent() {
+ while (true) {
+ final ResponseContentPart part;
+ synchronized (lock) {
+ if (!connected) {
+ // We expect a later invocation of onWebSocketConnect(). That will invoke this method again.
+ writingResponse = false;
+ return;
+ }
+ if (responseContentQueue.isEmpty()) {
+ writingResponse = false;
+ return; // application will call later
+ }
+ part = responseContentQueue.poll();
+ }
+ if (part.handler != null) {
+ try {
+ part.handler.completed();
+ } catch (final Throwable t) {
+ fail(t);
+ return;
+ }
+ }
+ final boolean isClosePart = part.buf == null;
+ if (isClosePart) {
+ return;
+ }
+ try {
+ getRemote().sendBytesByFuture(part.buf);
+ } catch (final Throwable t) {
+ fail(t);
+ }
+ }
+ }
+
+ private void fail_holdingLock(final Throwable failure) {
+ if (this.failure != null) {
+ return;
+ }
+ this.failure = failure;
+ if (requestContent != null) {
+ closeLater(requestContent);
+ }
+ requestContent = null;
+ for (ResponseContentPart part = responseContentQueue.poll(); part != null; part = responseContentQueue.poll()) {
+ failLater(part.handler, failure);
+ }
+ janitor.execute(() -> {
+ try {
+ getSession().close(StatusCode.SERVER_ERROR, failure.toString());
+ } catch (final Throwable ignored) {
+ }
+ });
+ }
+
+ private void closeLater(final ContentChannel content) {
+ janitor.execute(() -> {
+ try {
+ content.close(NOOP_COMPLETION_HANDLER);
+ } catch (final Throwable ignored) {
+ }
+ });
+ }
+
+ private void failLater(final CompletionHandler handler, final Throwable failure) {
+ if (handler == null) {
+ return;
+ }
+
+ final Throwable failureWithStack = new IllegalStateException(failure);
+ janitor.execute(() -> {
+ try {
+ handler.failed(failureWithStack);
+ } catch (final Throwable t) {
+ log.log(Level.WARNING, "Failure handling of " + failure +
+ " in application threw an exception.", t);
+ }
+ });
+ }
+
+ private static final CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() {
+ @Override public void completed() {}
+ @Override public void failed(final Throwable t) {}
+ };
+
+ private class GatedResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(final Response response) {
+ synchronized (lock) {
+ if (failure != null) {
+ return new FailedResponseContent(new IllegalStateException(failure));
+ }
+ }
+ final boolean firstToSetResponse = responseRef.compareAndSet(null, response);
+ if (!firstToSetResponse) {
+ log.finer("Ignoring async " + response.getStatus() + " response because sync websocket response has " +
+ "already been returned to client.");
+ // TODO(bakksjo): The message above is not necessarily correct. Getting here does not necessarily
+ // mean that a sync response has been returned to the client. It may just mean that dispatch() is
+ // finished, and the request handler's handleRequest() has been run. That does not mean that the
+ // request handler actually produced a sync response. If a response is produced asynchronously, we
+ // may get here and ignore that response. TODO: Analyze wire traffic. Maybe Jetty produces a response
+ // after dispatch(), even if we don't do it in our code. Besides, is the status code used for anything
+ // by the client anyway? Is it even available in client WebSocket implementations?
+ }
+ return new GatedResponseContent();
+ }
+ }
+
+ private class GatedResponseContent implements ContentChannel {
+
+ @Override
+ public void write(final ByteBuffer raw, final CompletionHandler handler) {
+ final ByteBuffer buf = raw != null ? raw : EMPTY_BUFFER;
+ int bytesSent = buf.remaining();
+ metric.set(JettyHttpServer.Metrics.NUM_BYTES_SENT, bytesSent, metricCtx);
+ metric.set(JettyHttpServer.Metrics.MANHATTAN_NUM_BYTES_SENT, bytesSent, metricCtx);
+ tryWriteResponseContent(buf, new MetricCompletionHandler(handler));
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ // The only reason to let this synthetic 'part' go into the queue is to have the completion handler
+ // for close() invoked in order (after the completion handlers for enqueued parts.
+ tryWriteResponseContent(null, new MetricCompletionHandler(handler));
+ }
+ }
+
+ private class FailedResponseContent implements ContentChannel {
+
+ final Throwable failure;
+
+ FailedResponseContent(final Throwable failure) {
+ this.failure = failure;
+ }
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ failLater(new MetricCompletionHandler(handler), failure);
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ failLater(new MetricCompletionHandler(handler), failure);
+ }
+ }
+
+ private class MetricCompletionHandler implements CompletionHandler {
+
+ final CompletionHandler delegate;
+
+ MetricCompletionHandler(CompletionHandler delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void completed() {
+ metric.add(JettyHttpServer.Metrics.NUM_SUCCESSFUL_WRITES, 1, metricCtx);
+ if (delegate != null)
+ delegate.completed();
+ }
+
+ @Override
+ public void failed(Throwable t) {
+ metric.add(JettyHttpServer.Metrics.NUM_FAILED_WRITES, 1, metricCtx);
+ if (delegate != null)
+ delegate.failed(t);
+ }
+ }
+
+ private static class ResponseContentPart {
+
+ final ByteBuffer buf;
+ final CompletionHandler handler;
+
+ ResponseContentPart(final ByteBuffer buf, final CompletionHandler handler) {
+ this.buf = buf;
+ this.handler = handler;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java
new file mode 100644
index 00000000000..8eebc11ce75
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/WebSocketRequestFactory.java
@@ -0,0 +1,38 @@
+// Copyright 2016 Yahoo Inc. 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.http.HttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
+import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
+
+import java.net.InetSocketAddress;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class WebSocketRequestFactory {
+
+ public static HttpRequest newJDiscRequest(final CurrentContainer container,
+ final ServletUpgradeRequest servletRequest) {
+ return HttpRequest.newServerRequest(
+ container,
+ servletRequest.getRequestURI(),
+ HttpRequest.Method.valueOf(servletRequest.getMethod()),
+ HttpRequest.Version.fromString(servletRequest.getHttpVersion()),
+ new InetSocketAddress(servletRequest.getRemoteAddress(), servletRequest.getRemotePort()));
+ }
+
+ public static void copyHeaders(final ServletUpgradeRequest from, final Request to) {
+ to.headers().addAll(from.getHeaders());
+ }
+
+ public static void copyHeaders(final Response from, final ServletUpgradeResponse to) {
+ for (final Map.Entry<String, String> entry : from.headers().entries()) {
+ to.addHeader(entry.getKey(), entry.getValue());
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java
new file mode 100644
index 00000000000..acebb1707a8
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java
@@ -0,0 +1,3 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@com.yahoo.osgi.annotation.ExportPackage
+package com.yahoo.jdisc.http.server.jetty;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java
new file mode 100644
index 00000000000..0fd783bc939
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.server;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java
new file mode 100644
index 00000000000..d98749c4cde
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java
@@ -0,0 +1,37 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.servlet;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.http.Cookie;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Common interface for JDisc and servlet http requests.
+ */
+public interface ServletOrJdiscHttpRequest {
+
+ public void copyHeaders(HeaderFields target);
+
+ public Map<String, List<String>> parameters();
+
+ public URI getUri();
+
+ public HttpRequest.Version getVersion();
+
+ public String getRemoteHostAddress();
+ public String getRemoteHostName();
+ public int getRemotePort();
+
+ public void setRemoteAddress(SocketAddress remoteAddress);
+
+ public Map<String, Object> context();
+
+ public List<Cookie> decodeCookieHeader();
+
+ public void encodeCookieHeader(List<Cookie> cookies);
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java
new file mode 100644
index 00000000000..afcb2861b1e
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java
@@ -0,0 +1,23 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.servlet;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.http.Cookie;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Common interface for JDisc and servlet http responses.
+ */
+public interface ServletOrJdiscHttpResponse {
+
+ public void copyHeaders(HeaderFields target);
+
+ public int getStatus();
+
+ public Map<String, Object> context();
+
+ public List<Cookie> decodeSetCookieHeader();
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java
new file mode 100644
index 00000000000..d4213452677
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java
@@ -0,0 +1,245 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.servlet;
+
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.http.Cookie;
+import com.yahoo.jdisc.http.HttpHeaders;
+import com.yahoo.jdisc.http.HttpRequest;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Mutable wrapper to use a {@link javax.servlet.http.HttpServletRequest}
+ * with JDisc security filters.
+ * <p>
+ * You might find it tempting to remove e.g. the getParameter... methods,
+ * but keep in mind that this IS-A servlet request and must provide the
+ * full api of such a request for use outside the "JDisc filter world".
+ *
+ * @since 5.27
+ */
+public class ServletRequest extends HttpServletRequestWrapper implements ServletOrJdiscHttpRequest {
+
+ private final HttpServletRequest request;
+ private final HeaderFields headerFields;
+ private final Set<String> headerBlacklist = new HashSet<>();
+ private final Map<String, Object> context = new HashMap<>();
+ private final Map<String, List<String>> parameters = new HashMap<>();
+
+ private URI uri;
+ private String remoteHostAddress;
+ private String remoteHostName;
+ private int remotePort;
+
+ public ServletRequest(HttpServletRequest request, URI uri) {
+ super(request);
+ this.request = request;
+
+ this.uri = uri;
+
+ super.getParameterMap().forEach(
+ (key, values) -> parameters.put(key, Arrays.asList(values)));
+
+ remoteHostAddress = request.getRemoteAddr();
+ remoteHostName = request.getRemoteHost();
+ remotePort = request.getRemotePort();
+
+ headerFields = new HeaderFields();
+ Enumeration<String> parentHeaders = request.getHeaderNames();
+ while (parentHeaders.hasMoreElements()) {
+ String name = parentHeaders.nextElement();
+ Enumeration<String> values = request.getHeaders(name);
+ while (values.hasMoreElements()) {
+ headerFields.add(name, values.nextElement());
+ }
+ }
+ }
+
+ public HttpServletRequest getRequest() {
+ return request;
+ }
+
+ @Override
+ public Map<String, List<String>> parameters() {
+ return parameters;
+ }
+
+ /* We cannot just return the parameter map from the request, as the map
+ * may have been modified by the JDisc filters. */
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ Map<String, String[]> parameterMap = new HashMap<>();
+ parameters().forEach(
+ (key, values) ->
+ parameterMap.put(key, values.toArray(new String[values.size()]))
+ );
+ return ImmutableMap.copyOf(parameterMap);
+ }
+
+ @Override
+ public String getParameter(String name) {
+ return parameters().containsKey(name) ?
+ parameters().get(name).get(0) :
+ null;
+ }
+
+ @Override
+ public Enumeration<String> getParameterNames() {
+ return Collections.enumeration(parameters.keySet());
+ }
+
+ @Override
+ public String[] getParameterValues(String name) {
+ List<String> values = parameters().get(name);
+ return values != null ?
+ values.toArray(new String[values.size()]) :
+ null;
+ }
+
+ @Override
+ public void copyHeaders(HeaderFields target) {
+ target.addAll(headerFields);
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ if (headerBlacklist.contains(name))
+ return null;
+
+ /* We don't need to merge headerFields and the servlet request's headers
+ * because setHeaders() replaces the old value. There is no 'addHeader(s)'. */
+ List<String> headerFields = this.headerFields.get(name);
+ return headerFields == null || headerFields.isEmpty() ?
+ super.getHeaders(name) :
+ Collections.enumeration(headerFields);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ if (headerBlacklist.contains(name))
+ return null;
+
+ String headerField = headerFields.getFirst(name);
+ return headerField != null ?
+ headerField :
+ super.getHeader(name);
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames() {
+ Set<String> names = new HashSet<>(Collections.list(super.getHeaderNames()));
+ names.addAll(headerFields.keySet());
+ names.removeAll(headerBlacklist);
+ return Collections.enumeration(names);
+ }
+
+ public void addHeader(String name, String value) {
+ headerFields.add(name, value);
+ headerBlacklist.remove(name);
+ }
+
+ public void setHeaders(String name, String value) {
+ headerFields.put(name, value);
+ headerBlacklist.remove(name);
+ }
+
+ public void setHeaders(String name, List<String> values) {
+ headerFields.put(name, values);
+ headerBlacklist.remove(name);
+ }
+
+ public void removeHeaders(String name) {
+ headerFields.remove(name);
+ headerBlacklist.add(name);
+ }
+
+ @Override
+ public URI getUri() {
+ return uri;
+ }
+
+ public void setUri(URI uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public HttpRequest.Version getVersion() {
+ String protocol = request.getProtocol();
+ try {
+ return HttpRequest.Version.fromString(protocol);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ throw new RuntimeException("Servlet request protocol '" + protocol +
+ "' could not be mapped to a JDisc http version.", e);
+ }
+ }
+
+ @Override
+ public String getRemoteHostAddress() {
+ return remoteHostAddress;
+ }
+
+ @Override
+ public String getRemoteHostName() {
+ return remoteHostName;
+ }
+
+ @Override
+ public int getRemotePort() {
+ return remotePort;
+ }
+
+ @Override
+ public void setRemoteAddress(SocketAddress remoteAddress) {
+ if (remoteAddress instanceof InetSocketAddress) {
+ remoteHostAddress = ((InetSocketAddress) remoteAddress).getAddress().getHostAddress();
+ remoteHostName = ((InetSocketAddress) remoteAddress).getAddress().getHostName();
+ remotePort = ((InetSocketAddress) remoteAddress).getPort();
+ } else
+ throw new RuntimeException("Unknown SocketAddress class: " + remoteHostAddress.getClass().getName());
+
+ }
+
+ @Override
+ public Map<String, Object> context() {
+ return context;
+ }
+
+ @Override
+ public javax.servlet.http.Cookie[] getCookies() {
+ return decodeCookieHeader().stream().
+ map(jdiscCookie -> new javax.servlet.http.Cookie(jdiscCookie.getName(), jdiscCookie.getValue())).
+ toArray(javax.servlet.http.Cookie[]::new);
+ }
+
+ @Override
+ public List<Cookie> decodeCookieHeader() {
+ Enumeration<String> cookies = getHeaders(HttpHeaders.Names.COOKIE);
+ if (cookies == null)
+ return Collections.emptyList();
+
+ List<Cookie> ret = new LinkedList<>();
+ while(cookies.hasMoreElements())
+ ret.addAll(Cookie.fromCookieHeader(cookies.nextElement()));
+
+ return ret;
+ }
+
+ @Override
+ public void encodeCookieHeader(List<Cookie> cookies) {
+ setHeaders(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies));
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java
new file mode 100644
index 00000000000..be5a3f67886
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.servlet;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.http.Cookie;
+import com.yahoo.jdisc.http.HttpHeaders;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JDisc wrapper to use a {@link javax.servlet.http.HttpServletResponse}
+ * with JDisc security filters.
+ *
+ * @since 5.26
+ */
+public class ServletResponse extends HttpServletResponseWrapper implements ServletOrJdiscHttpResponse {
+
+ private final HttpServletResponse response;
+ private final Map<String, Object> context = new HashMap<>();
+
+ public ServletResponse(HttpServletResponse response) {
+ super(response);
+ this.response = response;
+ }
+
+ public HttpServletResponse getResponse() {
+ return response;
+ }
+
+ @Override
+ public int getStatus() {
+ return response.getStatus();
+ }
+
+ @Override
+ public Map<String, Object> context() {
+ return context;
+ }
+
+ @Override
+ public void copyHeaders(HeaderFields target) {
+ response.getHeaderNames().forEach( header ->
+ target.add(header, new ArrayList<>(response.getHeaders(header)))
+ );
+ }
+
+ @Override
+ public List<Cookie> decodeSetCookieHeader() {
+ Collection<String> cookies = getHeaders(HttpHeaders.Names.SET_COOKIE);
+ if (cookies == null) {
+ return Collections.emptyList();
+ }
+ List<Cookie> ret = new LinkedList<>();
+ for (String cookie : cookies) {
+ ret.addAll(Cookie.fromSetCookieHeader(cookie));
+ }
+ return ret;
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java
new file mode 100644
index 00000000000..8aa50caac99
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.servlet;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java
new file mode 100644
index 00000000000..d9eebbeedc6
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/JKSKeyStore.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.ssl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+
+/**
+ * @author tonytv
+ */
+public class JKSKeyStore extends SslKeyStore {
+
+ private static final String keyStoreType = "JKS";
+ private final Path keyStoreFile;
+
+ public JKSKeyStore(Path keyStoreFile) {
+ this.keyStoreFile = keyStoreFile;
+ }
+
+ @Override
+ public KeyStore loadJavaKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
+ try(InputStream stream = Files.newInputStream(keyStoreFile)) {
+ KeyStore keystore = KeyStore.getInstance(keyStoreType);
+ keystore.load(stream, getKeyStorePassword().map(String::toCharArray).orElse(null));
+ return keystore;
+ }
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java
new file mode 100644
index 00000000000..8a3ac08a1cd
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/ReaderForPath.java
@@ -0,0 +1,22 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.ssl;
+
+import java.io.Reader;
+import java.nio.file.Path;
+
+/**
+ * A reader along with the path used to construct it.
+ *
+ * @author tonytv
+ */
+public final class ReaderForPath {
+
+ public final Reader reader;
+ public final Path path;
+
+ public ReaderForPath(Reader reader, Path path) {
+ this.reader = reader;
+ this.path = path;
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java
new file mode 100644
index 00000000000..93cf6683ed5
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactory.java
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.ssl;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:charlesk@yahoo-inc.com">Charles Kim</a>
+ */
+public class SslContextFactory {
+
+ private static final Logger log = Logger.getLogger(SslContextFactory.class.getName());
+ private static final String DEFAULT_ALGORITHM = "SunX509";
+ private static final String DEFAULT_PROTOCOL = "TLS";
+ private final SSLContext sslContext;
+
+ private SslContextFactory(SSLContext sslContext) {
+ this.sslContext = sslContext;
+ }
+
+ public SSLContext getServerSSLContext() {
+ return this.sslContext;
+ }
+
+ public static SslContextFactory newInstanceFromTrustStore(SslKeyStore trustStore) {
+ return newInstance(DEFAULT_ALGORITHM, DEFAULT_PROTOCOL, null, trustStore);
+ }
+
+ public static SslContextFactory newInstance(SslKeyStore trustStore, SslKeyStore keyStore) {
+ return newInstance(DEFAULT_ALGORITHM, DEFAULT_PROTOCOL, keyStore, trustStore);
+ }
+
+ public static SslContextFactory newInstance(String sslAlgorithm, String sslProtocol,
+ SslKeyStore keyStore, SslKeyStore trustStore) {
+ log.fine("Configuring SSLContext...");
+ log.fine("Using " + sslAlgorithm + " algorithm.");
+ try {
+ SSLContext sslContext = SSLContext.getInstance(sslProtocol);
+ sslContext.init(
+ keyStore == null ? null : getKeyManagers(keyStore, sslAlgorithm),
+ trustStore == null ? null : getTrustManagers(trustStore, sslAlgorithm),
+ null);
+ return new SslContextFactory(sslContext);
+ } catch (Exception e) {
+ log.log(Level.SEVERE, "Got exception creating SSLContext.", e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Used for the key store, which contains the SSL cert and private key.
+ */
+ public static javax.net.ssl.KeyManager[] getKeyManagers(SslKeyStore keyStore,
+ String sslAlgorithm)
+ throws NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException,
+ KeyStoreException {
+
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(sslAlgorithm);
+ keyManagerFactory.init(
+ keyStore.loadJavaKeyStore(),
+ keyStore.getKeyStorePassword().map(String::toCharArray).orElse(null));
+ log.fine("KeyManagerFactory initialized with keystore");
+ return keyManagerFactory.getKeyManagers();
+ }
+
+ /**
+ * Used for the trust store, which contains certificates from other parties that you expect to communicate with,
+ * or from Certificate Authorities that you trust to identify other parties.
+ */
+ public static javax.net.ssl.TrustManager[] getTrustManagers(SslKeyStore trustStore,
+ String sslAlgorithm)
+ throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
+
+ TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(sslAlgorithm);
+ trustManagerFactory.init(trustStore.loadJavaKeyStore());
+ log.fine("TrustManagerFactory initialized with truststore.");
+ return trustManagerFactory.getTrustManagers();
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java
new file mode 100644
index 00000000000..de65618a942
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStore.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.ssl;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.util.Optional;
+
+/**
+ *
+ * @author <a href="mailto:charlesk@yahoo-inc.com">Charles Kim</a>
+ */
+public abstract class SslKeyStore {
+
+ private Optional<String> keyStorePassword = Optional.empty();
+
+ public Optional<String> getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ public void setKeyStorePassword(String keyStorePassword) {
+ this.keyStorePassword = Optional.of(keyStorePassword);
+ }
+
+ public abstract KeyStore loadJavaKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException;
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java
new file mode 100644
index 00000000000..4d5a5b1c806
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslKeyStoreFactory.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.ssl;
+
+import java.nio.file.Paths;
+
+/**
+ * A factory for SSL key stores.
+ *
+ * @author bratseth
+ */
+public interface SslKeyStoreFactory {
+
+ SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile);
+
+ SslKeyStore createTrustStore(ReaderForPath certificateFile);
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java
new file mode 100644
index 00000000000..251a355d19b
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java
@@ -0,0 +1,4 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.jdisc.http.ssl;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java
new file mode 100644
index 00000000000..f045dbb0dca
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ChunkReader.java
@@ -0,0 +1,124 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ChunkReader {
+
+ private static final Pattern CONTENT_LENGTH = Pattern.compile(".+^content-length: (\\d+)$.*",
+ Pattern.CASE_INSENSITIVE |
+ Pattern.MULTILINE |
+ Pattern.DOTALL);
+ private static final Pattern CHUNKED_ENCODING = Pattern.compile(".+^transfer-encoding: chunked$.*",
+ Pattern.CASE_INSENSITIVE |
+ Pattern.MULTILINE |
+ Pattern.DOTALL);
+ private final InputStream in;
+ private StringBuilder reading = new StringBuilder();
+ private boolean readingHeader = true;
+
+ public ChunkReader(InputStream in) {
+ this.in = in;
+ }
+
+ public boolean isEndOfContent() throws IOException {
+ if (in.available() != 0) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(in.available()).append(": ");
+ for(int c = in.read(); c != -1; c = in.read()) {
+ sb.append('\'');
+ sb.append(c);
+ sb.append("' ");
+ }
+ throw new IllegalStateException("This is not the end '" + sb.toString());
+ }
+ return in.available() == 0;
+ }
+
+ public String readChunk() throws IOException {
+ while (true) {
+ String ret = removeNextChunk();
+ if (ret != null) {
+ return ret;
+ }
+ readFromStream();
+ }
+ }
+
+ private String readContent(int length) throws IOException {
+ while (reading.length() < length) {
+ readFromStream();
+ }
+ return splitReadBuffer(length);
+ }
+
+ private void readFromStream() throws IOException {
+ byte[] buf = new byte[4096];
+ try {
+ while (!Thread.currentThread().isInterrupted()) {
+ int len = in.read(buf, 0, buf.length);
+ if (len < 0) {
+ throw new IOException("Socket is closed.");
+ }
+ if (len > 0) {
+ reading.append(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(buf, 0, len)));
+ break;
+ }
+ Thread.sleep(10);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private String removeNextChunk() throws IOException {
+ if (readingHeader) {
+ int pos = reading.indexOf("\r\n\r\n");
+ if (pos < 0) {
+ return null;
+ }
+ String ret = splitReadBuffer(pos + 4);
+ Matcher m = CONTENT_LENGTH.matcher(ret);
+ if (m.matches()) {
+ ret += readContent(Integer.valueOf(m.group(1)));
+ }
+ readingHeader = !CHUNKED_ENCODING.matcher(ret).matches();
+ return ret;
+ } else if (reading.indexOf("0\r\n") == 0) {
+ int pos = reading.indexOf("\r\n\r\n", 1);
+ if (pos < 0) {
+ return null;
+ }
+ readingHeader = true;
+ return splitReadBuffer(pos + 4);
+ } else {
+ int pos = reading.indexOf("\r\n");
+ if (pos < 0) {
+ return null;
+ }
+ pos = reading.indexOf("\r\n", pos + 2);
+ if (pos < 0) {
+ return null;
+ }
+ return splitReadBuffer(pos + 2);
+ }
+ }
+
+ private String splitReadBuffer(int pos) {
+ String ret = reading.substring(0, pos);
+ if (pos < reading.length()) {
+ reading = new StringBuilder(reading.substring(pos));
+ } else {
+ reading = new StringBuilder();
+ }
+ return ret;
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java
new file mode 100644
index 00000000000..1a5553fb608
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ClientTestDriver.java
@@ -0,0 +1,119 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import com.ning.http.util.AllowAllHostnameVerifier;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.http.client.HttpClient;
+import com.yahoo.jdisc.http.client.HttpClientConfig;
+import com.yahoo.jdisc.http.client.filter.ResponseFilter;
+import com.yahoo.jdisc.service.CurrentContainer;
+import com.yahoo.jdisc.test.TestDriver;
+
+import javax.net.ssl.HostnameVerifier;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ClientTestDriver {
+
+ private final TestDriver driver;
+ private final HttpClient client;
+ private final RemoteServer server;
+
+ private ClientTestDriver(TestDriver driver, HttpClient client) throws IOException {
+ this.driver = driver;
+ this.client = client;
+ this.server = RemoteServer.newInstance();
+ }
+
+ public CurrentContainer currentContainer() {
+ return driver;
+ }
+
+ public boolean close() {
+ if (!server.close(60, TimeUnit.SECONDS)) {
+ return false;
+ }
+ client.release();
+ return driver.close();
+ }
+
+ public HttpClient client() {
+ return client;
+ }
+
+ public RemoteServer server() {
+ return server;
+ }
+
+ public static ClientTestDriver newInstance(Module... guiceModules) throws IOException {
+ return newInstance(new HttpClientConfig.Builder().sslConnectionPoolEnabled(false),
+ guiceModules);
+ }
+
+ public static ClientTestDriver newInstance(HttpClientConfig.Builder config, Module... guiceModules)
+ throws IOException {
+ Module[] lst = new Module[guiceModules.length + 2];
+ lst[0] = newDefaultModule();
+ lst[lst.length - 1] = newConfigModule(config);
+ System.arraycopy(guiceModules, 0, lst, 1, guiceModules.length);
+ return newInstanceImpl(HttpClient.class, lst);
+ }
+
+ private static ClientTestDriver newInstanceImpl(Class<? extends HttpClient> clientClass,
+ Module... guiceModules) throws IOException {
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(guiceModules);
+ ContainerBuilder builder = driver.newContainerBuilder();
+ HttpClient client = builder.guiceModules().getInstance(clientClass);
+ builder.serverBindings().bind("*://*/*", client);
+ driver.activateContainer(builder);
+ try {
+ client.start();
+ } catch (RuntimeException e) {
+ client.release();
+ driver.close();
+ throw e;
+ }
+ return new ClientTestDriver(driver, client);
+ }
+
+ public static Module newDefaultModule() {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(HostnameVerifier.class).to(AllowAllHostnameVerifier.class);
+ bind(new TypeLiteral<List<ResponseFilter>>() { }).toInstance(Collections.<ResponseFilter>emptyList());
+ }
+ };
+ }
+
+ public static Module newConfigModule(final HttpClientConfig.Builder config) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(HttpClientConfig.class).toInstance(new HttpClientConfig(config));
+ }
+ };
+ }
+
+ public static Module newFilterModule(final ResponseFilter... filters) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(new TypeLiteral<List<ResponseFilter>>() { }).toInstance(Arrays.asList(filters));
+ }
+ };
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java
new file mode 100644
index 00000000000..f752ac86dd0
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/FilterTestDriver.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+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.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+
+import java.io.IOException;
+import java.util.concurrent.Exchanger;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static com.yahoo.jdisc.http.test.ServerTestDriver.newFilterModule;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ *
+ * TODO: dead code?
+ */
+public class FilterTestDriver {
+
+ private final ServerTestDriver driver;
+ private final MyRequestHandler requestHandler;
+
+ private FilterTestDriver(ServerTestDriver driver, MyRequestHandler requestHandler) {
+ this.driver = driver;
+ this.requestHandler = requestHandler;
+ }
+
+ public boolean close() throws IOException {
+ return driver.close();
+ }
+
+ public HttpRequest filterRequest(String request) throws IOException, TimeoutException, InterruptedException {
+ driver.client().writeRequest(request);
+ return (HttpRequest)requestHandler.exchanger.exchange(null, 60, TimeUnit.SECONDS);
+ }
+
+ public static FilterTestDriver newInstance(final BindingRepository<RequestFilter> requestFilters,
+ final BindingRepository<ResponseFilter> responseFilters)
+ throws IOException {
+ MyRequestHandler handler = new MyRequestHandler();
+ return new FilterTestDriver(ServerTestDriver.newInstance(handler,
+ newFilterModule(requestFilters, responseFilters)),
+ handler);
+ }
+
+ private static class MyRequestHandler extends AbstractRequestHandler {
+
+ final Exchanger<Request> exchanger = new Exchanger<>();
+
+ @Override
+ public ContentChannel handleRequest(Request request, ResponseHandler handler) {
+ ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler);
+ try {
+ exchanger.exchange(request);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java
new file mode 100644
index 00000000000..e2c1a2a33d5
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteClient.java
@@ -0,0 +1,53 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import com.yahoo.jdisc.http.server.jetty.JettyHttpServer;
+import com.yahoo.jdisc.http.ssl.SslContextFactory;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class RemoteClient extends ChunkReader {
+
+ private final Socket socket;
+
+ private RemoteClient(Socket socket) throws IOException {
+ super(socket.getInputStream());
+ this.socket = socket;
+ }
+
+ public void close() throws IOException {
+ socket.close();
+ }
+
+ public void writeRequest(String request) throws IOException {
+ socket.getOutputStream().write(request.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public static RemoteClient newInstance(JettyHttpServer server) throws IOException {
+ return newInstance(server.getListenPort());
+ }
+
+ public static RemoteClient newInstance(int listenPort) throws IOException {
+ return new RemoteClient(new Socket("localhost", listenPort));
+ }
+
+ public static RemoteClient newSslInstance(int listenPort, SslKeyStore sslKeyStore) throws IOException {
+ SSLContext ctx = SslContextFactory.newInstanceFromTrustStore(sslKeyStore).getServerSSLContext();
+ if (ctx == null) {
+ throw new RuntimeException("Failed to create socket with SSLContext.");
+ }
+ return new RemoteClient(ctx.getSocketFactory().createSocket("localhost", listenPort));
+ }
+
+ public static RemoteClient newSslInstance(JettyHttpServer server, SslKeyStore keyStore) throws IOException {
+ return newSslInstance(server.getListenPort(), keyStore);
+ }
+
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java
new file mode 100644
index 00000000000..75368549ae6
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/RemoteServer.java
@@ -0,0 +1,110 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RemoteServer implements Runnable {
+
+ private final Thread thread = new Thread(this, "RemoteServer@" + System.identityHashCode(this));
+ private final LinkedBlockingQueue<Socket> clients = new LinkedBlockingQueue<>();
+ private final ServerSocket server;
+
+ private RemoteServer(int listenPort) throws IOException {
+ this.server = new ServerSocket(listenPort);
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (!Thread.interrupted()) {
+ Socket client = server.accept();
+ if (client != null) {
+ clients.add(client);
+ }
+ }
+ } catch (IOException e) {
+ if (!server.isClosed()) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public URI newRequestUri(String uri) {
+ return newRequestUri(URI.create(uri));
+ }
+
+ public URI newRequestUri(URI uri) {
+ URI serverUri = connectionSpec();
+ try {
+ return new URI(serverUri.getScheme(), serverUri.getUserInfo(), serverUri.getHost(),
+ serverUri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public URI connectionSpec() {
+ return URI.create("http://localhost:" + server.getLocalPort() + "/");
+ }
+
+ public Connection awaitConnection(int timeout, TimeUnit unit) throws InterruptedException, IOException {
+ Socket client = clients.poll(timeout, unit);
+ if (client == null) {
+ return null;
+ }
+ return new Connection(client);
+ }
+
+ public boolean close(int timeout, TimeUnit unit) {
+ try {
+ server.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ try {
+ thread.join(unit.toMillis(timeout));
+ } catch (InterruptedException e) {
+ return false;
+ }
+ return !thread.isAlive();
+ }
+
+ public static RemoteServer newInstance() throws IOException {
+ RemoteServer ret = new RemoteServer(0);
+ ret.thread.start();
+ return ret;
+ }
+
+ public static class Connection extends ChunkReader {
+
+ private final Socket socket;
+ private final PrintWriter out;
+
+ private Connection(Socket socket) throws IOException {
+ super(socket.getInputStream());
+ this.socket = socket;
+ this.out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
+ }
+
+ public void writeChunk(String chunk) {
+ out.print(chunk);
+ }
+
+ public void close() throws IOException {
+ out.close();
+ socket.close();
+ }
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java
new file mode 100644
index 00000000000..17a2b6ee6ee
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/test/ServerTestDriver.java
@@ -0,0 +1,146 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.application.ContainerActivator;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.RequestHandler;
+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.JettyHttpServer;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+import com.yahoo.jdisc.test.TestDriver;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ServerTestDriver {
+
+ private final TestDriver driver;
+ private final JettyHttpServer server;
+ private final RemoteClient client;
+
+ private ServerTestDriver(TestDriver driver, JettyHttpServer server, RemoteClient client) {
+ this.driver = driver;
+ this.server = server;
+ this.client = client;
+ }
+
+ public boolean close() throws IOException {
+ client.close();
+ server.close();
+ server.release();
+ return driver.close();
+ }
+
+ public TestDriver parent() {
+ return driver;
+ }
+
+ public ContainerActivator containerActivator() {
+ return driver;
+ }
+
+ public JettyHttpServer server() {
+ return server;
+ }
+
+ public RemoteClient client() {
+ return client;
+ }
+
+ public HttpRequest newRequest(HttpRequest.Method method, String uri, HttpRequest.Version version) {
+ return HttpRequest.newServerRequest(driver, newRequestUri(uri), method, version);
+ }
+
+ public URI newRequestUri(String uri) {
+ return newRequestUri(URI.create(uri));
+ }
+
+ public URI newRequestUri(URI uri) {
+ try {
+ return new URI("http", null, "locahost",
+ server.getListenPort(), uri.getPath(), uri.getQuery(), uri.getFragment());
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static ServerTestDriver newInstance(RequestHandler requestHandler, Module... guiceModules) throws IOException {
+ return newInstance(requestHandler, Arrays.asList(guiceModules));
+ }
+
+ public static ServerTestDriver newInstance(RequestHandler requestHandler, Iterable<Module> guiceModules)
+ throws IOException {
+ List<Module> lst = new LinkedList<>();
+ lst.add(newDefaultModule());
+ for (Module module : guiceModules) {
+ lst.add(module);
+ }
+ TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(lst.toArray(new Module[lst.size()]));
+ ContainerBuilder builder = driver.newContainerBuilder();
+ builder.serverBindings().bind("*://*/*", requestHandler);
+ JettyHttpServer server = builder.guiceModules().getInstance(JettyHttpServer.class);
+ return newInstance(null, driver, builder, server);
+ }
+
+ private static ServerTestDriver newInstance(SslKeyStore clientTrustStore, TestDriver driver, ContainerBuilder builder,
+ JettyHttpServer server) throws IOException {
+ builder.serverProviders().install(server);
+ driver.activateContainer(builder);
+ try {
+ server.start();
+ } catch (RuntimeException e) {
+ server.release();
+ driver.close();
+ throw e;
+ }
+ RemoteClient client;
+ if (clientTrustStore == null) {
+ client = RemoteClient.newInstance(server);
+ } else {
+ client = RemoteClient.newSslInstance(server, clientTrustStore);
+ }
+ return new ServerTestDriver(driver, server, client);
+ }
+
+ public static Module newDefaultModule() {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(new TypeLiteral<BindingRepository<RequestFilter>>() { })
+ .toInstance(new BindingRepository<>());
+ bind(new TypeLiteral<BindingRepository<ResponseFilter>>() { })
+ .toInstance(new BindingRepository<>());
+ }
+ };
+ }
+
+ public static Module newFilterModule(final BindingRepository<RequestFilter> requestFilters,
+ final BindingRepository<ResponseFilter> responseFilters) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ if (requestFilters != null) {
+ bind(new TypeLiteral<BindingRepository<RequestFilter>>() { }).toInstance(requestFilters);
+ }
+ if (responseFilters != null) {
+ bind(new TypeLiteral<BindingRepository<ResponseFilter>>() { }).toInstance(responseFilters);
+ }
+ }
+ };
+ }
+}
diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def
new file mode 100644
index 00000000000..8f2b4dfd86f
--- /dev/null
+++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.http-client.def
@@ -0,0 +1,36 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=jdisc.http.client
+
+userAgent string default = "JDisc/1.0"
+chunkedEncodingEnabled bool default = false
+compressionEnabled bool default = false
+connectionPoolEnabled bool default = true
+followRedirects bool default = false
+removeQueryParamsOnRedirect bool default = true
+sslConnectionPoolEnabled bool default = true
+proxyServer string default = ""
+useProxyProperties bool default = false
+useRawUri bool default = false
+compressionLevel int default = -1
+maxNumConnections int default = -1
+maxNumConnectionsPerHost int default = -1
+maxNumRedirects int default = 5
+maxNumRetries int default = 0
+connectionTimeout double default = 60
+idleConnectionInPoolTimeout double default = 60
+idleConnectionTimeout double default = 60
+idleWebSocketTimeout double default = 15
+requestTimeout double default = 60
+
+ssl.enabled bool default = false
+ssl.keyStoreType string default = "JKS"
+
+# Vespa home is prepended is path is relative
+ssl.keyStorePath string default = "jdisc_container/keyStore.jks"
+
+# Vespa home is prepended is path is relative
+ssl.trustStorePath string default = "conf/jdisc_container/trustStore.jks"
+
+ssl.keyDBKey string default = "jdisc_container"
+ssl.algorithm string default = "SunX509"
+ssl.protocol string default = "TLS"
diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def
new file mode 100644
index 00000000000..3e71212449e
--- /dev/null
+++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.connector.def
@@ -0,0 +1,79 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=jdisc.http
+
+# The TCP port to listen to for this connector.
+listenPort int default=0
+
+# The connector name
+name string default="default"
+
+# The header field cache size.
+headerCacheSize int default=512
+
+# The size of the buffer into which response content is aggregated before being sent to the client.
+outputBufferSize int default=65536
+
+# The maximum size of a request header.
+requestHeaderSize int default=65536
+
+# The maximum size of a response header.
+responseHeaderSize int default=65536
+
+# The accept queue size (also known as accept backlog).
+acceptQueueSize int default=0
+
+# Whether the server socket reuses addresses.
+reuseAddress bool default=true
+
+# The linger time. Use -1 to disable.
+soLingerTime int default=-1
+
+# The maximum idle time for a connection, which roughly translates to the Socket.setSoTimeout(int).
+idleTimeout double default=180.0
+
+# The stop timeout.
+stopTimeout double default=30.0
+
+# Whether or not to have socket keep alive turned on.
+tcpKeepAliveEnabled bool default=false
+
+# Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm).
+tcpNoDelay bool default=true
+
+# Whether to enable SSL for this connector.
+ssl.enabled bool default=false
+
+# The KeyDB key.
+ssl.keyDbKey string default=""
+
+# Names of protocols to exclude.
+ssl.excludeProtocol[].name string
+
+# Names of protocols to include.
+ssl.includeProtocol[].name string
+
+# Names of cipher suites to exclude.
+ssl.excludeCipherSuite[].name string
+
+# Names of cipher suites to include.
+ssl.includeCipherSuite[].name string
+
+# The type of the keystore.
+ssl.keyStoreType enum { JKS, PEM } default=JKS
+
+# JKS only - the path to the keystore.
+ssl.keyStorePath string default=""
+
+ssl.pemKeyStore.keyPath string default=""
+ssl.pemKeyStore.certificatePath string default=""
+
+ssl.trustStoreType enum { JKS } default="JKS"
+
+# JKS only - the path to the truststore.
+ssl.trustStorePath string default=""
+
+# The algorithm name used by the KeyManagerFactory.
+ssl.sslKeyManagerFactoryAlgorithm string default="SunX509"
+
+# The SSL protocol passed to SSLContext.getInstance()
+ssl.protocol string default="TLS"
diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def
new file mode 100644
index 00000000000..cfcd440939d
--- /dev/null
+++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.server.def
@@ -0,0 +1,23 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=jdisc.http
+
+# Whether to enable developer mode, where stack traces etc are visible in response bodies.
+developerMode bool default=false
+
+# The gzip compression level to use, if compression is enabled in a request.
+responseCompressionLevel int default=6
+
+# Whether to enable HTTP keep-alive for requests that support this.
+httpKeepAliveEnabled bool default=true
+
+# Whether the request body of POSTed forms should be removed (form parameters are available as request parameters).
+removeRawPostBodyForWwwUrlEncodedPost bool default=false
+
+# The component ID of a filter
+filter[].id string
+
+# The binding of a filter
+filter[].binding string
+
+# Max number of threads in pool
+maxWorkerThreads int default = 200
diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def
new file mode 100644
index 00000000000..4cc93d2b7e4
--- /dev/null
+++ b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.servlet-paths.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=jdisc.http
+
+# path by servlet componentId
+servlets{}.path string
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java
new file mode 100644
index 00000000000..9c85164dcb6
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertFile.java
@@ -0,0 +1,34 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+
+/**
+ * @author <a href="mailto:apurvak@yahoo-inc.com">Apurva Kumar</a>
+ */
+
+public class AssertFile {
+
+ public static void assertContains(File logFile, String expected) throws IOException {
+ String s = new String(
+ Files.readAllBytes(Paths.get(logFile.getAbsolutePath())),
+ StandardCharsets.UTF_8);
+ assertThat(s, containsString(expected));
+ }
+
+ public static void assertNotContains(File logFile, String expected) throws IOException {
+ String s = new String(
+ Files.readAllBytes(Paths.get(logFile.getAbsolutePath())),
+ StandardCharsets.UTF_8);
+ assertThat(s, not(containsString(expected)));
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java
new file mode 100644
index 00000000000..b60c269ac98
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/AssertHttp.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. 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.handler.RequestHandler;
+import com.yahoo.jdisc.http.test.RemoteClient;
+import com.yahoo.jdisc.http.test.ServerTestDriver;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public abstract class AssertHttp {
+
+ public static void assertChunk(String expected, String actual) {
+ if (expected.startsWith("HTTP/1.")) {
+ expected = sortChunk(expected);
+ actual = sortChunk(actual);
+ }
+ Pattern pattern = Pattern.compile(expected, Pattern.DOTALL | Pattern.MULTILINE);
+ if (pattern.matcher(actual).matches()) {
+ return;
+ }
+ assertEquals(expected, actual);
+ }
+
+ public static void assertResponse(RequestHandler requestHandler, String request,
+ String... expectedChunks) throws IOException {
+ ServerTestDriver driver = ServerTestDriver.newInstance(requestHandler);
+ assertResponse(driver, request, expectedChunks);
+ assertTrue(driver.close());
+ }
+
+ public static void assertResponse(ServerTestDriver driver, String request, String... expectedChunks)
+ throws IOException {
+ assertResponse(driver.client(), request, expectedChunks);
+ }
+
+ public static void assertResponse(RemoteClient client, String request, String... expectedChunks)
+ throws IOException {
+ client.writeRequest(request);
+ for (String expected : expectedChunks) {
+ assertChunk(expected, client.readChunk());
+ }
+ }
+
+ private static String sortChunk(String chunk) {
+ String[] lines = chunk.split("\r\n");
+ if (lines.length > 2) {
+ int prev = 1, next = 2;
+ for ( ; next < lines.length && !lines[next].isEmpty(); ++next) {
+ if (!Character.isLetterOrDigit(lines[next].charAt(0))) {
+ Arrays.sort(lines, prev, next);
+ prev = next + 1;
+ }
+ }
+ if (prev < next) {
+ Arrays.sort(lines, prev, next);
+ }
+ }
+ StringBuilder out = new StringBuilder();
+ for (String line : lines) {
+ out.append(line).append("\r\n");
+ }
+ return out.toString();
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java
new file mode 100644
index 00000000000..de2b0d453e6
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java
@@ -0,0 +1,315 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import org.jboss.netty.handler.codec.http.CookieDecoder;
+import org.jboss.netty.handler.codec.http.DefaultCookie;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNotSame;
+import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class CookieTestCase {
+
+ @Test
+ public void requireThatDefaultValuesAreSane() {
+ assertCookie(new DefaultCookie("foo", "bar"), new Cookie().setName("foo").setValue("bar"));
+ assertCookie(new DefaultCookie("foo", "bar"), new Cookie("foo", "bar"));
+ }
+
+ @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.setComment("foo");
+ assertEquals("foo", cookie.getComment());
+ cookie.setComment("bar");
+ assertEquals("bar", cookie.getComment());
+
+ cookie.setCommentUrl("foo");
+ assertEquals("foo", cookie.getCommentUrl());
+ assertSame(cookie.getCommentUrl(), cookie.getCommentURL());
+ cookie.setCommentUrl("bar");
+ assertEquals("bar", cookie.getCommentUrl());
+ assertSame(cookie.getCommentUrl(), cookie.getCommentURL());
+
+ cookie.setMaxAge(69, TimeUnit.DAYS);
+ assertEquals(69, cookie.getMaxAge(TimeUnit.DAYS));
+ assertEquals(TimeUnit.DAYS.toHours(69), cookie.getMaxAge(TimeUnit.HOURS));
+ cookie.setVersion(69);
+ assertEquals(69, cookie.getVersion());
+
+ cookie.setSecure(true);
+ assertTrue(cookie.isSecure());
+ cookie.setSecure(false);
+ assertFalse(cookie.isSecure());
+
+ cookie.setHttpOnly(true);
+ assertTrue(cookie.isHttpOnly());
+ cookie.setHttpOnly(false);
+ assertFalse(cookie.isHttpOnly());
+
+ cookie.setDiscard(true);
+ assertTrue(cookie.isDiscard());
+ cookie.setDiscard(false);
+ assertFalse(cookie.isDiscard());
+
+ cookie.ports().add(6);
+ assertEquals(1, cookie.ports().size());
+ assertTrue(cookie.ports().contains(6));
+ cookie.ports().add(9);
+ assertEquals(2, cookie.ports().size());
+ assertTrue(cookie.ports().contains(6));
+ assertTrue(cookie.ports().contains(9));
+ }
+
+ @Test
+ public void requireThatCopyConstructorWorks() {
+ final Cookie lhs = newCookie("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.getComment(), rhs.getComment());
+ assertEquals(rhs.getCommentUrl(), rhs.getCommentUrl());
+ assertEquals(rhs.getMaxAge(TimeUnit.MILLISECONDS), rhs.getMaxAge(TimeUnit.MILLISECONDS));
+ assertEquals(rhs.getVersion(), rhs.getVersion());
+ assertEquals(rhs.isSecure(), rhs.isSecure());
+ assertEquals(rhs.isHttpOnly(), rhs.isHttpOnly());
+ assertEquals(rhs.isDiscard(), rhs.isDiscard());
+ assertEquals(rhs.ports(), lhs.ports());
+ assertNotSame(rhs.ports(), lhs.ports());
+ }
+
+ @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(
+ Collections.singletonList("$Version=1; foo.name=foo.value; $Path=path; $Domain=domain; $Port=\"69\""),
+ Collections.singletonList(newCookie("foo")));
+ assertEncodeCookie(
+ Arrays.asList("$Version=1; bar.name=bar.value; $Path=path; $Domain=domain; $Port=\"69\"",
+ "$Version=1; foo.name=foo.value; $Path=path; $Domain=domain; $Port=\"69\""),
+ Arrays.asList(newCookie("foo"), newCookie("bar")));
+ }
+
+ @Test
+ public void requireThatSetCookieCanBeEncoded() {
+ assertEncodeSetCookie(
+ Collections.singletonList("foo.name=foo.value; Max-Age=0; Path=path; Domain=domain; Secure; " +
+ "HTTPOnly; Comment=comment; Version=1; CommentURL=\"commentUrl\"; " +
+ "Port=\"69\"; Discard"),
+ Collections.singletonList(newCookie("foo")));
+ }
+
+ @Test
+ public void requireThatOnlyOneSetCookieCanBeEncoded() {
+ try {
+ Cookie.toSetCookieHeader(Arrays.asList(newCookie("foo"), newCookie("bar")));
+ fail();
+ } catch (final IllegalStateException ignored) {
+
+ }
+ }
+
+ @Test
+ public void requireThatCookieCanBeDecoded() {
+ final Cookie foo = new Cookie();
+ foo.setName("foo.name");
+ foo.setValue("foo.value");
+ foo.setVersion(1);
+ foo.setPath("path");
+ foo.setDomain("domain");
+ foo.setMaxAge(-1, TimeUnit.SECONDS);
+ assertDecodeSetCookie(Collections.singletonList(foo),
+ "$Version=1;foo.name=foo.value;$Path=path;$Domain=domain;$Port=\"69\"");
+
+ final Cookie bar = new Cookie();
+ bar.setName("bar.name");
+ bar.setValue("bar.value");
+ bar.setVersion(1);
+ bar.setPath("path");
+ bar.setDomain("domain");
+ bar.setMaxAge(-1, TimeUnit.SECONDS);
+ assertDecodeCookie(Arrays.asList(foo, bar),
+ "$Version=1;foo.name=foo.value;$Path=path;$Domain=domain;$Port=\"69\";" +
+ "$Version=1;bar.name=bar.value;$Path=path;$Domain=domain;$Port=\"69\";");
+ }
+
+ @Test
+ public void requireThatSetCookieCanBeDecoded() {
+ final Cookie foo = new Cookie();
+ foo.setName("foo.name");
+ foo.setValue("foo.value");
+ foo.setVersion(1);
+ foo.setPath("path");
+ foo.setDomain("domain");
+ foo.setMaxAge(-1, TimeUnit.SECONDS);
+ assertDecodeSetCookie(Collections.singletonList(foo),
+ "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" +
+ "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard");
+
+ final Cookie bar = new Cookie();
+ bar.setName("bar.name");
+ bar.setValue("bar.value");
+ bar.setVersion(1);
+ bar.setPath("path");
+ bar.setDomain("domain");
+ bar.setMaxAge(-1, TimeUnit.SECONDS);
+ assertDecodeSetCookie(Arrays.asList(foo, bar),
+ "bar.name=bar.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" +
+ "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard;" +
+ "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;Comment=comment;" +
+ "Version=2;CommentURL=\"commentUrl\";Port=\"69\";Discard");
+ }
+
+ @Test
+ public void requireThatCookieDecoderWorksForGenericValidCookies() {
+ new CookieDecoder().decode("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() {
+ new CookieDecoder().decode("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() {
+ new CookieDecoder().decode("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() {
+ new CookieDecoder().decode("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--");
+ }
+
+ private static void assertDecodeCookie(final List<Cookie> expected, final String toDecode) {
+ assertCookies(expected, Cookie.fromCookieHeader(toDecode));
+ }
+
+ private static void assertDecodeSetCookie(final List<Cookie> expected, final String toDecode) {
+ assertCookies(expected, Cookie.fromSetCookieHeader(toDecode));
+ }
+
+ private static void assertCookies(final List<Cookie> expected, final List<Cookie> actual) {
+ assertEquals(expected.size(), actual.size());
+ for (final Cookie cookie : expected) {
+ assertNotNull(actual.remove(cookie));
+ }
+ }
+
+ private static void assertEncodeCookie(final List<String> expected, final List<Cookie> toEncode) {
+ assertCookies(expected, Cookie.toCookieHeader(toEncode));
+ }
+
+ private static void assertEncodeSetCookie(final List<String> expected, final List<Cookie> toEncode) {
+ assertCookies(expected, Cookie.toSetCookieHeader(toEncode));
+ }
+
+ private static void assertCookies(final List<String> expected, final String actual) {
+ final Set<Integer> seen = new HashSet<>();
+ for (final String str : expected) {
+ final int pos = actual.indexOf(str);
+ assertTrue(pos >= 0);
+ assertTrue(seen.add(pos));
+ }
+ }
+
+ private static void assertCookie(final DefaultCookie expected, final Cookie actual) {
+ assertEquals(expected.getName(), actual.getName());
+ assertEquals(expected.getValue(), actual.getValue());
+ assertEquals(expected.getDomain(), actual.getDomain());
+ assertEquals(expected.getPath(), actual.getPath());
+ assertEquals(expected.getComment(), actual.getComment());
+ assertEquals(expected.getCommentUrl(), actual.getCommentUrl());
+ assertEquals(expected.getMaxAge(), actual.getMaxAge(TimeUnit.SECONDS));
+ assertEquals(expected.getVersion(), actual.getVersion());
+ assertEquals(expected.isSecure(), actual.isSecure());
+ assertEquals(expected.isHttpOnly(), actual.isHttpOnly());
+ assertEquals(expected.isDiscard(), actual.isDiscard());
+ }
+
+ private static Cookie newCookie(final String name) {
+ final Cookie cookie = new Cookie();
+ cookie.setName(name + ".name");
+ cookie.setValue(name + ".value");
+ cookie.setDomain("domain");
+ cookie.setPath("path");
+ cookie.setComment("comment");
+ cookie.setCommentUrl("commentUrl");
+ cookie.setMaxAge(69, TimeUnit.MILLISECONDS);
+ cookie.setVersion(2);
+ cookie.setSecure(true);
+ cookie.setHttpOnly(true);
+ cookie.setDiscard(true);
+ cookie.ports().add(69);
+ return cookie;
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java
new file mode 100644
index 00000000000..2cfd1563b45
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/DummyMetricManager.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.application.MetricConsumer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:ssameer@yahoo-inc.com">ssameer</a>
+ * Date: 2/15/13
+ * Time: 11:49 AM
+ */
+public class DummyMetricManager extends AbstractModule implements MetricConsumer {
+
+ private final Map<String, Integer> metrics = new HashMap<>();
+ private Map<String, ?> lastContextDimensions;
+
+ @Override
+ protected void configure() {
+ bind(MetricConsumer.class).toInstance(this);
+ }
+
+ @Override
+ public void add(String key, Number val, Metric.Context ctx) {
+ synchronized (metrics) {
+ metrics.put(key, get(key) + val.intValue());
+ }
+ }
+
+ @Override
+ public void set(String key, Number val, Metric.Context ctx) {
+ synchronized (metrics) {
+ metrics.put(key, val.intValue());
+ }
+ }
+
+ @Override
+ public Metric.Context createContext(Map<String, ?> dimensions) {
+ lastContextDimensions = dimensions;
+ return new Metric.Context() { };
+ }
+
+ public Map<String, ?> getLastContextDimensions() {
+ return lastContextDimensions;
+ }
+
+ public int get(String key) {
+ Integer val;
+ synchronized (metrics) {
+ val = metrics.get(key);
+ }
+ return val != null ? val : 0;
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java
new file mode 100644
index 00000000000..1472c411c38
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java
@@ -0,0 +1,19 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http;
+
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HttpHeadersTestCase {
+
+ @Test
+ public void requireThatHeadersDoNotChange() {
+ assertEquals("X-JDisc-Disable-Chunking", HttpHeaders.Names.X_DISABLE_CHUNKING);
+ assertEquals("X-JDisc-Enable-TraceId", HttpHeaders.Names.X_ENABLE_TRACE_ID);
+ assertEquals("X-JDisc-TraceId", HttpHeaders.Names.X_TRACE_ID);
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java
new file mode 100644
index 00000000000..021a14b2ae7
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java
@@ -0,0 +1,249 @@
+// Copyright 2016 Yahoo Inc. 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 com.yahoo.jdisc.test.TestDriver;
+import org.jboss.netty.handler.codec.http.HttpHeaders;
+import org.jboss.netty.handler.codec.http.HttpMethod;
+import org.jboss.netty.handler.codec.http.HttpVersion;
+import org.testng.annotations.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.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class HttpRequestTestCase {
+
+ @Test
+ public void requireThatMethodIsCompatibleWithNetty() {
+ assertMethod(HttpRequest.Method.OPTIONS, HttpMethod.OPTIONS);
+ assertMethod(HttpRequest.Method.GET, HttpMethod.GET);
+ assertMethod(HttpRequest.Method.HEAD, HttpMethod.HEAD);
+ assertMethod(HttpRequest.Method.POST, HttpMethod.POST);
+ assertMethod(HttpRequest.Method.PUT, HttpMethod.PUT);
+ assertMethod(HttpRequest.Method.PATCH, HttpMethod.PATCH);
+ assertMethod(HttpRequest.Method.DELETE, HttpMethod.DELETE);
+ assertMethod(HttpRequest.Method.TRACE, HttpMethod.TRACE);
+ assertMethod(HttpRequest.Method.CONNECT, HttpMethod.CONNECT);
+ assertEquals(9, HttpRequest.Method.values().length);
+ }
+
+ @Test
+ public void requireThatVersionIsCompatibleWithNetty() {
+ assertVersion(HttpRequest.Version.HTTP_1_0, HttpVersion.HTTP_1_0);
+ assertVersion(HttpRequest.Version.HTTP_1_1, HttpVersion.HTTP_1_1);
+ assertEquals(2, HttpRequest.Version.values().length);
+ }
+
+ @Test
+ public void requireThatSimpleServerConstructorsUseReasonableDefaults() {
+ final 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() {
+ final Request parent = new Request(mockContainer(), URI.create("http://localhost/"));
+
+ final 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 requireThatTraceIsDisabledByDefault() throws Exception {
+ final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0);
+ assertFalse(request.headers().contains(com.yahoo.jdisc.http.HttpHeaders.Names.X_ENABLE_TRACE_ID, "true"));
+ }
+
+ @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 void assertMethod(final HttpRequest.Method discMethod, final HttpMethod nettyMethod) {
+ assertEquals(discMethod, HttpRequest.Method.valueOf(nettyMethod.getName()));
+ assertEquals(discMethod, HttpRequest.Method.valueOf(nettyMethod.toString()));
+ assertEquals(nettyMethod, HttpMethod.valueOf(discMethod.toString()));
+ }
+
+ private static void assertVersion(final HttpRequest.Version discVersion, final HttpVersion nettyVersion) {
+ assertEquals(discVersion, HttpRequest.Version.fromString(nettyVersion.getText()));
+ assertEquals(discVersion, HttpRequest.Version.fromString(nettyVersion.toString()));
+ assertEquals(nettyVersion, HttpVersion.valueOf(discVersion.toString()));
+ }
+
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java
new file mode 100644
index 00000000000..a6b3270002d
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java
@@ -0,0 +1,142 @@
+// Copyright 2016 Yahoo Inc. 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.testng.annotations.Test;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+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.toSetCookieHeader(cookies),
+ headers.get(0));
+ }
+
+ @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.toSetCookieHeader(Collections.singletonList(new Cookie("foo", "bar"))),
+ headers.get(0));
+ assertEquals(Cookie.toSetCookieHeader(Collections.singletonList(new Cookie("baz", "cox"))),
+ headers.get(1));
+ }
+
+ @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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java
new file mode 100644
index 00000000000..1d5ab557cd1
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AbstractClientTestCase.java
@@ -0,0 +1,230 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.BufferedContentChannel;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDispatch;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.http.test.RemoteServer;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static com.yahoo.jdisc.http.AssertHttp.assertChunk;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+abstract class AbstractClientTestCase {
+
+ protected static void assertRequest(CurrentContainer container, RemoteServer server, String requestUri,
+ HeaderFields requestHeaders, Iterable<ByteBuffer> requestContent,
+ Iterable<String> expectedRequestChunks, Iterable<String> responseChunks,
+ int expectedStatus, String expectedMessage,
+ HeaderFields expectedResponseHeaders,
+ Iterable<ByteBuffer> expectedResponseContent,
+ Map<String,Object> context) throws Exception {
+ MyRequestDispatch dispatch = new MyRequestDispatch(container, server.newRequestUri(requestUri),
+ requestHeaders, requestContent, context);
+ dispatch.dispatch();
+ assertRequest(server, expectedRequestChunks, responseChunks, dispatch);
+ assertResponse(dispatch.get(60, TimeUnit.SECONDS), expectedStatus, expectedMessage,
+ expectedResponseHeaders);
+ assertContent(expectedResponseContent, dispatch.responseContent.toReadable());
+ }
+
+ protected static void assertRequest(CurrentContainer container, RemoteServer server, String requestUri,
+ HeaderFields requestHeaders, Iterable<ByteBuffer> requestContent,
+ Iterable<String> expectedRequestChunks, Iterable<String> responseChunks,
+ int expectedStatus, String expectedMessage,
+ HeaderFields expectedResponseHeaders,
+ Iterable<ByteBuffer> expectedResponseContent) throws Exception {
+
+ assertRequest(container, server, requestUri, requestHeaders, requestContent, expectedRequestChunks,
+ responseChunks, expectedStatus, expectedMessage, expectedResponseHeaders, expectedResponseContent,
+ Collections.<String, Object>emptyMap());
+ }
+
+ protected static void assertRequest(RemoteServer server, Iterable<String> expectedRequestChunks,
+ Iterable<String> responseChunks, Future<Response> futureResponse)
+ throws Exception {
+ RemoteServer.Connection cnt = awaitConnection(server, futureResponse);
+ assertNotNull(cnt);
+ for (String expected : expectedRequestChunks) {
+ assertChunk(expected, cnt.readChunk());
+ }
+ for (String chunk : responseChunks) {
+ cnt.writeChunk(chunk);
+ }
+ cnt.close();
+ }
+
+ protected static RemoteServer.Connection awaitConnection(RemoteServer server, Future<Response> futureResponse)
+ throws Exception {
+ RemoteServer.Connection cnt = null;
+ for (int i = 0; i < 6000; ++i) {
+ cnt = server.awaitConnection(10, TimeUnit.MILLISECONDS);
+ if (cnt != null) {
+ break;
+ }
+ if (futureResponse.isDone()) {
+ HttpResponse response = (HttpResponse)futureResponse.get();
+ System.err.println("Unexpected " + response.getStatus() + " response: " + response.getMessage());
+ Throwable t = response.getError();
+ if (t instanceof Exception) {
+ throw (Exception)t;
+ } else if (t instanceof Error) {
+ throw (Error)t;
+ } else {
+ throw new RuntimeException(t);
+ }
+ }
+ }
+ return cnt;
+ }
+
+ protected static void assertResponse(Response response, int expectedStatus, String expectedMessage,
+ HeaderFields expectedHeaders) {
+ assertTrue(response instanceof HttpResponse);
+ HttpResponse httpResponse = (HttpResponse)response;
+ assertEquals(expectedStatus, httpResponse.getStatus());
+ assertEquals(expectedMessage, httpResponse.getMessage());
+
+ HeaderFields headers = response.headers();
+ for (Map.Entry<String, String> entry : expectedHeaders.entries()) {
+ assertTrue(headers.contains(entry.getKey(), entry.getValue()));
+ }
+ }
+
+ protected static void assertContent(Iterable<ByteBuffer> expected, Iterable<ByteBuffer> actual) {
+ Iterator<ByteBuffer> expectedIt = expected.iterator();
+ Iterator<ByteBuffer> actualIt = actual.iterator();
+ while (expectedIt.hasNext()) {
+ assertTrue(actualIt.hasNext());
+ assertEquals(expectedIt.next(), actualIt.next());
+ }
+ assertFalse(actualIt.hasNext());
+ }
+
+ protected static String requestUri(String uri) {
+ return uri;
+ }
+
+ protected static HeaderFields requestHeaders(HeaderEntry... entries) {
+ return asHeaders(entries);
+ }
+
+ protected static Iterable<ByteBuffer> requestContent(String... chunks) {
+ return asContent(chunks);
+ }
+
+ protected static Iterable<String> expectedRequestChunks(String... chunks) {
+ return Arrays.asList(chunks);
+ }
+
+ protected static Iterable<String> responseChunks(String... chunks) {
+ return Arrays.asList(chunks);
+ }
+
+ protected static int expectedResponseStatus(int status) {
+ return status;
+ }
+
+ protected static String expectedResponseMessage(String message) {
+ return message;
+ }
+
+ protected static HeaderFields expectedResponseHeaders(HeaderEntry... entries) {
+ return asHeaders(entries);
+ }
+
+ protected static Iterable<ByteBuffer> expectedResponseContent(String... chunks) {
+ return asContent(chunks);
+ }
+
+ protected static HeaderEntry newHeader(String key, String val) {
+ return new HeaderEntry(key, val);
+ }
+
+ protected static HeaderFields asHeaders(HeaderEntry... entries) {
+ HeaderFields ret = new HeaderFields();
+ for (HeaderEntry entry : entries) {
+ ret.add(entry.key, entry.val);
+ }
+ return ret;
+ }
+
+ protected static Iterable<ByteBuffer> asContent(String... chunks) {
+ List<ByteBuffer> ret = new LinkedList<>();
+ for (String chunk : chunks) {
+ ret.add(ByteBuffer.wrap(chunk.getBytes(StandardCharsets.UTF_8)));
+ }
+ return ret;
+ }
+
+ protected static class MyRequestDispatch extends RequestDispatch {
+
+ final Map<String, Object> context = new HashMap<>();
+ final CurrentContainer container;
+ final URI requestUri;
+ final HeaderFields requestHeaders;
+ final Iterable<ByteBuffer> requestContent;
+ final BufferedContentChannel responseContent = new BufferedContentChannel();
+
+ MyRequestDispatch(CurrentContainer container, URI requestUri, HeaderFields requestHeaders,
+ Iterable<ByteBuffer> requestContent, Map<String, Object> context) {
+ this.container = container;
+ this.requestUri = requestUri;
+ this.requestHeaders = requestHeaders;
+ this.requestContent = requestContent;
+ this.context.putAll(context);
+ }
+
+ @Override
+ protected Request newRequest() {
+ Request request = new Request(container, requestUri);
+ request.headers().addAll(requestHeaders);
+ request.context().putAll(context);
+ return request;
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return requestContent;
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return responseContent;
+ }
+ }
+
+ protected static class HeaderEntry {
+
+ final String key;
+ final String val;
+
+ HeaderEntry(String key, String val) {
+ this.key = key;
+ this.val = val;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java
new file mode 100644
index 00000000000..30cdd7acfe2
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/AsyncResponseHandlerTestCase.java
@@ -0,0 +1,60 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHandler;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ReadableContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.test.NonWorkingRequest;
+import org.testng.annotations.Test;
+
+import java.util.Map;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class AsyncResponseHandlerTestCase {
+
+ @Test(enabled = false)
+ public void requireThatOnThrowableAbortsHandler() throws Exception {
+ AsyncResponseHandler handler = new AsyncResponseHandler(NonWorkingRequest.newInstance("http://localhost/"),
+ new MyResponseHandler(), new MyMetric(),
+ new Metric.Context() { });
+ handler.onThrowable(new Throwable());
+ assertEquals(AsyncHandler.STATE.ABORT, handler.onStatusReceived(null));
+ assertEquals(AsyncHandler.STATE.ABORT, handler.onHeadersReceived(null));
+ assertEquals(AsyncHandler.STATE.ABORT, handler.onBodyPartReceived(null));
+ assertNull(handler.onCompleted());
+ }
+
+ private static class MyResponseHandler implements ResponseHandler {
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ return new ReadableContentChannel();
+ }
+ }
+
+ private static class MyMetric 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 null;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java
new file mode 100644
index 00000000000..84e758e868e
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientErrorTestCase.java
@@ -0,0 +1,17 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ClientErrorTestCase {
+
+ @Test(enabled = false)
+ public void requireNothing() {
+ assertTrue(true);
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java
new file mode 100644
index 00000000000..17140a88369
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ClientThreadingTestCase.java
@@ -0,0 +1,108 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.ContainerThread;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.RequestDispatch;
+import com.yahoo.jdisc.http.test.ClientTestDriver;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ClientThreadingTestCase extends AbstractClientTestCase {
+
+ @Test(enabled = false)
+ public void requireThatDefaultThreadFactoryCreatesContainerThreads() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder());
+ ThreadAwareDispatch dispatch = new ThreadAwareDispatch(driver, "/foo.html");
+ assertDispatch(dispatch);
+ assertTrue(dispatch.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(dispatch.thread instanceof ContainerThread);
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatThreadFactoryIsUsed() throws Exception {
+ final MyThreadFactory factory = new MyThreadFactory();
+ ClientTestDriver driver = ClientTestDriver.newInstance(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(ThreadFactory.class).toInstance(factory);
+ }
+ });
+ ThreadAwareDispatch dispatch = new ThreadAwareDispatch(driver, "/foo.html");
+ assertDispatch(dispatch);
+ assertTrue(dispatch.latch.await(60, TimeUnit.SECONDS));
+ assertTrue(factory.threads.contains(dispatch.thread));
+ assertTrue(driver.close());
+ }
+
+ private static void assertDispatch(ThreadAwareDispatch dispatch) throws Exception {
+ dispatch.dispatch();
+ assertRequest(dispatch.driver.server(),
+ expectedRequestChunks("POST " + dispatch.requestUri + " HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ dispatch);
+ assertResponse(dispatch.get(60, TimeUnit.SECONDS),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders());
+ }
+
+ private static class ThreadAwareDispatch extends RequestDispatch {
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final ClientTestDriver driver;
+ final String requestUri;
+ Thread thread;
+
+ ThreadAwareDispatch(ClientTestDriver driver, String requestUri) {
+ this.driver = driver;
+ this.requestUri = requestUri;
+ }
+
+ @Override
+ protected Request newRequest() {
+ return new Request(driver.currentContainer(), driver.server().newRequestUri(requestUri));
+ }
+
+ @Override
+ public ContentChannel handleResponse(Response response) {
+ thread = Thread.currentThread();
+ latch.countDown();
+ return null;
+ }
+ }
+
+ private static class MyThreadFactory implements ThreadFactory {
+
+ final BlockingQueue<Thread> threads = new LinkedBlockingQueue<>();
+
+ @Override
+ public Thread newThread(Runnable task) {
+ Thread thread = new Thread(task);
+ threads.add(thread);
+ return thread;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java
new file mode 100644
index 00000000000..62b9612e59b
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/EmptyResponseTestCase.java
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertFalse;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+public class EmptyResponseTestCase {
+ @Test(enabled = false)
+ public void testGetterSetters() throws IOException {
+ EmptyResponse underTest = EmptyResponse.INSTANCE;
+
+ assertEquals(0, underTest.getStatusCode());
+ assertNull(underTest.getStatusText());
+ assertEquals(0, underTest.getResponseBodyAsByteBuffer().remaining());
+ assertEquals(0, underTest.getResponseBodyAsBytes().length);
+ assertNull(underTest.getResponseBodyAsStream());
+ assertNull(underTest.getResponseBody());
+ assertNull(underTest.getResponseBodyExcerpt(10, ""));
+ assertNull(underTest.getResponseBodyExcerpt(10));
+ assertNull(underTest.getResponseBody());
+ assertNull(underTest.getUri());
+ assertNull(underTest.getContentType());
+ assertNull(underTest.getHeader(""));
+ assertNull(underTest.getHeaders(""));
+ assertNull(underTest.getHeaders());
+ assertFalse(underTest.isRedirected());
+ assertNull(underTest.getCookies());
+ assertFalse(underTest.hasResponseStatus());
+ assertFalse(underTest.hasResponseHeaders());
+ assertFalse(underTest.hasResponseBody());
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java
new file mode 100644
index 00000000000..751b696399e
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/HttpClientTestCase.java
@@ -0,0 +1,578 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.google.inject.AbstractModule;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.handler.RequestDispatch;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.http.client.filter.FilterException;
+import com.yahoo.jdisc.http.client.filter.ResponseFilter;
+import com.yahoo.jdisc.http.client.filter.ResponseFilterContext;
+import com.yahoo.jdisc.http.test.ClientTestDriver;
+import com.yahoo.jdisc.http.test.RemoteServer;
+import org.testng.annotations.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static com.yahoo.jdisc.http.test.ClientTestDriver.newFilterModule;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class HttpClientTestCase extends AbstractClientTestCase {
+
+ private static final int NUM_REQUESTS = 10;
+
+ @Test(enabled = false)
+ public void requireThatRequestCanBeSent() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatRequestHeadersAreSent() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(newHeader("foo", "bar")),
+ requestContent(),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "foo: bar\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatRequestContentIsSent() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent("foo", "bar"),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "Content-Length: 6\r\n" +
+ "\r\n" +
+ "foobar"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatRequestContentCanBeChunked() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder()
+ .chunkedEncodingEnabled(true));
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent("foo", "bar"),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "\r\n",
+ "3\r\nfoo\r\n",
+ "3\r\nbar\r\n",
+ "0\r\n\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatGetRequestsAreNeverChunked() throws Exception {
+ final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder()
+ .chunkedEncodingEnabled(true));
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ Future<Response> future = new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return HttpRequest.newServerRequest(driver.currentContainer(),
+ driver.server().newRequestUri("/status.html"),
+ HttpRequest.Method.GET);
+ }
+ }.dispatch();
+ assertRequest(driver.server(),
+ expectedRequestChunks("GET /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ future);
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatTraceRequestsDoNotAcceptContent() throws Exception {
+ final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder()
+ .chunkedEncodingEnabled(true));
+ RequestDispatch dispatch = new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ return HttpRequest.newServerRequest(driver.currentContainer(),
+ driver.server().newRequestUri("/status.html"),
+ HttpRequest.Method.TRACE);
+ }
+
+ @Override
+ protected Iterable<ByteBuffer> requestContent() {
+ return Arrays.asList(ByteBuffer.allocate(69));
+ }
+ };
+ try {
+ dispatch.dispatch();
+ fail();
+ } catch (UnsupportedOperationException e) {
+
+ }
+ assertRequest(driver.server(),
+ expectedRequestChunks("TRACE /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ dispatch);
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseCodeIsRead() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 69 foo\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(69),
+ expectedResponseMessage("foo"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseHeadersAreRead() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "foo: bar\r\n" +
+ "baz: cox\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(newHeader("foo", "bar"), newHeader("baz", "cox")),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseContentIsRead() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Length: 6\r\n" +
+ "\r\n" +
+ "foobar"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent("foobar"));
+ }
+ assertTrue(driver.close());
+ }
+
+ private void requireThatChunkedResponseContentIsRead() throws Exception {
+ ClientTestDriver driver = ClientTestDriver.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/status.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /status.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Transfer-Encoding: chunked\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Connection: keep-alive\r\n" +
+ "\r\n",
+ "3\r\nfoo\r\n",
+ "3\r\nbar\r\n",
+ "0\r\n\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent("foo", "bar"));
+ }
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatRequestTimeoutCanOccur() throws Exception {
+ final ClientTestDriver driver = ClientTestDriver.newInstance();
+ Response response = new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ Request request = new Request(driver.currentContainer(), driver.server().connectionSpec());
+ request.setTimeout(1, TimeUnit.MILLISECONDS);
+ return request;
+ }
+ }.dispatch().get(60, TimeUnit.SECONDS);
+ assertNotNull(response);
+ assertEquals(Response.Status.REQUEST_TIMEOUT, response.getStatus());
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatConnectionTimeoutCanOccur() throws Exception {
+ final ClientTestDriver driver = ClientTestDriver.newInstance();
+ Response response = new RequestDispatch() {
+
+ @Override
+ protected Request newRequest() {
+ HttpRequest request = HttpRequest.newServerRequest(driver.currentContainer(),
+ driver.server().connectionSpec());
+ request.setConnectionTimeout(1, TimeUnit.MILLISECONDS);
+ return request;
+ }
+ }.dispatch().get(60, TimeUnit.SECONDS);
+ assertTrue(response instanceof HttpResponse);
+ HttpResponse httpResponse = (HttpResponse)response;
+ assertEquals(Response.Status.REQUEST_TIMEOUT, httpResponse.getStatus());
+ assertEquals("java.util.concurrent.TimeoutException: No response received after 1", httpResponse.getMessage());
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatMetricContextIsCachedPerServer() throws Exception {
+ MyMetric metric = new MyMetric(new Metric.Context() {
+
+ });
+ ClientTestDriver driver = ClientTestDriver.newInstance(metric);
+ RemoteServer server1 = driver.server();
+ RemoteServer server2 = RemoteServer.newInstance();
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), server1,
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ assertRequest(driver.currentContainer(), server2,
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ assertTrue(server2.close(60, TimeUnit.SECONDS));
+ assertEquals(2, metric.numContexts.get());
+ assertTrue(metric.numCalls.get() > 0);
+ }
+
+ @Test(enabled = false)
+ public void requireThatNullMetricContextIsLegal() throws Exception {
+ MyMetric metric = new MyMetric(null);
+ ClientTestDriver driver = ClientTestDriver.newInstance(metric);
+ for (int i = 0; i < NUM_REQUESTS; ++i) {
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ }
+ assertTrue(driver.close());
+ assertEquals(1, metric.numContexts.get());
+ assertTrue(metric.numCalls.get() > 0);
+ }
+
+ @Test(enabled = false)
+ public void requireThatUnsupportedURISchemeThrowsException() throws Exception {
+ final ClientTestDriver driver = ClientTestDriver.newInstance(new HttpClientConfig.Builder()
+ .chunkedEncodingEnabled(true)
+ .connectionPoolEnabled(false));
+
+ try {
+ new RequestDispatch() {
+ @Override
+ public Request newRequest() {
+ return HttpRequest.newServerRequest(
+ driver.currentContainer(),
+ URI.create("ftp://localhost/"),
+ HttpRequest.Method.GET);
+ }
+ }.dispatch();
+ fail();
+ } catch (UnsupportedOperationException e) {
+ assertEquals("Unknown protocol: ftp", e.getMessage());
+ }
+
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseFilterIsInvoked() throws Exception {
+ final CountDownLatch filterInvokeCount = new CountDownLatch(2);
+ final StringBuffer responseBuffer = new StringBuffer();
+ ResponseFilter[] filters = new ResponseFilter[2];
+ filters[0] = (new ResponseFilter() {
+
+ @Override
+ public ResponseFilterContext filter(ResponseFilterContext filterContext) {
+ filterInvokeCount.countDown();
+ return filterContext;
+ }
+ });
+ filters[1] = (new ResponseFilter() {
+
+ @Override
+ public ResponseFilterContext filter(ResponseFilterContext filterContext) {
+ filterInvokeCount.countDown();
+ responseBuffer.append(filterContext.getRequestURI().getHost())
+ .append(filterContext.getRequestURI().getPath())
+ .append(filterContext.getResponseStatusCode())
+ .append(filterContext.getResponseFirstHeader("Content-Type"))
+ .append(filterContext.getRequestContext().get("key1"))
+ .append(filterContext.getRequestContext().get("key2"));
+ return filterContext;
+ }
+ });
+ Map<String, Object> context = new HashMap<>();
+ context.put("key1", "value1");
+ context.put("key2", "value2");
+ ClientTestDriver driver = ClientTestDriver.newInstance(newFilterModule(filters));
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks("POST /foo.html HTTP/1.1\r\n" +
+ "Host: .+\r\n" +
+ "Connection: keep-alive\r\n" +
+ "Accept: .+/.+\r\n" +
+ "User-Agent: JDisc/1.0\r\n" +
+ "\r\n"),
+ responseChunks("HTTP/1.1 200 OK\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "\r\n"),
+ expectedResponseStatus(200),
+ expectedResponseMessage("OK"),
+ expectedResponseHeaders(),
+ expectedResponseContent(),
+ context);
+
+ filterInvokeCount.await(60, TimeUnit.SECONDS);
+ assertEquals(0, filterInvokeCount.getCount());
+ assertEquals("localhost/foo.html200text/plain; charset=UTF-8value1value2", responseBuffer.toString());
+ assertTrue(driver.close());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseFilterHandlesFilterExceptionProperly() throws Exception {
+ ResponseFilter filter = new ResponseFilter() {
+
+ @Override
+ public ResponseFilterContext filter(ResponseFilterContext filterContext) throws FilterException {
+ throw new FilterException("Request aborted.");
+ }
+ };
+ ClientTestDriver driver = ClientTestDriver.newInstance(newFilterModule(filter));
+ assertRequest(driver.currentContainer(), driver.server(),
+ requestUri("/foo.html"),
+ requestHeaders(),
+ requestContent(),
+ expectedRequestChunks(),
+ responseChunks("HTTP/1.1 400 \r\n" +
+ "\r\n"),
+ expectedResponseStatus(400),
+ expectedResponseMessage("Request aborted."),
+ expectedResponseHeaders(),
+ expectedResponseContent());
+ assertTrue(driver.close());
+ }
+
+ private static class MyMetric extends AbstractModule implements MetricConsumer {
+
+ final AtomicInteger numContexts = new AtomicInteger(0);
+ final AtomicInteger numCalls = new AtomicInteger(0);
+ final Metric.Context context;
+
+ MyMetric(Metric.Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected void configure() {
+ bind(MetricConsumer.class).toInstance(this);
+ }
+
+ @Override
+ public void set(String key, Number val, Metric.Context context) {
+ assertSame(this.context, context);
+ numCalls.incrementAndGet();
+ }
+
+ @Override
+ public void add(String key, Number val, Metric.Context context) {
+ assertSame(this.context, context);
+ numCalls.incrementAndGet();
+ }
+
+ @Override
+ public Metric.Context createContext(Map<String, ?> properties) {
+ numContexts.incrementAndGet();
+ return context;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java
new file mode 100644
index 00000000000..7f5e22e48da
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/ProxyServerFactoryTestCase.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.ProxyServer;
+import org.testng.annotations.Test;
+
+import java.net.URI;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class ProxyServerFactoryTestCase {
+
+ @Test(enabled = false)
+ public void requireThatProxyServerFactoryWorks() {
+ assertNull(ProxyServerFactory.newInstance(null));
+
+ ProxyServer proxy = ProxyServerFactory.newInstance(URI.create("http://localhost:1234"));
+ assertEquals(ProxyServer.Protocol.HTTP, proxy.getProtocol());
+ assertEquals("localhost", proxy.getHost());
+ assertEquals(1234, proxy.getPort());
+ assertNull(proxy.getPrincipal());
+ assertNull(proxy.getPassword());
+
+ proxy = ProxyServerFactory.newInstance(URI.create("http://foo@localhost:1234"));
+ assertEquals(ProxyServer.Protocol.HTTP, proxy.getProtocol());
+ assertEquals("localhost", proxy.getHost());
+ assertEquals(1234, proxy.getPort());
+ assertEquals("foo", proxy.getPrincipal());
+ assertNull(proxy.getPassword());
+
+ proxy = ProxyServerFactory.newInstance(URI.create("https://foo:bar@localhost:1234"));
+ assertEquals(ProxyServer.Protocol.HTTPS, proxy.getProtocol());
+ assertEquals("localhost", proxy.getHost());
+ assertEquals(1234, proxy.getPort());
+ assertEquals("foo", proxy.getPrincipal());
+ assertEquals("bar", proxy.getPassword());
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java
new file mode 100644
index 00000000000..3f9912fda33
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketClientRequestTestCase.java
@@ -0,0 +1,30 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import static org.testng.AssertJUnit.assertTrue;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+public class WebSocketClientRequestTestCase {
+
+ @Test(enabled = false)
+ public void testWebSocketRequestReturnsCorrectContentChannel() {
+ AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class);
+ Request request = Mockito.mock(Request.class);
+ ResponseHandler respHandler = Mockito.mock(ResponseHandler.class);
+ Metric metric = Mockito.mock(Metric.class);
+ Metric.Context ctx = Mockito.mock(Metric.Context.class);
+
+ ContentChannel cc = WebSocketClientRequest.executeRequest(client, request, respHandler, metric, ctx);
+ assertTrue(cc instanceof WebSocketContent);
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java
new file mode 100644
index 00000000000..4ab851ac5b9
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketContentTestCase.java
@@ -0,0 +1,97 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.ListenableFuture;
+import com.ning.http.client.Request;
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketUpgradeHandler;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static org.testng.AssertJUnit.fail;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+@SuppressWarnings("unchecked")
+public class WebSocketContentTestCase {
+
+ private final byte[] TEST_DATA = "test data".getBytes(StandardCharsets.UTF_8);
+
+ @Test(enabled = false)
+ public void testContentChannelWriteAndClose() throws Exception{
+ AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class);
+ com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class);
+ Mockito.when(request.getUri()).thenReturn(new URI(""));
+
+ WebSocket websocket = Mockito.mock(WebSocket.class);
+ Mockito.when(websocket.isOpen()).thenReturn(true);
+ ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class);
+ Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject()))
+ .thenReturn(future);
+ Mockito.when(future.get()).thenReturn(websocket);
+
+ WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class));
+
+ CompletionHandler completionHandler = Mockito.mock(CompletionHandler.class);
+ underTest.write(ByteBuffer.wrap(TEST_DATA),completionHandler);
+
+ Mockito.verify(completionHandler,Mockito.atLeastOnce()).completed();
+
+ CompletionHandler closeHandler = Mockito.mock(CompletionHandler.class);
+ underTest.close(closeHandler);
+ Mockito.verify(closeHandler).completed();
+ Mockito.verify(websocket).close();
+ Mockito.verify(websocket).sendMessage(TEST_DATA);
+ }
+
+ @Test(enabled = false)
+ public void testWritingToAClosedContentChannel() throws Exception{
+ AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class);
+ com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class);
+ Mockito.when(request.getUri()).thenReturn(new URI(""));
+ WebSocket websocket = Mockito.mock(WebSocket.class);
+ ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class);
+ Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject()))
+ .thenReturn(future);
+ Mockito.when(future.get()).thenReturn(websocket);
+
+ WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class));
+ underTest.close(Mockito.mock(CompletionHandler.class));
+
+ // opens a new websocket
+ underTest.write(ByteBuffer.wrap(TEST_DATA), Mockito.mock(CompletionHandler.class));
+ }
+
+ @Test(enabled = false)
+ public void testExceptionalPathInExecuteRequest() throws Exception{
+ AsyncHttpClient client = Mockito.mock(AsyncHttpClient.class);
+ com.yahoo.jdisc.Request request = Mockito.mock(com.yahoo.jdisc.Request.class);
+ Mockito.when(request.getUri()).thenReturn(new URI(""));
+
+ WebSocket websocket = Mockito.mock(WebSocket.class);
+ Mockito.when(websocket.isOpen()).thenReturn(true);
+ ListenableFuture<WebSocket> future = Mockito.mock(ListenableFuture.class);
+ Mockito.when(client.executeRequest((Request)Mockito.isNotNull(), (WebSocketUpgradeHandler)Mockito.anyObject()))
+ .thenReturn(future);
+ Mockito.when(future.get()).thenReturn(websocket);
+ Mockito.when(websocket.sendMessage((byte[])Mockito.any())).thenThrow(new RuntimeException());
+
+ WebSocketContent underTest = new WebSocketContent(client, request, Mockito.mock(WebSocketUpgradeHandler.class));
+
+ CompletionHandler completionHandler = Mockito.mock(CompletionHandler.class);
+
+ try {
+ underTest.write(ByteBuffer.wrap(TEST_DATA),completionHandler);
+ fail();
+ } catch(RuntimeException e) {
+ // Expected
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java
new file mode 100644
index 00000000000..68283625579
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/WebSocketHandlerTestCase.java
@@ -0,0 +1,148 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client;
+
+import com.ning.http.client.websocket.WebSocket;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.CompletionHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.FutureResponse;
+import com.yahoo.jdisc.handler.ReadableContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.HttpResponse;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import java.net.ConnectException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+/**
+ * @author <a href="mailto:vikasp@yahoo-inc.com">Vikas Panwar</a>
+ */
+public class WebSocketHandlerTestCase {
+
+ @Test(enabled = false)
+ public void requireThatOnOpenDoesNothing() {
+ ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class);
+ newSocketHandler(responseHandler).onOpen(Mockito.mock(WebSocket.class));
+ Mockito.verifyZeroInteractions(responseHandler);
+ }
+
+ @Test(enabled = false)
+ public void requireThatOnFragmentDoesNothing() {
+ ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class);
+ newSocketHandler(responseHandler).onFragment(new byte[] { 6, 9 }, false);
+ Mockito.verifyZeroInteractions(responseHandler);
+ }
+
+ @Test(enabled = false)
+ public void requireThatOnLastFragmentDoesNothing() {
+ ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class);
+ newSocketHandler(responseHandler).onFragment(new byte[] { 6, 9 }, true);
+ Mockito.verifyZeroInteractions(responseHandler);
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseIsDispatchedOnFirstMessage() throws Exception {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse responseHandler = new FutureResponse(content);
+ try {
+ responseHandler.get(100, TimeUnit.MILLISECONDS);
+ fail();
+ } catch (TimeoutException e) {
+
+ }
+ newSocketHandler(responseHandler).onMessage(new byte[] { 6, 9 });
+ Response response = responseHandler.get(60, TimeUnit.SECONDS);
+ assertTrue(response instanceof HttpResponse);
+ assertEquals(Response.Status.OK, response.getStatus());
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseBytesAreWritten() {
+ ReadableContentChannel content = new ReadableContentChannel();
+ newSocketHandler(content).onMessage(new byte[] { 6, 9 });
+ ByteBuffer buf = content.read();
+ assertEquals(2, buf.remaining());
+ assertEquals(6, buf.get());
+ assertEquals(9, buf.get());
+ }
+
+ @Test(enabled = false)
+ public void requireThatEmptyResponsesCanBeSent() throws Exception {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse responseHandler = new FutureResponse(content);
+ newSocketHandler(responseHandler).onClose(Mockito.mock(WebSocket.class));
+ assertResponse(responseHandler, Response.Status.OK);
+ assertNull(content.read());
+ }
+
+ @Test(enabled = false)
+ public void requireThatEarlyErrorRespondsWithError() throws Exception {
+ assertErrorResponse(new ConnectException(), Response.Status.SERVICE_UNAVAILABLE);
+ assertErrorResponse(new TimeoutException(), Response.Status.REQUEST_TIMEOUT);
+ assertErrorResponse(new Throwable(), Response.Status.BAD_REQUEST);
+ }
+
+ @Test(enabled = false)
+ public void requireThatWriteCompletionFailureClosesResponseContent() {
+ CloseableContentChannel content = new CloseableContentChannel();
+ FutureResponse responseHandler = new FutureResponse(content);
+ WebSocketHandler socketHandler = newSocketHandler(responseHandler);
+ socketHandler.onMessage(new byte[] { 6, 9 });
+ assertFalse(content.closed);
+ content.handler.failed(new Throwable());
+ assertTrue(content.closed);
+ }
+
+ private static void assertErrorResponse(Throwable t, int expectedStatus) throws Exception {
+ ReadableContentChannel content = new ReadableContentChannel();
+ FutureResponse responseHandler = new FutureResponse(content);
+ newSocketHandler(responseHandler).onError(t);
+ assertResponse(responseHandler, expectedStatus);
+ assertNull(content.read());
+ }
+
+ private static void assertResponse(FutureResponse responseHandler, int expectedStatus) throws Exception {
+ Response response = responseHandler.get(60, TimeUnit.SECONDS);
+ assertTrue(response instanceof HttpResponse);
+ assertEquals(expectedStatus, response.getStatus());
+ }
+
+ private static WebSocketHandler newSocketHandler(ResponseHandler responseHandler) {
+ return new WebSocketHandler(Mockito.mock(Request.class), responseHandler, Mockito.mock(Metric.class),
+ Mockito.mock(Metric.Context.class));
+ }
+
+ private static WebSocketHandler newSocketHandler(ContentChannel responseContent) {
+ ResponseHandler responseHandler = Mockito.mock(ResponseHandler.class);
+ Mockito.when(responseHandler.handleResponse(Mockito.any(Response.class))).thenReturn(responseContent);
+ return new WebSocketHandler(Mockito.mock(Request.class), responseHandler, Mockito.mock(Metric.class),
+ Mockito.mock(Metric.Context.class));
+ }
+
+ private static class CloseableContentChannel implements ContentChannel {
+
+ CompletionHandler handler;
+ boolean closed = false;
+
+ @Override
+ public void write(ByteBuffer buf, CompletionHandler handler) {
+ this.handler = handler;
+ }
+
+ @Override
+ public void close(CompletionHandler handler) {
+ closed = true;
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java
new file mode 100644
index 00000000000..b2154349549
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/client/filter/core/ResponseFilterBridgeTestCase.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.client.filter.core;
+
+import com.ning.http.client.FluentCaseInsensitiveStringsMap;
+import com.ning.http.client.HttpResponseHeaders;
+import com.ning.http.client.HttpResponseStatus;
+import com.ning.http.client.Request;
+import com.ning.http.client.filter.FilterContext;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.client.filter.ResponseFilterContext;
+import org.testng.annotations.Test;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.AssertJUnit.assertEquals;
+
+/**
+ * @author <a href="mailto:alain@yahoo-inc.com">Alain Wan Buen Cheong</a>
+ */
+public class ResponseFilterBridgeTestCase {
+
+ @Test(enabled = false)
+ public void requireThatResponseFilterBridgeConvertsFieldsProperly() throws MalformedURLException, URISyntaxException {
+ ResponseFilterContext responseFilterContext = ResponseFilterBridge.toResponseFilterContext(
+ constructFilterContext(),
+ constructRequest()
+ );
+
+ assertEquals("http://localhost:8080/echo", responseFilterContext.getRequestURI().toString());
+ assertEquals(200, responseFilterContext.getResponseStatusCode());
+ assertEquals("v1", responseFilterContext.getResponseFirstHeader("k1"));
+ assertEquals("v2", responseFilterContext.getResponseFirstHeader("k2"));
+ Map<String, Object> customParams = responseFilterContext.getRequestContext();
+ assertEquals("cv1", customParams.get("c1"));
+ assertEquals("cv2", customParams.get("c2"));
+
+ }
+
+ private HttpRequest constructRequest() {
+ HttpRequest request = mock(HttpRequest.class);
+ Map<String, Object> customParams = new HashMap<>();
+ customParams.put("c1", "cv1");
+ customParams.put("c2", "cv2");
+ when(request.context()).thenReturn(customParams);
+ return request;
+ }
+
+ private FilterContext<?> constructFilterContext() throws MalformedURLException, URISyntaxException {
+ FilterContext.FilterContextBuilder<?> builder = new FilterContext.FilterContextBuilder<>();
+
+ Request request = mock(Request.class);
+ URL url = new URL("http://localhost:8080/echo");
+ URI reqURI = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(),
+ url.getQuery(), url.getRef());
+ when(request.getURI()).thenReturn(reqURI);
+
+ HttpResponseStatus responseStatus = mock(HttpResponseStatus.class);
+ when(responseStatus.getStatusCode()).thenReturn(200);
+
+ HttpResponseHeaders responseHeaders = mock(HttpResponseHeaders.class);
+ FluentCaseInsensitiveStringsMap headers = new FluentCaseInsensitiveStringsMap();
+ headers.add("k1", "v1", "v12", "v13");
+ headers.add("k2", "v2");
+ when(responseHeaders.getHeaders()).thenReturn(headers);
+
+ builder.request(request);
+ builder.responseStatus(responseStatus);
+ builder.responseHeaders(responseHeaders);
+
+ return builder.build();
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java
new file mode 100644
index 00000000000..149ddbc962c
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java
@@ -0,0 +1,357 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+
+import java.util.*;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.test.TestDriver;
+
+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;
+
+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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java
new file mode 100644
index 00000000000..f52d4be8c84
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java
@@ -0,0 +1,115 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import static org.testng.AssertJUnit.assertTrue;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Collections;
+
+import java.util.List;
+
+import com.yahoo.jdisc.Request;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.yahoo.jdisc.http.Cookie;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.test.TestDriver;
+
+public class DiscFilterResponseTest {
+
+ 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;
+ }
+
+ 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.corp.yahoo.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.corp.yahoo.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.corp.yahoo.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.corp.yahoo.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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java
new file mode 100644
index 00000000000..9086582ccae
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. 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.testng.annotations.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.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java
new file mode 100644
index 00000000000..1294e8e6b98
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. 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.testng.annotations.Test;
+
+import java.net.URI;
+
+import static com.yahoo.jdisc.http.HttpRequest.Method;
+import static com.yahoo.jdisc.http.HttpRequest.Version;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java
new file mode 100644
index 00000000000..4d0bfa8e334
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.filter;
+
+import java.util.concurrent.TimeUnit;
+
+import com.yahoo.jdisc.http.filter.JDiscCookieWrapper;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.yahoo.jdisc.http.Cookie;
+
+public class JDiscCookieWrapperTest {
+
+ @Test
+ public void requireThatWrapWorks() {
+ Cookie cookie = new Cookie("name", "value");
+ JDiscCookieWrapper wrapper = JDiscCookieWrapper.wrap(cookie);
+
+ wrapper.setComment("comment");
+ wrapper.setDomain("yahoo.com");
+ wrapper.setMaxAge(10);
+ wrapper.setPath("/path");
+ wrapper.setVersion(1);
+
+ Assert.assertEquals(wrapper.getName(), cookie.getName());
+ Assert.assertEquals(wrapper.getValue(), cookie.getValue());
+ Assert.assertEquals(wrapper.getDomain(), cookie.getDomain());
+ Assert.assertEquals(wrapper.getComment(), cookie.getComment());
+ Assert.assertEquals(wrapper.getMaxAge(), cookie.getMaxAge(TimeUnit.SECONDS));
+ Assert.assertEquals(wrapper.getPath(), cookie.getPath());
+ Assert.assertEquals(wrapper.getVersion(), cookie.getVersion());
+ Assert.assertEquals(wrapper.getSecure(), cookie.isSecure());
+
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java
new file mode 100644
index 00000000000..181298ea0e3
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. 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 <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java
new file mode 100644
index 00000000000..e0c949afe54
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java
@@ -0,0 +1,173 @@
+// Copyright 2016 Yahoo Inc. 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.springframework.mock.web.MockHttpServletRequest;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * 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;
+
+ @BeforeMethod
+ private 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);
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java
new file mode 100644
index 00000000000..dbbef448f18
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java
@@ -0,0 +1,87 @@
+// Copyright 2016 Yahoo Inc. 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.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+
+import static org.testng.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;
+
+ @BeforeMethod
+ private 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java
new file mode 100644
index 00000000000..3c3b541f986
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java
@@ -0,0 +1,80 @@
+// Copyright 2016 Yahoo Inc. 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.SecretStore;
+import com.yahoo.jdisc.http.server.jetty.ConnectorFactory;
+import com.yahoo.jdisc.http.server.jetty.TestDrivers;
+import com.yahoo.jdisc.http.ssl.ReaderForPath;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory;
+
+/**
+ * Guice module for test ConnectorFactories
+ *
+ * @author tonytv
+ */
+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 ThrowingSslKeyStoreFactory(), new MockSecretStore());
+ }
+
+ }
+
+ private static final class ThrowingSslKeyStoreFactory implements SslKeyStoreFactory {
+
+ @Override
+ public SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile) {
+ throw new UnsupportedOperationException("A SSL key store factory component is not available");
+ }
+
+ @Override
+ public SslKeyStore createTrustStore(ReaderForPath certificateFile) {
+ throw new UnsupportedOperationException("A SSL key store factory component is not available");
+ }
+
+ }
+
+ private static final class MockSecretStore implements SecretStore {
+
+ @Override
+ public String getSecret(String key) {
+ return TestDrivers.KEY_STORE_PASSWORD;
+ }
+
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java
new file mode 100644
index 00000000000..cb09d7cf9f8
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Yahoo Inc. 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 tonytv
+ */
+public class ServletModule implements Module {
+ @Override
+ public void configure(Binder binder) {
+ }
+
+ @Provides
+ public ComponentRegistry<ServletHolder> servletHolderComponentRegistry() {
+ return new ComponentRegistry<>();
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java
new file mode 100644
index 00000000000..7509598cbe2
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. 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 org.testng.annotations.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+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.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class AccessLogRequestLogTest {
+ @Test
+ public void requireThatQueryWithUnquotedSpecialCharactersIsHandled() {
+ final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
+ when(httpServletRequest.getRequestURI()).thenReturn("/search/");
+ when(httpServletRequest.getQueryString()).thenReturn("query=year:>2010");
+ final AccessLogEntry accessLogEntry = new AccessLogEntry();
+
+ AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry);
+
+ assertThat(accessLogEntry.getURI(), is(not(nullValue())));
+ }
+
+ @Test
+ public void requireThatDoubleQuotingIsNotPerformed() {
+ final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
+ final String path = "/search/";
+ when(httpServletRequest.getRequestURI()).thenReturn(path);
+ final String query = "query=year%252010+%3B&customParameter=something";
+ when(httpServletRequest.getQueryString()).thenReturn(query);
+ final AccessLogEntry accessLogEntry = new AccessLogEntry();
+
+ AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry);
+
+ assertThat(accessLogEntry.getURI().toString(), is(path + '?' + query));
+
+ }
+
+ @Test
+ public void requireThatNoQueryPartIsHandledWhenRequestIsMalformed() {
+ final HttpServletRequest httpServletRequest = mock(HttpServletRequest.class);
+ final String path = "/s>earch/";
+ when(httpServletRequest.getRequestURI()).thenReturn(path);
+ final String query = null;
+ when(httpServletRequest.getQueryString()).thenReturn(query);
+ final AccessLogEntry accessLogEntry = new AccessLogEntry();
+
+ AccessLogRequestLog.populateAccessLogEntryFromHttpServletRequest(httpServletRequest, accessLogEntry);
+
+ assertThat(accessLogEntry.getURI().toString(), is("/s%3Eearch/"));
+
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java
new file mode 100644
index 00000000000..0e666e826ae
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AlternativeTest.java
@@ -0,0 +1,136 @@
+// Copyright 2016 Yahoo Inc. 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.testng.annotations.Test;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+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.hamcrest.Matchers.greaterThan;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class AlternativeTest {
+ private static final String MAN = "man";
+ private static final String BEAR = "bear";
+ private static final String PIG = "pig";
+
+ @Test
+ public void singleValue() {
+ assertThat(
+ Alternative.preferred(MAN)
+ .orElseGet(() -> BEAR),
+ is(MAN));
+ }
+
+ @Test
+ public void singleNull() {
+ assertThat(
+ Alternative.preferred(null)
+ .orElseGet(() -> BEAR),
+ is(BEAR));
+ }
+
+ @Test
+ public void twoValues() {
+ assertThat(
+ Alternative.preferred(MAN)
+ .alternatively(() -> BEAR)
+ .orElseGet(() -> PIG),
+ is(MAN));
+ }
+
+ @Test
+ public void oneNullOneValue() {
+ assertThat(
+ Alternative.preferred(null)
+ .alternatively(() -> MAN)
+ .orElseGet(() -> BEAR),
+ is(MAN));
+ }
+
+ @Test
+ public void twoNulls() {
+ assertThat(
+ Alternative.preferred(null)
+ .alternatively(() -> null)
+ .orElseGet(() -> MAN),
+ is(MAN));
+ }
+
+ @Test
+ public void singleNullLastResortIsNull() {
+ assertThat(
+ Alternative.preferred(null)
+ .orElseGet(() -> null),
+ is(nullValue()));
+ }
+
+ @Test
+ public void twoNullsLastResortIsNull() {
+ assertThat(
+ Alternative.preferred(null)
+ .alternatively(() -> null)
+ .orElseGet(() -> null),
+ is(nullValue()));
+ }
+
+ @Test
+ public void oneNullTwoValues() {
+ assertThat(
+ Alternative.preferred(null)
+ .alternatively(() -> MAN)
+ .alternatively(() -> BEAR)
+ .orElseGet(() -> PIG),
+ is(MAN));
+ }
+
+ @Test
+ public void equalValuesMakeEqualAlternatives() {
+ assertThat(Alternative.preferred(MAN), is(equalTo(Alternative.preferred(MAN))));
+ assertThat(Alternative.preferred(BEAR), is(equalTo(Alternative.preferred(BEAR))));
+ assertThat(Alternative.preferred(PIG), is(equalTo(Alternative.preferred(PIG))));
+ assertThat(Alternative.preferred(null), is(equalTo(Alternative.preferred(null))));
+ }
+
+ @Test
+ public void equalValuesMakeEqualHashCodes() {
+ assertThat(Alternative.preferred(MAN).hashCode(), is(equalTo(Alternative.preferred(MAN).hashCode())));
+ assertThat(Alternative.preferred(BEAR).hashCode(), is(equalTo(Alternative.preferred(BEAR).hashCode())));
+ assertThat(Alternative.preferred(PIG).hashCode(), is(equalTo(Alternative.preferred(PIG).hashCode())));
+ assertThat(Alternative.preferred(null).hashCode(), is(equalTo(Alternative.preferred(null).hashCode())));
+ }
+
+ @Test
+ public void unequalValuesMakeUnequalAlternatives() {
+ assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(BEAR)))));
+ assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(PIG)))));
+ assertThat(Alternative.preferred(MAN), is(not(equalTo(Alternative.preferred(null)))));
+ assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(MAN)))));
+ assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(PIG)))));
+ assertThat(Alternative.preferred(BEAR), is(not(equalTo(Alternative.preferred(null)))));
+ assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(MAN)))));
+ assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(BEAR)))));
+ assertThat(Alternative.preferred(PIG), is(not(equalTo(Alternative.preferred(null)))));
+ assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(MAN)))));
+ assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(BEAR)))));
+ assertThat(Alternative.preferred(null), is(not(equalTo(Alternative.preferred(PIG)))));
+ }
+
+ @Test
+ public void hashValuesAreDecent() {
+ final String[] animals = { MAN, BEAR, PIG, "squirrel", "aardvark", "porcupine", "sasquatch", null };
+ final Set<Integer> hashCodes = Stream.of(animals)
+ .map(Alternative::preferred)
+ .map(Alternative::hashCode)
+ .collect(Collectors.toSet());
+ assertThat(hashCodes.size(), is(greaterThan(animals.length / 2))); // A modest requirement.
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java
new file mode 100644
index 00000000000..425c0444252
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Yahoo Inc. 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.CertificateStore;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.SecretStore;
+import com.yahoo.jdisc.http.ssl.ReaderForPath;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+import com.yahoo.jdisc.http.ssl.SslKeyStoreFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.testng.annotations.Test;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.channels.ServerSocketChannel;
+import java.util.Collections;
+import java.util.Map;
+
+import static com.yahoo.jdisc.http.ConnectorConfig.*;
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.JKS;
+import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.KeyStoreType.Enum.PEM;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ */
+public class ConnectorFactoryTest {
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void ssl_jks_config_is_validated() {
+ ConnectorConfig config = new ConnectorConfig(
+ new ConnectorConfig.Builder()
+ .ssl(new Ssl.Builder()
+ .enabled(true)
+ .keyStoreType(JKS)
+ .pemKeyStore(
+ new Ssl.PemKeyStore.Builder()
+ .keyPath("nonEmpty"))));
+
+ ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSslKeyStoreFactory(),
+ new ThrowingSecretStore());
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void ssl_pem_config_is_validated() {
+ ConnectorConfig config = new ConnectorConfig(
+ new ConnectorConfig.Builder()
+ .ssl(new Ssl.Builder()
+ .enabled(true)
+ .keyStoreType(PEM)
+ .keyStorePath("nonEmpty")));
+
+ ConnectorFactory willThrowException = new ConnectorFactory(config, new ThrowingSslKeyStoreFactory(),
+ new ThrowingSecretStore());
+ }
+
+ @Test
+ public void requireThatNoPreBoundChannelWorks() throws Exception {
+ Server server = new Server();
+ try {
+ ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()),
+ new ThrowingSslKeyStoreFactory(),
+ new ThrowingSecretStore());
+ ConnectorFactory.JDiscServerConnector connector =
+ (ConnectorFactory.JDiscServerConnector)factory.createConnector(new DummyMetric(), server, null, Collections.emptyMap());
+ 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
+ }
+ }
+ }
+
+ @Test
+ public void requireThatPreBoundChannelWorks() throws Exception {
+ Server server = new Server();
+ try {
+ ServerSocketChannel serverChannel = ServerSocketChannel.open();
+ serverChannel.socket().bind(new InetSocketAddress(0));
+
+ ConnectorFactory factory = new ConnectorFactory(new ConnectorConfig(new ConnectorConfig.Builder()), new ThrowingSslKeyStoreFactory(), new ThrowingSecretStore());
+ ConnectorFactory.JDiscServerConnector connector = (ConnectorFactory.JDiscServerConnector) factory.createConnector(new DummyMetric(), server, serverChannel, Collections.emptyMap());
+ 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 class HelloWorldHandler extends AbstractHandler {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ 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 {
+ }
+
+ private static final class ThrowingSslKeyStoreFactory implements SslKeyStoreFactory {
+
+ @Override
+ public SslKeyStore createKeyStore(ReaderForPath certificateFile, ReaderForPath keyFile) {
+ throw new UnsupportedOperationException("A SSL key store factory component is not available");
+ }
+
+ @Override
+ public SslKeyStore createTrustStore(ReaderForPath certificateFile) {
+ throw new UnsupportedOperationException("A SSL key store factory component is not available");
+ }
+
+ }
+
+ private static final class ThrowingSecretStore implements SecretStore {
+
+ @Override
+ public String getSecret(String key) {
+ throw new UnsupportedOperationException("A secret store is not available");
+ }
+
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java
new file mode 100644
index 00000000000..ae529fdfcca
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java
@@ -0,0 +1,51 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.testng.annotations.Test;
+
+/**
+ * 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java
new file mode 100644
index 00000000000..e9866a18d7c
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java
@@ -0,0 +1,513 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.AbstractResource;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingRepository;
+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.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.chain.RequestFilterChain;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+import com.yahoo.jdisc.http.filter.chain.ResponseFilterChain;
+import com.yahoo.jdisc.http.filter.ResponseHeaderFilter;
+import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule;
+import com.yahoo.jdisc.http.server.FilterBindings;
+import org.mockito.ArgumentCaptor;
+import org.testng.annotations.Test;
+
+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.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+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 <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class FilterTestCase {
+ @Test
+ public void requireThatRequestFilterIsNotRunOnUnboundPath() throws Exception {
+ final RequestFilter filter = mock(RequestFilterMockBase.class);
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/filtered/*", filter);
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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);
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/filtered/*", filter);
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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");
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/*", filter);
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 {
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/*", new RespondForbiddenFilter());
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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";
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/*", new NullCompletionHandlerFilter(responseStatus, responseMessage));
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 {
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ final BindingRepository<ResponseFilter> responseFilters = null;
+ requestFilters.bind("http://*/*", new ThrowingRequestFilter());
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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);
+ final BindingRepository<RequestFilter> requestFilters = null;
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ responseFilters.bind("http://*/filtered/*", filter);
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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);
+ final BindingRepository<RequestFilter> requestFilters = null;
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ responseFilters.bind("http://*/filtered/*", filter);
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 {
+ final BindingRepository<RequestFilter> requestFilters = null;
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ responseFilters.bind("http://*/*", new HeaderResponseFilter("foo", "bar"));
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 {
+ final BindingRepository<RequestFilter> requestFilters = null;
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ responseFilters.bind("http://*/*", new ThrowingResponseFilter());
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 String uriPattern = "http://*/*";
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ final RequestFilter requestFilter = mock(RequestFilterMockBase.class);
+ requestFilters.bind(uriPattern, requestFilter);
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ final ResponseFilter responseFilter = mock(ResponseFilterMockBase.class);
+ responseFilters.bind(uriPattern, responseFilter);
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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 {
+ final BindingRepository<RequestFilter> requestFilters = new BindingRepository<>();
+ requestFilters.bind("http://*/*", new RespondForbiddenFilter());
+ final BindingRepository<ResponseFilter> responseFilters = new BindingRepository<>();
+ responseFilters.bind("http://*/*", new HeaderResponseFilter("foo", "bar"));
+ final MyRequestHandler requestHandler = new MyRequestHandler();
+ final TestDriver testDriver = newDriver(requestHandler, requestFilters, responseFilters);
+
+ 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(any(HttpRequest.class), any(ResponseHandler.class));
+ verify(requestFilter2).filter(any(HttpRequest.class), 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(), any(ResponseHandler.class));
+ 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(any(HttpRequest.class), 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(), any(Request.class));
+ 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"));
+ }
+
+ private static TestDriver newDriver(
+ final MyRequestHandler requestHandler,
+ final BindingRepository<RequestFilter> requestFilters,
+ final BindingRepository<ResponseFilter> responseFilters)
+ throws IOException {
+ return TestDriver.newInstance(
+ JettyHttpServer.class,
+ requestHandler,
+ newFilterModule(requestFilters, responseFilters));
+ }
+
+ private static com.google.inject.Module newFilterModule(
+ final BindingRepository<RequestFilter> requestFilters,
+ final BindingRepository<ResponseFilter> responseFilters) {
+ return Modules.combine(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(FilterBindings.class).toInstance(
+ new FilterBindings(
+ requestFilters != null ? requestFilters : EMPTY_REQUEST_FILTER_REPOSITORY,
+ responseFilters != null ? responseFilters : EMPTY_RESPONSE_FILTER_REPOSITORY));
+ bind(ServerConfig.class).toInstance(new ServerConfig(new ServerConfig.Builder()));
+ }
+ },
+ new ConnectorFactoryRegistryModule());
+ }
+
+ private static final BindingRepository<RequestFilter> EMPTY_REQUEST_FILTER_REPOSITORY = new BindingRepository<>();
+ private static final BindingRepository<ResponseFilter> EMPTY_RESPONSE_FILTER_REPOSITORY = new BindingRepository<>();
+
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java
new file mode 100644
index 00000000000..1ed15d6d380
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java
@@ -0,0 +1,549 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.server.jetty;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpUpgradeHandler;
+import javax.servlet.http.Part;
+
+import com.yahoo.jdisc.ResourceReference;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.Response;
+import org.testng.annotations.Test;
+
+import com.google.inject.Key;
+import com.yahoo.jdisc.Container;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.service.CurrentContainer;
+
+import static org.testng.AssertJUnit.fail;
+
+/**
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class HttpRequestFactoryTest {
+
+ private static final class MockRequest implements HttpServletRequest {
+ String queryString = null;
+ StringBuffer requestUrl;
+ final String method = "GET";
+ final String protocol = "HTTP/1.1";
+ final String remoteAddr = "127.0.0.1";
+ final int remotePort = 0;
+
+ public MockRequest(String sortOfUri) {
+ int mark = sortOfUri.indexOf('?');
+ if (mark > 0) {
+ queryString = sortOfUri.substring(mark + 1);
+ requestUrl = new StringBuffer(sortOfUri.substring(0, mark));
+ } else {
+ queryString = null;
+ requestUrl = new StringBuffer(sortOfUri);
+ }
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getCharacterEncoding() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void setCharacterEncoding(String env)
+ throws UnsupportedEncodingException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public int getContentLength() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public long getContentLengthLong() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getContentType() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getParameter(String name) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getParameterNames() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String[] getParameterValues(String name) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Map<String, String[]> getParameterMap() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getProtocol() {
+ return protocol;
+ }
+
+ @Override
+ public String getScheme() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getServerName() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getServerPort() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return remoteAddr;
+ }
+
+ @Override
+ public String getRemoteHost() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void setAttribute(String name, Object o) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Locale getLocale() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Enumeration<Locale> getLocales() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean isSecure() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String path) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ @Deprecated
+ public String getRealPath(String path) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getRemotePort() {
+ return remotePort;
+ }
+
+ @Override
+ public String getLocalName() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getLocalAddr() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getLocalPort() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public AsyncContext startAsync() throws IllegalStateException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public AsyncContext startAsync(ServletRequest servletRequest,
+ ServletResponse servletResponse) throws IllegalStateException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean isAsyncStarted() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean isAsyncSupported() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public AsyncContext getAsyncContext() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public DispatcherType getDispatcherType() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getAuthType() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Cookie[] getCookies() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public long getDateHeader(String name) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getHeader(String name) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getHeaders(String name) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getHeaderNames() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public int getIntHeader(String name) {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ @Override
+ public String getMethod() {
+ return method;
+ }
+
+ @Override
+ public String getPathInfo() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getPathTranslated() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getContextPath() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getQueryString() {
+ return queryString;
+ }
+
+ @Override
+ public String getRemoteUser() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean isUserInRole(String role) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getRequestedSessionId() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String getRequestURI() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public StringBuffer getRequestURL() {
+ return requestUrl;
+ }
+
+ @Override
+ public String getServletPath() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public HttpSession getSession(boolean create) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public HttpSession getSession() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public String changeSessionId() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdValid() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromCookie() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromURL() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ @Deprecated
+ public boolean isRequestedSessionIdFromUrl() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean authenticate(HttpServletResponse response)
+ throws IOException, ServletException {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void login(String username, String password)
+ throws ServletException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void logout() throws ServletException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public Collection<Part> getParts() throws IOException, ServletException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public Part getPart(String name) throws IOException, ServletException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass)
+ throws IOException, ServletException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ }
+
+ @Test
+ public final void test() {
+ String noise = "query=a" + "\\" + "^{|}&other=madeit";
+ HttpServletRequest servletRequest = new MockRequest(
+ "http://yahoo.com/search?" + noise);
+ HttpRequest request = HttpRequestFactory.newJDiscRequest(
+ new MockContainer(), servletRequest);
+ assertThat(request.getUri().getQuery(), equalTo(noise));
+ }
+
+ @Test
+ public final void testIllegalQuery() {
+ try {
+ HttpRequestFactory.newJDiscRequest(
+ new MockContainer(),
+ new MockRequest("http://example.com/search?query=\"contains_quotes\""));
+ fail("Above statement should throw");
+ } catch (RequestException e) {
+ assertThat(e.getResponseStatus(), is(Response.Status.BAD_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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java
new file mode 100644
index 00000000000..b973a7e34bc
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java
@@ -0,0 +1,818 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule;
+import com.yahoo.jdisc.http.server.FilterBindings;
+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.testng.annotations.AfterClass;
+import org.testng.annotations.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.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 <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HttpServerConformanceTest extends ServerProviderConformanceTest {
+
+ private static final String REQUEST_CONTENT = "myRequestContent";
+ private static final String RESPONSE_CONTENT = "myResponseContent";
+
+ @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()))
+ .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(success())
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable {
+ new TestRunner().expect(successNoContent())
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable {
+ new TestRunner().expect(anyOf(success(), 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(), 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(), 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(), 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(), 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()))
+ .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()))
+ .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(), 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(), 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
+ + " }";
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ 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(
+ new BindingRepository<>(),
+ new BindingRepository<>()));
+ bind(ServerConfig.class)
+ .toInstance(new ServerConfig(new ServerConfig.Builder()));
+ }
+ },
+ 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;
+ }
+ System.out.println("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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java
new file mode 100644
index 00000000000..cf3721eef88
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerMetricTest.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.Metric;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.MetricConsumer;
+import com.yahoo.jdisc.handler.AbstractRequestHandler;
+import com.yahoo.jdisc.handler.ContentChannel;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.ServerConfig;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.testng.annotations.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.yahoo.jdisc.Response.Status.OK;
+import static org.cthul.matchers.CthulMatchers.isA;
+import static org.cthul.matchers.CthulMatchers.matchesPattern;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HttpServerMetricTest {
+
+ @Test(enabled = false)
+ public void requireThatNumActiveRequestsIsTracked() throws Exception {
+ final MetricConsumer metricConsumer = mock(MetricConsumer.class);
+ final TestDriver driver = TestDrivers.newInstance(
+ new EchoRequestHandler(),
+ newMetricModule(metricConsumer));
+ driver.client().get("/status.html")
+ .expectStatusCode(is(OK));
+ final InOrder order = inOrder(metricConsumer);
+ order.verify(metricConsumer).set(eq("serverNumActiveRequests"), eq(1), any(Metric.Context.class));
+ order.verify(metricConsumer).set(eq("serverNumActiveRequests"), eq(0), any(Metric.Context.class));
+ assertThat(driver.close(), is(true));
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test(enabled = false)
+ public void requireThatCustomMetricDimensionsAreSupported() throws Exception {
+ final MetricConsumer metricConsumer = mock(MetricConsumer.class);
+ // TODO: enable metrics
+ final ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder();
+
+ final Map<String, String> commonDimensions = new HashMap<>();
+ commonDimensions.put("key1", "value1");
+ commonDimensions.put("key2", "value2");
+ // TODO: serverConfig.commonMetricDimensions().add(...);
+
+ final TestDriver driver = TestDrivers.newConfiguredInstance(
+ new EchoRequestHandler(),
+ new ServerConfig.Builder(),
+ connectorConfig,
+ newMetricModule(metricConsumer));
+ driver.client().get("/status.html")
+ .expectStatusCode(is(OK));
+
+ final ArgumentCaptor<Map<String, ?>> contextCaptor = new ArgumentCaptor<>();
+ verify(metricConsumer).createContext(contextCaptor.capture());
+ final Map<String, ?> actualContext = contextCaptor.getValue();
+ for (final Map.Entry<String, String> entry : commonDimensions.entrySet()) {
+ assertThat(actualContext.get(entry.getKey()), isA(String.class).that(is(entry.getValue())));
+ }
+ assertThat(actualContext.get("serverName"), isA(String.class).that(matchesPattern("\\S+")));
+ assertThat(actualContext.get("serverPort"), isA(String.class).that(matchesPattern("\\d+")));
+ assertThat(driver.close(), is(true));
+ }
+
+ private static Module newMetricModule(final MetricConsumer metricConsumer) {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(MetricConsumer.class).toInstance(metricConsumer);
+ }
+ };
+ }
+
+ private static class EchoRequestHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ return handler.handleResponse(new Response(OK));
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java
new file mode 100644
index 00000000000..5ef0f7db742
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java
@@ -0,0 +1,710 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.HeaderFields;
+import com.yahoo.jdisc.Request;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.application.BindingSetSelector;
+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.Cookie;
+import com.yahoo.jdisc.http.HttpRequest;
+import com.yahoo.jdisc.http.HttpResponse;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.service.BindingSetNotFoundException;
+import com.yahoo.jdisc.References;
+import org.apache.http.entity.mime.FormBodyPart;
+import org.apache.http.entity.mime.content.StringBody;
+import org.testng.annotations.Test;
+
+import java.net.BindException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+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.concurrent.TimeUnit;
+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 com.yahoo.jdisc.Response.Status.REQUEST_TIMEOUT;
+import static com.yahoo.jdisc.Response.Status.REQUEST_URI_TOO_LONG;
+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.Names.X_TRACE_ID;
+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 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.not;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class HttpServerTest {
+
+ @Test
+ public void requireThatServerCanListenToRandomPort() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(mockRequestHandler());
+ assertThat(driver.server().getListenPort(), is(not(0)));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ }
+ assertThat(driver.close(), is(true));
+ }
+
+ @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)));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ 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));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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 + "=[]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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 + "=[]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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("{}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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=[\"% ]}"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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]"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test
+ public void requireThatSetCookieHeaderIsCorrect() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new CookieSetterRequestHandler(
+ new Cookie("foo", "bar").setComment("comment yeah")
+ .setCommentURL("http://comment.yes/")
+ .setDiscard(true)
+ .setDomain(".localhost")
+ .setHttpOnly(true)
+ .setMaxAge(5000, TimeUnit.SECONDS)
+ .setPath("/foopath")
+ .setSecure(true)
+ .setVersion(2)));
+ driver.client().get("/status.html")
+ .expectStatusCode(is(OK))
+ .expectHeader("Set-Cookie", is("foo=bar; " +
+ "Max-Age=5000; " +
+ "Path=\"/foopath\"; " +
+ "Domain=.localhost; " +
+ "Secure; HTTPOnly; " +
+ "Comment=\"comment yeah\"; " +
+ "Version=1; " +
+ "CommentURL=\"http://comment.yes/\"; " +
+ "Discard"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test(enabled = false)
+ public void requireThatGeneratedTraceIdIsSet() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler());
+ final SimpleHttpClient client1 = driver.client();
+ final SimpleHttpClient client2 = driver.newClient();
+
+ client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000000"));
+ client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000001"));
+ client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000000"));
+ client1.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000002"));
+ client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000001"));
+ client2.newGet("/status.html").addHeader(X_TRACE_ID, "true").execute()
+ .expectHeader("X-JDisc-TraceId", matchesPattern("\\w+00000002"));
+
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test(enabled = false)
+ public void requireThatClientTraceIdIsSet() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler());
+ driver.client().newGet("/status.html").addHeader(X_TRACE_ID, "foo").execute()
+ .expectHeader(X_TRACE_ID, is("foo"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test
+ public void requireThatTimeoutWorks() throws Exception {
+ final UnresponsiveHandler requestHandler = new UnresponsiveHandler();
+ final TestDriver driver = TestDrivers.newInstance(requestHandler);
+ driver.client().get("/status.html")
+ .expectStatusCode(is(REQUEST_TIMEOUT));
+ ResponseDispatch.newInstance(OK).dispatch(requestHandler.responseHandler);
+ assertThat(driver.close(), is(true));
+ }
+
+ // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2
+ @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");
+ assertThat(driver.close(), is(true));
+ }
+
+ // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2
+ @Test
+ public void requireThatHeaderWithEmptyValueIsOmitted() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", ""));
+ driver.client().get("/status.html")
+ .expectStatusCode(is(OK))
+ .expectNoHeader("X-Foo");
+ assertThat(driver.close(), is(true));
+ }
+
+ @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(enabled = false)
+ public void requireThatRequestTrailersAreSupported() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new RequestHandlerThatEchoesTrailers());
+ assertThat(driver.client().raw("GET /status.html HTTP/1.1\r\n" +
+ "Host: localhost\r\n" +
+ "Transfer-Encoding: chunked\r\n\r\n" +
+ "0\r\n" +
+ "X-Foo: foo\r\n" +
+ "X-Bar: bar\r\n" +
+ "\r\n"),
+ containsPattern(Pattern.quote("{X-Bar=[bar], X-Foo=[foo]}")));
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test(enabled = false)
+ public void requireThatResponseTrailersAreSupported() throws Exception {
+ final HeaderFields trailers = new HeaderFields();
+ trailers.add("X-Foo", "foo");
+ trailers.add("X-Bar", "bar");
+ final TestDriver driver = TestDrivers.newInstance(new RequestHandlerThatSetsResponseTrailers(trailers));
+ driver.client().get("/status.html")
+ .expectTrailer("X-Foo", is("foo"))
+ .expectTrailer("X-Bar", is("bar"));
+ assertThat(driver.close(), is(true));
+ }
+
+ @Test
+ public void requireThatServerCanRespondToSslRequest() throws Exception {
+ final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler());
+ driver.client().get("/status.html")
+ .expectStatusCode(is(OK));
+ assertThat(driver.close(), is(true));
+ }
+
+ @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));
+ }
+
+ 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(
+ final RequestHandler requestHandler, final 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)
+ throws Exception {
+ return new FormBodyPart(
+ parameterName,
+ new StringBody(fileContent) {
+ @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(final Request request, final ResponseHandler handler) {
+ final Map<String, List<String>> parameters =
+ new TreeMap<>(((HttpRequest)request).parameters());
+ final 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 RequestHandlerThatEchoesTrailers extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ final HttpRequest httpRequest = (HttpRequest)request;
+ final ContentChannel out = ResponseDispatch.newInstance(Response.Status.OK).connect(handler);
+ return new ContentChannel() {
+
+ @Override
+ public void write(final ByteBuffer buf, final CompletionHandler handler) {
+ handler.completed();
+ }
+
+ @Override
+ public void close(final CompletionHandler handler) {
+ synchronized (httpRequest.trailers()) {
+ out.write(StandardCharsets.UTF_8.encode(httpRequest.trailers().toString()), null);
+ }
+ out.close(null);
+ handler.completed();
+ }
+ };
+ }
+ }
+
+ private static class RequestHandlerThatSetsResponseTrailers extends AbstractRequestHandler {
+
+ final HeaderFields trailers;
+
+ RequestHandlerThatSetsResponseTrailers(final HeaderFields trailers) {
+ this.trailers = trailers;
+ }
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ final HttpResponse response = HttpResponse.newInstance(OK);
+ final ContentChannel content = handler.handleResponse(response);
+ synchronized (response.trailers()) {
+ response.trailers().putAll(this.trailers);
+ }
+ content.close(null);
+ return null;
+ }
+ }
+
+ 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) {
+ return handler.handleResponse(new Response(OK));
+ }
+ }
+
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java
new file mode 100644
index 00000000000..6bbd71c2cee
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. 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.HttpTrace;
+import org.testng.annotations.Test;
+
+import java.net.URI;
+
+import static com.yahoo.jdisc.Response.Status.OK;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+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));
+ }
+
+ private static RequestHandler newEchoHandler() {
+ return new AbstractRequestHandler() {
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ return handler.handleResponse(new Response(OK));
+ }
+ };
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java
new file mode 100644
index 00000000000..2f250799f1c
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java
@@ -0,0 +1,219 @@
+// Copyright 2016 Yahoo Inc. 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.HttpHeaders;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+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.config.Registry;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+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.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.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.regex.Pattern;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * A simple http client for testing
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class SimpleHttpClient {
+
+ private final HttpClient delegate;
+ private final String scheme;
+ private final int listenPort;
+
+ public SimpleHttpClient(final SSLContext sslContext, final int listenPort, final boolean useCompression) {
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ if (!useCompression) {
+ builder.disableContentCompression();
+ }
+ if (sslContext != null) {
+ SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory(
+ sslContext,
+ SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ 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;
+ }
+
+ public URI newUri(final String path) {
+ return URI.create(scheme + "://localhost:" + listenPort + path);
+ }
+
+ public RequestExecutor newGet(final String path) {
+ return newRequest(new HttpGet(newUri(path)));
+ }
+
+ public RequestExecutor newPost(final String path) {
+ return newRequest(new HttpPost(newUri(path)));
+ }
+
+ public RequestExecutor newRequest(final HttpUriRequest request) {
+ return new RequestExecutor().setRequest(request);
+ }
+
+ public ResponseValidator execute(final HttpUriRequest request) throws IOException {
+ return newRequest(request).execute();
+ }
+
+ public ResponseValidator get(final String path) throws IOException {
+ return newGet(path).execute();
+ }
+
+ public String raw(final String request) throws IOException {
+ final Socket socket = new Socket("localhost", listenPort);
+ final OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
+ out.write(request);
+ out.flush();
+
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ final InputStream in = socket.getInputStream();
+ final int[] TERMINATOR = { '\r', '\n', '\r', '\n' };
+ for (int pos = 0; pos < TERMINATOR.length; ++pos) {
+ final int b = in.read();
+ if (b < 0) {
+ throw new EOFException();
+ }
+ if (b != TERMINATOR[pos]) {
+ pos = -1;
+ }
+ buf.write(b);
+ }
+ final String response = buf.toString(StandardCharsets.UTF_8.name());
+ final java.util.regex.Matcher matcher = Pattern.compile(HttpHeaders.Names.CONTENT_LENGTH + ": (.+)\r\n").matcher(response);
+ if (matcher.find()) {
+ final int len = Integer.valueOf(matcher.group(1));
+ for (int i = 0; i < len; ++i) {
+ final int b = in.read();
+ if (b < 0) {
+ throw new EOFException();
+ }
+ buf.write(b);
+ }
+ }
+
+ socket.close();
+ return buf.toString(StandardCharsets.UTF_8.name());
+ }
+
+ 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 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);
+ }
+ return new ResponseValidator(delegate.execute(request));
+ }
+ }
+
+ public static class ResponseValidator {
+
+ private final HttpResponse response;
+ private final String content;
+
+ public ResponseValidator(final HttpResponse response) throws IOException {
+ this.response = response;
+
+ final HttpEntity entity = response.getEntity();
+ this.content = entity == null ? null :
+ EntityUtils.toString(entity, StandardCharsets.UTF_8);
+ }
+
+ public ResponseValidator expectStatusCode(final Matcher<Integer> matcher) {
+ MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), matcher);
+ return this;
+ }
+
+ public ResponseValidator expectHeader(final String headerName, final Matcher<String> matcher) {
+ final Header firstHeader = response.getFirstHeader(headerName);
+ final String headerValue = firstHeader != null ? firstHeader.getValue() : null;
+ MatcherAssert.assertThat(headerValue, matcher);
+ assertThat(firstHeader, is(not(nullValue())));
+ return this;
+ }
+
+ public ResponseValidator expectNoHeader(final String headerName) {
+ final Header firstHeader = response.getFirstHeader(headerName);
+ assertThat(firstHeader, is(nullValue()));
+ return this;
+ }
+
+ public ResponseValidator expectContent(final Matcher<String> matcher) throws IOException {
+ MatcherAssert.assertThat(content, matcher);
+ return this;
+ }
+
+ public ResponseValidator expectTrailer(final String trailerName, final Matcher<String> matcher) {
+ // TODO: check trailer, not header
+ return expectHeader(trailerName, matcher);
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java
new file mode 100644
index 00000000000..9d0aa02bbfe
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleWebSocketClient.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. 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.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.AsyncHttpClientConfig;
+import com.ning.http.client.ListenableFuture;
+import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider;
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketListener;
+import com.ning.http.client.websocket.WebSocketUpgradeHandler;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+class SimpleWebSocketClient {
+
+ private final AsyncHttpClient client;
+ private final String scheme;
+ private final int listenPort;
+
+ public SimpleWebSocketClient(final TestDriver driver) {
+ this(driver.newSslContext(), driver.server().getListenPort());
+ }
+
+ public SimpleWebSocketClient(final SSLContext sslContext, final int listenPort) {
+ final AsyncHttpClientConfig config = new AsyncHttpClientConfig.Builder().setSSLContext(sslContext).build();
+ this.client = new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config);
+ this.scheme = sslContext != null ? "wss" : "ws";
+ this.listenPort = listenPort;
+ }
+
+ public ListenableFuture<WebSocket> executeRequest(final String path, final WebSocketListener listener)
+ throws IOException {
+ return client.prepareGet(scheme + "://localhost:" + listenPort + path)
+ .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(listener).build());
+ }
+
+ public boolean close() {
+ client.close();
+ return true;
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java
new file mode 100644
index 00000000000..2f17d76a145
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. 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.google.inject.Module;
+import com.yahoo.jdisc.application.ContainerBuilder;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.ssl.JKSKeyStore;
+import com.yahoo.jdisc.http.ssl.SslContextFactory;
+import com.yahoo.jdisc.http.ssl.SslKeyStore;
+
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import static com.google.inject.name.Names.named;
+
+/**
+ * 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 <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+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)
+ throws IOException {
+ this.driver = driver;
+ this.server = server;
+ this.client = client;
+ }
+
+ public static TestDriver newInstance(Class<? extends JettyHttpServer> serverClass,
+ RequestHandler requestHandler,
+ Module testConfig) throws IOException {
+ 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("*://*/*", requestHandler);
+ driver.activateContainer(builder);
+ server.start();
+
+ SimpleHttpClient client = new SimpleHttpClient(newSslContext(builder), server.getListenPort(), false);
+ return new TestDriver(driver, server, client);
+ }
+
+ public boolean close() throws IOException {
+ server.close();
+ server.release();
+ return driver.close();
+ }
+
+ public JettyHttpServer server() { return server; }
+
+ public SimpleHttpClient client() { return client; }
+
+ public SimpleHttpClient newClient() throws IOException { return newClient(false); }
+
+ public SimpleHttpClient newClient(final boolean useCompression) throws IOException {
+ return new SimpleHttpClient(newSslContext(), server.getListenPort(), useCompression);
+ }
+
+ public SSLContext newSslContext() {
+ return newSslContext(driver.newContainerBuilder());
+ }
+
+ private static SSLContext newSslContext(final ContainerBuilder builder) {
+ ConnectorConfig.Ssl sslConfig = builder.getInstance(ConnectorConfig.class).ssl();
+ if (!sslConfig.enabled()) return null;
+
+ SslKeyStore keyStore = new JKSKeyStore(Paths.get(sslConfig.keyStorePath()));
+ keyStore.setKeyStorePassword(builder.getInstance(Key.get(String.class, named("keyStorePassword"))));
+ return SslContextFactory.newInstanceFromTrustStore(keyStore).getServerSSLContext();
+ }
+
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java
new file mode 100644
index 00000000000..3137ae24b7b
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. 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.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.http.ConnectorConfig;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule;
+import com.yahoo.jdisc.http.guiceModules.ServletModule;
+import com.yahoo.jdisc.http.server.FilterBindings;
+
+import java.io.IOException;
+
+import static com.google.inject.name.Names.named;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class TestDrivers {
+
+ private static final String KEY_STORE = "src/test/resources/ssl_keystore_test.jks";
+ public static final String KEY_STORE_PASSWORD = "secret";
+
+ public static TestDriver newConfiguredInstance(final RequestHandler requestHandler,
+ final ServerConfig.Builder serverConfig,
+ final ConnectorConfig.Builder connectorConfig,
+ final Module... guiceModules) throws IOException {
+ return TestDriver.newInstance(
+ JettyHttpServer.class,
+ requestHandler,
+ newConfigModule(serverConfig, connectorConfig, guiceModules));
+ }
+
+ public static TestDriver newInstance(final RequestHandler requestHandler,
+ final Module... guiceModules) throws IOException {
+ return TestDriver.newInstance(
+ JettyHttpServer.class,
+ requestHandler,
+ newConfigModule(
+ new ServerConfig.Builder(),
+ new ConnectorConfig.Builder(),
+ guiceModules
+ ));
+ }
+
+ public static TestDriver newInstanceWithSsl(final RequestHandler requestHandler,
+ final Module... guiceModules) throws IOException {
+ return TestDriver.newInstance(
+ JettyHttpServer.class,
+ requestHandler,
+ newConfigModule(
+ new ServerConfig.Builder(),
+ new ConnectorConfig.Builder()
+ .ssl(new ConnectorConfig.Ssl.Builder()
+ .enabled(true)
+ .keyDbKey("dummy-key-for-StaticKeyDbConnectorFactory.getPasswordFromKeydb")
+ .keyStorePath(KEY_STORE)
+ .trustStorePath(KEY_STORE)),
+ Modules.combine(new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(String.class).annotatedWith(named("keyStorePassword"))
+ .toInstance(KEY_STORE_PASSWORD);
+ }
+ }, Modules.combine(guiceModules))
+ ));
+ }
+
+ private static Module newConfigModule(
+ final ServerConfig.Builder serverConfig,
+ final ConnectorConfig.Builder connectorConfigBuilder,
+ final Module... guiceModules) {
+ return Modules.combine(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ServerConfig.class).toInstance(new ServerConfig(serverConfig));
+ bind(ConnectorConfig.class).toInstance(new ConnectorConfig(connectorConfigBuilder));
+ bind(FilterBindings.class).toInstance(
+ new FilterBindings(
+ new BindingRepository<RequestFilter>(),
+ new BindingRepository<ResponseFilter>()));
+ }
+ },
+ new ConnectorFactoryRegistryModule(connectorConfigBuilder),
+ new ServletModule(),
+ Modules.combine(guiceModules));
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java
new file mode 100644
index 00000000000..8a6ea28f2f9
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerConformanceTest.java
@@ -0,0 +1,766 @@
+// Copyright 2016 Yahoo Inc. 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.common.util.concurrent.SettableFuture;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketByteListener;
+import com.yahoo.jdisc.application.BindingRepository;
+import com.yahoo.jdisc.http.ServerConfig;
+import com.yahoo.jdisc.http.filter.RequestFilter;
+import com.yahoo.jdisc.http.filter.ResponseFilter;
+import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule;
+import com.yahoo.jdisc.http.server.FilterBindings;
+import com.yahoo.jdisc.test.ServerProviderConformanceTest;
+import org.glassfish.grizzly.websockets.HandshakeException;
+import org.hamcrest.Matcher;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+//Ignore: Broken by jetty 9.2.{3,4}
+class WebSocketServerConformanceTestIgnored extends ServerProviderConformanceTest {
+
+ /* Some tests here are disabled. What they have in common is that the scenario
+ * involves waiting for an event (response write) in the request content channel's close()
+ * method, but Jetty will sometimes use the thread that is supposed to generate that event
+ * (the thread that writes the response) to deliver the close() notification, causing a
+ * deadlock.
+ *
+ * All in all, the WebSocket protocol doesn't map beautifully to JDisc APIs, which makes
+ * it hard to do proper testing here. Specifically, in order to cause the request content
+ * channel to be closed, we have to close the socket from the client side, which means
+ * that all bets are off regarding what response the client will see. So, the tests here
+ * that close the socket early can do no verification at all. However, it will be
+ * verified by the test framework that the server-side request processing finishes
+ * without any unexpected side effects.
+ */
+
+ @Override
+ @Test
+ public void testContainerNotReadyException() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testBindingSetNotFoundException() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testNoBindingSetSelectedException() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testBindingNotFoundException() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncCloseResponse() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncWriteResponse() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithSyncHandleResponse() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestHandlerWithAsyncHandleResponse() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestException() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionWithSyncCloseResponse() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionWithSyncWriteResponse() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable {
+ new TestRunner().expectedError(instanceOf(HandshakeException.class))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncCompletion() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncCompletion() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicException() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncCompletion() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncCompletion() throws Throwable {
+ new TestRunner().expectResponseContent(is("myResponseContent"))
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicException() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test(enabled = false)
+ public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable {
+ }
+
+ @Override
+ @Test
+ public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable {
+ new TestRunner().setCloseRequestEarly(true)
+ .execute();
+ }
+
+ @Override
+ @Test
+ public void testResponseWriteCompletionException() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testResponseCloseCompletionException() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @Override
+ @Test
+ public void testResponseCloseCompletionExceptionNoContent() throws Throwable {
+ new TestRunner().execute();
+ }
+
+ @SuppressWarnings("deprecation")
+ private class TestRunner implements Adapter<JettyHttpServer, WebSocketClient, Future<String>> {
+
+ Matcher<String> expectedContent = null;
+ Matcher<Object> expectedError = null;
+ boolean closeRequestEarly;
+
+ void execute() throws Throwable {
+ runTest(this);
+ }
+
+ TestRunner expectResponseContent(final Matcher<String> matcher) {
+ assertThat(expectedError, is(nullValue()));
+ expectedContent = matcher;
+ return this;
+ }
+
+ TestRunner expectedError(final Matcher<Object> matcher) {
+ assertThat(expectedContent, is(nullValue()));
+ expectedError = matcher;
+ return this;
+ }
+
+ @Override
+ public Module newConfigModule() {
+ return Modules.combine(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(FilterBindings.class)
+ .toInstance(new FilterBindings(
+ new BindingRepository<RequestFilter>(),
+ new BindingRepository<ResponseFilter>()));
+ bind(ServerConfig.class)
+ .toInstance(new ServerConfig(new ServerConfig.Builder()));
+ }
+ },
+ new ConnectorFactoryRegistryModule());
+ }
+
+ @Override
+ public Class<JettyHttpServer> getServerProviderClass() {
+ return JettyHttpServer.class;
+ }
+
+ @Override
+ public WebSocketClient newClient(final JettyHttpServer server) throws Throwable {
+ return new WebSocketClient(server.getListenPort());
+ }
+
+ @Override
+ public Future<String> executeRequest(
+ final WebSocketClient client,
+ final boolean withRequestContent) throws Throwable {
+ final String requestContent = withRequestContent ? "myRequestContent" : null;
+ return client.executeRequest(requestContent, closeRequestEarly);
+ }
+
+ @Override
+ public Iterable<ByteBuffer> newResponseContent() {
+ return Collections.singleton(StandardCharsets.UTF_8.encode("myResponseContent"));
+ }
+
+ @Override
+ public void validateResponse(final Future<String> responseFuture) throws Throwable {
+ String content = null;
+ Throwable error = null;
+ try {
+ content = responseFuture.get(60, TimeUnit.SECONDS);
+ } catch (final ExecutionException e) {
+ error = e.getCause();
+ }
+ if (expectedContent != null) {
+ assertThat(content, expectedContent);
+ }
+ if (expectedError != null) {
+ assertThat(error, expectedError);
+ }
+ }
+
+ public TestRunner setCloseRequestEarly(final boolean closeRequestEarly) {
+ this.closeRequestEarly = closeRequestEarly;
+ return this;
+ }
+ }
+
+ private static class WebSocketClient implements Closeable {
+
+ final SimpleWebSocketClient delegate;
+
+ WebSocketClient(final int listenPort) {
+ delegate = new SimpleWebSocketClient(null, listenPort);
+ }
+
+ Future<String> executeRequest(final String requestContent, final boolean closeRequest) throws Exception {
+ final MyWebSocketListener listener = new MyWebSocketListener(requestContent, closeRequest);
+ delegate.executeRequest("/status.html", listener);
+ return listener.response;
+ }
+
+ @Override
+ public void close() throws IOException {
+ delegate.close();
+ }
+ }
+
+ // You may find this class slightly ugly, with all the logic to do closing in various places.
+ // The reason this is necessary is the combination of several things:
+ // 1) The way WebSocket is implemented in JDisc and mapped to JDisc APIs, specifically:
+ // - When the client closes a socket, it is not guaranteed to receive anything more from the server
+ // (the protocol could support it, but neither the client nor server library that we use do)
+ // - The server won't close a socket until the client does, but by then it is too late for the
+ // server to send responses.
+ // 2) The conformance test framework is designed mostly for request-response protocols. It assumes that
+ // it is self-evident when communication is over, and only _then_ moves on to validating the response.
+ //
+ // The problem is that we cannot close the socket right after sending the request, as we are then
+ // not guaranteed to receive response data (nondeterministic behavior). We cannot close the socket when
+ // the conformance test framework asks us to validate the response, because we'd never get to that
+ // - the request processing isn't finished until some party closes the socket! So how do we decide when
+ // to close the socket? Well, what any "real" client would do is close it when we're satisfied with the
+ // response. And these tests never return anything more than a single response message, so if we get one,
+ // we consider ourselves done. Also if we get an error.
+ private static class MyWebSocketListener implements WebSocketByteListener {
+
+ // This is used to temporarily concatenate response fragments until we have a complete response message.
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ // This is used to signal that we have received a response, which may be data or an error.
+ // This is attempted set from multiple code locations, but the first one "wins" (the others are ignored).
+ final SettableFuture<String> response = SettableFuture.create();
+
+ // If this is true, the client will close the socket immediately after sending the request content.
+ // This means that there is no guarantee that the client will receive any response from the server.
+ // If this is false, the socket is closed after receiving a response from the server. Since the server
+ // response in theory can be infinitely long, we define "receive a response" as receiving a single message,
+ // since that is what is sent from the server in these tests.
+ final boolean closeEarly;
+
+ final byte[] requestContent;
+
+ // We need to be able to close the WebSocket in methods that are not handed the WebSocket instance.
+ // We use this to keep a reference to it.
+ private final AtomicReference<WebSocket> webSocketRef = new AtomicReference<>(null);
+
+ MyWebSocketListener(final String requestContent, final boolean closeEarly) {
+ this.closeEarly = closeEarly;
+ this.requestContent = requestContent != null ? requestContent.getBytes(StandardCharsets.UTF_8) : null;
+ }
+
+ @Override
+ public void onOpen(final WebSocket webSocket) {
+ this.webSocketRef.set(webSocket);
+ if (requestContent != null) {
+ webSocket.sendMessage(requestContent);
+ }
+ if (closeEarly) {
+ webSocket.close();
+ }
+ }
+
+ @Override
+ public void onClose(final WebSocket webSocket) {
+ response.set("");
+ this.webSocketRef.set(null);
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ response.setException(t);
+ closeSocket();
+ }
+
+ @Override
+ public void onMessage(final byte[] buf) {
+ final String message = new String(buf, StandardCharsets.UTF_8);
+ response.set(message);
+ closeSocket();
+ }
+
+ @Override
+ public void onFragment(final byte[] buf, final boolean last) {
+ try {
+ out.write(buf);
+ if (last) {
+ response.set(new String(out.toByteArray(), StandardCharsets.UTF_8));
+ closeSocket();
+ }
+ } catch (final IOException e) {
+ response.setException(e);
+ }
+ }
+
+ private void closeSocket() {
+ final WebSocket webSocket = webSocketRef.get();
+ if (webSocket != null) {
+ webSocket.close();
+ }
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java
new file mode 100644
index 00000000000..e2f94a1949d
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/WebSocketServerTest.java
@@ -0,0 +1,102 @@
+// Copyright 2016 Yahoo Inc. 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.common.util.concurrent.SettableFuture;
+import com.ning.http.client.websocket.WebSocket;
+import com.ning.http.client.websocket.WebSocketByteListener;
+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.ResponseHandler;
+import org.testng.annotations.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+import static com.yahoo.jdisc.Response.Status.OK;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a>
+ */
+public class WebSocketServerTest {
+
+ @Test(enabled = false)
+ public void requireThatServerCanRespondToRequest() throws Exception {
+ final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler());
+ final SimpleWebSocketClient client = new SimpleWebSocketClient(driver);
+ final MyWebSocketListener listener = new MyWebSocketListener("Hello World!");
+ client.executeRequest("/status.html", listener);
+ assertThat(listener.response.get(60, TimeUnit.SECONDS), is("Hello World!"));
+ assertThat(client.close(), is(true));
+ assertThat(driver.close(), is(true));
+ }
+
+ //@Test Ignored: Broken in jetty 9.2.{3,4}
+ public void requireThatServerCanRespondToSslRequest() throws Exception {
+ final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler());
+ final SimpleWebSocketClient client = new SimpleWebSocketClient(driver);
+ final MyWebSocketListener listener = new MyWebSocketListener("Hello World!");
+ client.executeRequest("/status.html", listener);
+ assertThat(listener.response.get(60, TimeUnit.SECONDS), is("Hello World!"));
+ assertThat(client.close(), is(true));
+ assertThat(driver.close(), is(true));
+ }
+
+ private static class EchoRequestHandler extends AbstractRequestHandler {
+
+ @Override
+ public ContentChannel handleRequest(final Request request, final ResponseHandler handler) {
+ return handler.handleResponse(new Response(OK));
+ }
+ }
+
+ private static class MyWebSocketListener implements WebSocketByteListener {
+
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ final SettableFuture<String> response = SettableFuture.create();
+ final byte[] requestContent;
+
+ MyWebSocketListener(final String requestContent) {
+ this.requestContent = requestContent.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public void onOpen(final WebSocket webSocket) {
+ webSocket.sendMessage(requestContent);
+ webSocket.close();
+ }
+
+ @Override
+ public void onClose(final WebSocket webSocket) {
+ response.set(new String(out.toByteArray(), StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void onError(final Throwable t) {
+ response.setException(t);
+ }
+
+ @Override
+ public void onMessage(final byte[] buf) {
+ try {
+ out.write(buf);
+ } catch (final IOException e) {
+ response.setException(e);
+ }
+ }
+
+ @Override
+ public void onFragment(final byte[] buf, final boolean last) {
+ try {
+ out.write(buf);
+ } catch (final IOException e) {
+ response.setException(e);
+ }
+ }
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java
new file mode 100644
index 00000000000..b07499d0e02
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java
@@ -0,0 +1,175 @@
+// Copyright 2016 Yahoo Inc. 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.application.BindingRepository;
+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.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.testng.annotations.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 tonytv
+ */
+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 {
+ return TestDrivers.newInstance(dummyRequestHandler, bindings(requestFilters(), noBindings()));
+ }
+
+ private TestDriver responseFilterTestDriver() throws IOException {
+ return TestDrivers.newInstance(dummyRequestHandler, bindings(noBindings(), responseFilters()));
+ }
+
+ private Module bindings(BindingRepository<RequestFilter> requestFilters,
+ BindingRepository<ResponseFilter> responseFilters) {
+
+ return Modules.combine(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(FilterBindings.class).toInstance(new FilterBindings(requestFilters, responseFilters));
+ 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());
+ }
+
+ private BindingRepository<RequestFilter> requestFilters() {
+ BindingRepository<RequestFilter> repository = new BindingRepository<>();
+ repository.bind("http://*/*" , new TestRequestFilter());
+ return repository;
+ }
+
+ private BindingRepository<ResponseFilter> responseFilters() {
+ BindingRepository<ResponseFilter> repository = new BindingRepository<>();
+ repository.bind("http://*/*" , new TestResponseFilter());
+ return repository;
+ }
+
+ private <T> BindingRepository<T> noBindings() {
+ return new BindingRepository<>();
+ }
+
+
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java
new file mode 100644
index 00000000000..1aede0e9850
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java
@@ -0,0 +1,62 @@
+// Copyright 2016 Yahoo Inc. 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.AccessLogEntry;
+import com.yahoo.jdisc.http.server.jetty.TestDriver;
+import com.yahoo.jdisc.http.server.jetty.TestDrivers;
+import org.mockito.verification.VerificationMode;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class ServletAccessLoggingTest extends ServletTestBase {
+ private static final int MAX_LOG_WAIT_TIME_MILLIS = (int) 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(final AccessLog accessLog, final VerificationMode verificationMode) {
+ verify(accessLog, verificationMode).log(any(AccessLogEntry.class));
+ }
+
+ private TestDriver newTestDriver(final AccessLog accessLog) throws IOException {
+ return TestDrivers.newInstance(dummyRequestHandler, bindings(accessLog));
+ }
+
+ private Module bindings(final AccessLog accessLog) {
+ return Modules.combine(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(AccessLog.class).toInstance(accessLog);
+ }
+ },
+ guiceModule());
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java
new file mode 100644
index 00000000000..d427842c30e
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java
@@ -0,0 +1,123 @@
+// Copyright 2016 Yahoo Inc. 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.common.collect.ImmutableMap;
+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.apache.commons.lang3.tuple.ImmutablePair;
+import org.apache.commons.lang3.tuple.Pair;
+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;
+
+/**
+ * @author tonytv
+ * @author <a href="mailto:bakksjo@yahoo-inc.com">Oyvind Bakksjo</a>
+ */
+public class ServletTestBase {
+ private ImmutableMap<Pair<ComponentId, String>, HttpServlet> servlets = ImmutableMap.of(
+ ImmutablePair.of(TestServlet.ID, TestServlet.PATH), new TestServlet(),
+ ImmutablePair.of(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((idAndPath, servlet) ->
+ configBuilder.servlets(
+ idAndPath.getLeft().stringValue(),
+ new Builder().path(idAndPath.getRight())));
+
+ return new ServletPathsConfig(configBuilder);
+ }
+
+ protected ComponentRegistry<ServletHolder> servlets() {
+ ComponentRegistry<ServletHolder> result = new ComponentRegistry<>();
+
+ servlets.forEach((idAndPath, servlet) ->
+ result.register(idAndPath.getLeft(), new ServletHolder(servlet)));
+
+ 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/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java
new file mode 100644
index 00000000000..3693114dfdb
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/test/RemoteServerTestCase.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.jdisc.http.test;
+
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.TimeUnit;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RemoteServerTestCase {
+
+ @Test(enabled = false)
+ public void requireThatRequestUriFactoryWorks() throws IOException {
+ RemoteServer server = RemoteServer.newInstance();
+ try {
+ server.newRequestUri((String)null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ server.newRequestUri((URI)null);
+ fail();
+ } catch (NullPointerException e) {
+
+ }
+ try {
+ server.newRequestUri("foo");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getCause() instanceof URISyntaxException);
+ }
+ URI requestUri = server.newRequestUri("/foo?baz=cox#bar");
+ URI serverUri = server.connectionSpec();
+ assertEquals(serverUri.getScheme(), requestUri.getScheme());
+ assertEquals(serverUri.getUserInfo(), requestUri.getUserInfo());
+ assertEquals(serverUri.getHost(), requestUri.getHost());
+ assertEquals(serverUri.getPort(), requestUri.getPort());
+ assertEquals("/foo", requestUri.getPath());
+ assertEquals("baz=cox", requestUri.getQuery());
+ assertEquals("bar", requestUri.getFragment());
+ assertTrue(server.close(60, TimeUnit.SECONDS));
+ }
+}
diff --git a/jdisc_http_service/src/test/resources/ssl_keystore_test.jks b/jdisc_http_service/src/test/resources/ssl_keystore_test.jks
new file mode 100644
index 00000000000..6dbb19b9692
--- /dev/null
+++ b/jdisc_http_service/src/test/resources/ssl_keystore_test.jks
Binary files differ