summaryrefslogtreecommitdiffstats
path: root/jdisc_http_service/src/main/java/com/yahoo/jdisc
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/src/main/java/com/yahoo/jdisc
Publish
Diffstat (limited to 'jdisc_http_service/src/main/java/com/yahoo/jdisc')
-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
106 files changed, 10082 insertions, 0 deletions
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);
+ }
+ }
+ };
+ }
+}