From 121e22ef74bb74cbae8aac6d7c01cc2de6114086 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 11:49:26 +0100 Subject: Add config defs from jdisc_http_service and container-core-config --- .../container.core.access-log.def | 23 ++++ .../container.logging.connection-log.def | 10 ++ ...c.http.client.jdisc.http.client.http-client.def | 36 ++++++ .../jdisc.http.jdisc.http.connector.def | 127 +++++++++++++++++++++ .../jdisc.http.jdisc.http.server.def | 67 +++++++++++ .../jdisc.http.jdisc.http.servlet-paths.def | 5 + 6 files changed, 268 insertions(+) create mode 100644 container-core/src/main/resources/configdefinitions/container.core.access-log.def create mode 100644 container-core/src/main/resources/configdefinitions/container.logging.connection-log.def create mode 100644 container-core/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def create mode 100644 container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def create mode 100644 container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def create mode 100644 container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def diff --git a/container-core/src/main/resources/configdefinitions/container.core.access-log.def b/container-core/src/main/resources/configdefinitions/container.core.access-log.def new file mode 100644 index 00000000000..69058b3d8da --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/container.core.access-log.def @@ -0,0 +1,23 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container.core + +# File name patterns supporting the expected time variables, e.g. ".%Y%m%d%H%M%S" +fileHandler.pattern string + +# When should rotation happen, in minutes after midnight +# Does this really need to be configurable? +# Could just configure "every N minutes" instead +fileHandler.rotation string default="0 60 ..." + +# Use this as the name of the symlink created pointing to the newest file in the "date" naming scheme. +# This is ignored if the sequence naming scheme is used. +fileHandler.symlink string default="" + +# compress the previous access log after rotation +fileHandler.compressOnRotation bool default=true + +# Compression format +fileHandler.compressionFormat enum {GZIP, ZSTD} default=GZIP + +# Max queue length of file handler +fileHandler.queueSize int default=10000 diff --git a/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def b/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def new file mode 100644 index 00000000000..e845a8351d5 --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def @@ -0,0 +1,10 @@ +namespace=container.logging + +# Name of the cluster +cluster string + +# Log directory name +logDirectoryName string default="qrs" + +# Max queue length of file handler +queueSize int default=10000 \ No newline at end of file diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def b/container-core/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def new file mode 100644 index 00000000000..8f99fccec94 --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def @@ -0,0 +1,36 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http.client + +userAgent string default = "JDisc/1.0" +chunkedEncodingEnabled bool default = false +compressionEnabled bool default = false +connectionPoolEnabled bool default = true +followRedirects bool default = false +removeQueryParamsOnRedirect bool default = true +sslConnectionPoolEnabled bool default = true +proxyServer string default = "" +useProxyProperties bool default = false +useRawUri bool default = false +compressionLevel int default = -1 +maxNumConnections int default = -1 +maxNumConnectionsPerHost int default = -1 +maxNumRedirects int default = 5 +maxNumRetries int default = 0 +connectionTimeout double default = 60 +idleConnectionInPoolTimeout double default = 60 +idleConnectionTimeout double default = 60 +idleWebSocketTimeout double default = 15 +requestTimeout double default = 60 + +ssl.enabled bool default = false +ssl.keyStoreType string default = "JKS" + +# Vespa home is prepended is path is relative +ssl.keyStorePath string default = "jdisc_container/keyStore.jks" + +# Vespa home is prepended is path is relative +ssl.trustStorePath string default = "conf/jdisc_container/trustStore.jks" + +ssl.keyDBKey string default = "jdisc_container" +ssl.algorithm string default = "SunX509" +ssl.protocol string default = "TLS" diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def new file mode 100644 index 00000000000..055e5ad62d2 --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def @@ -0,0 +1,127 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# The TCP port to listen to for this connector. +listenPort int default=0 + +# The connector name +name string default="default" + +# The header field cache size. +headerCacheSize int default=512 + +# The size of the buffer into which response content is aggregated before being sent to the client. +outputBufferSize int default=65536 + +# The maximum size of a request header. +requestHeaderSize int default=65536 + +# The maximum size of a response header. +responseHeaderSize int default=65536 + +# The accept queue size (also known as accept backlog). +acceptQueueSize int default=0 + +# Whether the server socket reuses addresses. +reuseAddress bool default=true + +# The maximum idle time for a connection, which roughly translates to the Socket.setSoTimeout(int). +idleTimeout double default=180.0 + +# DEPRECATED - Ignored, no longer in use +stopTimeout double default = 30.0 +# TODO Vespa 8 Remove stop timeout + +# Whether or not to have socket keep alive turned on. +tcpKeepAliveEnabled bool default=false + +# Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm). +tcpNoDelay bool default=true + +# Whether to enable connection throttling. New connections will be dropped when a threshold is exceeded. +throttling.enabled bool default=false + +# Max number of connections. +throttling.maxConnections int default=-1 + +# Max memory utilization as a value between 0 and 1. +throttling.maxHeapUtilization double default=-1.0 + +# Max connection accept rate per second. +throttling.maxAcceptRate int default=-1 + +# Idle timeout in seconds applied to endpoints when a threshold is exceeded. +throttling.idleTimeout double default=-1.0 + +# Whether to enable TLS on connector when Vespa is configured with TLS. +# The connector will implicitly enable TLS if set to 'true' and Vespa TLS is enabled. +implicitTlsEnabled bool default=true + +# Whether to enable SSL for this connector. +ssl.enabled bool default=false + +# File with private key in PEM format. Specify either this or privateKey, but not both +ssl.privateKeyFile string default="" + +# Private key in PEM format. Specify either this or privateKeyFile, but not both +ssl.privateKey string default="" + +# File with certificate in PEM format. Specify either this or certificate, but not both +ssl.certificateFile string default="" + +# Certificate in PEM format. Specify either this or certificateFile, but not both +ssl.certificate string default="" + +# with trusted CA certificates in PEM format. Used to verify clients +# - this is the name of a file on the local container file system +# - only one of caCertificateFile and caCertificate +ssl.caCertificateFile string default="" + +# with trusted CA certificates in PEM format. Used to verify clients +# - this is the actual certificates instead of a pointer to the file +# - only one of caCertificateFile and caCertificate +ssl.caCertificate string default="" + +# Client authentication mode. See SSLEngine.getNeedClientAuth()/getWantClientAuth() for details. +ssl.clientAuth enum { DISABLED, WANT_AUTH, NEED_AUTH } default=DISABLED + +# List of enabled cipher suites. JDisc will use Vespa default if empty. +ssl.enabledCipherSuites[] string + +# List of enabled TLS protocol versions. JDisc will use Vespa default if empty. +ssl.enabledProtocols[] string + +# Enforce TLS client authentication for https requests at the http layer. +# Intended to be used with connectors with optional client authentication enabled. +# 401 status code is returned for requests from non-authenticated clients. +tlsClientAuthEnforcer.enable bool default=false + +# Paths where client authentication should not be enforced. To be used in combination with WANT_AUTH. Typically used for health checks. +tlsClientAuthEnforcer.pathWhitelist[] string + +# Use connector only for proxying '/status.html' health checks. Any ssl configuration will be ignored if this option is enabled. +healthCheckProxy.enable bool default=false + +# Which port to proxy +healthCheckProxy.port int default=8080 + +# Low-level timeout for proxy client (socket connect, socket read, connection pool). Aggregate timeout will be longer. +healthCheckProxy.clientTimeout double default=1.0 + +# Enable PROXY protocol V1/V2 support (only for https connectors). +proxyProtocol.enabled bool default=false + +# Allow https in parallel with proxy protocol +proxyProtocol.mixedMode bool default=false + +# Redirect all requests to https port +secureRedirect.enabled bool default=false + +# Target port for redirect +secureRedirect.port int default=443 + +# Maximum number of request per connection before server marks connections as non-persistent. Set to '0' to disable. +maxRequestsPerConnection int default=0 + +# Maximum number of seconds a connection can live before it's marked as non-persistent. Set to '0' to disable. +maxConnectionLife double default=0.0 diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def new file mode 100644 index 00000000000..049080dedbd --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def @@ -0,0 +1,67 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# Whether to enable developer mode, where stack traces etc are visible in response bodies. +developerMode bool default=false + +# The gzip compression level to use, if compression is enabled in a request. +responseCompressionLevel int default=6 + +# DEPRECATED - Ignored, no longer in use. +httpKeepAliveEnabled bool default=true +# TODO Vespa 8 Remove httpKeepAliveEnabled + +# Maximum number of request per http connection before server will hangup. +# Naming taken from apache http server. +# 0 means never hangup. +# DEPRECATED - Ignored, no longer in use. Use similar parameter in connector config instead. +maxKeepAliveRequests int default=0 +# TODO Vespa 8 Remove maxKeepAliveRequests + +# Whether the request body of POSTed forms should be removed (form parameters are available as request parameters). +removeRawPostBodyForWwwUrlEncodedPost bool default=false + +# The component ID of a filter +filter[].id string + +# The binding of a filter +filter[].binding string + +# Filter id for a default filter (chain) +defaultFilters[].filterId string + +# The local port which the default filter should be applied to +defaultFilters[].localPort int + +# Reject all requests not handled by a request filter (chain) +strictFiltering bool default = false + +# Max number of threads in underlying Jetty pool +maxWorkerThreads int default = 200 + +# Min number of threads in underlying Jetty pool +minWorkerThreads int default = 8 + +# Stop timeout in seconds. The maximum allowed time to process in-flight requests during server shutdown. Setting it to 0 disable graceful shutdown. +stopTimeout double default = 30.0 + +# Enable embedded JMX server. Note: only accessible through the loopback interface. +jmx.enabled bool default = false + +# Listen port for the JMX server. +jmx.listenPort int default = 1099 + +# Paths that should be reported with monitoring dimensions where applicable +metric.monitoringHandlerPaths[] string + +# Paths that should be reported with search dimensions where applicable +metric.searchHandlerPaths[] string + +# HTTP request headers that contain remote address +accessLog.remoteAddressHeaders[] string + +# HTTP request headers that contain remote port +accessLog.remotePortHeaders[] string + +# Whether to enable jdisc connection log +connectionLog.enabled bool default=false diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def new file mode 100644 index 00000000000..86707b027be --- /dev/null +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=jdisc.http + +# path by servlet componentId +servlets{}.path string -- cgit v1.2.3 From 53de5d070f8b6cc271c1642a5140d368444fc5b7 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 11:56:04 +0100 Subject: Remove the container-core-config module. --- CMakeLists.txt | 1 - cloud-tenant-base-dependencies-enforcer/pom.xml | 1 - container-core-config/CMakeLists.txt | 2 - container-core-config/OWNERS | 1 - container-core-config/README.md | 8 ---- container-core-config/pom.xml | 48 ---------------------- .../com/yahoo/container/core/package-info.java | 5 --- .../container.core.access-log.def | 23 ----------- container-core/pom.xml | 6 --- pom.xml | 1 - 10 files changed, 96 deletions(-) delete mode 100644 container-core-config/CMakeLists.txt delete mode 100644 container-core-config/OWNERS delete mode 100644 container-core-config/README.md delete mode 100644 container-core-config/pom.xml delete mode 100644 container-core-config/src/main/java/com/yahoo/container/core/package-info.java delete mode 100644 container-core-config/src/main/resources/configdefinitions/container.core.access-log.def diff --git a/CMakeLists.txt b/CMakeLists.txt index eae4cb338eb..40f60d7daab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,6 @@ add_subdirectory(configserver) add_subdirectory(configserver-flags) add_subdirectory(configutil) add_subdirectory(container-core) -add_subdirectory(container-core-config) add_subdirectory(container-di) add_subdirectory(container-disc) add_subdirectory(container-jersey2) diff --git a/cloud-tenant-base-dependencies-enforcer/pom.xml b/cloud-tenant-base-dependencies-enforcer/pom.xml index ee3ed48318b..d821501bd2f 100644 --- a/cloud-tenant-base-dependencies-enforcer/pom.xml +++ b/cloud-tenant-base-dependencies-enforcer/pom.xml @@ -149,7 +149,6 @@ com.yahoo.vespa:configdefinitions:*:jar:provided com.yahoo.vespa:configgen:*:jar:provided com.yahoo.vespa:container-core:*:jar:provided - com.yahoo.vespa:container-core-config:*:jar:provided com.yahoo.vespa:container-dev:*:jar:provided com.yahoo.vespa:container-di:*:jar:provided com.yahoo.vespa:container-disc:*:jar:provided diff --git a/container-core-config/CMakeLists.txt b/container-core-config/CMakeLists.txt deleted file mode 100644 index 307bb103e9f..00000000000 --- a/container-core-config/CMakeLists.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -install_config_definitions() diff --git a/container-core-config/OWNERS b/container-core-config/OWNERS deleted file mode 100644 index fb71c67318d..00000000000 --- a/container-core-config/OWNERS +++ /dev/null @@ -1 +0,0 @@ -bjorncs \ No newline at end of file diff --git a/container-core-config/README.md b/container-core-config/README.md deleted file mode 100644 index ac37c242a5f..00000000000 --- a/container-core-config/README.md +++ /dev/null @@ -1,8 +0,0 @@ - -# container-core-config - -Contains config definitions with package `com.yahoo.container.core` that are used by other modules. - -This artifact is embedded inside container-core jar, but built as bundle to allow other modules to depend on container-core config definitions without depending on container-core. -The generated config classes cannot be moved to container-core as it would introduce a cycles in Maven dependency graph. -This works at correctly runtime as OSGi allows cycling dependencies between bundles. diff --git a/container-core-config/pom.xml b/container-core-config/pom.xml deleted file mode 100644 index fb4aea4071e..00000000000 --- a/container-core-config/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - 4.0.0 - - com.yahoo.vespa - parent - 7-SNAPSHOT - ../parent/pom.xml - - container-core-config - 7-SNAPSHOT - container-plugin - - - com.yahoo.vespa - annotations - ${project.version} - provided - - - com.yahoo.vespa - config-lib - ${project.version} - provided - - - - - - com.yahoo.vespa - bundle-plugin - true - - - org.apache.maven.plugins - maven-compiler-plugin - - - ${buildOutputDirectory} - - - ${project.build.directory}/classes/ - - \ No newline at end of file diff --git a/container-core-config/src/main/java/com/yahoo/container/core/package-info.java b/container-core-config/src/main/java/com/yahoo/container/core/package-info.java deleted file mode 100644 index c9c683bd68a..00000000000 --- a/container-core-config/src/main/java/com/yahoo/container/core/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.container.core; - -import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file diff --git a/container-core-config/src/main/resources/configdefinitions/container.core.access-log.def b/container-core-config/src/main/resources/configdefinitions/container.core.access-log.def deleted file mode 100644 index 69058b3d8da..00000000000 --- a/container-core-config/src/main/resources/configdefinitions/container.core.access-log.def +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=container.core - -# File name patterns supporting the expected time variables, e.g. ".%Y%m%d%H%M%S" -fileHandler.pattern string - -# When should rotation happen, in minutes after midnight -# Does this really need to be configurable? -# Could just configure "every N minutes" instead -fileHandler.rotation string default="0 60 ..." - -# Use this as the name of the symlink created pointing to the newest file in the "date" naming scheme. -# This is ignored if the sequence naming scheme is used. -fileHandler.symlink string default="" - -# compress the previous access log after rotation -fileHandler.compressOnRotation bool default=true - -# Compression format -fileHandler.compressionFormat enum {GZIP, ZSTD} default=GZIP - -# Max queue length of file handler -fileHandler.queueSize int default=10000 diff --git a/container-core/pom.xml b/container-core/pom.xml index ea6e7d32310..3310aa07d92 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -97,12 +97,6 @@ org.apache.httpcomponents httpclient - - com.yahoo.vespa - container-core-config - ${project.version} - compile - com.yahoo.vespa vespajlib diff --git a/pom.xml b/pom.xml index def94f212b1..60fe298330f 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,6 @@ config_test container container-core - container-core-config container-dependencies-enforcer container-dependency-versions container-dev -- cgit v1.2.3 From f241e4ec9042a21dc85c02aeb94a448acf1a67cf Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 17:42:52 +0100 Subject: Rearrange deps, no functional changes. --- container-core/pom.xml | 108 ++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/container-core/pom.xml b/container-core/pom.xml index 3310aa07d92..e913d1d6d38 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -29,33 +29,27 @@ com.yahoo.vespa - http-utils + container-documentapi ${project.version} com.yahoo.vespa - linguistics + document ${project.version} - - - com.google.guava - guava - log4j log4j - - org.slf4j - slf4j-api + com.yahoo.vespa + config com.yahoo.vespa - messagebus + fileacquirer ${project.version} @@ -70,47 +64,48 @@ com.yahoo.vespa - document + http-utils + ${project.version} + + + com.yahoo.vespa + linguistics ${project.version} + + + com.google.guava + guava + log4j log4j - com.yahoo.vespa - config + + org.slf4j + slf4j-api com.yahoo.vespa - container-documentapi - ${project.version} - - - com.yahoo.vespa - vdslib - ${project.version} - - - org.apache.httpcomponents - httpclient - - - com.yahoo.vespa - vespajlib + messagebus ${project.version} log4j log4j + + com.yahoo.vespa + config + com.yahoo.vespa - fileacquirer + statistics ${project.version} @@ -125,23 +120,23 @@ com.yahoo.vespa - statistics + vdslib + ${project.version} + + + com.yahoo.vespa + vespajlib ${project.version} log4j log4j - - com.yahoo.vespa - config - - junit - junit - test + org.apache.httpcomponents + httpclient org.junit.jupiter @@ -191,6 +186,18 @@ ${project.version} provided + + com.yahoo.vespa + config + ${project.version} + provided + + + com.yahoo.vespa + config-bundle + ${project.version} + provided + com.yahoo.vespa defaults @@ -224,23 +231,11 @@ jetty-servlet provided - - com.yahoo.vespa - config-bundle - ${project.version} - provided - - - com.yahoo.vespa - config - ${project.version} - provided - - org.mockito - mockito-core + com.github.tomakehurst + wiremock-standalone test @@ -250,8 +245,8 @@ test - com.github.tomakehurst - wiremock-standalone + junit + junit test @@ -259,6 +254,11 @@ assertj-core test + + org.mockito + mockito-core + test + -- cgit v1.2.3 From 8e9b938a66a41bd5fa86c02a202d7a60001ac061 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 18:21:44 +0100 Subject: Explicitly add dependencies used by this module. --- container-core/pom.xml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/container-core/pom.xml b/container-core/pom.xml index e913d1d6d38..c38ff2fb103 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -103,6 +103,11 @@ + + com.yahoo.vespa + security-utils + ${project.version} + com.yahoo.vespa statistics @@ -180,6 +185,12 @@ guava provided + + com.google.inject + guice + no_aop + provided + com.yahoo.vespa component @@ -198,6 +209,12 @@ ${project.version} provided + + com.yahoo.vespa + config-lib + ${project.version} + provided + com.yahoo.vespa defaults @@ -216,6 +233,12 @@ ${project.version} provided + + com.yahoo.vespa + yolean + ${project.version} + provided + javax.servlet javax.servlet-api -- cgit v1.2.3 From c6d3b4003c5d5857817adb9db240ddd86bbc4348 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 18:44:22 +0100 Subject: Add dependencies required by jdisc_http_service - vespalog is exported by the container-disc bundle , and hence declared in compile scope. --- container-core/pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/container-core/pom.xml b/container-core/pom.xml index c38ff2fb103..f387172b91d 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -139,6 +139,11 @@ + + com.yahoo.vespa + vespalog + ${project.version} + org.apache.httpcomponents httpclient @@ -233,6 +238,12 @@ ${project.version} provided + + com.yahoo.vespa + jdisc_jetty + ${project.version} + provided + com.yahoo.vespa yolean -- cgit v1.2.3 From 17349ba3ab25ff89ba449244242f9cacb4846bb1 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 19:51:09 +0100 Subject: Add main java source from jdisc_http_service. --- .../com/yahoo/container/logging/AccessLog.java | 36 ++ .../yahoo/container/logging/AccessLogEntry.java | 112 ++++ .../yahoo/container/logging/AccessLogHandler.java | 36 ++ .../logging/CircularArrayAccessLogKeeper.java | 48 ++ .../com/yahoo/container/logging/ConnectionLog.java | 10 + .../container/logging/ConnectionLogEntry.java | 225 ++++++++ .../container/logging/ConnectionLogHandler.java | 30 ++ .../java/com/yahoo/container/logging/Coverage.java | 64 +++ .../yahoo/container/logging/FileConnectionLog.java | 30 ++ .../com/yahoo/container/logging/FormatUtil.java | 46 ++ .../com/yahoo/container/logging/HitCounts.java | 78 +++ .../com/yahoo/container/logging/JSONAccessLog.java | 27 + .../com/yahoo/container/logging/JSONFormatter.java | 193 +++++++ .../container/logging/JsonConnectionLogWriter.java | 122 +++++ .../yahoo/container/logging/LogFileHandler.java | 563 +++++++++++++++++++++ .../com/yahoo/container/logging/LogFormatter.java | 191 +++++++ .../com/yahoo/container/logging/LogWriter.java | 10 + .../com/yahoo/container/logging/RequestLog.java | 13 + .../yahoo/container/logging/RequestLogEntry.java | 186 +++++++ .../yahoo/container/logging/RequestLogHandler.java | 9 + .../com/yahoo/container/logging/TraceRenderer.java | 185 +++++++ .../yahoo/container/logging/VespaAccessLog.java | 113 +++++ .../com/yahoo/container/logging/package-info.java | 5 + .../com/yahoo/jdisc/http/CertificateStore.java | 26 + .../src/main/java/com/yahoo/jdisc/http/Cookie.java | 250 +++++++++ .../java/com/yahoo/jdisc/http/HttpHeaders.java | 122 +++++ .../java/com/yahoo/jdisc/http/HttpRequest.java | 342 +++++++++++++ .../java/com/yahoo/jdisc/http/HttpResponse.java | 125 +++++ .../java/com/yahoo/jdisc/http/SecretStore.java | 23 + .../com/yahoo/jdisc/http/cloud/package-info.java | 4 + .../yahoo/jdisc/http/filter/DiscFilterRequest.java | 543 ++++++++++++++++++++ .../jdisc/http/filter/DiscFilterResponse.java | 154 ++++++ .../com/yahoo/jdisc/http/filter/FilterConfig.java | 42 ++ .../jdisc/http/filter/JDiscCookieWrapper.java | 79 +++ .../jdisc/http/filter/JdiscFilterRequest.java | 133 +++++ .../jdisc/http/filter/JdiscFilterResponse.java | 67 +++ .../com/yahoo/jdisc/http/filter/RequestFilter.java | 14 + .../yahoo/jdisc/http/filter/RequestFilterBase.java | 9 + .../com/yahoo/jdisc/http/filter/RequestView.java | 45 ++ .../yahoo/jdisc/http/filter/ResponseFilter.java | 14 + .../jdisc/http/filter/ResponseFilterBase.java | 9 + .../jdisc/http/filter/SecurityFilterInvoker.java | 108 ++++ .../jdisc/http/filter/SecurityRequestFilter.java | 13 + .../http/filter/SecurityRequestFilterChain.java | 77 +++ .../jdisc/http/filter/SecurityResponseFilter.java | 8 + .../http/filter/SecurityResponseFilterChain.java | 101 ++++ .../jdisc/http/filter/ServletFilterRequest.java | 169 +++++++ .../jdisc/http/filter/ServletFilterResponse.java | 81 +++ .../http/filter/chain/EmptyRequestFilter.java | 24 + .../http/filter/chain/EmptyResponseFilter.java | 24 + .../http/filter/chain/RequestFilterChain.java | 55 ++ .../http/filter/chain/ResponseFilterChain.java | 54 ++ .../http/filter/chain/ResponseHandlerGuard.java | 29 ++ .../jdisc/http/filter/chain/package-info.java | 5 + .../com/yahoo/jdisc/http/filter/package-info.java | 7 + .../java/com/yahoo/jdisc/http/package-info.java | 7 + .../http/server/jetty/AccessLogRequestLog.java | 167 ++++++ .../server/jetty/AccessLoggingRequestHandler.java | 59 +++ .../http/server/jetty/AsyncCompleteListener.java | 22 + .../http/server/jetty/CompletionHandlerUtils.java | 14 + .../http/server/jetty/CompletionHandlers.java | 57 +++ .../http/server/jetty/ConnectionThrottler.java | 274 ++++++++++ .../jdisc/http/server/jetty/ConnectorFactory.java | 140 +++++ .../server/jetty/ErrorResponseContentCreator.java | 41 ++ .../jdisc/http/server/jetty/ExceptionWrapper.java | 59 +++ .../jdisc/http/server/jetty/FilterBindings.java | 102 ++++ .../jdisc/http/server/jetty/FilterInvoker.java | 28 + .../server/jetty/FilterInvokingPrintWriter.java | 266 ++++++++++ .../jetty/FilterInvokingServletOutputStream.java | 165 ++++++ .../jdisc/http/server/jetty/FilterResolver.java | 88 ++++ .../http/server/jetty/FilteringRequestHandler.java | 134 +++++ .../http/server/jetty/FormPostRequestHandler.java | 188 +++++++ .../http/server/jetty/HealthCheckProxyHandler.java | 274 ++++++++++ .../http/server/jetty/HttpRequestDispatch.java | 243 +++++++++ .../http/server/jetty/HttpRequestFactory.java | 87 ++++ .../jetty/HttpResponseStatisticsCollector.java | 300 +++++++++++ .../http/server/jetty/HttpServletRequestUtils.java | 38 ++ .../jdisc/http/server/jetty/JDiscContext.java | 33 ++ .../server/jetty/JDiscFilterInvokerFilter.java | 294 +++++++++++ .../jdisc/http/server/jetty/JDiscHttpServlet.java | 148 ++++++ .../http/server/jetty/JDiscServerConnector.java | 104 ++++ .../http/server/jetty/JettyConnectionLogger.java | 373 ++++++++++++++ .../jdisc/http/server/jetty/JettyHttpServer.java | 298 +++++++++++ .../jdisc/http/server/jetty/MetricDefinitions.java | 79 +++ .../jdisc/http/server/jetty/OneTimeRunnable.java | 23 + .../jetty/ReferenceCountingRequestHandler.java | 257 ++++++++++ .../jdisc/http/server/jetty/RequestException.java | 39 ++ .../http/server/jetty/RequestMetricReporter.java | 85 ++++ .../http/server/jetty/SecuredRedirectHandler.java | 58 +++ .../http/server/jetty/ServerMetricReporter.java | 115 +++++ .../server/jetty/ServletOutputStreamWriter.java | 299 +++++++++++ .../http/server/jetty/ServletRequestReader.java | 270 ++++++++++ .../server/jetty/ServletResponseController.java | 251 +++++++++ .../server/jetty/SslHandshakeFailedListener.java | 52 ++ .../http/server/jetty/SslHandshakeFailure.java | 61 +++ .../jetty/TlsClientAuthenticationEnforcer.java | 83 +++ .../server/jetty/UnsupportedFilterInvoker.java | 32 ++ .../jdisc/http/server/jetty/VoidConnectionLog.java | 16 + .../jdisc/http/server/jetty/VoidRequestLog.java | 14 + .../jdisc/http/server/jetty/package-info.java | 3 + .../http/servlet/ServletOrJdiscHttpRequest.java | 40 ++ .../http/servlet/ServletOrJdiscHttpResponse.java | 23 + .../yahoo/jdisc/http/servlet/ServletRequest.java | 272 ++++++++++ .../yahoo/jdisc/http/servlet/ServletResponse.java | 66 +++ .../com/yahoo/jdisc/http/servlet/package-info.java | 5 + .../jdisc/http/ssl/SslContextFactoryProvider.java | 21 + .../impl/ConfiguredSslContextFactoryProvider.java | 138 +++++ .../ssl/impl/DefaultSslContextFactoryProvider.java | 79 +++ .../http/ssl/impl/JDiscSslContextFactory.java | 37 ++ .../http/ssl/impl/SslContextFactoryUtils.java | 32 ++ .../http/ssl/impl/TlsContextBasedProvider.java | 42 ++ .../yahoo/jdisc/http/ssl/impl/package-info.java | 8 + .../com/yahoo/jdisc/http/ssl/package-info.java | 10 + 113 files changed, 11601 insertions(+) create mode 100644 container-core/src/main/java/com/yahoo/container/logging/AccessLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/AccessLogHandler.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/ConnectionLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/Coverage.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/FileConnectionLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/FormatUtil.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/HitCounts.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/JSONAccessLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/LogFileHandler.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/LogFormatter.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/LogWriter.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/RequestLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/RequestLogHandler.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/VespaAccessLog.java create mode 100644 container-core/src/main/java/com/yahoo/container/logging/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/CertificateStore.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/Cookie.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/HttpRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/HttpResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/SecretStore.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/filter/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactoryProvider.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/DefaultSslContextFactoryProvider.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/JDiscSslContextFactory.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/SslContextFactoryUtils.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProvider.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/impl/package-info.java create mode 100644 container-core/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java diff --git a/container-core/src/main/java/com/yahoo/container/logging/AccessLog.java b/container-core/src/main/java/com/yahoo/container/logging/AccessLog.java new file mode 100644 index 00000000000..2d46c53bca7 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/AccessLog.java @@ -0,0 +1,36 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + + +import com.google.inject.Inject; +import com.yahoo.component.provider.ComponentRegistry; + +/** + * Logs to all the configured access logs. + * + * @author Tony Vaagenes + * @author bjorncs + */ +public class AccessLog implements RequestLog { + + public static final AccessLog NONE_INSTANCE = new AccessLog(new ComponentRegistry<>()); + + private final ComponentRegistry implementers; + + @Inject + public AccessLog(ComponentRegistry implementers) { + this.implementers = implementers; + } + + public static AccessLog voidAccessLog() { + return NONE_INSTANCE; + } + + @Override + public void log(RequestLogEntry entry) { + for (RequestLogHandler handler: implementers.allComponents()) { + handler.log(entry); + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java b/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java new file mode 100644 index 00000000000..42285fb85bb --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/AccessLogEntry.java @@ -0,0 +1,112 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.collections.ListMap; +import com.yahoo.yolean.trace.TraceNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static java.util.stream.Collectors.toMap; + +/** + *

Information to be logged in the access log.

+ * + *

This class contains the union of all information that can be + * logged with all the supported access log formats.

+ * + *

The add methods can be called multiple times, + * but the parameters should be different for each + * invocation of the same method.

+ * + * This class is thread-safe. + * + * @author Tony Vaagenes + * @author bakksjo + * @author bjorncs + */ +public class AccessLogEntry { + + private final Object monitor = new Object(); + + private HitCounts hitCounts; + private TraceNode traceNode; + private ListMap keyValues=null; + + public void setHitCounts(final HitCounts hitCounts) { + synchronized (monitor) { + requireNull(this.hitCounts); + this.hitCounts = hitCounts; + } + } + + public HitCounts getHitCounts() { + synchronized (monitor) { + return hitCounts; + } + } + + public void addKeyValue(String key,String value) { + synchronized (monitor) { + if (keyValues == null) { + keyValues = new ListMap<>(); + } + keyValues.put(key,value); + } + } + + public Map> getKeyValues() { + synchronized (monitor) { + if (keyValues == null) { + return null; + } + + final Map> newMapWithImmutableValues = mapValues( + keyValues.entrySet(), + valueList -> Collections.unmodifiableList(new ArrayList<>(valueList))); + return Collections.unmodifiableMap(newMapWithImmutableValues); + } + } + + private static Map mapValues( + final Set> entrySet, + final Function valueConverter) { + return entrySet.stream() + .collect(toMap( + entry -> entry.getKey(), + entry -> valueConverter.apply(entry.getValue()))); + } + + public void setTrace(TraceNode traceNode) { + synchronized (monitor) { + requireNull(this.traceNode); + this.traceNode = traceNode; + } + } + + public TraceNode getTrace() { + synchronized (monitor) { + return traceNode; + } + } + + @Override + public String toString() { + return "AccessLogEntry{" + + "hitCounts=" + hitCounts + + ", traceNode=" + traceNode + + ", keyValues=" + keyValues + + '}'; + } + + private static void requireNull(final Object value) { + if (value != null) { + throw new IllegalStateException("Attempt to overwrite field that has been assigned. Value: " + value); + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/AccessLogHandler.java b/container-core/src/main/java/com/yahoo/container/logging/AccessLogHandler.java new file mode 100644 index 00000000000..89aab1513ee --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/AccessLogHandler.java @@ -0,0 +1,36 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.container.core.AccessLogConfig; + +/** + * @author Bjorn Borud + */ +class AccessLogHandler { + + private final LogFileHandler logFileHandler; + + AccessLogHandler(AccessLogConfig.FileHandler config, LogWriter logWriter) { + logFileHandler = new LogFileHandler<>( + toCompression(config), config.pattern(), config.rotation(), + config.symlink(), config.queueSize(), "request-logger", logWriter); + } + + public void log(RequestLogEntry entry) { + logFileHandler.publish(entry); + } + + private LogFileHandler.Compression toCompression(AccessLogConfig.FileHandler config) { + if (!config.compressOnRotation()) return LogFileHandler.Compression.NONE; + switch (config.compressionFormat()) { + case ZSTD: return LogFileHandler.Compression.ZSTD; + case GZIP: return LogFileHandler.Compression.GZIP; + default: throw new IllegalArgumentException(config.compressionFormat().toString()); + } + } + + void shutdown() { + logFileHandler.close(); + logFileHandler.shutdown(); + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java b/container-core/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java new file mode 100644 index 00000000000..dc749c71613 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java @@ -0,0 +1,48 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +/** + * This class keeps some information from the access log from the requests in memory. It is thread-safe. + * + * @author dybis + */ +public class CircularArrayAccessLogKeeper { + public static final int SIZE = 1000; + private final Deque uris = new ArrayDeque<>(SIZE); + private final Object monitor = new Object(); + + /** + * This class is intended to be used with injection so it can be shared between other classes. + */ + public CircularArrayAccessLogKeeper() {} + + /** + * Creates a list of Uris. + * @return URIs as string + */ + public List getUris() { + final List uriList = new ArrayList<>(); + synchronized (monitor) { + uris.iterator().forEachRemaining(uri -> uriList.add(uri)); + } + return uriList; + } + + /** + * Add a new URI. It might remove an old entry to make space for new entry. + * @param uri uri as string + */ + public void addUri(String uri) { + synchronized (monitor) { + if (uris.size() == SIZE) { + uris.pop(); + } + uris.add(uri); + } + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/ConnectionLog.java b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLog.java new file mode 100644 index 00000000000..310231a4a1e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLog.java @@ -0,0 +1,10 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.logging; + +/** + * @author mortent + */ +public interface ConnectionLog { + void log(ConnectionLogEntry connectionLogEntry); +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java new file mode 100644 index 00000000000..6afe3b74329 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java @@ -0,0 +1,225 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.logging; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * @author mortent + */ +public class ConnectionLogEntry { + + private final UUID id; + private final Instant timestamp; + private final Double durationSeconds; + private final String peerAddress; + private final Integer peerPort; + private final String localAddress; + private final Integer localPort; + private final String remoteAddress; + private final Integer remotePort; + private final Long httpBytesReceived; + private final Long httpBytesSent; + private final Long requests; + private final Long responses; + private final String sslSessionId; + private final String sslProtocol; + private final String sslCipherSuite; + private final String sslPeerSubject; + private final Instant sslPeerNotBefore; + private final Instant sslPeerNotAfter; + private final String sslSniServerName; + private final SslHandshakeFailure sslHandshakeFailure; + + + private ConnectionLogEntry(Builder builder) { + this.id = builder.id; + this.timestamp = builder.timestamp; + this.durationSeconds = builder.durationSeconds; + this.peerAddress = builder.peerAddress; + this.peerPort = builder.peerPort; + this.localAddress = builder.localAddress; + this.localPort = builder.localPort; + this.remoteAddress = builder.remoteAddress; + this.remotePort = builder.remotePort; + this.httpBytesReceived = builder.httpBytesReceived; + this.httpBytesSent = builder.httpBytesSent; + this.requests = builder.requests; + this.responses = builder.responses; + this.sslSessionId = builder.sslSessionId; + this.sslProtocol = builder.sslProtocol; + this.sslCipherSuite = builder.sslCipherSuite; + this.sslPeerSubject = builder.sslPeerSubject; + this.sslPeerNotBefore = builder.sslPeerNotBefore; + this.sslPeerNotAfter = builder.sslPeerNotAfter; + this.sslSniServerName = builder.sslSniServerName; + this.sslHandshakeFailure = builder.sslHandshakeFailure; + } + + public static Builder builder(UUID id, Instant timestamp) { + return new Builder(id, timestamp); + } + + public String id() { return id.toString(); } + public Instant timestamp() { return timestamp; } + public Optional durationSeconds() { return Optional.ofNullable(durationSeconds); } + public Optional peerAddress() { return Optional.ofNullable(peerAddress); } + public Optional peerPort() { return Optional.ofNullable(peerPort); } + public Optional localAddress() { return Optional.ofNullable(localAddress); } + public Optional localPort() { return Optional.ofNullable(localPort); } + public Optional remoteAddress() { return Optional.ofNullable(remoteAddress); } + public Optional remotePort() { return Optional.ofNullable(remotePort); } + public Optional httpBytesReceived() { return Optional.ofNullable(httpBytesReceived); } + public Optional httpBytesSent() { return Optional.ofNullable(httpBytesSent); } + public Optional requests() { return Optional.ofNullable(requests); } + public Optional responses() { return Optional.ofNullable(responses); } + public Optional sslSessionId() { return Optional.ofNullable(sslSessionId); } + public Optional sslProtocol() { return Optional.ofNullable(sslProtocol); } + public Optional sslCipherSuite() { return Optional.ofNullable(sslCipherSuite); } + public Optional sslPeerSubject() { return Optional.ofNullable(sslPeerSubject); } + public Optional sslPeerNotBefore() { return Optional.ofNullable(sslPeerNotBefore); } + public Optional sslPeerNotAfter() { return Optional.ofNullable(sslPeerNotAfter); } + public Optional sslSniServerName() { return Optional.ofNullable(sslSniServerName); } + public Optional sslHandshakeFailure() { return Optional.ofNullable(sslHandshakeFailure); } + + public static class SslHandshakeFailure { + private final String type; + private final List exceptionChain; + + public SslHandshakeFailure(String type, List exceptionChain) { + this.type = type; + this.exceptionChain = List.copyOf(exceptionChain); + } + + public String type() { return type; } + public List exceptionChain() { return exceptionChain; } + + public static class ExceptionEntry { + private final String name; + private final String message; + + public ExceptionEntry(String name, String message) { + this.name = name; + this.message = message; + } + + public String name() { return name; } + public String message() { return message; } + } + } + + public static class Builder { + private final UUID id; + private final Instant timestamp; + private Double durationSeconds; + private String peerAddress; + private Integer peerPort; + private String localAddress; + private Integer localPort; + private String remoteAddress; + private Integer remotePort; + private Long httpBytesReceived; + private Long httpBytesSent; + private Long requests; + private Long responses; + private String sslSessionId; + private String sslProtocol; + private String sslCipherSuite; + private String sslPeerSubject; + private Instant sslPeerNotBefore; + private Instant sslPeerNotAfter; + private String sslSniServerName; + private SslHandshakeFailure sslHandshakeFailure; + + + Builder(UUID id, Instant timestamp) { + this.id = id; + this.timestamp = timestamp; + } + + public Builder withDuration(double durationSeconds) { + this.durationSeconds = durationSeconds; + return this; + } + + public Builder withPeerAddress(String peerAddress) { + this.peerAddress = peerAddress; + return this; + } + public Builder withPeerPort(int peerPort) { + this.peerPort = peerPort; + return this; + } + public Builder withLocalAddress(String localAddress) { + this.localAddress = localAddress; + return this; + } + public Builder withLocalPort(int localPort) { + this.localPort = localPort; + return this; + } + public Builder withRemoteAddress(String remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + public Builder withRemotePort(int remotePort) { + this.remotePort = remotePort; + return this; + } + public Builder withHttpBytesReceived(long bytesReceived) { + this.httpBytesReceived = bytesReceived; + return this; + } + public Builder withHttpBytesSent(long bytesSent) { + this.httpBytesSent = bytesSent; + return this; + } + public Builder withRequests(long requests) { + this.requests = requests; + return this; + } + public Builder withResponses(long responses) { + this.responses = responses; + return this; + } + public Builder withSslSessionId(String sslSessionId) { + this.sslSessionId = sslSessionId; + return this; + } + public Builder withSslProtocol(String sslProtocol) { + this.sslProtocol = sslProtocol; + return this; + } + public Builder withSslCipherSuite(String sslCipherSuite) { + this.sslCipherSuite = sslCipherSuite; + return this; + } + public Builder withSslPeerSubject(String sslPeerSubject) { + this.sslPeerSubject = sslPeerSubject; + return this; + } + public Builder withSslPeerNotBefore(Instant sslPeerNotBefore) { + this.sslPeerNotBefore = sslPeerNotBefore; + return this; + } + public Builder withSslPeerNotAfter(Instant sslPeerNotAfter) { + this.sslPeerNotAfter = sslPeerNotAfter; + return this; + } + public Builder withSslSniServerName(String sslSniServerName) { + this.sslSniServerName = sslSniServerName; + return this; + } + public Builder withSslHandshakeFailure(SslHandshakeFailure sslHandshakeFailure) { + this.sslHandshakeFailure = sslHandshakeFailure; + return this; + } + + public ConnectionLogEntry build(){ + return new ConnectionLogEntry(this); + } + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java new file mode 100644 index 00000000000..7a0e8aca95e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java @@ -0,0 +1,30 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.logging; + +/** + * @author mortent + */ +class ConnectionLogHandler { + private final LogFileHandler logFileHandler; + + public ConnectionLogHandler(String logDirectoryName, String clusterName, int queueSize, LogWriter logWriter) { + logFileHandler = new LogFileHandler<>( + LogFileHandler.Compression.ZSTD, + String.format("logs/vespa/%s/ConnectionLog.%s.%s", logDirectoryName, clusterName, "%Y%m%d%H%M%S"), + "0 60 ...", + String.format("ConnectionLog.%s", clusterName), + queueSize, + "connection-logger", + logWriter); + } + + public void log(ConnectionLogEntry entry) { + logFileHandler.publish(entry); + } + + public void shutdown() { + logFileHandler.close(); + logFileHandler.shutdown(); + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/Coverage.java b/container-core/src/main/java/com/yahoo/container/logging/Coverage.java new file mode 100644 index 00000000000..9d122b90641 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/Coverage.java @@ -0,0 +1,64 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +/** + * Carry information about how the query covered the document corpus. + */ +public class Coverage { + private final long docs; + private final long active; + private final long soonActive; + private final int degradedReason; + private final static int DEGRADED_BY_MATCH_PHASE = 1; + private final static int DEGRADED_BY_TIMEOUT = 2; + private final static int DEGRADED_BY_ADAPTIVE_TIMEOUT = 4; + public Coverage(long docs, long active, long soonActive, int degradedReason) { + this.docs = docs; + this.active = active; + this.soonActive = soonActive; + this.degradedReason = degradedReason; + } + + public long getDocs() { + return docs; + } + + public long getActive() { + return active; + } + + public static int toDegradation(boolean degradeByMatchPhase, boolean degradedByTimeout, boolean degradedByAdaptiveTimeout) { + int v = 0; + if (degradeByMatchPhase) { + v |= DEGRADED_BY_MATCH_PHASE; + } + if (degradedByTimeout) { + v |= DEGRADED_BY_TIMEOUT; + } + if (degradedByAdaptiveTimeout) { + v |= DEGRADED_BY_ADAPTIVE_TIMEOUT; + } + return v; + } + + public long getSoonActive() { return soonActive; } + + public boolean isDegraded() { return (degradedReason != 0) || isDegradedByNonIdealState(); } + public boolean isDegradedByMatchPhase() { return (degradedReason & DEGRADED_BY_MATCH_PHASE) != 0; } + public boolean isDegradedByTimeout() { return (degradedReason & DEGRADED_BY_TIMEOUT) != 0; } + public boolean isDegradedByAdapativeTimeout() { return (degradedReason & DEGRADED_BY_ADAPTIVE_TIMEOUT) != 0; } + public boolean isDegradedByNonIdealState() { return (degradedReason == 0) && (getResultPercentage() != 100);} + + /** + * An int between 0 (inclusive) and 100 (inclusive) representing how many + * percent coverage the result sets this Coverage instance contains information + * about had. + */ + public int getResultPercentage() { + if (docs < active) { + return (int) Math.round(docs * 100.0d / active); + } + return 100; + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/FileConnectionLog.java b/container-core/src/main/java/com/yahoo/container/logging/FileConnectionLog.java new file mode 100644 index 00000000000..7432c313286 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/FileConnectionLog.java @@ -0,0 +1,30 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.logging; + +import com.google.inject.Inject; +import com.yahoo.component.AbstractComponent; + +/** + * @author mortent + */ +public class FileConnectionLog extends AbstractComponent implements ConnectionLog { + + private final ConnectionLogHandler logHandler; + + @Inject + public FileConnectionLog(ConnectionLogConfig config) { + logHandler = new ConnectionLogHandler(config.logDirectoryName(), config.cluster(), config.queueSize(), new JsonConnectionLogWriter()); + } + + @Override + public void log(ConnectionLogEntry connectionLogEntry) { + logHandler.log(connectionLogEntry); + } + + @Override + public void deconstruct() { + logHandler.shutdown(); + } + +} \ No newline at end of file diff --git a/container-core/src/main/java/com/yahoo/container/logging/FormatUtil.java b/container-core/src/main/java/com/yahoo/container/logging/FormatUtil.java new file mode 100644 index 00000000000..ee780ad2a83 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/FormatUtil.java @@ -0,0 +1,46 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.fasterxml.jackson.core.JsonGenerator; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; + +/** + * @author bjorncs + */ +class FormatUtil { + + private FormatUtil() {} + + static void writeSecondsField(JsonGenerator generator, String fieldName, Instant instant) throws IOException { + writeSecondsField(generator, fieldName, instant.toEpochMilli()); + } + + static void writeSecondsField(JsonGenerator generator, String fieldName, Duration duration) throws IOException { + writeSecondsField(generator, fieldName, duration.toMillis()); + } + + static void writeSecondsField(JsonGenerator generator, String fieldName, double seconds) throws IOException { + writeSecondsField(generator, fieldName, (long)(seconds * 1000)); + } + + static void writeSecondsField(JsonGenerator generator, String fieldName, long milliseconds) throws IOException { + generator.writeFieldName(fieldName); + generator.writeRawValue(toSecondsString(milliseconds)); + } + + /** @return a string with number of seconds with 3 decimals */ + static String toSecondsString(long milliseconds) { + StringBuilder builder = new StringBuilder().append(milliseconds / 1000L).append('.'); + long decimals = milliseconds % 1000; + if (decimals < 100) { + builder.append('0'); + if (decimals < 10) { + builder.append('0'); + } + } + return builder.append(decimals).toString(); + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/HitCounts.java b/container-core/src/main/java/com/yahoo/container/logging/HitCounts.java new file mode 100644 index 00000000000..fed12281962 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/HitCounts.java @@ -0,0 +1,78 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +/** + * A wrapper for hit counts, modelled after a search system. + * Advanced database searches and similar could use these + * structures as well. + * + * @author Steinar Knutsen + */ +public class HitCounts { + + // see the javadoc for the accessors for short comments on each field + private final int retrievedHits; + private final int summaryCount; + private final long totalHitCount; + private final int requestedHits; + private final int requestedOffset; + private final Coverage coverage; + + HitCounts(int retrievedHits, int summaryCount, long totalHitCount, int requestedHits, int requestedOffset) { + this(retrievedHits, summaryCount, totalHitCount, requestedHits, requestedOffset, + new Coverage(1,1,1,0)); + } + + public HitCounts(int retrievedHits, int summaryCount, long totalHitCount, + int requestedHits, int requestedOffset, Coverage coverage) + { + + this.retrievedHits = retrievedHits; + this.summaryCount = summaryCount; + this.totalHitCount = totalHitCount; + this.requestedHits = requestedHits; + this.requestedOffset = requestedOffset; + this.coverage = coverage; + } + + /** + * The number of hits returned by the server. + * Compare to getRequestedHits(). + */ + public int getRetrievedHitCount() { + return retrievedHits; + } + + /** + * The number of hit summaries ("document contents") fetched. + */ + public int getSummaryCount() { + return summaryCount; + } + + /** + * The total number of matching hits + * for the request. + */ + public long getTotalHitCount() { + return totalHitCount; + } + + /** + * The number of hits requested by the user. + * Compare to getRetrievedHitCount(). + */ + public int getRequestedHits() { + return requestedHits; + } + + /** + * The user requested offset into the linear mapping of the result space. + */ + public int getRequestedOffset() { + return requestedOffset; + } + + public Coverage getCoverage() { return coverage; } + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/JSONAccessLog.java b/container-core/src/main/java/com/yahoo/container/logging/JSONAccessLog.java new file mode 100644 index 00000000000..ece9d0d2c4a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/JSONAccessLog.java @@ -0,0 +1,27 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.container.core.AccessLogConfig; + +/** + * Log a message in Vespa JSON access log format. + * + * @author frodelu + * @author Tony Vaagenes + */ +public final class JSONAccessLog extends AbstractComponent implements RequestLogHandler { + + private final AccessLogHandler logHandler; + + public JSONAccessLog(AccessLogConfig config) { + logHandler = new AccessLogHandler(config.fileHandler(), new JSONFormatter()); + } + + @Override + public void log(RequestLogEntry entry) { + logHandler.log(entry); + } + + @Override public void deconstruct() { logHandler.shutdown(); } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java b/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java new file mode 100644 index 00000000000..680ee5acbd9 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java @@ -0,0 +1,193 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.yolean.trace.TraceNode; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.Principal; +import java.util.Collection; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Formatting of an {@link AccessLogEntry} in the Vespa JSON access log format. + * + * @author frodelu + */ +public class JSONFormatter implements LogWriter { + private static final String COVERAGE = "coverage"; + private static final String COVERAGE_COVERAGE = "coverage"; + private static final String COVERAGE_DOCUMENTS = "documents"; + private static final String COVERAGE_DEGRADE = "degraded"; + private static final String COVERAGE_DEGRADE_MATCHPHASE = "match-phase"; + private static final String COVERAGE_DEGRADE_TIMEOUT = "timeout"; + private static final String COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT = "adaptive-timeout"; + private static final String COVERAGE_DEGRADED_NON_IDEAL_STATE = "non-ideal-state"; + + private final JsonFactory generatorFactory; + + private static Logger logger = Logger.getLogger(JSONFormatter.class.getName()); + + public JSONFormatter() { + generatorFactory = new JsonFactory(new ObjectMapper()); + } + + @Override + public void write(RequestLogEntry entry, OutputStream outputStream) throws IOException { + try (JsonGenerator generator = createJsonGenerator(outputStream)){ + generator.writeStartObject(); + String peerAddress = entry.peerAddress().get(); + generator.writeStringField("ip", peerAddress); + long time = entry.timestamp().get().toEpochMilli(); + FormatUtil.writeSecondsField(generator, "time", time); + FormatUtil.writeSecondsField(generator, "duration", entry.duration().get()); + generator.writeNumberField("responsesize", entry.contentSize().orElse(0)); + generator.writeNumberField("code", entry.statusCode().orElse(0)); + generator.writeStringField("method", entry.httpMethod().orElse("")); + generator.writeStringField("uri", getNormalizedURI(entry.rawPath().orElse(null), entry.rawQuery().orElse(null))); + generator.writeStringField("version", entry.httpVersion().orElse("")); + generator.writeStringField("agent", entry.userAgent().orElse("")); + generator.writeStringField("host", entry.hostString().orElse("")); + generator.writeStringField("scheme", entry.scheme().orElse(null)); + generator.writeNumberField("localport", entry.localPort().getAsInt()); + + String connectionId = entry.connectionId().orElse(null); + if (connectionId != null) { + generator.writeStringField("connection", connectionId); + } + + Principal userPrincipal = entry.userPrincipal().orElse(null); + if (userPrincipal != null) { + generator.writeStringField("user-principal", userPrincipal.getName()); + } + + Principal sslPrincipal = entry.sslPrincipal().orElse(null); + if (sslPrincipal != null) { + generator.writeStringField("ssl-principal", sslPrincipal.getName()); + } + + String remoteAddress = entry.remoteAddress().orElse(null); + int remotePort = entry.remotePort().orElse(0); + // Only add remote address/port fields if relevant + if (remoteAddressDiffers(peerAddress, remoteAddress)) { + generator.writeStringField("remoteaddr", remoteAddress); + if (remotePort > 0) { + generator.writeNumberField("remoteport", remotePort); + } + } + + // Only add peer address/port fields if relevant + if (peerAddress != null) { + generator.writeStringField("peeraddr", peerAddress); + + int peerPort = entry.peerPort().getAsInt(); + if (peerPort > 0 && peerPort != remotePort) { + generator.writeNumberField("peerport", peerPort); + } + } + + TraceNode trace = entry.traceNode().orElse(null); + if (trace != null) { + long timestamp = trace.timestamp(); + if (timestamp == 0L) { + timestamp = time; + } + trace.accept(new TraceRenderer(generator, timestamp)); + } + + // Only add search sub block of this is a search request + if (isSearchRequest(entry)) { + HitCounts hitCounts = entry.hitCounts().get(); + generator.writeObjectFieldStart("search"); + generator.writeNumberField("totalhits", getTotalHitCount(hitCounts)); + generator.writeNumberField("hits", getRetrievedHitCount(hitCounts)); + Coverage c = hitCounts.getCoverage(); + if (c != null) { + generator.writeObjectFieldStart(COVERAGE); + generator.writeNumberField(COVERAGE_COVERAGE, c.getResultPercentage()); + generator.writeNumberField(COVERAGE_DOCUMENTS, c.getDocs()); + if (c.isDegraded()) { + generator.writeObjectFieldStart(COVERAGE_DEGRADE); + if (c.isDegradedByMatchPhase()) + generator.writeBooleanField(COVERAGE_DEGRADE_MATCHPHASE, c.isDegradedByMatchPhase()); + if (c.isDegradedByTimeout()) + generator.writeBooleanField(COVERAGE_DEGRADE_TIMEOUT, c.isDegradedByTimeout()); + if (c.isDegradedByAdapativeTimeout()) + generator.writeBooleanField(COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT, c.isDegradedByAdapativeTimeout()); + if (c.isDegradedByNonIdealState()) + generator.writeBooleanField(COVERAGE_DEGRADED_NON_IDEAL_STATE, c.isDegradedByNonIdealState()); + generator.writeEndObject(); + } + generator.writeEndObject(); + } + generator.writeEndObject(); + } + + // Add key/value access log entries. Keys with single values are written as single + // string value fields while keys with multiple values are written as string arrays + Collection keys = entry.extraAttributeKeys(); + if (!keys.isEmpty()) { + generator.writeObjectFieldStart("attributes"); + for (String key : keys) { + Collection values = entry.extraAttributeValues(key); + if (values.size() == 1) { + generator.writeStringField(key, values.iterator().next()); + } else { + generator.writeFieldName(key); + generator.writeStartArray(); + for (String s : values) { + generator.writeString(s); + } + generator.writeEndArray(); + } + } + generator.writeEndObject(); + } + + generator.writeEndObject(); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to generate JSON access log entry: " + e.getMessage(), e); + } + } + + private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException { + return generatorFactory.createGenerator(outputStream, JsonEncoding.UTF8) + .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false) + .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false); + } + + private boolean remoteAddressDiffers(String ipV4Address, String remoteAddress) { + return remoteAddress != null && !Objects.equals(ipV4Address, remoteAddress); + } + + private boolean isSearchRequest(RequestLogEntry entry) { + return entry != null && entry.hitCounts().isPresent(); + } + + private long getTotalHitCount(HitCounts counts) { + if (counts == null) { + return 0; + } + + return counts.getTotalHitCount(); + } + + private int getRetrievedHitCount(HitCounts counts) { + if (counts == null) { + return 0; + } + + return counts.getRetrievedHitCount(); + } + + private static String getNormalizedURI(String rawPath, String rawQuery) { + if (rawPath == null) return null; + return rawQuery != null ? rawPath + "?" + rawQuery : rawPath; + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java b/container-core/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java new file mode 100644 index 00000000000..158d2ec4ea6 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java @@ -0,0 +1,122 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; + +import java.io.IOException; +import java.io.OutputStream; +import java.time.Instant; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; + +/** + * @author bjorncs + */ +class JsonConnectionLogWriter implements LogWriter { + + private final JsonFactory jsonFactory = new JsonFactory(new ObjectMapper()); + + @Override + public void write(ConnectionLogEntry record, OutputStream outputStream) throws IOException { + try (JsonGenerator generator = createJsonGenerator(outputStream)) { + generator.writeStartObject(); + generator.writeStringField("id", record.id()); + generator.writeStringField("timestamp", record.timestamp().toString()); + + writeOptionalSeconds(generator, "duration", unwrap(record.durationSeconds())); + writeOptionalString(generator, "peerAddress", unwrap(record.peerAddress())); + writeOptionalInteger(generator, "peerPort", unwrap(record.peerPort())); + writeOptionalString(generator, "localAddress", unwrap(record.localAddress())); + writeOptionalInteger(generator, "localPort", unwrap(record.localPort())); + writeOptionalString(generator, "remoteAddress", unwrap(record.remoteAddress())); + writeOptionalInteger(generator, "remotePort", unwrap(record.remotePort())); + writeOptionalLong(generator, "httpBytesReceived", unwrap(record.httpBytesReceived())); + writeOptionalLong(generator, "httpBytesSent", unwrap(record.httpBytesSent())); + writeOptionalLong(generator, "requests", unwrap(record.requests())); + writeOptionalLong(generator, "responses", unwrap(record.responses())); + + String sslProtocol = unwrap(record.sslProtocol()); + String sslSessionId = unwrap(record.sslSessionId()); + String sslCipherSuite = unwrap(record.sslCipherSuite()); + String sslPeerSubject = unwrap(record.sslPeerSubject()); + Instant sslPeerNotBefore = unwrap(record.sslPeerNotBefore()); + Instant sslPeerNotAfter = unwrap(record.sslPeerNotAfter()); + String sslSniServerName = unwrap(record.sslSniServerName()); + ConnectionLogEntry.SslHandshakeFailure sslHandshakeFailure = unwrap(record.sslHandshakeFailure()); + + if (isAnyValuePresent( + sslProtocol, sslSessionId, sslCipherSuite, sslPeerSubject, sslPeerNotBefore, sslPeerNotAfter, + sslSniServerName, sslHandshakeFailure)) { + generator.writeObjectFieldStart("ssl"); + + writeOptionalString(generator, "protocol", sslProtocol); + writeOptionalString(generator, "sessionId", sslSessionId); + writeOptionalString(generator, "cipherSuite", sslCipherSuite); + writeOptionalString(generator, "peerSubject", sslPeerSubject); + writeOptionalTimestamp(generator, "peerNotBefore", sslPeerNotBefore); + writeOptionalTimestamp(generator, "peerNotAfter", sslPeerNotAfter); + writeOptionalString(generator, "sniServerName", sslSniServerName); + + if (sslHandshakeFailure != null) { + generator.writeObjectFieldStart("handshake-failure"); + generator.writeArrayFieldStart("exception"); + for (ExceptionEntry entry : sslHandshakeFailure.exceptionChain()) { + generator.writeStartObject(); + generator.writeStringField("cause", entry.name()); + generator.writeStringField("message", entry.message()); + generator.writeEndObject(); + } + generator.writeEndArray(); + generator.writeStringField("type", sslHandshakeFailure.type()); + generator.writeEndObject(); + } + + generator.writeEndObject(); + } + } + } + + private void writeOptionalString(JsonGenerator generator, String name, String value) throws IOException { + if (value != null) { + generator.writeStringField(name, value); + } + } + + private void writeOptionalInteger(JsonGenerator generator, String name, Integer value) throws IOException { + if (value != null) { + generator.writeNumberField(name, value); + } + } + + private void writeOptionalLong(JsonGenerator generator, String name, Long value) throws IOException { + if (value != null) { + generator.writeNumberField(name, value); + } + } + + private void writeOptionalTimestamp(JsonGenerator generator, String name, Instant value) throws IOException { + if (value != null) { + generator.writeStringField(name, value.toString()); + } + } + + private void writeOptionalSeconds(JsonGenerator generator, String name, Double value) throws IOException { + if (value != null) { + FormatUtil.writeSecondsField(generator, name, value); + } + } + + private static boolean isAnyValuePresent(Object... values) { return Arrays.stream(values).anyMatch(Objects::nonNull); } + private static T unwrap(Optional maybeValue) { return maybeValue.orElse(null); } + + private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException { + return jsonFactory.createGenerator(outputStream, JsonEncoding.UTF8) + .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false) + .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false); + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/LogFileHandler.java b/container-core/src/main/java/com/yahoo/container/logging/LogFileHandler.java new file mode 100644 index 00000000000..0f2a9e42eb8 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/LogFileHandler.java @@ -0,0 +1,563 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.compress.ZstdOuputStream; +import com.yahoo.io.NativeIO; +import com.yahoo.log.LogFileDb; +import com.yahoo.protect.Process; +import com.yahoo.yolean.Exceptions; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; + +/** + * Implements log file naming/rotating logic for container logs. + * + * @author Bob Travis + * @author bjorncs + */ +class LogFileHandler { + + enum Compression {NONE, GZIP, ZSTD} + + private final static Logger logger = Logger.getLogger(LogFileHandler.class.getName()); + private final BlockingQueue> logQueue; + final LogThread logThread; + + @FunctionalInterface private interface Pollable { Operation poll() throws InterruptedException; } + + LogFileHandler(Compression compression, String filePattern, String rotationTimes, String symlinkName, int queueSize, + String threadName, LogWriter logWriter) { + this(compression, filePattern, calcTimesMinutes(rotationTimes), symlinkName, queueSize, threadName, logWriter); + } + + LogFileHandler( + Compression compression, + String filePattern, + long[] rotationTimes, + String symlinkName, + int queueSize, + String threadName, + LogWriter logWriter) { + this.logQueue = new LinkedBlockingQueue<>(queueSize); + this.logThread = new LogThread<>(logWriter, filePattern, compression, rotationTimes, symlinkName, threadName, this::poll); + this.logThread.start(); + } + + private Operation poll() throws InterruptedException { + return logQueue.poll(100, TimeUnit.MILLISECONDS); + } + + /** + * Sends logrecord to file, first rotating file if needed. + * + * @param r logrecord to publish + */ + public void publish(LOGTYPE r) { + addOperation(new Operation<>(r)); + } + + void publishAndWait(LOGTYPE r) { + addOperationAndWait(new Operation<>(r)); + } + + public void flush() { + addOperationAndWait(new Operation<>(Operation.Type.flush)); + } + + /** + * Force file rotation now, independent of schedule. + */ + void rotateNow() { + addOperationAndWait(new Operation<>(Operation.Type.rotate)); + } + + public void close() { + addOperationAndWait(new Operation<>(Operation.Type.close)); + } + + private void addOperation(Operation op) { + try { + logQueue.put(op); + } catch (InterruptedException e) { + } + } + + private void addOperationAndWait(Operation op) { + try { + logQueue.put(op); + op.countDownLatch.await(); + } catch (InterruptedException e) { + } + } + + /** + * Flushes all queued messages, interrupts the log thread in this and + * waits for it to end before returning + */ + void shutdown() { + logThread.interrupt(); + try { + logThread.executor.shutdownNow(); + logThread.executor.awaitTermination(600, TimeUnit.SECONDS); + logThread.join(); + } catch (InterruptedException e) { + } + } + + /** + * Calculate rotation times array, given times in minutes, as "0 60 ..." + */ + private static long[] calcTimesMinutes(String times) { + ArrayList list = new ArrayList<>(50); + int i = 0; + boolean etc = false; + + while (i < times.length()) { + if (times.charAt(i) == ' ') { + i++; + continue; + } // skip spaces + int j = i; // start of string + i = times.indexOf(' ', i); + if (i == -1) i = times.length(); + if (times.charAt(j) == '.' && times.substring(j, i).equals("...")) { // ... + etc = true; + break; + } + list.add(Long.valueOf(times.substring(j, i))); + } + + int size = list.size(); + long[] longtimes = new long[size]; + for (i = 0; i < size; i++) { + longtimes[i] = list.get(i) // pick up value in minutes past midnight + * 60000; // and multiply to get millis + } + + if (etc) { // fill out rest of day, same as final interval + long endOfDay = 24 * 60 * 60 * 1000; + long lasttime = longtimes[size - 1]; + long interval = lasttime - longtimes[size - 2]; + long moreneeded = (endOfDay - lasttime) / interval; + if (moreneeded > 0) { + int newsize = size + (int) moreneeded; + long[] temp = new long[newsize]; + for (i = 0; i < size; i++) { + temp[i] = longtimes[i]; + } + while (size < newsize) { + lasttime += interval; + temp[size++] = lasttime; + } + longtimes = temp; + } + } + + return longtimes; + } + + /** + * Only for unit testing. Do not use. + */ + String getFileName() { + return logThread.fileName; + } + + /** + * Handle logging and file operations + */ + static class LogThread extends Thread { + private final Pollable operationProvider; + long lastFlush = 0; + private PageCacheFriendlyFileOutputStream fileOutput = null; + private long nextRotationTime = 0; + private final String filePattern; // default to current directory, ms time stamp + private volatile String fileName; + private final LogWriter logWriter; + private final Compression compression; + private final long[] rotationTimes; + private final String symlinkName; + private final ExecutorService executor = createCompressionTaskExecutor(); + private final NativeIO nativeIO = new NativeIO(); + + + LogThread(LogWriter logWriter, + String filePattern, + Compression compression, + long[] rotationTimes, + String symlinkName, + String threadName, + Pollable operationProvider) { + super(threadName); + setDaemon(true); + this.logWriter = logWriter; + this.filePattern = filePattern; + this.compression = compression; + this.rotationTimes = rotationTimes; + this.symlinkName = (symlinkName != null && !symlinkName.isBlank()) ? symlinkName : null; + this.operationProvider = operationProvider; + } + + private static ExecutorService createCompressionTaskExecutor() { + return Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "logfilehandler.compression"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + } + + @Override + public void run() { + try { + handleLogOperations(); + } catch (InterruptedException e) { + } catch (Exception e) { + Process.logAndDie("Failed storing log records", e); + } + + internalFlush(); + } + + private void handleLogOperations() throws InterruptedException { + while (!isInterrupted()) { + Operation r = operationProvider.poll(); + if (r != null) { + if (r.type == Operation.Type.flush) { + internalFlush(); + } else if (r.type == Operation.Type.close) { + internalClose(); + } else if (r.type == Operation.Type.rotate) { + internalRotateNow(); + lastFlush = System.nanoTime(); + } else if (r.type == Operation.Type.log) { + internalPublish(r.log.get()); + flushIfOld(3, TimeUnit.SECONDS); + } + r.countDownLatch.countDown(); + } else { + flushIfOld(100, TimeUnit.MILLISECONDS); + } + } + } + + private void flushIfOld(long age, TimeUnit unit) { + long now = System.nanoTime(); + if (TimeUnit.NANOSECONDS.toMillis(now - lastFlush) > unit.toMillis(age)) { + internalFlush(); + lastFlush = now; + } + } + + private void internalFlush() { + try { + if (fileOutput != null) { + fileOutput.flush(); + } + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to flush file output: " + Exceptions.toMessageString(e), e); + } + } + + private void internalClose() { + try { + if (fileOutput != null) { + fileOutput.flush(); + fileOutput.close(); + fileOutput = null; + } + } catch (Exception e) { + logger.log(Level.WARNING, "Got error while closing log file: " + e.getMessage(), e); + } + } + + private void internalPublish(LOGTYPE r) { + // first check to see if new file needed. + // if so, use this.internalRotateNow() to do it + + long now = System.currentTimeMillis(); + if (nextRotationTime <= 0) { + nextRotationTime = getNextRotationTime(now); // lazy initialization + } + if (now > nextRotationTime || fileOutput == null) { + internalRotateNow(); + } + try { + logWriter.write(r, fileOutput); + fileOutput.write('\n'); + } catch (IOException e) { + logger.warning("Failed writing log record: " + Exceptions.toMessageString(e)); + } + } + + /** + * Find next rotation after specified time. + * + * @param now the specified time; if zero, current time is used. + * @return the next rotation time + */ + long getNextRotationTime(long now) { + if (now <= 0) { + now = System.currentTimeMillis(); + } + long nowTod = timeOfDayMillis(now); + long next = 0; + for (long rotationTime : rotationTimes) { + if (nowTod < rotationTime) { + next = rotationTime - nowTod + now; + break; + } + } + if (next == 0) { // didn't find one -- use 1st time 'tomorrow' + next = rotationTimes[0] + lengthOfDayMillis - nowTod + now; + } + + return next; + } + + private void checkAndCreateDir(String pathname) { + int lastSlash = pathname.lastIndexOf("/"); + if (lastSlash > -1) { + String pathExcludingFilename = pathname.substring(0, lastSlash); + File filepath = new File(pathExcludingFilename); + if (!filepath.exists()) { + filepath.mkdirs(); + } + } + } + + + // Throw InterruptedException upwards rather than relying on isInterrupted to stop the thread as + // isInterrupted() returns false after interruption in p.waitFor + private void internalRotateNow() { + // figure out new file name, then + + String oldFileName = fileName; + long now = System.currentTimeMillis(); + fileName = LogFormatter.insertDate(filePattern, now); + internalClose(); + try { + checkAndCreateDir(fileName); + fileOutput = new PageCacheFriendlyFileOutputStream(nativeIO, Paths.get(fileName), 4 * 1024 * 1024); + LogFileDb.nowLoggingTo(fileName); + } catch (IOException e) { + throw new RuntimeException("Couldn't open log file '" + fileName + "'", e); + } + + if(oldFileName == null) oldFileName = getOldFileNameFromSymlink(); // To compress previous file, if so configured + createSymlinkToCurrentFile(); + + nextRotationTime = 0; //figure it out later (lazy evaluation) + if ((oldFileName != null)) { + Path oldFile = Paths.get(oldFileName); + if (Files.exists(oldFile)) { + executor.execute(() -> runCompression(nativeIO, oldFile, compression)); + } + } + } + + + private static void runCompression(NativeIO nativeIO, Path oldFile, Compression compression) { + switch (compression) { + case ZSTD: + runCompressionZstd(nativeIO, oldFile); + break; + case GZIP: + runCompressionGzip(nativeIO, oldFile); + break; + case NONE: + runCompressionNone(nativeIO, oldFile); + break; + default: + throw new IllegalArgumentException("Unknown compression " + compression); + } + } + + private static void runCompressionNone(NativeIO nativeIO, Path oldFile) { + nativeIO.dropFileFromCache(oldFile.toFile()); + } + + private static void runCompressionZstd(NativeIO nativeIO, Path oldFile) { + try { + Path compressedFile = Paths.get(oldFile.toString() + ".zst"); + int bufferSize = 2*1024*1024; + try (FileOutputStream fileOut = AtomicFileOutputStream.create(compressedFile); + ZstdOuputStream out = new ZstdOuputStream(fileOut, bufferSize); + FileInputStream in = new FileInputStream(oldFile.toFile())) { + pageFriendlyTransfer(nativeIO, out, fileOut.getFD(), in, bufferSize); + out.flush(); + } + Files.delete(oldFile); + nativeIO.dropFileFromCache(compressedFile.toFile()); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to compress log file with zstd: " + oldFile, e); + } finally { + nativeIO.dropFileFromCache(oldFile.toFile()); + } + } + + private static void runCompressionGzip(NativeIO nativeIO, Path oldFile) { + try { + Path gzippedFile = Paths.get(oldFile.toString() + ".gz"); + try (FileOutputStream fileOut = AtomicFileOutputStream.create(gzippedFile); + GZIPOutputStream compressor = new GZIPOutputStream(fileOut, 0x100000); + FileInputStream inputStream = new FileInputStream(oldFile.toFile())) { + pageFriendlyTransfer(nativeIO, compressor, fileOut.getFD(), inputStream, 0x400000); + compressor.finish(); + compressor.flush(); + } + Files.delete(oldFile); + nativeIO.dropFileFromCache(gzippedFile.toFile()); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to compress log file with gzip: " + oldFile, e); + } finally { + nativeIO.dropFileFromCache(oldFile.toFile()); + } + } + + private static void pageFriendlyTransfer(NativeIO nativeIO, OutputStream out, FileDescriptor outDescriptor, FileInputStream in, int bufferSize) throws IOException { + int read; + long totalBytesRead = 0; + byte[] buffer = new byte[bufferSize]; + while ((read = in.read(buffer)) >= 0) { + out.write(buffer, 0, read); + if (read > 0) { + nativeIO.dropPartialFileFromCache(in.getFD(), totalBytesRead, read, false); + nativeIO.dropPartialFileFromCache(outDescriptor, totalBytesRead, read, false); + } + totalBytesRead += read; + } + } + + /** + * Name files by date - create a symlink with a constant name to the newest file + */ + private void createSymlinkToCurrentFile() { + if (symlinkName == null) return; + Path target = Paths.get(fileName); + Path link = target.resolveSibling(symlinkName); + try { + Files.deleteIfExists(link); + Files.createSymbolicLink(link, target.getFileName()); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to create symbolic link to current log file: " + e.getMessage(), e); + } + } + + private String getOldFileNameFromSymlink() { + if(symlinkName == null) return null; + try { + return Paths.get(fileName).resolveSibling(symlinkName).toRealPath().toString(); + } catch (IOException e) { + return null; + } + } + + private static final long lengthOfDayMillis = 24 * 60 * 60 * 1000; + private static long timeOfDayMillis(long time) { + return time % lengthOfDayMillis; + } + + } + + private static class Operation { + enum Type {log, flush, close, rotate} + + final Type type; + + final Optional log; + final CountDownLatch countDownLatch = new CountDownLatch(1); + + Operation(Type type) { + this(type, Optional.empty()); + } + + Operation(LOGTYPE log) { + this(Type.log, Optional.of(log)); + } + + private Operation(Type type, Optional log) { + this.type = type; + this.log = log; + } + } + + /** File output stream that signals to kernel to drop previous pages after write */ + private static class PageCacheFriendlyFileOutputStream extends OutputStream { + + private final NativeIO nativeIO; + private final FileOutputStream fileOut; + private final BufferedOutputStream bufferedOut; + private final int bufferSize; + private long lastDropPosition = 0; + + PageCacheFriendlyFileOutputStream(NativeIO nativeIO, Path file, int bufferSize) throws FileNotFoundException { + this.nativeIO = nativeIO; + this.fileOut = new FileOutputStream(file.toFile(), true); + this.bufferedOut = new BufferedOutputStream(fileOut, bufferSize); + this.bufferSize = bufferSize; + } + + @Override public void write(byte[] b) throws IOException { bufferedOut.write(b); } + @Override public void write(byte[] b, int off, int len) throws IOException { bufferedOut.write(b, off, len); } + @Override public void write(int b) throws IOException { bufferedOut.write(b); } + @Override public void close() throws IOException { bufferedOut.close(); } + + @Override + public void flush() throws IOException { + bufferedOut.flush(); + long newPos = fileOut.getChannel().position(); + if (newPos >= lastDropPosition + bufferSize) { + nativeIO.dropPartialFileFromCache(fileOut.getFD(), lastDropPosition, newPos, true); + lastDropPosition = newPos; + } + } + } + + private static class AtomicFileOutputStream extends FileOutputStream { + private final Path path; + private final Path tmpPath; + private volatile boolean closed = false; + + private AtomicFileOutputStream(Path path, Path tmpPath) throws FileNotFoundException { + super(tmpPath.toFile()); + this.path = path; + this.tmpPath = tmpPath; + } + + @Override + public synchronized void close() throws IOException { + super.close(); + if (!closed) { + Files.move(tmpPath, path, StandardCopyOption.ATOMIC_MOVE); + closed = true; + } + } + + private static AtomicFileOutputStream create(Path path) throws FileNotFoundException { + return new AtomicFileOutputStream(path, path.resolveSibling("." + path.getFileName() + ".tmp")); + } + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/LogFormatter.java b/container-core/src/main/java/com/yahoo/container/logging/LogFormatter.java new file mode 100644 index 00000000000..cc1dcb579aa --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/LogFormatter.java @@ -0,0 +1,191 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + + +/** + * Produces compact output format for prelude logs + * + * @author Bob Travis + */ +public class LogFormatter extends Formatter { + + /** date format objects */ + static SimpleDateFormat ddMMMyyyy; + static DateFormat dfMMM; + static SimpleDateFormat yyyyMMdd; + + static { + ddMMMyyyy = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]", Locale.US); + ddMMMyyyy.setTimeZone(TimeZone.getTimeZone("UTC")); + + dfMMM = new SimpleDateFormat("MMM", Locale.US); + dfMMM.setTimeZone(TimeZone.getTimeZone("UTC")); + + yyyyMMdd = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]", Locale.US); + yyyyMMdd.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + /** Whether to strip down the message to only the message or not */ + private boolean messageOnly = false; + + /** Controls which of the available timestamp formats is used in all log records + */ + private static final int timestampFormat = 2; // 0=millis, 1=mm/dd/yyyy, 2=yyyy-mm-dd + + /** + * Standard constructor + */ + + public LogFormatter() {} + + /** + * Make it possible to log stripped messages + */ + public void messageOnly (boolean messageOnly) { + this.messageOnly = messageOnly; + } + + public String format(LogRecord record) { + + // if we don't want any other stuff we just return the message + if (messageOnly) { + return formatMessage(record); + } + + String rawMsg = record.getMessage(); + boolean isLogMsg = + rawMsg.charAt(0) == 'L' + && rawMsg.charAt(1) == 'O' + && rawMsg.charAt(2) == 'G' + && rawMsg.charAt(3) == ':'; + String nameInsert = + (!isLogMsg) + ? record.getLevel().getName() + ": " + : ""; + return (timeStamp(record) + + nameInsert + + formatMessage(record) + + "\n" + ); + } + + /** + * Public support methods + */ + + /** + * Static insertDate method will insert date fragments into a string + * based on '%x' pattern elements. Equivalents in SimpleDateFormatter patterns, + * with examples: + *
    + *
  • %Y YYYY 2003 + *
  • %m MM 08 + *
  • %x MMM Aug + *
  • %d dd 25 + *
  • %H HH 14 + *
  • %M mm 30 + *
  • %S ss 35 + *
  • %s SSS 123 + *
  • %Z Z -0400 + *
+ *Others: + *
    + *
  • %T Long.toString(time) + *
  • %% % + *
+ */ + public static String insertDate(String pattern, long time) { + DateFormat df = new SimpleDateFormat("yyyy.MM.dd:HH:mm:ss.SSS Z", Locale.US); + df.setTimeZone(TimeZone.getTimeZone("UTC")); + Date date = new Date(time); + String datetime = df.format(date); + StringBuilder result = new StringBuilder(); + int i=0; + while (i < pattern.length()) { + int j = pattern.indexOf('%',i); + if (j == -1 || j >= pattern.length()-1) { // done + result.append(pattern.substring(i)); // copy rest of pattern and quit + break; + } + result.append(pattern.substring(i, j)); + switch (pattern.charAt(j+1)) { + case 'Y': + result.append(datetime.substring(0,4)); // year + break; + case 'm': + result.append(datetime.substring(5,7)); // month + break; + case 'd': + result.append(datetime.substring(8,10)); // day of month + break; + case 'H': + result.append(datetime.substring(11,13)); // hour + break; + case 'M': + result.append(datetime.substring(14,16)); // minute + break; + case 'S': + result.append(datetime.substring(17,19)); // second + break; + case 's': + result.append(datetime.substring(20,23)); // thousanths + break; + case 'Z': + result.append(datetime.substring(24)); // time zone string + break; + case 'T': + result.append(Long.toString(time)); //time in Millis + break; + case 'x': + result.append(capitalize(dfMMM.format(date))); + break; + case '%': + result.append("%%"); + break; + default: + result.append("%"); // copy pattern escape and move on + j--; // only want to bump by one position.... + break; + } + i = j+2; + } + + return result.toString(); + } + + /** + * Private methods: timeStamp(LogRecord) + */ + private String timeStamp(LogRecord record) { + Date date = new Date(record.getMillis()); + String stamp; + switch (timestampFormat) { + case 0: + stamp = Long.toString(record.getMillis()); + break; + case 1: + stamp = ddMMMyyyy.format(date); + break; + case 2: + default: + stamp = yyyyMMdd.format(date); + break; + } + return stamp; + } + + /** Return the given string with the first letter in upper case */ + private static String capitalize(String string) { + if (Character.isUpperCase(string.charAt(0))) return string; + return Character.toUpperCase(string.charAt(0)) + string.substring(1); + } + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/LogWriter.java b/container-core/src/main/java/com/yahoo/container/logging/LogWriter.java new file mode 100644 index 00000000000..15a983cfb43 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/LogWriter.java @@ -0,0 +1,10 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.logging; + +import java.io.IOException; +import java.io.OutputStream; + +interface LogWriter { + void write(LOGTYPE record, OutputStream outputStream) throws IOException; +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/RequestLog.java b/container-core/src/main/java/com/yahoo/container/logging/RequestLog.java new file mode 100644 index 00000000000..2090ba1b9f1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/RequestLog.java @@ -0,0 +1,13 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +/** + * Access logging for requests + * + * @author bjorncs + */ +public interface RequestLog { + + void log(RequestLogEntry entry); + +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java b/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java new file mode 100644 index 00000000000..819907fc9f1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/RequestLogEntry.java @@ -0,0 +1,186 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.yolean.trace.TraceNode; + +import java.security.Principal; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.OptionalLong; + +import static java.util.Objects.requireNonNull; + +/** + * A immutable request log entry + * + * @author bjorncs + */ +public class RequestLogEntry { + + private final String connectionId; + private final Instant timestamp; + private final Duration duration; + private final int localPort; + private final String peerAddress; + private final int peerPort; + private final String remoteAddress; + private final int remotePort; + private final String userAgent; + private final String referer; + private final String httpMethod; + private final String httpVersion; + private final String hostString; + private final int statusCode; + private final long contentSize; + private final String scheme; + private final String rawPath; + private final String rawQuery; + private final Principal userPrincipal; + private final Principal sslPrincipal; + private final HitCounts hitCounts; + private final TraceNode traceNode; + private final Map> extraAttributes; + + private RequestLogEntry(Builder builder) { + this.connectionId = builder.connectionId; + this.timestamp = builder.timestamp; + this.duration = builder.duration; + this.localPort = builder.localPort; + this.peerAddress = builder.peerAddress; + this.peerPort = builder.peerPort; + this.remoteAddress = builder.remoteAddress; + this.remotePort = builder.remotePort; + this.userAgent = builder.userAgent; + this.referer = builder.referer; + this.httpMethod = builder.httpMethod; + this.httpVersion = builder.httpVersion; + this.hostString = builder.hostString; + this.statusCode = builder.statusCode; + this.contentSize = builder.contentSize; + this.scheme = builder.scheme; + this.rawPath = builder.rawPath; + this.rawQuery = builder.rawQuery; + this.userPrincipal = builder.userPrincipal; + this.sslPrincipal = builder.sslPrincipal; + this.hitCounts = builder.hitCounts; + this.traceNode = builder.traceNode; + this.extraAttributes = copyExtraAttributes(builder.extraAttributes); + } + + public Optional connectionId() { return Optional.ofNullable(connectionId); } + public Optional timestamp() { return Optional.ofNullable(timestamp); } + public Optional duration() { return Optional.ofNullable(duration); } + public OptionalInt localPort() { return optionalInt(localPort); } + public Optional peerAddress() { return Optional.ofNullable(peerAddress); } + public OptionalInt peerPort() { return optionalInt(peerPort); } + public Optional remoteAddress() { return Optional.ofNullable(remoteAddress); } + public OptionalInt remotePort() { return optionalInt(remotePort); } + public Optional userAgent() { return Optional.ofNullable(userAgent); } + public Optional referer() { return Optional.ofNullable(referer); } + public Optional httpMethod() { return Optional.ofNullable(httpMethod); } + public Optional httpVersion() { return Optional.ofNullable(httpVersion); } + public Optional hostString() { return Optional.ofNullable(hostString); } + public OptionalInt statusCode() { return optionalInt(statusCode); } + public OptionalLong contentSize() { return optionalLong(contentSize); } + public Optional scheme() { return Optional.ofNullable(scheme); } + public Optional rawPath() { return Optional.ofNullable(rawPath); } + public Optional rawQuery() { return Optional.ofNullable(rawQuery); } + public Optional userPrincipal() { return Optional.ofNullable(userPrincipal); } + public Optional sslPrincipal() { return Optional.ofNullable(sslPrincipal); } + public Optional hitCounts() { return Optional.ofNullable(hitCounts); } + public Optional traceNode() { return Optional.ofNullable(traceNode); } + public Collection extraAttributeKeys() { return Collections.unmodifiableCollection(extraAttributes.keySet()); } + public Collection extraAttributeValues(String key) { return Collections.unmodifiableCollection(extraAttributes.get(key)); } + + private static OptionalInt optionalInt(int value) { + if (value == -1) return OptionalInt.empty(); + return OptionalInt.of(value); + } + + private static OptionalLong optionalLong(long value) { + if (value == -1) return OptionalLong.empty(); + return OptionalLong.of(value); + } + + private static Map> copyExtraAttributes(Map> extraAttributes) { + Map> copy = new HashMap<>(); + extraAttributes.forEach((key, value) -> copy.put(key, new ArrayList<>(value))); + return copy; + } + + public static class Builder { + + private String connectionId; + private Instant timestamp; + private Duration duration; + private int localPort = -1; + private String peerAddress; + private int peerPort = -1; + private String remoteAddress; + private int remotePort = -1; + private String userAgent; + private String referer; + private String httpMethod; + private String httpVersion; + private String hostString; + private int statusCode = -1; + private long contentSize = -1; + private String scheme; + private String rawPath; + private String rawQuery; + private Principal userPrincipal; + private HitCounts hitCounts; + private TraceNode traceNode; + private Principal sslPrincipal; + private final Map> extraAttributes = new HashMap<>(); + + public Builder connectionId(String connectionId) { this.connectionId = requireNonNull(connectionId); return this; } + public Builder timestamp(Instant timestamp) { this.timestamp = requireNonNull(timestamp); return this; } + public Builder duration(Duration duration) { this.duration = requireNonNull(duration); return this; } + public Builder localPort(int localPort) { this.localPort = requireNonNegative(localPort); return this; } + public Builder peerAddress(String peerAddress) { this.peerAddress = requireNonNull(peerAddress); return this; } + public Builder peerPort(int peerPort) { this.peerPort = requireNonNegative(peerPort); return this; } + public Builder remoteAddress(String remoteAddress) { this.remoteAddress = requireNonNull(remoteAddress); return this; } + public Builder remotePort(int remotePort) { this.remotePort = requireNonNegative(remotePort); return this; } + public Builder userAgent(String userAgent) { this.userAgent = requireNonNull(userAgent); return this; } + public Builder referer(String referer) { this.referer = requireNonNull(referer); return this; } + public Builder httpMethod(String httpMethod) { this.httpMethod = requireNonNull(httpMethod); return this; } + public Builder httpVersion(String httpVersion) { this.httpVersion = requireNonNull(httpVersion); return this; } + public Builder hostString(String hostString) { this.hostString = requireNonNull(hostString); return this; } + public Builder statusCode(int statusCode) { this.statusCode = requireNonNegative(statusCode); return this; } + public Builder contentSize(long contentSize) { this.contentSize = requireNonNegative(contentSize); return this; } + public Builder scheme(String scheme) { this.scheme = requireNonNull(scheme); return this; } + public Builder rawPath(String rawPath) { this.rawPath = requireNonNull(rawPath); return this; } + public Builder rawQuery(String rawQuery) { this.rawQuery = requireNonNull(rawQuery); return this; } + public Builder userPrincipal(Principal userPrincipal) { this.userPrincipal = requireNonNull(userPrincipal); return this; } + public Builder sslPrincipal(Principal sslPrincipal) { this.sslPrincipal = requireNonNull(sslPrincipal); return this; } + public Builder hitCounts(HitCounts hitCounts) { this.hitCounts = requireNonNull(hitCounts); return this; } + public Builder traceNode(TraceNode traceNode) { this.traceNode = requireNonNull(traceNode); return this; } + public Builder addExtraAttribute(String key, String value) { + this.extraAttributes.computeIfAbsent(requireNonNull(key), __ -> new ArrayList<>()).add(requireNonNull(value)); + return this; + } + public Builder addExtraAttributes(String key, Collection values) { + this.extraAttributes.computeIfAbsent(requireNonNull(key), __ -> new ArrayList<>()).addAll(requireNonNull(values)); + return this; + } + public RequestLogEntry build() { return new RequestLogEntry(this); } + + private static int requireNonNegative(int value) { + if (value < 0) throw new IllegalArgumentException("Value must be non-negative: " + value); + return value; + } + + private static long requireNonNegative(long value) { + if (value < 0) throw new IllegalArgumentException("Value must be non-negative: " + value); + return value; + } + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/RequestLogHandler.java b/container-core/src/main/java/com/yahoo/container/logging/RequestLogHandler.java new file mode 100644 index 00000000000..85df08e4abb --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/RequestLogHandler.java @@ -0,0 +1,9 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +/** + * @author Tony Vaagenes + */ +public interface RequestLogHandler { + void log(RequestLogEntry entry); +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java b/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java new file mode 100644 index 00000000000..295786aa15d --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java @@ -0,0 +1,185 @@ +package com.yahoo.container.logging; + +import com.yahoo.data.access.Inspectable; +import com.yahoo.data.access.Inspector; +import com.yahoo.data.access.simple.JsonRender; +import com.yahoo.yolean.trace.TraceNode; +import com.yahoo.yolean.trace.TraceVisitor; +import com.fasterxml.jackson.core.JsonGenerator; + +import java.io.IOException; + +public class TraceRenderer extends TraceVisitor { + private static final String TRACE_CHILDREN = "children"; + private static final String TRACE_MESSAGE = "message"; + private static final String TRACE_TIMESTAMP = "timestamp"; + private static final String TRACE = "trace"; + + private final long basetime; + private final JsonGenerator generator; + private final FieldConsumer fieldConsumer; + private boolean hasFieldName = false; + int emittedChildNesting = 0; + int currentChildNesting = 0; + private boolean insideOpenObject = false; + + public interface FieldConsumer { + void accept(Object object) throws IOException; + } + + private static class Consumer implements FieldConsumer { + private final JsonGenerator generator; + + Consumer(JsonGenerator generator) { + this.generator = generator; + } + + @Override + public void accept(Object object) throws IOException { + if (object instanceof Inspectable) { + renderInspectorDirect(((Inspectable) object).inspect()); + } else { + generator.writeObject(object); + } + } + private void renderInspectorDirect(Inspector data) throws IOException { + StringBuilder intermediate = new StringBuilder(); + JsonRender.render(data, intermediate, true); + generator.writeRawValue(intermediate.toString()); + } + } + + TraceRenderer(JsonGenerator generator, long basetime) { + this(generator, new Consumer(generator), basetime); + } + public TraceRenderer(JsonGenerator generator, FieldConsumer consumer, long basetime) { + this.generator = generator; + this.fieldConsumer = consumer; + this.basetime = basetime; + } + + @Override + public void entering(TraceNode node) { + ++currentChildNesting; + } + + @Override + public void leaving(TraceNode node) { + conditionalEndObject(); + if (currentChildNesting == emittedChildNesting) { + try { + generator.writeEndArray(); + generator.writeEndObject(); + } catch (IOException e) { + throw new TraceRenderWrapper(e); + } + --emittedChildNesting; + } + --currentChildNesting; + } + + @Override + public void visit(TraceNode node) { + try { + doVisit(node.timestamp(), node.payload(), node.children().iterator().hasNext()); + } catch (IOException e) { + throw new TraceRenderWrapper(e); + } + } + + private void doVisit(long timestamp, Object payload, boolean hasChildren) throws IOException { + boolean dirty = false; + if (timestamp != 0L) { + header(); + generator.writeStartObject(); + generator.writeNumberField(TRACE_TIMESTAMP, timestamp - basetime); + dirty = true; + } + if (payload != null) { + if (!dirty) { + header(); + generator.writeStartObject(); + } + generator.writeFieldName(TRACE_MESSAGE); + fieldConsumer.accept(payload); + dirty = true; + } + if (dirty) { + if (!hasChildren) { + generator.writeEndObject(); + } else { + setInsideOpenObject(true); + } + } + } + private void header() { + fieldName(); + for (int i = 0; i < (currentChildNesting - emittedChildNesting); ++i) { + startChildArray(); + } + emittedChildNesting = currentChildNesting; + } + + private void startChildArray() { + try { + conditionalStartObject(); + generator.writeArrayFieldStart(TRACE_CHILDREN); + } catch (IOException e) { + throw new TraceRenderWrapper(e); + } + } + + private void conditionalStartObject() throws IOException { + if (!isInsideOpenObject()) { + generator.writeStartObject(); + } else { + setInsideOpenObject(false); + } + } + + private void conditionalEndObject() { + if (isInsideOpenObject()) { + // This triggers if we were inside a data node with payload and + // subnodes, but none of the subnodes contained data + try { + generator.writeEndObject(); + setInsideOpenObject(false); + } catch (IOException e) { + throw new TraceRenderWrapper(e); + } + } + } + + private void fieldName() { + if (hasFieldName) { + return; + } + + try { + generator.writeFieldName(TRACE); + } catch (IOException e) { + throw new TraceRenderWrapper(e); + } + hasFieldName = true; + } + + boolean isInsideOpenObject() { + return insideOpenObject; + } + + void setInsideOpenObject(boolean insideOpenObject) { + this.insideOpenObject = insideOpenObject; + } + public static final class TraceRenderWrapper extends RuntimeException { + + /** + * Should never be serialized, but this is still needed. + */ + private static final long serialVersionUID = 2L; + + TraceRenderWrapper(IOException wrapped) { + super(wrapped); + } + + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/VespaAccessLog.java b/container-core/src/main/java/com/yahoo/container/logging/VespaAccessLog.java new file mode 100644 index 00000000000..254b7fe5385 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/VespaAccessLog.java @@ -0,0 +1,113 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; + +import com.yahoo.component.AbstractComponent; +import com.yahoo.container.core.AccessLogConfig; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * @author Bjorn Borud + * @author Oyvind Bakksjo + */ +public final class VespaAccessLog extends AbstractComponent implements RequestLogHandler, LogWriter { + + private static final ThreadLocal dateFormat = ThreadLocal.withInitial(VespaAccessLog::createDateFormat); + + private final AccessLogHandler logHandler; + + public VespaAccessLog(AccessLogConfig config) { + logHandler = new AccessLogHandler(config.fileHandler(), this); + } + + private static SimpleDateFormat createDateFormat() { + SimpleDateFormat format = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]"); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format; + } + + private static String getDate() { + return dateFormat.get().format(new Date()); + } + + private String getRequest(final String httpMethod, final String rawPath, final String rawQuery, final String httpVersion) { + return httpMethod + " " + (rawQuery != null ? rawPath + "?" + rawQuery : rawPath) + " " + httpVersion; + } + + private String getUser(String user) { + return (user == null) ? "-" : user; + } + + private String toLogline(String ipAddr, String user, String request, String referer, String agent, + long durationMillis, long byteCount, HitCounts hitcounts, int returnCode) + { + long ms = Math.max(0L, durationMillis); + StringBuilder sb = new StringBuilder() + .append(ipAddr) + .append(" - ") + .append(getUser(user)) + .append(' ') + .append(getDate()) + .append(" \"") + .append(request) + .append("\" ") + .append(returnCode) + .append(' ') + .append(byteCount) + .append(" \"") + .append(referer) + .append("\" \"") + .append(agent) + .append("\" ") + .append(ms/1000) + .append('.'); + decimalsOfSecondsFromMilliseconds(ms, sb); + sb.append(' ') + .append((hitcounts == null) ? 0 : hitcounts.getTotalHitCount()) + .append(" 0.0 ") + .append((hitcounts == null) ? 0 : hitcounts.getSummaryCount()); + return sb.toString(); + } + + private void decimalsOfSecondsFromMilliseconds(long ms, StringBuilder sb) { + long dec = ms % 1000; + String numbers = String.valueOf(dec); + if (dec <= 9) { + sb.append("00"); + } else if (dec <= 99) { + sb.append('0'); + } + sb.append(numbers); + } + + @Override public void deconstruct() { logHandler.shutdown(); } + + @Override + public void log(RequestLogEntry entry) { + logHandler.log(entry); + } + + @Override + public void write(RequestLogEntry entry, OutputStream outputStream) throws IOException { + outputStream.write( + toLogline( + entry.peerAddress().get(), + null, + getRequest( + entry.httpMethod().orElse(null), + entry.rawPath().orElse(null), + entry.rawQuery().orElse(null), + entry.httpVersion().orElse(null)), + entry.referer().orElse(null), + entry.userAgent().orElse(null), + entry.duration().get().toMillis(), + entry.contentSize().orElse(0L), + entry.hitCounts().orElse(null), + entry.statusCode().orElse(0)).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/container-core/src/main/java/com/yahoo/container/logging/package-info.java b/container-core/src/main/java/com/yahoo/container/logging/package-info.java new file mode 100644 index 00000000000..fc2abb7b609 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/container/logging/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.logging; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/CertificateStore.java b/container-core/src/main/java/com/yahoo/jdisc/http/CertificateStore.java new file mode 100644 index 00000000000..3a63726b951 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/CertificateStore.java @@ -0,0 +1,26 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * 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/container-core/src/main/java/com/yahoo/jdisc/http/Cookie.java b/container-core/src/main/java/com/yahoo/jdisc/http/Cookie.java new file mode 100644 index 00000000000..d882cf7a34a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/Cookie.java @@ -0,0 +1,250 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.server.CookieCutter; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static java.util.stream.Collectors.toList; + +/** + * A RFC 6265 compliant cookie. + * + * Note: RFC 2109 and RFC 2965 is no longer supported. All fields that are not part of RFC 6265 are deprecated. + * + * @author Einar M R Rosenvinge + * @author bjorncs + */ +public class Cookie { + + private final Set ports = new HashSet<>(); + private String name; + private String value; + private String domain; + private String path; + private SameSite sameSite; + private long maxAgeSeconds = Integer.MIN_VALUE; + 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; + sameSite = cookie.sameSite; + maxAgeSeconds = cookie.maxAgeSeconds; + 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 SameSite getSameSite() { + return sameSite; + } + + public Cookie setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + return this; + } + + public int getMaxAge(TimeUnit unit) { + return (int)unit.convert(maxAgeSeconds, TimeUnit.SECONDS); + } + + public Cookie setMaxAge(int maxAge, TimeUnit unit) { + this.maxAgeSeconds = maxAge >= 0 ? unit.toSeconds(maxAge) : Integer.MIN_VALUE; + 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Cookie cookie = (Cookie) o; + return maxAgeSeconds == cookie.maxAgeSeconds && + secure == cookie.secure && + httpOnly == cookie.httpOnly && + discard == cookie.discard && + sameSite == cookie.sameSite && + Objects.equals(ports, cookie.ports) && + Objects.equals(name, cookie.name) && + Objects.equals(value, cookie.value) && + Objects.equals(domain, cookie.domain) && + Objects.equals(path, cookie.path); + } + + @Override + public int hashCode() { + return Objects.hash(ports, name, value, domain, path, sameSite, maxAgeSeconds, secure, httpOnly, discard); + } + + @Override + public String toString() { + StringBuilder ret = new StringBuilder(); + ret.append(name).append("=").append(value); + return ret.toString(); + } + // NOTE cookie encoding and decoding: + // The implementation uses Jetty for server-side (encoding of Set-Cookie and decoding of Cookie header), + // and java.net.HttpCookie for client-side (encoding of Cookie and decoding of Set-Cookie header). + // + // Implementation is RFC-6265 compliant. + + public static String toCookieHeader(Iterable cookies) { + return StreamSupport.stream(cookies.spliterator(), false) + .map(cookie -> { + java.net.HttpCookie httpCookie = new java.net.HttpCookie(cookie.getName(), cookie.getValue()); + httpCookie.setDomain(cookie.getDomain()); + httpCookie.setHttpOnly(cookie.isHttpOnly()); + httpCookie.setMaxAge(cookie.getMaxAge(TimeUnit.SECONDS)); + httpCookie.setPath(cookie.getPath()); + httpCookie.setSecure(cookie.isSecure()); + httpCookie.setVersion(0); + return httpCookie.toString(); + }) + .collect(Collectors.joining(";")); + } + + public static List fromCookieHeader(String headerVal) { + CookieCutter cookieCutter = new CookieCutter(); + cookieCutter.addCookieField(headerVal); + return Arrays.stream(cookieCutter.getCookies()) + .map(servletCookie -> { + Cookie cookie = new Cookie(); + cookie.setName(servletCookie.getName()); + cookie.setValue(servletCookie.getValue()); + cookie.setPath(servletCookie.getPath()); + cookie.setDomain(servletCookie.getDomain()); + cookie.setMaxAge(servletCookie.getMaxAge(), TimeUnit.SECONDS); + cookie.setSecure(servletCookie.getSecure()); + cookie.setHttpOnly(servletCookie.isHttpOnly()); + return cookie; + }) + .collect(toList()); + } + + public static List toSetCookieHeaders(Iterable cookies) { + return StreamSupport.stream(cookies.spliterator(), false) + .map(cookie -> + new org.eclipse.jetty.http.HttpCookie( + cookie.getName(), + cookie.getValue(), + cookie.getDomain(), + cookie.getPath(), + cookie.getMaxAge(TimeUnit.SECONDS), + cookie.isHttpOnly(), + cookie.isSecure(), + null, /* comment */ + 0, /* version */ + Optional.ofNullable(cookie.getSameSite()).map(SameSite::jettySameSite).orElse(null) + ).getRFC6265SetCookie()) + .collect(toList()); + } + + @Deprecated // TODO Vespa 8 Remove + public static List toSetCookieHeaderAll(Iterable cookies) { + return toSetCookieHeaders(cookies); + } + + public static Cookie fromSetCookieHeader(String headerVal) { + return java.net.HttpCookie.parse(headerVal).stream() + .map(httpCookie -> { + Cookie cookie = new Cookie(); + cookie.setName(httpCookie.getName()); + cookie.setValue(httpCookie.getValue()); + cookie.setDomain(httpCookie.getDomain()); + cookie.setHttpOnly(httpCookie.isHttpOnly()); + cookie.setMaxAge((int) httpCookie.getMaxAge(), TimeUnit.SECONDS); + cookie.setPath(httpCookie.getPath()); + cookie.setSecure(httpCookie.getSecure()); + return cookie; + }) + .findFirst().get(); + } + + public enum SameSite { + NONE, STRICT, LAX; + + HttpCookie.SameSite jettySameSite() { + return HttpCookie.SameSite.valueOf(name()); + } + + static SameSite fromJettySameSite(HttpCookie.SameSite jettySameSite) { + return valueOf(jettySameSite.name()); + } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java b/container-core/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java new file mode 100644 index 00000000000..039966133e8 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java @@ -0,0 +1,122 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * @author Anirudha Khanna + */ +@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_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/container-core/src/main/java/com/yahoo/jdisc/http/HttpRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/HttpRequest.java new file mode 100644 index 00000000000..118c34245c0 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/HttpRequest.java @@ -0,0 +1,342 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.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.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.util.MultiMap; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.security.Principal; +import java.util.ArrayList; +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 Anirudha Khanna + * @author Einar M R Rosenvinge + */ +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> parameters = new HashMap<>(); + private Principal principal; + 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(getUriQueryParameters(uri)); + 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(getUriQueryParameters(uri)); + this.connectedAt = creationTime(TimeUnit.MILLISECONDS); + } catch (RuntimeException e) { + release(); + throw e; + } + } + + private static Map> getUriQueryParameters(URI uri) { + MultiMap queryParameters = new MultiMap<>(); + new HttpURI(uri).decodeQueryTo(queryParameters); + + // Do a deep copy so we do not leak Jetty classes outside + Map> deepCopiedQueryParameters = new HashMap<>(); + for (Map.Entry> entry : queryParameters.entrySet()) { + deepCopiedQueryParameters.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + return deepCopiedQueryParameters; + } + + public Method getMethod() { + return method; + } + + public void setMethod(Method method) { + this.method = method; + } + + public Version getVersion() { + return version; + } + + /** Returns the remote address, or null if unresolved */ + @Override + public String getRemoteHostAddress() { + if (remoteAddress instanceof InetSocketAddress) { + InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress(); + if (remoteInetAddress == null) + return null; + return remoteInetAddress.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; + } + + /** + *

For server requests, this returns the timestamp of when the underlying HTTP channel was connected. + * + *

For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.

+ * + * @param unit the unit to return the time in + * @return the timestamp of when the underlying HTTP channel was connected, or request creation time + */ + @Override + 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); + } + + /** + *

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 client configuration.

+ * + *

NOTE: 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 client.

+ * + * @param timeout The allocated amount of time. + * @param unit The time unit of the timeout argument. + */ + public void setConnectionTimeout(long timeout, TimeUnit unit) { + this.connectionTimeout = unit.toMillis(timeout); + } + + public Map> parameters() { + return parameters; + } + + @Override + public void copyHeaders(HeaderFields target) { + target.addAll(headers()); + } + + public List decodeCookieHeader() { + List cookies = headers().get(HttpHeaders.Names.COOKIE); + if (cookies == null) { + return Collections.emptyList(); + } + List ret = new LinkedList<>(); + for (String cookie : cookies) { + ret.addAll(Cookie.fromCookieHeader(cookie)); + } + return ret; + } + + public void encodeCookieHeader(List cookies) { + headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies)); + } + + /** + *

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.

+ * + *

NOTE: These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to + * prevent concurrency issues such as ConcurrentModificationExceptions.

+ * + * @return The trailer headers of this HttpRequest. + */ + public HeaderFields trailers() { + return trailers; + } + + /** + * Returns whether this request was explicitly chunked from the client. 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 Principal getUserPrincipal() { + return principal; + } + + public void setUserPrincipal(Principal principal) { + this.principal = principal; + } + + 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); + } + + 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); + } + + public static HttpRequest newClientRequest(Request parent, URI uri, Method method, Version version) { + return new HttpRequest(parent, uri, method, version); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/HttpResponse.java b/container-core/src/main/java/com/yahoo/jdisc/http/HttpResponse.java new file mode 100644 index 00000000000..f7138ba0e2b --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/HttpResponse.java @@ -0,0 +1,125 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +import com.yahoo.jdisc.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 java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * A HTTP response. + * + * @author Einar M R Rosenvinge + */ +public class HttpResponse extends Response implements ServletOrJdiscHttpResponse { + + private final HeaderFields trailers = new HeaderFields(); + private boolean chunkedEncodingEnabled = true; + private String message; + + 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; + } + + 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 decodeSetCookieHeader() { + List cookies = headers().get(HttpHeaders.Names.SET_COOKIE); + if (cookies == null) { + return Collections.emptyList(); + } + List ret = new LinkedList<>(); + for (String cookie : cookies) { + ret.add(Cookie.fromSetCookieHeader(cookie)); + } + return ret; + } + + public void encodeSetCookieHeader(List cookies) { + headers().remove(HttpHeaders.Names.SET_COOKIE); + for (Cookie cookie : cookies) { + headers().add(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeaders(Arrays.asList(cookie))); + } + } + + /** + *

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.

+ * + *

NOTE: These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to + * prevent concurrency issues such as ConcurrentModificationExceptions.

+ * + * @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/container-core/src/main/java/com/yahoo/jdisc/http/SecretStore.java b/container-core/src/main/java/com/yahoo/jdisc/http/SecretStore.java new file mode 100644 index 00000000000..4f739c5bd78 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/SecretStore.java @@ -0,0 +1,23 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http; + +/** + * An abstraction of a secret store for e.g passwords. + * Implementations can be plugged in to provide passwords for various keys. + * + * @author bratseth + * @author bjorncs + * @deprecated Use com.yahoo.container.jdisc.secretstore.SecretStore + */ +@Deprecated // Vespa 8 +public interface SecretStore { + + /** Returns the secret for this key */ + String getSecret(String key); + + /** Returns the secret for this key and version */ + default String getSecret(String key, int version) { + throw new UnsupportedOperationException("SecretStore implementation does not support versioned secrets"); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java b/container-core/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java new file mode 100644 index 00000000000..43da1a82077 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java @@ -0,0 +1,4 @@ +// Copyright 2017 Yahoo Holdings. 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/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java new file mode 100644 index 00000000000..f7ab399574c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java @@ -0,0 +1,543 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.HeaderFields; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Version; +import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.Principal; +import java.security.cert.X509Certificate; +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.concurrent.TimeUnit; +import java.util.regex.Pattern; + +/** + * The Request class on which all filters will operate upon. + * 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 Map> untreatedParams; + private final HeaderFields untreatedHeaders; + private List untreatedCookies = 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()); + } + + 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() { + InetSocketAddress localAddress = localAddress(); + if (localAddress.getAddress() == null) return null; + return localAddress.getAddress().getHostAddress(); + } + + private InetSocketAddress localAddress() { + int port = parent.getUri().getPort(); + if (port < 0) + port = 0; + return new InetSocketAddress(parent.getUri().getHost(), port); + } + + public Enumeration 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 getParameterNames(); + + public List getParameterNamesAsList() { + return new ArrayList(parent.parameters().keySet()); + } + + public Enumeration getParameterValues(String name) { + return Collections.enumeration(parent.parameters().get(name)); + } + + public List getParameterValuesAsList(String name) { + return parent.parameters().get(name); + } + + public Map> 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> getUntreatedParams() { + return Collections.unmodifiableMap(untreatedParams); + } + + + /** + * Returns the untreatedHeaders from + * parent request + */ + public HeaderFields getUntreatedHeaders() { + return untreatedHeaders; + } + + /** + * Returns the untreatedCookies from + * parent request + */ + public List 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 getHeaderNames(); + + public abstract List getHeaderNamesAsList(); + + public abstract Enumeration getHeaders(String name); + + public abstract List 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 values); + + public int getIntHeader(String name) { + String value = getHeader(name); + if (value == null) { + return -1; + } else { + return Integer.parseInt(value); + } + } + + + public List getCookies() { + return parent.decodeCookieHeader(); + } + + public void setCookies(List cookies) { + parent.encodeCookieHeader(cookies); + } + + public long getConnectedAt(TimeUnit unit) { + return parent.getConnectedAt(unit); + } + + 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 abstract Principal getUserPrincipal(); + + 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 abstract void setUserPrincipal(Principal principal); + + /** + * @return The client certificate chain in ascending order of trust. The first certificate is the one sent from the client. + * Returns an empty list if the client did not provide a certificate. + */ + public abstract List getClientCertificateChain(); + + 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 cookies = new ArrayList<>(); + // Get current set of cookies first + List c = getCookies(); + if (c != null && !c.isEmpty()) { + cookies.addAll(c); + } + cookies.add(cookie.getCookie()); + setCookies(cookies); + } + } + + public abstract void clearCookies(); + + public JDiscCookieWrapper[] getWrappedCookies() { + List cookies = getCookies(); + if (cookies == null) { + return null; + } + List 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 { + + 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/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java new file mode 100644 index 00000000000..4e8b779c516 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java @@ -0,0 +1,154 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import 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 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 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 getUntreatedCookies() { + return 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 setHeader(String name, String value); + + 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 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 getCookies() { + return parent.decodeSetCookieHeader(); + } + + public abstract void setCookies(List 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 cookies = new ArrayList<>(); + //Get current set of cookies first + List 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/container-core/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java new file mode 100644 index 00000000000..af9e2b5e99a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java @@ -0,0 +1,42 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.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 getInitParameterNames(); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java new file mode 100644 index 00000000000..2b9c650d545 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java @@ -0,0 +1,79 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; + +import java.util.concurrent.TimeUnit; + +/** + * Wrapper of Cookie. + * + * @author Tejal Knot + * + */ +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 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 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); + } + + /** + * Return com.yahoo.jdisc.http.Cookie + * + * @return - cookie + */ + public Cookie getCookie() { + return cookie; + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java new file mode 100644 index 00000000000..f8d9e6b2642 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java @@ -0,0 +1,133 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.servlet.ServletRequest; + +import java.net.URI; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; + +/** + * 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 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 values = parent.headers().get(name); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(values.size() - 1); + } + + public Enumeration getHeaderNames() { + return Collections.enumeration(parent.headers().keySet()); + } + + public List getHeaderNamesAsList() { + return new ArrayList(parent.headers().keySet()); + } + + @Override + public Enumeration getHeaders(String name) { + return Collections.enumeration(getHeadersAsList(name)); + } + + public List getHeadersAsList(String name) { + List values = parent.headers().get(name); + if(values == null) { + return Collections.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 values) { + parent.headers().put(name, values); + } + + @Override + public Principal getUserPrincipal() { + return parent.getUserPrincipal(); + } + + @Override + public void setUserPrincipal(Principal principal) { + this.parent.setUserPrincipal(principal); + } + + @Override + public List getClientCertificateChain() { + return Optional.ofNullable(parent.context().get(ServletRequest.JDISC_REQUEST_X509CERT)) + .map(X509Certificate[].class::cast) + .map(Arrays::asList) + .orElse(Collections.emptyList()); + } + + @Override + public void clearCookies() { + parent.headers().remove(HttpHeaders.Names.COOKIE); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java new file mode 100644 index 00000000000..ff81359f93c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java @@ -0,0 +1,67 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.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 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 values = parent.headers().get(name); + if (values == null || values.isEmpty()) { + return null; + } + return values.get(values.size() - 1); + } + + @Override + public void setCookies(List cookies) { + parent.encodeSetCookieHeader(cookies); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java new file mode 100644 index 00000000000..977e3ab5d1d --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java @@ -0,0 +1,14 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; + +/** + * @author Einar M R Rosenvinge + */ +public interface RequestFilter extends com.yahoo.jdisc.SharedResource, RequestFilterBase { + + void filter(HttpRequest request, ResponseHandler handler); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java new file mode 100644 index 00000000000..4eb7091f378 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java @@ -0,0 +1,9 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +/** + * @author gjoranv + * @since 2.4 + */ +public interface RequestFilterBase { +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java new file mode 100644 index 00000000000..e5e7ae1ef56 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java @@ -0,0 +1,45 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpRequest.Method; + +import java.net.URI; +import java.util.List; +import java.util.Optional; + +/** + * Read-only view of the request for use by SecurityResponseFilters. + * + * @author Tony Vaagenes + */ +public interface RequestView { + + /** + * Returns a named attribute. + * + * @see javax.servlet.ServletRequest.getAttribute(java.lang.String) + * @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) + */ + Object getAttribute(String name); + + /** + * Returns an immutable view of all values of a named header field. + * Returns an empty list if no such header is present. + */ + List getHeaders(String name); + + /** + * Convenience method for retrieving the first value of a named header field. + * Returns empty if the header is not set, or if the value list is empty. + */ + Optional getFirstHeader(String name); + + /** + * Returns the Http method. Only present if the underlying request has http-like semantics. + */ + Optional getMethod(); + + URI getUri(); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java new file mode 100644 index 00000000000..44fe7d9fcf1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java @@ -0,0 +1,14 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.SharedResource; + +/** + * @author Einar M R Rosenvinge + */ +public interface ResponseFilter extends SharedResource, ResponseFilterBase { + + public void filter(Response response, Request request); +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java new file mode 100644 index 00000000000..b869c882351 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java @@ -0,0 +1,9 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +/** + * @author gjoranv + * @since 2.4 + */ +public interface ResponseFilterBase { +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java new file mode 100644 index 00000000000..cbed273b7ee --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java @@ -0,0 +1,108 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.google.common.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.Collections; +import java.util.List; +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 Tony Vaagenes + */ +@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 Optional cast(Class 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 List getHeaders(String name) { + return Collections.unmodifiableList(Collections.list(request.getHeaders(name))); + } + + @Override + public Optional getFirstHeader(String name) { + return getHeaders(name).stream().findFirst(); + } + + @Override + public Optional getMethod() { + return Optional.of(Method.valueOf(request.getMethod())); + } + + @Override + public URI getUri() { + return uri; + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java new file mode 100644 index 00000000000..e6f4add49de --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java @@ -0,0 +1,13 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author Simon Thoresen Hult + */ +public interface SecurityRequestFilter extends RequestFilterBase { + + void filter(DiscFilterRequest request, ResponseHandler handler); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java new file mode 100644 index 00000000000..2d97bbdc494 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java @@ -0,0 +1,77 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.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 filters = new ArrayList<>(); + + private SecurityRequestFilterChain(Iterable 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 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 view of the filters in this */ + public List getFilters() { + return Collections.unmodifiableList(filters); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java new file mode 100644 index 00000000000..aa4f7d29b89 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java @@ -0,0 +1,8 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +public interface SecurityResponseFilter extends ResponseFilterBase { + + void filter(DiscFilterResponse response, RequestView request); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java new file mode 100644 index 00000000000..d45b406a375 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java @@ -0,0 +1,101 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import 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 filters = new ArrayList<>(); + + private SecurityResponseFilterChain(Iterable 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 filters) { + return new SecurityResponseFilterChain(filters); + } + + /** Returns an unmodifiable view of the filters in this */ + public List getFilters() { + return Collections.unmodifiableList(filters); + } + + static class RequestViewImpl implements RequestView { + + private final Request request; + private final Optional 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 List getHeaders(String name) { + List headers = request.headers().get(name); + return headers == null ? Collections.emptyList() : Collections.unmodifiableList(headers); + } + + @Override + public Optional getFirstHeader(String name) { + return getHeaders(name).stream().findFirst(); + } + + @Override + public Optional getMethod() { + return method; + } + + @Override + public URI getUri() { + return request.getUri(); + } + + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java new file mode 100644 index 00000000000..f06f9e256ff --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java @@ -0,0 +1,169 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.servlet.ServletRequest; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Servlet implementation for JDisc filter requests. + */ +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 getAttributeNames() { + Set 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 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 getHeaderNames() { + return parent.getHeaderNames(); + } + + public List getHeaderNamesAsList() { + return Collections.list(getHeaderNames()); + } + + @Override + public Enumeration getHeaders(String name) { + return parent.getHeaders(name); + } + + @Override + public List 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 values) { + parent.setHeaders(name, values); + } + + @Override + public Principal getUserPrincipal() { + return parent.getUserPrincipal(); + } + + @Override + public void setUserPrincipal(Principal principal) { + parent.setUserPrincipal(principal); + } + + @Override + public List getClientCertificateChain() { + return Optional.ofNullable(parent.getRequest().getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT)) + .map(X509Certificate[].class::cast) + .map(Arrays::asList) + .orElse(Collections.emptyList()); + } + + @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/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java new file mode 100644 index 00000000000..b603e7776f1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java @@ -0,0 +1,81 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter; + +import com.google.common.collect.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.Collection; +import java.util.List; + +/** + * Servlet implementation for JDisc filter responses. + */ +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 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 headers = parent.getHeaders(name); + return headers.isEmpty() + ? null + : Iterables.getLast(headers); + } + + @Override + public void setCookies(List cookies) { + removeHeaders(HttpHeaders.Names.SET_COOKIE); + List setCookieHeaders = Cookie.toSetCookieHeaders(cookies); + setCookieHeaders.forEach(cookie -> addHeader(HttpHeaders.Names.SET_COOKIE, cookie)); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java new file mode 100644 index 00000000000..e1834fd8b7d --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java @@ -0,0 +1,24 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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 Einar M R Rosenvinge + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java new file mode 100644 index 00000000000..5ce3f6a496f --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java @@ -0,0 +1,24 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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 Einar M R Rosenvinge + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java new file mode 100644 index 00000000000..85f71777cf3 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java @@ -0,0 +1,55 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.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 Einar M R Rosenvinge + */ +public final class RequestFilterChain extends AbstractResource implements RequestFilter { + + private final List filters = new ArrayList<>(); + private final ResourcePool filterReferences = new ResourcePool(); + + private RequestFilterChain(Iterable 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 filters) { + if (filters.size() == 0) { + return EmptyRequestFilter.INSTANCE; + } + if (filters.size() == 1) { + return filters.get(0); + } + return new RequestFilterChain(filters); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java new file mode 100644 index 00000000000..5c5eda1f139 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java @@ -0,0 +1,54 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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 Simon Thoresen Hult + */ +public final class ResponseFilterChain extends AbstractResource implements ResponseFilter { + + private final List filters = new ArrayList<>(); + private final ResourcePool filterReferences = new ResourcePool(); + + private ResponseFilterChain(Iterable 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 filters) { + if (filters.size() == 0) { + return EmptyResponseFilter.INSTANCE; + } + if (filters.size() == 1) { + return filters.get(0); + } + return new ResponseFilterChain(filters); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java new file mode 100644 index 00000000000..02600683e27 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java @@ -0,0 +1,29 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.chain; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; + +/** + * @author Simon Thoresen Hult + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java new file mode 100644 index 00000000000..540a1be7b73 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. 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/container-core/src/main/java/com/yahoo/jdisc/http/filter/package-info.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/package-info.java new file mode 100644 index 00000000000..e97d447adbb --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2017 Yahoo Holdings. 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/container-core/src/main/java/com/yahoo/jdisc/http/package-info.java b/container-core/src/main/java/com/yahoo/jdisc/http/package-info.java new file mode 100644 index 00000000000..b8bd76483cf --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2017 Yahoo Holdings. 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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java new file mode 100644 index 00000000000..4de5e5e5387 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java @@ -0,0 +1,167 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Objects; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +import javax.servlet.http.HttpServletRequest; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.OptionalInt; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; + +/** + * 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 Oyvind Bakksjo + * @author bjorncs + */ +class AccessLogRequestLog extends AbstractLifeCycle implements org.eclipse.jetty.server.RequestLog { + + private static final Logger logger = Logger.getLogger(AccessLogRequestLog.class.getName()); + + // HTTP headers that are logged as extra key-value-pairs in access log entries + private static final List LOGGED_REQUEST_HEADERS = List.of("Vespa-Client-Version"); + + private final RequestLog requestLog; + private final List remoteAddressHeaders; + private final List remotePortHeaders; + + AccessLogRequestLog(RequestLog requestLog, ServerConfig.AccessLog config) { + this.requestLog = requestLog; + this.remoteAddressHeaders = config.remoteAddressHeaders(); + this.remotePortHeaders = config.remotePortHeaders(); + } + + @Override + public void log(Request request, Response response) { + try { + RequestLogEntry.Builder builder = new RequestLogEntry.Builder(); + + String peerAddress = request.getRemoteAddr(); + int peerPort = request.getRemotePort(); + long startTime = request.getTimeStamp(); + long endTime = System.currentTimeMillis(); + builder.peerAddress(peerAddress) + .peerPort(peerPort) + .localPort(getLocalPort(request)) + .timestamp(Instant.ofEpochMilli(startTime)) + .duration(Duration.ofMillis(Math.max(0, endTime - startTime))) + .contentSize(response.getHttpChannel().getBytesWritten()) + .statusCode(response.getCommittedMetaData().getStatus()); + + addNonNullValue(builder, request.getMethod(), RequestLogEntry.Builder::httpMethod); + addNonNullValue(builder, request.getRequestURI(), RequestLogEntry.Builder::rawPath); + addNonNullValue(builder, request.getProtocol(), RequestLogEntry.Builder::httpVersion); + addNonNullValue(builder, request.getScheme(), RequestLogEntry.Builder::scheme); + addNonNullValue(builder, request.getHeader("User-Agent"), RequestLogEntry.Builder::userAgent); + addNonNullValue(builder, request.getHeader("Host"), RequestLogEntry.Builder::hostString); + addNonNullValue(builder, request.getHeader("Referer"), RequestLogEntry.Builder::referer); + addNonNullValue(builder, request.getQueryString(), RequestLogEntry.Builder::rawQuery); + + Principal principal = (Principal) request.getAttribute(ServletRequest.JDISC_REQUEST_PRINCIPAL); + addNonNullValue(builder, principal, RequestLogEntry.Builder::userPrincipal); + + String requestFilterId = (String) request.getAttribute(ServletRequest.JDISC_REQUEST_CHAIN); + addNonNullValue(builder, requestFilterId, (b, chain) -> b.addExtraAttribute("request-chain", chain)); + + String responseFilterId = (String) request.getAttribute(ServletRequest.JDISC_RESPONSE_CHAIN); + addNonNullValue(builder, responseFilterId, (b, chain) -> b.addExtraAttribute("response-chain", chain)); + + UUID connectionId = (UUID) request.getAttribute(JettyConnectionLogger.CONNECTION_ID_REQUEST_ATTRIBUTE); + addNonNullValue(builder, connectionId, (b, uuid) -> b.connectionId(uuid.toString())); + + String remoteAddress = getRemoteAddress(request); + if (!Objects.equal(remoteAddress, peerAddress)) { + builder.remoteAddress(remoteAddress); + } + int remotePort = getRemotePort(request); + if (remotePort != peerPort) { + builder.remotePort(remotePort); + } + LOGGED_REQUEST_HEADERS.forEach(header -> { + String value = request.getHeader(header); + if (value != null) { + builder.addExtraAttribute(header, value); + } + }); + X509Certificate[] clientCert = (X509Certificate[]) request.getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT); + if (clientCert != null && clientCert.length > 0) { + builder.sslPrincipal(clientCert[0].getSubjectX500Principal()); + } + + AccessLogEntry accessLogEntry = (AccessLogEntry) request.getAttribute(JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY); + if (accessLogEntry != null) { + var extraAttributes = accessLogEntry.getKeyValues(); + if (extraAttributes != null) { + extraAttributes.forEach(builder::addExtraAttributes); + } + addNonNullValue(builder, accessLogEntry.getHitCounts(), RequestLogEntry.Builder::hitCounts); + addNonNullValue(builder, accessLogEntry.getTrace(), RequestLogEntry.Builder::traceNode); + } + + requestLog.log(builder.build()); + } catch (Exception e) { + // Catching any exceptions here as it is unclear how Jetty handles exceptions from a RequestLog. + logger.log(Level.SEVERE, "Failed to log access log entry: " + e.getMessage(), e); + } + } + + private String getRemoteAddress(HttpServletRequest request) { + for (String header : remoteAddressHeaders) { + String value = request.getHeader(header); + if (value != null) return value; + } + return request.getRemoteAddr(); + } + + private int getRemotePort(HttpServletRequest request) { + for (String header : remotePortHeaders) { + String value = request.getHeader(header); + if (value != null) { + OptionalInt maybePort = parsePort(value); + if (maybePort.isPresent()) return maybePort.getAsInt(); + } + } + return request.getRemotePort(); + } + + private static int getLocalPort(Request request) { + int connectorLocalPort = getConnectorLocalPort(request); + if (connectorLocalPort <= 0) return request.getLocalPort(); // If connector is already closed + return connectorLocalPort; + } + + private static OptionalInt parsePort(String port) { + try { + return OptionalInt.of(Integer.parseInt(port)); + } catch (IllegalArgumentException e) { + return OptionalInt.empty(); + } + } + + private static void addNonNullValue( + RequestLogEntry.Builder builder, T value, BiConsumer setter) { + if (value != null) { + setter.accept(builder, value); + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java new file mode 100644 index 00000000000..842ab75a312 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java @@ -0,0 +1,59 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.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 bakksjo + */ +public class AccessLoggingRequestHandler extends AbstractRequestHandler { + public static final String CONTEXT_KEY_ACCESS_LOG_ENTRY + = AccessLoggingRequestHandler.class.getName() + "_access-log-entry"; + + public static Optional getAccessLogEntry(final HttpRequest jdiscRequest) { + final Map requestContextMap = jdiscRequest.context(); + return getAccessLogEntry(requestContextMap); + } + + public static Optional getAccessLogEntry(final Map 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); + return delegate.handleRequest(request, handler); + } + + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java new file mode 100644 index 00000000000..7dba217e01c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java @@ -0,0 +1,22 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import java.io.IOException; + +/** + * Interface for async listeners only interested in onComplete. + * @author Tony Vaagenes + */ +@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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java new file mode 100644 index 00000000000..f436d5490d7 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java @@ -0,0 +1,14 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.CompletionHandler; + +/** + * @author bjorncs + */ +public interface CompletionHandlerUtils { + CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() { + @Override public void completed() {} + @Override public void failed(final Throwable t) {} + }; +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java new file mode 100644 index 00000000000..975d88f5c34 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java @@ -0,0 +1,57 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.CompletionHandler; + +import java.util.Arrays; + +/** + * @author Simon Thoresen Hult + */ +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 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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java new file mode 100644 index 00000000000..b9001d187a9 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java @@ -0,0 +1,274 @@ +// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.ConnectorConfig; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.ConnectionLimit; +import org.eclipse.jetty.server.LowResourceMonitor; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.statistic.RateStatistic; +import org.eclipse.jetty.util.thread.Scheduler; + +import java.nio.channels.SelectableChannel; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import static java.util.stream.Collectors.toList; + +/** + * Monitor various resource constraints and throttles new connections once a threshold is exceeded. + * Implementation inspired by Jetty's {@link LowResourceMonitor}, {@link AcceptRateLimit} and {@link ConnectionLimit}. + * + * @author bjorncs + */ +@ManagedObject("Monitor various resource constraints and throttles new connections once a threshold is exceeded") +class ConnectionThrottler extends ContainerLifeCycle implements SelectorManager.AcceptListener { + + private static final Logger log = Logger.getLogger(ConnectionThrottler.class.getName()); + + private final Object monitor = new Object(); + private final Collection resourceLimits = new ArrayList<>(); + private final AbstractConnector connector; + private final Duration idleTimeout; + private final Scheduler scheduler; + + private boolean isRegistered = false; + private boolean isThrottling = false; + + ConnectionThrottler(AbstractConnector connector, ConnectorConfig.Throttling config) { + this(Runtime.getRuntime(), new RateStatistic(1, TimeUnit.SECONDS), connector.getScheduler(), connector, config); + } + + // Intended for unit testing + ConnectionThrottler(Runtime runtime, + RateStatistic rateStatistic, + Scheduler scheduler, + AbstractConnector connector, + ConnectorConfig.Throttling config) { + this.connector = connector; + if (config.maxHeapUtilization() != -1) { + this.resourceLimits.add(new HeapResourceLimit(runtime, config.maxHeapUtilization())); + } + if (config.maxConnections() != -1) { + this.resourceLimits.add(new ConnectionLimitThreshold(config.maxConnections())); + } + if (config.maxAcceptRate() != -1) { + this.resourceLimits.add(new AcceptRateLimit(rateStatistic, config.maxAcceptRate())); + } + this.idleTimeout = config.idleTimeout() != -1 ? Duration.ofMillis((long) (config.idleTimeout()*1000)) : null; + this.scheduler = scheduler; + } + + void registerWithConnector() { + synchronized (monitor) { + if (isRegistered) return; + isRegistered = true; + resourceLimits.forEach(connector::addBean); + connector.addBean(this); + } + } + + @Override + public void onAccepting(SelectableChannel channel) { + throttleIfAnyThresholdIsExceeded(); + } + + private void throttleIfAnyThresholdIsExceeded() { + synchronized (monitor) { + if (isThrottling) return; + List reasons = getThrottlingReasons(); + if (reasons.isEmpty()) return; + log.warning(String.format("Throttling new connection. Reasons: %s", reasons)); + isThrottling = true; + if (connector.isAccepting()) { + connector.setAccepting(false); + } + if (idleTimeout != null) { + log.warning(String.format("Applying idle timeout to existing connections: timeout=%sms", idleTimeout)); + connector.getConnectedEndPoints() + .forEach(endPoint -> endPoint.setIdleTimeout(idleTimeout.toMillis())); + } + scheduler.schedule(this::unthrottleIfBelowThresholds, 1, TimeUnit.SECONDS); + } + } + + private void unthrottleIfBelowThresholds() { + synchronized (monitor) { + if (!isThrottling) return; + List reasons = getThrottlingReasons(); + if (!reasons.isEmpty()) { + log.warning(String.format("Throttling continued. Reasons: %s", reasons)); + scheduler.schedule(this::unthrottleIfBelowThresholds, 1, TimeUnit.SECONDS); + return; + } + if (idleTimeout != null) { + long originalTimeout = connector.getIdleTimeout(); + log.info(String.format("Reverting idle timeout for existing connections: timeout=%sms", originalTimeout)); + connector.getConnectedEndPoints() + .forEach(endPoint -> endPoint.setIdleTimeout(originalTimeout)); + } + log.info("Throttling disabled - resource thresholds no longer exceeded"); + if (!connector.isAccepting()) { + connector.setAccepting(true); + } + isThrottling = false; + } + } + + private List getThrottlingReasons() { + synchronized (monitor) { + return resourceLimits.stream() + .map(ResourceLimit::isThresholdExceeded) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toList()); + } + } + + private interface ResourceLimit extends LifeCycle, SelectorManager.AcceptListener, Connection.Listener { + /** + * @return A string containing the reason if threshold exceeded, empty otherwise. + */ + Optional isThresholdExceeded(); + + @Override default void onOpened(Connection connection) {} + + @Override default void onClosed(Connection connection) {} + } + + /** + * Note: implementation inspired by Jetty's {@link LowResourceMonitor} + */ + private static class HeapResourceLimit extends AbstractLifeCycle implements ResourceLimit { + private final Runtime runtime; + private final double maxHeapUtilization; + + HeapResourceLimit(Runtime runtime, double maxHeapUtilization) { + this.runtime = runtime; + this.maxHeapUtilization = maxHeapUtilization; + } + + @Override + public Optional isThresholdExceeded() { + double heapUtilization = (runtime.maxMemory() - runtime.freeMemory()) / (double) runtime.maxMemory(); + if (heapUtilization > maxHeapUtilization) { + return Optional.of(String.format("Max heap utilization exceeded: %f%%>%f%%", heapUtilization*100, maxHeapUtilization*100)); + } + return Optional.empty(); + } + } + + /** + * Note: implementation inspired by Jetty's {@link org.eclipse.jetty.server.AcceptRateLimit} + */ + private static class AcceptRateLimit extends AbstractLifeCycle implements ResourceLimit { + private final Object monitor = new Object(); + private final RateStatistic rateStatistic; + private final int maxAcceptRate; + + AcceptRateLimit(RateStatistic rateStatistic, int maxAcceptRate) { + this.rateStatistic = rateStatistic; + this.maxAcceptRate = maxAcceptRate; + } + + @Override + public Optional isThresholdExceeded() { + synchronized (monitor) { + int acceptRate = rateStatistic.getRate(); + if (acceptRate > maxAcceptRate) { + return Optional.of(String.format("Max accept rate exceeded: %d>%d", acceptRate, maxAcceptRate)); + } + return Optional.empty(); + } + } + + @Override + public void onAccepting(SelectableChannel channel) { + synchronized (monitor) { + rateStatistic.record(); + } + } + + @Override + protected void doStop() { + synchronized (monitor) { + rateStatistic.reset(); + } + } + } + + /** + * Note: implementation inspired by Jetty's {@link ConnectionLimit}. + */ + private static class ConnectionLimitThreshold extends AbstractLifeCycle implements ResourceLimit { + private final Object monitor = new Object(); + private final int maxConnections; + private final Set connectionsAccepting = new HashSet<>(); + private int connectionOpened; + + ConnectionLimitThreshold(int maxConnections) { + this.maxConnections = maxConnections; + } + + @Override + public Optional isThresholdExceeded() { + synchronized (monitor) { + int totalConnections = connectionOpened + connectionsAccepting.size(); + if (totalConnections > maxConnections) { + return Optional.of(String.format("Max connection exceeded: %d>%d", totalConnections, maxConnections)); + } + return Optional.empty(); + } + } + + @Override + public void onOpened(Connection connection) { + synchronized (monitor) { + connectionsAccepting.remove(connection.getEndPoint().getTransport()); + ++connectionOpened; + } + } + + @Override + public void onClosed(Connection connection) { + synchronized (monitor) { + --connectionOpened; + } + } + + @Override + public void onAccepting(SelectableChannel channel) { + synchronized (monitor) { + connectionsAccepting.add(channel); + } + + } + + @Override + public void onAcceptFailed(SelectableChannel channel, Throwable cause) { + synchronized (monitor) { + connectionsAccepting.remove(channel); + } + } + + @Override + protected void doStop() { + synchronized (monitor) { + connectionsAccepting.clear(); + connectionOpened = 0; + } + } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java new file mode 100644 index 00000000000..d7ad12a5c64 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java @@ -0,0 +1,140 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Inject; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider; +import com.yahoo.security.tls.MixedMode; +import com.yahoo.security.tls.TransportSecurityUtils; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.DetectorConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.ProxyConnectionFactory; +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 java.util.List; + +/** + * @author Einar M R Rosenvinge + * @author bjorncs + */ +public class ConnectorFactory { + + private final ConnectorConfig connectorConfig; + private final SslContextFactoryProvider sslContextFactoryProvider; + + @Inject + public ConnectorFactory(ConnectorConfig connectorConfig, + SslContextFactoryProvider sslContextFactoryProvider) { + runtimeConnectorConfigValidation(connectorConfig); + this.connectorConfig = connectorConfig; + this.sslContextFactoryProvider = sslContextFactoryProvider; + } + + // Perform extra connector config validation that can only be performed at runtime, + // e.g. due to TLS configuration through environment variables. + private static void runtimeConnectorConfigValidation(ConnectorConfig config) { + validateProxyProtocolConfiguration(config); + validateSecureRedirectConfig(config); + } + + private static void validateProxyProtocolConfiguration(ConnectorConfig config) { + ConnectorConfig.ProxyProtocol proxyProtocolConfig = config.proxyProtocol(); + if (proxyProtocolConfig.enabled()) { + boolean tlsMixedModeEnabled = TransportSecurityUtils.getInsecureMixedMode() != MixedMode.DISABLED; + if (!isSslEffectivelyEnabled(config) || tlsMixedModeEnabled) { + throw new IllegalArgumentException("Proxy protocol can only be enabled if connector is effectively HTTPS only"); + } + } + } + + private static void validateSecureRedirectConfig(ConnectorConfig config) { + if (config.secureRedirect().enabled() && isSslEffectivelyEnabled(config)) { + throw new IllegalArgumentException("Secure redirect can only be enabled on connectors without HTTPS"); + } + } + + public ConnectorConfig getConnectorConfig() { + return connectorConfig; + } + + public ServerConnector createConnector(final Metric metric, final Server server, JettyConnectionLogger connectionLogger) { + ServerConnector connector = new JDiscServerConnector( + connectorConfig, metric, server, connectionLogger, createConnectionFactories(metric).toArray(ConnectionFactory[]::new)); + connector.setPort(connectorConfig.listenPort()); + connector.setName(connectorConfig.name()); + connector.setAcceptQueueSize(connectorConfig.acceptQueueSize()); + connector.setReuseAddress(connectorConfig.reuseAddress()); + connector.setIdleTimeout((long)(connectorConfig.idleTimeout() * 1000.0)); + return connector; + } + + private List createConnectionFactories(Metric metric) { + HttpConnectionFactory httpFactory = newHttpConnectionFactory(); + if (!isSslEffectivelyEnabled(connectorConfig)) { + return List.of(httpFactory); + } else if (connectorConfig.ssl().enabled()) { + return connectionFactoriesForHttps(metric, httpFactory); + } else if (TransportSecurityUtils.isTransportSecurityEnabled()) { + switch (TransportSecurityUtils.getInsecureMixedMode()) { + case TLS_CLIENT_MIXED_SERVER: + case PLAINTEXT_CLIENT_MIXED_SERVER: + return List.of(new DetectorConnectionFactory(newSslConnectionFactory(metric, httpFactory)), httpFactory); + case DISABLED: + return connectionFactoriesForHttps(metric, httpFactory); + default: + throw new IllegalStateException(); + } + } else { + return List.of(httpFactory); + } + } + + private List connectionFactoriesForHttps(Metric metric, HttpConnectionFactory httpFactory) { + ConnectorConfig.ProxyProtocol proxyProtocolConfig = connectorConfig.proxyProtocol(); + SslConnectionFactory sslFactory = newSslConnectionFactory(metric, httpFactory); + if (proxyProtocolConfig.enabled()) { + if (proxyProtocolConfig.mixedMode()) { + return List.of(new DetectorConnectionFactory(sslFactory, new ProxyConnectionFactory(sslFactory.getProtocol())), sslFactory, httpFactory); + } else { + return List.of(new ProxyConnectionFactory(sslFactory.getProtocol()), sslFactory, httpFactory); + } + } else { + return List.of(sslFactory, httpFactory); + } + } + + private HttpConnectionFactory newHttpConnectionFactory() { + 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 (isSslEffectivelyEnabled(connectorConfig)) { + httpConfig.addCustomizer(new SecureRequestCustomizer()); + } + return new HttpConnectionFactory(httpConfig); + } + + private SslConnectionFactory newSslConnectionFactory(Metric metric, HttpConnectionFactory httpFactory) { + SslContextFactory ctxFactory = sslContextFactoryProvider.getInstance(connectorConfig.name(), connectorConfig.listenPort()); + SslConnectionFactory connectionFactory = new SslConnectionFactory(ctxFactory, httpFactory.getProtocol()); + connectionFactory.addBean(new SslHandshakeFailedListener(metric, connectorConfig.name(), connectorConfig.listenPort())); + return connectionFactory; + } + + private static boolean isSslEffectivelyEnabled(ConnectorConfig config) { + return config.ssl().enabled() + || (config.implicitTlsEnabled() && TransportSecurityUtils.isTransportSecurityEnabled()); + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java new file mode 100644 index 00000000000..cd21dccde0e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java @@ -0,0 +1,41 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.eclipse.jetty.util.ByteArrayISO8859Writer; +import org.eclipse.jetty.util.StringUtil; + +import java.io.IOException; +import java.util.Optional; + +/** + * Creates HTML body having the status code, error message and request uri. + * The body is constructed from a template that is inspired by the default Jetty template (see {@link org.eclipse.jetty.server.Response#sendError(int, String)}). + * The content is written using the ISO-8859-1 charset. + * + * @author bjorncs + */ +public class ErrorResponseContentCreator { + + private final ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer(2048); + + public byte[] createErrorContent(String requestUri, int statusCode, Optional message) { + String sanitizedString = message.map(StringUtil::sanitizeXmlString).orElse(""); + String statusCodeString = Integer.toString(statusCode); + writer.resetWriter(); + try { + writer.write("\n\n\nError "); + writer.write(statusCodeString); + writer.write("\n\n\n

HTTP ERROR: "); + writer.write(statusCodeString); + writer.write("

\n

Problem accessing "); + writer.write(StringUtil.sanitizeXmlString(requestUri)); + writer.write(". Reason:\n

    ");
+            writer.write(sanitizedString);
+            writer.write("

\n
\n\n\n"); + } catch (IOException e) { + // IOException should not be thrown unless writer is constructed using byte[] parameter + throw new RuntimeException(e); + } + return writer.getByteArray(); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java new file mode 100644 index 00000000000..ebc10482600 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java @@ -0,0 +1,59 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * 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 Steinar Knutsen + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java new file mode 100644 index 00000000000..310f3c9a646 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java @@ -0,0 +1,102 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.application.BindingRepository; +import com.yahoo.jdisc.application.BindingSet; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +/** + * Resolves request/response filter (chain) from a {@link URI} instance. + * + * @author Oyvind Bakksjo + * @author bjorncs + */ +public class FilterBindings { + + private final Map requestFilters; + private final Map responseFilters; + private final Map defaultRequestFilters; + private final Map defaultResponseFilters; + private final BindingSet requestFilterBindings; + private final BindingSet responseFilterBindings; + + private FilterBindings( + Map requestFilters, + Map responseFilters, + Map defaultRequestFilters, + Map defaultResponseFilters, + BindingSet requestFilterBindings, + BindingSet responseFilterBindings) { + this.requestFilters = requestFilters; + this.responseFilters = responseFilters; + this.defaultRequestFilters = defaultRequestFilters; + this.defaultResponseFilters = defaultResponseFilters; + this.requestFilterBindings = requestFilterBindings; + this.responseFilterBindings = responseFilterBindings; + } + + public Optional resolveRequestFilter(URI uri, int localPort) { + String filterId = requestFilterBindings.resolve(uri); + if (filterId != null) return Optional.of(filterId); + return Optional.ofNullable(defaultRequestFilters.get(localPort)); + } + + public Optional resolveResponseFilter(URI uri, int localPort) { + String filterId = responseFilterBindings.resolve(uri); + if (filterId != null) return Optional.of(filterId); + return Optional.ofNullable(defaultResponseFilters.get(localPort)); + } + + public RequestFilter getRequestFilter(String filterId) { return requestFilters.get(filterId); } + + public ResponseFilter getResponseFilter(String filterId) { return responseFilters.get(filterId); } + + public Collection requestFilterIds() { return requestFilters.keySet(); } + + public Collection responseFilterIds() { return responseFilters.keySet(); } + + public Collection requestFilters() { return requestFilters.values(); } + + public Collection responseFilters() { return responseFilters.values(); } + + public static class Builder { + private final Map requestFilters = new TreeMap<>(); + private final Map responseFilters = new TreeMap<>(); + private final Map defaultRequestFilters = new TreeMap<>(); + private final Map defaultResponseFilters = new TreeMap<>(); + private final BindingRepository requestFilterBindings = new BindingRepository<>(); + private final BindingRepository responseFilterBindings = new BindingRepository<>(); + + public Builder() {} + + public Builder addRequestFilter(String id, RequestFilter filter) { requestFilters.put(id, filter); return this; } + + public Builder addResponseFilter(String id, ResponseFilter filter) { responseFilters.put(id, filter); return this; } + + public Builder addRequestFilterBinding(String id, String binding) { requestFilterBindings.bind(binding, id); return this; } + + public Builder addResponseFilterBinding(String id, String binding) { responseFilterBindings.bind(binding, id); return this; } + + public Builder setRequestFilterDefaultForPort(String id, int port) { defaultRequestFilters.put(port, id); return this; } + + public Builder setResponseFilterDefaultForPort(String id, int port) { defaultResponseFilters.put(port, id); return this; } + + public FilterBindings build() { + return new FilterBindings( + Collections.unmodifiableMap(requestFilters), + Collections.unmodifiableMap(responseFilters), + Collections.unmodifiableMap(defaultRequestFilters), + Collections.unmodifiableMap(defaultResponseFilters), + requestFilterBindings.activate(), + responseFilterBindings.activate()); + } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java new file mode 100644 index 00000000000..0827ccdc39e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java @@ -0,0 +1,28 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java new file mode 100644 index 00000000000..3ebc7bbc551 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java @@ -0,0 +1,266 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import 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 Tony Vaagenes + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java new file mode 100644 index 00000000000..a605ccebfa7 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java @@ -0,0 +1,165 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import 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 Tony Vaagenes + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java new file mode 100644 index 00000000000..1e2686aa184 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java @@ -0,0 +1,88 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.NoopSharedResource; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.FastContentWriter; +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 com.yahoo.jdisc.http.servlet.ServletRequest; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.Map; +import java.util.Optional; + +import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; + +/** + * Resolve request/response filter (chain) based on {@link FilterBindings}. + * + * @author bjorncs + */ +class FilterResolver { + + private final FilterBindings bindings; + private final Metric metric; + private final boolean strictFiltering; + + FilterResolver(FilterBindings bindings, Metric metric, boolean strictFiltering) { + this.bindings = bindings; + this.metric = metric; + this.strictFiltering = strictFiltering; + } + + Optional resolveRequestFilter(HttpServletRequest servletRequest, URI jdiscUri) { + Optional maybeFilterId = bindings.resolveRequestFilter(jdiscUri, getConnector(servletRequest).listenPort()); + if (maybeFilterId.isPresent()) { + metric.add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, createMetricContext(servletRequest, maybeFilterId.get())); + servletRequest.setAttribute(ServletRequest.JDISC_REQUEST_CHAIN, maybeFilterId.get()); + } else if (!strictFiltering) { + metric.add(MetricDefinitions.FILTERING_REQUEST_UNHANDLED, 1L, createMetricContext(servletRequest, null)); + } else { + String syntheticFilterId = RejectingRequestFilter.SYNTHETIC_FILTER_CHAIN_ID; + metric.add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, createMetricContext(servletRequest, syntheticFilterId)); + servletRequest.setAttribute(ServletRequest.JDISC_REQUEST_CHAIN, syntheticFilterId); + return Optional.of(RejectingRequestFilter.INSTANCE); + } + return maybeFilterId.map(bindings::getRequestFilter); + } + + Optional resolveResponseFilter(HttpServletRequest servletRequest, URI jdiscUri) { + Optional maybeFilterId = bindings.resolveResponseFilter(jdiscUri, getConnector(servletRequest).listenPort()); + if (maybeFilterId.isPresent()) { + metric.add(MetricDefinitions.FILTERING_RESPONSE_HANDLED, 1L, createMetricContext(servletRequest, maybeFilterId.get())); + servletRequest.setAttribute(ServletRequest.JDISC_RESPONSE_CHAIN, maybeFilterId.get()); + } else { + metric.add(MetricDefinitions.FILTERING_RESPONSE_UNHANDLED, 1L, createMetricContext(servletRequest, null)); + } + return maybeFilterId.map(bindings::getResponseFilter); + } + + private Metric.Context createMetricContext(HttpServletRequest request, String filterId) { + Map extraDimensions = filterId != null + ? Map.of(MetricDefinitions.FILTER_CHAIN_ID_DIMENSION, filterId) + : Map.of(); + return JDiscHttpServlet.getConnector(request).createRequestMetricContext(request, extraDimensions); + } + + private static class RejectingRequestFilter extends NoopSharedResource implements RequestFilter { + + private static final RejectingRequestFilter INSTANCE = new RejectingRequestFilter(); + private static final String SYNTHETIC_FILTER_CHAIN_ID = "strict-reject"; + + @Override + public void filter(HttpRequest request, ResponseHandler handler) { + Response response = new Response(Response.Status.FORBIDDEN); + response.headers().add("Content-Type", "text/plain"); + try (FastContentWriter writer = ResponseDispatch.newInstance(response).connectFastWriter(handler)) { + writer.write("Request did not match any request filter chain"); + } + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java new file mode 100644 index 00000000000..de768f979a1 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java @@ -0,0 +1,134 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +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.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; + +import javax.servlet.http.HttpServletRequest; +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 Øyvind Bakksjø + */ +class FilteringRequestHandler extends AbstractRequestHandler { + + private static final ContentChannel COMPLETING_CONTENT_CHANNEL = new ContentChannel() { + + @Override + public void write(ByteBuffer buf, CompletionHandler handler) { + CompletionHandlers.tryComplete(handler); + } + + @Override + public void close(CompletionHandler handler) { + CompletionHandlers.tryComplete(handler); + } + + }; + + private final FilterResolver filterResolver; + private final HttpServletRequest servletRequest; + + public FilteringRequestHandler(FilterResolver filterResolver, HttpServletRequest servletRequest) { + this.filterResolver = filterResolver; + this.servletRequest = servletRequest; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler originalResponseHandler) { + Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); + Objects.requireNonNull(originalResponseHandler, "responseHandler"); + + RequestFilter requestFilter = filterResolver.resolveRequestFilter(servletRequest, request.getUri()) + .orElse(null); + ResponseFilter responseFilter = filterResolver.resolveResponseFilter(servletRequest, request.getUri()) + .orElse(null); + + // Not using request.connect() here - it adds logic for error handling that we'd rather leave to the framework. + RequestHandler resolvedRequestHandler = request.container().resolveHandler(request); + + if (resolvedRequestHandler == null) { + throw new BindingNotFoundException(request.getUri()); + } + + RequestHandler requestHandler = new ReferenceCountingRequestHandler(resolvedRequestHandler); + + ResponseHandler responseHandler; + if (responseFilter != null) { + responseHandler = new FilteringResponseHandler(originalResponseHandler, responseFilter, request); + } else { + responseHandler = originalResponseHandler; + } + + if (requestFilter != null) { + InterceptingResponseHandler interceptingResponseHandler = new InterceptingResponseHandler(responseHandler); + requestFilter.filter(HttpRequest.class.cast(request), interceptingResponseHandler); + if (interceptingResponseHandler.hasProducedResponse()) { + return COMPLETING_CONTENT_CHANNEL; + } + } + + 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(ResponseHandler delegate, ResponseFilter responseFilter, Request request) { + this.delegate = Objects.requireNonNull(delegate); + this.responseFilter = Objects.requireNonNull(responseFilter); + this.request = request; + } + + @Override + public ContentChannel handleResponse(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(ResponseHandler delegate) { + this.delegate = Objects.requireNonNull(delegate); + } + + @Override + public ContentChannel handleResponse(Response response) { + ContentChannel content = delegate.handleResponse(response); + hasResponded.set(true); + return content; + } + + public boolean hasProducedResponse() { + return hasResponded.get(); + } + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java new file mode 100644 index 00000000000..38f84438526 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java @@ -0,0 +1,188 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.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; +import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; + +/** + * 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 bakksjo + * $Id$ + */ +class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel { + + 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(); + } + + @SuppressWarnings("try") + @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> 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> parseFormParameters(final String formContent) { + if (formContent.isEmpty()) { + return Collections.emptyMap(); + } + + final Map> 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 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> source, + final Map> destination) { + for (Map.Entry> entry : source.entrySet()) { + final List destinationValues = destination.get(entry.getKey()); + if (destinationValues != null) { + destinationValues.addAll(entry.getValue()); + } else { + destination.put(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java new file mode 100644 index 00000000000..0f7ce77e4cd --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java @@ -0,0 +1,274 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.concurrent.DaemonThreadFactory; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.tls.TransportSecurityOptions; +import com.yahoo.security.tls.TransportSecurityUtils; +import com.yahoo.security.tls.TrustAllX509TrustManager; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.server.DetectorConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import javax.net.ssl.SSLContext; +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; + +/** + * A handler that proxies status.html health checks + * + * @author bjorncs + */ +class HealthCheckProxyHandler extends HandlerWrapper { + + private static final Logger log = Logger.getLogger(HealthCheckProxyHandler.class.getName()); + + private static final String HEALTH_CHECK_PATH = "/status.html"; + + private final Executor executor = Executors.newSingleThreadExecutor(new DaemonThreadFactory("health-check-proxy-client-")); + private final Map portToProxyTargetMapping; + + HealthCheckProxyHandler(List connectors) { + this.portToProxyTargetMapping = createPortToProxyTargetMapping(connectors); + } + + private static Map createPortToProxyTargetMapping(List connectors) { + var mapping = new HashMap(); + for (JDiscServerConnector connector : connectors) { + ConnectorConfig.HealthCheckProxy proxyConfig = connector.connectorConfig().healthCheckProxy(); + if (proxyConfig.enable()) { + Duration targetTimeout = Duration.ofMillis((int) (proxyConfig.clientTimeout() * 1000)); + mapping.put(connector.listenPort(), createProxyTarget(proxyConfig.port(), targetTimeout, connectors)); + log.info(String.format("Port %1$d is configured as a health check proxy for port %2$d. " + + "HTTP requests to '%3$s' on %1$d are proxied as HTTPS to %2$d.", + connector.listenPort(), proxyConfig.port(), HEALTH_CHECK_PATH)); + } + } + return mapping; + } + + private static ProxyTarget createProxyTarget(int targetPort, Duration targetTimeout, List connectors) { + JDiscServerConnector targetConnector = connectors.stream() + .filter(connector -> connector.listenPort() == targetPort) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Could not find any connector with listen port " + targetPort)); + SslContextFactory.Server sslContextFactory = + Optional.ofNullable(targetConnector.getConnectionFactory(SslConnectionFactory.class)) + .or(() -> Optional.ofNullable(targetConnector.getConnectionFactory(DetectorConnectionFactory.class)) + .map(detectorConnFactory -> detectorConnFactory.getBean(SslConnectionFactory.class))) + .map(connFactory -> (SslContextFactory.Server) connFactory.getSslContextFactory()) + .orElseThrow(() -> new IllegalArgumentException("Health check proxy can only target https port")); + return new ProxyTarget(targetPort, targetTimeout, sslContextFactory); + } + + @Override + public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { + int localPort = getConnectorLocalPort(servletRequest); + ProxyTarget proxyTarget = portToProxyTargetMapping.get(localPort); + if (proxyTarget != null) { + AsyncContext asyncContext = servletRequest.startAsync(); + ServletOutputStream out = servletResponse.getOutputStream(); + if (servletRequest.getRequestURI().equals(HEALTH_CHECK_PATH)) { + executor.execute(new ProxyRequestTask(asyncContext, proxyTarget, servletResponse, out)); + } else { + servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); + asyncContext.complete(); + } + request.setHandled(true); + } else { + _handler.handle(target, request, servletRequest, servletResponse); + } + } + + @Override + protected void doStop() throws Exception { + for (ProxyTarget target : portToProxyTargetMapping.values()) { + target.close(); + } + super.doStop(); + } + + private static class ProxyRequestTask implements Runnable { + + final AsyncContext asyncContext; + final ProxyTarget target; + final HttpServletResponse servletResponse; + final ServletOutputStream output; + + ProxyRequestTask(AsyncContext asyncContext, ProxyTarget target, HttpServletResponse servletResponse, ServletOutputStream output) { + this.asyncContext = asyncContext; + this.target = target; + this.servletResponse = servletResponse; + this.output = output; + } + + @Override + public void run() { + StatusResponse statusResponse = target.requestStatusHtml(); + servletResponse.setStatus(statusResponse.statusCode); + if (statusResponse.contentType != null) { + servletResponse.setHeader("Content-Type", statusResponse.contentType); + } + servletResponse.setHeader("Vespa-Health-Check-Proxy-Target", Integer.toString(target.port)); + output.setWriteListener(new WriteListener() { + @Override + public void onWritePossible() throws IOException { + if (output.isReady()) { + if (statusResponse.content != null) { + output.write(statusResponse.content); + } + asyncContext.complete(); + } + } + + @Override + public void onError(Throwable t) { + log.log(Level.FINE, t, () -> "Failed to write status response: " + t.getMessage()); + asyncContext.complete(); + } + }); + } + } + + private static class ProxyTarget implements AutoCloseable { + final int port; + final Duration timeout; + final SslContextFactory.Server sslContextFactory; + volatile CloseableHttpClient client; + volatile StatusResponse lastResponse; + + ProxyTarget(int port, Duration timeout, SslContextFactory.Server sslContextFactory) { + this.port = port; + this.timeout = timeout; + this.sslContextFactory = sslContextFactory; + } + + StatusResponse requestStatusHtml() { + StatusResponse response = lastResponse; + if (response != null && !response.isExpired()) { + return response; + } + return this.lastResponse = getStatusResponse(); + } + + private StatusResponse getStatusResponse() { + try (CloseableHttpResponse clientResponse = client().execute(new HttpGet("https://localhost:" + port + HEALTH_CHECK_PATH))) { + int statusCode = clientResponse.getStatusLine().getStatusCode(); + HttpEntity entity = clientResponse.getEntity(); + if (entity != null) { + Header contentTypeHeader = entity.getContentType(); + String contentType = contentTypeHeader != null ? contentTypeHeader.getValue() : null; + byte[] content = EntityUtils.toByteArray(entity); + return new StatusResponse(statusCode, contentType, content); + } else { + return new StatusResponse(statusCode, null, null); + } + } catch (Exception e) { + log.log(Level.FINE, e, () -> "Proxy request failed" + e.getMessage()); + return new StatusResponse(500, "text/plain", e.getMessage().getBytes()); + } + } + + // Client construction must be delayed to ensure that the SslContextFactory is started before calling getSslContext(). + private CloseableHttpClient client() { + if (client == null) { + synchronized (this) { + if (client == null) { + int timeoutMillis = (int) timeout.toMillis(); + client = HttpClientBuilder.create() + .disableAutomaticRetries() + .setMaxConnPerRoute(4) + .setSSLContext(getSslContext(sslContextFactory)) + .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // Certificate may not match "localhost" + .setUserTokenHandler(context -> null) // https://stackoverflow.com/a/42112034/1615280 + .setUserAgent("health-check-proxy-client") + .setDefaultRequestConfig( + RequestConfig.custom() + .setConnectTimeout(timeoutMillis) + .setConnectionRequestTimeout(timeoutMillis) + .setSocketTimeout(timeoutMillis) + .build()) + .build(); + } + } + } + return client; + } + + private SSLContext getSslContext(SslContextFactory.Server sslContextFactory) { + // A client certificate is only required if the server connector's ssl context factory is configured with "need-auth". + if (sslContextFactory.getNeedClientAuth()) { + log.info(String.format("Port %d requires client certificate - client will provide its node certificate", port)); + // We should ideally specify the client certificate through connector config, but the model has currently no knowledge of node certificate location on disk. + // Instead we assume that the server connector will accept its own node certificate. This will work for the current hosted use-case. + // The Vespa TLS config will provide us the location of certificate and key. + TransportSecurityOptions options = TransportSecurityUtils.getOptions() + .orElseThrow(() -> + new IllegalStateException("Vespa TLS configuration is required when using health check proxy to a port with client auth 'need'")); + return new SslContextBuilder() + .withKeyStore(options.getPrivateKeyFile().get(), options.getCertificatesFile().get()) + .withTrustManager(new TrustAllX509TrustManager()) + .build(); + } else { + log.info(String.format( + "Port %d does not require a client certificate - client will not provide a certificate", port)); + return new SslContextBuilder() + .withTrustManager(new TrustAllX509TrustManager()) + .build(); + } + } + + @Override + public void close() throws IOException { + synchronized (this) { + if (client != null) { + client.close(); + client = null; + } + } + } + } + + private static class StatusResponse { + final long createdAt = System.nanoTime(); + final int statusCode; + final String contentType; + final byte[] content; + + StatusResponse(int statusCode, String contentType, byte[] content) { + this.statusCode = statusCode; + this.contentType = contentType; + this.content = content; + } + + boolean isExpired() { return System.nanoTime() - createdAt > Duration.ofSeconds(1).toNanos(); } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java new file mode 100644 index 00000000000..05715b13d10 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java @@ -0,0 +1,243 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.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.ConnectorConfig; +import com.yahoo.jdisc.http.HttpHeaders; +import com.yahoo.jdisc.http.HttpRequest; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; + +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.time.Instant; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +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.HttpServletRequestUtils.getConnection; +import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; +import static com.yahoo.yolean.Exceptions.throwUnchecked; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +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 Request jettyRequest; + + private final ServletResponseController servletResponseController; + private final RequestHandler requestHandler; + private final RequestMetricReporter metricReporter; + + public HttpRequestDispatch(JDiscContext jDiscContext, + AccessLogEntry accessLogEntry, + Context metricContext, + HttpServletRequest servletRequest, + HttpServletResponse servletResponse) throws IOException { + this.jDiscContext = jDiscContext; + + requestHandler = newRequestHandler(jDiscContext, accessLogEntry, servletRequest); + + this.jettyRequest = (Request) servletRequest; + this.metricReporter = new RequestMetricReporter(jDiscContext.metric, metricContext, jettyRequest.getTimeStamp()); + this.servletResponseController = new ServletResponseController(servletRequest, + servletResponse, + jDiscContext.janitor, + metricReporter, + jDiscContext.developerMode()); + markConnectionAsNonPersistentIfThresholdReached(servletRequest); + this.async = servletRequest.startAsync(); + async.setTimeout(0); + metricReporter.uriLength(jettyRequest.getOriginalURI().length()); + } + + public void dispatch() throws IOException { + 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 final BiConsumer completeRequestCallback; + { + AtomicBoolean completeRequestCalled = new AtomicBoolean(false); + HttpRequestDispatch parent = this; //used to avoid binding uninitialized variables + + completeRequestCallback = (result, error) -> { + 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; + } + + boolean reportedError = false; + + if (error != null) { + if (isErrorOfType(error, EofException.class, IOException.class)) { + log.log(Level.FINE, + error, + () -> "Network connection was unexpectedly terminated: " + parent.jettyRequest.getRequestURI()); + parent.metricReporter.prematurelyClosed(); + } else if (!isErrorOfType(error, OverloadException.class, BindingNotFoundException.class, RequestException.class)) { + log.log(Level.WARNING, "Request failed: " + parent.jettyRequest.getRequestURI(), error); + } + reportedError = true; + parent.metricReporter.failedResponse(); + } else { + parent.metricReporter.successfulResponse(); + } + + try { + parent.async.complete(); + log.finest(() -> "Request completed successfully: " + parent.jettyRequest.getRequestURI()); + } catch (Throwable throwable) { + Level level = reportedError ? Level.FINE: Level.WARNING; + log.log(level, "Async.complete failed", throwable); + } + }; + } + + private static void markConnectionAsNonPersistentIfThresholdReached(HttpServletRequest request) { + ConnectorConfig connectorConfig = getConnector(request).connectorConfig(); + int maxRequestsPerConnection = connectorConfig.maxRequestsPerConnection(); + if (maxRequestsPerConnection > 0) { + HttpConnection connection = getConnection(request); + if (connection.getMessagesIn() >= maxRequestsPerConnection) { + connection.getGenerator().setPersistent(false); + } + } + double maxConnectionLifeInSeconds = connectorConfig.maxConnectionLife(); + if (maxConnectionLifeInSeconds > 0) { + HttpConnection connection = getConnection(request); + Instant expireAt = Instant.ofEpochMilli((long)(connection.getCreatedTimeStamp() + maxConnectionLifeInSeconds * 1000)); + if (Instant.now().isAfter(expireAt)) { + connection.getGenerator().setPersistent(false); + } + } + } + + @SafeVarargs + @SuppressWarnings("varargs") + private static boolean isErrorOfType(Throwable throwable, Class... handledTypes) { + return Arrays.stream(handledTypes) + .anyMatch( + exceptionType -> exceptionType.isInstance(throwable) + || throwable instanceof CompletionException && exceptionType.isInstance(throwable.getCause())); + } + + @SuppressWarnings("try") + private ServletRequestReader handleRequest() throws IOException { + HttpRequest jdiscRequest = HttpRequestFactory.newJDiscRequest(jDiscContext.container, jettyRequest); + ContentChannel requestContentChannel; + + try (ResourceReference ref = References.fromResource(jdiscRequest)) { + HttpRequestFactory.copyHeaders(jettyRequest, jdiscRequest); + requestContentChannel = requestHandler.handleRequest(jdiscRequest, servletResponseController.responseHandler); + } + + ServletInputStream servletInputStream = jettyRequest.getInputStream(); + + ServletRequestReader servletRequestReader = new ServletRequestReader(servletInputStream, + requestContentChannel, + jDiscContext.janitor, + metricReporter); + + servletInputStream.setReadListener(servletRequestReader); + return servletRequestReader; + } + + private static void onError(CompletableFuture future, Consumer errorHandler) { + future.whenComplete((result, exception) -> { + if (exception != null) { + errorHandler.accept(exception); + } + }); + } + + ContentChannel handleRequestFilterResponse(Response response) { + try { + jettyRequest.getInputStream().close(); + ContentChannel responseContentChannel = servletResponseController.responseHandler.handleResponse(response); + servletResponseController.finishedFuture().whenComplete(completeRequestCallback); + return responseContentChannel; + } catch (IOException e) { + throw throwUnchecked(e); + } + } + + + private static RequestHandler newRequestHandler(JDiscContext context, + AccessLogEntry accessLogEntry, + HttpServletRequest servletRequest) { + RequestHandler requestHandler = wrapHandlerIfFormPost( + new FilteringRequestHandler(context.filterResolver, servletRequest), + servletRequest, context.serverConfig.removeRawPostBodyForWwwUrlEncodedPost()); + + return new AccessLoggingRequestHandler(requestHandler, accessLogEntry); + } + + private static RequestHandler wrapHandlerIfFormPost(RequestHandler requestHandler, + HttpServletRequest servletRequest, + boolean removeBodyForFormPost) { + if (!servletRequest.getMethod().equals("POST")) { + return requestHandler; + } + 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(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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java new file mode 100644 index 00000000000..e8d37cfadb5 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java @@ -0,0 +1,87 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.util.Utf8Appendable; + +import javax.servlet.http.HttpServletRequest; +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + +import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +class HttpRequestFactory { + + public static HttpRequest newJDiscRequest(CurrentContainer container, HttpServletRequest servletRequest) { + try { + HttpRequest httpRequest = HttpRequest.newServerRequest( + container, + getUri(servletRequest), + HttpRequest.Method.valueOf(servletRequest.getMethod()), + HttpRequest.Version.fromString(servletRequest.getProtocol()), + new InetSocketAddress(servletRequest.getRemoteAddr(), servletRequest.getRemotePort()), + getConnection(servletRequest).getCreatedTimeStamp()); + httpRequest.context().put(ServletRequest.JDISC_REQUEST_X509CERT, getCertChain(servletRequest)); + return httpRequest; + } catch (Utf8Appendable.NotUtf8Exception e) { + throw createBadQueryException(e); + } + } + + // Implementation based on org.eclipse.jetty.server.Request.getRequestURL(), but with the connector's local port instead + public static URI getUri(HttpServletRequest servletRequest) { + try { + String scheme = servletRequest.getScheme(); + String host = servletRequest.getServerName(); + int port = getConnectorLocalPort(servletRequest); + String path = servletRequest.getRequestURI(); + String query = servletRequest.getQueryString(); + + URI uri = URI.create(scheme + "://" + + host + ":" + port + + (path != null ? path : "") + + (query != null ? "?" + query : "")); + + validateSchemeHostPort(scheme, host, port, uri); + return uri; + } + catch (IllegalArgumentException e) { + throw createBadQueryException(e); + } + } + + private static void validateSchemeHostPort(String scheme, String host, int port, URI uri) { + if ( ! scheme.equals(uri.getScheme())) + throw new IllegalArgumentException("Bad scheme: " + scheme); + + if ( ! host.equals(uri.getHost()) || port != uri.getPort()) + throw new IllegalArgumentException("Bad authority: " + uri.getRawAuthority() + " != " + host + ":" + port); + } + + private static RequestException createBadQueryException(IllegalArgumentException e) { + return new RequestException(BAD_REQUEST, "URL violates RFC 2396: " + e.getMessage(), e); + } + + public static void copyHeaders(HttpServletRequest from, HttpRequest to) { + for (Enumeration it = from.getHeaderNames(); it.hasMoreElements(); ) { + String key = it.nextElement(); + for (Enumeration value = from.getHeaders(key); value.hasMoreElements(); ) { + to.headers().add(key, value.nextElement()); + } + } + } + + private static X509Certificate[] getCertChain(HttpServletRequest servletRequest) { + return (X509Certificate[]) servletRequest.getAttribute("javax.servlet.request.X509Certificate"); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java new file mode 100644 index 00000000000..82c445c7ca9 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java @@ -0,0 +1,300 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.HttpRequest; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.AsyncContextEvent; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpChannelState; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.component.Graceful; + +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; + +/** + * HttpResponseStatisticsCollector collects statistics about HTTP response types aggregated by category + * (1xx, 2xx, etc). It is similar to {@link org.eclipse.jetty.server.handler.StatisticsHandler} + * with the distinction that this class collects response type statistics grouped + * by HTTP method and only collects the numbers that are reported as metrics from Vespa. + * + * @author ollivir + */ +public class HttpResponseStatisticsCollector extends HandlerWrapper implements Graceful { + + static final String requestTypeAttribute = "requestType"; + + private final AtomicReference shutdown = new AtomicReference<>(); + private final List monitoringHandlerPaths; + private final List searchHandlerPaths; + + public enum HttpMethod { + GET, PATCH, POST, PUT, DELETE, OPTIONS, HEAD, OTHER + } + + public enum HttpScheme { + HTTP, HTTPS, OTHER + } + + private static final String[] HTTP_RESPONSE_GROUPS = { + MetricDefinitions.RESPONSES_1XX, + MetricDefinitions.RESPONSES_2XX, + MetricDefinitions.RESPONSES_3XX, + MetricDefinitions.RESPONSES_4XX, + MetricDefinitions.RESPONSES_5XX, + MetricDefinitions.RESPONSES_401, + MetricDefinitions.RESPONSES_403 + }; + + private final AtomicLong inFlight = new AtomicLong(); + private final LongAdder[][][][] statistics; + + public HttpResponseStatisticsCollector(List monitoringHandlerPaths, List searchHandlerPaths) { + this.monitoringHandlerPaths = monitoringHandlerPaths; + this.searchHandlerPaths = searchHandlerPaths; + statistics = new LongAdder[HttpScheme.values().length][HttpMethod.values().length][][]; + for (int scheme = 0; scheme < HttpScheme.values().length; ++scheme) { + for (int method = 0; method < HttpMethod.values().length; method++) { + statistics[scheme][method] = new LongAdder[HTTP_RESPONSE_GROUPS.length][]; + for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { + statistics[scheme][method][group] = new LongAdder[HttpRequest.RequestType.values().length]; + for (int requestType = 0; requestType < HttpRequest.RequestType.values().length; requestType++) { + statistics[scheme][method][group][requestType] = new LongAdder(); + } + } + } + } + } + + private final AsyncListener completionWatcher = new AsyncListener() { + + @Override + public void onTimeout(AsyncEvent event) { } + + @Override + public void onStartAsync(AsyncEvent event) { + event.getAsyncContext().addListener(this); + } + + @Override + public void onError(AsyncEvent event) { } + + @Override + public void onComplete(AsyncEvent event) throws IOException { + HttpChannelState state = ((AsyncContextEvent) event).getHttpChannelState(); + Request request = state.getBaseRequest(); + + observeEndOfRequest(request, null); + } + }; + + @Override + public void handle(String path, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + inFlight.incrementAndGet(); + + try { + Handler handler = getHandler(); + if (handler != null && shutdown.get() == null && isStarted()) { + handler.handle(path, baseRequest, request, response); + } else if ( ! baseRequest.isHandled()) { + baseRequest.setHandled(true); + response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + } + } finally { + HttpChannelState state = baseRequest.getHttpChannelState(); + if (state.isSuspended()) { + if (state.isInitial()) { + state.addListener(completionWatcher); + } + } else if (state.isInitial()) { + observeEndOfRequest(baseRequest, response); + } + } + } + + private void observeEndOfRequest(Request request, HttpServletResponse flushableResponse) throws IOException { + int group = groupIndex(request); + if (group >= 0) { + HttpScheme scheme = getScheme(request); + HttpMethod method = getMethod(request); + HttpRequest.RequestType requestType = getRequestType(request); + + statistics[scheme.ordinal()][method.ordinal()][group][requestType.ordinal()].increment(); + if (group == 5 || group == 6) { // if 401/403, also increment 4xx + statistics[scheme.ordinal()][method.ordinal()][3][requestType.ordinal()].increment(); + } + } + + long live = inFlight.decrementAndGet(); + FutureCallback shutdownCb = shutdown.get(); + if (shutdownCb != null) { + if (flushableResponse != null) { + flushableResponse.flushBuffer(); + } + if (live == 0) { + shutdownCb.succeeded(); + } + } + } + + private int groupIndex(Request request) { + int index = request.getResponse().getStatus(); + if (index == 401) { + return 5; + } + if (index == 403) { + return 6; + } + + index = index / 100 - 1; // 1xx = 0, 2xx = 1 etc. + if (index < 0 || index >= statistics[0].length) { + return -1; + } else { + return index; + } + } + + private HttpScheme getScheme(Request request) { + switch (request.getScheme()) { + case "http": + return HttpScheme.HTTP; + case "https": + return HttpScheme.HTTPS; + default: + return HttpScheme.OTHER; + } + } + + private HttpMethod getMethod(Request request) { + switch (request.getMethod()) { + case "GET": + return HttpMethod.GET; + case "PATCH": + return HttpMethod.PATCH; + case "POST": + return HttpMethod.POST; + case "PUT": + return HttpMethod.PUT; + case "DELETE": + return HttpMethod.DELETE; + case "OPTIONS": + return HttpMethod.OPTIONS; + case "HEAD": + return HttpMethod.HEAD; + default: + return HttpMethod.OTHER; + } + } + + private HttpRequest.RequestType getRequestType(Request request) { + HttpRequest.RequestType requestType = (HttpRequest.RequestType)request.getAttribute(requestTypeAttribute); + if (requestType != null) return requestType; + + // Deduce from path and method: + String path = request.getRequestURI(); + for (String monitoringHandlerPath : monitoringHandlerPaths) { + if (path.startsWith(monitoringHandlerPath)) return HttpRequest.RequestType.MONITORING; + } + for (String searchHandlerPath : searchHandlerPaths) { + if (path.startsWith(searchHandlerPath)) return HttpRequest.RequestType.READ; + } + if ("GET".equals(request.getMethod())) { + return HttpRequest.RequestType.READ; + } else { + return HttpRequest.RequestType.WRITE; + } + } + + public List takeStatistics() { + var ret = new ArrayList(); + for (HttpScheme scheme : HttpScheme.values()) { + int schemeIndex = scheme.ordinal(); + for (HttpMethod method : HttpMethod.values()) { + int methodIndex = method.ordinal(); + for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { + for (HttpRequest.RequestType type : HttpRequest.RequestType.values()) { + long value = statistics[schemeIndex][methodIndex][group][type.ordinal()].sumThenReset(); + if (value > 0) { + ret.add(new StatisticsEntry(scheme.name().toLowerCase(), method.name(), HTTP_RESPONSE_GROUPS[group], type.name().toLowerCase(), value)); + } + } + } + } + } + return ret; + } + + @Override + protected void doStart() throws Exception { + shutdown.set(null); + super.doStart(); + } + + @Override + protected void doStop() throws Exception { + super.doStop(); + FutureCallback shutdownCb = shutdown.get(); + if ( ! shutdownCb.isDone()) { + shutdownCb.failed(new TimeoutException()); + } + } + + @Override + public Future shutdown() { + FutureCallback shutdownCb = new FutureCallback(false); + shutdown.compareAndSet(null, shutdownCb); + shutdownCb = shutdown.get(); + if (inFlight.get() == 0) { + shutdownCb.succeeded(); + } + return shutdownCb; + } + + @Override + public boolean isShutdown() { + FutureCallback futureCallback = shutdown.get(); + return futureCallback != null && futureCallback.isDone(); + } + + public static class StatisticsEntry { + + public final String scheme; + public final String method; + public final String name; + public final String requestType; + public final long value; + + public StatisticsEntry(String scheme, String method, String name, String requestType, long value) { + this.scheme = scheme; + this.method = method; + this.name = name; + this.requestType = requestType; + this.value = value; + } + + @Override + public String toString() { + return "scheme: " + scheme + + ", method: " + method + + ", name: " + name + + ", requestType: " + requestType + + ", value: " + value; + } + + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java new file mode 100644 index 00000000000..e7b9f459d2e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java @@ -0,0 +1,38 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.eclipse.jetty.server.HttpConnection; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author bjorncs + */ +public class HttpServletRequestUtils { + private HttpServletRequestUtils() {} + + public static HttpConnection getConnection(HttpServletRequest request) { + return (HttpConnection)request.getAttribute("org.eclipse.jetty.server.HttpConnection"); + } + + /** + * Note: {@link HttpServletRequest#getLocalPort()} may return the local port of the load balancer / reverse proxy if proxy-protocol is enabled. + * @return the actual local port of the underlying Jetty connector + */ + public static int getConnectorLocalPort(HttpServletRequest request) { + JDiscServerConnector connector = (JDiscServerConnector) getConnection(request).getConnector(); + int actualLocalPort = connector.getLocalPort(); + int localPortIfConnectorUnopened = -1; + int localPortIfConnectorClosed = -2; + if (actualLocalPort == localPortIfConnectorUnopened || actualLocalPort == localPortIfConnectorClosed) { + int configuredLocalPort = connector.listenPort(); + int localPortEphemeralPort = 0; + if (configuredLocalPort == localPortEphemeralPort) { + throw new IllegalStateException("Unable to determine connector's listen port"); + } + return configuredLocalPort; + } + return actualLocalPort; + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java new file mode 100644 index 00000000000..b37a7352dc6 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java @@ -0,0 +1,33 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.util.concurrent.Executor; + +public class JDiscContext { + final FilterResolver filterResolver; + final CurrentContainer container; + final Executor janitor; + final Metric metric; + final ServerConfig serverConfig; + + public JDiscContext(FilterBindings filterBindings, + CurrentContainer container, + Executor janitor, + Metric metric, + ServerConfig serverConfig) { + + this.filterResolver = new FilterResolver(filterBindings, metric, serverConfig.strictFiltering()); + this.container = container; + this.janitor = janitor; + this.metric = metric; + this.serverConfig = serverConfig; + } + + public boolean developerMode() { + return serverConfig.developerMode(); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java new file mode 100644 index 00000000000..a89c115a1c2 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java @@ -0,0 +1,294 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.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.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; +import static com.yahoo.yolean.Exceptions.throwUnchecked; + +/** + * Runs JDisc security filters for Servlets + * This component is split in two: + * 1) JDiscFilterInvokerFilter, which uses package private methods to support JDisc APIs + * 2) SecurityFilterInvoker, which is intended for use in a servlet context. + * + * @author Tony Vaagenes + */ +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; + try { + uri = HttpRequestFactory.getUri(httpRequest); + } catch (RequestException e) { + httpResponse.sendError(e.getResponseStatus(), e.getMessage()); + return; + } + + AtomicReference 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 responseFilterInvoker = + jDiscContext.filterResolver.resolveResponseFilter(request, uri) + .map(responseFilter -> + new OneTimeRunnable(() -> + filterInvoker.invokeResponseFilterChain(responseFilter, uri, request, response))); + + + HttpServletResponse responseForServlet = responseFilterInvoker + .map(invoker -> + new FilterInvokingResponseWrapper(response, invoker)) + .orElse(response); + + HttpServletRequest requestForServlet = responseFilterInvoker + .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 responseReturned, URI uri, HttpServletRequest request, HttpServletResponse response) throws IOException { + try { + RequestFilter requestFilter = jDiscContext.filterResolver.resolveRequestFilter(request, uri).orElse(null); + 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 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).createRequestMetricContext(request, Map.of()), + 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 createListener(Class 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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java new file mode 100644 index 00000000000..41a1ffc2709 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java @@ -0,0 +1,148 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.AccessLogEntry; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.handler.OverloadException; +import com.yahoo.jdisc.http.HttpRequest.Method; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +@WebServlet(asyncSupported = true, description = "Bridge between Servlet and JDisc APIs") +class JDiscHttpServlet extends HttpServlet { + + 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; + + private static final Set servletSupportedMethods = + Stream.of(Method.OPTIONS, Method.GET, Method.HEAD, Method.POST, Method.PUT, Method.DELETE, Method.TRACE) + .map(Method::name) + .collect(Collectors.toSet()); + + public JDiscHttpServlet(JDiscContext context) { + this.context = context; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + @Override + protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws IOException { + dispatchHttpRequest(request, response); + } + + /** + * 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(MetricDefinitions.NUM_REQUESTS, 1, metricContext); + context.metric.add(MetricDefinitions.JDISC_HTTP_REQUESTS, 1, metricContext); + + String method = request.getMethod().toUpperCase(); + if (servletSupportedMethods.contains(method)) { + super.service(request, response); + } else if (method.equals(Method.PATCH.name())) { + // PATCH method is not handled by the Servlet spec + dispatchHttpRequest(request, response); + } else { + // Divergence from HTTP / Servlet spec: JDisc returns 405 for both unknown and known (but unsupported) methods. + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + } + } + + static JDiscServerConnector getConnector(HttpServletRequest request) { + return (JDiscServerConnector)getConnection(request).getConnector(); + } + + private void dispatchHttpRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { + AccessLogEntry accessLogEntry = new 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 static Metric.Context getMetricContext(HttpServletRequest request) { + return JDiscServerConnector.fromRequest(request).createRequestMetricContext(request, Map.of()); + } + + private static String formatAttributes(final HttpServletRequest request) { + StringBuilder out = new StringBuilder(); + out.append("attributes = {"); + for (Enumeration 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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java new file mode 100644 index 00000000000..99d0c5c8d8c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java @@ -0,0 +1,104 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ConnectorConfig; +import org.eclipse.jetty.io.ConnectionStatistics; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import java.net.Socket; +import java.net.SocketException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * @author bjorncs + */ +class JDiscServerConnector extends ServerConnector { + + public static final String REQUEST_ATTRIBUTE = JDiscServerConnector.class.getName(); + private final Metric.Context metricCtx; + private final ConnectionStatistics statistics; + private final ConnectorConfig config; + private final boolean tcpKeepAlive; + private final boolean tcpNoDelay; + private final Metric metric; + private final String connectorName; + private final int listenPort; + + JDiscServerConnector(ConnectorConfig config, Metric metric, Server server, JettyConnectionLogger connectionLogger, ConnectionFactory... factories) { + super(server, factories); + this.config = config; + this.tcpKeepAlive = config.tcpKeepAliveEnabled(); + this.tcpNoDelay = config.tcpNoDelay(); + this.metric = metric; + this.connectorName = config.name(); + this.listenPort = config.listenPort(); + this.metricCtx = metric.createContext(createConnectorDimensions(listenPort, connectorName)); + + this.statistics = new ConnectionStatistics(); + addBean(statistics); + ConnectorConfig.Throttling throttlingConfig = config.throttling(); + if (throttlingConfig.enabled()) { + new ConnectionThrottler(this, throttlingConfig).registerWithConnector(); + } + addBean(connectionLogger); + } + + @Override + protected void configure(final Socket socket) { + super.configure(socket); + try { + socket.setKeepAlive(tcpKeepAlive); + socket.setTcpNoDelay(tcpNoDelay); + } catch (SocketException ignored) { + } + } + + public ConnectionStatistics getStatistics() { + return statistics; + } + + public Metric.Context getConnectorMetricContext() { + return metricCtx; + } + + public Metric.Context createRequestMetricContext(HttpServletRequest request, Map extraDimensions) { + String method = request.getMethod(); + String scheme = request.getScheme(); + boolean clientAuthenticated = request.getAttribute(com.yahoo.jdisc.http.servlet.ServletRequest.SERVLET_REQUEST_X509CERT) != null; + Map dimensions = createConnectorDimensions(listenPort, connectorName); + dimensions.put(MetricDefinitions.METHOD_DIMENSION, method); + dimensions.put(MetricDefinitions.SCHEME_DIMENSION, scheme); + dimensions.put(MetricDefinitions.CLIENT_AUTHENTICATED_DIMENSION, Boolean.toString(clientAuthenticated)); + String serverName = Optional.ofNullable(request.getServerName()).orElse("unknown"); + dimensions.put(MetricDefinitions.REQUEST_SERVER_NAME_DIMENSION, serverName); + dimensions.putAll(extraDimensions); + return metric.createContext(dimensions); + } + + public static JDiscServerConnector fromRequest(ServletRequest request) { + return (JDiscServerConnector) request.getAttribute(REQUEST_ATTRIBUTE); + } + + ConnectorConfig connectorConfig() { + return config; + } + + int listenPort() { + return listenPort; + } + + private static Map createConnectorDimensions(int listenPort, String connectorName) { + Map props = new HashMap<>(); + props.put(MetricDefinitions.NAME_DIMENSION, connectorName); + props.put(MetricDefinitions.PORT_DIMENSION, listenPort); + return props; + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java new file mode 100644 index 00000000000..cd1ca490f61 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java @@ -0,0 +1,373 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; +import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; +import com.yahoo.io.HexDump; +import com.yahoo.jdisc.http.ServerConfig; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.ProxyConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +import javax.net.ssl.ExtendedSSLSession; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.StandardConstants; +import java.net.InetSocketAddress; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Jetty integration for jdisc connection log ({@link ConnectionLog}). + * + * @author bjorncs + */ +class JettyConnectionLogger extends AbstractLifeCycle implements Connection.Listener, HttpChannel.Listener, SslHandshakeListener { + + static final String CONNECTION_ID_REQUEST_ATTRIBUTE = "jdisc.request.connection.id"; + + private static final Logger log = Logger.getLogger(JettyConnectionLogger.class.getName()); + + private final ConcurrentMap, ConnectionInfo> connectionInfo = new ConcurrentHashMap<>(); + private final ConcurrentMap, ConnectionInfo> sslToConnectionInfo = new ConcurrentHashMap<>(); + + private final boolean enabled; + private final ConnectionLog connectionLog; + + JettyConnectionLogger(ServerConfig.ConnectionLog config, ConnectionLog connectionLog) { + this.enabled = config.enabled(); + this.connectionLog = connectionLog; + log.log(Level.FINE, () -> "Jetty connection logger is " + (config.enabled() ? "enabled" : "disabled")); + } + + // + // AbstractLifeCycle methods start + // + @Override + protected void doStop() { + handleListenerInvocation("AbstractLifeCycle", "doStop", "", List.of(), () -> { + log.log(Level.FINE, () -> "Jetty connection logger is stopped"); + }); + } + + @Override + protected void doStart() { + handleListenerInvocation("AbstractLifeCycle", "doStart", "", List.of(), () -> { + log.log(Level.FINE, () -> "Jetty connection logger is started"); + }); + } + // + // AbstractLifeCycle methods stop + // + + // + // Connection.Listener methods start + // + @Override + public void onOpened(Connection connection) { + handleListenerInvocation("Connection.Listener", "onOpened", "%h", List.of(connection), () -> { + SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint()); + var endpointKey = IdentityKey.of(endpoint); + ConnectionInfo info = connectionInfo.get(endpointKey); + if (info == null) { + info = ConnectionInfo.from(endpoint); + connectionInfo.put(IdentityKey.of(endpoint), info); + } + if (connection instanceof SslConnection) { + SSLEngine sslEngine = ((SslConnection) connection).getSSLEngine(); + sslToConnectionInfo.put(IdentityKey.of(sslEngine), info); + } + if (connection.getEndPoint() instanceof ProxyConnectionFactory.ProxyEndPoint) { + InetSocketAddress remoteAddress = connection.getEndPoint().getRemoteAddress(); + info.setRemoteAddress(remoteAddress); + } + }); + } + + @Override + public void onClosed(Connection connection) { + handleListenerInvocation("Connection.Listener", "onClosed", "%h", List.of(connection), () -> { + SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint()); + var endpointKey = IdentityKey.of(endpoint); + ConnectionInfo info = connectionInfo.get(endpointKey); + if (info == null) return; // Closed connection already handled + if (connection instanceof HttpConnection) { + info.setHttpBytes(connection.getBytesIn(), connection.getBytesOut()); + } + if (!endpoint.isOpen()) { + info.setClosedAt(System.currentTimeMillis()); + connectionLog.log(info.toLogEntry()); + connectionInfo.remove(endpointKey); + } + }); + } + // + // Connection.Listener methods end + // + + // + // HttpChannel.Listener methods start + // + @Override + public void onRequestBegin(Request request) { + handleListenerInvocation("HttpChannel.Listener", "onRequestBegin", "%h", List.of(request), () -> { + SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint()); + ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint))); + info.incrementRequests(); + request.setAttribute(CONNECTION_ID_REQUEST_ATTRIBUTE, info.uuid()); + }); + } + + @Override + public void onResponseBegin(Request request) { + handleListenerInvocation("HttpChannel.Listener", "onResponseBegin", "%h", List.of(request), () -> { + SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint()); + ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint))); + info.incrementResponses(); + }); + } + // + // HttpChannel.Listener methods end + // + + // + // SslHandshakeListener methods start + // + @Override + public void handshakeSucceeded(Event event) { + SSLEngine sslEngine = event.getSSLEngine(); + handleListenerInvocation("SslHandshakeListener", "handshakeSucceeded", "sslEngine=%h", List.of(sslEngine), () -> { + ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine)); + info.setSslSessionDetails(sslEngine.getSession()); + }); + } + + @Override + public void handshakeFailed(Event event, Throwable failure) { + SSLEngine sslEngine = event.getSSLEngine(); + handleListenerInvocation("SslHandshakeListener", "handshakeFailed", "sslEngine=%h,failure=%s", List.of(sslEngine, failure), () -> { + log.log(Level.FINE, failure, failure::toString); + ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine)); + info.setSslHandshakeFailure((SSLHandshakeException)failure); + }); + } + // + // SslHandshakeListener methods end + // + + private void handleListenerInvocation( + String listenerType, String methodName, String methodArgumentsFormat, List methodArguments, ListenerHandler handler) { + if (!enabled) return; + try { + log.log(Level.FINE, () -> String.format(listenerType + "." + methodName + "(" + methodArgumentsFormat + ")", methodArguments.toArray())); + handler.run(); + } catch (Exception e) { + log.log(Level.WARNING, String.format("Exception in %s.%s listener: %s", listenerType, methodName, e.getMessage()), e); + } + } + + /** + * Protocol layers are connected through each {@link Connection}'s {@link EndPoint} reference. + * This methods iterates through the endpoints recursively to find the underlying socket endpoint. + */ + private static SocketChannelEndPoint findUnderlyingSocketEndpoint(EndPoint endpoint) { + if (endpoint instanceof SocketChannelEndPoint) { + return (SocketChannelEndPoint) endpoint; + } else if (endpoint instanceof SslConnection.DecryptedEndPoint) { + var decryptedEndpoint = (SslConnection.DecryptedEndPoint) endpoint; + return findUnderlyingSocketEndpoint(decryptedEndpoint.getSslConnection().getEndPoint()); + } else if (endpoint instanceof ProxyConnectionFactory.ProxyEndPoint) { + var proxyEndpoint = (ProxyConnectionFactory.ProxyEndPoint) endpoint; + return findUnderlyingSocketEndpoint(proxyEndpoint.unwrap()); + } else { + throw new IllegalArgumentException("Unknown connection endpoint type: " + endpoint.getClass().getName()); + } + } + + @FunctionalInterface private interface ListenerHandler { void run() throws Exception; } + + private static class ConnectionInfo { + private final UUID uuid; + private final long createdAt; + private final InetSocketAddress localAddress; + private final InetSocketAddress peerAddress; + + private long closedAt = 0; + private long httpBytesReceived = 0; + private long httpBytesSent = 0; + private long requests = 0; + private long responses = 0; + private InetSocketAddress remoteAddress; + private byte[] sslSessionId; + private String sslProtocol; + private String sslCipherSuite; + private String sslPeerSubject; + private Date sslPeerNotBefore; + private Date sslPeerNotAfter; + private List sslSniServerNames; + private SSLHandshakeException sslHandshakeException; + + private ConnectionInfo(UUID uuid, long createdAt, InetSocketAddress localAddress, InetSocketAddress peerAddress) { + this.uuid = uuid; + this.createdAt = createdAt; + this.localAddress = localAddress; + this.peerAddress = peerAddress; + } + + static ConnectionInfo from(SocketChannelEndPoint endpoint) { + return new ConnectionInfo( + UUID.randomUUID(), + endpoint.getCreatedTimeStamp(), + endpoint.getLocalAddress(), + endpoint.getRemoteAddress()); + } + + synchronized UUID uuid() { return uuid; } + + synchronized ConnectionInfo setClosedAt(long closedAt) { + this.closedAt = closedAt; + return this; + } + + synchronized ConnectionInfo setHttpBytes(long received, long sent) { + this.httpBytesReceived = received; + this.httpBytesSent = sent; + return this; + } + + synchronized ConnectionInfo incrementRequests() { ++this.requests; return this; } + + synchronized ConnectionInfo incrementResponses() { ++this.responses; return this; } + + synchronized ConnectionInfo setRemoteAddress(InetSocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + + synchronized ConnectionInfo setSslSessionDetails(SSLSession session) { + this.sslCipherSuite = session.getCipherSuite(); + this.sslProtocol = session.getProtocol(); + this.sslSessionId = session.getId(); + if (session instanceof ExtendedSSLSession) { + ExtendedSSLSession extendedSession = (ExtendedSSLSession) session; + this.sslSniServerNames = extendedSession.getRequestedServerNames(); + } + try { + this.sslPeerSubject = session.getPeerPrincipal().getName(); + X509Certificate peerCertificate = (X509Certificate) session.getPeerCertificates()[0]; + this.sslPeerNotBefore = peerCertificate.getNotBefore(); + this.sslPeerNotAfter = peerCertificate.getNotAfter(); + } catch (SSLPeerUnverifiedException e) { + // Throw if peer is not authenticated (e.g when client auth is disabled) + // JSSE provides no means of checking for client authentication without catching this exception + } + return this; + } + + synchronized ConnectionInfo setSslHandshakeFailure(SSLHandshakeException exception) { + this.sslHandshakeException = exception; + return this; + } + + synchronized ConnectionLogEntry toLogEntry() { + ConnectionLogEntry.Builder builder = ConnectionLogEntry.builder(uuid, Instant.ofEpochMilli(createdAt)); + if (closedAt > 0) { + builder.withDuration((closedAt - createdAt) / 1000D); + } + if (httpBytesReceived > 0) { + builder.withHttpBytesReceived(httpBytesReceived); + } + if (httpBytesSent > 0) { + builder.withHttpBytesSent(httpBytesSent); + } + if (requests > 0) { + builder.withRequests(requests); + } + if (responses > 0) { + builder.withResponses(responses); + } + if (peerAddress != null) { + builder.withPeerAddress(peerAddress.getHostString()) + .withPeerPort(peerAddress.getPort()); + } + if (localAddress != null) { + builder.withLocalAddress(localAddress.getHostString()) + .withLocalPort(localAddress.getPort()); + } + if (remoteAddress != null) { + builder.withRemoteAddress(remoteAddress.getHostString()) + .withRemotePort(remoteAddress.getPort()); + } + if (sslProtocol != null && sslCipherSuite != null && sslSessionId != null) { + builder.withSslProtocol(sslProtocol) + .withSslCipherSuite(sslCipherSuite) + .withSslSessionId(HexDump.toHexString(sslSessionId)); + } + if (sslSniServerNames != null) { + sslSniServerNames.stream() + .filter(name -> name instanceof SNIHostName && name.getType() == StandardConstants.SNI_HOST_NAME) + .map(name -> ((SNIHostName) name).getAsciiName()) + .findAny() + .ifPresent(builder::withSslSniServerName); + } + if (sslPeerSubject != null && sslPeerNotAfter != null && sslPeerNotBefore != null) { + builder.withSslPeerSubject(sslPeerSubject) + .withSslPeerNotAfter(sslPeerNotAfter.toInstant()) + .withSslPeerNotBefore(sslPeerNotBefore.toInstant()); + } + if (sslHandshakeException != null) { + List exceptionChain = new ArrayList<>(); + Throwable cause = sslHandshakeException; + while (cause != null) { + exceptionChain.add(new ExceptionEntry(cause.getClass().getName(), cause.getMessage())); + cause = cause.getCause(); + } + String type = SslHandshakeFailure.fromSslHandshakeException(sslHandshakeException) + .map(SslHandshakeFailure::failureType) + .orElse("UNKNOWN"); + builder.withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure(type, exceptionChain)); + } + return builder.build(); + } + + } + + private static class IdentityKey { + final T instance; + + IdentityKey(T instance) { this.instance = instance; } + + static IdentityKey of(T instance) { return new IdentityKey<>(instance); } + + @Override public int hashCode() { return System.identityHashCode(instance); } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof IdentityKey)) return false; + IdentityKey other = (IdentityKey) obj; + return this.instance == other.instance; + } + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java new file mode 100644 index 00000000000..510c561c10f --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java @@ -0,0 +1,298 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Inject; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.concurrent.DaemonThreadFactory; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.service.AbstractServerProvider; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.jmx.ConnectorServer; +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.server.handler.gzip.GzipHttpOutputInterceptor; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.log.JavaUtilLog; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import javax.management.remote.JMXServiceURL; +import javax.servlet.DispatcherType; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.BindException; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class JettyHttpServer extends AbstractServerProvider { + + private final static Logger log = Logger.getLogger(JettyHttpServer.class.getName()); + + private final ExecutorService janitor; + + private final Server server; + private final List listenedPorts = new ArrayList<>(); + private final ServerMetricReporter metricsReporter; + + @Inject + public JettyHttpServer(CurrentContainer container, + Metric metric, + ServerConfig serverConfig, + ServletPathsConfig servletPathsConfig, + FilterBindings filterBindings, + ComponentRegistry connectorFactories, + ComponentRegistry servletHolders, + FilterInvoker filterInvoker, + RequestLog requestLog, + ConnectionLog connectionLog) { + super(container); + if (connectorFactories.allComponents().isEmpty()) + throw new IllegalArgumentException("No connectors configured."); + + initializeJettyLogging(); + + server = new Server(); + server.setStopTimeout((long)(serverConfig.stopTimeout() * 1000.0)); + server.setRequestLog(new AccessLogRequestLog(requestLog, serverConfig.accessLog())); + setupJmx(server, serverConfig); + configureJettyThreadpool(server, serverConfig); + JettyConnectionLogger connectionLogger = new JettyConnectionLogger(serverConfig.connectionLog(), connectionLog); + + for (ConnectorFactory connectorFactory : connectorFactories.allComponents()) { + ConnectorConfig connectorConfig = connectorFactory.getConnectorConfig(); + server.addConnector(connectorFactory.createConnector(metric, server, connectionLogger)); + listenedPorts.add(connectorConfig.listenPort()); + } + + janitor = newJanitor(); + + JDiscContext jDiscContext = new JDiscContext(filterBindings, + container, + janitor, + metric, + serverConfig); + + ServletHolder jdiscServlet = new ServletHolder(new JDiscHttpServlet(jDiscContext)); + FilterHolder jDiscFilterInvokerFilter = new FilterHolder(new JDiscFilterInvokerFilter(jDiscContext, filterInvoker)); + + List connectors = Arrays.stream(server.getConnectors()) + .map(JDiscServerConnector.class::cast) + .collect(toList()); + + server.setHandler(getHandlerCollection(serverConfig, + servletPathsConfig, + connectors, + jdiscServlet, + servletHolders, + jDiscFilterInvokerFilter)); + this.metricsReporter = new ServerMetricReporter(metric, server); + } + + private static void initializeJettyLogging() { + // Note: Jetty is logging stderr if no logger is explicitly configured + try { + Log.setLog(new JavaUtilLog()); + } catch (Exception e) { + throw new RuntimeException("Unable to initialize logging framework for Jetty"); + } + } + + private static void setupJmx(Server server, ServerConfig serverConfig) { + if (serverConfig.jmx().enabled()) { + System.setProperty("java.rmi.server.hostname", "localhost"); + server.addBean(new MBeanContainer(ManagementFactory.getPlatformMBeanServer())); + server.addBean(new ConnectorServer(createJmxLoopbackOnlyServiceUrl(serverConfig.jmx().listenPort()), + "org.eclipse.jetty.jmx:name=rmiconnectorserver")); + } + } + + private static void configureJettyThreadpool(Server server, ServerConfig config) { + QueuedThreadPool pool = (QueuedThreadPool) server.getThreadPool(); + pool.setMaxThreads(config.maxWorkerThreads()); + pool.setMinThreads(config.minWorkerThreads()); + } + + private static JMXServiceURL createJmxLoopbackOnlyServiceUrl(int port) { + try { + return new JMXServiceURL("rmi", "localhost", port, "/jndi/rmi://localhost:" + port + "/jmxrmi"); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private HandlerCollection getHandlerCollection(ServerConfig serverConfig, + ServletPathsConfig servletPathsConfig, + List connectors, + ServletHolder jdiscServlet, + ComponentRegistry servletHolders, + FilterHolder jDiscFilterInvokerFilter) { + 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, "/*"); + + List connectorConfigs = connectors.stream().map(JDiscServerConnector::connectorConfig).collect(toList()); + var secureRedirectHandler = new SecuredRedirectHandler(connectorConfigs); + secureRedirectHandler.setHandler(servletContextHandler); + + var proxyHandler = new HealthCheckProxyHandler(connectors); + proxyHandler.setHandler(secureRedirectHandler); + + var authEnforcer = new TlsClientAuthenticationEnforcer(connectorConfigs); + authEnforcer.setHandler(proxyHandler); + + GzipHandler gzipHandler = newGzipHandler(serverConfig); + gzipHandler.setHandler(authEnforcer); + + HttpResponseStatisticsCollector statisticsCollector = + new HttpResponseStatisticsCollector(serverConfig.metric().monitoringHandlerPaths(), + serverConfig.metric().searchHandlerPaths()); + statisticsCollector.setHandler(gzipHandler); + + StatisticsHandler statisticsHandler = newStatisticsHandler(); + statisticsHandler.setHandler(statisticsCollector); + + HandlerCollection handlerCollection = new HandlerCollection(); + handlerCollection.setHandlers(new Handler[] { statisticsHandler }); + return handlerCollection; + } + + private static String getServletPath(ServletPathsConfig servletPathsConfig, ComponentId id) { + return "/" + servletPathsConfig.servlets(id.stringValue()).path(); + } + + private ServletContextHandler createServletContextHandler() { + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); + servletContextHandler.setContextPath("/"); + servletContextHandler.setDisplayName(getDisplayName(listenedPorts)); + return servletContextHandler; + } + + private static String getDisplayName(List ports) { + return ports.stream().map(Object::toString).collect(Collectors.joining(":")); + } + + // Separate threadpool for tasks that cannot be executed on the jdisc default threadpool due to risk of deadlock + private static ExecutorService newJanitor() { + int threadPoolSize = Math.max(1, Runtime.getRuntime().availableProcessors()/8); + log.info("Creating janitor executor with " + threadPoolSize + " threads"); + return Executors.newFixedThreadPool( + threadPoolSize, + new DaemonThreadFactory(JettyHttpServer.class.getName() + "-Janitor-")); + } + + @Override + public void start() { + try { + server.start(); + metricsReporter.start(); + logEffectiveSslConfiguration(); + } catch (final Exception e) { + if (e instanceof IOException && e.getCause() instanceof BindException) { + throw new RuntimeException("Failed to start server due to BindException. ListenPorts = " + listenedPorts.toString(), e.getCause()); + } + throw new RuntimeException("Failed to start server.", e); + } + } + + private void logEffectiveSslConfiguration() { + if (!server.isStarted()) throw new IllegalStateException(); + for (Connector connector : server.getConnectors()) { + ServerConnector serverConnector = (ServerConnector) connector; + int localPort = serverConnector.getLocalPort(); + var sslConnectionFactory = serverConnector.getConnectionFactory(SslConnectionFactory.class); + if (sslConnectionFactory != null) { + var sslContextFactory = sslConnectionFactory.getSslContextFactory(); + log.info(String.format("Enabled SSL cipher suites for port '%d': %s", + localPort, Arrays.toString(sslContextFactory.getSelectedCipherSuites()))); + log.info(String.format("Enabled SSL protocols for port '%d': %s", + localPort, Arrays.toString(sslContextFactory.getSelectedProtocols()))); + } + } + } + + @Override + public void close() { + try { + log.log(Level.INFO, String.format("Shutting down server (graceful=%b, timeout=%.1fs)", isGracefulShutdownEnabled(), server.getStopTimeout()/1000d)); + server.stop(); + log.log(Level.INFO, "Server shutdown completed"); + } catch (final Exception e) { + log.log(Level.SEVERE, "Server shutdown threw an unexpected exception.", e); + } + + metricsReporter.shutdown(); + janitor.shutdown(); + } + + private boolean isGracefulShutdownEnabled() { + return server.getChildHandlersByClass(StatisticsHandler.class).length > 0 && server.getStopTimeout() > 0; + } + + public int getListenPort() { + return ((ServerConnector)server.getConnectors()[0]).getLocalPort(); + } + + Server server() { return server; } + + private StatisticsHandler newStatisticsHandler() { + StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.statsReset(); + return statisticsHandler; + } + + private GzipHandler newGzipHandler(ServerConfig serverConfig) { + GzipHandler gzipHandler = new GzipHandlerWithVaryHeaderFixed(); + gzipHandler.setCompressionLevel(serverConfig.responseCompressionLevel()); + gzipHandler.setInflateBufferSize(8 * 1024); + gzipHandler.setIncludedMethods("GET", "POST", "PUT", "PATCH"); + return gzipHandler; + } + + /** A subclass which overrides Jetty's default behavior of including user-agent in the vary field */ + private static class GzipHandlerWithVaryHeaderFixed extends GzipHandler { + + @Override + public HttpField getVaryField() { + return GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; + } + + } + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java new file mode 100644 index 00000000000..5e953179b53 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java @@ -0,0 +1,79 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * Name and dimensions for jdisc/container metrics + * + * @author bjorncs + */ +class MetricDefinitions { + static final String NAME_DIMENSION = "serverName"; + static final String PORT_DIMENSION = "serverPort"; + static final String METHOD_DIMENSION = "httpMethod"; + static final String SCHEME_DIMENSION = "scheme"; + static final String REQUEST_TYPE_DIMENSION = "requestType"; + static final String CLIENT_IP_DIMENSION = "clientIp"; + static final String CLIENT_AUTHENTICATED_DIMENSION = "clientAuthenticated"; + static final String REQUEST_SERVER_NAME_DIMENSION = "requestServerName"; + static final String FILTER_CHAIN_ID_DIMENSION = "chainId"; + + static final String NUM_OPEN_CONNECTIONS = "serverNumOpenConnections"; + static final String NUM_CONNECTIONS_OPEN_MAX = "serverConnectionsOpenMax"; + static final String CONNECTION_DURATION_MAX = "serverConnectionDurationMax"; + static final String CONNECTION_DURATION_MEAN = "serverConnectionDurationMean"; + static final String CONNECTION_DURATION_STD_DEV = "serverConnectionDurationStdDev"; + static final String NUM_PREMATURELY_CLOSED_CONNECTIONS = "jdisc.http.request.prematurely_closed"; + + static final String NUM_BYTES_RECEIVED = "serverBytesReceived"; + static final String NUM_BYTES_SENT = "serverBytesSent"; + + static final String NUM_CONNECTIONS = "serverNumConnections"; + + /* For historical reasons, these are all aliases for the same metric. 'jdisc.http' should ideally be the only one. */ + static final String JDISC_HTTP_REQUESTS = "jdisc.http.requests"; + static final String NUM_REQUESTS = "serverNumRequests"; + + static final String NUM_SUCCESSFUL_RESPONSES = "serverNumSuccessfulResponses"; + static final String NUM_FAILED_RESPONSES = "serverNumFailedResponses"; + static final String NUM_SUCCESSFUL_WRITES = "serverNumSuccessfulResponseWrites"; + static final String NUM_FAILED_WRITES = "serverNumFailedResponseWrites"; + + static final String TOTAL_SUCCESSFUL_LATENCY = "serverTotalSuccessfulResponseLatency"; + static final String TOTAL_FAILED_LATENCY = "serverTotalFailedResponseLatency"; + static final String TIME_TO_FIRST_BYTE = "serverTimeToFirstByte"; + + static final String RESPONSES_1XX = "http.status.1xx"; + static final String RESPONSES_2XX = "http.status.2xx"; + static final String RESPONSES_3XX = "http.status.3xx"; + static final String RESPONSES_4XX = "http.status.4xx"; + static final String RESPONSES_5XX = "http.status.5xx"; + static final String RESPONSES_401 = "http.status.401"; + static final String RESPONSES_403 = "http.status.403"; + + static final String STARTED_MILLIS = "serverStartedMillis"; + + static final String URI_LENGTH = "jdisc.http.request.uri_length"; + static final String CONTENT_SIZE = "jdisc.http.request.content_size"; + + static final String SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.missing_client_cert"; + static final String SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.expired_client_cert"; + static final String SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.invalid_client_cert"; + static final String SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS = "jdisc.http.ssl.handshake.failure.incompatible_protocols"; + static final String SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS = "jdisc.http.ssl.handshake.failure.incompatible_ciphers"; + static final String SSL_HANDSHAKE_FAILURE_UNKNOWN = "jdisc.http.ssl.handshake.failure.unknown"; + + static final String JETTY_THREADPOOL_MAX_THREADS = "jdisc.http.jetty.threadpool.thread.max"; + static final String JETTY_THREADPOOL_MIN_THREADS = "jdisc.http.jetty.threadpool.thread.min"; + static final String JETTY_THREADPOOL_RESERVED_THREADS = "jdisc.http.jetty.threadpool.thread.reserved"; + static final String JETTY_THREADPOOL_BUSY_THREADS = "jdisc.http.jetty.threadpool.thread.busy"; + static final String JETTY_THREADPOOL_IDLE_THREADS = "jdisc.http.jetty.threadpool.thread.idle"; + static final String JETTY_THREADPOOL_TOTAL_THREADS = "jdisc.http.jetty.threadpool.thread.total"; + static final String JETTY_THREADPOOL_QUEUE_SIZE = "jdisc.http.jetty.threadpool.queue.size"; + + static final String FILTERING_REQUEST_HANDLED = "jdisc.http.filtering.request.handled"; + static final String FILTERING_REQUEST_UNHANDLED = "jdisc.http.filtering.request.unhandled"; + static final String FILTERING_RESPONSE_HANDLED = "jdisc.http.filtering.response.handled"; + static final String FILTERING_RESPONSE_UNHANDLED = "jdisc.http.filtering.response.unhandled"; + + private MetricDefinitions() {} +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java new file mode 100644 index 00000000000..eb83d3d7d03 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java @@ -0,0 +1,23 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Tony Vaagenes + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java new file mode 100644 index 00000000000..f2bf5b56d5c --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java @@ -0,0 +1,257 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.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 bakksjo + */ +@SuppressWarnings("try") +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java new file mode 100644 index 00000000000..eea69cd7f74 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java @@ -0,0 +1,39 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +/** + * 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 bakksjo + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java new file mode 100644 index 00000000000..7596be0415a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java @@ -0,0 +1,85 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.Metric.Context; + +import java.util.concurrent.atomic.AtomicBoolean; + + +/** + * Responsible for metric reporting for JDisc http request handler support. + * @author Tony Vaagenes + */ +class RequestMetricReporter { + private final Metric metric; + private final Context context; + + private final long requestStartTime; + + //TODO: rename + private final AtomicBoolean firstSetOfTimeToFirstByte = new AtomicBoolean(true); + + + RequestMetricReporter(Metric metric, Context context, long requestStartTime) { + this.metric = metric; + this.context = context; + this.requestStartTime = requestStartTime; + } + + void successfulWrite(int numBytes) { + setTimeToFirstByteFirstTime(); + + metric.add(MetricDefinitions.NUM_SUCCESSFUL_WRITES, 1, context); + metric.set(MetricDefinitions.NUM_BYTES_SENT, numBytes, context); + } + + private void setTimeToFirstByteFirstTime() { + boolean isFirstWrite = firstSetOfTimeToFirstByte.getAndSet(false); + if (isFirstWrite) { + long timeToFirstByte = getRequestLatency(); + metric.set(MetricDefinitions.TIME_TO_FIRST_BYTE, timeToFirstByte, context); + } + } + + void failedWrite() { + metric.add(MetricDefinitions.NUM_FAILED_WRITES, 1, context); + } + + void successfulResponse() { + setTimeToFirstByteFirstTime(); + + long requestLatency = getRequestLatency(); + + metric.set(MetricDefinitions.TOTAL_SUCCESSFUL_LATENCY, requestLatency, context); + + metric.add(MetricDefinitions.NUM_SUCCESSFUL_RESPONSES, 1, context); + } + + void failedResponse() { + setTimeToFirstByteFirstTime(); + + metric.set(MetricDefinitions.TOTAL_FAILED_LATENCY, getRequestLatency(), context); + metric.add(MetricDefinitions.NUM_FAILED_RESPONSES, 1, context); + } + + void prematurelyClosed() { + metric.add(MetricDefinitions.NUM_PREMATURELY_CLOSED_CONNECTIONS, 1, context); + } + + void successfulRead(int bytes_received) { + metric.set(MetricDefinitions.NUM_BYTES_RECEIVED, bytes_received, context); + } + + private long getRequestLatency() { + return System.currentTimeMillis() - requestStartTime; + } + + void uriLength(int length) { + metric.set(MetricDefinitions.URI_LENGTH, length, context); + } + + void contentSize(int size) { + metric.set(MetricDefinitions.CONTENT_SIZE, size, context); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java new file mode 100644 index 00000000000..e32c9d46deb --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java @@ -0,0 +1,58 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.ConnectorConfig; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.URIUtil; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; + +/** + * A secure redirect handler inspired by {@link org.eclipse.jetty.server.handler.SecuredRedirectHandler}. + * + * @author bjorncs + */ +class SecuredRedirectHandler extends HandlerWrapper { + + private static final String HEALTH_CHECK_PATH = "/status.html"; + + private final Map redirectMap; + + SecuredRedirectHandler(List connectorConfigs) { + this.redirectMap = createRedirectMap(connectorConfigs); + } + + @Override + public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { + int localPort = getConnectorLocalPort(servletRequest); + if (!redirectMap.containsKey(localPort)) { + _handler.handle(target, request, servletRequest, servletResponse); + return; + } + servletResponse.setContentLength(0); + if (!servletRequest.getRequestURI().equals(HEALTH_CHECK_PATH)) { + servletResponse.sendRedirect( + URIUtil.newURI("https", request.getServerName(), redirectMap.get(localPort), request.getRequestURI(), request.getQueryString())); + } + request.setHandled(true); + } + + private static Map createRedirectMap(List connectorConfigs) { + var redirectMap = new HashMap(); + for (ConnectorConfig connectorConfig : connectorConfigs) { + if (connectorConfig.secureRedirect().enabled()) { + redirectMap.put(connectorConfig.listenPort(), connectorConfig.secureRedirect().port()); + } + } + return redirectMap; + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java new file mode 100644 index 00000000000..ba3694ffc2f --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java @@ -0,0 +1,115 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.concurrent.DaemonThreadFactory; +import com.yahoo.jdisc.Metric; +import org.eclipse.jetty.io.ConnectionStatistics; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandlerContainer; +import org.eclipse.jetty.server.handler.StatisticsHandler; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Reports server/connector specific metrics for Jdisc and Jetty + * + * @author bjorncs + */ +class ServerMetricReporter { + + private final ScheduledExecutorService executor = + Executors.newScheduledThreadPool(1, new DaemonThreadFactory("jdisc-jetty-metric-reporter-")); + private final Metric metric; + private final Server jetty; + + ServerMetricReporter(Metric metric, Server jetty) { + this.metric = metric; + this.jetty = jetty; + } + + void start() { + executor.scheduleAtFixedRate(new ReporterTask(), 0, 2, TimeUnit.SECONDS); + } + + void shutdown() { + try { + executor.shutdownNow(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private class ReporterTask implements Runnable { + + private final Instant timeStarted = Instant.now(); + + @Override + public void run() { + HttpResponseStatisticsCollector statisticsCollector = ((AbstractHandlerContainer) jetty.getHandler()) + .getChildHandlerByClass(HttpResponseStatisticsCollector.class); + if (statisticsCollector != null) { + setServerMetrics(statisticsCollector); + } + + // reset statisticsHandler to preserve earlier behavior + StatisticsHandler statisticsHandler = ((AbstractHandlerContainer) jetty.getHandler()) + .getChildHandlerByClass(StatisticsHandler.class); + if (statisticsHandler != null) { + statisticsHandler.statsReset(); + } + + for (Connector connector : jetty.getConnectors()) { + setConnectorMetrics((JDiscServerConnector)connector); + } + + setJettyThreadpoolMetrics(); + } + + private void setServerMetrics(HttpResponseStatisticsCollector statisticsCollector) { + long timeSinceStarted = System.currentTimeMillis() - timeStarted.toEpochMilli(); + metric.set(MetricDefinitions.STARTED_MILLIS, timeSinceStarted, null); + + addResponseMetrics(statisticsCollector); + } + + private void addResponseMetrics(HttpResponseStatisticsCollector statisticsCollector) { + for (var metricEntry : statisticsCollector.takeStatistics()) { + Map dimensions = new HashMap<>(); + dimensions.put(MetricDefinitions.METHOD_DIMENSION, metricEntry.method); + dimensions.put(MetricDefinitions.SCHEME_DIMENSION, metricEntry.scheme); + dimensions.put(MetricDefinitions.REQUEST_TYPE_DIMENSION, metricEntry.requestType); + metric.add(metricEntry.name, metricEntry.value, metric.createContext(dimensions)); + } + } + + private void setJettyThreadpoolMetrics() { + QueuedThreadPool threadpool = (QueuedThreadPool) jetty.getThreadPool(); + metric.set(MetricDefinitions.JETTY_THREADPOOL_MAX_THREADS, threadpool.getMaxThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_MIN_THREADS, threadpool.getMinThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_RESERVED_THREADS, threadpool.getReservedThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_BUSY_THREADS, threadpool.getBusyThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_IDLE_THREADS, threadpool.getIdleThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_TOTAL_THREADS, threadpool.getThreads(), null); + metric.set(MetricDefinitions.JETTY_THREADPOOL_QUEUE_SIZE, threadpool.getQueueSize(), null); + } + + private void setConnectorMetrics(JDiscServerConnector connector) { + ConnectionStatistics statistics = connector.getStatistics(); + metric.set(MetricDefinitions.NUM_CONNECTIONS, statistics.getConnectionsTotal(), connector.getConnectorMetricContext()); + metric.set(MetricDefinitions.NUM_OPEN_CONNECTIONS, statistics.getConnections(), connector.getConnectorMetricContext()); + metric.set(MetricDefinitions.NUM_CONNECTIONS_OPEN_MAX, statistics.getConnectionsMax(), connector.getConnectorMetricContext()); + metric.set(MetricDefinitions.CONNECTION_DURATION_MAX, statistics.getConnectionDurationMax(), connector.getConnectorMetricContext()); + metric.set(MetricDefinitions.CONNECTION_DURATION_MEAN, statistics.getConnectionDurationMean(), connector.getConnectorMetricContext()); + metric.set(MetricDefinitions.CONNECTION_DURATION_STD_DEV, statistics.getConnectionDurationStdDev(), connector.getConnectorMetricContext()); + } + + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java new file mode 100644 index 00000000000..b4d03385c3b --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java @@ -0,0 +1,299 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.handler.CompletionHandler; + +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.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; + +/** + * @author Tony Vaagenes + * @author bjorncs + */ +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()); + + // If so, application code could fake a close by writing such a byte buffer. + // The problem can be solved by filtering out zero-length byte buffers from application code. + // Other ways to express this are also possible, e.g. with a 'closed' state checked when queue goes empty. + 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 responseContentQueue = new ArrayDeque<>(); + + private final RequestMetricReporter 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 finishedFuture = new CompletableFuture<>(); + + + public ServletOutputStreamWriter(ServletOutputStream outputStream, Executor executor, RequestMetricReporter metricReporter) { + this.outputStream = outputStream; + this.executor = executor; + this.metricReporter = metricReporter; + } + + public void sendErrorContentAndCloseAsync(ByteBuffer errorContent) { + synchronized (monitor) { + // Assert that no content has been written as it is too late to write error response if the response is committed. + assertStateIs(state, State.NOT_STARTED); + queueErrorContent_holdingLock(errorContent); + state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK; + outputStream.setWriteListener(writeListener); + } + } + + private void queueErrorContent_holdingLock(ByteBuffer errorContent) { + responseContentQueue.addLast(new ResponseContentPart(errorContent, NOOP_COMPLETION_HANDLER)); + responseContentQueue.addLast(new ResponseContentPart(CLOSE_STREAM_BUFFER, NOOP_COMPLETION_HANDLER)); + } + + public void writeBuffer(ByteBuffer buf, CompletionHandler handler) { + boolean thisThreadShouldWrite = false; + + synchronized (monitor) { + if (state == State.FINISHED_OR_ERROR) { + 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); + } + + public void close() { + close(NOOP_COMPLETION_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) { + callCompletionHandlerWhenDone(contentPart.handler, outputStream::close); + setFinished(Optional.empty()); + return; + } else { + writeBufferToOutputStream(contentPart); + } + } catch (Throwable e) { + setFinished(Optional.of(e)); + return; + } + } + } + + private void setFinished(Optional 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 failedParts = new ArrayList<>(responseContentQueue); + responseContentQueue.clear(); + + @SuppressWarnings("ThrowableInstanceNeverThrown") + RuntimeException failReason = new RuntimeException("Failing due to earlier ServletOutputStream write failure", e); + + Consumer failCompletionHandler = responseContentPart -> + runCompletionHandler_logOnExceptions( + () -> responseContentPart.handler.failed(failReason)); + + executor.execute( + () -> failedParts.forEach(failCompletionHandler)); + } + + 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; + } + }); + } + + private static void callCompletionHandlerWhenDone(CompletionHandler handler, IORunnable runnable) throws Exception { + try { + runnable.run(); + } catch (Throwable e) { + runCompletionHandler_logOnExceptions(() -> handler.failed(e)); + throw e; + } + handler.completed(); //Might throw an exception, handling in the enclosing scope. + } + + private static void runCompletionHandler_logOnExceptions(Runnable runnable) { + try { + runnable.run(); + } catch (Throwable e) { + log.log(Level.WARNING, "Unexpected exception from CompletionHandler.", e); + } + } + + private static 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)); + } + }; + + private static class ResponseContentPart { + public final ByteBuffer buf; + public final CompletionHandler handler; + + public ResponseContentPart(ByteBuffer buf, CompletionHandler handler) { + this.buf = buf; + this.handler = handler; + } + } + + @FunctionalInterface + private interface IORunnable { + void run() throws IOException; + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java new file mode 100644 index 00000000000..1882448757a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java @@ -0,0 +1,270 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.common.base.Preconditions; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; + +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 BUFFER_SIZE_BYTES = 8 * 1024; + + private final Object monitor = new Object(); + + private final ServletInputStream servletInputStream; + private final ContentChannel requestContentChannel; + + private final Executor executor; + private final RequestMetricReporter metricReporter; + + private int bytesRead; + + /** + * 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 finishedFuture = new CompletableFuture<>(); + + public ServletRequestReader( + ServletInputStream servletInputStream, + ContentChannel requestContentChannel, + Executor executor, + RequestMetricReporter 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 byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + int numBytesRead; + + synchronized (monitor) { + numBytesRead = servletInputStream.read(buffer); + if (numBytesRead < 0) { + // End of stream; there should be no more data available, ever. + return; + } + 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; + bytesRead += numBytesRead; + } + + try { + requestContentChannel.write(ByteBuffer.wrap(buffer, 0, numBytesRead), writeCompletionHandler); + metricReporter.successfulRead(numBytesRead); + } + catch (Throwable t) { + finishedFuture.completeExceptionally(t); + } + finally { + //decrease due to this method completing. + decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally(); + } + } + } + + private void decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally() { + 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; + + int bytesRead; + synchronized (monitor) { + if (state != State.READING) { + return; + } + + state = State.ALL_DATA_READ; + + shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0; + if (shouldCloseRequestContentChannel) { + state = State.REQUEST_CONTENT_CLOSED; + } + bytesRead = this.bytesRead; + } + + if (shouldCloseRequestContentChannel) { + closeCompletionHandler_noThrow(); + } + + metricReporter.contentSize(bytesRead); + } + + 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 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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java new file mode 100644 index 00000000000..60b7878156f --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java @@ -0,0 +1,251 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.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.HttpHeaders; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import org.eclipse.jetty.http.MimeTypes; + +import javax.servlet.http.HttpServletRequest; +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.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; + +/** + * @author Tony Vaagenes + * @author bjorncs + */ +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 HttpServletRequest servletRequest; + private final HttpServletResponse servletResponse; + private final boolean developerMode; + private final ErrorResponseContentCreator errorResponseContentCreator = new ErrorResponseContentCreator(); + + //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( + HttpServletRequest servletRequest, + HttpServletResponse servletResponse, + Executor executor, + RequestMetricReporter metricReporter, + boolean developerMode) throws IOException { + + this.servletRequest = servletRequest; + 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; + try { + synchronized (monitor) { + String reasonPhrase = getReasonPhrase(t, developerMode); + int statusCode = getStatusCode(t); + responseWasCommitted = responseCommitted; + if (!responseCommitted) { + responseCommitted = true; + sendErrorAsync(statusCode, reasonPhrase); + } + } + } catch (Throwable e) { + servletOutputStreamWriter.fail(t); + return; + } + + //Must be evaluated after state transition for test purposes(See ConformanceTestException) + //Done outside the monitor since it causes a callback in tests. + 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(); + } + + } + + /** + * Async version of {@link org.eclipse.jetty.server.Response#sendError(int, String)}. + */ + private void sendErrorAsync(int statusCode, String reasonPhrase) { + servletResponse.setHeader(HttpHeaders.Names.EXPIRES, null); + servletResponse.setHeader(HttpHeaders.Names.LAST_MODIFIED, null); + servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, null); + servletResponse.setHeader(HttpHeaders.Names.CONTENT_TYPE, null); + servletResponse.setHeader(HttpHeaders.Names.CONTENT_LENGTH, null); + setStatus(servletResponse, statusCode, Optional.of(reasonPhrase)); + + // If we are allowed to have a body + if (statusCode != HttpServletResponse.SC_NO_CONTENT && + statusCode != HttpServletResponse.SC_NOT_MODIFIED && + statusCode != HttpServletResponse.SC_PARTIAL_CONTENT && + statusCode >= HttpServletResponse.SC_OK) { + servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); + servletResponse.setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString()); + byte[] errorContent = errorResponseContentCreator + .createErrorContent(servletRequest.getRequestURI(), statusCode, Optional.ofNullable(reasonPhrase)); + servletResponse.setContentLength(errorContent.length); + servletOutputStreamWriter.sendErrorContentAndCloseAsync(ByteBuffer.wrap(errorContent)); + } else { + servletResponse.setContentLength(0); + servletOutputStreamWriter.close(); + } + } + + /** + * 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 finishedFuture() { + return servletOutputStreamWriter.finishedFuture; + } + + private void setResponse(Response jdiscResponse) { + synchronized (monitor) { + servletRequest.setAttribute(HttpResponseStatisticsCollector.requestTypeAttribute, jdiscResponse.getRequestType()); + 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(); + return; + } + + setStatus_holdingLock(jdiscResponse, servletResponse); + setHeaders_holdingLock(jdiscResponse, servletResponse); + } + } + + private static void setHeaders_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { + for (final Map.Entry entry : jdiscResponse.headers().entries()) { + servletResponse.addHeader(entry.getKey(), entry.getValue()); + } + + if (servletResponse.getContentType() == null) { + servletResponse.setContentType("text/plain;charset=utf-8"); + } + } + + private static void setStatus_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { + if (jdiscResponse instanceof HttpResponse) { + setStatus(servletResponse, jdiscResponse.getStatus(), Optional.ofNullable(((HttpResponse) jdiscResponse).getMessage())); + } else { + setStatus(servletResponse, jdiscResponse.getStatus(), getErrorMessage(jdiscResponse)); + } + } + + @SuppressWarnings("deprecation") + private static void setStatus(HttpServletResponse response, int statusCode, Optional reasonPhrase) { + if (reasonPhrase.isPresent()) { + // Sets the status line: a status code along with a custom message. + // Using a custom status message is deprecated in the Servlet API. No alternative exist. + response.setStatus(statusCode, reasonPhrase.get()); // DEPRECATED + } else { + response.setStatus(statusCode); + } + } + + private static Optional 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, handlerOrNoopHandler(handler)); + } + + @Override + public void close(CompletionHandler handler) { + commitResponse(); + servletOutputStreamWriter.close(handlerOrNoopHandler(handler)); + } + + private CompletionHandler handlerOrNoopHandler(CompletionHandler handler) { + return handler != null ? handler : NOOP_COMPLETION_HANDLER; + } + }; +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java new file mode 100644 index 00000000000..822e1c2ffb8 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java @@ -0,0 +1,52 @@ +// Copyright 2020 Oath 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 org.eclipse.jetty.io.ssl.SslHandshakeListener; + +import javax.net.ssl.SSLHandshakeException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * A {@link SslHandshakeListener} that reports metrics for SSL handshake failures. + * + * @author bjorncs + */ +class SslHandshakeFailedListener implements SslHandshakeListener { + + private final static Logger log = Logger.getLogger(SslHandshakeFailedListener.class.getName()); + + private final Metric metric; + private final String connectorName; + private final int listenPort; + + SslHandshakeFailedListener(Metric metric, String connectorName, int listenPort) { + this.metric = metric; + this.connectorName = connectorName; + this.listenPort = listenPort; + } + + @Override + public void handshakeFailed(Event event, Throwable throwable) { + log.log(Level.FINE, throwable, () -> "Ssl handshake failed: " + throwable.getMessage()); + String metricName = SslHandshakeFailure.fromSslHandshakeException((SSLHandshakeException) throwable) + .map(SslHandshakeFailure::metricName) + .orElse(MetricDefinitions.SSL_HANDSHAKE_FAILURE_UNKNOWN); + metric.add(metricName, 1L, metric.createContext(createDimensions(event))); + } + + private Map createDimensions(Event event) { + Map dimensions = new HashMap<>(); + dimensions.put(MetricDefinitions.NAME_DIMENSION, connectorName); + dimensions.put(MetricDefinitions.PORT_DIMENSION, listenPort); + Optional.ofNullable(event.getSSLEngine().getPeerHost()) + .ifPresent(clientIp -> dimensions.put(MetricDefinitions.CLIENT_IP_DIMENSION, clientIp)); + return Map.copyOf(dimensions); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java new file mode 100644 index 00000000000..64f70564137 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java @@ -0,0 +1,61 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import javax.net.ssl.SSLHandshakeException; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Categorizes instances of {@link SSLHandshakeException} + * + * @author bjorncs + */ +enum SslHandshakeFailure { + INCOMPATIBLE_PROTOCOLS( + MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, + "INCOMPATIBLE_CLIENT_PROTOCOLS", + "(Client requested protocol \\S+? is not enabled or supported in server context" + + "|The client supported protocol versions \\[.+?\\] are not accepted by server preferences \\[.+?\\])"), + INCOMPATIBLE_CIPHERS( + MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, + "INCOMPATIBLE_CLIENT_CIPHER_SUITES", + "no cipher suites in common"), + MISSING_CLIENT_CERT( + MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, + "MISSING_CLIENT_CERTIFICATE", + "Empty (server|client) certificate chain"), + EXPIRED_CLIENT_CERTIFICATE( + MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, + "EXPIRED_CLIENT_CERTIFICATE", + // Note: this pattern will match certificates with too late notBefore as well + "PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed"), + INVALID_CLIENT_CERT( + MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, // Includes mismatch of client certificate and private key + "INVALID_CLIENT_CERTIFICATE", + "(PKIX path (building|validation) failed: .+)|(Invalid CertificateVerify signature)"); + + private final String metricName; + private final String failureType; + private final Predicate messageMatcher; + + SslHandshakeFailure(String metricName, String failureType, String messagePattern) { + this.metricName = metricName; + this.failureType = failureType; + this.messageMatcher = Pattern.compile(messagePattern).asMatchPredicate(); + } + + String metricName() { return metricName; } + String failureType() { return failureType; } + + static Optional fromSslHandshakeException(SSLHandshakeException exception) { + String message = exception.getMessage(); + if (message == null || message.isBlank()) return Optional.empty(); + for (SslHandshakeFailure failure : values()) { + if (failure.messageMatcher.test(message)) { + return Optional.of(failure); + } + } + return Optional.empty(); + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java new file mode 100644 index 00000000000..10a6c4702b5 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java @@ -0,0 +1,83 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.servlet.ServletRequest; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; + +/** + * A Jetty handler that enforces TLS client authentication with configurable white list. + * + * @author bjorncs + */ +class TlsClientAuthenticationEnforcer extends HandlerWrapper { + + private final Map> portToWhitelistedPathsMapping; + + TlsClientAuthenticationEnforcer(List connectorConfigs) { + portToWhitelistedPathsMapping = createWhitelistMapping(connectorConfigs); + } + + @Override + public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { + if (isHttpsRequest(request) + && !isRequestToWhitelistedBinding(servletRequest) + && !isClientAuthenticated(servletRequest)) { + servletResponse.sendError( + Response.Status.UNAUTHORIZED, + "Client did not present a x509 certificate, " + + "or presented a certificate not issued by any of the CA certificates in trust store."); + } else { + _handler.handle(target, request, servletRequest, servletResponse); + } + } + + private static Map> createWhitelistMapping(List connectorConfigs) { + var mapping = new HashMap>(); + for (ConnectorConfig connectorConfig : connectorConfigs) { + var enforcerConfig = connectorConfig.tlsClientAuthEnforcer(); + if (enforcerConfig.enable()) { + mapping.put(connectorConfig.listenPort(), enforcerConfig.pathWhitelist()); + } + } + return mapping; + } + + private boolean isHttpsRequest(Request request) { + return request.getDispatcherType() == DispatcherType.REQUEST && request.getScheme().equalsIgnoreCase("https"); + } + + private boolean isRequestToWhitelistedBinding(HttpServletRequest servletRequest) { + int localPort = getConnectorLocalPort(servletRequest); + List whiteListedPaths = getWhitelistedPathsForPort(localPort); + if (whiteListedPaths == null) { + return true; // enforcer not enabled + } + // Note: Same path definition as HttpRequestFactory.getUri() + return whiteListedPaths.contains(servletRequest.getRequestURI()); + } + + private List getWhitelistedPathsForPort(int localPort) { + if (portToWhitelistedPathsMapping.containsKey(0) && portToWhitelistedPathsMapping.size() == 1) { + return portToWhitelistedPathsMapping.get(0); // for unit tests which uses 0 for listen port + } + return portToWhitelistedPathsMapping.get(localPort); + } + + private boolean isClientAuthenticated(HttpServletRequest servletRequest) { + return servletRequest.getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT) != null; + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java new file mode 100644 index 00000000000..ce52bccf52d --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java @@ -0,0 +1,32 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.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 Tony Vaagenes + */ +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/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java new file mode 100644 index 00000000000..5d33cc0835e --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java @@ -0,0 +1,16 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; + +/** + * @author mortent + */ +public class VoidConnectionLog implements ConnectionLog { + + @Override + public void log(ConnectionLogEntry connectionLogEntry) { + } +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java new file mode 100644 index 00000000000..9db5ba99115 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java @@ -0,0 +1,14 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; + +/** + * @author bjorncs + */ +public class VoidRequestLog implements RequestLog { + + @Override public void log(RequestLogEntry entry) {} + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java new file mode 100644 index 00000000000..189751aa9c0 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java @@ -0,0 +1,3 @@ +// Copyright 2017 Yahoo Holdings. 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/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java new file mode 100644 index 00000000000..eaac2b1c415 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java @@ -0,0 +1,40 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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; +import java.util.concurrent.TimeUnit; + +/** + * Common interface for JDisc and servlet http requests. + */ +public interface ServletOrJdiscHttpRequest { + + void copyHeaders(HeaderFields target); + + Map> parameters(); + + URI getUri(); + + HttpRequest.Version getVersion(); + + String getRemoteHostAddress(); + String getRemoteHostName(); + int getRemotePort(); + + void setRemoteAddress(SocketAddress remoteAddress); + + Map context(); + + List decodeCookieHeader(); + + void encodeCookieHeader(List cookies); + + long getConnectedAt(TimeUnit unit); +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java new file mode 100644 index 00000000000..a24ada05b3d --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java @@ -0,0 +1,23 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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 context(); + + public List decodeSetCookieHeader(); + +} diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java new file mode 100644 index 00000000000..c945dc6d8b6 --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java @@ -0,0 +1,272 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.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.security.Principal; +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; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; + +/** + * Mutable wrapper to use a {@link javax.servlet.http.HttpServletRequest} + * with JDisc security filters. + *

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

HTTP ERROR: 200

\n" + + "

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

    My custom error message

\n" + + "
\n" + + "\n" + + "\n"; + + ErrorResponseContentCreator c = new ErrorResponseContentCreator(); + byte[] rawContent = c.createErrorContent( + "http://foo.bar", + HttpServletResponse.SC_OK, + Optional.of("My custom error message")); + String actualHtml = new String(rawContent, StandardCharsets.ISO_8859_1); + assertEquals(expectedHtml, actualHtml); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java new file mode 100644 index 00000000000..de8df283afe --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java @@ -0,0 +1,51 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * Check basic error message formatting. Do note these tests are sensitive to + * the line numbering in this file. (And that's a feature, not a bug.) + * + * @author Steinar Knutsen + */ +public class ExceptionWrapperTest { + + @Test + public final void requireNoMessageIsOK() { + final Throwable t = new Throwable(); + final ExceptionWrapper e = new ExceptionWrapper(t); + final String expected = "Throwable() at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:19)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireAllWrappedLevelsShowUp() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable("t1", t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:30):" + + " Throwable(\"t1\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:29):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:28)"; + + assertThat(e.getMessage(), equalTo(expected)); + } + + @Test + public final void requireMixOfMessageAndNoMessageWorks() { + final Throwable t0 = new Throwable("t0"); + final Throwable t1 = new Throwable(t0); + final Throwable t2 = new Throwable("t2", t1); + final ExceptionWrapper e = new ExceptionWrapper(t2); + final String expected = "Throwable(\"t2\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:43):" + + " Throwable(\"java.lang.Throwable: t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:42):" + + " Throwable(\"t0\") at com.yahoo.jdisc.http.server.jetty.ExceptionWrapperTest(ExceptionWrapperTest.java:41)"; + + assertThat(e.getMessage(), equalTo(expected)); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java new file mode 100644 index 00000000000..a67656dd5ca --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java @@ -0,0 +1,667 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.filter.ResponseHeaderFilter; +import com.yahoo.jdisc.http.filter.chain.RequestFilterChain; +import com.yahoo.jdisc.http.filter.chain.ResponseFilterChain; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Oyvind Bakksjo + * @author bjorncs + */ +public class FilterTestCase { + @Test + public void requireThatRequestFilterIsNotRunOnUnboundPath() throws Exception { + RequestFilterMockBase filter = mock(RequestFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterIsRunOnBoundPath() throws Exception { + final RequestFilter filter = mock(RequestFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChangesAreSeenByRequestHandler() throws Exception { + final RequestFilter filter = new HeaderRequestFilter("foo", "bar"); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + assertThat(requestHandler.getHeaderMap().get("foo").get(0), is("bar")); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterCanRespond() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new RespondForbiddenFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.FORBIDDEN)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatFilterCanHaveNullCompletionHandler() throws Exception { + final int responseStatus = Response.Status.OK; + final String responseMessage = "Excellent"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new NullCompletionHandlerFilter(responseStatus, responseMessage)) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectStatusCode(is(responseStatus)) + .expectContent(is(responseMessage)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterExecutionIsExceptionSafe() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new ThrowingRequestFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsNotRunOnUnboundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filter) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, never()).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterIsRunOnBoundPath() throws Exception { + final ResponseFilter filter = mock(ResponseFilterMockBase.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filter) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(filter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterChangesAreWrittenToResponse() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new HeaderResponseFilter("foo", "bar")) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterExecutionIsExceptionSafe() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new ThrowingResponseFilter()) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html").expectStatusCode(is(Response.Status.INTERNAL_SERVER_ERROR)); + + assertThat(requestHandler.awaitInvocation(), is(true)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterAndResponseFilterCanBindToSamePath() throws Exception { + final RequestFilter requestFilter = mock(RequestFilterMockBase.class); + final ResponseFilter responseFilter = mock(ResponseFilterMockBase.class); + final String uriPattern = "http://*/*"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", requestFilter) + .addRequestFilterBinding("my-request-filter", uriPattern) + .addResponseFilter("my-response-filter", responseFilter) + .addResponseFilterBinding("my-response-filter", uriPattern) + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(requestFilter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(responseFilter, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFromRequestFilterGoesThroughResponseFilter() throws Exception { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new RespondForbiddenFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .addResponseFilter("my-response-filter", new HeaderResponseFilter("foo", "bar")) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + final MyRequestHandler requestHandler = new MyRequestHandler(); + final TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html") + .expectStatusCode(is(Response.Status.FORBIDDEN)) + .expectHeader("foo", is("bar")); + + assertThat(requestHandler.hasBeenInvokedYet(), is(false)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterChainRetainsFilters() { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + + verify(requestFilter1, never()).refer(); + verify(requestFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(requestFilter1.refer()).thenReturn(reference1); + when(requestFilter2.refer()).thenReturn(reference2); + final RequestFilter chain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + verify(requestFilter1, times(1)).refer(); + verify(requestFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatRequestFilterChainIsRun() throws Exception { + final RequestFilter requestFilter1 = mock(RequestFilter.class); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + verify(requestFilter1).filter(isNull(), any(ResponseHandler.class)); + verify(requestFilter2).filter(isNull(), any(ResponseHandler.class)); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalRequest() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = mock(HttpRequest.class); + final ResponseHandler responseHandler = null; + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same request argument as the chain was, + // in a manner that allows the request object to be wrapped. + final ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(HttpRequest.class); + verify(requestFilter).filter(requestCaptor.capture(), isNull()); + verify(request, never()).getUri(); + requestCaptor.getValue().getUri(); + verify(request, times(1)).getUri(); + } + + @Test + public void requireThatRequestFilterChainCallsFilterWithOriginalResponseHandler() throws Exception { + final RequestFilter requestFilter = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + requestFilterChain.filter(request, responseHandler); + + // Check that the filter is called with the same response handler argument as the chain was, + // in a manner that allows the handler object to be wrapped. + final ArgumentCaptor responseHandlerCaptor = ArgumentCaptor.forClass(ResponseHandler.class); + verify(requestFilter).filter(isNull(), responseHandlerCaptor.capture()); + verify(responseHandler, never()).handleResponse(any(Response.class)); + responseHandlerCaptor.getValue().handleResponse(mock(Response.class)); + verify(responseHandler, times(1)).handleResponse(any(Response.class)); + } + + @Test + public void requireThatRequestFilterCanTerminateChain() throws Exception { + final RequestFilter requestFilter1 = new RespondForbiddenFilter(); + final RequestFilter requestFilter2 = mock(RequestFilter.class); + final RequestFilter requestFilterChain = RequestFilterChain.newInstance(requestFilter1, requestFilter2); + final HttpRequest request = null; + final ResponseHandler responseHandler = mock(ResponseHandler.class); + when(responseHandler.handleResponse(any(Response.class))).thenReturn(mock(ContentChannel.class)); + + requestFilterChain.filter(request, responseHandler); + + verify(requestFilter2, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseHandler).handleResponse(responseCaptor.capture()); + assertThat(responseCaptor.getValue().getStatus(), is(Response.Status.FORBIDDEN)); + } + + @Test + public void requireThatResponseFilterChainRetainsFilters() { + final ResponseFilter responseFilter1 = mock(ResponseFilter.class); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + + verify(responseFilter1, never()).refer(); + verify(responseFilter2, never()).refer(); + final ResourceReference reference1 = mock(ResourceReference.class); + final ResourceReference reference2 = mock(ResourceReference.class); + when(responseFilter1.refer()).thenReturn(reference1); + when(responseFilter2.refer()).thenReturn(reference2); + final ResponseFilter chain = ResponseFilterChain.newInstance(responseFilter1, responseFilter2); + verify(responseFilter1, times(1)).refer(); + verify(responseFilter2, times(1)).refer(); + + verify(reference1, never()).close(); + verify(reference2, never()).close(); + chain.release(); + verify(reference1, times(1)).close(); + verify(reference2, times(1)).close(); + } + + @Test + public void requireThatResponseFilterChainIsRun() { + final ResponseFilter responseFilter1 = new ResponseHeaderFilter("foo", "bar"); + final ResponseFilter responseFilter2 = mock(ResponseFilter.class); + final int statusCode = Response.Status.BAD_GATEWAY; + final Response response = new Response(statusCode); + final Request request = null; + + ResponseFilterChain.newInstance(responseFilter1, responseFilter2).filter(response, request); + + final ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(Response.class); + verify(responseFilter2).filter(responseCaptor.capture(), isNull()); + assertThat(responseCaptor.getValue().getStatus(), is(statusCode)); + assertThat(responseCaptor.getValue().headers().getFirst("foo"), is("bar")); + + assertThat(response.getStatus(), is(statusCode)); + assertThat(response.headers().getFirst("foo"), is("bar")); + } + + @Test + public void requireThatDefaultRequestFilterChainIsRunIfNoOtherFilterChainMatches() throws IOException, InterruptedException { + RequestFilter filterWithBinding = mock(RequestFilter.class); + RequestFilter defaultFilter = mock(RequestFilter.class); + String defaultFilterId = "default-request-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filterWithBinding) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .addRequestFilter(defaultFilterId, defaultFilter) + .setRequestFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, times(1)).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(filterWithBinding, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatDefaultResponseFilterChainIsRunIfNoOtherFilterChainMatches() throws IOException, InterruptedException { + ResponseFilter filterWithBinding = mock(ResponseFilter.class); + ResponseFilter defaultFilter = mock(ResponseFilter.class); + String defaultFilterId = "default-response-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filterWithBinding) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .addResponseFilter(defaultFilterId, defaultFilter) + .setResponseFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, times(1)).filter(any(Response.class), any(Request.class)); + verify(filterWithBinding, never()).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatRequestFilterWithBindingMatchHasPrecedenceOverDefaultFilter() throws IOException, InterruptedException { + RequestFilterMockBase filterWithBinding = mock(RequestFilterMockBase.class); + RequestFilterMockBase defaultFilter = mock(RequestFilterMockBase.class); + String defaultFilterId = "default-request-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filterWithBinding) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .addRequestFilter(defaultFilterId, defaultFilter) + .setRequestFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, never()).filter(any(HttpRequest.class), any(ResponseHandler.class)); + verify(filterWithBinding).filter(any(HttpRequest.class), any(ResponseHandler.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatResponseFilterWithBindingMatchHasPrecedenceOverDefaultFilter() throws IOException, InterruptedException { + ResponseFilter filterWithBinding = mock(ResponseFilter.class); + ResponseFilter defaultFilter = mock(ResponseFilter.class); + String defaultFilterId = "default-response-filter"; + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", filterWithBinding) + .addResponseFilterBinding("my-response-filter", "http://*/filtered/*") + .addResponseFilter(defaultFilterId, defaultFilter) + .setResponseFilterDefaultForPort(defaultFilterId, 0) + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings); + + testDriver.client().get("/filtered/status.html"); + + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(defaultFilter, never()).filter(any(Response.class), any(Request.class)); + verify(filterWithBinding, times(1)).filter(any(Response.class), any(Request.class)); + + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatMetricAreReported() throws IOException, InterruptedException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", mock(RequestFilter.class)) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + MetricConsumerMock metricConsumerMock = new MetricConsumerMock(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings, metricConsumerMock, false); + + testDriver.client().get("/status.html"); + assertThat(requestHandler.awaitInvocation(), is(true)); + verify(metricConsumerMock.mockitoMock()) + .add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock(), never()) + .add(MetricDefinitions.FILTERING_REQUEST_UNHANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock(), never()) + .add(MetricDefinitions.FILTERING_RESPONSE_HANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + verify(metricConsumerMock.mockitoMock()) + .add(MetricDefinitions.FILTERING_RESPONSE_UNHANDLED, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertThat(testDriver.close(), is(true)); + } + + @Test + public void requireThatStrictFilteringRejectsRequestsNotMatchingFilterChains() throws IOException { + RequestFilter filter = mock(RequestFilter.class); + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", filter) + .addRequestFilterBinding("my-request-filter", "http://*/filtered/*") + .build(); + MyRequestHandler requestHandler = new MyRequestHandler(); + TestDriver testDriver = newDriver(requestHandler, filterBindings, new MetricConsumerMock(), true); + + testDriver.client().get("/unfiltered/") + .expectStatusCode(is(Response.Status.FORBIDDEN)) + .expectContent(containsString("Request did not match any request filter chain")); + verify(filter, never()).filter(any(), any()); + assertThat(testDriver.close(), is(true)); + } + + private static TestDriver newDriver(MyRequestHandler requestHandler, FilterBindings filterBindings) { + return newDriver(requestHandler, filterBindings, new MetricConsumerMock(), false); + } + + private static TestDriver newDriver( + MyRequestHandler requestHandler, + FilterBindings filterBindings, + MetricConsumerMock metricConsumer, + boolean strictFiltering) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newFilterModule(filterBindings, metricConsumer, strictFiltering)); + } + + private static com.google.inject.Module newFilterModule( + FilterBindings filterBindings, MetricConsumerMock metricConsumer, boolean strictFiltering) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + + bind(FilterBindings.class).toInstance(filterBindings); + bind(ServerConfig.class).toInstance(new ServerConfig(new ServerConfig.Builder().strictFiltering(strictFiltering))); + bind(ConnectorConfig.class).toInstance(new ConnectorConfig(new ConnectorConfig.Builder())); + bind(ServletPathsConfig.class).toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ConnectionLog.class).toInstance(new VoidConnectionLog()); + bind(RequestLog.class).toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule(), + metricConsumer.asGuiceModule()); + } + + private static abstract class RequestFilterMockBase extends AbstractResource implements RequestFilter {} + private static abstract class ResponseFilterMockBase extends AbstractResource implements ResponseFilter {} + + private static class MyRequestHandler extends AbstractRequestHandler { + private final CountDownLatch invocationLatch = new CountDownLatch(1); + private final AtomicReference>> headerCopy = new AtomicReference<>(null); + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + try { + headerCopy.set(new HashMap>(request.headers())); + ResponseDispatch.newInstance(Response.Status.OK).dispatch(handler); + return null; + } finally { + invocationLatch.countDown(); + } + } + + public boolean hasBeenInvokedYet() { + return invocationLatch.getCount() == 0L; + } + + public boolean awaitInvocation() throws InterruptedException { + return invocationLatch.await(60, TimeUnit.SECONDS); + } + + public Map> getHeaderMap() { + return headerCopy.get(); + } + } + + private static class RespondForbiddenFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + ResponseDispatch.newInstance(Response.Status.FORBIDDEN).dispatch(handler); + } + } + + private static class ThrowingRequestFilter extends AbstractResource implements RequestFilter { + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + throw new RuntimeException(); + } + } + + private static class ThrowingResponseFilter extends AbstractResource implements ResponseFilter { + @Override + public void filter(final Response response, final Request request) { + throw new RuntimeException(); + } + } + + private static class HeaderRequestFilter extends AbstractResource implements RequestFilter { + private final String key; + private final String val; + + public HeaderRequestFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler handler) { + request.headers().add(key, val); + } + } + + private static class HeaderResponseFilter extends AbstractResource implements ResponseFilter { + private final String key; + private final String val; + + public HeaderResponseFilter(final String key, final String val) { + this.key = key; + this.val = val; + } + + @Override + public void filter(final Response response, final Request request) { + response.headers().add(key, val); + } + } + + public class NullCompletionHandlerFilter extends AbstractResource implements RequestFilter { + private final int responseStatus; + private final String responseMessage; + + public NullCompletionHandlerFilter(final int responseStatus, final String responseMessage) { + this.responseStatus = responseStatus; + this.responseMessage = responseMessage; + } + + @Override + public void filter(final HttpRequest request, final ResponseHandler responseHandler) { + final HttpResponse response = HttpResponse.newInstance(responseStatus); + final ContentChannel channel = responseHandler.handleResponse(response); + final CompletionHandler completionHandler = null; + channel.write(ByteBuffer.wrap(responseMessage.getBytes()), completionHandler); + channel.close(null); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java new file mode 100644 index 00000000000..9c1348004ee --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java @@ -0,0 +1,204 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Key; +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.service.CurrentContainer; +import org.eclipse.jetty.server.HttpConnection; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Steinar Knutsen + * @author bjorncs + */ +public class HttpRequestFactoryTest { + + private static final int LOCAL_PORT = 80; + + @Test + public void testLegalURIs() { + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", null, null)); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertNull(uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "", "")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("http", "host.a1-2-3", "", "")); + assertEquals("http", uri.getScheme()); + assertEquals("host.a1-2-3", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "/:1/../1=.", "")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("/:1/../1=.", uri.getRawPath()); + assertEquals("", uri.getRawQuery()); + } + { + URI uri = HttpRequestFactory.getUri(createMockRequest("https", "host", "", "a=/../&?=")); + assertEquals("https", uri.getScheme()); + assertEquals("host", uri.getHost()); + assertEquals("", uri.getRawPath()); + assertEquals("a=/../&?=", uri.getRawQuery()); + } + } + + @Test + public void testIllegalQuery() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "example.com", "/search", "query=\"contains_quotes\"")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception1() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "?", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception2() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", ".", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_host_throws_requestexception3() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "*", "/foo", "")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + } + } + + @Test + public final void illegal_unicode_in_query_throws_requestexception() { + try { + HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("http", "example.com", "/search", "query=%c0%ae")); + fail("Above statement should throw"); + } catch (RequestException e) { + assertThat(e.getResponseStatus(), is(Response.Status.BAD_REQUEST)); + assertThat(e.getMessage(), equalTo("URL violates RFC 2396: Not valid UTF8! byte C0 in state 0")); + } + } + + @Test + public void request_uri_uses_local_port() { + HttpRequest request = HttpRequestFactory.newJDiscRequest( + new MockContainer(), + createMockRequest("https", "example.com", "/search", "query=value")); + assertEquals(LOCAL_PORT, request.getUri().getPort()); + } + + private static HttpServletRequest createMockRequest(String scheme, String serverName, String path, String queryString) { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpConnection connection = mock(HttpConnection.class); + JDiscServerConnector connector = mock(JDiscServerConnector.class); + when(connector.connectorConfig()).thenReturn(new ConnectorConfig(new ConnectorConfig.Builder().listenPort(LOCAL_PORT))); + when(connector.getLocalPort()).thenReturn(LOCAL_PORT); + when(connection.getCreatedTimeStamp()).thenReturn(System.currentTimeMillis()); + when(connection.getConnector()).thenReturn(connector); + when(request.getAttribute("org.eclipse.jetty.server.HttpConnection")).thenReturn(connection); + when(request.getProtocol()).thenReturn("HTTP/1.1"); + when(request.getScheme()).thenReturn(scheme); + when(request.getServerName()).thenReturn(serverName); + when(request.getRemoteAddr()).thenReturn("127.0.0.1"); + when(request.getRemotePort()).thenReturn(1234); + when(request.getLocalPort()).thenReturn(LOCAL_PORT); + when(request.getMethod()).thenReturn("GET"); + when(request.getQueryString()).thenReturn(queryString); + when(request.getRequestURI()).thenReturn(path); + return request; + } + + private static final class MockContainer implements CurrentContainer { + + @Override + public Container newReference(URI uri) { + return new Container() { + + @Override + public RequestHandler resolveHandler(com.yahoo.jdisc.Request request) { + return null; + } + + @Override + public T getInstance(Key tKey) { + return null; + } + + @Override + public T getInstance(Class tClass) { + return null; + } + + @Override + public ResourceReference refer() { + return References.NOOP_REFERENCE; + } + + @Override + public void release() { + + } + + @Override + public long currentTimeMillis() { + return 0; + } + }; + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java new file mode 100644 index 00000000000..bb92d75bed5 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java @@ -0,0 +1,221 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.http.server.jetty.HttpResponseStatisticsCollector.StatisticsEntry; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.MetaData.Response; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpTransport; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.Callback; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +/** + * @author ollivir + */ +public class HttpResponseStatisticsCollectorTest { + + private Connector connector; + private List monitoringPaths = List.of("/status.html"); + private List searchPaths = List.of("/search"); + private HttpResponseStatisticsCollector collector = new HttpResponseStatisticsCollector(monitoringPaths, searchPaths); + private int httpResponseCode = 500; + + @Test + public void statistics_are_aggregated_by_category() { + testRequest("http", 300, "GET"); + testRequest("http", 301, "GET"); + testRequest("http", 200, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_3XX, 2L); + } + + @Test + public void statistics_are_grouped_by_http_method_and_scheme() { + testRequest("http", 200, "GET"); + testRequest("http", 200, "PUT"); + testRequest("http", 200, "POST"); + testRequest("http", 200, "POST"); + testRequest("http", 404, "GET"); + testRequest("https", 404, "GET"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + testRequest("https", 200, "POST"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "http", "PUT", MetricDefinitions.RESPONSES_2XX, 1L); + assertStatisticsEntryPresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, 2L); + assertStatisticsEntryPresent(stats, "https", "GET", MetricDefinitions.RESPONSES_4XX, 1L); + assertStatisticsEntryPresent(stats, "https", "POST", MetricDefinitions.RESPONSES_2XX, 4L); + } + + @Test + public void statistics_include_grouped_and_single_statuscodes() { + testRequest("http", 401, "GET"); + testRequest("http", 404, "GET"); + testRequest("http", 403, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_4XX, 3L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_401, 1L); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_403, 1L); + + } + + @Test + public void retrieving_statistics_resets_the_counters() { + testRequest("http", 200, "GET"); + testRequest("http", 200, "GET"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 2L); + + testRequest("http", 200, "GET"); + + stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + } + + @Test + public void statistics_include_request_type_dimension() { + testRequest("http", 200, "GET", "/search"); + testRequest("http", 200, "POST", "/search"); + testRequest("http", 200, "POST", "/feed"); + testRequest("http", 200, "GET", "/status.html?foo=bar"); + + var stats = collector.takeStatistics(); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "monitoring", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "read", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, "read", 1L); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "POST", MetricDefinitions.RESPONSES_2XX, "write", 1L); + + testRequest("http", 200, "GET"); + + stats = collector.takeStatistics(); + assertStatisticsEntryPresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, 1L); + } + + @Test + public void request_type_can_be_set_explicitly() { + testRequest("http", 200, "GET", "/search", com.yahoo.jdisc.Request.RequestType.WRITE); + + var stats = collector.takeStatistics(); + assertStatisticsEntryWithRequestTypePresent(stats, "http", "GET", MetricDefinitions.RESPONSES_2XX, "write", 1L); + } + + @Before + public void initializeCollector() throws Exception { + Server server = new Server(); + connector = new AbstractConnector(server, null, null, null, 0) { + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException { + } + + @Override + public Object getTransport() { + return null; + } + }; + collector.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + baseRequest.setHandled(true); + baseRequest.getResponse().setStatus(httpResponseCode); + } + }); + server.setHandler(collector); + server.start(); + } + + private Request testRequest(String scheme, int responseCode, String httpMethod) { + return testRequest(scheme, responseCode, httpMethod, "foo/bar"); + } + private Request testRequest(String scheme, int responseCode, String httpMethod, String path) { + return testRequest(scheme, responseCode, httpMethod, path, null); + } + private Request testRequest(String scheme, int responseCode, String httpMethod, String path, + com.yahoo.jdisc.Request.RequestType explicitRequestType) { + HttpChannel channel = new HttpChannel(connector, new HttpConfiguration(), null, new DummyTransport()); + MetaData.Request metaData = new MetaData.Request(httpMethod, new HttpURI(scheme + "://" + path), HttpVersion.HTTP_1_1, new HttpFields()); + Request req = channel.getRequest(); + if (explicitRequestType != null) + req.setAttribute("requestType", explicitRequestType); + req.setMetaData(metaData); + + this.httpResponseCode = responseCode; + channel.handle(); + return req; + } + + private static void assertStatisticsEntryPresent(List result, String scheme, String method, String name, long expectedValue) { + long value = result.stream() + .filter(entry -> entry.method.equals(method) && entry.scheme.equals(scheme) && entry.name.equals(name)) + .mapToLong(entry -> entry.value) + .findAny() + .orElseThrow(() -> new AssertionError(String.format("Not matching entry in result (scheme=%s, method=%s, name=%s)", scheme, method, name))); + assertThat(value, equalTo(expectedValue)); + } + + private static void assertStatisticsEntryWithRequestTypePresent(List result, String scheme, String method, String name, String requestType, long expectedValue) { + long value = result.stream() + .filter(entry -> entry.method.equals(method) && entry.scheme.equals(scheme) && entry.name.equals(name) && entry.requestType.equals(requestType)) + .mapToLong(entry -> entry.value) + .reduce(Long::sum) + .orElseThrow(() -> new AssertionError(String.format("Not matching entry in result (scheme=%s, method=%s, name=%s, type=%s)", scheme, method, name, requestType))); + assertThat(value, equalTo(expectedValue)); + } + + private final class DummyTransport implements HttpTransport { + @Override + public void send(Response info, boolean head, ByteBuffer content, boolean lastContent, Callback callback) { + callback.succeeded(); + } + + @Override + public boolean isPushSupported() { + return false; + } + + @Override + public boolean isOptimizedForDirectBuffers() { + return false; + } + + @Override + public void push(MetaData.Request request) { + } + + @Override + public void onCompleted() { + } + + @Override + public void abort(Throwable failure) { + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java new file mode 100644 index 00000000000..5659dfc2d3c --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java @@ -0,0 +1,847 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.test.ServerProviderConformanceTest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpVersion; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; +import static org.apache.http.HttpStatus.SC_NOT_FOUND; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.any; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Simon Thoresen Hult + */ +public class HttpServerConformanceTest extends ServerProviderConformanceTest { + + private static final Logger log = Logger.getLogger(HttpServerConformanceTest.class.getName()); + + private static final String REQUEST_CONTENT = "myRequestContent"; + private static final String RESPONSE_CONTENT = "myResponseContent"; + + @SuppressWarnings("LoggerInitializedWithForeignClass") + private static Logger httpRequestDispatchLogger = Logger.getLogger(HttpRequestDispatch.class.getName()); + private static Level httpRequestDispatchLoggerOriginalLevel; + + /* + * Reduce logging of every stack trace for {@link ServerProviderConformanceTest.ConformanceException} thrown. + * This makes the log more readable and the test faster as well. + */ + @BeforeClass + public static void reduceExcessiveLogging() { + httpRequestDispatchLoggerOriginalLevel = httpRequestDispatchLogger.getLevel(); + httpRequestDispatchLogger.setLevel(Level.SEVERE); + } + + @AfterClass + public static void restoreExcessiveLogging() { + httpRequestDispatchLogger.setLevel(httpRequestDispatchLoggerOriginalLevel); + } + + @AfterClass + public static void reportDiagnostics() { + System.out.println( + "After " + HttpServerConformanceTest.class.getSimpleName() + + ": #threads=" + Thread.getAllStackTraces().size()); + } + + @Override + @Test + public void testContainerNotReadyException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), containsString("Container not ready."))) + .execute(); + } + + @Override + @Test + public void testBindingSetNotFoundException() throws Throwable { + new TestRunner().expect(errorWithReason(is(SC_NOT_FOUND), containsString("No binding set named 'unknown'."))) + .execute(); + } + + @Override + @Test + public void testNoBindingSetSelectedException() throws Throwable { + final Pattern reasonPattern = Pattern.compile(".*No binding set selected for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(SC_INTERNAL_SERVER_ERROR), matchesPattern(reasonPattern))) + .execute(); + } + + @Override + @Test + public void testBindingNotFoundException() throws Throwable { + final Pattern contentPattern = Pattern.compile("No binding for URI 'http://.+/status.html'\\."); + new TestRunner().expect(errorWithReason(is(NOT_FOUND), containsPattern(contentPattern))) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestHandlerWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestException() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncCloseResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionWithSyncWriteResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithSyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestNondeterministicExceptionWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(anyOf(successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestExceptionBeforeResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseCloseNoContentWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestExceptionAfterResponseWriteWithAsyncHandleResponse() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner() + .expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentWriteExceptionWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithSyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithNondeterministicAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseWithAsyncFailureAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicException() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWrite() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWrite() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncCompletion() throws Throwable { + new TestRunner().expect(anyOf(success(), serverError(), successNoContent())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncCompletion() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncCompletion() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithSyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithSyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithSyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseNondeterministicExceptionWithAsyncFailure() throws Throwable { + new TestRunner().expect(anyOf(success(), successNoContent(), serverError())) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionBeforeResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(serverError()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseWriteWithAsyncFailure() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testRequestContentCloseExceptionAfterResponseCloseNoContentWithAsyncFailure() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + @Override + @Test + public void testResponseWriteCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionException() throws Throwable { + new TestRunner().expect(success()) + .execute(); + } + + @Override + @Test + public void testResponseCloseCompletionExceptionNoContent() throws Throwable { + new TestRunner().expect(successNoContent()) + .execute(); + } + + private static Matcher success() { + final Matcher expectedStatusCode = is(OK); + final Matcher expectedReasonPhrase = is("OK"); + final Matcher expectedContent = is(RESPONSE_CONTENT); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher successNoContent() { + final Matcher expectedStatusCode = is(OK); + final Matcher expectedReasonPhrase = is("OK"); + final Matcher expectedContent = is(""); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher serverError() { + final Matcher expectedStatusCode = is(INTERNAL_SERVER_ERROR); + final Matcher expectedReasonPhrase = any(String.class); + final Matcher expectedContent = containsString(ConformanceException.class.getSimpleName()); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher errorWithReason( + final Matcher expectedStatusCode, final Matcher expectedReasonPhrase) { + final Matcher expectedContent = any(String.class); + return responseMatcher(expectedStatusCode, expectedReasonPhrase, expectedContent); + } + + private static Matcher responseMatcher( + final Matcher expectedStatusCode, + final Matcher expectedReasonPhrase, + final Matcher expectedContent) { + return new TypeSafeMatcher() { + @Override + public void describeTo(final Description description) { + description.appendText("status code "); + expectedStatusCode.describeTo(description); + description.appendText(", reason "); + expectedReasonPhrase.describeTo(description); + description.appendText(" and content "); + expectedContent.describeTo(description); + } + + @Override + protected void describeMismatchSafely( + final ResponseGist response, final Description mismatchDescription) { + mismatchDescription.appendText(" status code was ").appendValue(response.getStatusCode()) + .appendText(", reason was ").appendValue(response.getReasonPhrase()) + .appendText(" and content was ").appendValue(response.getContent()); + } + + @Override + protected boolean matchesSafely(final ResponseGist response) { + return expectedStatusCode.matches(response.getStatusCode()) + && expectedReasonPhrase.matches(response.getReasonPhrase()) + && expectedContent.matches(response.getContent()); + } + }; + } + + private static class ResponseGist { + private final int statusCode; + private final String content; + private String reasonPhrase; + + public ResponseGist(int statusCode, String reasonPhrase, String content) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() { + return content; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + @Override + public String toString() { + return "ResponseGist {" + + " statusCode=" + statusCode + + " reasonPhrase=" + reasonPhrase + + " content=" + content + + " }"; + } + } + + private class TestRunner implements Adapter> { + + private Matcher expectedResponse = null; + HttpVersion requestVersion; + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + void execute() throws Throwable { + requestVersion = HttpVersion.HTTP_1_0; + runTest(this); + + requestVersion = HttpVersion.HTTP_1_1; + runTest(this); + + executorService.shutdown(); + } + + TestRunner expect(final Matcher matcher) { + expectedResponse = matcher; + return this; + } + + @Override + public Module newConfigModule() { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class) + .toInstance(new FilterBindings.Builder().build()); + bind(ServerConfig.class) + .toInstance(new ServerConfig(new ServerConfig.Builder())); + bind(ServletPathsConfig.class) + .toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ConnectionLog.class) + .toInstance(new VoidConnectionLog()); + bind(RequestLog.class) + .toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule()); + } + + @Override + public Class getServerProviderClass() { + return JettyHttpServer.class; + } + + @Override + public ClientProxy newClient(final JettyHttpServer server) throws Throwable { + return new ClientProxy(server.getListenPort(), requestVersion); + } + + @Override + public Future executeRequest( + final ClientProxy client, + final boolean withRequestContent) throws Throwable { + final HttpUriRequest request; + final URI requestUri = URI.create("http://localhost:" + client.listenPort + "/status.html"); + if (!withRequestContent) { + HttpGet httpGet = new HttpGet(requestUri); + httpGet.setProtocolVersion(client.requestVersion); + request = httpGet; + } else { + final HttpPost post = new HttpPost(requestUri); + post.setEntity(new StringEntity(REQUEST_CONTENT, StandardCharsets.UTF_8)); + post.setProtocolVersion(client.requestVersion); + request = post; + } + log.fine(() -> "executorService:" + + " .isShutDown()=" + executorService.isShutdown() + + " .isTerminated()=" + executorService.isTerminated()); + return executorService.submit(() -> client.delegate.execute(request)); + } + + @Override + public Iterable newResponseContent() { + return Collections.singleton(StandardCharsets.UTF_8.encode(RESPONSE_CONTENT)); + } + + @Override + public void validateResponse(final Future responseFuture) throws Throwable { + final HttpResponse response = responseFuture.get(); + final ResponseGist responseGist = new ResponseGist( + response.getStatusLine().getStatusCode(), + response.getStatusLine().getReasonPhrase(), + EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + assertThat(responseGist, expectedResponse); + } + } + + private static class ClientProxy { + + final HttpClient delegate; + final int listenPort; + final ProtocolVersion requestVersion; + + ClientProxy(final int listenPort, final HttpVersion requestVersion) { + this.delegate = HttpClientBuilder.create().build(); + this.requestVersion = requestVersion; + this.listenPort = listenPort; + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java new file mode 100644 index 00000000000..c00525a3ddc --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -0,0 +1,1201 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; +import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingSetSelector; +import com.yahoo.jdisc.application.MetricConsumer; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.CompletionHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseDispatch; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ConnectorConfig.Throttling; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpResponse; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.server.jetty.TestDrivers.TlsClientAuth; +import com.yahoo.jdisc.service.BindingSetNotFoundException; +import com.yahoo.security.KeyUtils; +import com.yahoo.security.Pkcs10Csr; +import com.yahoo.security.Pkcs10CsrBuilder; +import com.yahoo.security.SslContextBuilder; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.X509CertificateUtils; +import com.yahoo.security.tls.TlsContext; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.content.StringBody; +import org.assertj.core.api.Assertions; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V1; +import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.server.handler.AbstractHandlerContainer; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.security.auth.x500.X500Principal; +import java.io.IOException; +import java.math.BigInteger; +import java.net.BindException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static com.yahoo.jdisc.Response.Status.GATEWAY_TIMEOUT; +import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR; +import static com.yahoo.jdisc.Response.Status.NOT_FOUND; +import static com.yahoo.jdisc.Response.Status.OK; +import static com.yahoo.jdisc.Response.Status.REQUEST_URI_TOO_LONG; +import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED; +import static com.yahoo.jdisc.Response.Status.UNSUPPORTED_MEDIA_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONNECTION; +import static com.yahoo.jdisc.http.HttpHeaders.Names.CONTENT_TYPE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE; +import static com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING; +import static com.yahoo.jdisc.http.HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED; +import static com.yahoo.jdisc.http.HttpHeaders.Values.CLOSE; +import static com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import static com.yahoo.security.KeyAlgorithm.EC; +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static org.cthul.matchers.CthulMatchers.containsPattern; +import static org.cthul.matchers.CthulMatchers.matchesPattern; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author Oyvind Bakksjo + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class HttpServerTest { + + private static final Logger log = Logger.getLogger(HttpServerTest.class.getName()); + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Test + public void requireThatServerCanListenToRandomPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + assertNotEquals(0, driver.server().getListenPort()); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanNotListenToBoundPort() throws Exception { + final TestDriver driver = TestDrivers.newInstance(mockRequestHandler()); + try { + TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .listenPort(driver.server().getListenPort()) + ); + } catch (final Throwable t) { + assertThat(t.getCause(), instanceOf(BindException.class)); + } + assertTrue(driver.close()); + } + + @Test + public void requireThatBindingSetNotFoundReturns404() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder() + .developerMode(true), + new ConnectorConfig.Builder(), + newBindingSetSelector("unknown")); + driver.client().get("/status.html") + .expectStatusCode(is(NOT_FOUND)) + .expectContent(containsPattern(Pattern.compile( + Pattern.quote(BindingSetNotFoundException.class.getName()) + + ": No binding set named 'unknown'\\.\n\tat .+", + Pattern.DOTALL | Pattern.MULTILINE))); + assertTrue(driver.close()); + } + + @Test + public void requireThatTooLongInitLineReturns414() throws Exception { + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .requestHeaderSize(1)); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_URI_TOO_LONG)); + assertTrue(driver.close()); + } + + @Test + public void requireThatAccessLogIsCalledForRequestRejectedByJetty() throws Exception { + BlockingQueueRequestLog requestLogMock = new BlockingQueueRequestLog(); + final TestDriver driver = TestDrivers.newConfiguredInstance( + mockRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder().requestHeaderSize(1), + binder -> binder.bind(RequestLog.class).toInstance(requestLogMock)); + driver.client().get("/status.html") + .expectStatusCode(is(REQUEST_URI_TOO_LONG)); + RequestLogEntry entry = requestLogMock.poll(Duration.ofSeconds(30)); + assertEquals(414, entry.statusCode().getAsInt()); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerCanEcho() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanEchoCompressed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + SimpleHttpClient client = driver.newClient(true); + client.get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanHandleMultipleRequests() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostDoesNotRemoveContentByDefault() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostKeepsContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), false); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}foo=bar")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostRemovesContentWhenConfiguredTo() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{foo=[bar]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithCharsetSpecifiedWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final String requestContent = generateContent('a', 30); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(X_DISABLE_CHUNKING, "true") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=UTF-8") + .setContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatEmptyFormPostWorks() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b&c=d") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatUriParametersAreParsed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{a=[b], c=[d]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormAndUriParametersAreMerged() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html?a=b&c=d1") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("c=d2&e=f") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{a=[b], c=[d1, d2], e=[f]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormCharsetIsHonored() throws Exception { + final TestDriver driver = newDriverWithFormPostContentRemoved(new ParameterPrinterRequestHandler(), true); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=ISO-8859-1") + .setBinaryContent(new byte[]{66, (byte) 230, 114, 61, 98, 108, (byte) 229}) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(is("{B\u00e6r=[bl\u00e5]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatUnknownFormCharsetIsTreatedAsBadRequest() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED + ";charset=FLARBA-GARBA-7") + .setContent("a=b") + .execute(); + response.expectStatusCode(is(UNSUPPORTED_MEDIA_TYPE)); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithPercentEncodedContentIsDecoded() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("%20%3D%C3%98=%22%25+") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith("{ =\u00d8=[\"% ]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatFormPostWithThrowingHandlerIsExceptionSafe() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ThrowingHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setContent("a=b") + .execute(); + response.expectStatusCode(is(INTERNAL_SERVER_ERROR)); + assertTrue(driver.close()); + } + + @Test + public void requireThatMultiPostWorks() throws Exception { + // This is taken from tcpdump of bug 5433352 and reassembled here to see that httpserver passes things on. + final String startTxtContent = "this is a test for POST."; + final String updaterConfContent + = "identifier = updater\n" + + "server_type = gds\n"; + final TestDriver driver = TestDrivers.newInstance(new EchoRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .setMultipartContent( + newFileBody("", "start.txt", startTxtContent), + newFileBody("", "updater.conf", updaterConfContent)) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString(startTxtContent)) + .expectContent(containsString(updaterConfContent)); + } + + @Test + public void requireThatRequestCookiesAreReceived() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookiePrinterRequestHandler()); + final ResponseValidator response = + driver.client().newPost("/status.html") + .addHeader(COOKIE, "foo=bar") + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(containsString("[foo=bar]")); + assertTrue(driver.close()); + } + + @Test + public void requireThatSetCookieHeaderIsCorrect() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new CookieSetterRequestHandler( + new Cookie("foo", "bar") + .setDomain(".localhost") + .setHttpOnly(true) + .setPath("/foopath") + .setSecure(true))); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader("Set-Cookie", + is("foo=bar; Path=/foopath; Domain=.localhost; Secure; HttpOnly")); + assertTrue(driver.close()); + } + + @Test + public void requireThatTimeoutWorks() throws Exception { + final UnresponsiveHandler requestHandler = new UnresponsiveHandler(); + final TestDriver driver = TestDrivers.newInstance(requestHandler); + driver.client().get("/status.html") + .expectStatusCode(is(GATEWAY_TIMEOUT)); + ResponseDispatch.newInstance(OK).dispatch(requestHandler.responseHandler); + assertTrue(driver.close()); + } + + // Header with no value is disallowed by https://tools.ietf.org/html/rfc7230#section-3.2 + // Details in https://github.com/eclipse/jetty.project/issues/1116 + @Test + public void requireThatHeaderWithNullValueIsOmitted() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", null)); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader("X-Foo"); + assertTrue(driver.close()); + } + + // Header with empty value is allowed by https://tools.ietf.org/html/rfc7230#section-3.2 + // Details in https://github.com/eclipse/jetty.project/issues/1116 + @Test + public void requireThatHeaderWithEmptyValueIsAllowed() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler("X-Foo", "")); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader("X-Foo", is("")); + assertTrue(driver.close()); + } + + @Test + public void requireThatNoConnectionHeaderMeansKeepAliveInHttp11KeepAliveDisabled() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new EchoWithHeaderRequestHandler(CONNECTION, CLOSE)); + driver.client().get("/status.html") + .expectHeader(CONNECTION, is(CLOSE)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatConnectionIsClosedAfterXRequests() throws Exception { + final int MAX_KEEPALIVE_REQUESTS = 100; + final TestDriver driver = TestDrivers.newConfiguredInstance(new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder().maxRequestsPerConnection(MAX_KEEPALIVE_REQUESTS)); + for (int i = 0; i < MAX_KEEPALIVE_REQUESTS - 1; i++) { + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectNoHeader(CONNECTION); + } + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectHeader(CONNECTION, is(CLOSE)); + assertTrue(driver.close()); + } + + @Test + public void requireThatServerCanRespondToSslRequest() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + + final TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatTlsClientAuthenticationEnforcerRejectsRequestsForNonWhitelistedPaths() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + + SSLContext trustStoreOnlyCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + + new SimpleHttpClient(trustStoreOnlyCtx, driver.server().getListenPort(), false) + .get("/dummy.html") + .expectStatusCode(is(UNAUTHORIZED)); + + assertTrue(driver.close()); + } + + @Test + public void requireThatTlsClientAuthenticationEnforcerAllowsRequestForWhitelistedPaths() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.WANT); + + SSLContext trustStoreOnlyCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + + new SimpleHttpClient(trustStoreOnlyCtx, driver.server().getListenPort(), false) + .get("/status.html") + .expectStatusCode(is(OK)); + + assertTrue(driver.close()); + } + + @Test + public void requireThatConnectedAtReturnsNonZero() throws Exception { + final TestDriver driver = TestDrivers.newInstance(new ConnectedAtRequestHandler()); + driver.client().get("/status.html") + .expectStatusCode(is(OK)) + .expectContent(matchesPattern("\\d{13,}")); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatGzipEncodingRequestsAreAutomaticallyDecompressed() throws Exception { + TestDriver driver = TestDrivers.newInstance(new ParameterPrinterRequestHandler()); + String requestContent = generateContent('a', 30); + ResponseValidator response = driver.client().newPost("/status.html") + .addHeader(CONTENT_TYPE, APPLICATION_X_WWW_FORM_URLENCODED) + .setGzipContent(requestContent) + .execute(); + response.expectStatusCode(is(OK)) + .expectContent(startsWith('{' + requestContent + "=[]}")); + assertTrue(driver.close()); + } + + @Test + public void requireThatResponseStatsAreCollected() throws Exception { + RequestTypeHandler handler = new RequestTypeHandler(); + TestDriver driver = TestDrivers.newInstance(handler); + HttpResponseStatisticsCollector statisticsCollector = ((AbstractHandlerContainer) driver.server().server().getHandler()) + .getChildHandlerByClass(HttpResponseStatisticsCollector.class); + + { + List stats = statisticsCollector.takeStatistics(); + assertEquals(0, stats.size()); + } + + { + driver.client().newPost("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("http", entry.scheme); + assertEquals("POST", entry.method); + assertEquals("http.status.2xx", entry.name); + assertEquals("write", entry.requestType); + assertEquals(1, entry.value); + } + + { + driver.client().newGet("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("http", entry.scheme); + assertEquals("GET", entry.method); + assertEquals("http.status.2xx", entry.name); + assertEquals("read", entry.requestType); + assertEquals(1, entry.value); + } + + { + handler.setRequestType(Request.RequestType.READ); + driver.client().newPost("/status.html").execute(); + var entry = waitForStatistics(statisticsCollector); + assertEquals("Handler overrides request type", "read", entry.requestType); + } + + assertTrue(driver.close()); + } + + private HttpResponseStatisticsCollector.StatisticsEntry waitForStatistics(HttpResponseStatisticsCollector + statisticsCollector) { + List entries = Collections.emptyList(); + int tries = 0; + while (entries.isEmpty() && tries < 10000) { + entries = statisticsCollector.takeStatistics(); + if (entries.isEmpty()) + try {Thread.sleep(100); } catch (InterruptedException e) {} + tries++; + } + assertEquals(1, entries.size()); + return entries.get(0); + } + + @Test + public void requireThatConnectionThrottleDoesNotBlockConnectionsBelowThreshold() throws Exception { + TestDriver driver = TestDrivers.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .throttling(new Throttling.Builder() + .enabled(true) + .maxAcceptRate(10) + .maxHeapUtilization(1.0) + .maxConnections(10))); + driver.client().get("/status.html") + .expectStatusCode(is(OK)); + assertTrue(driver.close()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientIsMissingCertificateOnHandshake() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .build(); + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: bad_certificate"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.MISSING_CLIENT_CERT.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleTlsVersion() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + boolean tlsv11Enabled = List.of(clientCtx.getDefaultSSLParameters().getProtocols()).contains("TLSv1.1"); + assumeTrue("TLSv1.1 must be enabled in installed JDK", tlsv11Enabled); + + assertHttpsRequestTriggersSslHandshakeException(driver, clientCtx, "TLSv1.1", null, "protocol"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_PROTOCOLS.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesIncompatibleCiphers() throws IOException { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(certificateFile, privateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(certificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "Received fatal alert: handshake_failure"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INCOMPATIBLE_CIPHERS.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesInvalidCertificateInHandshake() throws IOException { + Path serverPrivateKeyFile = tmpFolder.newFile().toPath(); + Path serverCertificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(serverPrivateKeyFile, serverCertificateFile); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(serverCertificateFile, serverPrivateKeyFile, metricConsumer, connectionLog); + + Path clientPrivateKeyFile = tmpFolder.newFile().toPath(); + Path clientCertificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(clientPrivateKeyFile, clientCertificateFile); + + SSLContext clientCtx = new SslContextBuilder() + .withKeyStore(clientPrivateKeyFile, clientCertificateFile) + .withTrustStore(serverCertificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + assertSslHandshakeFailurePresent( + connectionLog.logEntries().get(0), SSLHandshakeException.class, SslHandshakeFailure.INVALID_CLIENT_CERT.failureType()); + } + + @Test + public void requireThatMetricIsIncrementedWhenClientUsesExpiredCertificateInHandshake() throws IOException { + Path rootPrivateKeyFile = tmpFolder.newFile().toPath(); + Path rootCertificateFile = tmpFolder.newFile().toPath(); + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + Instant notAfter = Instant.now().minus(100, ChronoUnit.DAYS); + generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile, privateKeyFile, certificateFile, notAfter); + var metricConsumer = new MetricConsumerMock(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslTestDriver(rootCertificateFile, rootPrivateKeyFile, metricConsumer, connectionLog); + + SSLContext clientCtx = new SslContextBuilder() + .withTrustStore(rootCertificateFile) + .withKeyStore(privateKeyFile, certificateFile) + .build(); + + assertHttpsRequestTriggersSslHandshakeException( + driver, clientCtx, null, null, "Received fatal alert: certificate_unknown"); + verify(metricConsumer.mockitoMock(), atLeast(1)) + .add(MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, 1L, MetricConsumerMock.STATIC_CONTEXT); + assertTrue(driver.close()); + Assertions.assertThat(connectionLog.logEntries()).hasSize(1); + + } + + @Test + public void requireThatProxyProtocolIsAcceptedAndActualRemoteAddressStoredInAccessLog() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, false); + + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + sendJettyClientRequest(driver, certificateFile, new V1.Tag(proxiedRemoteAddress, proxiedRemotePort)); + sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, proxiedRemotePort)); + assertTrue(driver.close()); + + assertEquals(2, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, proxiedRemotePort); + Assertions.assertThat(connectionLog.logEntries()).hasSize(2); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), proxiedRemoteAddress, proxiedRemotePort); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, proxiedRemotePort); + } + + @Test + public void requireThatConnectorWithProxyProtocolMixedEnabledAcceptsBothProxyProtocolAndHttps() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, /*mixedMode*/connectionLog, true); + + String proxiedRemoteAddress = "192.168.0.100"; + sendJettyClientRequest(driver, certificateFile, null); + sendJettyClientRequest(driver, certificateFile, new V2.Tag(proxiedRemoteAddress, 12345)); + assertTrue(driver.close()); + + assertEquals(2, requestLogMock.entries().size()); + assertLogEntryHasRemote(requestLogMock.entries().get(0), "127.0.0.1", 0); + assertLogEntryHasRemote(requestLogMock.entries().get(1), proxiedRemoteAddress, 0); + Assertions.assertThat(connectionLog.logEntries()).hasSize(2); + assertLogEntryHasRemote(connectionLog.logEntries().get(0), null, 0); + assertLogEntryHasRemote(connectionLog.logEntries().get(1), proxiedRemoteAddress, 12345); + } + + @Test + public void requireThatJdiscLocalPortPropertyIsNotOverriddenByProxyProtocol() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryRequestLog requestLogMock = new InMemoryRequestLog(); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + TestDriver driver = createSslWithProxyProtocolTestDriver(certificateFile, privateKeyFile, requestLogMock, connectionLog, /*mixedMode*/false); + + String proxiedRemoteAddress = "192.168.0.100"; + int proxiedRemotePort = 12345; + String proxyLocalAddress = "10.0.0.10"; + int proxyLocalPort = 23456; + V2.Tag v2Tag = new V2.Tag(V2.Tag.Command.PROXY, null, V2.Tag.Protocol.STREAM, + proxiedRemoteAddress, proxiedRemotePort, proxyLocalAddress, proxyLocalPort, null); + ContentResponse response = sendJettyClientRequest(driver, certificateFile, v2Tag); + assertTrue(driver.close()); + + int clientPort = Integer.parseInt(response.getHeaders().get("Jdisc-Local-Port")); + assertNotEquals(proxyLocalPort, clientPort); + assertNotEquals(proxyLocalPort, connectionLog.logEntries().get(0).localPort().get().intValue()); + } + + @Test + public void requireThatConnectionIsTrackedInConnectionLog() throws Exception { + Path privateKeyFile = tmpFolder.newFile().toPath(); + Path certificateFile = tmpFolder.newFile().toPath(); + generatePrivateKeyAndCertificate(privateKeyFile, certificateFile); + InMemoryConnectionLog connectionLog = new InMemoryConnectionLog(); + Module overrideModule = binder -> binder.bind(ConnectionLog.class).toInstance(connectionLog); + TestDriver driver = TestDrivers.newInstanceWithSsl(new EchoRequestHandler(), certificateFile, privateKeyFile, TlsClientAuth.NEED, overrideModule); + int listenPort = driver.server().getListenPort(); + driver.client().get("/status.html"); + assertTrue(driver.close()); + List logEntries = connectionLog.logEntries(); + Assertions.assertThat(logEntries).hasSize(1); + ConnectionLogEntry logEntry = logEntries.get(0); + assertEquals(4, UUID.fromString(logEntry.id()).version()); + Assertions.assertThat(logEntry.timestamp()).isAfter(Instant.EPOCH); + Assertions.assertThat(logEntry.requests()).hasValue(1L); + Assertions.assertThat(logEntry.responses()).hasValue(1L); + Assertions.assertThat(logEntry.peerAddress()).hasValue("127.0.0.1"); + Assertions.assertThat(logEntry.localAddress()).hasValue("127.0.0.1"); + Assertions.assertThat(logEntry.localPort()).hasValue(listenPort); + Assertions.assertThat(logEntry.httpBytesReceived()).hasValueSatisfying(value -> Assertions.assertThat(value).isPositive()); + Assertions.assertThat(logEntry.httpBytesSent()).hasValueSatisfying(value -> Assertions.assertThat(value).isPositive()); + Assertions.assertThat(logEntry.sslProtocol()).hasValueSatisfying(TlsContext.ALLOWED_PROTOCOLS::contains); + Assertions.assertThat(logEntry.sslPeerSubject()).hasValue("CN=localhost"); + Assertions.assertThat(logEntry.sslCipherSuite()).hasValueSatisfying(cipher -> Assertions.assertThat(cipher).isNotBlank()); + Assertions.assertThat(logEntry.sslSessionId()).hasValueSatisfying(sessionId -> Assertions.assertThat(sessionId).hasSize(64)); + Assertions.assertThat(logEntry.sslPeerNotBefore()).hasValue(Instant.EPOCH); + Assertions.assertThat(logEntry.sslPeerNotAfter()).hasValue(Instant.EPOCH.plus(100_000, ChronoUnit.DAYS)); + } + + private ContentResponse sendJettyClientRequest(TestDriver testDriver, Path certificateFile, Object tag) + throws Exception { + HttpClient client = createJettyHttpClient(certificateFile); + try { + int maxAttempts = 3; + for (int attempt = 0; attempt < maxAttempts; attempt++) { + try { + ContentResponse response = client.newRequest(URI.create("https://localhost:" + testDriver.server().getListenPort() + "/")) + .tag(tag) + .send(); + assertEquals(200, response.getStatus()); + return response; + } catch (ExecutionException e) { + // Retry when the server closes the connection before the TLS handshake is completed. This have been observed in CI. + // We have been unable to reproduce this locally. The cause is therefor currently unknown. + log.log(Level.WARNING, String.format("Attempt %d failed: %s", attempt, e.getMessage()), e); + Thread.sleep(10); + } + } + throw new AssertionError("Failed to send request, see log for details"); + } finally { + client.stop(); + } + } + + // Using Jetty's http client as Apache httpclient does not support the proxy-protocol v1/v2. + private static HttpClient createJettyHttpClient(Path certificateFile) throws Exception { + SslContextFactory.Client clientSslCtxFactory = new SslContextFactory.Client(); + clientSslCtxFactory.setHostnameVerifier(NoopHostnameVerifier.INSTANCE); + clientSslCtxFactory.setSslContext(new SslContextBuilder().withTrustStore(certificateFile).build()); + + HttpClient client = new HttpClient(clientSslCtxFactory); + client.start(); + return client; + } + + private static void assertLogEntryHasRemote(RequestLogEntry entry, String expectedAddress, int expectedPort) { + assertEquals(expectedAddress, entry.peerAddress().get()); + if (expectedPort > 0) { + assertEquals(expectedPort, entry.peerPort().getAsInt()); + } + } + + private static void assertLogEntryHasRemote(ConnectionLogEntry entry, String expectedAddress, int expectedPort) { + if (expectedAddress != null) { + Assertions.assertThat(entry.remoteAddress()).hasValue(expectedAddress); + } else { + Assertions.assertThat(entry.remoteAddress()).isEmpty(); + } + if (expectedPort > 0) { + Assertions.assertThat(entry.remotePort()).hasValue(expectedPort); + } else { + Assertions.assertThat(entry.remotePort()).isEmpty(); + } + } + + private static void assertSslHandshakeFailurePresent( + ConnectionLogEntry entry, Class expectedException, String expectedType) { + Assertions.assertThat(entry.sslHandshakeFailure()).isPresent(); + ConnectionLogEntry.SslHandshakeFailure failure = entry.sslHandshakeFailure().get(); + assertEquals(expectedType, failure.type()); + ExceptionEntry exceptionEntry = failure.exceptionChain().get(0); + assertEquals(expectedException.getName(), exceptionEntry.name()); + } + + private static TestDriver createSslWithProxyProtocolTestDriver( + Path certificateFile, Path privateKeyFile, RequestLog requestLog, + ConnectionLog connectionLog, boolean mixedMode) { + ConnectorConfig.Builder connectorConfig = new ConnectorConfig.Builder() + .proxyProtocol(new ConnectorConfig.ProxyProtocol.Builder() + .enabled(true) + .mixedMode(mixedMode)) + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .privateKeyFile(privateKeyFile.toString()) + .certificateFile(certificateFile.toString()) + .caCertificateFile(certificateFile.toString())); + return TestDrivers.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), + connectorConfig, + binder -> { + binder.bind(RequestLog.class).toInstance(requestLog); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }); + } + + private static TestDriver createSslTestDriver( + Path serverCertificateFile, Path serverPrivateKeyFile, MetricConsumerMock metricConsumer, InMemoryConnectionLog connectionLog) throws IOException { + Module extraModule = binder -> { + binder.bind(MetricConsumer.class).toInstance(metricConsumer.mockitoMock()); + binder.bind(ConnectionLog.class).toInstance(connectionLog); + }; + return TestDrivers.newInstanceWithSsl( + new EchoRequestHandler(), serverCertificateFile, serverPrivateKeyFile, TlsClientAuth.NEED, extraModule); + } + + private static void assertHttpsRequestTriggersSslHandshakeException( + TestDriver testDriver, + SSLContext sslContext, + String protocolOverride, + String cipherOverride, + String expectedExceptionSubstring) throws IOException { + List protocols = protocolOverride != null ? List.of(protocolOverride) : null; + List ciphers = cipherOverride != null ? List.of(cipherOverride) : null; + try (var client = new SimpleHttpClient(sslContext, protocols, ciphers, testDriver.server().getListenPort(), false)) { + client.get("/status.html"); + fail("SSLHandshakeException expected"); + } catch (SSLHandshakeException e) { + assertThat(e.getMessage(), containsString(expectedExceptionSubstring)); + } catch (SSLException e) { + // This exception is thrown if Apache httpclient's write thread detects the handshake failure before the read thread. + log.log(Level.WARNING, "Client failed to get a proper TLS handshake response: " + e.getMessage(), e); + // Only ignore a subset of exceptions + assertThat(e.getMessage(), anyOf(containsString("readHandshakeRecord"), containsString("Broken pipe"))); + } + } + + private static void generatePrivateKeyAndCertificate(Path privateKeyFile, Path certificateFile) throws IOException { + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, new X500Principal("CN=localhost"), Instant.EPOCH, Instant.EPOCH.plus(100_000, ChronoUnit.DAYS), SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } + + private static void generatePrivateKeyAndCertificate(Path rootPrivateKeyFile, Path rootCertificateFile, + Path privateKeyFile, Path certificateFile, Instant notAfter) throws IOException { + generatePrivateKeyAndCertificate(rootPrivateKeyFile, rootCertificateFile); + X509Certificate rootCertificate = X509CertificateUtils.fromPem(Files.readString(rootCertificateFile)); + PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(Files.readString(rootPrivateKeyFile)); + + KeyPair keyPair = KeyUtils.generateKeypair(EC); + Files.writeString(privateKeyFile, KeyUtils.toPem(keyPair.getPrivate())); + Pkcs10Csr csr = Pkcs10CsrBuilder.fromKeypair(new X500Principal("CN=myclient"), keyPair, SHA256_WITH_ECDSA).build(); + X509Certificate certificate = X509CertificateBuilder + .fromCsr(csr, rootCertificate.getSubjectX500Principal(), Instant.EPOCH, notAfter, privateKey, SHA256_WITH_ECDSA, BigInteger.ONE) + .build(); + Files.writeString(certificateFile, X509CertificateUtils.toPem(certificate)); + } + + private static RequestHandler mockRequestHandler() { + final RequestHandler mockRequestHandler = mock(RequestHandler.class); + when(mockRequestHandler.refer()).thenReturn(References.NOOP_REFERENCE); + return mockRequestHandler; + } + + private static String generateContent(final char c, final int len) { + final StringBuilder ret = new StringBuilder(len); + for (int i = 0; i < len; ++i) { + ret.append(c); + } + return ret.toString(); + } + + private static TestDriver newDriverWithFormPostContentRemoved(RequestHandler requestHandler, + boolean removeFormPostBody) throws Exception { + return TestDrivers.newConfiguredInstance( + requestHandler, + new ServerConfig.Builder() + .removeRawPostBodyForWwwUrlEncodedPost(removeFormPostBody), + new ConnectorConfig.Builder()); + } + + private static FormBodyPart newFileBody(final String parameterName, final String fileName, final String fileContent) { + return new FormBodyPart( + parameterName, + new StringBody(fileContent, ContentType.TEXT_PLAIN) { + @Override + public String getFilename() { + return fileName; + } + + @Override + public String getTransferEncoding() { + return "binary"; + } + + @Override + public String getMimeType() { + return ""; + } + + @Override + public String getCharset() { + return null; + } + }); + } + + private static class ConnectedAtRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpRequest httpRequest = (HttpRequest)request; + final String connectedAt = String.valueOf(httpRequest.getConnectedAt(TimeUnit.MILLISECONDS)); + final ContentChannel ch = handler.handleResponse(new Response(OK)); + ch.write(ByteBuffer.wrap(connectedAt.getBytes(StandardCharsets.UTF_8)), null); + ch.close(null); + return null; + } + } + + private static class CookieSetterRequestHandler extends AbstractRequestHandler { + + final Cookie cookie; + + CookieSetterRequestHandler(final Cookie cookie) { + this.cookie = cookie; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final HttpResponse response = HttpResponse.newInstance(OK); + response.encodeSetCookieHeader(Collections.singletonList(cookie)); + ResponseDispatch.newInstance(response).dispatch(handler); + return null; + } + } + + private static class CookiePrinterRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final List cookies = new ArrayList<>(((HttpRequest)request).decodeCookieHeader()); + Collections.sort(cookies, new CookieComparator()); + final ContentChannel out = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + out.write(StandardCharsets.UTF_8.encode(cookies.toString()), null); + out.close(null); + return null; + } + } + + private static class ParameterPrinterRequestHandler extends AbstractRequestHandler { + + private static final CompletionHandler NULL_COMPLETION_HANDLER = null; + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + Map> parameters = new TreeMap<>(((HttpRequest)request).parameters()); + ContentChannel responseContentChannel = ResponseDispatch.newInstance(Response.Status.OK).connect(handler); + responseContentChannel.write(ByteBuffer.wrap(parameters.toString().getBytes(StandardCharsets.UTF_8)), + NULL_COMPLETION_HANDLER); + + // Have the request content written back to the response. + return responseContentChannel; + } + } + + private static class RequestTypeHandler extends AbstractRequestHandler { + + private Request.RequestType requestType = null; + + public void setRequestType(Request.RequestType requestType) { + this.requestType = requestType; + } + + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + Response response = new Response(OK); + response.setRequestType(requestType); + return handler.handleResponse(response); + } + } + + private static class ThrowingHandler extends AbstractRequestHandler { + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + throw new RuntimeException("Deliberately thrown exception"); + } + } + + private static class UnresponsiveHandler extends AbstractRequestHandler { + + ResponseHandler responseHandler; + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + request.setTimeout(100, TimeUnit.MILLISECONDS); + responseHandler = handler; + return null; + } + } + + private static class EchoRequestHandler extends AbstractRequestHandler { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + int port = request.getUri().getPort(); + Response response = new Response(OK); + response.headers().put("Jdisc-Local-Port", Integer.toString(port)); + return handler.handleResponse(response); + } + } + + private static class EchoWithHeaderRequestHandler extends AbstractRequestHandler { + + final String headerName; + final String headerValue; + + EchoWithHeaderRequestHandler(final String headerName, final String headerValue) { + this.headerName = headerName; + this.headerValue = headerValue; + } + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + final Response response = new Response(OK); + response.headers().add(headerName, headerValue); + return handler.handleResponse(response); + } + } + + private static Module newBindingSetSelector(final String setName) { + return new AbstractModule() { + + @Override + protected void configure() { + bind(BindingSetSelector.class).toInstance(new BindingSetSelector() { + + @Override + public String select(final URI uri) { + return setName; + } + }); + } + }; + } + + private static class CookieComparator implements Comparator { + + @Override + public int compare(final Cookie lhs, final Cookie rhs) { + return lhs.getName().compareTo(rhs.getName()); + } + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java new file mode 100644 index 00000000000..6d1baf0423f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java @@ -0,0 +1,25 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.ConnectionLogEntry; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A {@link ConnectionLog} that aggregates log entries in memory + * + * @author bjorncs + */ +class InMemoryConnectionLog implements ConnectionLog { + + private final List logEntries = new CopyOnWriteArrayList<>(); + + @Override + public void log(ConnectionLogEntry entry) { + logEntries.add(entry); + } + + List logEntries() { return logEntries; } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java new file mode 100644 index 00000000000..b87ec5e8b8b --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java @@ -0,0 +1,20 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * @author bjorncs + */ +public class InMemoryRequestLog implements RequestLog { + + private final List entries = new CopyOnWriteArrayList<>(); + + @Override public void log(RequestLogEntry entry) { entries.add(entry); } + + List entries() { return entries; } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java new file mode 100644 index 00000000000..230f59cbb34 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java @@ -0,0 +1,80 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.HttpTrace; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; + +import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED; +import static com.yahoo.jdisc.Response.Status.OK; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Simon Thoresen Hult + */ +public class JDiscHttpServletTest { + + @Test + public void requireThatServerRespondsToAllMethods() throws Exception { + final TestDriver driver = TestDrivers.newInstance(newEchoHandler()); + final URI uri = driver.client().newUri("/status.html"); + driver.client().execute(new HttpGet(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPost(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpHead(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPut(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpDelete(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpOptions(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpTrace(uri)) + .expectStatusCode(is(OK)); + driver.client().execute(new HttpPatch(uri)) + .expectStatusCode(is(OK)); + assertThat(driver.close(), is(true)); + } + + @Test + public void requireThatServerResponds405ToUnknownMethods() throws IOException { + TestDriver driver = TestDrivers.newInstance(newEchoHandler()); + final URI uri = driver.client().newUri("/status.html"); + driver.client().execute(new UnknownMethodHttpRequest(uri)) + .expectStatusCode(is(METHOD_NOT_ALLOWED)); + assertThat(driver.close(), is(true)); + } + + private static RequestHandler newEchoHandler() { + return new AbstractRequestHandler() { + + @Override + public ContentChannel handleRequest(final Request request, final ResponseHandler handler) { + return handler.handleResponse(new Response(OK)); + } + }; + } + + private static class UnknownMethodHttpRequest extends HttpRequestBase { + UnknownMethodHttpRequest(URI uri) { setURI(uri); } + @Override public String getMethod() { return "UNKNOWN_METHOD"; } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java new file mode 100644 index 00000000000..f839d83a800 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java @@ -0,0 +1,28 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Module; +import com.yahoo.jdisc.Metric; +import com.yahoo.jdisc.application.MetricConsumer; + +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author bjorncs + */ +class MetricConsumerMock { + + static final Metric.Context STATIC_CONTEXT = new Metric.Context() {}; + + private final MetricConsumer mockitoMock = mock(MetricConsumer.class); + + MetricConsumerMock() { + when(mockitoMock.createContext(anyMap())).thenReturn(STATIC_CONTEXT); + } + + MetricConsumer mockitoMock() { return mockitoMock; } + Module asGuiceModule() { return binder -> binder.bind(MetricConsumer.class).toInstance(mockitoMock); } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java new file mode 100644 index 00000000000..f1d710bd10f --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java @@ -0,0 +1,202 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Request; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.entity.GzipCompressingEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.DefaultHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.util.EntityUtils; +import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; + +import javax.net.ssl.SSLContext; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNotNull; + +/** + * A simple http client for testing + * + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class SimpleHttpClient implements AutoCloseable { + + private final CloseableHttpClient delegate; + private final String scheme; + private final int listenPort; + + public SimpleHttpClient(SSLContext sslContext, int listenPort, boolean useCompression) { + this(sslContext, null, null, listenPort, useCompression); + } + + public SimpleHttpClient(SSLContext sslContext, List enabledProtocols, List enabledCiphers, + int listenPort, boolean useCompression) { + HttpClientBuilder builder = HttpClientBuilder.create(); + if (!useCompression) { + builder.disableContentCompression(); + } + if (sslContext != null) { + SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( + sslContext, + toArray(enabledProtocols), + toArray(enabledCiphers), + new DefaultHostnameVerifier()); + builder.setSSLSocketFactory(sslConnectionFactory); + + Registry registry = RegistryBuilder.create() + .register("https", sslConnectionFactory) + .build(); + builder.setConnectionManager(new BasicHttpClientConnectionManager(registry)); + scheme = "https"; + } else { + scheme = "http"; + } + this.delegate = builder.build(); + this.listenPort = listenPort; + } + + private static String[] toArray(List list) { + return list != null ? list.toArray(new String[0]) : null; + } + + public URI newUri(final String path) { + return URI.create(scheme + "://localhost:" + listenPort + path); + } + + public RequestExecutor newGet(String path) { + return newRequest(new HttpGet(newUri(path))); + } + + public RequestExecutor newPost(String path) { + return newRequest(new HttpPost(newUri(path))); + } + + public RequestExecutor newRequest(HttpUriRequest request) { + return new RequestExecutor().setRequest(request); + } + + public ResponseValidator execute(HttpUriRequest request) throws IOException { + return newRequest(request).execute(); + } + + public ResponseValidator get(String path) throws IOException { + return newGet(path).execute(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + + public class RequestExecutor { + + private HttpUriRequest request; + private HttpEntity entity; + + public RequestExecutor setRequest(final HttpUriRequest request) { + this.request = request; + return this; + } + + public RequestExecutor addHeader(final String name, final String value) { + this.request.addHeader(name, value); + return this; + } + + public RequestExecutor setContent(final String content) { + this.entity = new StringEntity(content, StandardCharsets.UTF_8); + return this; + } + + public RequestExecutor setGzipContent(String content) { + this.entity = new GzipCompressingEntity(new StringEntity(content, StandardCharsets.UTF_8)); + return this; + } + + public RequestExecutor setBinaryContent(final byte[] content) { + this.entity = new ByteArrayEntity(content); + return this; + } + + public RequestExecutor setMultipartContent(final FormBodyPart... parts) { + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + Arrays.stream(parts).forEach(part -> builder.addPart(part.getName(), part.getBody())); + this.entity = builder.build(); + return this; + } + + public ResponseValidator execute() throws IOException { + if (entity != null) { + ((HttpPost)request).setEntity(entity); + } + try (CloseableHttpResponse response = delegate.execute(request)){ + return new ResponseValidator(response); + } + } + } + + public static class ResponseValidator { + + private final HttpResponse response; + private final String content; + + public ResponseValidator(HttpResponse response) throws IOException { + this.response = response; + + HttpEntity entity = response.getEntity(); + this.content = entity == null ? null : EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + + public ResponseValidator expectStatusCode(Matcher matcher) { + MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), matcher); + return this; + } + + public ResponseValidator expectHeader(String headerName, Matcher matcher) { + Header firstHeader = response.getFirstHeader(headerName); + String headerValue = firstHeader != null ? firstHeader.getValue() : null; + MatcherAssert.assertThat(headerValue, matcher); + assertNotNull(firstHeader); + return this; + } + + public ResponseValidator expectNoHeader(String headerName) { + Header firstHeader = response.getFirstHeader(headerName); + assertThat(firstHeader, is(nullValue())); + return this; + } + + public ResponseValidator expectContent(final Matcher matcher) { + MatcherAssert.assertThat(content, matcher); + return this; + } + + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java new file mode 100644 index 00000000000..20f050d715d --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java @@ -0,0 +1,42 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.yahoo.jdisc.Metric; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.junit.Test; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author mortent + */ +public class SslHandshakeFailedListenerTest { + + private Metric metrics = mock(Metric.class); + SslHandshakeFailedListener listener = new SslHandshakeFailedListener(metrics, "connector", 1234); + + @Test + public void includes_client_ip_dimension_present_when_peer_available() { + listener.handshakeFailed(handshakeEvent(true), new SSLHandshakeException("Empty server certificate chain")); + verify(metrics).createContext(eq(Map.of("clientIp", "127.0.0.1", "serverName", "connector", "serverPort", 1234))); + } + + @Test + public void does_not_include_client_ip_dimension_present_when_peer_unavailable() { + listener.handshakeFailed(handshakeEvent(false), new SSLHandshakeException("Empty server certificate chain")); + verify(metrics).createContext(eq(Map.of("serverName", "connector", "serverPort", 1234))); + } + + private SslHandshakeListener.Event handshakeEvent(boolean includePeer) { + var sslEngine = mock(SSLEngine.class); + if(includePeer) when(sslEngine.getPeerHost()).thenReturn("127.0.0.1"); + return new SslHandshakeListener.Event(sslEngine); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java new file mode 100644 index 00000000000..875889ed5ce --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java @@ -0,0 +1,79 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.Module; +import com.yahoo.jdisc.application.ContainerBuilder; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.security.SslContextBuilder; + +import javax.net.ssl.SSLContext; +import java.nio.file.Paths; + +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * This class is based on the class by the same name in the jdisc_http_service module. + * It provides functionality for setting up a jdisc container with an HTTP server and handlers. + * + * @author Simon Thoresen Hult + * @author bakksjo + */ +public class TestDriver { + + private final com.yahoo.jdisc.test.TestDriver driver; + private final JettyHttpServer server; + private final SimpleHttpClient client; + + private TestDriver(com.yahoo.jdisc.test.TestDriver driver, JettyHttpServer server, SimpleHttpClient client) { + this.driver = driver; + this.server = server; + this.client = client; + } + + public static TestDriver newInstance(Class serverClass, + RequestHandler requestHandler, + Module testConfig) { + com.yahoo.jdisc.test.TestDriver driver = + com.yahoo.jdisc.test.TestDriver.newSimpleApplicationInstance(testConfig); + ContainerBuilder builder = driver.newContainerBuilder(); + JettyHttpServer server = builder.getInstance(serverClass); + builder.serverProviders().install(server); + builder.serverBindings().bind("http://*/*", requestHandler); + driver.activateContainer(builder); + server.start(); + + SimpleHttpClient client = new SimpleHttpClient(newSslContext(builder), server.getListenPort(), false); + return new TestDriver(driver, server, client); + } + + public boolean close() { + server.close(); + server.release(); + uncheck(client::close); + return driver.close(); + } + + public JettyHttpServer server() { return server; } + + public SimpleHttpClient client() { return client; } + + public SimpleHttpClient newClient(final boolean useCompression) { + return new SimpleHttpClient(newSslContext(), server.getListenPort(), useCompression); + } + + public SSLContext newSslContext() { + return newSslContext(driver.newContainerBuilder()); + } + + private static SSLContext newSslContext(ContainerBuilder builder) { + ConnectorConfig.Ssl sslConfig = builder.getInstance(ConnectorConfig.class).ssl(); + if (!sslConfig.enabled()) return null; + + return new SslContextBuilder() + .withKeyStore(Paths.get(sslConfig.privateKeyFile()), Paths.get(sslConfig.certificateFile())) + .withTrustStore(Paths.get(sslConfig.caCertificateFile())) + .build(); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java new file mode 100644 index 00000000000..7d7530c32e0 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java @@ -0,0 +1,94 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.ConnectionLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.http.ConnectorConfig; +import com.yahoo.jdisc.http.ServerConfig; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.guiceModules.ConnectorFactoryRegistryModule; +import com.yahoo.jdisc.http.guiceModules.ServletModule; + +import java.nio.file.Path; + +/** + * @author Simon Thoresen Hult + * @author bjorncs + */ +public class TestDrivers { + + public static TestDriver newConfiguredInstance(RequestHandler requestHandler, + ServerConfig.Builder serverConfig, + ConnectorConfig.Builder connectorConfig, + Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule(serverConfig, connectorConfig, guiceModules)); + } + + public static TestDriver newInstance(RequestHandler requestHandler, Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder(), + new ConnectorConfig.Builder(), + guiceModules + )); + } + + public enum TlsClientAuth { NEED, WANT } + + public static TestDriver newInstanceWithSsl(RequestHandler requestHandler, + Path certificateFile, + Path privateKeyFile, + TlsClientAuth tlsClientAuth, + Module... guiceModules) { + return TestDriver.newInstance( + JettyHttpServer.class, + requestHandler, + newConfigModule( + new ServerConfig.Builder().connectionLog(new ServerConfig.ConnectionLog.Builder().enabled(true)), + new ConnectorConfig.Builder() + .tlsClientAuthEnforcer( + new ConnectorConfig.TlsClientAuthEnforcer.Builder() + .enable(true) + .pathWhitelist("/status.html")) + .ssl(new ConnectorConfig.Ssl.Builder() + .enabled(true) + .clientAuth(tlsClientAuth == TlsClientAuth.NEED + ? ConnectorConfig.Ssl.ClientAuth.Enum.NEED_AUTH + : ConnectorConfig.Ssl.ClientAuth.Enum.WANT_AUTH) + .privateKeyFile(privateKeyFile.toString()) + .certificateFile(certificateFile.toString()) + .caCertificateFile(certificateFile.toString())), + guiceModules)); + } + + private static Module newConfigModule(ServerConfig.Builder serverConfig, + ConnectorConfig.Builder connectorConfigBuilder, + Module... guiceModules) { + return Modules.override( + Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(ServletPathsConfig.class).toInstance(new ServletPathsConfig(new ServletPathsConfig.Builder())); + bind(ServerConfig.class).toInstance(new ServerConfig(serverConfig)); + bind(ConnectorConfig.class).toInstance(new ConnectorConfig(connectorConfigBuilder)); + bind(FilterBindings.class).toInstance(new FilterBindings.Builder().build()); + bind(ConnectionLog.class).toInstance(new VoidConnectionLog()); + bind(RequestLog.class).toInstance(new VoidRequestLog()); + } + }, + new ConnectorFactoryRegistryModule(connectorConfigBuilder), + new ServletModule())) + .with(guiceModules); + } + +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java new file mode 100644 index 00000000000..16969a47b84 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java @@ -0,0 +1,166 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.jdisc.AbstractResource; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.filter.RequestFilter; +import com.yahoo.jdisc.http.filter.ResponseFilter; +import com.yahoo.jdisc.http.server.jetty.FilterBindings; +import com.yahoo.jdisc.http.server.jetty.FilterInvoker; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.ResponseValidator; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; + +/** + * @author Tony Vaagenes + * @author bjorncs + */ +public class JDiscFilterForServletTest extends ServletTestBase { + @Test + public void request_filter_can_return_response() throws IOException, InterruptedException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectContent(containsString(TestRequestFilter.responseContent)); + } + + @Test + public void request_can_be_forwarded_through_request_filter_to_servlet() throws IOException { + TestDriver testDriver = requestFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH). + addHeader(TestRequestFilter.BYPASS_FILTER_HEADER, Boolean.TRUE.toString()). + execute(); + + response.expectContent(containsString(TestServlet.RESPONSE_CONTENT)); + } + + @Test + public void response_filter_can_modify_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, TestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_sync_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH).execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + @Test + public void response_filter_is_run_on_empty_async_response() throws IOException { + TestDriver testDriver = responseFilterTestDriver(); + ResponseValidator response = httpGet(testDriver, NoContentTestServlet.PATH). + addHeader(NoContentTestServlet.HEADER_ASYNC, Boolean.TRUE.toString()). + execute(); + + response.expectHeader(TestResponseFilter.INVOKED_HEADER, is(Boolean.TRUE.toString())); + } + + private TestDriver requestFilterTestDriver() throws IOException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addRequestFilter("my-request-filter", new TestRequestFilter()) + .addRequestFilterBinding("my-request-filter", "http://*/*") + .build(); + return TestDrivers.newInstance(dummyRequestHandler, bindings(filterBindings)); + } + + private TestDriver responseFilterTestDriver() throws IOException { + FilterBindings filterBindings = new FilterBindings.Builder() + .addResponseFilter("my-response-filter", new TestResponseFilter()) + .addResponseFilterBinding("my-response-filter", "http://*/*") + .build(); + return TestDrivers.newInstance(dummyRequestHandler, bindings(filterBindings)); + } + + + + private Module bindings(FilterBindings filterBindings) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(FilterBindings.class).toInstance(filterBindings); + bind(FilterInvoker.class).toInstance(new FilterInvoker() { + @Override + public HttpServletRequest invokeRequestFilterChain( + RequestFilter requestFilter, + URI uri, + HttpServletRequest httpRequest, + ResponseHandler responseHandler) { + TestRequestFilter filter = (TestRequestFilter) requestFilter; + filter.runAsSecurityFilter(httpRequest, responseHandler); + return httpRequest; + } + + @Override + public void invokeResponseFilterChain( + ResponseFilter responseFilter, + URI uri, + HttpServletRequest request, + HttpServletResponse response) { + + TestResponseFilter filter = (TestResponseFilter) responseFilter; + filter.runAsSecurityFilter(request, response); + } + }); + } + }, + guiceModule()); + } + + static class TestRequestFilter extends AbstractResource implements RequestFilter { + static final String simpleName = TestRequestFilter.class.getSimpleName(); + static final String responseContent = "Rejected by " + simpleName; + static final String BYPASS_FILTER_HEADER = "BYPASS_HEADER" + simpleName; + + @Override + public void filter(HttpRequest request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + + public void runAsSecurityFilter(HttpServletRequest request, ResponseHandler responseHandler) { + if (Boolean.parseBoolean(request.getHeader(BYPASS_FILTER_HEADER))) + return; + + ContentChannel contentChannel = responseHandler.handleResponse(new Response(500)); + contentChannel.write(ByteBuffer.wrap(responseContent.getBytes(StandardCharsets.UTF_8)), null); + contentChannel.close(null); + } + } + + + static class TestResponseFilter extends AbstractResource implements ResponseFilter { + static final String INVOKED_HEADER = TestResponseFilter.class.getSimpleName() + "_INVOKED_HEADER"; + + @Override + public void filter(Response response, Request request) { + throw new UnsupportedClassVersionError(); + } + + public void runAsSecurityFilter(HttpServletRequest request, HttpServletResponse response) { + response.addHeader(INVOKED_HEADER, Boolean.TRUE.toString()); + } + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java new file mode 100644 index 00000000000..a533a447f6a --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java @@ -0,0 +1,64 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.container.logging.RequestLog; +import com.yahoo.container.logging.RequestLogEntry; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import com.yahoo.jdisc.http.server.jetty.TestDrivers; +import org.junit.Test; +import org.mockito.verification.VerificationMode; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +/** + * @author bakksjo + * @author bjorncs + */ +public class ServletAccessLoggingTest extends ServletTestBase { + private static final long MAX_LOG_WAIT_TIME_MILLIS = TimeUnit.SECONDS.toMillis(60); + + @Test + public void accessLogIsInvokedForNonJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + httpGet(testDriver, TestServlet.PATH).execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + @Test + public void accessLogIsInvokedForJDiscServlet() throws Exception { + final AccessLog accessLog = mock(AccessLog.class); + final TestDriver testDriver = newTestDriver(accessLog); + testDriver.client().newGet("/status.html").execute(); + verifyCallsLog(accessLog, timeout(MAX_LOG_WAIT_TIME_MILLIS).times(1)); + } + + private void verifyCallsLog(RequestLog requestLog, final VerificationMode verificationMode) { + verify(requestLog, verificationMode).log(any(RequestLogEntry.class)); + } + + private TestDriver newTestDriver(RequestLog requestLog) throws IOException { + return TestDrivers.newInstance(dummyRequestHandler, bindings(requestLog)); + } + + private Module bindings(RequestLog requestLog) { + return Modules.combine( + new AbstractModule() { + @Override + protected void configure() { + bind(RequestLog.class).toInstance(requestLog); + } + }, + guiceModule()); + } +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java new file mode 100644 index 00000000000..54bfe8c026d --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java @@ -0,0 +1,132 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.server.jetty.servlet; + +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.handler.AbstractRequestHandler; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.ServletPathsConfig; +import com.yahoo.jdisc.http.ServletPathsConfig.Servlets.Builder; +import com.yahoo.jdisc.http.server.jetty.SimpleHttpClient.RequestExecutor; +import com.yahoo.jdisc.http.server.jetty.TestDriver; +import org.eclipse.jetty.servlet.ServletHolder; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +/** + * @author Tony Vaagenes + * @author bakksjo + */ +public class ServletTestBase { + + private static class ServletInstance { + final ComponentId componentId; final String path; final HttpServlet instance; + + ServletInstance(ComponentId componentId, String path, HttpServlet instance) { + this.componentId = componentId; + this.path = path; + this.instance = instance; + } + } + + private final List servlets = List.of( + new ServletInstance(TestServlet.ID, TestServlet.PATH, new TestServlet()), + new ServletInstance(NoContentTestServlet.ID, NoContentTestServlet.PATH, new NoContentTestServlet())); + + protected RequestExecutor httpGet(TestDriver testDriver, String path) { + return testDriver.client().newGet("/" + path); + } + + protected ServletPathsConfig createServletPathConfig() { + ServletPathsConfig.Builder configBuilder = new ServletPathsConfig.Builder(); + + servlets.forEach(servlet -> + configBuilder.servlets( + servlet.componentId.stringValue(), + new Builder().path(servlet.path))); + + return new ServletPathsConfig(configBuilder); + } + + protected ComponentRegistry servlets() { + ComponentRegistry result = new ComponentRegistry<>(); + + servlets.forEach(servlet -> + result.register(servlet.componentId, new ServletHolder(servlet.instance))); + + result.freeze(); + return result; + } + + protected Module guiceModule() { + return new AbstractModule() { + @Override + protected void configure() { + bind(new TypeLiteral>(){}).toInstance(servlets()); + bind(ServletPathsConfig.class).toInstance(createServletPathConfig()); + } + }; + } + + protected static class TestServlet extends HttpServlet { + static final String PATH = "servlet/test-servlet"; + static final ComponentId ID = ComponentId.fromString("test-servlet"); + static final String RESPONSE_CONTENT = "Response from " + TestServlet.class.getSimpleName(); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setContentType("text/plain"); + PrintWriter writer = response.getWriter(); + writer.write(RESPONSE_CONTENT); + writer.close(); + } + } + + @WebServlet(asyncSupported = true) + protected static class NoContentTestServlet extends HttpServlet { + static final String HEADER_ASYNC = "HEADER_ASYNC"; + + static final String PATH = "servlet/no-content-test-servlet"; + static final ComponentId ID = ComponentId.fromString("no-content-test-servlet"); + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + if (request.getHeader(HEADER_ASYNC) != null) { + asyncGet(request); + } + } + + private void asyncGet(HttpServletRequest request) { + request.startAsync().start(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + log("Interrupted", e); + } finally { + request.getAsyncContext().complete(); + } + }); + } + } + + + protected static final RequestHandler dummyRequestHandler = new AbstractRequestHandler() { + @Override + public ContentChannel handleRequest(Request request, ResponseHandler handler) { + throw new UnsupportedOperationException(); + } + }; +} diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java new file mode 100644 index 00000000000..eb292199ea2 --- /dev/null +++ b/container-core/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java @@ -0,0 +1,71 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.ssl.impl; + +import com.yahoo.security.KeyUtils; +import com.yahoo.security.X509CertificateBuilder; +import com.yahoo.security.tls.AuthorizationMode; +import com.yahoo.security.tls.DefaultTlsContext; +import com.yahoo.security.tls.HostnameVerification; +import com.yahoo.security.tls.PeerAuthentication; +import com.yahoo.security.tls.TlsContext; +import com.yahoo.security.tls.policy.AuthorizedPeers; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Test; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Set; + +import static com.yahoo.security.KeyAlgorithm.EC; +import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author bjorncs + */ +public class TlsContextBasedProviderTest { + + @Test + public void creates_sslcontextfactory_from_tlscontext() { + TlsContext tlsContext = createTlsContext(); + var provider = new SimpleTlsContextBasedProvider(tlsContext); + SslContextFactory sslContextFactory = provider.getInstance("dummyContainerId", 8080); + assertNotNull(sslContextFactory); + assertArrayEquals(tlsContext.parameters().getCipherSuites(), sslContextFactory.getIncludeCipherSuites()); + } + + private static TlsContext createTlsContext() { + KeyPair keyPair = KeyUtils.generateKeypair(EC); + X509Certificate certificate = X509CertificateBuilder + .fromKeypair( + keyPair, + new X500Principal("CN=dummy"), + Instant.EPOCH, + Instant.EPOCH.plus(100000, ChronoUnit.DAYS), + SHA256_WITH_ECDSA, + BigInteger.ONE) + .build(); + return new DefaultTlsContext( + List.of(certificate), keyPair.getPrivate(), List.of(certificate), new AuthorizedPeers(Set.of()), AuthorizationMode.ENFORCE, PeerAuthentication.NEED, HostnameVerification.ENABLED); + } + + private static class SimpleTlsContextBasedProvider extends TlsContextBasedProvider { + final TlsContext tlsContext; + + SimpleTlsContextBasedProvider(TlsContext tlsContext) { + this.tlsContext = tlsContext; + } + + @Override + protected TlsContext getTlsContext(String containerId, int port) { + return tlsContext; + } + + } +} \ No newline at end of file -- cgit v1.2.3 From cb3b722ef00dc440c95c39d6a034f34a7204d59d Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 20:29:50 +0100 Subject: Add necessary test deps from jdisc_http_service. - org.bouncycastle:bcpkix-jdk15on could be skipped --- container-core/pom.xml | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/container-core/pom.xml b/container-core/pom.xml index f387172b91d..12097aef787 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -232,12 +232,6 @@ ${project.version} provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa jdisc_jetty @@ -283,16 +277,52 @@ junit test + + org.apache.httpcomponents + httpmime + test + org.assertj assertj-core test + + org.cthul + cthul-matchers + test + + + org.eclipse.jetty + jetty-client + ${jetty.version} + test + + + org.hamcrest + hamcrest-library + test + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.vintage + junit-vintage-engine + test + org.mockito mockito-core test + + org.springframework + spring-test + test + -- cgit v1.2.3 From 384345c6b6926cca10b0fce281127067f229c2dd Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 20:49:37 +0100 Subject: Add preinstall bundles from jdisc_http_service to container-disc. --- container-disc/pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/container-disc/pom.xml b/container-disc/pom.xml index a5f678ad828..c88ca8f1291 100644 --- a/container-disc/pom.xml +++ b/container-disc/pom.xml @@ -178,6 +178,7 @@ true + component-jar-with-dependencies.jar, configgen.jar, config-bundle-jar-with-dependencies.jar, configdefinitions-jar-with-dependencies.jar, @@ -198,9 +199,21 @@ component-jar-with-dependencies.jar, zkfacade-jar-with-dependencies.jar, zookeeper-server-jar-with-dependencies.jar, + + jetty-continuation-${jetty.version}.jar, + jetty-http-${jetty.version}.jar, + jetty-io-${jetty.version}.jar, + jetty-jmx-${jetty.version}.jar, + jetty-security-${jetty.version}.jar, + jetty-server-${jetty.version}.jar, + jetty-servlet-${jetty.version}.jar, + jetty-servlets-${jetty.version}.jar, + jetty-util-${jetty.version}.jar, + jetty-util-ajax-${jetty.version}.jar, bcpkix-jdk15on-${bouncycastle.version}.jar, bcprov-jdk15on-${bouncycastle.version}.jar, + javax.servlet-api-3.1.0.jar, aopalliance-repackaged-${hk2.version}.jar, hk2-api-${hk2.version}.jar, -- cgit v1.2.3 From e73d53af8da2de34825674fb2b1fad82763f43ac Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 21:08:46 +0100 Subject: Copy plugin configuration from jdisc_http_service. - Copy bouncycastle deps to target/deps --- container-core/pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/container-core/pom.xml b/container-core/pom.xml index 12097aef787..d0722a081d1 100644 --- a/container-core/pom.xml +++ b/container-core/pom.xml @@ -384,6 +384,22 @@ com.yahoo.vespa abi-check-plugin + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + org.bouncycastle + + + + ${buildOutputDirectory} -- cgit v1.2.3 From e28e5db69333a98dcf6d6525a6b54f78f35e20d5 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Thu, 18 Mar 2021 21:09:48 +0100 Subject: Update abi spec after merging in jdisc_http_service. --- container-core/abi-spec.json | 1480 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1480 insertions(+) diff --git a/container-core/abi-spec.json b/container-core/abi-spec.json index d549ba1098d..da8ed609dfc 100644 --- a/container-core/abi-spec.json +++ b/container-core/abi-spec.json @@ -860,6 +860,1486 @@ ], "fields": [] }, + "com.yahoo.jdisc.http.CertificateStore": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public java.lang.String getCertificate(java.lang.String)", + "public java.lang.String getCertificate(java.lang.String, long)", + "public abstract java.lang.String getCertificate(java.lang.String, long, long)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Builder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder listenPort(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder name(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder headerCacheSize(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder outputBufferSize(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder requestHeaderSize(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder responseHeaderSize(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder acceptQueueSize(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder reuseAddress(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder idleTimeout(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder stopTimeout(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpKeepAliveEnabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpNoDelay(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder throttling(com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder implicitTlsEnabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder ssl(com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder tlsClientAuthEnforcer(com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder healthCheckProxy(com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder proxyProtocol(com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder secureRedirect(com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxRequestsPerConnection(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxConnectionLife(double)", + "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", + "public final java.lang.String getDefMd5()", + "public final java.lang.String getDefName()", + "public final java.lang.String getDefNamespace()", + "public final boolean getApplyOnRestart()", + "public final void setApplyOnRestart(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig build()" + ], + "fields": [ + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder throttling", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder ssl", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder tlsClientAuthEnforcer", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder healthCheckProxy", + "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder proxyProtocol", + "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder secureRedirect" + ] + }, + "com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy)", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder enable(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder port(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder clientTimeout(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder)", + "public boolean enable()", + "public int port()", + "public double clientTimeout()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Producer": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Producer" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void getConfig(com.yahoo.jdisc.http.ConnectorConfig$Builder)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol)", + "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder mixedMode(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder)", + "public boolean enabled()", + "public boolean mixedMode()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect)", + "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder port(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", + "public boolean enabled()", + "public int port()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKeyFile(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKey(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificateFile(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificate(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder caCertificateFile(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder caCertificate(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder clientAuth(com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledCipherSuites(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledCipherSuites(java.util.Collection)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledProtocols(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledProtocols(java.util.Collection)", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl build()" + ], + "fields": [ + "public java.util.List enabledCipherSuites", + "public java.util.List enabledProtocols" + ] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum[] values()", + "public static com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum valueOf(java.lang.String)" + ], + "fields": [ + "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum DISABLED", + "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum WANT_AUTH", + "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum NEED_AUTH" + ] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth": { + "superClass": "com.yahoo.config.EnumNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum)" + ], + "fields": [ + "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum DISABLED", + "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum WANT_AUTH", + "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum NEED_AUTH" + ] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Ssl": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder)", + "public boolean enabled()", + "public java.lang.String privateKeyFile()", + "public java.lang.String privateKey()", + "public java.lang.String certificateFile()", + "public java.lang.String certificate()", + "public java.lang.String caCertificateFile()", + "public java.lang.String caCertificate()", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum clientAuth()", + "public java.util.List enabledCipherSuites()", + "public java.lang.String enabledCipherSuites(int)", + "public java.util.List enabledProtocols()", + "public java.lang.String enabledProtocols(int)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$Throttling)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxConnections(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxHeapUtilization(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxAcceptRate(int)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder idleTimeout(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$Throttling": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder)", + "public boolean enabled()", + "public int maxConnections()", + "public double maxHeapUtilization()", + "public int maxAcceptRate()", + "public double idleTimeout()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer)", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder enable(boolean)", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder pathWhitelist(java.lang.String)", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder pathWhitelist(java.util.Collection)", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer build()" + ], + "fields": [ + "public java.util.List pathWhitelist" + ] + }, + "com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder)", + "public boolean enable()", + "public java.util.List pathWhitelist()", + "public java.lang.String pathWhitelist(int)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ConnectorConfig": { + "superClass": "com.yahoo.config.ConfigInstance", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public static java.lang.String getDefMd5()", + "public static java.lang.String getDefName()", + "public static java.lang.String getDefNamespace()", + "public static java.lang.String getDefVersion()", + "public void (com.yahoo.jdisc.http.ConnectorConfig$Builder)", + "public int listenPort()", + "public java.lang.String name()", + "public int headerCacheSize()", + "public int outputBufferSize()", + "public int requestHeaderSize()", + "public int responseHeaderSize()", + "public int acceptQueueSize()", + "public boolean reuseAddress()", + "public double idleTimeout()", + "public double stopTimeout()", + "public boolean tcpKeepAliveEnabled()", + "public boolean tcpNoDelay()", + "public com.yahoo.jdisc.http.ConnectorConfig$Throttling throttling()", + "public boolean implicitTlsEnabled()", + "public com.yahoo.jdisc.http.ConnectorConfig$Ssl ssl()", + "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer tlsClientAuthEnforcer()", + "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy healthCheckProxy()", + "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol proxyProtocol()", + "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect secureRedirect()", + "public int maxRequestsPerConnection()", + "public double maxConnectionLife()" + ], + "fields": [ + "public static final java.lang.String CONFIG_DEF_MD5", + "public static final java.lang.String CONFIG_DEF_NAME", + "public static final java.lang.String CONFIG_DEF_NAMESPACE", + "public static final java.lang.String CONFIG_DEF_VERSION", + "public static final java.lang.String[] CONFIG_DEF_SCHEMA" + ] + }, + "com.yahoo.jdisc.http.Cookie$SameSite": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static com.yahoo.jdisc.http.Cookie$SameSite[] values()", + "public static com.yahoo.jdisc.http.Cookie$SameSite valueOf(java.lang.String)" + ], + "fields": [ + "public static final enum com.yahoo.jdisc.http.Cookie$SameSite NONE", + "public static final enum com.yahoo.jdisc.http.Cookie$SameSite STRICT", + "public static final enum com.yahoo.jdisc.http.Cookie$SameSite LAX" + ] + }, + "com.yahoo.jdisc.http.Cookie": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.Cookie)", + "public void (java.lang.String, java.lang.String)", + "public java.lang.String getName()", + "public com.yahoo.jdisc.http.Cookie setName(java.lang.String)", + "public java.lang.String getValue()", + "public com.yahoo.jdisc.http.Cookie setValue(java.lang.String)", + "public java.lang.String getDomain()", + "public com.yahoo.jdisc.http.Cookie setDomain(java.lang.String)", + "public java.lang.String getPath()", + "public com.yahoo.jdisc.http.Cookie setPath(java.lang.String)", + "public com.yahoo.jdisc.http.Cookie$SameSite getSameSite()", + "public com.yahoo.jdisc.http.Cookie setSameSite(com.yahoo.jdisc.http.Cookie$SameSite)", + "public int getMaxAge(java.util.concurrent.TimeUnit)", + "public com.yahoo.jdisc.http.Cookie setMaxAge(int, java.util.concurrent.TimeUnit)", + "public boolean isSecure()", + "public com.yahoo.jdisc.http.Cookie setSecure(boolean)", + "public boolean isHttpOnly()", + "public com.yahoo.jdisc.http.Cookie setHttpOnly(boolean)", + "public boolean equals(java.lang.Object)", + "public int hashCode()", + "public java.lang.String toString()", + "public static java.lang.String toCookieHeader(java.lang.Iterable)", + "public static java.util.List fromCookieHeader(java.lang.String)", + "public static java.util.List toSetCookieHeaders(java.lang.Iterable)", + "public static java.util.List toSetCookieHeaderAll(java.lang.Iterable)", + "public static com.yahoo.jdisc.http.Cookie fromSetCookieHeader(java.lang.String)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.HttpHeaders$Names": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [], + "fields": [ + "public static final java.lang.String ACCEPT", + "public static final java.lang.String ACCEPT_CHARSET", + "public static final java.lang.String ACCEPT_ENCODING", + "public static final java.lang.String ACCEPT_LANGUAGE", + "public static final java.lang.String ACCEPT_RANGES", + "public static final java.lang.String ACCEPT_PATCH", + "public static final java.lang.String AGE", + "public static final java.lang.String ALLOW", + "public static final java.lang.String AUTHORIZATION", + "public static final java.lang.String CACHE_CONTROL", + "public static final java.lang.String CONNECTION", + "public static final java.lang.String CONTENT_BASE", + "public static final java.lang.String CONTENT_ENCODING", + "public static final java.lang.String CONTENT_LANGUAGE", + "public static final java.lang.String CONTENT_LENGTH", + "public static final java.lang.String CONTENT_LOCATION", + "public static final java.lang.String CONTENT_TRANSFER_ENCODING", + "public static final java.lang.String CONTENT_MD5", + "public static final java.lang.String CONTENT_RANGE", + "public static final java.lang.String CONTENT_TYPE", + "public static final java.lang.String COOKIE", + "public static final java.lang.String DATE", + "public static final java.lang.String ETAG", + "public static final java.lang.String EXPECT", + "public static final java.lang.String EXPIRES", + "public static final java.lang.String FROM", + "public static final java.lang.String HOST", + "public static final java.lang.String IF_MATCH", + "public static final java.lang.String IF_MODIFIED_SINCE", + "public static final java.lang.String IF_NONE_MATCH", + "public static final java.lang.String IF_RANGE", + "public static final java.lang.String IF_UNMODIFIED_SINCE", + "public static final java.lang.String LAST_MODIFIED", + "public static final java.lang.String LOCATION", + "public static final java.lang.String MAX_FORWARDS", + "public static final java.lang.String ORIGIN", + "public static final java.lang.String PRAGMA", + "public static final java.lang.String PROXY_AUTHENTICATE", + "public static final java.lang.String PROXY_AUTHORIZATION", + "public static final java.lang.String RANGE", + "public static final java.lang.String REFERER", + "public static final java.lang.String RETRY_AFTER", + "public static final java.lang.String SEC_WEBSOCKET_KEY1", + "public static final java.lang.String SEC_WEBSOCKET_KEY2", + "public static final java.lang.String SEC_WEBSOCKET_LOCATION", + "public static final java.lang.String SEC_WEBSOCKET_ORIGIN", + "public static final java.lang.String SEC_WEBSOCKET_PROTOCOL", + "public static final java.lang.String SEC_WEBSOCKET_VERSION", + "public static final java.lang.String SEC_WEBSOCKET_KEY", + "public static final java.lang.String SEC_WEBSOCKET_ACCEPT", + "public static final java.lang.String SERVER", + "public static final java.lang.String SET_COOKIE", + "public static final java.lang.String SET_COOKIE2", + "public static final java.lang.String TE", + "public static final java.lang.String TRAILER", + "public static final java.lang.String TRANSFER_ENCODING", + "public static final java.lang.String UPGRADE", + "public static final java.lang.String USER_AGENT", + "public static final java.lang.String VARY", + "public static final java.lang.String VIA", + "public static final java.lang.String WARNING", + "public static final java.lang.String WEBSOCKET_LOCATION", + "public static final java.lang.String WEBSOCKET_ORIGIN", + "public static final java.lang.String WEBSOCKET_PROTOCOL", + "public static final java.lang.String WWW_AUTHENTICATE", + "public static final java.lang.String X_DISABLE_CHUNKING", + "public static final java.lang.String X_YAHOO_SERVING_HOST" + ] + }, + "com.yahoo.jdisc.http.HttpHeaders$Values": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [], + "fields": [ + "public static final java.lang.String APPLICATION_X_WWW_FORM_URLENCODED", + "public static final java.lang.String BASE64", + "public static final java.lang.String BINARY", + "public static final java.lang.String BYTES", + "public static final java.lang.String CHARSET", + "public static final java.lang.String CHUNKED", + "public static final java.lang.String CLOSE", + "public static final java.lang.String COMPRESS", + "public static final java.lang.String CONTINUE", + "public static final java.lang.String DEFLATE", + "public static final java.lang.String GZIP", + "public static final java.lang.String IDENTITY", + "public static final java.lang.String KEEP_ALIVE", + "public static final java.lang.String MAX_AGE", + "public static final java.lang.String MAX_STALE", + "public static final java.lang.String MIN_FRESH", + "public static final java.lang.String MUST_REVALIDATE", + "public static final java.lang.String NO_CACHE", + "public static final java.lang.String NO_STORE", + "public static final java.lang.String NO_TRANSFORM", + "public static final java.lang.String NONE", + "public static final java.lang.String ONLY_IF_CACHED", + "public static final java.lang.String PRIVATE", + "public static final java.lang.String PROXY_REVALIDATE", + "public static final java.lang.String PUBLIC", + "public static final java.lang.String QUOTED_PRINTABLE", + "public static final java.lang.String S_MAXAGE", + "public static final java.lang.String TRAILERS", + "public static final java.lang.String UPGRADE", + "public static final java.lang.String WEBSOCKET" + ] + }, + "com.yahoo.jdisc.http.HttpHeaders": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void ()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.HttpRequest$Method": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static com.yahoo.jdisc.http.HttpRequest$Method[] values()", + "public static com.yahoo.jdisc.http.HttpRequest$Method valueOf(java.lang.String)" + ], + "fields": [ + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method OPTIONS", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method GET", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method HEAD", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method POST", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method PUT", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method PATCH", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method DELETE", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method TRACE", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Method CONNECT" + ] + }, + "com.yahoo.jdisc.http.HttpRequest$Version": { + "superClass": "java.lang.Enum", + "interfaces": [], + "attributes": [ + "public", + "final", + "enum" + ], + "methods": [ + "public static com.yahoo.jdisc.http.HttpRequest$Version[] values()", + "public static com.yahoo.jdisc.http.HttpRequest$Version valueOf(java.lang.String)", + "public java.lang.String toString()", + "public static com.yahoo.jdisc.http.HttpRequest$Version fromString(java.lang.String)" + ], + "fields": [ + "public static final enum com.yahoo.jdisc.http.HttpRequest$Version HTTP_1_0", + "public static final enum com.yahoo.jdisc.http.HttpRequest$Version HTTP_1_1" + ] + }, + "com.yahoo.jdisc.http.HttpRequest": { + "superClass": "com.yahoo.jdisc.Request", + "interfaces": [ + "com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest" + ], + "attributes": [ + "public" + ], + "methods": [ + "protected void (com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress, java.lang.Long)", + "public com.yahoo.jdisc.http.HttpRequest$Method getMethod()", + "public void setMethod(com.yahoo.jdisc.http.HttpRequest$Method)", + "public com.yahoo.jdisc.http.HttpRequest$Version getVersion()", + "public java.lang.String getRemoteHostAddress()", + "public java.lang.String getRemoteHostName()", + "public int getRemotePort()", + "public void setVersion(com.yahoo.jdisc.http.HttpRequest$Version)", + "public java.net.SocketAddress getRemoteAddress()", + "public void setRemoteAddress(java.net.SocketAddress)", + "public java.net.URI getProxyServer()", + "public void setProxyServer(java.net.URI)", + "public long getConnectedAt(java.util.concurrent.TimeUnit)", + "public java.lang.Long getConnectionTimeout(java.util.concurrent.TimeUnit)", + "public void setConnectionTimeout(long, java.util.concurrent.TimeUnit)", + "public java.util.Map parameters()", + "public void copyHeaders(com.yahoo.jdisc.HeaderFields)", + "public java.util.List decodeCookieHeader()", + "public void encodeCookieHeader(java.util.List)", + "public com.yahoo.jdisc.HeaderFields trailers()", + "public boolean isChunked()", + "public boolean hasChunkedResponse()", + "public boolean isKeepAlive()", + "public java.security.Principal getUserPrincipal()", + "public void setUserPrincipal(java.security.Principal)", + "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI)", + "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method)", + "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version)", + "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress)", + "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress, long)", + "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI)", + "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method)", + "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.HttpResponse$Status": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.Response$Status" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [], + "fields": [ + "public static final int REQUEST_ENTITY_TOO_LARGE", + "public static final int REQUEST_RANGE_NOT_SATISFIABLE" + ] + }, + "com.yahoo.jdisc.http.HttpResponse": { + "superClass": "com.yahoo.jdisc.Response", + "interfaces": [ + "com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse" + ], + "attributes": [ + "public" + ], + "methods": [ + "protected void (com.yahoo.jdisc.Request, int, java.lang.String, java.lang.Throwable)", + "public boolean isChunkedEncodingEnabled()", + "public void setChunkedEncodingEnabled(boolean)", + "public void setMessage(java.lang.String)", + "public java.lang.String getMessage()", + "public void copyHeaders(com.yahoo.jdisc.HeaderFields)", + "public java.util.List decodeSetCookieHeader()", + "public void encodeSetCookieHeader(java.util.List)", + "public com.yahoo.jdisc.HeaderFields trailers()", + "public static boolean isServerError(com.yahoo.jdisc.Response)", + "public static com.yahoo.jdisc.http.HttpResponse newInstance(int)", + "public static com.yahoo.jdisc.http.HttpResponse newInstance(int, java.lang.String)", + "public static com.yahoo.jdisc.http.HttpResponse newError(com.yahoo.jdisc.Request, int, java.lang.Throwable)", + "public static com.yahoo.jdisc.http.HttpResponse newInternalServerError(com.yahoo.jdisc.Request, java.lang.Throwable)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.SecretStore": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.String getSecret(java.lang.String)", + "public java.lang.String getSecret(java.lang.String, int)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$AccessLog)", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remoteAddressHeaders(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remoteAddressHeaders(java.util.Collection)", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remotePortHeaders(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remotePortHeaders(java.util.Collection)", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog build()" + ], + "fields": [ + "public java.util.List remoteAddressHeaders", + "public java.util.List remotePortHeaders" + ] + }, + "com.yahoo.jdisc.http.ServerConfig$AccessLog": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder)", + "public java.util.List remoteAddressHeaders()", + "public java.lang.String remoteAddressHeaders(int)", + "public java.util.List remotePortHeaders()", + "public java.lang.String remotePortHeaders(int)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Builder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig)", + "public com.yahoo.jdisc.http.ServerConfig$Builder developerMode(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$Builder responseCompressionLevel(int)", + "public com.yahoo.jdisc.http.ServerConfig$Builder httpKeepAliveEnabled(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$Builder maxKeepAliveRequests(int)", + "public com.yahoo.jdisc.http.ServerConfig$Builder removeRawPostBodyForWwwUrlEncodedPost(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$Builder filter(com.yahoo.jdisc.http.ServerConfig$Filter$Builder)", + "public com.yahoo.jdisc.http.ServerConfig$Builder filter(java.util.List)", + "public com.yahoo.jdisc.http.ServerConfig$Builder defaultFilters(com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder)", + "public com.yahoo.jdisc.http.ServerConfig$Builder defaultFilters(java.util.List)", + "public com.yahoo.jdisc.http.ServerConfig$Builder strictFiltering(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$Builder maxWorkerThreads(int)", + "public com.yahoo.jdisc.http.ServerConfig$Builder minWorkerThreads(int)", + "public com.yahoo.jdisc.http.ServerConfig$Builder stopTimeout(double)", + "public com.yahoo.jdisc.http.ServerConfig$Builder jmx(com.yahoo.jdisc.http.ServerConfig$Jmx$Builder)", + "public com.yahoo.jdisc.http.ServerConfig$Builder metric(com.yahoo.jdisc.http.ServerConfig$Metric$Builder)", + "public com.yahoo.jdisc.http.ServerConfig$Builder accessLog(com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder)", + "public com.yahoo.jdisc.http.ServerConfig$Builder connectionLog(com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder)", + "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", + "public final java.lang.String getDefMd5()", + "public final java.lang.String getDefName()", + "public final java.lang.String getDefNamespace()", + "public final boolean getApplyOnRestart()", + "public final void setApplyOnRestart(boolean)", + "public com.yahoo.jdisc.http.ServerConfig build()" + ], + "fields": [ + "public java.util.List filter", + "public java.util.List defaultFilters", + "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder jmx", + "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder metric", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder accessLog", + "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder connectionLog" + ] + }, + "com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$ConnectionLog)", + "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$ConnectionLog": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder)", + "public boolean enabled()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$DefaultFilters)", + "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder filterId(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder localPort(int)", + "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$DefaultFilters": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder)", + "public java.lang.String filterId()", + "public int localPort()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Filter$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$Filter)", + "public com.yahoo.jdisc.http.ServerConfig$Filter$Builder id(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$Filter$Builder binding(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$Filter build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Filter": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$Filter$Builder)", + "public java.lang.String id()", + "public java.lang.String binding()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Jmx$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$Jmx)", + "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder enabled(boolean)", + "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder listenPort(int)", + "public com.yahoo.jdisc.http.ServerConfig$Jmx build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Jmx": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$Jmx$Builder)", + "public boolean enabled()", + "public int listenPort()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Metric$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServerConfig$Metric)", + "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder monitoringHandlerPaths(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder monitoringHandlerPaths(java.util.Collection)", + "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder searchHandlerPaths(java.lang.String)", + "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder searchHandlerPaths(java.util.Collection)", + "public com.yahoo.jdisc.http.ServerConfig$Metric build()" + ], + "fields": [ + "public java.util.List monitoringHandlerPaths", + "public java.util.List searchHandlerPaths" + ] + }, + "com.yahoo.jdisc.http.ServerConfig$Metric": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServerConfig$Metric$Builder)", + "public java.util.List monitoringHandlerPaths()", + "public java.lang.String monitoringHandlerPaths(int)", + "public java.util.List searchHandlerPaths()", + "public java.lang.String searchHandlerPaths(int)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig$Producer": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Producer" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void getConfig(com.yahoo.jdisc.http.ServerConfig$Builder)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServerConfig": { + "superClass": "com.yahoo.config.ConfigInstance", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public static java.lang.String getDefMd5()", + "public static java.lang.String getDefName()", + "public static java.lang.String getDefNamespace()", + "public static java.lang.String getDefVersion()", + "public void (com.yahoo.jdisc.http.ServerConfig$Builder)", + "public boolean developerMode()", + "public int responseCompressionLevel()", + "public boolean httpKeepAliveEnabled()", + "public int maxKeepAliveRequests()", + "public boolean removeRawPostBodyForWwwUrlEncodedPost()", + "public java.util.List filter()", + "public com.yahoo.jdisc.http.ServerConfig$Filter filter(int)", + "public java.util.List defaultFilters()", + "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters defaultFilters(int)", + "public boolean strictFiltering()", + "public int maxWorkerThreads()", + "public int minWorkerThreads()", + "public double stopTimeout()", + "public com.yahoo.jdisc.http.ServerConfig$Jmx jmx()", + "public com.yahoo.jdisc.http.ServerConfig$Metric metric()", + "public com.yahoo.jdisc.http.ServerConfig$AccessLog accessLog()", + "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog connectionLog()" + ], + "fields": [ + "public static final java.lang.String CONFIG_DEF_MD5", + "public static final java.lang.String CONFIG_DEF_NAME", + "public static final java.lang.String CONFIG_DEF_NAMESPACE", + "public static final java.lang.String CONFIG_DEF_VERSION", + "public static final java.lang.String[] CONFIG_DEF_SCHEMA" + ] + }, + "com.yahoo.jdisc.http.ServletPathsConfig$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Builder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServletPathsConfig)", + "public com.yahoo.jdisc.http.ServletPathsConfig$Builder servlets(java.lang.String, com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder)", + "public com.yahoo.jdisc.http.ServletPathsConfig$Builder servlets(java.util.Map)", + "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", + "public final java.lang.String getDefMd5()", + "public final java.lang.String getDefName()", + "public final java.lang.String getDefNamespace()", + "public final boolean getApplyOnRestart()", + "public final void setApplyOnRestart(boolean)", + "public com.yahoo.jdisc.http.ServletPathsConfig build()" + ], + "fields": [ + "public java.util.Map servlets" + ] + }, + "com.yahoo.jdisc.http.ServletPathsConfig$Producer": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigInstance$Producer" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void getConfig(com.yahoo.jdisc.http.ServletPathsConfig$Builder)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.config.ConfigBuilder" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public void (com.yahoo.jdisc.http.ServletPathsConfig$Servlets)", + "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder path(java.lang.String)", + "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets build()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServletPathsConfig$Servlets": { + "superClass": "com.yahoo.config.InnerNode", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder)", + "public java.lang.String path()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ServletPathsConfig": { + "superClass": "com.yahoo.config.ConfigInstance", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public static java.lang.String getDefMd5()", + "public static java.lang.String getDefName()", + "public static java.lang.String getDefNamespace()", + "public static java.lang.String getDefVersion()", + "public void (com.yahoo.jdisc.http.ServletPathsConfig$Builder)", + "public java.util.Map servlets()", + "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets servlets(java.lang.String)" + ], + "fields": [ + "public static final java.lang.String CONFIG_DEF_MD5", + "public static final java.lang.String CONFIG_DEF_NAME", + "public static final java.lang.String CONFIG_DEF_NAMESPACE", + "public static final java.lang.String CONFIG_DEF_VERSION", + "public static final java.lang.String[] CONFIG_DEF_SCHEMA" + ] + }, + "com.yahoo.jdisc.http.filter.DiscFilterRequest$ThreadLocalSimpleDateFormat": { + "superClass": "java.lang.ThreadLocal", + "interfaces": [], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void (java.lang.String, java.util.Locale)", + "public java.util.Date parse(java.lang.String)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.DiscFilterRequest": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "abstract" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest)", + "public abstract java.lang.String getMethod()", + "public com.yahoo.jdisc.http.HttpRequest$Version getVersion()", + "public java.net.URI getUri()", + "public abstract void setUri(java.net.URI)", + "public com.yahoo.jdisc.http.HttpRequest getParentRequest()", + "public java.lang.String getRemoteAddr()", + "public void setRemoteAddr(java.lang.String)", + "public java.lang.String getLocalAddr()", + "public java.util.Enumeration getAttributeNames()", + "public java.lang.Object getAttribute(java.lang.String)", + "public void setAttribute(java.lang.String, java.lang.Object)", + "public boolean containsAttribute(java.lang.String)", + "public void removeAttribute(java.lang.String)", + "public abstract java.lang.String getParameter(java.lang.String)", + "public abstract java.util.Enumeration getParameterNames()", + "public java.util.List getParameterNamesAsList()", + "public java.util.Enumeration getParameterValues(java.lang.String)", + "public java.util.List getParameterValuesAsList(java.lang.String)", + "public java.util.Map getParameterMap()", + "public java.lang.String getRemoteHost()", + "public int getLocalPort()", + "public int getRemotePort()", + "public java.util.Map getUntreatedParams()", + "public com.yahoo.jdisc.HeaderFields getUntreatedHeaders()", + "public java.util.List getUntreatedCookies()", + "public abstract void addHeader(java.lang.String, java.lang.String)", + "public long getDateHeader(java.lang.String)", + "public abstract java.lang.String getHeader(java.lang.String)", + "public abstract java.util.Enumeration getHeaderNames()", + "public abstract java.util.List getHeaderNamesAsList()", + "public abstract java.util.Enumeration getHeaders(java.lang.String)", + "public abstract java.util.List getHeadersAsList(java.lang.String)", + "public abstract void removeHeaders(java.lang.String)", + "public abstract void setHeaders(java.lang.String, java.lang.String)", + "public abstract void setHeaders(java.lang.String, java.util.List)", + "public int getIntHeader(java.lang.String)", + "public java.util.List getCookies()", + "public void setCookies(java.util.List)", + "public long getConnectedAt(java.util.concurrent.TimeUnit)", + "public java.lang.String getProtocol()", + "public java.lang.String getQueryString()", + "public java.lang.String getRemoteUser()", + "public java.lang.String getRequestURI()", + "public java.lang.String getRequestedSessionId()", + "public java.lang.String getScheme()", + "public void setScheme(java.lang.String, boolean)", + "public java.lang.String getServerName()", + "public int getServerPort()", + "public abstract java.security.Principal getUserPrincipal()", + "public boolean isSecure()", + "public boolean isUserInRole(java.lang.String)", + "public void setOverrideIsUserInRole(boolean)", + "public void setRemoteHost(java.lang.String)", + "public void setRemoteUser(java.lang.String)", + "public abstract void setUserPrincipal(java.security.Principal)", + "public abstract java.util.List getClientCertificateChain()", + "public void setUserRoles(java.lang.String[])", + "public java.lang.String getContentType()", + "public java.lang.String getCharacterEncoding()", + "public void setCharacterEncoding(java.lang.String)", + "public void addCookie(com.yahoo.jdisc.http.filter.JDiscCookieWrapper)", + "public abstract void clearCookies()", + "public com.yahoo.jdisc.http.filter.JDiscCookieWrapper[] getWrappedCookies()", + "public static boolean isMultipart(com.yahoo.jdisc.http.filter.DiscFilterRequest)" + ], + "fields": [ + "protected static final java.lang.String HTTPS_PREFIX", + "protected static final int DEFAULT_HTTP_PORT", + "protected static final int DEFAULT_HTTPS_PORT", + "protected final java.util.Map untreatedParams", + "protected static com.yahoo.jdisc.http.filter.DiscFilterRequest$ThreadLocalSimpleDateFormat[] formats" + ] + }, + "com.yahoo.jdisc.http.filter.DiscFilterResponse": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "abstract" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse)", + "public java.util.Enumeration getAttributeNames()", + "public java.lang.Object getAttribute(java.lang.String)", + "public void setAttribute(java.lang.String, java.lang.Object)", + "public void removeAttribute(java.lang.String)", + "public com.yahoo.jdisc.HeaderFields getUntreatedHeaders()", + "public java.util.List getUntreatedCookies()", + "public abstract void setHeader(java.lang.String, java.lang.String)", + "public abstract void removeHeaders(java.lang.String)", + "public abstract void setHeaders(java.lang.String, java.lang.String)", + "public abstract void setHeaders(java.lang.String, java.util.List)", + "public abstract void addHeader(java.lang.String, java.lang.String)", + "public abstract java.lang.String getHeader(java.lang.String)", + "public java.util.List getCookies()", + "public abstract void setCookies(java.util.List)", + "public int getStatus()", + "public abstract void setStatus(int)", + "public com.yahoo.jdisc.http.HttpResponse getParentResponse()", + "public void addCookie(com.yahoo.jdisc.http.filter.JDiscCookieWrapper)", + "public void sendError(int)", + "public void setCookie(java.lang.String, java.lang.String)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.FilterConfig": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.String getFilterName()", + "public abstract java.lang.String getFilterClass()", + "public abstract java.lang.String getInitParameter(java.lang.String)", + "public abstract boolean getBooleanInitParameter(java.lang.String, boolean)", + "public abstract java.util.Collection getInitParameterNames()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.JDiscCookieWrapper": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "protected void (com.yahoo.jdisc.http.Cookie)", + "public static com.yahoo.jdisc.http.filter.JDiscCookieWrapper wrap(com.yahoo.jdisc.http.Cookie)", + "public java.lang.String getDomain()", + "public int getMaxAge()", + "public java.lang.String getName()", + "public java.lang.String getPath()", + "public boolean getSecure()", + "public java.lang.String getValue()", + "public void setDomain(java.lang.String)", + "public void setMaxAge(int)", + "public void setPath(java.lang.String)", + "public void setSecure(boolean)", + "public void setValue(java.lang.String)", + "public com.yahoo.jdisc.http.Cookie getCookie()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.JdiscFilterRequest": { + "superClass": "com.yahoo.jdisc.http.filter.DiscFilterRequest", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.HttpRequest)", + "public com.yahoo.jdisc.http.HttpRequest getParentRequest()", + "public void setUri(java.net.URI)", + "public java.lang.String getMethod()", + "public java.lang.String getParameter(java.lang.String)", + "public java.util.Enumeration getParameterNames()", + "public void addHeader(java.lang.String, java.lang.String)", + "public java.lang.String getHeader(java.lang.String)", + "public java.util.Enumeration getHeaderNames()", + "public java.util.List getHeaderNamesAsList()", + "public java.util.Enumeration getHeaders(java.lang.String)", + "public java.util.List getHeadersAsList(java.lang.String)", + "public void removeHeaders(java.lang.String)", + "public void setHeaders(java.lang.String, java.lang.String)", + "public void setHeaders(java.lang.String, java.util.List)", + "public java.security.Principal getUserPrincipal()", + "public void setUserPrincipal(java.security.Principal)", + "public java.util.List getClientCertificateChain()", + "public void clearCookies()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.JdiscFilterResponse": { + "superClass": "com.yahoo.jdisc.http.filter.DiscFilterResponse", + "interfaces": [], + "attributes": [ + "public" + ], + "methods": [ + "public void (com.yahoo.jdisc.http.HttpResponse)", + "public void setStatus(int)", + "public void setHeader(java.lang.String, java.lang.String)", + "public void removeHeaders(java.lang.String)", + "public void setHeaders(java.lang.String, java.lang.String)", + "public void setHeaders(java.lang.String, java.util.List)", + "public void addHeader(java.lang.String, java.lang.String)", + "public java.lang.String getHeader(java.lang.String)", + "public void setCookies(java.util.List)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.RequestFilter": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.SharedResource", + "com.yahoo.jdisc.http.filter.RequestFilterBase" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void filter(com.yahoo.jdisc.http.HttpRequest, com.yahoo.jdisc.handler.ResponseHandler)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.RequestFilterBase": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.RequestView": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract java.lang.Object getAttribute(java.lang.String)", + "public abstract java.util.List getHeaders(java.lang.String)", + "public abstract java.util.Optional getFirstHeader(java.lang.String)", + "public abstract java.util.Optional getMethod()", + "public abstract java.net.URI getUri()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.ResponseFilter": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.SharedResource", + "com.yahoo.jdisc.http.filter.ResponseFilterBase" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void filter(com.yahoo.jdisc.Response, com.yahoo.jdisc.Request)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.ResponseFilterBase": { + "superClass": "java.lang.Object", + "interfaces": [], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.SecurityFilterInvoker": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.http.server.jetty.FilterInvoker" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void ()", + "public javax.servlet.http.HttpServletRequest invokeRequestFilterChain(com.yahoo.jdisc.http.filter.RequestFilter, java.net.URI, javax.servlet.http.HttpServletRequest, com.yahoo.jdisc.handler.ResponseHandler)", + "public void invokeResponseFilterChain(com.yahoo.jdisc.http.filter.ResponseFilter, java.net.URI, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.SecurityRequestFilter": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.http.filter.RequestFilterBase" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void filter(com.yahoo.jdisc.http.filter.DiscFilterRequest, com.yahoo.jdisc.handler.ResponseHandler)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.SecurityRequestFilterChain": { + "superClass": "com.yahoo.jdisc.AbstractResource", + "interfaces": [ + "com.yahoo.jdisc.http.filter.RequestFilter" + ], + "attributes": [ + "public", + "final" + ], + "methods": [ + "public void filter(com.yahoo.jdisc.http.HttpRequest, com.yahoo.jdisc.handler.ResponseHandler)", + "public void filter(com.yahoo.jdisc.http.filter.DiscFilterRequest, com.yahoo.jdisc.handler.ResponseHandler)", + "public static varargs com.yahoo.jdisc.http.filter.RequestFilter newInstance(com.yahoo.jdisc.http.filter.SecurityRequestFilter[])", + "public static com.yahoo.jdisc.http.filter.RequestFilter newInstance(java.util.List)", + "public java.util.List getFilters()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.SecurityResponseFilter": { + "superClass": "java.lang.Object", + "interfaces": [ + "com.yahoo.jdisc.http.filter.ResponseFilterBase" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract void filter(com.yahoo.jdisc.http.filter.DiscFilterResponse, com.yahoo.jdisc.http.filter.RequestView)" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.filter.SecurityResponseFilterChain": { + "superClass": "com.yahoo.jdisc.AbstractResource", + "interfaces": [ + "com.yahoo.jdisc.http.filter.ResponseFilter" + ], + "attributes": [ + "public" + ], + "methods": [ + "public void filter(com.yahoo.jdisc.Response, com.yahoo.jdisc.Request)", + "public void filter(com.yahoo.jdisc.http.filter.RequestView, com.yahoo.jdisc.http.filter.DiscFilterResponse)", + "public static varargs com.yahoo.jdisc.http.filter.ResponseFilter newInstance(com.yahoo.jdisc.http.filter.SecurityResponseFilter[])", + "public static com.yahoo.jdisc.http.filter.ResponseFilter newInstance(java.util.List)", + "public java.util.List getFilters()" + ], + "fields": [] + }, + "com.yahoo.jdisc.http.ssl.SslContextFactoryProvider": { + "superClass": "java.lang.Object", + "interfaces": [ + "java.lang.AutoCloseable" + ], + "attributes": [ + "public", + "interface", + "abstract" + ], + "methods": [ + "public abstract org.eclipse.jetty.util.ssl.SslContextFactory getInstance(java.lang.String, int)", + "public void close()" + ], + "fields": [] + }, "com.yahoo.processing.IllegalInputException": { "superClass": "java.lang.IllegalArgumentException", "interfaces": [], -- cgit v1.2.3 From 91af6a9faba14093880ff83a927f2da6dad9cc9b Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 10:54:58 +0100 Subject: Install bouncycastle dependency jars. --- container-core/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/container-core/CMakeLists.txt b/container-core/CMakeLists.txt index 74f02518b59..433e3c799b5 100644 --- a/container-core/CMakeLists.txt +++ b/container-core/CMakeLists.txt @@ -1,4 +1,5 @@ # Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. install_config_definitions() +install_java_artifact_dependencies(container-core) vespa_install_script(src/main/sh/vespa-load-balancer-status libexec/vespa) -- cgit v1.2.3 From 1c33f2616f3e40e624d83efc9b2ffa19741394d2 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 11:33:10 +0100 Subject: Remove all dependencies to jdisc_http_service --- application/pom.xml | 2 +- cloud-tenant-base-dependencies-enforcer/pom.xml | 1 - config-model/pom.xml | 6 ------ container-dev/pom.xml | 11 ----------- container-search-and-docproc/pom.xml | 6 ------ container-search-gui/pom.xml | 6 ------ controller-server/pom.xml | 7 ------- fat-model-dependencies/pom.xml | 5 ----- jdisc-cloud-aws/pom.xml | 6 ------ metrics-proxy/pom.xml | 6 ------ model-evaluation/pom.xml | 6 ------ provided-dependencies/pom.xml | 5 ----- vespaclient-java/pom.xml | 5 ----- 13 files changed, 1 insertion(+), 71 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 96250acb5a9..1e14610af69 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -60,7 +60,7 @@ ${project.version} compile - + com.yahoo.vespa jdisc_jetty diff --git a/cloud-tenant-base-dependencies-enforcer/pom.xml b/cloud-tenant-base-dependencies-enforcer/pom.xml index d821501bd2f..75c837cc593 100644 --- a/cloud-tenant-base-dependencies-enforcer/pom.xml +++ b/cloud-tenant-base-dependencies-enforcer/pom.xml @@ -167,7 +167,6 @@ com.yahoo.vespa:hosted-zone-api:*:jar:provided com.yahoo.vespa:http-utils:*:jar:provided com.yahoo.vespa:jdisc_core:*:jar:provided - com.yahoo.vespa:jdisc_http_service:*:jar:provided com.yahoo.vespa:jdisc_messagebus_service:*:jar:provided com.yahoo.vespa:jrt:*:jar:provided com.yahoo.vespa:linguistics:*:jar:provided diff --git a/config-model/pom.xml b/config-model/pom.xml index 8d587c2ec8c..daedc7b4a96 100644 --- a/config-model/pom.xml +++ b/config-model/pom.xml @@ -288,12 +288,6 @@ searchsummary ${project.version} - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa jdisc-security-filters diff --git a/container-dev/pom.xml b/container-dev/pom.xml index 48f409dfc2c..2dcb03bba58 100644 --- a/container-dev/pom.xml +++ b/container-dev/pom.xml @@ -34,17 +34,6 @@ - - com.yahoo.vespa - jdisc_http_service - ${project.version} - - - org.apache.httpcomponents - httpclient - - - com.yahoo.vespa simplemetrics diff --git a/container-search-and-docproc/pom.xml b/container-search-and-docproc/pom.xml index 6e256fc0ef6..27334a8ddf3 100644 --- a/container-search-and-docproc/pom.xml +++ b/container-search-and-docproc/pom.xml @@ -126,12 +126,6 @@ ${project.version} provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa simplemetrics diff --git a/container-search-gui/pom.xml b/container-search-gui/pom.xml index 64c53a1f453..b1a02a2411a 100644 --- a/container-search-gui/pom.xml +++ b/container-search-gui/pom.xml @@ -26,12 +26,6 @@ ${project.version} provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa jdisc_core diff --git a/controller-server/pom.xml b/controller-server/pom.xml index 906cc5c0ee1..0c05f7d70bb 100644 --- a/controller-server/pom.xml +++ b/controller-server/pom.xml @@ -40,13 +40,6 @@ provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - - com.yahoo.vespa zkfacade diff --git a/fat-model-dependencies/pom.xml b/fat-model-dependencies/pom.xml index 694f4b4d14f..d3228cf3933 100644 --- a/fat-model-dependencies/pom.xml +++ b/fat-model-dependencies/pom.xml @@ -206,11 +206,6 @@ searchsummary ${project.version} - - com.yahoo.vespa - jdisc_http_service - ${project.version} - com.google.protobuf protobuf-java diff --git a/jdisc-cloud-aws/pom.xml b/jdisc-cloud-aws/pom.xml index 1406a70ece0..9f4572736c8 100644 --- a/jdisc-cloud-aws/pom.xml +++ b/jdisc-cloud-aws/pom.xml @@ -41,12 +41,6 @@ ${project.version} provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa component diff --git a/metrics-proxy/pom.xml b/metrics-proxy/pom.xml index 90d9f093da1..928179a6124 100644 --- a/metrics-proxy/pom.xml +++ b/metrics-proxy/pom.xml @@ -77,12 +77,6 @@ ${project.version} provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa jrt diff --git a/model-evaluation/pom.xml b/model-evaluation/pom.xml index 3cf3988fc48..00560a22bc7 100644 --- a/model-evaluation/pom.xml +++ b/model-evaluation/pom.xml @@ -72,12 +72,6 @@ guava provided - - com.yahoo.vespa - jdisc_http_service - ${project.version} - provided - com.yahoo.vespa jdisc_jetty diff --git a/provided-dependencies/pom.xml b/provided-dependencies/pom.xml index e73d9902ebe..acad1f9e444 100755 --- a/provided-dependencies/pom.xml +++ b/provided-dependencies/pom.xml @@ -22,11 +22,6 @@ jdisc_core ${project.version} - - com.yahoo.vespa - jdisc_http_service - ${project.version} - org.bouncycastle bcprov-jdk15on diff --git a/vespaclient-java/pom.xml b/vespaclient-java/pom.xml index 93f1579f58c..566b48ba933 100644 --- a/vespaclient-java/pom.xml +++ b/vespaclient-java/pom.xml @@ -49,11 +49,6 @@ documentapi ${project.version} - - com.yahoo.vespa - jdisc_http_service - ${project.version} - com.yahoo.vespa predicate-search-core -- cgit v1.2.3 From 9eee7e2229d5a810fe20f2e656b972d437daa62f Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 18:27:34 +0100 Subject: Do not install the jdisc_http_service bundle. --- container-disc/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/container-disc/pom.xml b/container-disc/pom.xml index c88ca8f1291..b8fbf5fae2a 100644 --- a/container-disc/pom.xml +++ b/container-disc/pom.xml @@ -188,7 +188,6 @@ docprocs-jar-with-dependencies.jar, hosted-zone-api-jar-with-dependencies.jar, jdisc-security-filters-jar-with-dependencies.jar, - jdisc_http_service-jar-with-dependencies.jar, model-evaluation-jar-with-dependencies.jar, model-integration-jar-with-dependencies.jar, vespaclient-container-plugin-jar-with-dependencies.jar, -- cgit v1.2.3 From 8576c65baa3b66a06ae25a6342c589bcfd2d5cbe Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 18:28:17 +0100 Subject: Do not build or install the jdisc_http_service module. --- CMakeLists.txt | 1 - dist/vespa.spec | 1 - pom.xml | 1 - 3 files changed, 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 40f60d7daab..1b4a87aa39c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,6 @@ add_subdirectory(hosted-zone-api) add_subdirectory(jdisc-cloud-aws) add_subdirectory(jdisc_core) add_subdirectory(jdisc-security-filters) -add_subdirectory(jdisc_http_service) add_subdirectory(jdisc_jetty) add_subdirectory(jrt_test) add_subdirectory(juniper) diff --git a/dist/vespa.spec b/dist/vespa.spec index 6e154a64a80..c357784fde7 100644 --- a/dist/vespa.spec +++ b/dist/vespa.spec @@ -648,7 +648,6 @@ fi %{_prefix}/lib/jars/javax.*.jar %{_prefix}/lib/jars/jdisc-cloud-aws-jar-with-dependencies.jar %{_prefix}/lib/jars/jdisc_core-jar-with-dependencies.jar -%{_prefix}/lib/jars/jdisc_http_service-jar-with-dependencies.jar %{_prefix}/lib/jars/jdisc-security-filters-jar-with-dependencies.jar %{_prefix}/lib/jars/jersey-*.jar %{_prefix}/lib/jars/jetty-*.jar diff --git a/pom.xml b/pom.xml index 60fe298330f..abba8594956 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,6 @@ jdisc-security-filters jdisc_core jdisc_core_test - jdisc_http_service jdisc_jetty jdisc_messagebus_service jrt -- cgit v1.2.3 From b767a2a69fb57e2a534ee5021aaac83f07bed9a2 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 18:31:36 +0100 Subject: Remove jdisc_http_service, chain and processing from Code-map. --- Code-map.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Code-map.md b/Code-map.md index 3c54be35b9d..0ed326a2005 100644 --- a/Code-map.md +++ b/Code-map.md @@ -40,16 +40,13 @@ The stateless container is implemented in Java. jDisc core modules: - [jdisc_core](https://github.com/vespa-engine/vespa/tree/master/jdisc_core) - the core jDisc functionality -- [jdisc_http_service](https://github.com/vespa-engine/vespa/tree/master/jdisc_http_service) - HTTP connector for jDisc, implemented using Jetty. jDisc container modules, layered on jDisc core: - [container-disc](https://github.com/vespa-engine/vespa/tree/master/container-disc) - integration between the jDisc container and jDisc core layers. -- [container-core](https://github.com/vespa-engine/vespa/tree/master/container-core) - core jDisc container functionality: Metrics, OSGi integration for component bundles, etc. +- [container-core](https://github.com/vespa-engine/vespa/tree/master/container-core) - core jDisc container functionality: Metrics, OSGi integration for component bundles, HTTP connector, etc. - [component](https://github.com/vespa-engine/vespa/tree/master/component) - the component model. Components (in Java) will implement or subclass a type for this module. -- [chain](https://github.com/vespa-engine/vespa/tree/master/chain) - generic support for chaining components in a Chain of Responsibility structure, which is a pattern used repeatedly in higher level modules. - [container-di](https://github.com/vespa-engine/vespa/tree/master/container-di) - component dependency injection framework for the container, compatible with Guice annotations but an separate implementation which handles injection of config and injection of component collections. -- [processing](https://github.com/vespa-engine/vespa/tree/master/processing) - generic, chainable request-response processing framework (Processors). Search container, layered on jDisc container: -- cgit v1.2.3 From ba7050936f25f26deb247cdbb1defabbf7d9acee Mon Sep 17 00:00:00 2001 From: gjoranv Date: Fri, 19 Mar 2021 18:37:00 +0100 Subject: Update class comment. - This class was originally in jdisc_http_service, so it's unclear what its intention was. --- .../src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java index 875889ed5ce..49928a5679e 100644 --- a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java @@ -13,8 +13,7 @@ import java.nio.file.Paths; import static com.yahoo.yolean.Exceptions.uncheck; /** - * This class is based on the class by the same name in the jdisc_http_service module. - * It provides functionality for setting up a jdisc container with an HTTP server and handlers. + * Provides functionality for setting up a jdisc container with an HTTP server and handlers. * * @author Simon Thoresen Hult * @author bakksjo -- cgit v1.2.3 From bee5245ef2ed520a449b514750314e36ac39ffec Mon Sep 17 00:00:00 2001 From: gjoranv Date: Tue, 23 Mar 2021 20:22:22 +0100 Subject: Update all component specs to use the container-disc bundle .. instead of jdisc_http_service. --- .../java/com/yahoo/vespa/model/container/Container.java | 2 +- .../yahoo/vespa/model/container/ContainerCluster.java | 2 +- .../container/component/ConnectionLogComponent.java | 2 +- .../vespa/model/container/http/ConnectorFactory.java | 8 +++----- .../vespa/model/container/http/JettyHttpServer.java | 16 ++++++---------- .../container/http/ssl/ConfiguredDirectSslProvider.java | 3 +-- .../http/ssl/ConfiguredFilebasedSslProvider.java | 3 +-- .../model/container/http/ssl/DefaultSslProvider.java | 3 +-- .../model/container/http/xml/JettyHttpServerBuilder.java | 3 +-- .../vespa/model/container/xml/ContainerModelBuilder.java | 2 +- .../container/xml/JettyContainerModelBuilderTest.java | 3 ++- 11 files changed, 19 insertions(+), 28 deletions(-) diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java index 5e95403313c..d075e661149 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/Container.java @@ -88,7 +88,7 @@ public abstract class Container extends AbstractService implements this.parent = parent; this.retired = retired; this.index = index; - this.defaultHttpServer = new JettyHttpServer(new ComponentId("DefaultHttpServer"), containerClusterOrNull(parent), isHostedVespa); + this.defaultHttpServer = new JettyHttpServer("DefaultHttpServer", containerClusterOrNull(parent), isHostedVespa); if (getHttp() == null) { addChild(defaultHttpServer); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java index db4cda8b72e..8868c55becb 100755 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/ContainerCluster.java @@ -173,7 +173,7 @@ public abstract class ContainerCluster componentGroup = new ComponentGroup<>(this, "component"); addComponent(new StatisticsComponent()); - addSimpleComponent(AccessLog.class.getName(), null, "jdisc_http_service"); + addSimpleComponent(AccessLog.class); addComponent(new DefaultThreadpoolProvider(this)); addSimpleComponent(com.yahoo.concurrent.classlock.ClassLocking.class); addSimpleComponent(SecurityFilterInvoker.class); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java index 8eff7bf7201..83e06aab703 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/component/ConnectionLogComponent.java @@ -20,7 +20,7 @@ public class ConnectionLogComponent extends SimpleComponent implements Connectio } public ConnectionLogComponent(ContainerCluster cluster, Class cls, String logDirectoryName, String clusterName) { - super(new ComponentModel(cls.getName(), null, "jdisc_http_service", null)); + super(cls.getName()); this.logDirectoryName = logDirectoryName; this.clusterName = clusterName; this.queueSize = queueSize(cluster).orElse(-1); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java index 67b3ec0f6a7..989f911fc5e 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ConnectorFactory.java @@ -27,11 +27,9 @@ public class ConnectorFactory extends SimpleComponent implements ConnectorConfig private volatile ComponentId defaultResponseFilterChain; protected ConnectorFactory(Builder builder) { - super(new ComponentModel( - new BundleInstantiationSpecification( - new ComponentId(builder.name), - fromString("com.yahoo.jdisc.http.server.jetty.ConnectorFactory"), - fromString("jdisc_http_service")))); + super(new ComponentModel(builder.name, + com.yahoo.jdisc.http.server.jetty.ConnectorFactory.class.getName(), + null)); this.name = builder.name; this.listenPort = builder.listenPort; this.sslProviderComponent = builder.sslProvider != null ? builder.sslProvider : new DefaultSslProvider(name); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java index 674d2ce3325..f36a1448d5c 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/JettyHttpServer.java @@ -26,15 +26,11 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro private volatile boolean isHostedVespa; private final List connectorFactories = new ArrayList<>(); - public JettyHttpServer(ComponentId id, ContainerCluster cluster, boolean isHostedVespa) { - super(new ComponentModel( - new BundleInstantiationSpecification(id, - fromString("com.yahoo.jdisc.http.server.jetty.JettyHttpServer"), - fromString("jdisc_http_service")) - )); + public JettyHttpServer(String componentId, ContainerCluster cluster, boolean isHostedVespa) { + super(new ComponentModel(componentId, com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName(),null)); this.isHostedVespa = isHostedVespa; this.cluster = cluster; - final FilterBindingsProviderComponent filterBindingsProviderComponent = new FilterBindingsProviderComponent(id); + final FilterBindingsProviderComponent filterBindingsProviderComponent = new FilterBindingsProviderComponent(componentId); addChild(filterBindingsProviderComponent); inject(filterBindingsProviderComponent); } @@ -89,17 +85,17 @@ public class JettyHttpServer extends SimpleComponent implements ServerConfig.Pro } } - static ComponentModel providerComponentModel(final ComponentId parentId, String className) { + static ComponentModel providerComponentModel(String parentId, String className) { final ComponentSpecification classNameSpec = new ComponentSpecification( className); return new ComponentModel(new BundleInstantiationSpecification( - classNameSpec.nestInNamespace(parentId), + classNameSpec.nestInNamespace(new ComponentId(parentId)), classNameSpec, null)); } public static final class FilterBindingsProviderComponent extends SimpleComponent { - public FilterBindingsProviderComponent(final ComponentId parentId) { + public FilterBindingsProviderComponent(String parentId) { super(providerComponentModel(parentId, "com.yahoo.container.jdisc.FilterBindingsProvider")); } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java index 8f5970453a5..690e4376120 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredDirectSslProvider.java @@ -17,7 +17,6 @@ import static com.yahoo.jdisc.http.ConnectorConfig.Ssl.ClientAuth; public class ConfiguredDirectSslProvider extends SslProvider { public static final String COMPONENT_ID_PREFIX = "configured-ssl-provider@"; public static final String COMPONENT_CLASS = ConfiguredSslContextFactoryProvider.class.getName(); - public static final String COMPONENT_BUNDLE = "jdisc_http_service"; private final String privateKey; private final String certificate; @@ -26,7 +25,7 @@ public class ConfiguredDirectSslProvider extends SslProvider { private final ClientAuth.Enum clientAuthentication; public ConfiguredDirectSslProvider(String servername, String privateKey, String certificate, String caCertificatePath, String caCertificate, ClientAuth.Enum clientAuthentication) { - super(COMPONENT_ID_PREFIX, servername, COMPONENT_CLASS, COMPONENT_BUNDLE); + super(COMPONENT_ID_PREFIX, servername, COMPONENT_CLASS, null); this.privateKey = privateKey; this.certificate = certificate; this.caCertificatePath = caCertificatePath; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java index a19626db8bc..656c7d8e778 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/ConfiguredFilebasedSslProvider.java @@ -16,7 +16,6 @@ import java.util.Optional; public class ConfiguredFilebasedSslProvider extends SslProvider { public static final String COMPONENT_ID_PREFIX = "configured-ssl-provider@"; public static final String COMPONENT_CLASS = ConfiguredSslContextFactoryProvider.class.getName(); - public static final String COMPONENT_BUNDLE = "jdisc_http_service"; private final String privateKeyPath; private final String certificatePath; @@ -32,7 +31,7 @@ public class ConfiguredFilebasedSslProvider extends SslProvider { String clientAuthentication, List cipherSuites, List protocolVersions) { - super(COMPONENT_ID_PREFIX, servername, COMPONENT_CLASS, COMPONENT_BUNDLE); + super(COMPONENT_ID_PREFIX, servername, COMPONENT_CLASS, null); this.privateKeyPath = privateKeyPath; this.certificatePath = certificatePath; this.caCertificatePath = caCertificatePath; diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/DefaultSslProvider.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/DefaultSslProvider.java index 215c1813e95..6504d8ac653 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/DefaultSslProvider.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/ssl/DefaultSslProvider.java @@ -11,10 +11,9 @@ public class DefaultSslProvider extends SslProvider { public static final String COMPONENT_ID_PREFIX = "default-ssl-provider@"; public static final String COMPONENT_CLASS = DefaultSslContextFactoryProvider.class.getName(); - public static final String COMPONENT_BUNDLE = "jdisc_http_service"; public DefaultSslProvider(String serverName) { - super(COMPONENT_ID_PREFIX, serverName, COMPONENT_CLASS, COMPONENT_BUNDLE); + super(COMPONENT_ID_PREFIX, serverName, COMPONENT_CLASS, null); } @Override public void amendConnectorConfig(ConnectorConfig.Builder builder) {} diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java index f3e82d515df..f0be8ebc29e 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/http/xml/JettyHttpServerBuilder.java @@ -24,8 +24,7 @@ public class JettyHttpServerBuilder extends VespaDomBuilder.DomConfigProducerBui @Override protected JettyHttpServer doBuild(DeployState deployState, AbstractConfigProducer ancestor, Element http) { - JettyHttpServer jettyHttpServer = new JettyHttpServer( - new ComponentId("jdisc-jetty"), cluster, deployState.isHosted()); + JettyHttpServer jettyHttpServer = new JettyHttpServer("jdisc-jetty", cluster, deployState.isHosted()); for (Element serverSpec: XML.getChildren(http, "server")) { ConnectorFactory connectorFactory = new JettyConnectorBuilder().build(deployState, ancestor, serverSpec); jettyHttpServer.addConnector(connectorFactory); diff --git a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java index f2e8757c115..19141bd1f4d 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/container/xml/ContainerModelBuilder.java @@ -464,7 +464,7 @@ public class ContainerModelBuilder extends ConfigModelBuilder { } JettyHttpServer httpServer = cluster.getHttp().getHttpServer().orElse(null); if (httpServer == null) { - httpServer = new JettyHttpServer(new ComponentId("DefaultHttpServer"), cluster, cluster.isHostedVespa()); + httpServer = new JettyHttpServer("DefaultHttpServer", cluster, cluster.isHostedVespa()); cluster.getHttp().setHttpServer(httpServer); } int defaultPort = Defaults.getDefaults().vespaWebServicePort(); diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java index 0f9bd506310..ca19b8196df 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java @@ -308,7 +308,8 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas JettyHttpServer server = jettyServers.get(0); assertThat(server.model.bundleInstantiationSpec.classId.toString(), is(com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName())); - assertThat(server.model.bundleInstantiationSpec.bundle.toString(), is("jdisc_http_service")); + assertThat(server.model.bundleInstantiationSpec.bundle.toString(), + is(com.yahoo.jdisc.http.server.jetty.JettyHttpServer.class.getName())); assertThat(server.getConnectorFactories().size(), is(1)); assertThat( -- cgit v1.2.3 From 266046b2bb8bebbb683499f558a05aaf8a4f1ff3 Mon Sep 17 00:00:00 2001 From: gjoranv Date: Tue, 23 Mar 2021 21:20:45 +0100 Subject: Fix raw usage of ContainerCluster in unit test. --- .../vespa/model/container/xml/JettyContainerModelBuilderTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java index ca19b8196df..b3b347aaa7f 100644 --- a/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java +++ b/config-model/src/test/java/com/yahoo/vespa/model/container/xml/JettyContainerModelBuilderTest.java @@ -194,7 +194,7 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas assertThat(withCiphersAndProtocols.ssl().enabledCipherSuites(), is(equalTo(List.of("TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384")))); assertThat(withCiphersAndProtocols.ssl().enabledProtocols(), is(equalTo(List.of("TLSv1.3")))); - ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); + ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); List connectorFactories = cluster.getChildrenByTypeRecursive(ConnectorFactory.class); connectorFactories.forEach(connectorFactory -> assertChildComponentExists(connectorFactory, ConfiguredFilebasedSslProvider.COMPONENT_CLASS)); } @@ -217,7 +217,7 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas assertTrue(sslProvider.ssl().enabled()); - ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); + ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); List connectorFactories = cluster.getChildrenByTypeRecursive(ConnectorFactory.class); ConnectorFactory connectorFactory = connectorFactories.get(0); assertChildComponentExists(connectorFactory, "com.yahoo.CustomSslProvider"); @@ -300,7 +300,7 @@ public class JettyContainerModelBuilderTest extends ContainerModelBuilderTestBas } private void assertJettyServerInConfig() { - ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); + ContainerCluster cluster = (ContainerCluster) root.getChildren().get("default"); List jettyServers = cluster.getChildrenByTypeRecursive(JettyHttpServer.class); assertThat(jettyServers.size(), is(1)); -- cgit v1.2.3 From 5617a82f7a32ebcc37be226b27f6ff284f5c896d Mon Sep 17 00:00:00 2001 From: gjoranv Date: Tue, 23 Mar 2021 21:21:39 +0100 Subject: Remove the jdisc_http_service module. - It has been merged into container-core. --- .../com/yahoo/container/logging/TraceRenderer.java | 1 + .../container.logging.connection-log.def | 1 + .../logging/JsonConnectionLogWriterTest.java | 3 +- jdisc_http_service/.gitignore | 2 - jdisc_http_service/CMakeLists.txt | 5 - jdisc_http_service/OWNERS | 2 - jdisc_http_service/README.md | 4 - jdisc_http_service/abi-spec.json | 1482 -------------------- jdisc_http_service/pom.xml | 222 --- .../com/yahoo/container/logging/AccessLog.java | 36 - .../yahoo/container/logging/AccessLogEntry.java | 112 -- .../yahoo/container/logging/AccessLogHandler.java | 36 - .../logging/CircularArrayAccessLogKeeper.java | 48 - .../com/yahoo/container/logging/ConnectionLog.java | 10 - .../container/logging/ConnectionLogEntry.java | 225 --- .../container/logging/ConnectionLogHandler.java | 30 - .../java/com/yahoo/container/logging/Coverage.java | 64 - .../yahoo/container/logging/FileConnectionLog.java | 30 - .../com/yahoo/container/logging/FormatUtil.java | 46 - .../com/yahoo/container/logging/HitCounts.java | 78 -- .../com/yahoo/container/logging/JSONAccessLog.java | 27 - .../com/yahoo/container/logging/JSONFormatter.java | 193 --- .../container/logging/JsonConnectionLogWriter.java | 122 -- .../yahoo/container/logging/LogFileHandler.java | 563 -------- .../com/yahoo/container/logging/LogFormatter.java | 191 --- .../com/yahoo/container/logging/LogWriter.java | 10 - .../com/yahoo/container/logging/RequestLog.java | 13 - .../yahoo/container/logging/RequestLogEntry.java | 186 --- .../yahoo/container/logging/RequestLogHandler.java | 9 - .../com/yahoo/container/logging/TraceRenderer.java | 186 --- .../yahoo/container/logging/VespaAccessLog.java | 113 -- .../com/yahoo/container/logging/package-info.java | 5 - .../com/yahoo/jdisc/http/CertificateStore.java | 26 - .../src/main/java/com/yahoo/jdisc/http/Cookie.java | 250 ---- .../java/com/yahoo/jdisc/http/HttpHeaders.java | 122 -- .../java/com/yahoo/jdisc/http/HttpRequest.java | 342 ----- .../java/com/yahoo/jdisc/http/HttpResponse.java | 125 -- .../java/com/yahoo/jdisc/http/SecretStore.java | 23 - .../com/yahoo/jdisc/http/cloud/package-info.java | 4 - .../yahoo/jdisc/http/filter/DiscFilterRequest.java | 543 ------- .../jdisc/http/filter/DiscFilterResponse.java | 154 -- .../com/yahoo/jdisc/http/filter/FilterConfig.java | 42 - .../jdisc/http/filter/JDiscCookieWrapper.java | 79 -- .../jdisc/http/filter/JdiscFilterRequest.java | 133 -- .../jdisc/http/filter/JdiscFilterResponse.java | 67 - .../com/yahoo/jdisc/http/filter/RequestFilter.java | 14 - .../yahoo/jdisc/http/filter/RequestFilterBase.java | 9 - .../com/yahoo/jdisc/http/filter/RequestView.java | 45 - .../yahoo/jdisc/http/filter/ResponseFilter.java | 14 - .../jdisc/http/filter/ResponseFilterBase.java | 9 - .../jdisc/http/filter/SecurityFilterInvoker.java | 108 -- .../jdisc/http/filter/SecurityRequestFilter.java | 13 - .../http/filter/SecurityRequestFilterChain.java | 77 - .../jdisc/http/filter/SecurityResponseFilter.java | 8 - .../http/filter/SecurityResponseFilterChain.java | 101 -- .../jdisc/http/filter/ServletFilterRequest.java | 169 --- .../jdisc/http/filter/ServletFilterResponse.java | 81 -- .../http/filter/chain/EmptyRequestFilter.java | 24 - .../http/filter/chain/EmptyResponseFilter.java | 24 - .../http/filter/chain/RequestFilterChain.java | 55 - .../http/filter/chain/ResponseFilterChain.java | 54 - .../http/filter/chain/ResponseHandlerGuard.java | 29 - .../jdisc/http/filter/chain/package-info.java | 5 - .../com/yahoo/jdisc/http/filter/package-info.java | 7 - .../java/com/yahoo/jdisc/http/package-info.java | 7 - .../http/server/jetty/AccessLogRequestLog.java | 167 --- .../server/jetty/AccessLoggingRequestHandler.java | 59 - .../http/server/jetty/AsyncCompleteListener.java | 22 - .../http/server/jetty/CompletionHandlerUtils.java | 14 - .../http/server/jetty/CompletionHandlers.java | 57 - .../http/server/jetty/ConnectionThrottler.java | 274 ---- .../jdisc/http/server/jetty/ConnectorFactory.java | 140 -- .../server/jetty/ErrorResponseContentCreator.java | 41 - .../jdisc/http/server/jetty/ExceptionWrapper.java | 59 - .../jdisc/http/server/jetty/FilterBindings.java | 102 -- .../jdisc/http/server/jetty/FilterInvoker.java | 28 - .../server/jetty/FilterInvokingPrintWriter.java | 266 ---- .../jetty/FilterInvokingServletOutputStream.java | 165 --- .../jdisc/http/server/jetty/FilterResolver.java | 88 -- .../http/server/jetty/FilteringRequestHandler.java | 134 -- .../http/server/jetty/FormPostRequestHandler.java | 188 --- .../http/server/jetty/HealthCheckProxyHandler.java | 274 ---- .../http/server/jetty/HttpRequestDispatch.java | 243 ---- .../http/server/jetty/HttpRequestFactory.java | 87 -- .../jetty/HttpResponseStatisticsCollector.java | 300 ---- .../http/server/jetty/HttpServletRequestUtils.java | 38 - .../jdisc/http/server/jetty/JDiscContext.java | 33 - .../server/jetty/JDiscFilterInvokerFilter.java | 294 ---- .../jdisc/http/server/jetty/JDiscHttpServlet.java | 148 -- .../http/server/jetty/JDiscServerConnector.java | 104 -- .../http/server/jetty/JettyConnectionLogger.java | 373 ----- .../jdisc/http/server/jetty/JettyHttpServer.java | 298 ---- .../jdisc/http/server/jetty/MetricDefinitions.java | 79 -- .../jdisc/http/server/jetty/OneTimeRunnable.java | 23 - .../jetty/ReferenceCountingRequestHandler.java | 257 ---- .../jdisc/http/server/jetty/RequestException.java | 39 - .../http/server/jetty/RequestMetricReporter.java | 85 -- .../http/server/jetty/SecuredRedirectHandler.java | 58 - .../http/server/jetty/ServerMetricReporter.java | 115 -- .../server/jetty/ServletOutputStreamWriter.java | 299 ---- .../http/server/jetty/ServletRequestReader.java | 270 ---- .../server/jetty/ServletResponseController.java | 251 ---- .../server/jetty/SslHandshakeFailedListener.java | 52 - .../http/server/jetty/SslHandshakeFailure.java | 61 - .../jetty/TlsClientAuthenticationEnforcer.java | 83 -- .../server/jetty/UnsupportedFilterInvoker.java | 32 - .../jdisc/http/server/jetty/VoidConnectionLog.java | 16 - .../jdisc/http/server/jetty/VoidRequestLog.java | 14 - .../jdisc/http/server/jetty/package-info.java | 3 - .../http/servlet/ServletOrJdiscHttpRequest.java | 40 - .../http/servlet/ServletOrJdiscHttpResponse.java | 23 - .../yahoo/jdisc/http/servlet/ServletRequest.java | 272 ---- .../yahoo/jdisc/http/servlet/ServletResponse.java | 66 - .../com/yahoo/jdisc/http/servlet/package-info.java | 5 - .../jdisc/http/ssl/SslContextFactoryProvider.java | 21 - .../impl/ConfiguredSslContextFactoryProvider.java | 138 -- .../ssl/impl/DefaultSslContextFactoryProvider.java | 79 -- .../http/ssl/impl/JDiscSslContextFactory.java | 37 - .../http/ssl/impl/SslContextFactoryUtils.java | 32 - .../http/ssl/impl/TlsContextBasedProvider.java | 42 - .../yahoo/jdisc/http/ssl/impl/package-info.java | 8 - .../com/yahoo/jdisc/http/ssl/package-info.java | 10 - .../container.logging.connection-log.def | 11 - ...c.http.client.jdisc.http.client.http-client.def | 36 - .../jdisc.http.jdisc.http.connector.def | 127 -- .../jdisc.http.jdisc.http.server.def | 67 - .../jdisc.http.jdisc.http.servlet-paths.def | 5 - .../logging/CircularArrayAccessLogKeeperTest.java | 42 - .../yahoo/container/logging/JSONLogTestCase.java | 295 ---- .../logging/JsonConnectionLogWriterTest.java | 44 - .../container/logging/LogFileHandlerTestCase.java | 208 --- .../logging/test/LogFormatterTestCase.java | 27 - .../java/com/yahoo/jdisc/http/CookieTestCase.java | 238 ---- .../com/yahoo/jdisc/http/HttpHeadersTestCase.java | 17 - .../com/yahoo/jdisc/http/HttpRequestTestCase.java | 206 --- .../com/yahoo/jdisc/http/HttpResponseTestCase.java | 139 -- .../jdisc/http/filter/DiscFilterRequestTest.java | 357 ----- .../jdisc/http/filter/DiscFilterResponseTest.java | 113 -- .../http/filter/EmptyRequestFilterTestCase.java | 48 - .../http/filter/EmptyResponseFilterTestCase.java | 45 - .../jdisc/http/filter/JDiscCookieWrapperTest.java | 29 - .../jdisc/http/filter/RequestViewImplTest.java | 57 - .../jdisc/http/filter/ResponseHeaderFilter.java | 25 - .../filter/SecurityRequestFilterChainTest.java | 145 -- .../filter/SecurityResponseFilterChainTest.java | 74 - .../http/filter/ServletFilterRequestTest.java | 179 --- .../http/filter/ServletFilterResponseTest.java | 87 -- .../ConnectorFactoryRegistryModule.java | 54 - .../jdisc/http/guiceModules/ServletModule.java | 24 - .../http/server/jetty/AccessLogRequestLogTest.java | 156 --- .../http/server/jetty/BlockingQueueRequestLog.java | 24 - .../http/server/jetty/ConnectionThrottlerTest.java | 78 -- .../http/server/jetty/ConnectorFactoryTest.java | 83 -- .../jetty/ErrorResponseContentCreatorTest.java | 44 - .../http/server/jetty/ExceptionWrapperTest.java | 51 - .../jdisc/http/server/jetty/FilterTestCase.java | 667 --------- .../http/server/jetty/HttpRequestFactoryTest.java | 204 --- .../jetty/HttpResponseStatisticsCollectorTest.java | 221 --- .../server/jetty/HttpServerConformanceTest.java | 847 ----------- .../jdisc/http/server/jetty/HttpServerTest.java | 1201 ---------------- .../http/server/jetty/InMemoryConnectionLog.java | 25 - .../http/server/jetty/InMemoryRequestLog.java | 20 - .../http/server/jetty/JDiscHttpServletTest.java | 80 -- .../http/server/jetty/MetricConsumerMock.java | 28 - .../jdisc/http/server/jetty/SimpleHttpClient.java | 202 --- .../jetty/SslHandshakeFailedListenerTest.java | 42 - .../yahoo/jdisc/http/server/jetty/TestDriver.java | 79 -- .../yahoo/jdisc/http/server/jetty/TestDrivers.java | 94 -- .../jetty/servlet/JDiscFilterForServletTest.java | 166 --- .../jetty/servlet/ServletAccessLoggingTest.java | 64 - .../http/server/jetty/servlet/ServletTestBase.java | 132 -- .../http/ssl/impl/TlsContextBasedProviderTest.java | 71 - 172 files changed, 4 insertions(+), 20598 deletions(-) delete mode 100644 jdisc_http_service/.gitignore delete mode 100644 jdisc_http_service/CMakeLists.txt delete mode 100644 jdisc_http_service/OWNERS delete mode 100644 jdisc_http_service/README.md delete mode 100644 jdisc_http_service/abi-spec.json delete mode 100644 jdisc_http_service/pom.xml delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogEntry.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/Coverage.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/HitCounts.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONAccessLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFileHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFormatter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/LogWriter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogEntry.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/TraceRenderer.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/VespaAccessLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/container/logging/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactoryProvider.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/DefaultSslContextFactoryProvider.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/JDiscSslContextFactory.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/SslContextFactoryUtils.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProvider.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/package-info.java delete mode 100644 jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java delete mode 100644 jdisc_http_service/src/main/resources/configdefinitions/container.logging.connection-log.def delete mode 100644 jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def delete mode 100644 jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def delete mode 100644 jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def delete mode 100644 jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapperTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/FilterTestCase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactoryTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollectorTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryConnectionLog.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/InMemoryRequestLog.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServletTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/MetricConsumerMock.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SimpleHttpClient.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListenerTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDriver.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/TestDrivers.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/JDiscFilterForServletTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletAccessLoggingTest.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/servlet/ServletTestBase.java delete mode 100644 jdisc_http_service/src/test/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProviderTest.java diff --git a/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java b/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java index 295786aa15d..41b88e08c19 100644 --- a/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java +++ b/container-core/src/main/java/com/yahoo/container/logging/TraceRenderer.java @@ -1,3 +1,4 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.container.logging; import com.yahoo.data.access.Inspectable; diff --git a/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def b/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def index e845a8351d5..65b632c9008 100644 --- a/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def +++ b/container-core/src/main/resources/configdefinitions/container.logging.connection-log.def @@ -1,3 +1,4 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. namespace=container.logging # Name of the cluster diff --git a/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java b/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java index 15118b23f85..75bc0c915d3 100644 --- a/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java +++ b/container-core/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java @@ -1,4 +1,5 @@ -package com.yahoo.container.logging;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.logging; import com.yahoo.test.json.JsonTestHelper; import org.junit.jupiter.api.Test; diff --git a/jdisc_http_service/.gitignore b/jdisc_http_service/.gitignore deleted file mode 100644 index 3cc25b51fc4..00000000000 --- a/jdisc_http_service/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/pom.xml.build -/target diff --git a/jdisc_http_service/CMakeLists.txt b/jdisc_http_service/CMakeLists.txt deleted file mode 100644 index e9e9447bd03..00000000000 --- a/jdisc_http_service/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -install_fat_java_artifact(jdisc_http_service) -install_java_artifact_dependencies(jdisc_http_service) - -install_config_definitions() diff --git a/jdisc_http_service/OWNERS b/jdisc_http_service/OWNERS deleted file mode 100644 index 78b92e411b4..00000000000 --- a/jdisc_http_service/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -gjoranv -bjorncs diff --git a/jdisc_http_service/README.md b/jdisc_http_service/README.md deleted file mode 100644 index bb7882b9a2a..00000000000 --- a/jdisc_http_service/README.md +++ /dev/null @@ -1,4 +0,0 @@ - -# JDisc HTTP Service - -This package provides a ClientProvider and a ServerProvider implementation using HTTP on JDisc. Built on Jetty. diff --git a/jdisc_http_service/abi-spec.json b/jdisc_http_service/abi-spec.json deleted file mode 100644 index 34e021eec62..00000000000 --- a/jdisc_http_service/abi-spec.json +++ /dev/null @@ -1,1482 +0,0 @@ -{ - "com.yahoo.jdisc.http.CertificateStore": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public java.lang.String getCertificate(java.lang.String)", - "public java.lang.String getCertificate(java.lang.String, long)", - "public abstract java.lang.String getCertificate(java.lang.String, long, long)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Builder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder listenPort(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder name(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder headerCacheSize(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder outputBufferSize(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder requestHeaderSize(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder responseHeaderSize(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder acceptQueueSize(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder reuseAddress(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder idleTimeout(double)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder stopTimeout(double)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpKeepAliveEnabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpNoDelay(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder throttling(com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder implicitTlsEnabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder ssl(com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder tlsClientAuthEnforcer(com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder healthCheckProxy(com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder proxyProtocol(com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder secureRedirect(com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxRequestsPerConnection(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxConnectionLife(double)", - "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", - "public final java.lang.String getDefMd5()", - "public final java.lang.String getDefName()", - "public final java.lang.String getDefNamespace()", - "public final boolean getApplyOnRestart()", - "public final void setApplyOnRestart(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig build()" - ], - "fields": [ - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder throttling", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder ssl", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder tlsClientAuthEnforcer", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder healthCheckProxy", - "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder proxyProtocol", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder secureRedirect" - ] - }, - "com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy)", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder enable(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder port(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder clientTimeout(double)", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy$Builder)", - "public boolean enable()", - "public int port()", - "public double clientTimeout()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Producer": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Producer" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void getConfig(com.yahoo.jdisc.http.ConnectorConfig$Builder)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol)", - "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder mixedMode(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol$Builder)", - "public boolean enabled()", - "public boolean mixedMode()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder port(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect$Builder)", - "public boolean enabled()", - "public int port()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKeyFile(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder privateKey(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificateFile(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder certificate(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder caCertificateFile(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder caCertificate(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder clientAuth(com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledCipherSuites(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledCipherSuites(java.util.Collection)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledProtocols(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder enabledProtocols(java.util.Collection)", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl build()" - ], - "fields": [ - "public java.util.List enabledCipherSuites", - "public java.util.List enabledProtocols" - ] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum": { - "superClass": "java.lang.Enum", - "interfaces": [], - "attributes": [ - "public", - "final", - "enum" - ], - "methods": [ - "public static com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum[] values()", - "public static com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum valueOf(java.lang.String)" - ], - "fields": [ - "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum DISABLED", - "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum WANT_AUTH", - "public static final enum com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum NEED_AUTH" - ] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth": { - "superClass": "com.yahoo.config.EnumNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum)" - ], - "fields": [ - "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum DISABLED", - "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum WANT_AUTH", - "public static final com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum NEED_AUTH" - ] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Ssl": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$Ssl$Builder)", - "public boolean enabled()", - "public java.lang.String privateKeyFile()", - "public java.lang.String privateKey()", - "public java.lang.String certificateFile()", - "public java.lang.String certificate()", - "public java.lang.String caCertificateFile()", - "public java.lang.String caCertificate()", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl$ClientAuth$Enum clientAuth()", - "public java.util.List enabledCipherSuites()", - "public java.lang.String enabledCipherSuites(int)", - "public java.util.List enabledProtocols()", - "public java.lang.String enabledProtocols(int)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$Throttling)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxConnections(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxHeapUtilization(double)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder maxAcceptRate(int)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder idleTimeout(double)", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$Throttling": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder)", - "public boolean enabled()", - "public int maxConnections()", - "public double maxHeapUtilization()", - "public int maxAcceptRate()", - "public double idleTimeout()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer)", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder enable(boolean)", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder pathWhitelist(java.lang.String)", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder pathWhitelist(java.util.Collection)", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer build()" - ], - "fields": [ - "public java.util.List pathWhitelist" - ] - }, - "com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer$Builder)", - "public boolean enable()", - "public java.util.List pathWhitelist()", - "public java.lang.String pathWhitelist(int)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ConnectorConfig": { - "superClass": "com.yahoo.config.ConfigInstance", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public static java.lang.String getDefMd5()", - "public static java.lang.String getDefName()", - "public static java.lang.String getDefNamespace()", - "public static java.lang.String getDefVersion()", - "public void (com.yahoo.jdisc.http.ConnectorConfig$Builder)", - "public int listenPort()", - "public java.lang.String name()", - "public int headerCacheSize()", - "public int outputBufferSize()", - "public int requestHeaderSize()", - "public int responseHeaderSize()", - "public int acceptQueueSize()", - "public boolean reuseAddress()", - "public double idleTimeout()", - "public double stopTimeout()", - "public boolean tcpKeepAliveEnabled()", - "public boolean tcpNoDelay()", - "public com.yahoo.jdisc.http.ConnectorConfig$Throttling throttling()", - "public boolean implicitTlsEnabled()", - "public com.yahoo.jdisc.http.ConnectorConfig$Ssl ssl()", - "public com.yahoo.jdisc.http.ConnectorConfig$TlsClientAuthEnforcer tlsClientAuthEnforcer()", - "public com.yahoo.jdisc.http.ConnectorConfig$HealthCheckProxy healthCheckProxy()", - "public com.yahoo.jdisc.http.ConnectorConfig$ProxyProtocol proxyProtocol()", - "public com.yahoo.jdisc.http.ConnectorConfig$SecureRedirect secureRedirect()", - "public int maxRequestsPerConnection()", - "public double maxConnectionLife()" - ], - "fields": [ - "public static final java.lang.String CONFIG_DEF_MD5", - "public static final java.lang.String CONFIG_DEF_NAME", - "public static final java.lang.String CONFIG_DEF_NAMESPACE", - "public static final java.lang.String CONFIG_DEF_VERSION", - "public static final java.lang.String[] CONFIG_DEF_SCHEMA" - ] - }, - "com.yahoo.jdisc.http.Cookie$SameSite": { - "superClass": "java.lang.Enum", - "interfaces": [], - "attributes": [ - "public", - "final", - "enum" - ], - "methods": [ - "public static com.yahoo.jdisc.http.Cookie$SameSite[] values()", - "public static com.yahoo.jdisc.http.Cookie$SameSite valueOf(java.lang.String)" - ], - "fields": [ - "public static final enum com.yahoo.jdisc.http.Cookie$SameSite NONE", - "public static final enum com.yahoo.jdisc.http.Cookie$SameSite STRICT", - "public static final enum com.yahoo.jdisc.http.Cookie$SameSite LAX" - ] - }, - "com.yahoo.jdisc.http.Cookie": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.Cookie)", - "public void (java.lang.String, java.lang.String)", - "public java.lang.String getName()", - "public com.yahoo.jdisc.http.Cookie setName(java.lang.String)", - "public java.lang.String getValue()", - "public com.yahoo.jdisc.http.Cookie setValue(java.lang.String)", - "public java.lang.String getDomain()", - "public com.yahoo.jdisc.http.Cookie setDomain(java.lang.String)", - "public java.lang.String getPath()", - "public com.yahoo.jdisc.http.Cookie setPath(java.lang.String)", - "public com.yahoo.jdisc.http.Cookie$SameSite getSameSite()", - "public com.yahoo.jdisc.http.Cookie setSameSite(com.yahoo.jdisc.http.Cookie$SameSite)", - "public int getMaxAge(java.util.concurrent.TimeUnit)", - "public com.yahoo.jdisc.http.Cookie setMaxAge(int, java.util.concurrent.TimeUnit)", - "public boolean isSecure()", - "public com.yahoo.jdisc.http.Cookie setSecure(boolean)", - "public boolean isHttpOnly()", - "public com.yahoo.jdisc.http.Cookie setHttpOnly(boolean)", - "public boolean equals(java.lang.Object)", - "public int hashCode()", - "public java.lang.String toString()", - "public static java.lang.String toCookieHeader(java.lang.Iterable)", - "public static java.util.List fromCookieHeader(java.lang.String)", - "public static java.util.List toSetCookieHeaders(java.lang.Iterable)", - "public static java.util.List toSetCookieHeaderAll(java.lang.Iterable)", - "public static com.yahoo.jdisc.http.Cookie fromSetCookieHeader(java.lang.String)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.HttpHeaders$Names": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [], - "fields": [ - "public static final java.lang.String ACCEPT", - "public static final java.lang.String ACCEPT_CHARSET", - "public static final java.lang.String ACCEPT_ENCODING", - "public static final java.lang.String ACCEPT_LANGUAGE", - "public static final java.lang.String ACCEPT_RANGES", - "public static final java.lang.String ACCEPT_PATCH", - "public static final java.lang.String AGE", - "public static final java.lang.String ALLOW", - "public static final java.lang.String AUTHORIZATION", - "public static final java.lang.String CACHE_CONTROL", - "public static final java.lang.String CONNECTION", - "public static final java.lang.String CONTENT_BASE", - "public static final java.lang.String CONTENT_ENCODING", - "public static final java.lang.String CONTENT_LANGUAGE", - "public static final java.lang.String CONTENT_LENGTH", - "public static final java.lang.String CONTENT_LOCATION", - "public static final java.lang.String CONTENT_TRANSFER_ENCODING", - "public static final java.lang.String CONTENT_MD5", - "public static final java.lang.String CONTENT_RANGE", - "public static final java.lang.String CONTENT_TYPE", - "public static final java.lang.String COOKIE", - "public static final java.lang.String DATE", - "public static final java.lang.String ETAG", - "public static final java.lang.String EXPECT", - "public static final java.lang.String EXPIRES", - "public static final java.lang.String FROM", - "public static final java.lang.String HOST", - "public static final java.lang.String IF_MATCH", - "public static final java.lang.String IF_MODIFIED_SINCE", - "public static final java.lang.String IF_NONE_MATCH", - "public static final java.lang.String IF_RANGE", - "public static final java.lang.String IF_UNMODIFIED_SINCE", - "public static final java.lang.String LAST_MODIFIED", - "public static final java.lang.String LOCATION", - "public static final java.lang.String MAX_FORWARDS", - "public static final java.lang.String ORIGIN", - "public static final java.lang.String PRAGMA", - "public static final java.lang.String PROXY_AUTHENTICATE", - "public static final java.lang.String PROXY_AUTHORIZATION", - "public static final java.lang.String RANGE", - "public static final java.lang.String REFERER", - "public static final java.lang.String RETRY_AFTER", - "public static final java.lang.String SEC_WEBSOCKET_KEY1", - "public static final java.lang.String SEC_WEBSOCKET_KEY2", - "public static final java.lang.String SEC_WEBSOCKET_LOCATION", - "public static final java.lang.String SEC_WEBSOCKET_ORIGIN", - "public static final java.lang.String SEC_WEBSOCKET_PROTOCOL", - "public static final java.lang.String SEC_WEBSOCKET_VERSION", - "public static final java.lang.String SEC_WEBSOCKET_KEY", - "public static final java.lang.String SEC_WEBSOCKET_ACCEPT", - "public static final java.lang.String SERVER", - "public static final java.lang.String SET_COOKIE", - "public static final java.lang.String SET_COOKIE2", - "public static final java.lang.String TE", - "public static final java.lang.String TRAILER", - "public static final java.lang.String TRANSFER_ENCODING", - "public static final java.lang.String UPGRADE", - "public static final java.lang.String USER_AGENT", - "public static final java.lang.String VARY", - "public static final java.lang.String VIA", - "public static final java.lang.String WARNING", - "public static final java.lang.String WEBSOCKET_LOCATION", - "public static final java.lang.String WEBSOCKET_ORIGIN", - "public static final java.lang.String WEBSOCKET_PROTOCOL", - "public static final java.lang.String WWW_AUTHENTICATE", - "public static final java.lang.String X_DISABLE_CHUNKING", - "public static final java.lang.String X_YAHOO_SERVING_HOST" - ] - }, - "com.yahoo.jdisc.http.HttpHeaders$Values": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [], - "fields": [ - "public static final java.lang.String APPLICATION_X_WWW_FORM_URLENCODED", - "public static final java.lang.String BASE64", - "public static final java.lang.String BINARY", - "public static final java.lang.String BYTES", - "public static final java.lang.String CHARSET", - "public static final java.lang.String CHUNKED", - "public static final java.lang.String CLOSE", - "public static final java.lang.String COMPRESS", - "public static final java.lang.String CONTINUE", - "public static final java.lang.String DEFLATE", - "public static final java.lang.String GZIP", - "public static final java.lang.String IDENTITY", - "public static final java.lang.String KEEP_ALIVE", - "public static final java.lang.String MAX_AGE", - "public static final java.lang.String MAX_STALE", - "public static final java.lang.String MIN_FRESH", - "public static final java.lang.String MUST_REVALIDATE", - "public static final java.lang.String NO_CACHE", - "public static final java.lang.String NO_STORE", - "public static final java.lang.String NO_TRANSFORM", - "public static final java.lang.String NONE", - "public static final java.lang.String ONLY_IF_CACHED", - "public static final java.lang.String PRIVATE", - "public static final java.lang.String PROXY_REVALIDATE", - "public static final java.lang.String PUBLIC", - "public static final java.lang.String QUOTED_PRINTABLE", - "public static final java.lang.String S_MAXAGE", - "public static final java.lang.String TRAILERS", - "public static final java.lang.String UPGRADE", - "public static final java.lang.String WEBSOCKET" - ] - }, - "com.yahoo.jdisc.http.HttpHeaders": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public" - ], - "methods": [ - "public void ()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.HttpRequest$Method": { - "superClass": "java.lang.Enum", - "interfaces": [], - "attributes": [ - "public", - "final", - "enum" - ], - "methods": [ - "public static com.yahoo.jdisc.http.HttpRequest$Method[] values()", - "public static com.yahoo.jdisc.http.HttpRequest$Method valueOf(java.lang.String)" - ], - "fields": [ - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method OPTIONS", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method GET", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method HEAD", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method POST", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method PUT", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method PATCH", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method DELETE", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method TRACE", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Method CONNECT" - ] - }, - "com.yahoo.jdisc.http.HttpRequest$Version": { - "superClass": "java.lang.Enum", - "interfaces": [], - "attributes": [ - "public", - "final", - "enum" - ], - "methods": [ - "public static com.yahoo.jdisc.http.HttpRequest$Version[] values()", - "public static com.yahoo.jdisc.http.HttpRequest$Version valueOf(java.lang.String)", - "public java.lang.String toString()", - "public static com.yahoo.jdisc.http.HttpRequest$Version fromString(java.lang.String)" - ], - "fields": [ - "public static final enum com.yahoo.jdisc.http.HttpRequest$Version HTTP_1_0", - "public static final enum com.yahoo.jdisc.http.HttpRequest$Version HTTP_1_1" - ] - }, - "com.yahoo.jdisc.http.HttpRequest": { - "superClass": "com.yahoo.jdisc.Request", - "interfaces": [ - "com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest" - ], - "attributes": [ - "public" - ], - "methods": [ - "protected void (com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress, java.lang.Long)", - "public com.yahoo.jdisc.http.HttpRequest$Method getMethod()", - "public void setMethod(com.yahoo.jdisc.http.HttpRequest$Method)", - "public com.yahoo.jdisc.http.HttpRequest$Version getVersion()", - "public java.lang.String getRemoteHostAddress()", - "public java.lang.String getRemoteHostName()", - "public int getRemotePort()", - "public void setVersion(com.yahoo.jdisc.http.HttpRequest$Version)", - "public java.net.SocketAddress getRemoteAddress()", - "public void setRemoteAddress(java.net.SocketAddress)", - "public java.net.URI getProxyServer()", - "public void setProxyServer(java.net.URI)", - "public long getConnectedAt(java.util.concurrent.TimeUnit)", - "public java.lang.Long getConnectionTimeout(java.util.concurrent.TimeUnit)", - "public void setConnectionTimeout(long, java.util.concurrent.TimeUnit)", - "public java.util.Map parameters()", - "public void copyHeaders(com.yahoo.jdisc.HeaderFields)", - "public java.util.List decodeCookieHeader()", - "public void encodeCookieHeader(java.util.List)", - "public com.yahoo.jdisc.HeaderFields trailers()", - "public boolean isChunked()", - "public boolean hasChunkedResponse()", - "public boolean isKeepAlive()", - "public java.security.Principal getUserPrincipal()", - "public void setUserPrincipal(java.security.Principal)", - "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI)", - "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method)", - "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version)", - "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress)", - "public static com.yahoo.jdisc.http.HttpRequest newServerRequest(com.yahoo.jdisc.service.CurrentContainer, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version, java.net.SocketAddress, long)", - "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI)", - "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method)", - "public static com.yahoo.jdisc.http.HttpRequest newClientRequest(com.yahoo.jdisc.Request, java.net.URI, com.yahoo.jdisc.http.HttpRequest$Method, com.yahoo.jdisc.http.HttpRequest$Version)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.HttpResponse$Status": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.Response$Status" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [], - "fields": [ - "public static final int REQUEST_ENTITY_TOO_LARGE", - "public static final int REQUEST_RANGE_NOT_SATISFIABLE" - ] - }, - "com.yahoo.jdisc.http.HttpResponse": { - "superClass": "com.yahoo.jdisc.Response", - "interfaces": [ - "com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse" - ], - "attributes": [ - "public" - ], - "methods": [ - "protected void (com.yahoo.jdisc.Request, int, java.lang.String, java.lang.Throwable)", - "public boolean isChunkedEncodingEnabled()", - "public void setChunkedEncodingEnabled(boolean)", - "public void setMessage(java.lang.String)", - "public java.lang.String getMessage()", - "public void copyHeaders(com.yahoo.jdisc.HeaderFields)", - "public java.util.List decodeSetCookieHeader()", - "public void encodeSetCookieHeader(java.util.List)", - "public com.yahoo.jdisc.HeaderFields trailers()", - "public static boolean isServerError(com.yahoo.jdisc.Response)", - "public static com.yahoo.jdisc.http.HttpResponse newInstance(int)", - "public static com.yahoo.jdisc.http.HttpResponse newInstance(int, java.lang.String)", - "public static com.yahoo.jdisc.http.HttpResponse newError(com.yahoo.jdisc.Request, int, java.lang.Throwable)", - "public static com.yahoo.jdisc.http.HttpResponse newInternalServerError(com.yahoo.jdisc.Request, java.lang.Throwable)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.SecretStore": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract java.lang.String getSecret(java.lang.String)", - "public java.lang.String getSecret(java.lang.String, int)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$AccessLog)", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remoteAddressHeaders(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remoteAddressHeaders(java.util.Collection)", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remotePortHeaders(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder remotePortHeaders(java.util.Collection)", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog build()" - ], - "fields": [ - "public java.util.List remoteAddressHeaders", - "public java.util.List remotePortHeaders" - ] - }, - "com.yahoo.jdisc.http.ServerConfig$AccessLog": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder)", - "public java.util.List remoteAddressHeaders()", - "public java.lang.String remoteAddressHeaders(int)", - "public java.util.List remotePortHeaders()", - "public java.lang.String remotePortHeaders(int)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Builder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig)", - "public com.yahoo.jdisc.http.ServerConfig$Builder developerMode(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$Builder responseCompressionLevel(int)", - "public com.yahoo.jdisc.http.ServerConfig$Builder httpKeepAliveEnabled(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$Builder maxKeepAliveRequests(int)", - "public com.yahoo.jdisc.http.ServerConfig$Builder removeRawPostBodyForWwwUrlEncodedPost(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$Builder filter(com.yahoo.jdisc.http.ServerConfig$Filter$Builder)", - "public com.yahoo.jdisc.http.ServerConfig$Builder filter(java.util.List)", - "public com.yahoo.jdisc.http.ServerConfig$Builder defaultFilters(com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder)", - "public com.yahoo.jdisc.http.ServerConfig$Builder defaultFilters(java.util.List)", - "public com.yahoo.jdisc.http.ServerConfig$Builder strictFiltering(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$Builder maxWorkerThreads(int)", - "public com.yahoo.jdisc.http.ServerConfig$Builder minWorkerThreads(int)", - "public com.yahoo.jdisc.http.ServerConfig$Builder stopTimeout(double)", - "public com.yahoo.jdisc.http.ServerConfig$Builder jmx(com.yahoo.jdisc.http.ServerConfig$Jmx$Builder)", - "public com.yahoo.jdisc.http.ServerConfig$Builder metric(com.yahoo.jdisc.http.ServerConfig$Metric$Builder)", - "public com.yahoo.jdisc.http.ServerConfig$Builder accessLog(com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder)", - "public com.yahoo.jdisc.http.ServerConfig$Builder connectionLog(com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder)", - "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", - "public final java.lang.String getDefMd5()", - "public final java.lang.String getDefName()", - "public final java.lang.String getDefNamespace()", - "public final boolean getApplyOnRestart()", - "public final void setApplyOnRestart(boolean)", - "public com.yahoo.jdisc.http.ServerConfig build()" - ], - "fields": [ - "public java.util.List filter", - "public java.util.List defaultFilters", - "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder jmx", - "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder metric", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog$Builder accessLog", - "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder connectionLog" - ] - }, - "com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$ConnectionLog)", - "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$ConnectionLog": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$ConnectionLog$Builder)", - "public boolean enabled()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$DefaultFilters)", - "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder filterId(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder localPort(int)", - "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$DefaultFilters": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$DefaultFilters$Builder)", - "public java.lang.String filterId()", - "public int localPort()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Filter$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$Filter)", - "public com.yahoo.jdisc.http.ServerConfig$Filter$Builder id(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$Filter$Builder binding(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$Filter build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Filter": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$Filter$Builder)", - "public java.lang.String id()", - "public java.lang.String binding()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Jmx$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$Jmx)", - "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder enabled(boolean)", - "public com.yahoo.jdisc.http.ServerConfig$Jmx$Builder listenPort(int)", - "public com.yahoo.jdisc.http.ServerConfig$Jmx build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Jmx": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$Jmx$Builder)", - "public boolean enabled()", - "public int listenPort()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Metric$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServerConfig$Metric)", - "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder monitoringHandlerPaths(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder monitoringHandlerPaths(java.util.Collection)", - "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder searchHandlerPaths(java.lang.String)", - "public com.yahoo.jdisc.http.ServerConfig$Metric$Builder searchHandlerPaths(java.util.Collection)", - "public com.yahoo.jdisc.http.ServerConfig$Metric build()" - ], - "fields": [ - "public java.util.List monitoringHandlerPaths", - "public java.util.List searchHandlerPaths" - ] - }, - "com.yahoo.jdisc.http.ServerConfig$Metric": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServerConfig$Metric$Builder)", - "public java.util.List monitoringHandlerPaths()", - "public java.lang.String monitoringHandlerPaths(int)", - "public java.util.List searchHandlerPaths()", - "public java.lang.String searchHandlerPaths(int)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig$Producer": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Producer" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void getConfig(com.yahoo.jdisc.http.ServerConfig$Builder)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServerConfig": { - "superClass": "com.yahoo.config.ConfigInstance", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public static java.lang.String getDefMd5()", - "public static java.lang.String getDefName()", - "public static java.lang.String getDefNamespace()", - "public static java.lang.String getDefVersion()", - "public void (com.yahoo.jdisc.http.ServerConfig$Builder)", - "public boolean developerMode()", - "public int responseCompressionLevel()", - "public boolean httpKeepAliveEnabled()", - "public int maxKeepAliveRequests()", - "public boolean removeRawPostBodyForWwwUrlEncodedPost()", - "public java.util.List filter()", - "public com.yahoo.jdisc.http.ServerConfig$Filter filter(int)", - "public java.util.List defaultFilters()", - "public com.yahoo.jdisc.http.ServerConfig$DefaultFilters defaultFilters(int)", - "public boolean strictFiltering()", - "public int maxWorkerThreads()", - "public int minWorkerThreads()", - "public double stopTimeout()", - "public com.yahoo.jdisc.http.ServerConfig$Jmx jmx()", - "public com.yahoo.jdisc.http.ServerConfig$Metric metric()", - "public com.yahoo.jdisc.http.ServerConfig$AccessLog accessLog()", - "public com.yahoo.jdisc.http.ServerConfig$ConnectionLog connectionLog()" - ], - "fields": [ - "public static final java.lang.String CONFIG_DEF_MD5", - "public static final java.lang.String CONFIG_DEF_NAME", - "public static final java.lang.String CONFIG_DEF_NAMESPACE", - "public static final java.lang.String CONFIG_DEF_VERSION", - "public static final java.lang.String[] CONFIG_DEF_SCHEMA" - ] - }, - "com.yahoo.jdisc.http.ServletPathsConfig$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Builder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServletPathsConfig)", - "public com.yahoo.jdisc.http.ServletPathsConfig$Builder servlets(java.lang.String, com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder)", - "public com.yahoo.jdisc.http.ServletPathsConfig$Builder servlets(java.util.Map)", - "public final boolean dispatchGetConfig(com.yahoo.config.ConfigInstance$Producer)", - "public final java.lang.String getDefMd5()", - "public final java.lang.String getDefName()", - "public final java.lang.String getDefNamespace()", - "public final boolean getApplyOnRestart()", - "public final void setApplyOnRestart(boolean)", - "public com.yahoo.jdisc.http.ServletPathsConfig build()" - ], - "fields": [ - "public java.util.Map servlets" - ] - }, - "com.yahoo.jdisc.http.ServletPathsConfig$Producer": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigInstance$Producer" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void getConfig(com.yahoo.jdisc.http.ServletPathsConfig$Builder)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.config.ConfigBuilder" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public void (com.yahoo.jdisc.http.ServletPathsConfig$Servlets)", - "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder path(java.lang.String)", - "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets build()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServletPathsConfig$Servlets": { - "superClass": "com.yahoo.config.InnerNode", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.ServletPathsConfig$Servlets$Builder)", - "public java.lang.String path()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ServletPathsConfig": { - "superClass": "com.yahoo.config.ConfigInstance", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public static java.lang.String getDefMd5()", - "public static java.lang.String getDefName()", - "public static java.lang.String getDefNamespace()", - "public static java.lang.String getDefVersion()", - "public void (com.yahoo.jdisc.http.ServletPathsConfig$Builder)", - "public java.util.Map servlets()", - "public com.yahoo.jdisc.http.ServletPathsConfig$Servlets servlets(java.lang.String)" - ], - "fields": [ - "public static final java.lang.String CONFIG_DEF_MD5", - "public static final java.lang.String CONFIG_DEF_NAME", - "public static final java.lang.String CONFIG_DEF_NAMESPACE", - "public static final java.lang.String CONFIG_DEF_VERSION", - "public static final java.lang.String[] CONFIG_DEF_SCHEMA" - ] - }, - "com.yahoo.jdisc.http.filter.DiscFilterRequest$ThreadLocalSimpleDateFormat": { - "superClass": "java.lang.ThreadLocal", - "interfaces": [], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void (java.lang.String, java.util.Locale)", - "public java.util.Date parse(java.lang.String)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.DiscFilterRequest": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "abstract" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest)", - "public abstract java.lang.String getMethod()", - "public com.yahoo.jdisc.http.HttpRequest$Version getVersion()", - "public java.net.URI getUri()", - "public abstract void setUri(java.net.URI)", - "public com.yahoo.jdisc.http.HttpRequest getParentRequest()", - "public java.lang.String getRemoteAddr()", - "public void setRemoteAddr(java.lang.String)", - "public java.lang.String getLocalAddr()", - "public java.util.Enumeration getAttributeNames()", - "public java.lang.Object getAttribute(java.lang.String)", - "public void setAttribute(java.lang.String, java.lang.Object)", - "public boolean containsAttribute(java.lang.String)", - "public void removeAttribute(java.lang.String)", - "public abstract java.lang.String getParameter(java.lang.String)", - "public abstract java.util.Enumeration getParameterNames()", - "public java.util.List getParameterNamesAsList()", - "public java.util.Enumeration getParameterValues(java.lang.String)", - "public java.util.List getParameterValuesAsList(java.lang.String)", - "public java.util.Map getParameterMap()", - "public java.lang.String getRemoteHost()", - "public int getLocalPort()", - "public int getRemotePort()", - "public java.util.Map getUntreatedParams()", - "public com.yahoo.jdisc.HeaderFields getUntreatedHeaders()", - "public java.util.List getUntreatedCookies()", - "public abstract void addHeader(java.lang.String, java.lang.String)", - "public long getDateHeader(java.lang.String)", - "public abstract java.lang.String getHeader(java.lang.String)", - "public abstract java.util.Enumeration getHeaderNames()", - "public abstract java.util.List getHeaderNamesAsList()", - "public abstract java.util.Enumeration getHeaders(java.lang.String)", - "public abstract java.util.List getHeadersAsList(java.lang.String)", - "public abstract void removeHeaders(java.lang.String)", - "public abstract void setHeaders(java.lang.String, java.lang.String)", - "public abstract void setHeaders(java.lang.String, java.util.List)", - "public int getIntHeader(java.lang.String)", - "public java.util.List getCookies()", - "public void setCookies(java.util.List)", - "public long getConnectedAt(java.util.concurrent.TimeUnit)", - "public java.lang.String getProtocol()", - "public java.lang.String getQueryString()", - "public java.lang.String getRemoteUser()", - "public java.lang.String getRequestURI()", - "public java.lang.String getRequestedSessionId()", - "public java.lang.String getScheme()", - "public void setScheme(java.lang.String, boolean)", - "public java.lang.String getServerName()", - "public int getServerPort()", - "public abstract java.security.Principal getUserPrincipal()", - "public boolean isSecure()", - "public boolean isUserInRole(java.lang.String)", - "public void setOverrideIsUserInRole(boolean)", - "public void setRemoteHost(java.lang.String)", - "public void setRemoteUser(java.lang.String)", - "public abstract void setUserPrincipal(java.security.Principal)", - "public abstract java.util.List getClientCertificateChain()", - "public void setUserRoles(java.lang.String[])", - "public java.lang.String getContentType()", - "public java.lang.String getCharacterEncoding()", - "public void setCharacterEncoding(java.lang.String)", - "public void addCookie(com.yahoo.jdisc.http.filter.JDiscCookieWrapper)", - "public abstract void clearCookies()", - "public com.yahoo.jdisc.http.filter.JDiscCookieWrapper[] getWrappedCookies()", - "public static boolean isMultipart(com.yahoo.jdisc.http.filter.DiscFilterRequest)" - ], - "fields": [ - "protected static final java.lang.String HTTPS_PREFIX", - "protected static final int DEFAULT_HTTP_PORT", - "protected static final int DEFAULT_HTTPS_PORT", - "protected final java.util.Map untreatedParams", - "protected static com.yahoo.jdisc.http.filter.DiscFilterRequest$ThreadLocalSimpleDateFormat[] formats" - ] - }, - "com.yahoo.jdisc.http.filter.DiscFilterResponse": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "abstract" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpResponse)", - "public java.util.Enumeration getAttributeNames()", - "public java.lang.Object getAttribute(java.lang.String)", - "public void setAttribute(java.lang.String, java.lang.Object)", - "public void removeAttribute(java.lang.String)", - "public com.yahoo.jdisc.HeaderFields getUntreatedHeaders()", - "public java.util.List getUntreatedCookies()", - "public abstract void setHeader(java.lang.String, java.lang.String)", - "public abstract void removeHeaders(java.lang.String)", - "public abstract void setHeaders(java.lang.String, java.lang.String)", - "public abstract void setHeaders(java.lang.String, java.util.List)", - "public abstract void addHeader(java.lang.String, java.lang.String)", - "public abstract java.lang.String getHeader(java.lang.String)", - "public java.util.List getCookies()", - "public abstract void setCookies(java.util.List)", - "public int getStatus()", - "public abstract void setStatus(int)", - "public com.yahoo.jdisc.http.HttpResponse getParentResponse()", - "public void addCookie(com.yahoo.jdisc.http.filter.JDiscCookieWrapper)", - "public void sendError(int)", - "public void setCookie(java.lang.String, java.lang.String)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.FilterConfig": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract java.lang.String getFilterName()", - "public abstract java.lang.String getFilterClass()", - "public abstract java.lang.String getInitParameter(java.lang.String)", - "public abstract boolean getBooleanInitParameter(java.lang.String, boolean)", - "public abstract java.util.Collection getInitParameterNames()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.JDiscCookieWrapper": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public" - ], - "methods": [ - "protected void (com.yahoo.jdisc.http.Cookie)", - "public static com.yahoo.jdisc.http.filter.JDiscCookieWrapper wrap(com.yahoo.jdisc.http.Cookie)", - "public java.lang.String getDomain()", - "public int getMaxAge()", - "public java.lang.String getName()", - "public java.lang.String getPath()", - "public boolean getSecure()", - "public java.lang.String getValue()", - "public void setDomain(java.lang.String)", - "public void setMaxAge(int)", - "public void setPath(java.lang.String)", - "public void setSecure(boolean)", - "public void setValue(java.lang.String)", - "public com.yahoo.jdisc.http.Cookie getCookie()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.JdiscFilterRequest": { - "superClass": "com.yahoo.jdisc.http.filter.DiscFilterRequest", - "interfaces": [], - "attributes": [ - "public" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.HttpRequest)", - "public com.yahoo.jdisc.http.HttpRequest getParentRequest()", - "public void setUri(java.net.URI)", - "public java.lang.String getMethod()", - "public java.lang.String getParameter(java.lang.String)", - "public java.util.Enumeration getParameterNames()", - "public void addHeader(java.lang.String, java.lang.String)", - "public java.lang.String getHeader(java.lang.String)", - "public java.util.Enumeration getHeaderNames()", - "public java.util.List getHeaderNamesAsList()", - "public java.util.Enumeration getHeaders(java.lang.String)", - "public java.util.List getHeadersAsList(java.lang.String)", - "public void removeHeaders(java.lang.String)", - "public void setHeaders(java.lang.String, java.lang.String)", - "public void setHeaders(java.lang.String, java.util.List)", - "public java.security.Principal getUserPrincipal()", - "public void setUserPrincipal(java.security.Principal)", - "public java.util.List getClientCertificateChain()", - "public void clearCookies()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.JdiscFilterResponse": { - "superClass": "com.yahoo.jdisc.http.filter.DiscFilterResponse", - "interfaces": [], - "attributes": [ - "public" - ], - "methods": [ - "public void (com.yahoo.jdisc.http.HttpResponse)", - "public void setStatus(int)", - "public void setHeader(java.lang.String, java.lang.String)", - "public void removeHeaders(java.lang.String)", - "public void setHeaders(java.lang.String, java.lang.String)", - "public void setHeaders(java.lang.String, java.util.List)", - "public void addHeader(java.lang.String, java.lang.String)", - "public java.lang.String getHeader(java.lang.String)", - "public void setCookies(java.util.List)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.RequestFilter": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.SharedResource", - "com.yahoo.jdisc.http.filter.RequestFilterBase" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void filter(com.yahoo.jdisc.http.HttpRequest, com.yahoo.jdisc.handler.ResponseHandler)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.RequestFilterBase": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.RequestView": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract java.lang.Object getAttribute(java.lang.String)", - "public abstract java.util.List getHeaders(java.lang.String)", - "public abstract java.util.Optional getFirstHeader(java.lang.String)", - "public abstract java.util.Optional getMethod()", - "public abstract java.net.URI getUri()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.ResponseFilter": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.SharedResource", - "com.yahoo.jdisc.http.filter.ResponseFilterBase" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void filter(com.yahoo.jdisc.Response, com.yahoo.jdisc.Request)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.ResponseFilterBase": { - "superClass": "java.lang.Object", - "interfaces": [], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.SecurityFilterInvoker": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.http.server.jetty.FilterInvoker" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void ()", - "public javax.servlet.http.HttpServletRequest invokeRequestFilterChain(com.yahoo.jdisc.http.filter.RequestFilter, java.net.URI, javax.servlet.http.HttpServletRequest, com.yahoo.jdisc.handler.ResponseHandler)", - "public void invokeResponseFilterChain(com.yahoo.jdisc.http.filter.ResponseFilter, java.net.URI, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.SecurityRequestFilter": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.http.filter.RequestFilterBase" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void filter(com.yahoo.jdisc.http.filter.DiscFilterRequest, com.yahoo.jdisc.handler.ResponseHandler)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.SecurityRequestFilterChain": { - "superClass": "com.yahoo.jdisc.AbstractResource", - "interfaces": [ - "com.yahoo.jdisc.http.filter.RequestFilter" - ], - "attributes": [ - "public", - "final" - ], - "methods": [ - "public void filter(com.yahoo.jdisc.http.HttpRequest, com.yahoo.jdisc.handler.ResponseHandler)", - "public void filter(com.yahoo.jdisc.http.filter.DiscFilterRequest, com.yahoo.jdisc.handler.ResponseHandler)", - "public static varargs com.yahoo.jdisc.http.filter.RequestFilter newInstance(com.yahoo.jdisc.http.filter.SecurityRequestFilter[])", - "public static com.yahoo.jdisc.http.filter.RequestFilter newInstance(java.util.List)", - "public java.util.List getFilters()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.SecurityResponseFilter": { - "superClass": "java.lang.Object", - "interfaces": [ - "com.yahoo.jdisc.http.filter.ResponseFilterBase" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract void filter(com.yahoo.jdisc.http.filter.DiscFilterResponse, com.yahoo.jdisc.http.filter.RequestView)" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.filter.SecurityResponseFilterChain": { - "superClass": "com.yahoo.jdisc.AbstractResource", - "interfaces": [ - "com.yahoo.jdisc.http.filter.ResponseFilter" - ], - "attributes": [ - "public" - ], - "methods": [ - "public void filter(com.yahoo.jdisc.Response, com.yahoo.jdisc.Request)", - "public void filter(com.yahoo.jdisc.http.filter.RequestView, com.yahoo.jdisc.http.filter.DiscFilterResponse)", - "public static varargs com.yahoo.jdisc.http.filter.ResponseFilter newInstance(com.yahoo.jdisc.http.filter.SecurityResponseFilter[])", - "public static com.yahoo.jdisc.http.filter.ResponseFilter newInstance(java.util.List)", - "public java.util.List getFilters()" - ], - "fields": [] - }, - "com.yahoo.jdisc.http.ssl.SslContextFactoryProvider": { - "superClass": "java.lang.Object", - "interfaces": [ - "java.lang.AutoCloseable" - ], - "attributes": [ - "public", - "interface", - "abstract" - ], - "methods": [ - "public abstract org.eclipse.jetty.util.ssl.SslContextFactory getInstance(java.lang.String, int)", - "public void close()" - ], - "fields": [] - } -} \ No newline at end of file diff --git a/jdisc_http_service/pom.xml b/jdisc_http_service/pom.xml deleted file mode 100644 index 57d69a2f2dd..00000000000 --- a/jdisc_http_service/pom.xml +++ /dev/null @@ -1,222 +0,0 @@ - - - - 4.0.0 - - com.yahoo.vespa - parent - 7-SNAPSHOT - ../parent/pom.xml - - jdisc_http_service - 7-SNAPSHOT - container-plugin - ${project.artifactId} - - - - org.apache.httpcomponents - httpclient - compile - - - - - com.google.inject - guice - provided - no_aop - - - com.yahoo.vespa - jdisc_jetty - ${project.version} - provided - - - com.yahoo.vespa - config-lib - ${project.version} - provided - - - com.yahoo.vespa - defaults - ${project.version} - provided - - - com.yahoo.vespa - jdisc_core - ${project.version} - provided - - - com.yahoo.vespa - annotations - ${project.version} - provided - - - com.yahoo.vespa - component - ${project.version} - provided - - - com.yahoo.vespa - security-utils - ${project.version} - provided - - - com.yahoo.vespa - yolean - ${project.version} - provided - - - com.yahoo.vespa - vespalog - ${project.version} - provided - - - com.yahoo.vespa - container-core-config - ${project.version} - provided - - - com.yahoo.vespa - vespajlib - ${project.version} - provided - - - - - - org.apache.httpcomponents - httpmime - test - - - org.eclipse.jetty - jetty-client - ${jetty.version} - test - - - org.cthul - cthul-matchers - test - - - org.mockito - mockito-core - test - - - junit - junit - test - - - org.hamcrest - hamcrest-library - test - - - org.springframework - spring-test - test - - - org.bouncycastle - bcpkix-jdk15on - test - - - org.assertj - assertj-core - test - - - com.yahoo.vespa - testutil - ${project.version} - test - - - org.junit.jupiter - junit-jupiter - test - - - org.junit.vintage - junit-vintage-engine - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - -Xlint:all - -Xlint:-serial - -Werror - - - - - com.yahoo.vespa - bundle-plugin - true - - true - - javax.servlet-api-3.1.0.jar, - jetty-continuation-${jetty.version}.jar, - jetty-http-${jetty.version}.jar, - jetty-io-${jetty.version}.jar, - jetty-jmx-${jetty.version}.jar, - jetty-security-${jetty.version}.jar, - jetty-server-${jetty.version}.jar, - jetty-servlet-${jetty.version}.jar, - jetty-servlets-${jetty.version}.jar, - jetty-util-${jetty.version}.jar, - jetty-util-ajax-${jetty.version}.jar, - component-jar-with-dependencies.jar - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - package - - copy-dependencies - - - org.bouncycastle - - - - - - com.yahoo.vespa - abi-check-plugin - - - - diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLog.java deleted file mode 100644 index 2d46c53bca7..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLog.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - - -import com.google.inject.Inject; -import com.yahoo.component.provider.ComponentRegistry; - -/** - * Logs to all the configured access logs. - * - * @author Tony Vaagenes - * @author bjorncs - */ -public class AccessLog implements RequestLog { - - public static final AccessLog NONE_INSTANCE = new AccessLog(new ComponentRegistry<>()); - - private final ComponentRegistry implementers; - - @Inject - public AccessLog(ComponentRegistry implementers) { - this.implementers = implementers; - } - - public static AccessLog voidAccessLog() { - return NONE_INSTANCE; - } - - @Override - public void log(RequestLogEntry entry) { - for (RequestLogHandler handler: implementers.allComponents()) { - handler.log(entry); - } - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogEntry.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogEntry.java deleted file mode 100644 index 42285fb85bb..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogEntry.java +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.collections.ListMap; -import com.yahoo.yolean.trace.TraceNode; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; - -import static java.util.stream.Collectors.toMap; - -/** - *

Information to be logged in the access log.

- * - *

This class contains the union of all information that can be - * logged with all the supported access log formats.

- * - *

The add methods can be called multiple times, - * but the parameters should be different for each - * invocation of the same method.

- * - * This class is thread-safe. - * - * @author Tony Vaagenes - * @author bakksjo - * @author bjorncs - */ -public class AccessLogEntry { - - private final Object monitor = new Object(); - - private HitCounts hitCounts; - private TraceNode traceNode; - private ListMap keyValues=null; - - public void setHitCounts(final HitCounts hitCounts) { - synchronized (monitor) { - requireNull(this.hitCounts); - this.hitCounts = hitCounts; - } - } - - public HitCounts getHitCounts() { - synchronized (monitor) { - return hitCounts; - } - } - - public void addKeyValue(String key,String value) { - synchronized (monitor) { - if (keyValues == null) { - keyValues = new ListMap<>(); - } - keyValues.put(key,value); - } - } - - public Map> getKeyValues() { - synchronized (monitor) { - if (keyValues == null) { - return null; - } - - final Map> newMapWithImmutableValues = mapValues( - keyValues.entrySet(), - valueList -> Collections.unmodifiableList(new ArrayList<>(valueList))); - return Collections.unmodifiableMap(newMapWithImmutableValues); - } - } - - private static Map mapValues( - final Set> entrySet, - final Function valueConverter) { - return entrySet.stream() - .collect(toMap( - entry -> entry.getKey(), - entry -> valueConverter.apply(entry.getValue()))); - } - - public void setTrace(TraceNode traceNode) { - synchronized (monitor) { - requireNull(this.traceNode); - this.traceNode = traceNode; - } - } - - public TraceNode getTrace() { - synchronized (monitor) { - return traceNode; - } - } - - @Override - public String toString() { - return "AccessLogEntry{" + - "hitCounts=" + hitCounts + - ", traceNode=" + traceNode + - ", keyValues=" + keyValues + - '}'; - } - - private static void requireNull(final Object value) { - if (value != null) { - throw new IllegalStateException("Attempt to overwrite field that has been assigned. Value: " + value); - } - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogHandler.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogHandler.java deleted file mode 100644 index 89aab1513ee..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/AccessLogHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.container.core.AccessLogConfig; - -/** - * @author Bjorn Borud - */ -class AccessLogHandler { - - private final LogFileHandler logFileHandler; - - AccessLogHandler(AccessLogConfig.FileHandler config, LogWriter logWriter) { - logFileHandler = new LogFileHandler<>( - toCompression(config), config.pattern(), config.rotation(), - config.symlink(), config.queueSize(), "request-logger", logWriter); - } - - public void log(RequestLogEntry entry) { - logFileHandler.publish(entry); - } - - private LogFileHandler.Compression toCompression(AccessLogConfig.FileHandler config) { - if (!config.compressOnRotation()) return LogFileHandler.Compression.NONE; - switch (config.compressionFormat()) { - case ZSTD: return LogFileHandler.Compression.ZSTD; - case GZIP: return LogFileHandler.Compression.GZIP; - default: throw new IllegalArgumentException(config.compressionFormat().toString()); - } - } - - void shutdown() { - logFileHandler.close(); - logFileHandler.shutdown(); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java deleted file mode 100644 index dc749c71613..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/CircularArrayAccessLogKeeper.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.List; - -/** - * This class keeps some information from the access log from the requests in memory. It is thread-safe. - * - * @author dybis - */ -public class CircularArrayAccessLogKeeper { - public static final int SIZE = 1000; - private final Deque uris = new ArrayDeque<>(SIZE); - private final Object monitor = new Object(); - - /** - * This class is intended to be used with injection so it can be shared between other classes. - */ - public CircularArrayAccessLogKeeper() {} - - /** - * Creates a list of Uris. - * @return URIs as string - */ - public List getUris() { - final List uriList = new ArrayList<>(); - synchronized (monitor) { - uris.iterator().forEachRemaining(uri -> uriList.add(uri)); - } - return uriList; - } - - /** - * Add a new URI. It might remove an old entry to make space for new entry. - * @param uri uri as string - */ - public void addUri(String uri) { - synchronized (monitor) { - if (uris.size() == SIZE) { - uris.pop(); - } - uris.add(uri); - } - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLog.java deleted file mode 100644 index 310231a4a1e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLog.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.container.logging; - -/** - * @author mortent - */ -public interface ConnectionLog { - void log(ConnectionLogEntry connectionLogEntry); -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java deleted file mode 100644 index 6afe3b74329..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.container.logging; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -/** - * @author mortent - */ -public class ConnectionLogEntry { - - private final UUID id; - private final Instant timestamp; - private final Double durationSeconds; - private final String peerAddress; - private final Integer peerPort; - private final String localAddress; - private final Integer localPort; - private final String remoteAddress; - private final Integer remotePort; - private final Long httpBytesReceived; - private final Long httpBytesSent; - private final Long requests; - private final Long responses; - private final String sslSessionId; - private final String sslProtocol; - private final String sslCipherSuite; - private final String sslPeerSubject; - private final Instant sslPeerNotBefore; - private final Instant sslPeerNotAfter; - private final String sslSniServerName; - private final SslHandshakeFailure sslHandshakeFailure; - - - private ConnectionLogEntry(Builder builder) { - this.id = builder.id; - this.timestamp = builder.timestamp; - this.durationSeconds = builder.durationSeconds; - this.peerAddress = builder.peerAddress; - this.peerPort = builder.peerPort; - this.localAddress = builder.localAddress; - this.localPort = builder.localPort; - this.remoteAddress = builder.remoteAddress; - this.remotePort = builder.remotePort; - this.httpBytesReceived = builder.httpBytesReceived; - this.httpBytesSent = builder.httpBytesSent; - this.requests = builder.requests; - this.responses = builder.responses; - this.sslSessionId = builder.sslSessionId; - this.sslProtocol = builder.sslProtocol; - this.sslCipherSuite = builder.sslCipherSuite; - this.sslPeerSubject = builder.sslPeerSubject; - this.sslPeerNotBefore = builder.sslPeerNotBefore; - this.sslPeerNotAfter = builder.sslPeerNotAfter; - this.sslSniServerName = builder.sslSniServerName; - this.sslHandshakeFailure = builder.sslHandshakeFailure; - } - - public static Builder builder(UUID id, Instant timestamp) { - return new Builder(id, timestamp); - } - - public String id() { return id.toString(); } - public Instant timestamp() { return timestamp; } - public Optional durationSeconds() { return Optional.ofNullable(durationSeconds); } - public Optional peerAddress() { return Optional.ofNullable(peerAddress); } - public Optional peerPort() { return Optional.ofNullable(peerPort); } - public Optional localAddress() { return Optional.ofNullable(localAddress); } - public Optional localPort() { return Optional.ofNullable(localPort); } - public Optional remoteAddress() { return Optional.ofNullable(remoteAddress); } - public Optional remotePort() { return Optional.ofNullable(remotePort); } - public Optional httpBytesReceived() { return Optional.ofNullable(httpBytesReceived); } - public Optional httpBytesSent() { return Optional.ofNullable(httpBytesSent); } - public Optional requests() { return Optional.ofNullable(requests); } - public Optional responses() { return Optional.ofNullable(responses); } - public Optional sslSessionId() { return Optional.ofNullable(sslSessionId); } - public Optional sslProtocol() { return Optional.ofNullable(sslProtocol); } - public Optional sslCipherSuite() { return Optional.ofNullable(sslCipherSuite); } - public Optional sslPeerSubject() { return Optional.ofNullable(sslPeerSubject); } - public Optional sslPeerNotBefore() { return Optional.ofNullable(sslPeerNotBefore); } - public Optional sslPeerNotAfter() { return Optional.ofNullable(sslPeerNotAfter); } - public Optional sslSniServerName() { return Optional.ofNullable(sslSniServerName); } - public Optional sslHandshakeFailure() { return Optional.ofNullable(sslHandshakeFailure); } - - public static class SslHandshakeFailure { - private final String type; - private final List exceptionChain; - - public SslHandshakeFailure(String type, List exceptionChain) { - this.type = type; - this.exceptionChain = List.copyOf(exceptionChain); - } - - public String type() { return type; } - public List exceptionChain() { return exceptionChain; } - - public static class ExceptionEntry { - private final String name; - private final String message; - - public ExceptionEntry(String name, String message) { - this.name = name; - this.message = message; - } - - public String name() { return name; } - public String message() { return message; } - } - } - - public static class Builder { - private final UUID id; - private final Instant timestamp; - private Double durationSeconds; - private String peerAddress; - private Integer peerPort; - private String localAddress; - private Integer localPort; - private String remoteAddress; - private Integer remotePort; - private Long httpBytesReceived; - private Long httpBytesSent; - private Long requests; - private Long responses; - private String sslSessionId; - private String sslProtocol; - private String sslCipherSuite; - private String sslPeerSubject; - private Instant sslPeerNotBefore; - private Instant sslPeerNotAfter; - private String sslSniServerName; - private SslHandshakeFailure sslHandshakeFailure; - - - Builder(UUID id, Instant timestamp) { - this.id = id; - this.timestamp = timestamp; - } - - public Builder withDuration(double durationSeconds) { - this.durationSeconds = durationSeconds; - return this; - } - - public Builder withPeerAddress(String peerAddress) { - this.peerAddress = peerAddress; - return this; - } - public Builder withPeerPort(int peerPort) { - this.peerPort = peerPort; - return this; - } - public Builder withLocalAddress(String localAddress) { - this.localAddress = localAddress; - return this; - } - public Builder withLocalPort(int localPort) { - this.localPort = localPort; - return this; - } - public Builder withRemoteAddress(String remoteAddress) { - this.remoteAddress = remoteAddress; - return this; - } - public Builder withRemotePort(int remotePort) { - this.remotePort = remotePort; - return this; - } - public Builder withHttpBytesReceived(long bytesReceived) { - this.httpBytesReceived = bytesReceived; - return this; - } - public Builder withHttpBytesSent(long bytesSent) { - this.httpBytesSent = bytesSent; - return this; - } - public Builder withRequests(long requests) { - this.requests = requests; - return this; - } - public Builder withResponses(long responses) { - this.responses = responses; - return this; - } - public Builder withSslSessionId(String sslSessionId) { - this.sslSessionId = sslSessionId; - return this; - } - public Builder withSslProtocol(String sslProtocol) { - this.sslProtocol = sslProtocol; - return this; - } - public Builder withSslCipherSuite(String sslCipherSuite) { - this.sslCipherSuite = sslCipherSuite; - return this; - } - public Builder withSslPeerSubject(String sslPeerSubject) { - this.sslPeerSubject = sslPeerSubject; - return this; - } - public Builder withSslPeerNotBefore(Instant sslPeerNotBefore) { - this.sslPeerNotBefore = sslPeerNotBefore; - return this; - } - public Builder withSslPeerNotAfter(Instant sslPeerNotAfter) { - this.sslPeerNotAfter = sslPeerNotAfter; - return this; - } - public Builder withSslSniServerName(String sslSniServerName) { - this.sslSniServerName = sslSniServerName; - return this; - } - public Builder withSslHandshakeFailure(SslHandshakeFailure sslHandshakeFailure) { - this.sslHandshakeFailure = sslHandshakeFailure; - return this; - } - - public ConnectionLogEntry build(){ - return new ConnectionLogEntry(this); - } - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java deleted file mode 100644 index 7a0e8aca95e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.container.logging; - -/** - * @author mortent - */ -class ConnectionLogHandler { - private final LogFileHandler logFileHandler; - - public ConnectionLogHandler(String logDirectoryName, String clusterName, int queueSize, LogWriter logWriter) { - logFileHandler = new LogFileHandler<>( - LogFileHandler.Compression.ZSTD, - String.format("logs/vespa/%s/ConnectionLog.%s.%s", logDirectoryName, clusterName, "%Y%m%d%H%M%S"), - "0 60 ...", - String.format("ConnectionLog.%s", clusterName), - queueSize, - "connection-logger", - logWriter); - } - - public void log(ConnectionLogEntry entry) { - logFileHandler.publish(entry); - } - - public void shutdown() { - logFileHandler.close(); - logFileHandler.shutdown(); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/Coverage.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/Coverage.java deleted file mode 100644 index 9d122b90641..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/Coverage.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -/** - * Carry information about how the query covered the document corpus. - */ -public class Coverage { - private final long docs; - private final long active; - private final long soonActive; - private final int degradedReason; - private final static int DEGRADED_BY_MATCH_PHASE = 1; - private final static int DEGRADED_BY_TIMEOUT = 2; - private final static int DEGRADED_BY_ADAPTIVE_TIMEOUT = 4; - public Coverage(long docs, long active, long soonActive, int degradedReason) { - this.docs = docs; - this.active = active; - this.soonActive = soonActive; - this.degradedReason = degradedReason; - } - - public long getDocs() { - return docs; - } - - public long getActive() { - return active; - } - - public static int toDegradation(boolean degradeByMatchPhase, boolean degradedByTimeout, boolean degradedByAdaptiveTimeout) { - int v = 0; - if (degradeByMatchPhase) { - v |= DEGRADED_BY_MATCH_PHASE; - } - if (degradedByTimeout) { - v |= DEGRADED_BY_TIMEOUT; - } - if (degradedByAdaptiveTimeout) { - v |= DEGRADED_BY_ADAPTIVE_TIMEOUT; - } - return v; - } - - public long getSoonActive() { return soonActive; } - - public boolean isDegraded() { return (degradedReason != 0) || isDegradedByNonIdealState(); } - public boolean isDegradedByMatchPhase() { return (degradedReason & DEGRADED_BY_MATCH_PHASE) != 0; } - public boolean isDegradedByTimeout() { return (degradedReason & DEGRADED_BY_TIMEOUT) != 0; } - public boolean isDegradedByAdapativeTimeout() { return (degradedReason & DEGRADED_BY_ADAPTIVE_TIMEOUT) != 0; } - public boolean isDegradedByNonIdealState() { return (degradedReason == 0) && (getResultPercentage() != 100);} - - /** - * An int between 0 (inclusive) and 100 (inclusive) representing how many - * percent coverage the result sets this Coverage instance contains information - * about had. - */ - public int getResultPercentage() { - if (docs < active) { - return (int) Math.round(docs * 100.0d / active); - } - return 100; - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java deleted file mode 100644 index 7432c313286..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.container.logging; - -import com.google.inject.Inject; -import com.yahoo.component.AbstractComponent; - -/** - * @author mortent - */ -public class FileConnectionLog extends AbstractComponent implements ConnectionLog { - - private final ConnectionLogHandler logHandler; - - @Inject - public FileConnectionLog(ConnectionLogConfig config) { - logHandler = new ConnectionLogHandler(config.logDirectoryName(), config.cluster(), config.queueSize(), new JsonConnectionLogWriter()); - } - - @Override - public void log(ConnectionLogEntry connectionLogEntry) { - logHandler.log(connectionLogEntry); - } - - @Override - public void deconstruct() { - logHandler.shutdown(); - } - -} \ No newline at end of file diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java deleted file mode 100644 index ee780ad2a83..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.fasterxml.jackson.core.JsonGenerator; - -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; - -/** - * @author bjorncs - */ -class FormatUtil { - - private FormatUtil() {} - - static void writeSecondsField(JsonGenerator generator, String fieldName, Instant instant) throws IOException { - writeSecondsField(generator, fieldName, instant.toEpochMilli()); - } - - static void writeSecondsField(JsonGenerator generator, String fieldName, Duration duration) throws IOException { - writeSecondsField(generator, fieldName, duration.toMillis()); - } - - static void writeSecondsField(JsonGenerator generator, String fieldName, double seconds) throws IOException { - writeSecondsField(generator, fieldName, (long)(seconds * 1000)); - } - - static void writeSecondsField(JsonGenerator generator, String fieldName, long milliseconds) throws IOException { - generator.writeFieldName(fieldName); - generator.writeRawValue(toSecondsString(milliseconds)); - } - - /** @return a string with number of seconds with 3 decimals */ - static String toSecondsString(long milliseconds) { - StringBuilder builder = new StringBuilder().append(milliseconds / 1000L).append('.'); - long decimals = milliseconds % 1000; - if (decimals < 100) { - builder.append('0'); - if (decimals < 10) { - builder.append('0'); - } - } - return builder.append(decimals).toString(); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/HitCounts.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/HitCounts.java deleted file mode 100644 index fed12281962..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/HitCounts.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -/** - * A wrapper for hit counts, modelled after a search system. - * Advanced database searches and similar could use these - * structures as well. - * - * @author Steinar Knutsen - */ -public class HitCounts { - - // see the javadoc for the accessors for short comments on each field - private final int retrievedHits; - private final int summaryCount; - private final long totalHitCount; - private final int requestedHits; - private final int requestedOffset; - private final Coverage coverage; - - HitCounts(int retrievedHits, int summaryCount, long totalHitCount, int requestedHits, int requestedOffset) { - this(retrievedHits, summaryCount, totalHitCount, requestedHits, requestedOffset, - new Coverage(1,1,1,0)); - } - - public HitCounts(int retrievedHits, int summaryCount, long totalHitCount, - int requestedHits, int requestedOffset, Coverage coverage) - { - - this.retrievedHits = retrievedHits; - this.summaryCount = summaryCount; - this.totalHitCount = totalHitCount; - this.requestedHits = requestedHits; - this.requestedOffset = requestedOffset; - this.coverage = coverage; - } - - /** - * The number of hits returned by the server. - * Compare to getRequestedHits(). - */ - public int getRetrievedHitCount() { - return retrievedHits; - } - - /** - * The number of hit summaries ("document contents") fetched. - */ - public int getSummaryCount() { - return summaryCount; - } - - /** - * The total number of matching hits - * for the request. - */ - public long getTotalHitCount() { - return totalHitCount; - } - - /** - * The number of hits requested by the user. - * Compare to getRetrievedHitCount(). - */ - public int getRequestedHits() { - return requestedHits; - } - - /** - * The user requested offset into the linear mapping of the result space. - */ - public int getRequestedOffset() { - return requestedOffset; - } - - public Coverage getCoverage() { return coverage; } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONAccessLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONAccessLog.java deleted file mode 100644 index ece9d0d2c4a..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONAccessLog.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.component.AbstractComponent; -import com.yahoo.container.core.AccessLogConfig; - -/** - * Log a message in Vespa JSON access log format. - * - * @author frodelu - * @author Tony Vaagenes - */ -public final class JSONAccessLog extends AbstractComponent implements RequestLogHandler { - - private final AccessLogHandler logHandler; - - public JSONAccessLog(AccessLogConfig config) { - logHandler = new AccessLogHandler(config.fileHandler(), new JSONFormatter()); - } - - @Override - public void log(RequestLogEntry entry) { - logHandler.log(entry); - } - - @Override public void deconstruct() { logHandler.shutdown(); } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java deleted file mode 100644 index 680ee5acbd9..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.yolean.trace.TraceNode; - -import java.io.IOException; -import java.io.OutputStream; -import java.security.Principal; -import java.util.Collection; -import java.util.Objects; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Formatting of an {@link AccessLogEntry} in the Vespa JSON access log format. - * - * @author frodelu - */ -public class JSONFormatter implements LogWriter { - private static final String COVERAGE = "coverage"; - private static final String COVERAGE_COVERAGE = "coverage"; - private static final String COVERAGE_DOCUMENTS = "documents"; - private static final String COVERAGE_DEGRADE = "degraded"; - private static final String COVERAGE_DEGRADE_MATCHPHASE = "match-phase"; - private static final String COVERAGE_DEGRADE_TIMEOUT = "timeout"; - private static final String COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT = "adaptive-timeout"; - private static final String COVERAGE_DEGRADED_NON_IDEAL_STATE = "non-ideal-state"; - - private final JsonFactory generatorFactory; - - private static Logger logger = Logger.getLogger(JSONFormatter.class.getName()); - - public JSONFormatter() { - generatorFactory = new JsonFactory(new ObjectMapper()); - } - - @Override - public void write(RequestLogEntry entry, OutputStream outputStream) throws IOException { - try (JsonGenerator generator = createJsonGenerator(outputStream)){ - generator.writeStartObject(); - String peerAddress = entry.peerAddress().get(); - generator.writeStringField("ip", peerAddress); - long time = entry.timestamp().get().toEpochMilli(); - FormatUtil.writeSecondsField(generator, "time", time); - FormatUtil.writeSecondsField(generator, "duration", entry.duration().get()); - generator.writeNumberField("responsesize", entry.contentSize().orElse(0)); - generator.writeNumberField("code", entry.statusCode().orElse(0)); - generator.writeStringField("method", entry.httpMethod().orElse("")); - generator.writeStringField("uri", getNormalizedURI(entry.rawPath().orElse(null), entry.rawQuery().orElse(null))); - generator.writeStringField("version", entry.httpVersion().orElse("")); - generator.writeStringField("agent", entry.userAgent().orElse("")); - generator.writeStringField("host", entry.hostString().orElse("")); - generator.writeStringField("scheme", entry.scheme().orElse(null)); - generator.writeNumberField("localport", entry.localPort().getAsInt()); - - String connectionId = entry.connectionId().orElse(null); - if (connectionId != null) { - generator.writeStringField("connection", connectionId); - } - - Principal userPrincipal = entry.userPrincipal().orElse(null); - if (userPrincipal != null) { - generator.writeStringField("user-principal", userPrincipal.getName()); - } - - Principal sslPrincipal = entry.sslPrincipal().orElse(null); - if (sslPrincipal != null) { - generator.writeStringField("ssl-principal", sslPrincipal.getName()); - } - - String remoteAddress = entry.remoteAddress().orElse(null); - int remotePort = entry.remotePort().orElse(0); - // Only add remote address/port fields if relevant - if (remoteAddressDiffers(peerAddress, remoteAddress)) { - generator.writeStringField("remoteaddr", remoteAddress); - if (remotePort > 0) { - generator.writeNumberField("remoteport", remotePort); - } - } - - // Only add peer address/port fields if relevant - if (peerAddress != null) { - generator.writeStringField("peeraddr", peerAddress); - - int peerPort = entry.peerPort().getAsInt(); - if (peerPort > 0 && peerPort != remotePort) { - generator.writeNumberField("peerport", peerPort); - } - } - - TraceNode trace = entry.traceNode().orElse(null); - if (trace != null) { - long timestamp = trace.timestamp(); - if (timestamp == 0L) { - timestamp = time; - } - trace.accept(new TraceRenderer(generator, timestamp)); - } - - // Only add search sub block of this is a search request - if (isSearchRequest(entry)) { - HitCounts hitCounts = entry.hitCounts().get(); - generator.writeObjectFieldStart("search"); - generator.writeNumberField("totalhits", getTotalHitCount(hitCounts)); - generator.writeNumberField("hits", getRetrievedHitCount(hitCounts)); - Coverage c = hitCounts.getCoverage(); - if (c != null) { - generator.writeObjectFieldStart(COVERAGE); - generator.writeNumberField(COVERAGE_COVERAGE, c.getResultPercentage()); - generator.writeNumberField(COVERAGE_DOCUMENTS, c.getDocs()); - if (c.isDegraded()) { - generator.writeObjectFieldStart(COVERAGE_DEGRADE); - if (c.isDegradedByMatchPhase()) - generator.writeBooleanField(COVERAGE_DEGRADE_MATCHPHASE, c.isDegradedByMatchPhase()); - if (c.isDegradedByTimeout()) - generator.writeBooleanField(COVERAGE_DEGRADE_TIMEOUT, c.isDegradedByTimeout()); - if (c.isDegradedByAdapativeTimeout()) - generator.writeBooleanField(COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT, c.isDegradedByAdapativeTimeout()); - if (c.isDegradedByNonIdealState()) - generator.writeBooleanField(COVERAGE_DEGRADED_NON_IDEAL_STATE, c.isDegradedByNonIdealState()); - generator.writeEndObject(); - } - generator.writeEndObject(); - } - generator.writeEndObject(); - } - - // Add key/value access log entries. Keys with single values are written as single - // string value fields while keys with multiple values are written as string arrays - Collection keys = entry.extraAttributeKeys(); - if (!keys.isEmpty()) { - generator.writeObjectFieldStart("attributes"); - for (String key : keys) { - Collection values = entry.extraAttributeValues(key); - if (values.size() == 1) { - generator.writeStringField(key, values.iterator().next()); - } else { - generator.writeFieldName(key); - generator.writeStartArray(); - for (String s : values) { - generator.writeString(s); - } - generator.writeEndArray(); - } - } - generator.writeEndObject(); - } - - generator.writeEndObject(); - } catch (IOException e) { - logger.log(Level.WARNING, "Unable to generate JSON access log entry: " + e.getMessage(), e); - } - } - - private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException { - return generatorFactory.createGenerator(outputStream, JsonEncoding.UTF8) - .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false) - .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false); - } - - private boolean remoteAddressDiffers(String ipV4Address, String remoteAddress) { - return remoteAddress != null && !Objects.equals(ipV4Address, remoteAddress); - } - - private boolean isSearchRequest(RequestLogEntry entry) { - return entry != null && entry.hitCounts().isPresent(); - } - - private long getTotalHitCount(HitCounts counts) { - if (counts == null) { - return 0; - } - - return counts.getTotalHitCount(); - } - - private int getRetrievedHitCount(HitCounts counts) { - if (counts == null) { - return 0; - } - - return counts.getRetrievedHitCount(); - } - - private static String getNormalizedURI(String rawPath, String rawQuery) { - if (rawPath == null) return null; - return rawQuery != null ? rawPath + "?" + rawQuery : rawPath; - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java deleted file mode 100644 index 158d2ec4ea6..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; - -import java.io.IOException; -import java.io.OutputStream; -import java.time.Instant; -import java.util.Arrays; -import java.util.Objects; -import java.util.Optional; - -/** - * @author bjorncs - */ -class JsonConnectionLogWriter implements LogWriter { - - private final JsonFactory jsonFactory = new JsonFactory(new ObjectMapper()); - - @Override - public void write(ConnectionLogEntry record, OutputStream outputStream) throws IOException { - try (JsonGenerator generator = createJsonGenerator(outputStream)) { - generator.writeStartObject(); - generator.writeStringField("id", record.id()); - generator.writeStringField("timestamp", record.timestamp().toString()); - - writeOptionalSeconds(generator, "duration", unwrap(record.durationSeconds())); - writeOptionalString(generator, "peerAddress", unwrap(record.peerAddress())); - writeOptionalInteger(generator, "peerPort", unwrap(record.peerPort())); - writeOptionalString(generator, "localAddress", unwrap(record.localAddress())); - writeOptionalInteger(generator, "localPort", unwrap(record.localPort())); - writeOptionalString(generator, "remoteAddress", unwrap(record.remoteAddress())); - writeOptionalInteger(generator, "remotePort", unwrap(record.remotePort())); - writeOptionalLong(generator, "httpBytesReceived", unwrap(record.httpBytesReceived())); - writeOptionalLong(generator, "httpBytesSent", unwrap(record.httpBytesSent())); - writeOptionalLong(generator, "requests", unwrap(record.requests())); - writeOptionalLong(generator, "responses", unwrap(record.responses())); - - String sslProtocol = unwrap(record.sslProtocol()); - String sslSessionId = unwrap(record.sslSessionId()); - String sslCipherSuite = unwrap(record.sslCipherSuite()); - String sslPeerSubject = unwrap(record.sslPeerSubject()); - Instant sslPeerNotBefore = unwrap(record.sslPeerNotBefore()); - Instant sslPeerNotAfter = unwrap(record.sslPeerNotAfter()); - String sslSniServerName = unwrap(record.sslSniServerName()); - ConnectionLogEntry.SslHandshakeFailure sslHandshakeFailure = unwrap(record.sslHandshakeFailure()); - - if (isAnyValuePresent( - sslProtocol, sslSessionId, sslCipherSuite, sslPeerSubject, sslPeerNotBefore, sslPeerNotAfter, - sslSniServerName, sslHandshakeFailure)) { - generator.writeObjectFieldStart("ssl"); - - writeOptionalString(generator, "protocol", sslProtocol); - writeOptionalString(generator, "sessionId", sslSessionId); - writeOptionalString(generator, "cipherSuite", sslCipherSuite); - writeOptionalString(generator, "peerSubject", sslPeerSubject); - writeOptionalTimestamp(generator, "peerNotBefore", sslPeerNotBefore); - writeOptionalTimestamp(generator, "peerNotAfter", sslPeerNotAfter); - writeOptionalString(generator, "sniServerName", sslSniServerName); - - if (sslHandshakeFailure != null) { - generator.writeObjectFieldStart("handshake-failure"); - generator.writeArrayFieldStart("exception"); - for (ExceptionEntry entry : sslHandshakeFailure.exceptionChain()) { - generator.writeStartObject(); - generator.writeStringField("cause", entry.name()); - generator.writeStringField("message", entry.message()); - generator.writeEndObject(); - } - generator.writeEndArray(); - generator.writeStringField("type", sslHandshakeFailure.type()); - generator.writeEndObject(); - } - - generator.writeEndObject(); - } - } - } - - private void writeOptionalString(JsonGenerator generator, String name, String value) throws IOException { - if (value != null) { - generator.writeStringField(name, value); - } - } - - private void writeOptionalInteger(JsonGenerator generator, String name, Integer value) throws IOException { - if (value != null) { - generator.writeNumberField(name, value); - } - } - - private void writeOptionalLong(JsonGenerator generator, String name, Long value) throws IOException { - if (value != null) { - generator.writeNumberField(name, value); - } - } - - private void writeOptionalTimestamp(JsonGenerator generator, String name, Instant value) throws IOException { - if (value != null) { - generator.writeStringField(name, value.toString()); - } - } - - private void writeOptionalSeconds(JsonGenerator generator, String name, Double value) throws IOException { - if (value != null) { - FormatUtil.writeSecondsField(generator, name, value); - } - } - - private static boolean isAnyValuePresent(Object... values) { return Arrays.stream(values).anyMatch(Objects::nonNull); } - private static T unwrap(Optional maybeValue) { return maybeValue.orElse(null); } - - private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException { - return jsonFactory.createGenerator(outputStream, JsonEncoding.UTF8) - .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false) - .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFileHandler.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFileHandler.java deleted file mode 100644 index 0f2a9e42eb8..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFileHandler.java +++ /dev/null @@ -1,563 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.compress.ZstdOuputStream; -import com.yahoo.io.NativeIO; -import com.yahoo.log.LogFileDb; -import com.yahoo.protect.Process; -import com.yahoo.yolean.Exceptions; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.Optional; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.GZIPOutputStream; - -/** - * Implements log file naming/rotating logic for container logs. - * - * @author Bob Travis - * @author bjorncs - */ -class LogFileHandler { - - enum Compression {NONE, GZIP, ZSTD} - - private final static Logger logger = Logger.getLogger(LogFileHandler.class.getName()); - private final BlockingQueue> logQueue; - final LogThread logThread; - - @FunctionalInterface private interface Pollable { Operation poll() throws InterruptedException; } - - LogFileHandler(Compression compression, String filePattern, String rotationTimes, String symlinkName, int queueSize, - String threadName, LogWriter logWriter) { - this(compression, filePattern, calcTimesMinutes(rotationTimes), symlinkName, queueSize, threadName, logWriter); - } - - LogFileHandler( - Compression compression, - String filePattern, - long[] rotationTimes, - String symlinkName, - int queueSize, - String threadName, - LogWriter logWriter) { - this.logQueue = new LinkedBlockingQueue<>(queueSize); - this.logThread = new LogThread<>(logWriter, filePattern, compression, rotationTimes, symlinkName, threadName, this::poll); - this.logThread.start(); - } - - private Operation poll() throws InterruptedException { - return logQueue.poll(100, TimeUnit.MILLISECONDS); - } - - /** - * Sends logrecord to file, first rotating file if needed. - * - * @param r logrecord to publish - */ - public void publish(LOGTYPE r) { - addOperation(new Operation<>(r)); - } - - void publishAndWait(LOGTYPE r) { - addOperationAndWait(new Operation<>(r)); - } - - public void flush() { - addOperationAndWait(new Operation<>(Operation.Type.flush)); - } - - /** - * Force file rotation now, independent of schedule. - */ - void rotateNow() { - addOperationAndWait(new Operation<>(Operation.Type.rotate)); - } - - public void close() { - addOperationAndWait(new Operation<>(Operation.Type.close)); - } - - private void addOperation(Operation op) { - try { - logQueue.put(op); - } catch (InterruptedException e) { - } - } - - private void addOperationAndWait(Operation op) { - try { - logQueue.put(op); - op.countDownLatch.await(); - } catch (InterruptedException e) { - } - } - - /** - * Flushes all queued messages, interrupts the log thread in this and - * waits for it to end before returning - */ - void shutdown() { - logThread.interrupt(); - try { - logThread.executor.shutdownNow(); - logThread.executor.awaitTermination(600, TimeUnit.SECONDS); - logThread.join(); - } catch (InterruptedException e) { - } - } - - /** - * Calculate rotation times array, given times in minutes, as "0 60 ..." - */ - private static long[] calcTimesMinutes(String times) { - ArrayList list = new ArrayList<>(50); - int i = 0; - boolean etc = false; - - while (i < times.length()) { - if (times.charAt(i) == ' ') { - i++; - continue; - } // skip spaces - int j = i; // start of string - i = times.indexOf(' ', i); - if (i == -1) i = times.length(); - if (times.charAt(j) == '.' && times.substring(j, i).equals("...")) { // ... - etc = true; - break; - } - list.add(Long.valueOf(times.substring(j, i))); - } - - int size = list.size(); - long[] longtimes = new long[size]; - for (i = 0; i < size; i++) { - longtimes[i] = list.get(i) // pick up value in minutes past midnight - * 60000; // and multiply to get millis - } - - if (etc) { // fill out rest of day, same as final interval - long endOfDay = 24 * 60 * 60 * 1000; - long lasttime = longtimes[size - 1]; - long interval = lasttime - longtimes[size - 2]; - long moreneeded = (endOfDay - lasttime) / interval; - if (moreneeded > 0) { - int newsize = size + (int) moreneeded; - long[] temp = new long[newsize]; - for (i = 0; i < size; i++) { - temp[i] = longtimes[i]; - } - while (size < newsize) { - lasttime += interval; - temp[size++] = lasttime; - } - longtimes = temp; - } - } - - return longtimes; - } - - /** - * Only for unit testing. Do not use. - */ - String getFileName() { - return logThread.fileName; - } - - /** - * Handle logging and file operations - */ - static class LogThread extends Thread { - private final Pollable operationProvider; - long lastFlush = 0; - private PageCacheFriendlyFileOutputStream fileOutput = null; - private long nextRotationTime = 0; - private final String filePattern; // default to current directory, ms time stamp - private volatile String fileName; - private final LogWriter logWriter; - private final Compression compression; - private final long[] rotationTimes; - private final String symlinkName; - private final ExecutorService executor = createCompressionTaskExecutor(); - private final NativeIO nativeIO = new NativeIO(); - - - LogThread(LogWriter logWriter, - String filePattern, - Compression compression, - long[] rotationTimes, - String symlinkName, - String threadName, - Pollable operationProvider) { - super(threadName); - setDaemon(true); - this.logWriter = logWriter; - this.filePattern = filePattern; - this.compression = compression; - this.rotationTimes = rotationTimes; - this.symlinkName = (symlinkName != null && !symlinkName.isBlank()) ? symlinkName : null; - this.operationProvider = operationProvider; - } - - private static ExecutorService createCompressionTaskExecutor() { - return Executors.newSingleThreadExecutor(runnable -> { - Thread thread = new Thread(runnable, "logfilehandler.compression"); - thread.setDaemon(true); - thread.setPriority(Thread.MIN_PRIORITY); - return thread; - }); - } - - @Override - public void run() { - try { - handleLogOperations(); - } catch (InterruptedException e) { - } catch (Exception e) { - Process.logAndDie("Failed storing log records", e); - } - - internalFlush(); - } - - private void handleLogOperations() throws InterruptedException { - while (!isInterrupted()) { - Operation r = operationProvider.poll(); - if (r != null) { - if (r.type == Operation.Type.flush) { - internalFlush(); - } else if (r.type == Operation.Type.close) { - internalClose(); - } else if (r.type == Operation.Type.rotate) { - internalRotateNow(); - lastFlush = System.nanoTime(); - } else if (r.type == Operation.Type.log) { - internalPublish(r.log.get()); - flushIfOld(3, TimeUnit.SECONDS); - } - r.countDownLatch.countDown(); - } else { - flushIfOld(100, TimeUnit.MILLISECONDS); - } - } - } - - private void flushIfOld(long age, TimeUnit unit) { - long now = System.nanoTime(); - if (TimeUnit.NANOSECONDS.toMillis(now - lastFlush) > unit.toMillis(age)) { - internalFlush(); - lastFlush = now; - } - } - - private void internalFlush() { - try { - if (fileOutput != null) { - fileOutput.flush(); - } - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to flush file output: " + Exceptions.toMessageString(e), e); - } - } - - private void internalClose() { - try { - if (fileOutput != null) { - fileOutput.flush(); - fileOutput.close(); - fileOutput = null; - } - } catch (Exception e) { - logger.log(Level.WARNING, "Got error while closing log file: " + e.getMessage(), e); - } - } - - private void internalPublish(LOGTYPE r) { - // first check to see if new file needed. - // if so, use this.internalRotateNow() to do it - - long now = System.currentTimeMillis(); - if (nextRotationTime <= 0) { - nextRotationTime = getNextRotationTime(now); // lazy initialization - } - if (now > nextRotationTime || fileOutput == null) { - internalRotateNow(); - } - try { - logWriter.write(r, fileOutput); - fileOutput.write('\n'); - } catch (IOException e) { - logger.warning("Failed writing log record: " + Exceptions.toMessageString(e)); - } - } - - /** - * Find next rotation after specified time. - * - * @param now the specified time; if zero, current time is used. - * @return the next rotation time - */ - long getNextRotationTime(long now) { - if (now <= 0) { - now = System.currentTimeMillis(); - } - long nowTod = timeOfDayMillis(now); - long next = 0; - for (long rotationTime : rotationTimes) { - if (nowTod < rotationTime) { - next = rotationTime - nowTod + now; - break; - } - } - if (next == 0) { // didn't find one -- use 1st time 'tomorrow' - next = rotationTimes[0] + lengthOfDayMillis - nowTod + now; - } - - return next; - } - - private void checkAndCreateDir(String pathname) { - int lastSlash = pathname.lastIndexOf("/"); - if (lastSlash > -1) { - String pathExcludingFilename = pathname.substring(0, lastSlash); - File filepath = new File(pathExcludingFilename); - if (!filepath.exists()) { - filepath.mkdirs(); - } - } - } - - - // Throw InterruptedException upwards rather than relying on isInterrupted to stop the thread as - // isInterrupted() returns false after interruption in p.waitFor - private void internalRotateNow() { - // figure out new file name, then - - String oldFileName = fileName; - long now = System.currentTimeMillis(); - fileName = LogFormatter.insertDate(filePattern, now); - internalClose(); - try { - checkAndCreateDir(fileName); - fileOutput = new PageCacheFriendlyFileOutputStream(nativeIO, Paths.get(fileName), 4 * 1024 * 1024); - LogFileDb.nowLoggingTo(fileName); - } catch (IOException e) { - throw new RuntimeException("Couldn't open log file '" + fileName + "'", e); - } - - if(oldFileName == null) oldFileName = getOldFileNameFromSymlink(); // To compress previous file, if so configured - createSymlinkToCurrentFile(); - - nextRotationTime = 0; //figure it out later (lazy evaluation) - if ((oldFileName != null)) { - Path oldFile = Paths.get(oldFileName); - if (Files.exists(oldFile)) { - executor.execute(() -> runCompression(nativeIO, oldFile, compression)); - } - } - } - - - private static void runCompression(NativeIO nativeIO, Path oldFile, Compression compression) { - switch (compression) { - case ZSTD: - runCompressionZstd(nativeIO, oldFile); - break; - case GZIP: - runCompressionGzip(nativeIO, oldFile); - break; - case NONE: - runCompressionNone(nativeIO, oldFile); - break; - default: - throw new IllegalArgumentException("Unknown compression " + compression); - } - } - - private static void runCompressionNone(NativeIO nativeIO, Path oldFile) { - nativeIO.dropFileFromCache(oldFile.toFile()); - } - - private static void runCompressionZstd(NativeIO nativeIO, Path oldFile) { - try { - Path compressedFile = Paths.get(oldFile.toString() + ".zst"); - int bufferSize = 2*1024*1024; - try (FileOutputStream fileOut = AtomicFileOutputStream.create(compressedFile); - ZstdOuputStream out = new ZstdOuputStream(fileOut, bufferSize); - FileInputStream in = new FileInputStream(oldFile.toFile())) { - pageFriendlyTransfer(nativeIO, out, fileOut.getFD(), in, bufferSize); - out.flush(); - } - Files.delete(oldFile); - nativeIO.dropFileFromCache(compressedFile.toFile()); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to compress log file with zstd: " + oldFile, e); - } finally { - nativeIO.dropFileFromCache(oldFile.toFile()); - } - } - - private static void runCompressionGzip(NativeIO nativeIO, Path oldFile) { - try { - Path gzippedFile = Paths.get(oldFile.toString() + ".gz"); - try (FileOutputStream fileOut = AtomicFileOutputStream.create(gzippedFile); - GZIPOutputStream compressor = new GZIPOutputStream(fileOut, 0x100000); - FileInputStream inputStream = new FileInputStream(oldFile.toFile())) { - pageFriendlyTransfer(nativeIO, compressor, fileOut.getFD(), inputStream, 0x400000); - compressor.finish(); - compressor.flush(); - } - Files.delete(oldFile); - nativeIO.dropFileFromCache(gzippedFile.toFile()); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to compress log file with gzip: " + oldFile, e); - } finally { - nativeIO.dropFileFromCache(oldFile.toFile()); - } - } - - private static void pageFriendlyTransfer(NativeIO nativeIO, OutputStream out, FileDescriptor outDescriptor, FileInputStream in, int bufferSize) throws IOException { - int read; - long totalBytesRead = 0; - byte[] buffer = new byte[bufferSize]; - while ((read = in.read(buffer)) >= 0) { - out.write(buffer, 0, read); - if (read > 0) { - nativeIO.dropPartialFileFromCache(in.getFD(), totalBytesRead, read, false); - nativeIO.dropPartialFileFromCache(outDescriptor, totalBytesRead, read, false); - } - totalBytesRead += read; - } - } - - /** - * Name files by date - create a symlink with a constant name to the newest file - */ - private void createSymlinkToCurrentFile() { - if (symlinkName == null) return; - Path target = Paths.get(fileName); - Path link = target.resolveSibling(symlinkName); - try { - Files.deleteIfExists(link); - Files.createSymbolicLink(link, target.getFileName()); - } catch (IOException e) { - logger.log(Level.WARNING, "Failed to create symbolic link to current log file: " + e.getMessage(), e); - } - } - - private String getOldFileNameFromSymlink() { - if(symlinkName == null) return null; - try { - return Paths.get(fileName).resolveSibling(symlinkName).toRealPath().toString(); - } catch (IOException e) { - return null; - } - } - - private static final long lengthOfDayMillis = 24 * 60 * 60 * 1000; - private static long timeOfDayMillis(long time) { - return time % lengthOfDayMillis; - } - - } - - private static class Operation { - enum Type {log, flush, close, rotate} - - final Type type; - - final Optional log; - final CountDownLatch countDownLatch = new CountDownLatch(1); - - Operation(Type type) { - this(type, Optional.empty()); - } - - Operation(LOGTYPE log) { - this(Type.log, Optional.of(log)); - } - - private Operation(Type type, Optional log) { - this.type = type; - this.log = log; - } - } - - /** File output stream that signals to kernel to drop previous pages after write */ - private static class PageCacheFriendlyFileOutputStream extends OutputStream { - - private final NativeIO nativeIO; - private final FileOutputStream fileOut; - private final BufferedOutputStream bufferedOut; - private final int bufferSize; - private long lastDropPosition = 0; - - PageCacheFriendlyFileOutputStream(NativeIO nativeIO, Path file, int bufferSize) throws FileNotFoundException { - this.nativeIO = nativeIO; - this.fileOut = new FileOutputStream(file.toFile(), true); - this.bufferedOut = new BufferedOutputStream(fileOut, bufferSize); - this.bufferSize = bufferSize; - } - - @Override public void write(byte[] b) throws IOException { bufferedOut.write(b); } - @Override public void write(byte[] b, int off, int len) throws IOException { bufferedOut.write(b, off, len); } - @Override public void write(int b) throws IOException { bufferedOut.write(b); } - @Override public void close() throws IOException { bufferedOut.close(); } - - @Override - public void flush() throws IOException { - bufferedOut.flush(); - long newPos = fileOut.getChannel().position(); - if (newPos >= lastDropPosition + bufferSize) { - nativeIO.dropPartialFileFromCache(fileOut.getFD(), lastDropPosition, newPos, true); - lastDropPosition = newPos; - } - } - } - - private static class AtomicFileOutputStream extends FileOutputStream { - private final Path path; - private final Path tmpPath; - private volatile boolean closed = false; - - private AtomicFileOutputStream(Path path, Path tmpPath) throws FileNotFoundException { - super(tmpPath.toFile()); - this.path = path; - this.tmpPath = tmpPath; - } - - @Override - public synchronized void close() throws IOException { - super.close(); - if (!closed) { - Files.move(tmpPath, path, StandardCopyOption.ATOMIC_MOVE); - closed = true; - } - } - - private static AtomicFileOutputStream create(Path path) throws FileNotFoundException { - return new AtomicFileOutputStream(path, path.resolveSibling("." + path.getFileName() + ".tmp")); - } - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFormatter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFormatter.java deleted file mode 100644 index cc1dcb579aa..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogFormatter.java +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; -import java.util.logging.Formatter; -import java.util.logging.LogRecord; - - -/** - * Produces compact output format for prelude logs - * - * @author Bob Travis - */ -public class LogFormatter extends Formatter { - - /** date format objects */ - static SimpleDateFormat ddMMMyyyy; - static DateFormat dfMMM; - static SimpleDateFormat yyyyMMdd; - - static { - ddMMMyyyy = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]", Locale.US); - ddMMMyyyy.setTimeZone(TimeZone.getTimeZone("UTC")); - - dfMMM = new SimpleDateFormat("MMM", Locale.US); - dfMMM.setTimeZone(TimeZone.getTimeZone("UTC")); - - yyyyMMdd = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]", Locale.US); - yyyyMMdd.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - /** Whether to strip down the message to only the message or not */ - private boolean messageOnly = false; - - /** Controls which of the available timestamp formats is used in all log records - */ - private static final int timestampFormat = 2; // 0=millis, 1=mm/dd/yyyy, 2=yyyy-mm-dd - - /** - * Standard constructor - */ - - public LogFormatter() {} - - /** - * Make it possible to log stripped messages - */ - public void messageOnly (boolean messageOnly) { - this.messageOnly = messageOnly; - } - - public String format(LogRecord record) { - - // if we don't want any other stuff we just return the message - if (messageOnly) { - return formatMessage(record); - } - - String rawMsg = record.getMessage(); - boolean isLogMsg = - rawMsg.charAt(0) == 'L' - && rawMsg.charAt(1) == 'O' - && rawMsg.charAt(2) == 'G' - && rawMsg.charAt(3) == ':'; - String nameInsert = - (!isLogMsg) - ? record.getLevel().getName() + ": " - : ""; - return (timeStamp(record) - + nameInsert - + formatMessage(record) - + "\n" - ); - } - - /** - * Public support methods - */ - - /** - * Static insertDate method will insert date fragments into a string - * based on '%x' pattern elements. Equivalents in SimpleDateFormatter patterns, - * with examples: - *
    - *
  • %Y YYYY 2003 - *
  • %m MM 08 - *
  • %x MMM Aug - *
  • %d dd 25 - *
  • %H HH 14 - *
  • %M mm 30 - *
  • %S ss 35 - *
  • %s SSS 123 - *
  • %Z Z -0400 - *
- *Others: - *
    - *
  • %T Long.toString(time) - *
  • %% % - *
- */ - public static String insertDate(String pattern, long time) { - DateFormat df = new SimpleDateFormat("yyyy.MM.dd:HH:mm:ss.SSS Z", Locale.US); - df.setTimeZone(TimeZone.getTimeZone("UTC")); - Date date = new Date(time); - String datetime = df.format(date); - StringBuilder result = new StringBuilder(); - int i=0; - while (i < pattern.length()) { - int j = pattern.indexOf('%',i); - if (j == -1 || j >= pattern.length()-1) { // done - result.append(pattern.substring(i)); // copy rest of pattern and quit - break; - } - result.append(pattern.substring(i, j)); - switch (pattern.charAt(j+1)) { - case 'Y': - result.append(datetime.substring(0,4)); // year - break; - case 'm': - result.append(datetime.substring(5,7)); // month - break; - case 'd': - result.append(datetime.substring(8,10)); // day of month - break; - case 'H': - result.append(datetime.substring(11,13)); // hour - break; - case 'M': - result.append(datetime.substring(14,16)); // minute - break; - case 'S': - result.append(datetime.substring(17,19)); // second - break; - case 's': - result.append(datetime.substring(20,23)); // thousanths - break; - case 'Z': - result.append(datetime.substring(24)); // time zone string - break; - case 'T': - result.append(Long.toString(time)); //time in Millis - break; - case 'x': - result.append(capitalize(dfMMM.format(date))); - break; - case '%': - result.append("%%"); - break; - default: - result.append("%"); // copy pattern escape and move on - j--; // only want to bump by one position.... - break; - } - i = j+2; - } - - return result.toString(); - } - - /** - * Private methods: timeStamp(LogRecord) - */ - private String timeStamp(LogRecord record) { - Date date = new Date(record.getMillis()); - String stamp; - switch (timestampFormat) { - case 0: - stamp = Long.toString(record.getMillis()); - break; - case 1: - stamp = ddMMMyyyy.format(date); - break; - case 2: - default: - stamp = yyyyMMdd.format(date); - break; - } - return stamp; - } - - /** Return the given string with the first letter in upper case */ - private static String capitalize(String string) { - if (Character.isUpperCase(string.charAt(0))) return string; - return Character.toUpperCase(string.charAt(0)) + string.substring(1); - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogWriter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogWriter.java deleted file mode 100644 index 15a983cfb43..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/LogWriter.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.container.logging; - -import java.io.IOException; -import java.io.OutputStream; - -interface LogWriter { - void write(LOGTYPE record, OutputStream outputStream) throws IOException; -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLog.java deleted file mode 100644 index 2090ba1b9f1..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLog.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -/** - * Access logging for requests - * - * @author bjorncs - */ -public interface RequestLog { - - void log(RequestLogEntry entry); - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogEntry.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogEntry.java deleted file mode 100644 index 819907fc9f1..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogEntry.java +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.yolean.trace.TraceNode; - -import java.security.Principal; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.OptionalLong; - -import static java.util.Objects.requireNonNull; - -/** - * A immutable request log entry - * - * @author bjorncs - */ -public class RequestLogEntry { - - private final String connectionId; - private final Instant timestamp; - private final Duration duration; - private final int localPort; - private final String peerAddress; - private final int peerPort; - private final String remoteAddress; - private final int remotePort; - private final String userAgent; - private final String referer; - private final String httpMethod; - private final String httpVersion; - private final String hostString; - private final int statusCode; - private final long contentSize; - private final String scheme; - private final String rawPath; - private final String rawQuery; - private final Principal userPrincipal; - private final Principal sslPrincipal; - private final HitCounts hitCounts; - private final TraceNode traceNode; - private final Map> extraAttributes; - - private RequestLogEntry(Builder builder) { - this.connectionId = builder.connectionId; - this.timestamp = builder.timestamp; - this.duration = builder.duration; - this.localPort = builder.localPort; - this.peerAddress = builder.peerAddress; - this.peerPort = builder.peerPort; - this.remoteAddress = builder.remoteAddress; - this.remotePort = builder.remotePort; - this.userAgent = builder.userAgent; - this.referer = builder.referer; - this.httpMethod = builder.httpMethod; - this.httpVersion = builder.httpVersion; - this.hostString = builder.hostString; - this.statusCode = builder.statusCode; - this.contentSize = builder.contentSize; - this.scheme = builder.scheme; - this.rawPath = builder.rawPath; - this.rawQuery = builder.rawQuery; - this.userPrincipal = builder.userPrincipal; - this.sslPrincipal = builder.sslPrincipal; - this.hitCounts = builder.hitCounts; - this.traceNode = builder.traceNode; - this.extraAttributes = copyExtraAttributes(builder.extraAttributes); - } - - public Optional connectionId() { return Optional.ofNullable(connectionId); } - public Optional timestamp() { return Optional.ofNullable(timestamp); } - public Optional duration() { return Optional.ofNullable(duration); } - public OptionalInt localPort() { return optionalInt(localPort); } - public Optional peerAddress() { return Optional.ofNullable(peerAddress); } - public OptionalInt peerPort() { return optionalInt(peerPort); } - public Optional remoteAddress() { return Optional.ofNullable(remoteAddress); } - public OptionalInt remotePort() { return optionalInt(remotePort); } - public Optional userAgent() { return Optional.ofNullable(userAgent); } - public Optional referer() { return Optional.ofNullable(referer); } - public Optional httpMethod() { return Optional.ofNullable(httpMethod); } - public Optional httpVersion() { return Optional.ofNullable(httpVersion); } - public Optional hostString() { return Optional.ofNullable(hostString); } - public OptionalInt statusCode() { return optionalInt(statusCode); } - public OptionalLong contentSize() { return optionalLong(contentSize); } - public Optional scheme() { return Optional.ofNullable(scheme); } - public Optional rawPath() { return Optional.ofNullable(rawPath); } - public Optional rawQuery() { return Optional.ofNullable(rawQuery); } - public Optional userPrincipal() { return Optional.ofNullable(userPrincipal); } - public Optional sslPrincipal() { return Optional.ofNullable(sslPrincipal); } - public Optional hitCounts() { return Optional.ofNullable(hitCounts); } - public Optional traceNode() { return Optional.ofNullable(traceNode); } - public Collection extraAttributeKeys() { return Collections.unmodifiableCollection(extraAttributes.keySet()); } - public Collection extraAttributeValues(String key) { return Collections.unmodifiableCollection(extraAttributes.get(key)); } - - private static OptionalInt optionalInt(int value) { - if (value == -1) return OptionalInt.empty(); - return OptionalInt.of(value); - } - - private static OptionalLong optionalLong(long value) { - if (value == -1) return OptionalLong.empty(); - return OptionalLong.of(value); - } - - private static Map> copyExtraAttributes(Map> extraAttributes) { - Map> copy = new HashMap<>(); - extraAttributes.forEach((key, value) -> copy.put(key, new ArrayList<>(value))); - return copy; - } - - public static class Builder { - - private String connectionId; - private Instant timestamp; - private Duration duration; - private int localPort = -1; - private String peerAddress; - private int peerPort = -1; - private String remoteAddress; - private int remotePort = -1; - private String userAgent; - private String referer; - private String httpMethod; - private String httpVersion; - private String hostString; - private int statusCode = -1; - private long contentSize = -1; - private String scheme; - private String rawPath; - private String rawQuery; - private Principal userPrincipal; - private HitCounts hitCounts; - private TraceNode traceNode; - private Principal sslPrincipal; - private final Map> extraAttributes = new HashMap<>(); - - public Builder connectionId(String connectionId) { this.connectionId = requireNonNull(connectionId); return this; } - public Builder timestamp(Instant timestamp) { this.timestamp = requireNonNull(timestamp); return this; } - public Builder duration(Duration duration) { this.duration = requireNonNull(duration); return this; } - public Builder localPort(int localPort) { this.localPort = requireNonNegative(localPort); return this; } - public Builder peerAddress(String peerAddress) { this.peerAddress = requireNonNull(peerAddress); return this; } - public Builder peerPort(int peerPort) { this.peerPort = requireNonNegative(peerPort); return this; } - public Builder remoteAddress(String remoteAddress) { this.remoteAddress = requireNonNull(remoteAddress); return this; } - public Builder remotePort(int remotePort) { this.remotePort = requireNonNegative(remotePort); return this; } - public Builder userAgent(String userAgent) { this.userAgent = requireNonNull(userAgent); return this; } - public Builder referer(String referer) { this.referer = requireNonNull(referer); return this; } - public Builder httpMethod(String httpMethod) { this.httpMethod = requireNonNull(httpMethod); return this; } - public Builder httpVersion(String httpVersion) { this.httpVersion = requireNonNull(httpVersion); return this; } - public Builder hostString(String hostString) { this.hostString = requireNonNull(hostString); return this; } - public Builder statusCode(int statusCode) { this.statusCode = requireNonNegative(statusCode); return this; } - public Builder contentSize(long contentSize) { this.contentSize = requireNonNegative(contentSize); return this; } - public Builder scheme(String scheme) { this.scheme = requireNonNull(scheme); return this; } - public Builder rawPath(String rawPath) { this.rawPath = requireNonNull(rawPath); return this; } - public Builder rawQuery(String rawQuery) { this.rawQuery = requireNonNull(rawQuery); return this; } - public Builder userPrincipal(Principal userPrincipal) { this.userPrincipal = requireNonNull(userPrincipal); return this; } - public Builder sslPrincipal(Principal sslPrincipal) { this.sslPrincipal = requireNonNull(sslPrincipal); return this; } - public Builder hitCounts(HitCounts hitCounts) { this.hitCounts = requireNonNull(hitCounts); return this; } - public Builder traceNode(TraceNode traceNode) { this.traceNode = requireNonNull(traceNode); return this; } - public Builder addExtraAttribute(String key, String value) { - this.extraAttributes.computeIfAbsent(requireNonNull(key), __ -> new ArrayList<>()).add(requireNonNull(value)); - return this; - } - public Builder addExtraAttributes(String key, Collection values) { - this.extraAttributes.computeIfAbsent(requireNonNull(key), __ -> new ArrayList<>()).addAll(requireNonNull(values)); - return this; - } - public RequestLogEntry build() { return new RequestLogEntry(this); } - - private static int requireNonNegative(int value) { - if (value < 0) throw new IllegalArgumentException("Value must be non-negative: " + value); - return value; - } - - private static long requireNonNegative(long value) { - if (value < 0) throw new IllegalArgumentException("Value must be non-negative: " + value); - return value; - } - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogHandler.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogHandler.java deleted file mode 100644 index 85df08e4abb..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/RequestLogHandler.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -/** - * @author Tony Vaagenes - */ -public interface RequestLogHandler { - void log(RequestLogEntry entry); -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/TraceRenderer.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/TraceRenderer.java deleted file mode 100644 index 41b88e08c19..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/TraceRenderer.java +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.data.access.Inspectable; -import com.yahoo.data.access.Inspector; -import com.yahoo.data.access.simple.JsonRender; -import com.yahoo.yolean.trace.TraceNode; -import com.yahoo.yolean.trace.TraceVisitor; -import com.fasterxml.jackson.core.JsonGenerator; - -import java.io.IOException; - -public class TraceRenderer extends TraceVisitor { - private static final String TRACE_CHILDREN = "children"; - private static final String TRACE_MESSAGE = "message"; - private static final String TRACE_TIMESTAMP = "timestamp"; - private static final String TRACE = "trace"; - - private final long basetime; - private final JsonGenerator generator; - private final FieldConsumer fieldConsumer; - private boolean hasFieldName = false; - int emittedChildNesting = 0; - int currentChildNesting = 0; - private boolean insideOpenObject = false; - - public interface FieldConsumer { - void accept(Object object) throws IOException; - } - - private static class Consumer implements FieldConsumer { - private final JsonGenerator generator; - - Consumer(JsonGenerator generator) { - this.generator = generator; - } - - @Override - public void accept(Object object) throws IOException { - if (object instanceof Inspectable) { - renderInspectorDirect(((Inspectable) object).inspect()); - } else { - generator.writeObject(object); - } - } - private void renderInspectorDirect(Inspector data) throws IOException { - StringBuilder intermediate = new StringBuilder(); - JsonRender.render(data, intermediate, true); - generator.writeRawValue(intermediate.toString()); - } - } - - TraceRenderer(JsonGenerator generator, long basetime) { - this(generator, new Consumer(generator), basetime); - } - public TraceRenderer(JsonGenerator generator, FieldConsumer consumer, long basetime) { - this.generator = generator; - this.fieldConsumer = consumer; - this.basetime = basetime; - } - - @Override - public void entering(TraceNode node) { - ++currentChildNesting; - } - - @Override - public void leaving(TraceNode node) { - conditionalEndObject(); - if (currentChildNesting == emittedChildNesting) { - try { - generator.writeEndArray(); - generator.writeEndObject(); - } catch (IOException e) { - throw new TraceRenderWrapper(e); - } - --emittedChildNesting; - } - --currentChildNesting; - } - - @Override - public void visit(TraceNode node) { - try { - doVisit(node.timestamp(), node.payload(), node.children().iterator().hasNext()); - } catch (IOException e) { - throw new TraceRenderWrapper(e); - } - } - - private void doVisit(long timestamp, Object payload, boolean hasChildren) throws IOException { - boolean dirty = false; - if (timestamp != 0L) { - header(); - generator.writeStartObject(); - generator.writeNumberField(TRACE_TIMESTAMP, timestamp - basetime); - dirty = true; - } - if (payload != null) { - if (!dirty) { - header(); - generator.writeStartObject(); - } - generator.writeFieldName(TRACE_MESSAGE); - fieldConsumer.accept(payload); - dirty = true; - } - if (dirty) { - if (!hasChildren) { - generator.writeEndObject(); - } else { - setInsideOpenObject(true); - } - } - } - private void header() { - fieldName(); - for (int i = 0; i < (currentChildNesting - emittedChildNesting); ++i) { - startChildArray(); - } - emittedChildNesting = currentChildNesting; - } - - private void startChildArray() { - try { - conditionalStartObject(); - generator.writeArrayFieldStart(TRACE_CHILDREN); - } catch (IOException e) { - throw new TraceRenderWrapper(e); - } - } - - private void conditionalStartObject() throws IOException { - if (!isInsideOpenObject()) { - generator.writeStartObject(); - } else { - setInsideOpenObject(false); - } - } - - private void conditionalEndObject() { - if (isInsideOpenObject()) { - // This triggers if we were inside a data node with payload and - // subnodes, but none of the subnodes contained data - try { - generator.writeEndObject(); - setInsideOpenObject(false); - } catch (IOException e) { - throw new TraceRenderWrapper(e); - } - } - } - - private void fieldName() { - if (hasFieldName) { - return; - } - - try { - generator.writeFieldName(TRACE); - } catch (IOException e) { - throw new TraceRenderWrapper(e); - } - hasFieldName = true; - } - - boolean isInsideOpenObject() { - return insideOpenObject; - } - - void setInsideOpenObject(boolean insideOpenObject) { - this.insideOpenObject = insideOpenObject; - } - public static final class TraceRenderWrapper extends RuntimeException { - - /** - * Should never be serialized, but this is still needed. - */ - private static final long serialVersionUID = 2L; - - TraceRenderWrapper(IOException wrapped) { - super(wrapped); - } - - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/VespaAccessLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/VespaAccessLog.java deleted file mode 100644 index 254b7fe5385..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/VespaAccessLog.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.component.AbstractComponent; -import com.yahoo.container.core.AccessLogConfig; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -/** - * @author Bjorn Borud - * @author Oyvind Bakksjo - */ -public final class VespaAccessLog extends AbstractComponent implements RequestLogHandler, LogWriter { - - private static final ThreadLocal dateFormat = ThreadLocal.withInitial(VespaAccessLog::createDateFormat); - - private final AccessLogHandler logHandler; - - public VespaAccessLog(AccessLogConfig config) { - logHandler = new AccessLogHandler(config.fileHandler(), this); - } - - private static SimpleDateFormat createDateFormat() { - SimpleDateFormat format = new SimpleDateFormat("[dd/MMM/yyyy:HH:mm:ss Z]"); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format; - } - - private static String getDate() { - return dateFormat.get().format(new Date()); - } - - private String getRequest(final String httpMethod, final String rawPath, final String rawQuery, final String httpVersion) { - return httpMethod + " " + (rawQuery != null ? rawPath + "?" + rawQuery : rawPath) + " " + httpVersion; - } - - private String getUser(String user) { - return (user == null) ? "-" : user; - } - - private String toLogline(String ipAddr, String user, String request, String referer, String agent, - long durationMillis, long byteCount, HitCounts hitcounts, int returnCode) - { - long ms = Math.max(0L, durationMillis); - StringBuilder sb = new StringBuilder() - .append(ipAddr) - .append(" - ") - .append(getUser(user)) - .append(' ') - .append(getDate()) - .append(" \"") - .append(request) - .append("\" ") - .append(returnCode) - .append(' ') - .append(byteCount) - .append(" \"") - .append(referer) - .append("\" \"") - .append(agent) - .append("\" ") - .append(ms/1000) - .append('.'); - decimalsOfSecondsFromMilliseconds(ms, sb); - sb.append(' ') - .append((hitcounts == null) ? 0 : hitcounts.getTotalHitCount()) - .append(" 0.0 ") - .append((hitcounts == null) ? 0 : hitcounts.getSummaryCount()); - return sb.toString(); - } - - private void decimalsOfSecondsFromMilliseconds(long ms, StringBuilder sb) { - long dec = ms % 1000; - String numbers = String.valueOf(dec); - if (dec <= 9) { - sb.append("00"); - } else if (dec <= 99) { - sb.append('0'); - } - sb.append(numbers); - } - - @Override public void deconstruct() { logHandler.shutdown(); } - - @Override - public void log(RequestLogEntry entry) { - logHandler.log(entry); - } - - @Override - public void write(RequestLogEntry entry, OutputStream outputStream) throws IOException { - outputStream.write( - toLogline( - entry.peerAddress().get(), - null, - getRequest( - entry.httpMethod().orElse(null), - entry.rawPath().orElse(null), - entry.rawQuery().orElse(null), - entry.httpVersion().orElse(null)), - entry.referer().orElse(null), - entry.userAgent().orElse(null), - entry.duration().get().toMillis(), - entry.contentSize().orElse(0L), - entry.hitCounts().orElse(null), - entry.statusCode().orElse(0)).getBytes(StandardCharsets.UTF_8)); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/package-info.java deleted file mode 100644 index fc2abb7b609..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -@ExportPackage -package com.yahoo.container.logging; - -import com.yahoo.osgi.annotation.ExportPackage; 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 deleted file mode 100644 index 3a63726b951..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/CertificateStore.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -/** - * 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 deleted file mode 100644 index d882cf7a34a..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/Cookie.java +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import org.eclipse.jetty.http.HttpCookie; -import org.eclipse.jetty.server.CookieCutter; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import static java.util.stream.Collectors.toList; - -/** - * A RFC 6265 compliant cookie. - * - * Note: RFC 2109 and RFC 2965 is no longer supported. All fields that are not part of RFC 6265 are deprecated. - * - * @author Einar M R Rosenvinge - * @author bjorncs - */ -public class Cookie { - - private final Set ports = new HashSet<>(); - private String name; - private String value; - private String domain; - private String path; - private SameSite sameSite; - private long maxAgeSeconds = Integer.MIN_VALUE; - 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; - sameSite = cookie.sameSite; - maxAgeSeconds = cookie.maxAgeSeconds; - 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 SameSite getSameSite() { - return sameSite; - } - - public Cookie setSameSite(SameSite sameSite) { - this.sameSite = sameSite; - return this; - } - - public int getMaxAge(TimeUnit unit) { - return (int)unit.convert(maxAgeSeconds, TimeUnit.SECONDS); - } - - public Cookie setMaxAge(int maxAge, TimeUnit unit) { - this.maxAgeSeconds = maxAge >= 0 ? unit.toSeconds(maxAge) : Integer.MIN_VALUE; - 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; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Cookie cookie = (Cookie) o; - return maxAgeSeconds == cookie.maxAgeSeconds && - secure == cookie.secure && - httpOnly == cookie.httpOnly && - discard == cookie.discard && - sameSite == cookie.sameSite && - Objects.equals(ports, cookie.ports) && - Objects.equals(name, cookie.name) && - Objects.equals(value, cookie.value) && - Objects.equals(domain, cookie.domain) && - Objects.equals(path, cookie.path); - } - - @Override - public int hashCode() { - return Objects.hash(ports, name, value, domain, path, sameSite, maxAgeSeconds, secure, httpOnly, discard); - } - - @Override - public String toString() { - StringBuilder ret = new StringBuilder(); - ret.append(name).append("=").append(value); - return ret.toString(); - } - // NOTE cookie encoding and decoding: - // The implementation uses Jetty for server-side (encoding of Set-Cookie and decoding of Cookie header), - // and java.net.HttpCookie for client-side (encoding of Cookie and decoding of Set-Cookie header). - // - // Implementation is RFC-6265 compliant. - - public static String toCookieHeader(Iterable cookies) { - return StreamSupport.stream(cookies.spliterator(), false) - .map(cookie -> { - java.net.HttpCookie httpCookie = new java.net.HttpCookie(cookie.getName(), cookie.getValue()); - httpCookie.setDomain(cookie.getDomain()); - httpCookie.setHttpOnly(cookie.isHttpOnly()); - httpCookie.setMaxAge(cookie.getMaxAge(TimeUnit.SECONDS)); - httpCookie.setPath(cookie.getPath()); - httpCookie.setSecure(cookie.isSecure()); - httpCookie.setVersion(0); - return httpCookie.toString(); - }) - .collect(Collectors.joining(";")); - } - - public static List fromCookieHeader(String headerVal) { - CookieCutter cookieCutter = new CookieCutter(); - cookieCutter.addCookieField(headerVal); - return Arrays.stream(cookieCutter.getCookies()) - .map(servletCookie -> { - Cookie cookie = new Cookie(); - cookie.setName(servletCookie.getName()); - cookie.setValue(servletCookie.getValue()); - cookie.setPath(servletCookie.getPath()); - cookie.setDomain(servletCookie.getDomain()); - cookie.setMaxAge(servletCookie.getMaxAge(), TimeUnit.SECONDS); - cookie.setSecure(servletCookie.getSecure()); - cookie.setHttpOnly(servletCookie.isHttpOnly()); - return cookie; - }) - .collect(toList()); - } - - public static List toSetCookieHeaders(Iterable cookies) { - return StreamSupport.stream(cookies.spliterator(), false) - .map(cookie -> - new org.eclipse.jetty.http.HttpCookie( - cookie.getName(), - cookie.getValue(), - cookie.getDomain(), - cookie.getPath(), - cookie.getMaxAge(TimeUnit.SECONDS), - cookie.isHttpOnly(), - cookie.isSecure(), - null, /* comment */ - 0, /* version */ - Optional.ofNullable(cookie.getSameSite()).map(SameSite::jettySameSite).orElse(null) - ).getRFC6265SetCookie()) - .collect(toList()); - } - - @Deprecated // TODO Vespa 8 Remove - public static List toSetCookieHeaderAll(Iterable cookies) { - return toSetCookieHeaders(cookies); - } - - public static Cookie fromSetCookieHeader(String headerVal) { - return java.net.HttpCookie.parse(headerVal).stream() - .map(httpCookie -> { - Cookie cookie = new Cookie(); - cookie.setName(httpCookie.getName()); - cookie.setValue(httpCookie.getValue()); - cookie.setDomain(httpCookie.getDomain()); - cookie.setHttpOnly(httpCookie.isHttpOnly()); - cookie.setMaxAge((int) httpCookie.getMaxAge(), TimeUnit.SECONDS); - cookie.setPath(httpCookie.getPath()); - cookie.setSecure(httpCookie.getSecure()); - return cookie; - }) - .findFirst().get(); - } - - public enum SameSite { - NONE, STRICT, LAX; - - HttpCookie.SameSite jettySameSite() { - return HttpCookie.SameSite.valueOf(name()); - } - - static SameSite fromJettySameSite(HttpCookie.SameSite jettySameSite) { - return valueOf(jettySameSite.name()); - } - } -} 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 deleted file mode 100644 index 039966133e8..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpHeaders.java +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -/** - * @author Anirudha Khanna - */ -@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_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 deleted file mode 100644 index 118c34245c0..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpRequest.java +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import com.yahoo.jdisc.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.eclipse.jetty.http.HttpURI; -import org.eclipse.jetty.util.MultiMap; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.URI; -import java.security.Principal; -import java.util.ArrayList; -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 Anirudha Khanna - * @author Einar M R Rosenvinge - */ -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> parameters = new HashMap<>(); - private Principal principal; - 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(getUriQueryParameters(uri)); - 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(getUriQueryParameters(uri)); - this.connectedAt = creationTime(TimeUnit.MILLISECONDS); - } catch (RuntimeException e) { - release(); - throw e; - } - } - - private static Map> getUriQueryParameters(URI uri) { - MultiMap queryParameters = new MultiMap<>(); - new HttpURI(uri).decodeQueryTo(queryParameters); - - // Do a deep copy so we do not leak Jetty classes outside - Map> deepCopiedQueryParameters = new HashMap<>(); - for (Map.Entry> entry : queryParameters.entrySet()) { - deepCopiedQueryParameters.put(entry.getKey(), new ArrayList<>(entry.getValue())); - } - return deepCopiedQueryParameters; - } - - public Method getMethod() { - return method; - } - - public void setMethod(Method method) { - this.method = method; - } - - public Version getVersion() { - return version; - } - - /** Returns the remote address, or null if unresolved */ - @Override - public String getRemoteHostAddress() { - if (remoteAddress instanceof InetSocketAddress) { - InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress(); - if (remoteInetAddress == null) - return null; - return remoteInetAddress.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; - } - - /** - *

For server requests, this returns the timestamp of when the underlying HTTP channel was connected. - * - *

For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.

- * - * @param unit the unit to return the time in - * @return the timestamp of when the underlying HTTP channel was connected, or request creation time - */ - @Override - 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); - } - - /** - *

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 client configuration.

- * - *

NOTE: 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 client.

- * - * @param timeout The allocated amount of time. - * @param unit The time unit of the timeout argument. - */ - public void setConnectionTimeout(long timeout, TimeUnit unit) { - this.connectionTimeout = unit.toMillis(timeout); - } - - public Map> parameters() { - return parameters; - } - - @Override - public void copyHeaders(HeaderFields target) { - target.addAll(headers()); - } - - public List decodeCookieHeader() { - List cookies = headers().get(HttpHeaders.Names.COOKIE); - if (cookies == null) { - return Collections.emptyList(); - } - List ret = new LinkedList<>(); - for (String cookie : cookies) { - ret.addAll(Cookie.fromCookieHeader(cookie)); - } - return ret; - } - - public void encodeCookieHeader(List cookies) { - headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies)); - } - - /** - *

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.

- * - *

NOTE: These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to - * prevent concurrency issues such as ConcurrentModificationExceptions.

- * - * @return The trailer headers of this HttpRequest. - */ - public HeaderFields trailers() { - return trailers; - } - - /** - * Returns whether this request was explicitly chunked from the client. 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 Principal getUserPrincipal() { - return principal; - } - - public void setUserPrincipal(Principal principal) { - this.principal = principal; - } - - 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); - } - - 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); - } - - 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 deleted file mode 100644 index f7138ba0e2b..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/HttpResponse.java +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import com.yahoo.jdisc.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 java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -/** - * A HTTP response. - * - * @author Einar M R Rosenvinge - */ -public class HttpResponse extends Response implements ServletOrJdiscHttpResponse { - - private final HeaderFields trailers = new HeaderFields(); - private boolean chunkedEncodingEnabled = true; - private String message; - - 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; - } - - 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 decodeSetCookieHeader() { - List cookies = headers().get(HttpHeaders.Names.SET_COOKIE); - if (cookies == null) { - return Collections.emptyList(); - } - List ret = new LinkedList<>(); - for (String cookie : cookies) { - ret.add(Cookie.fromSetCookieHeader(cookie)); - } - return ret; - } - - public void encodeSetCookieHeader(List cookies) { - headers().remove(HttpHeaders.Names.SET_COOKIE); - for (Cookie cookie : cookies) { - headers().add(HttpHeaders.Names.SET_COOKIE, Cookie.toSetCookieHeaders(Arrays.asList(cookie))); - } - } - - /** - *

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.

- * - *

NOTE: These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to - * prevent concurrency issues such as ConcurrentModificationExceptions.

- * - * @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 deleted file mode 100644 index 4f739c5bd78..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/SecretStore.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -/** - * An abstraction of a secret store for e.g passwords. - * Implementations can be plugged in to provide passwords for various keys. - * - * @author bratseth - * @author bjorncs - * @deprecated Use com.yahoo.container.jdisc.secretstore.SecretStore - */ -@Deprecated // Vespa 8 -public interface SecretStore { - - /** Returns the secret for this key */ - String getSecret(String key); - - /** Returns the secret for this key and version */ - default String getSecret(String key, int version) { - throw new UnsupportedOperationException("SecretStore implementation does not support versioned secrets"); - } - -} 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 deleted file mode 100644 index 43da1a82077..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/cloud/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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/filter/DiscFilterRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java deleted file mode 100644 index f7ab399574c..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java +++ /dev/null @@ -1,543 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.HeaderFields; -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.HttpRequest.Version; -import com.yahoo.jdisc.http.servlet.ServletOrJdiscHttpRequest; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.security.Principal; -import java.security.cert.X509Certificate; -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.concurrent.TimeUnit; -import java.util.regex.Pattern; - -/** - * The Request class on which all filters will operate upon. - * 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 Map> untreatedParams; - private final HeaderFields untreatedHeaders; - private List untreatedCookies = 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()); - } - - 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() { - InetSocketAddress localAddress = localAddress(); - if (localAddress.getAddress() == null) return null; - return localAddress.getAddress().getHostAddress(); - } - - private InetSocketAddress localAddress() { - int port = parent.getUri().getPort(); - if (port < 0) - port = 0; - return new InetSocketAddress(parent.getUri().getHost(), port); - } - - public Enumeration 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 getParameterNames(); - - public List getParameterNamesAsList() { - return new ArrayList(parent.parameters().keySet()); - } - - public Enumeration getParameterValues(String name) { - return Collections.enumeration(parent.parameters().get(name)); - } - - public List getParameterValuesAsList(String name) { - return parent.parameters().get(name); - } - - public Map> 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> getUntreatedParams() { - return Collections.unmodifiableMap(untreatedParams); - } - - - /** - * Returns the untreatedHeaders from - * parent request - */ - public HeaderFields getUntreatedHeaders() { - return untreatedHeaders; - } - - /** - * Returns the untreatedCookies from - * parent request - */ - public List 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 getHeaderNames(); - - public abstract List getHeaderNamesAsList(); - - public abstract Enumeration getHeaders(String name); - - public abstract List 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 values); - - public int getIntHeader(String name) { - String value = getHeader(name); - if (value == null) { - return -1; - } else { - return Integer.parseInt(value); - } - } - - - public List getCookies() { - return parent.decodeCookieHeader(); - } - - public void setCookies(List cookies) { - parent.encodeCookieHeader(cookies); - } - - public long getConnectedAt(TimeUnit unit) { - return parent.getConnectedAt(unit); - } - - 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 abstract Principal getUserPrincipal(); - - 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 abstract void setUserPrincipal(Principal principal); - - /** - * @return The client certificate chain in ascending order of trust. The first certificate is the one sent from the client. - * Returns an empty list if the client did not provide a certificate. - */ - public abstract List getClientCertificateChain(); - - 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 cookies = new ArrayList<>(); - // Get current set of cookies first - List c = getCookies(); - if (c != null && !c.isEmpty()) { - cookies.addAll(c); - } - cookies.add(cookie.getCookie()); - setCookies(cookies); - } - } - - public abstract void clearCookies(); - - public JDiscCookieWrapper[] getWrappedCookies() { - List cookies = getCookies(); - if (cookies == null) { - return null; - } - List 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 { - - 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 deleted file mode 100644 index 4e8b779c516..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterResponse.java +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import 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 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 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 getUntreatedCookies() { - return 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 setHeader(String name, String value); - - 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 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 getCookies() { - return parent.decodeSetCookieHeader(); - } - - public abstract void setCookies(List 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 cookies = new ArrayList<>(); - //Get current set of cookies first - List 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 deleted file mode 100644 index af9e2b5e99a..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/FilterConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import 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 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 deleted file mode 100644 index 2b9c650d545..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapper.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.Cookie; - -import java.util.concurrent.TimeUnit; - -/** - * Wrapper of Cookie. - * - * @author Tejal Knot - * - */ -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 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 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); - } - - /** - * 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 deleted file mode 100644 index f8d9e6b2642..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterRequest.java +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.servlet.ServletRequest; - -import java.net.URI; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.Optional; - -/** - * 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 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 values = parent.headers().get(name); - if (values == null || values.isEmpty()) { - return null; - } - return values.get(values.size() - 1); - } - - public Enumeration getHeaderNames() { - return Collections.enumeration(parent.headers().keySet()); - } - - public List getHeaderNamesAsList() { - return new ArrayList(parent.headers().keySet()); - } - - @Override - public Enumeration getHeaders(String name) { - return Collections.enumeration(getHeadersAsList(name)); - } - - public List getHeadersAsList(String name) { - List values = parent.headers().get(name); - if(values == null) { - return Collections.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 values) { - parent.headers().put(name, values); - } - - @Override - public Principal getUserPrincipal() { - return parent.getUserPrincipal(); - } - - @Override - public void setUserPrincipal(Principal principal) { - this.parent.setUserPrincipal(principal); - } - - @Override - public List getClientCertificateChain() { - return Optional.ofNullable(parent.context().get(ServletRequest.JDISC_REQUEST_X509CERT)) - .map(X509Certificate[].class::cast) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - } - - @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 deleted file mode 100644 index ff81359f93c..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/JdiscFilterResponse.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.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 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 values = parent.headers().get(name); - if (values == null || values.isEmpty()) { - return null; - } - return values.get(values.size() - 1); - } - - @Override - public void setCookies(List 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 deleted file mode 100644 index 977e3ab5d1d..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilter.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.handler.ResponseHandler; -import com.yahoo.jdisc.http.HttpRequest; - -/** - * @author Einar M R Rosenvinge - */ -public interface RequestFilter extends com.yahoo.jdisc.SharedResource, RequestFilterBase { - - 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 deleted file mode 100644 index 4eb7091f378..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestFilterBase.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -/** - * @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 deleted file mode 100644 index e5e7ae1ef56..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/RequestView.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.HttpRequest.Method; - -import java.net.URI; -import java.util.List; -import java.util.Optional; - -/** - * Read-only view of the request for use by SecurityResponseFilters. - * - * @author Tony Vaagenes - */ -public interface RequestView { - - /** - * Returns a named attribute. - * - * @see javax.servlet.ServletRequest.getAttribute(java.lang.String) - * @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) - */ - Object getAttribute(String name); - - /** - * Returns an immutable view of all values of a named header field. - * Returns an empty list if no such header is present. - */ - List getHeaders(String name); - - /** - * Convenience method for retrieving the first value of a named header field. - * Returns empty if the header is not set, or if the value list is empty. - */ - Optional getFirstHeader(String name); - - /** - * Returns the Http method. Only present if the underlying request has http-like semantics. - */ - Optional getMethod(); - - 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 deleted file mode 100644 index 44fe7d9fcf1..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilter.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.SharedResource; - -/** - * @author Einar M R Rosenvinge - */ -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 deleted file mode 100644 index b869c882351..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ResponseFilterBase.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -/** - * @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 deleted file mode 100644 index cbed273b7ee..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityFilterInvoker.java +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.google.common.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.Collections; -import java.util.List; -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 Tony Vaagenes - */ -@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 Optional cast(Class 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 List getHeaders(String name) { - return Collections.unmodifiableList(Collections.list(request.getHeaders(name))); - } - - @Override - public Optional getFirstHeader(String name) { - return getHeaders(name).stream().findFirst(); - } - - @Override - public Optional 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 deleted file mode 100644 index e6f4add49de..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilter.java +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.handler.ResponseHandler; - -/** - * @author Simon Thoresen Hult - */ -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 deleted file mode 100644 index 2d97bbdc494..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChain.java +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.AbstractResource; -import com.yahoo.jdisc.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 filters = new ArrayList<>(); - - private SecurityRequestFilterChain(Iterable 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 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 view of the filters in this */ - public List 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 deleted file mode 100644 index aa4f7d29b89..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilter.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -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 deleted file mode 100644 index d45b406a375..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChain.java +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import 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 filters = new ArrayList<>(); - - private SecurityResponseFilterChain(Iterable 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 filters) { - return new SecurityResponseFilterChain(filters); - } - - /** Returns an unmodifiable view of the filters in this */ - public List getFilters() { - return Collections.unmodifiableList(filters); - } - - static class RequestViewImpl implements RequestView { - - private final Request request; - private final Optional 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 List getHeaders(String name) { - List headers = request.headers().get(name); - return headers == null ? Collections.emptyList() : Collections.unmodifiableList(headers); - } - - @Override - public Optional getFirstHeader(String name) { - return getHeaders(name).stream().findFirst(); - } - - @Override - public Optional 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 deleted file mode 100644 index f06f9e256ff..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterRequest.java +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.servlet.ServletRequest; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * Servlet implementation for JDisc filter requests. - */ -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 getAttributeNames() { - Set 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 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 getHeaderNames() { - return parent.getHeaderNames(); - } - - public List getHeaderNamesAsList() { - return Collections.list(getHeaderNames()); - } - - @Override - public Enumeration getHeaders(String name) { - return parent.getHeaders(name); - } - - @Override - public List 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 values) { - parent.setHeaders(name, values); - } - - @Override - public Principal getUserPrincipal() { - return parent.getUserPrincipal(); - } - - @Override - public void setUserPrincipal(Principal principal) { - parent.setUserPrincipal(principal); - } - - @Override - public List getClientCertificateChain() { - return Optional.ofNullable(parent.getRequest().getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT)) - .map(X509Certificate[].class::cast) - .map(Arrays::asList) - .orElse(Collections.emptyList()); - } - - @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 deleted file mode 100644 index b603e7776f1..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/ServletFilterResponse.java +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.google.common.collect.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.Collection; -import java.util.List; - -/** - * Servlet implementation for JDisc filter responses. - */ -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 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 headers = parent.getHeaders(name); - return headers.isEmpty() - ? null - : Iterables.getLast(headers); - } - - @Override - public void setCookies(List cookies) { - removeHeaders(HttpHeaders.Names.SET_COOKIE); - List setCookieHeaders = Cookie.toSetCookieHeaders(cookies); - setCookieHeaders.forEach(cookie -> addHeader(HttpHeaders.Names.SET_COOKIE, 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 deleted file mode 100644 index e1834fd8b7d..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyRequestFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter.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 Einar M R Rosenvinge - */ -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 deleted file mode 100644 index 5ce3f6a496f..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/EmptyResponseFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter.chain; - -import com.yahoo.jdisc.NoopSharedResource; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.http.filter.ResponseFilter; - -/** - * @author Einar M R Rosenvinge - */ -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 deleted file mode 100644 index 85f71777cf3..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/RequestFilterChain.java +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter.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 Einar M R Rosenvinge - */ -public final class RequestFilterChain extends AbstractResource implements RequestFilter { - - private final List filters = new ArrayList<>(); - private final ResourcePool filterReferences = new ResourcePool(); - - private RequestFilterChain(Iterable 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 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 deleted file mode 100644 index 5c5eda1f139..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseFilterChain.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter.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 Simon Thoresen Hult - */ -public final class ResponseFilterChain extends AbstractResource implements ResponseFilter { - - private final List filters = new ArrayList<>(); - private final ResourcePool filterReferences = new ResourcePool(); - - private ResponseFilterChain(Iterable 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 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 deleted file mode 100644 index 02600683e27..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/ResponseHandlerGuard.java +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter.chain; - -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.handler.ContentChannel; -import com.yahoo.jdisc.handler.ResponseHandler; - -/** - * @author Simon Thoresen Hult - */ -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 deleted file mode 100644 index 540a1be7b73..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/chain/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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 deleted file mode 100644 index e97d447adbb..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/filter/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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 deleted file mode 100644 index b8bd76483cf..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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/jetty/AccessLogRequestLog.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java deleted file mode 100644 index 4de5e5e5387..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLog.java +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.common.base.Objects; -import com.yahoo.container.logging.AccessLog; -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.container.logging.RequestLog; -import com.yahoo.container.logging.RequestLogEntry; -import com.yahoo.jdisc.http.ServerConfig; -import com.yahoo.jdisc.http.servlet.ServletRequest; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.eclipse.jetty.util.component.AbstractLifeCycle; - -import javax.servlet.http.HttpServletRequest; -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.OptionalInt; -import java.util.UUID; -import java.util.function.BiConsumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; - -/** - * 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 Oyvind Bakksjo - * @author bjorncs - */ -class AccessLogRequestLog extends AbstractLifeCycle implements org.eclipse.jetty.server.RequestLog { - - private static final Logger logger = Logger.getLogger(AccessLogRequestLog.class.getName()); - - // HTTP headers that are logged as extra key-value-pairs in access log entries - private static final List LOGGED_REQUEST_HEADERS = List.of("Vespa-Client-Version"); - - private final RequestLog requestLog; - private final List remoteAddressHeaders; - private final List remotePortHeaders; - - AccessLogRequestLog(RequestLog requestLog, ServerConfig.AccessLog config) { - this.requestLog = requestLog; - this.remoteAddressHeaders = config.remoteAddressHeaders(); - this.remotePortHeaders = config.remotePortHeaders(); - } - - @Override - public void log(Request request, Response response) { - try { - RequestLogEntry.Builder builder = new RequestLogEntry.Builder(); - - String peerAddress = request.getRemoteAddr(); - int peerPort = request.getRemotePort(); - long startTime = request.getTimeStamp(); - long endTime = System.currentTimeMillis(); - builder.peerAddress(peerAddress) - .peerPort(peerPort) - .localPort(getLocalPort(request)) - .timestamp(Instant.ofEpochMilli(startTime)) - .duration(Duration.ofMillis(Math.max(0, endTime - startTime))) - .contentSize(response.getHttpChannel().getBytesWritten()) - .statusCode(response.getCommittedMetaData().getStatus()); - - addNonNullValue(builder, request.getMethod(), RequestLogEntry.Builder::httpMethod); - addNonNullValue(builder, request.getRequestURI(), RequestLogEntry.Builder::rawPath); - addNonNullValue(builder, request.getProtocol(), RequestLogEntry.Builder::httpVersion); - addNonNullValue(builder, request.getScheme(), RequestLogEntry.Builder::scheme); - addNonNullValue(builder, request.getHeader("User-Agent"), RequestLogEntry.Builder::userAgent); - addNonNullValue(builder, request.getHeader("Host"), RequestLogEntry.Builder::hostString); - addNonNullValue(builder, request.getHeader("Referer"), RequestLogEntry.Builder::referer); - addNonNullValue(builder, request.getQueryString(), RequestLogEntry.Builder::rawQuery); - - Principal principal = (Principal) request.getAttribute(ServletRequest.JDISC_REQUEST_PRINCIPAL); - addNonNullValue(builder, principal, RequestLogEntry.Builder::userPrincipal); - - String requestFilterId = (String) request.getAttribute(ServletRequest.JDISC_REQUEST_CHAIN); - addNonNullValue(builder, requestFilterId, (b, chain) -> b.addExtraAttribute("request-chain", chain)); - - String responseFilterId = (String) request.getAttribute(ServletRequest.JDISC_RESPONSE_CHAIN); - addNonNullValue(builder, responseFilterId, (b, chain) -> b.addExtraAttribute("response-chain", chain)); - - UUID connectionId = (UUID) request.getAttribute(JettyConnectionLogger.CONNECTION_ID_REQUEST_ATTRIBUTE); - addNonNullValue(builder, connectionId, (b, uuid) -> b.connectionId(uuid.toString())); - - String remoteAddress = getRemoteAddress(request); - if (!Objects.equal(remoteAddress, peerAddress)) { - builder.remoteAddress(remoteAddress); - } - int remotePort = getRemotePort(request); - if (remotePort != peerPort) { - builder.remotePort(remotePort); - } - LOGGED_REQUEST_HEADERS.forEach(header -> { - String value = request.getHeader(header); - if (value != null) { - builder.addExtraAttribute(header, value); - } - }); - X509Certificate[] clientCert = (X509Certificate[]) request.getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT); - if (clientCert != null && clientCert.length > 0) { - builder.sslPrincipal(clientCert[0].getSubjectX500Principal()); - } - - AccessLogEntry accessLogEntry = (AccessLogEntry) request.getAttribute(JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY); - if (accessLogEntry != null) { - var extraAttributes = accessLogEntry.getKeyValues(); - if (extraAttributes != null) { - extraAttributes.forEach(builder::addExtraAttributes); - } - addNonNullValue(builder, accessLogEntry.getHitCounts(), RequestLogEntry.Builder::hitCounts); - addNonNullValue(builder, accessLogEntry.getTrace(), RequestLogEntry.Builder::traceNode); - } - - requestLog.log(builder.build()); - } catch (Exception e) { - // Catching any exceptions here as it is unclear how Jetty handles exceptions from a RequestLog. - logger.log(Level.SEVERE, "Failed to log access log entry: " + e.getMessage(), e); - } - } - - private String getRemoteAddress(HttpServletRequest request) { - for (String header : remoteAddressHeaders) { - String value = request.getHeader(header); - if (value != null) return value; - } - return request.getRemoteAddr(); - } - - private int getRemotePort(HttpServletRequest request) { - for (String header : remotePortHeaders) { - String value = request.getHeader(header); - if (value != null) { - OptionalInt maybePort = parsePort(value); - if (maybePort.isPresent()) return maybePort.getAsInt(); - } - } - return request.getRemotePort(); - } - - private static int getLocalPort(Request request) { - int connectorLocalPort = getConnectorLocalPort(request); - if (connectorLocalPort <= 0) return request.getLocalPort(); // If connector is already closed - return connectorLocalPort; - } - - private static OptionalInt parsePort(String port) { - try { - return OptionalInt.of(Integer.parseInt(port)); - } catch (IllegalArgumentException e) { - return OptionalInt.empty(); - } - } - - private static void addNonNullValue( - RequestLogEntry.Builder builder, T value, BiConsumer setter) { - if (value != null) { - setter.accept(builder, value); - } - } - -} 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 deleted file mode 100644 index 842ab75a312..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AccessLoggingRequestHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.common.base.Preconditions; -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.handler.AbstractRequestHandler; -import com.yahoo.jdisc.handler.ContentChannel; -import com.yahoo.jdisc.handler.RequestHandler; -import com.yahoo.jdisc.handler.ResponseHandler; -import com.yahoo.jdisc.http.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 bakksjo - */ -public class AccessLoggingRequestHandler extends AbstractRequestHandler { - public static final String CONTEXT_KEY_ACCESS_LOG_ENTRY - = AccessLoggingRequestHandler.class.getName() + "_access-log-entry"; - - public static Optional getAccessLogEntry(final HttpRequest jdiscRequest) { - final Map requestContextMap = jdiscRequest.context(); - return getAccessLogEntry(requestContextMap); - } - - public static Optional getAccessLogEntry(final Map 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); - return delegate.handleRequest(request, handler); - } - - -} 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 deleted file mode 100644 index 7dba217e01c..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/AsyncCompleteListener.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import javax.servlet.AsyncEvent; -import javax.servlet.AsyncListener; -import java.io.IOException; - -/** - * Interface for async listeners only interested in onComplete. - * @author Tony Vaagenes - */ -@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/CompletionHandlerUtils.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java deleted file mode 100644 index f436d5490d7..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlerUtils.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.handler.CompletionHandler; - -/** - * @author bjorncs - */ -public interface CompletionHandlerUtils { - CompletionHandler NOOP_COMPLETION_HANDLER = new CompletionHandler() { - @Override public void completed() {} - @Override public void failed(final Throwable t) {} - }; -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java deleted file mode 100644 index 975d88f5c34..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/CompletionHandlers.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.handler.CompletionHandler; - -import java.util.Arrays; - -/** - * @author Simon Thoresen Hult - */ -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 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/server/jetty/ConnectionThrottler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java deleted file mode 100644 index b9001d187a9..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottler.java +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.ConnectorConfig; -import org.eclipse.jetty.io.Connection; -import org.eclipse.jetty.io.SelectorManager; -import org.eclipse.jetty.server.AbstractConnector; -import org.eclipse.jetty.server.ConnectionLimit; -import org.eclipse.jetty.server.LowResourceMonitor; -import org.eclipse.jetty.util.annotation.ManagedObject; -import org.eclipse.jetty.util.component.AbstractLifeCycle; -import org.eclipse.jetty.util.component.ContainerLifeCycle; -import org.eclipse.jetty.util.component.LifeCycle; -import org.eclipse.jetty.util.statistic.RateStatistic; -import org.eclipse.jetty.util.thread.Scheduler; - -import java.nio.channels.SelectableChannel; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; - -import static java.util.stream.Collectors.toList; - -/** - * Monitor various resource constraints and throttles new connections once a threshold is exceeded. - * Implementation inspired by Jetty's {@link LowResourceMonitor}, {@link AcceptRateLimit} and {@link ConnectionLimit}. - * - * @author bjorncs - */ -@ManagedObject("Monitor various resource constraints and throttles new connections once a threshold is exceeded") -class ConnectionThrottler extends ContainerLifeCycle implements SelectorManager.AcceptListener { - - private static final Logger log = Logger.getLogger(ConnectionThrottler.class.getName()); - - private final Object monitor = new Object(); - private final Collection resourceLimits = new ArrayList<>(); - private final AbstractConnector connector; - private final Duration idleTimeout; - private final Scheduler scheduler; - - private boolean isRegistered = false; - private boolean isThrottling = false; - - ConnectionThrottler(AbstractConnector connector, ConnectorConfig.Throttling config) { - this(Runtime.getRuntime(), new RateStatistic(1, TimeUnit.SECONDS), connector.getScheduler(), connector, config); - } - - // Intended for unit testing - ConnectionThrottler(Runtime runtime, - RateStatistic rateStatistic, - Scheduler scheduler, - AbstractConnector connector, - ConnectorConfig.Throttling config) { - this.connector = connector; - if (config.maxHeapUtilization() != -1) { - this.resourceLimits.add(new HeapResourceLimit(runtime, config.maxHeapUtilization())); - } - if (config.maxConnections() != -1) { - this.resourceLimits.add(new ConnectionLimitThreshold(config.maxConnections())); - } - if (config.maxAcceptRate() != -1) { - this.resourceLimits.add(new AcceptRateLimit(rateStatistic, config.maxAcceptRate())); - } - this.idleTimeout = config.idleTimeout() != -1 ? Duration.ofMillis((long) (config.idleTimeout()*1000)) : null; - this.scheduler = scheduler; - } - - void registerWithConnector() { - synchronized (monitor) { - if (isRegistered) return; - isRegistered = true; - resourceLimits.forEach(connector::addBean); - connector.addBean(this); - } - } - - @Override - public void onAccepting(SelectableChannel channel) { - throttleIfAnyThresholdIsExceeded(); - } - - private void throttleIfAnyThresholdIsExceeded() { - synchronized (monitor) { - if (isThrottling) return; - List reasons = getThrottlingReasons(); - if (reasons.isEmpty()) return; - log.warning(String.format("Throttling new connection. Reasons: %s", reasons)); - isThrottling = true; - if (connector.isAccepting()) { - connector.setAccepting(false); - } - if (idleTimeout != null) { - log.warning(String.format("Applying idle timeout to existing connections: timeout=%sms", idleTimeout)); - connector.getConnectedEndPoints() - .forEach(endPoint -> endPoint.setIdleTimeout(idleTimeout.toMillis())); - } - scheduler.schedule(this::unthrottleIfBelowThresholds, 1, TimeUnit.SECONDS); - } - } - - private void unthrottleIfBelowThresholds() { - synchronized (monitor) { - if (!isThrottling) return; - List reasons = getThrottlingReasons(); - if (!reasons.isEmpty()) { - log.warning(String.format("Throttling continued. Reasons: %s", reasons)); - scheduler.schedule(this::unthrottleIfBelowThresholds, 1, TimeUnit.SECONDS); - return; - } - if (idleTimeout != null) { - long originalTimeout = connector.getIdleTimeout(); - log.info(String.format("Reverting idle timeout for existing connections: timeout=%sms", originalTimeout)); - connector.getConnectedEndPoints() - .forEach(endPoint -> endPoint.setIdleTimeout(originalTimeout)); - } - log.info("Throttling disabled - resource thresholds no longer exceeded"); - if (!connector.isAccepting()) { - connector.setAccepting(true); - } - isThrottling = false; - } - } - - private List getThrottlingReasons() { - synchronized (monitor) { - return resourceLimits.stream() - .map(ResourceLimit::isThresholdExceeded) - .filter(Optional::isPresent) - .map(Optional::get) - .collect(toList()); - } - } - - private interface ResourceLimit extends LifeCycle, SelectorManager.AcceptListener, Connection.Listener { - /** - * @return A string containing the reason if threshold exceeded, empty otherwise. - */ - Optional isThresholdExceeded(); - - @Override default void onOpened(Connection connection) {} - - @Override default void onClosed(Connection connection) {} - } - - /** - * Note: implementation inspired by Jetty's {@link LowResourceMonitor} - */ - private static class HeapResourceLimit extends AbstractLifeCycle implements ResourceLimit { - private final Runtime runtime; - private final double maxHeapUtilization; - - HeapResourceLimit(Runtime runtime, double maxHeapUtilization) { - this.runtime = runtime; - this.maxHeapUtilization = maxHeapUtilization; - } - - @Override - public Optional isThresholdExceeded() { - double heapUtilization = (runtime.maxMemory() - runtime.freeMemory()) / (double) runtime.maxMemory(); - if (heapUtilization > maxHeapUtilization) { - return Optional.of(String.format("Max heap utilization exceeded: %f%%>%f%%", heapUtilization*100, maxHeapUtilization*100)); - } - return Optional.empty(); - } - } - - /** - * Note: implementation inspired by Jetty's {@link org.eclipse.jetty.server.AcceptRateLimit} - */ - private static class AcceptRateLimit extends AbstractLifeCycle implements ResourceLimit { - private final Object monitor = new Object(); - private final RateStatistic rateStatistic; - private final int maxAcceptRate; - - AcceptRateLimit(RateStatistic rateStatistic, int maxAcceptRate) { - this.rateStatistic = rateStatistic; - this.maxAcceptRate = maxAcceptRate; - } - - @Override - public Optional isThresholdExceeded() { - synchronized (monitor) { - int acceptRate = rateStatistic.getRate(); - if (acceptRate > maxAcceptRate) { - return Optional.of(String.format("Max accept rate exceeded: %d>%d", acceptRate, maxAcceptRate)); - } - return Optional.empty(); - } - } - - @Override - public void onAccepting(SelectableChannel channel) { - synchronized (monitor) { - rateStatistic.record(); - } - } - - @Override - protected void doStop() { - synchronized (monitor) { - rateStatistic.reset(); - } - } - } - - /** - * Note: implementation inspired by Jetty's {@link ConnectionLimit}. - */ - private static class ConnectionLimitThreshold extends AbstractLifeCycle implements ResourceLimit { - private final Object monitor = new Object(); - private final int maxConnections; - private final Set connectionsAccepting = new HashSet<>(); - private int connectionOpened; - - ConnectionLimitThreshold(int maxConnections) { - this.maxConnections = maxConnections; - } - - @Override - public Optional isThresholdExceeded() { - synchronized (monitor) { - int totalConnections = connectionOpened + connectionsAccepting.size(); - if (totalConnections > maxConnections) { - return Optional.of(String.format("Max connection exceeded: %d>%d", totalConnections, maxConnections)); - } - return Optional.empty(); - } - } - - @Override - public void onOpened(Connection connection) { - synchronized (monitor) { - connectionsAccepting.remove(connection.getEndPoint().getTransport()); - ++connectionOpened; - } - } - - @Override - public void onClosed(Connection connection) { - synchronized (monitor) { - --connectionOpened; - } - } - - @Override - public void onAccepting(SelectableChannel channel) { - synchronized (monitor) { - connectionsAccepting.add(channel); - } - - } - - @Override - public void onAcceptFailed(SelectableChannel channel, Throwable cause) { - synchronized (monitor) { - connectionsAccepting.remove(channel); - } - } - - @Override - protected void doStop() { - synchronized (monitor) { - connectionsAccepting.clear(); - connectionOpened = 0; - } - } - } -} 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 deleted file mode 100644 index d7ad12a5c64..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactory.java +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.inject.Inject; -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider; -import com.yahoo.security.tls.MixedMode; -import com.yahoo.security.tls.TransportSecurityUtils; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.DetectorConnectionFactory; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.ProxyConnectionFactory; -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 java.util.List; - -/** - * @author Einar M R Rosenvinge - * @author bjorncs - */ -public class ConnectorFactory { - - private final ConnectorConfig connectorConfig; - private final SslContextFactoryProvider sslContextFactoryProvider; - - @Inject - public ConnectorFactory(ConnectorConfig connectorConfig, - SslContextFactoryProvider sslContextFactoryProvider) { - runtimeConnectorConfigValidation(connectorConfig); - this.connectorConfig = connectorConfig; - this.sslContextFactoryProvider = sslContextFactoryProvider; - } - - // Perform extra connector config validation that can only be performed at runtime, - // e.g. due to TLS configuration through environment variables. - private static void runtimeConnectorConfigValidation(ConnectorConfig config) { - validateProxyProtocolConfiguration(config); - validateSecureRedirectConfig(config); - } - - private static void validateProxyProtocolConfiguration(ConnectorConfig config) { - ConnectorConfig.ProxyProtocol proxyProtocolConfig = config.proxyProtocol(); - if (proxyProtocolConfig.enabled()) { - boolean tlsMixedModeEnabled = TransportSecurityUtils.getInsecureMixedMode() != MixedMode.DISABLED; - if (!isSslEffectivelyEnabled(config) || tlsMixedModeEnabled) { - throw new IllegalArgumentException("Proxy protocol can only be enabled if connector is effectively HTTPS only"); - } - } - } - - private static void validateSecureRedirectConfig(ConnectorConfig config) { - if (config.secureRedirect().enabled() && isSslEffectivelyEnabled(config)) { - throw new IllegalArgumentException("Secure redirect can only be enabled on connectors without HTTPS"); - } - } - - public ConnectorConfig getConnectorConfig() { - return connectorConfig; - } - - public ServerConnector createConnector(final Metric metric, final Server server, JettyConnectionLogger connectionLogger) { - ServerConnector connector = new JDiscServerConnector( - connectorConfig, metric, server, connectionLogger, createConnectionFactories(metric).toArray(ConnectionFactory[]::new)); - connector.setPort(connectorConfig.listenPort()); - connector.setName(connectorConfig.name()); - connector.setAcceptQueueSize(connectorConfig.acceptQueueSize()); - connector.setReuseAddress(connectorConfig.reuseAddress()); - connector.setIdleTimeout((long)(connectorConfig.idleTimeout() * 1000.0)); - return connector; - } - - private List createConnectionFactories(Metric metric) { - HttpConnectionFactory httpFactory = newHttpConnectionFactory(); - if (!isSslEffectivelyEnabled(connectorConfig)) { - return List.of(httpFactory); - } else if (connectorConfig.ssl().enabled()) { - return connectionFactoriesForHttps(metric, httpFactory); - } else if (TransportSecurityUtils.isTransportSecurityEnabled()) { - switch (TransportSecurityUtils.getInsecureMixedMode()) { - case TLS_CLIENT_MIXED_SERVER: - case PLAINTEXT_CLIENT_MIXED_SERVER: - return List.of(new DetectorConnectionFactory(newSslConnectionFactory(metric, httpFactory)), httpFactory); - case DISABLED: - return connectionFactoriesForHttps(metric, httpFactory); - default: - throw new IllegalStateException(); - } - } else { - return List.of(httpFactory); - } - } - - private List connectionFactoriesForHttps(Metric metric, HttpConnectionFactory httpFactory) { - ConnectorConfig.ProxyProtocol proxyProtocolConfig = connectorConfig.proxyProtocol(); - SslConnectionFactory sslFactory = newSslConnectionFactory(metric, httpFactory); - if (proxyProtocolConfig.enabled()) { - if (proxyProtocolConfig.mixedMode()) { - return List.of(new DetectorConnectionFactory(sslFactory, new ProxyConnectionFactory(sslFactory.getProtocol())), sslFactory, httpFactory); - } else { - return List.of(new ProxyConnectionFactory(sslFactory.getProtocol()), sslFactory, httpFactory); - } - } else { - return List.of(sslFactory, httpFactory); - } - } - - private HttpConnectionFactory newHttpConnectionFactory() { - 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 (isSslEffectivelyEnabled(connectorConfig)) { - httpConfig.addCustomizer(new SecureRequestCustomizer()); - } - return new HttpConnectionFactory(httpConfig); - } - - private SslConnectionFactory newSslConnectionFactory(Metric metric, HttpConnectionFactory httpFactory) { - SslContextFactory ctxFactory = sslContextFactoryProvider.getInstance(connectorConfig.name(), connectorConfig.listenPort()); - SslConnectionFactory connectionFactory = new SslConnectionFactory(ctxFactory, httpFactory.getProtocol()); - connectionFactory.addBean(new SslHandshakeFailedListener(metric, connectorConfig.name(), connectorConfig.listenPort())); - return connectionFactory; - } - - private static boolean isSslEffectivelyEnabled(ConnectorConfig config) { - return config.ssl().enabled() - || (config.implicitTlsEnabled() && TransportSecurityUtils.isTransportSecurityEnabled()); - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java deleted file mode 100644 index cd21dccde0e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreator.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import org.eclipse.jetty.util.ByteArrayISO8859Writer; -import org.eclipse.jetty.util.StringUtil; - -import java.io.IOException; -import java.util.Optional; - -/** - * Creates HTML body having the status code, error message and request uri. - * The body is constructed from a template that is inspired by the default Jetty template (see {@link org.eclipse.jetty.server.Response#sendError(int, String)}). - * The content is written using the ISO-8859-1 charset. - * - * @author bjorncs - */ -public class ErrorResponseContentCreator { - - private final ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer(2048); - - public byte[] createErrorContent(String requestUri, int statusCode, Optional message) { - String sanitizedString = message.map(StringUtil::sanitizeXmlString).orElse(""); - String statusCodeString = Integer.toString(statusCode); - writer.resetWriter(); - try { - writer.write("\n\n\nError "); - writer.write(statusCodeString); - writer.write("\n\n\n

HTTP ERROR: "); - writer.write(statusCodeString); - writer.write("

\n

Problem accessing "); - writer.write(StringUtil.sanitizeXmlString(requestUri)); - writer.write(". Reason:\n

    ");
-            writer.write(sanitizedString);
-            writer.write("

\n
\n\n\n"); - } catch (IOException e) { - // IOException should not be thrown unless writer is constructed using byte[] parameter - throw new RuntimeException(e); - } - return writer.getByteArray(); - } -} 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 deleted file mode 100644 index ebc10482600..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ExceptionWrapper.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -/** - * 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 Steinar Knutsen - */ -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/FilterBindings.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java deleted file mode 100644 index 310f3c9a646..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterBindings.java +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.application.BindingRepository; -import com.yahoo.jdisc.application.BindingSet; -import com.yahoo.jdisc.http.filter.RequestFilter; -import com.yahoo.jdisc.http.filter.ResponseFilter; - -import java.net.URI; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Optional; -import java.util.TreeMap; - -/** - * Resolves request/response filter (chain) from a {@link URI} instance. - * - * @author Oyvind Bakksjo - * @author bjorncs - */ -public class FilterBindings { - - private final Map requestFilters; - private final Map responseFilters; - private final Map defaultRequestFilters; - private final Map defaultResponseFilters; - private final BindingSet requestFilterBindings; - private final BindingSet responseFilterBindings; - - private FilterBindings( - Map requestFilters, - Map responseFilters, - Map defaultRequestFilters, - Map defaultResponseFilters, - BindingSet requestFilterBindings, - BindingSet responseFilterBindings) { - this.requestFilters = requestFilters; - this.responseFilters = responseFilters; - this.defaultRequestFilters = defaultRequestFilters; - this.defaultResponseFilters = defaultResponseFilters; - this.requestFilterBindings = requestFilterBindings; - this.responseFilterBindings = responseFilterBindings; - } - - public Optional resolveRequestFilter(URI uri, int localPort) { - String filterId = requestFilterBindings.resolve(uri); - if (filterId != null) return Optional.of(filterId); - return Optional.ofNullable(defaultRequestFilters.get(localPort)); - } - - public Optional resolveResponseFilter(URI uri, int localPort) { - String filterId = responseFilterBindings.resolve(uri); - if (filterId != null) return Optional.of(filterId); - return Optional.ofNullable(defaultResponseFilters.get(localPort)); - } - - public RequestFilter getRequestFilter(String filterId) { return requestFilters.get(filterId); } - - public ResponseFilter getResponseFilter(String filterId) { return responseFilters.get(filterId); } - - public Collection requestFilterIds() { return requestFilters.keySet(); } - - public Collection responseFilterIds() { return responseFilters.keySet(); } - - public Collection requestFilters() { return requestFilters.values(); } - - public Collection responseFilters() { return responseFilters.values(); } - - public static class Builder { - private final Map requestFilters = new TreeMap<>(); - private final Map responseFilters = new TreeMap<>(); - private final Map defaultRequestFilters = new TreeMap<>(); - private final Map defaultResponseFilters = new TreeMap<>(); - private final BindingRepository requestFilterBindings = new BindingRepository<>(); - private final BindingRepository responseFilterBindings = new BindingRepository<>(); - - public Builder() {} - - public Builder addRequestFilter(String id, RequestFilter filter) { requestFilters.put(id, filter); return this; } - - public Builder addResponseFilter(String id, ResponseFilter filter) { responseFilters.put(id, filter); return this; } - - public Builder addRequestFilterBinding(String id, String binding) { requestFilterBindings.bind(binding, id); return this; } - - public Builder addResponseFilterBinding(String id, String binding) { responseFilterBindings.bind(binding, id); return this; } - - public Builder setRequestFilterDefaultForPort(String id, int port) { defaultRequestFilters.put(port, id); return this; } - - public Builder setResponseFilterDefaultForPort(String id, int port) { defaultResponseFilters.put(port, id); return this; } - - public FilterBindings build() { - return new FilterBindings( - Collections.unmodifiableMap(requestFilters), - Collections.unmodifiableMap(responseFilters), - Collections.unmodifiableMap(defaultRequestFilters), - Collections.unmodifiableMap(defaultResponseFilters), - requestFilterBindings.activate(), - responseFilterBindings.activate()); - } - } -} 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 deleted file mode 100644 index 0827ccdc39e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvoker.java +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.inject.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 deleted file mode 100644 index 3ebc7bbc551..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingPrintWriter.java +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import 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 Tony Vaagenes - */ -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 deleted file mode 100644 index a605ccebfa7..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterInvokingServletOutputStream.java +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import 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 Tony Vaagenes - */ -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/FilterResolver.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java deleted file mode 100644 index 1e2686aa184..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilterResolver.java +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.NoopSharedResource; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.handler.FastContentWriter; -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 com.yahoo.jdisc.http.servlet.ServletRequest; - -import javax.servlet.http.HttpServletRequest; -import java.net.URI; -import java.util.Map; -import java.util.Optional; - -import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; - -/** - * Resolve request/response filter (chain) based on {@link FilterBindings}. - * - * @author bjorncs - */ -class FilterResolver { - - private final FilterBindings bindings; - private final Metric metric; - private final boolean strictFiltering; - - FilterResolver(FilterBindings bindings, Metric metric, boolean strictFiltering) { - this.bindings = bindings; - this.metric = metric; - this.strictFiltering = strictFiltering; - } - - Optional resolveRequestFilter(HttpServletRequest servletRequest, URI jdiscUri) { - Optional maybeFilterId = bindings.resolveRequestFilter(jdiscUri, getConnector(servletRequest).listenPort()); - if (maybeFilterId.isPresent()) { - metric.add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, createMetricContext(servletRequest, maybeFilterId.get())); - servletRequest.setAttribute(ServletRequest.JDISC_REQUEST_CHAIN, maybeFilterId.get()); - } else if (!strictFiltering) { - metric.add(MetricDefinitions.FILTERING_REQUEST_UNHANDLED, 1L, createMetricContext(servletRequest, null)); - } else { - String syntheticFilterId = RejectingRequestFilter.SYNTHETIC_FILTER_CHAIN_ID; - metric.add(MetricDefinitions.FILTERING_REQUEST_HANDLED, 1L, createMetricContext(servletRequest, syntheticFilterId)); - servletRequest.setAttribute(ServletRequest.JDISC_REQUEST_CHAIN, syntheticFilterId); - return Optional.of(RejectingRequestFilter.INSTANCE); - } - return maybeFilterId.map(bindings::getRequestFilter); - } - - Optional resolveResponseFilter(HttpServletRequest servletRequest, URI jdiscUri) { - Optional maybeFilterId = bindings.resolveResponseFilter(jdiscUri, getConnector(servletRequest).listenPort()); - if (maybeFilterId.isPresent()) { - metric.add(MetricDefinitions.FILTERING_RESPONSE_HANDLED, 1L, createMetricContext(servletRequest, maybeFilterId.get())); - servletRequest.setAttribute(ServletRequest.JDISC_RESPONSE_CHAIN, maybeFilterId.get()); - } else { - metric.add(MetricDefinitions.FILTERING_RESPONSE_UNHANDLED, 1L, createMetricContext(servletRequest, null)); - } - return maybeFilterId.map(bindings::getResponseFilter); - } - - private Metric.Context createMetricContext(HttpServletRequest request, String filterId) { - Map extraDimensions = filterId != null - ? Map.of(MetricDefinitions.FILTER_CHAIN_ID_DIMENSION, filterId) - : Map.of(); - return JDiscHttpServlet.getConnector(request).createRequestMetricContext(request, extraDimensions); - } - - private static class RejectingRequestFilter extends NoopSharedResource implements RequestFilter { - - private static final RejectingRequestFilter INSTANCE = new RejectingRequestFilter(); - private static final String SYNTHETIC_FILTER_CHAIN_ID = "strict-reject"; - - @Override - public void filter(HttpRequest request, ResponseHandler handler) { - Response response = new Response(Response.Status.FORBIDDEN); - response.headers().add("Content-Type", "text/plain"); - try (FastContentWriter writer = ResponseDispatch.newInstance(response).connectFastWriter(handler)) { - writer.write("Request did not match any request filter chain"); - } - } - } - -} 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 deleted file mode 100644 index de768f979a1..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FilteringRequestHandler.java +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.common.base.Preconditions; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.Response; -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.filter.RequestFilter; -import com.yahoo.jdisc.http.filter.ResponseFilter; - -import javax.servlet.http.HttpServletRequest; -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 Øyvind Bakksjø - */ -class FilteringRequestHandler extends AbstractRequestHandler { - - private static final ContentChannel COMPLETING_CONTENT_CHANNEL = new ContentChannel() { - - @Override - public void write(ByteBuffer buf, CompletionHandler handler) { - CompletionHandlers.tryComplete(handler); - } - - @Override - public void close(CompletionHandler handler) { - CompletionHandlers.tryComplete(handler); - } - - }; - - private final FilterResolver filterResolver; - private final HttpServletRequest servletRequest; - - public FilteringRequestHandler(FilterResolver filterResolver, HttpServletRequest servletRequest) { - this.filterResolver = filterResolver; - this.servletRequest = servletRequest; - } - - @Override - public ContentChannel handleRequest(Request request, ResponseHandler originalResponseHandler) { - Preconditions.checkArgument(request instanceof HttpRequest, "Expected HttpRequest, got " + request); - Objects.requireNonNull(originalResponseHandler, "responseHandler"); - - RequestFilter requestFilter = filterResolver.resolveRequestFilter(servletRequest, request.getUri()) - .orElse(null); - ResponseFilter responseFilter = filterResolver.resolveResponseFilter(servletRequest, request.getUri()) - .orElse(null); - - // Not using request.connect() here - it adds logic for error handling that we'd rather leave to the framework. - RequestHandler resolvedRequestHandler = request.container().resolveHandler(request); - - if (resolvedRequestHandler == null) { - throw new BindingNotFoundException(request.getUri()); - } - - RequestHandler requestHandler = new ReferenceCountingRequestHandler(resolvedRequestHandler); - - ResponseHandler responseHandler; - if (responseFilter != null) { - responseHandler = new FilteringResponseHandler(originalResponseHandler, responseFilter, request); - } else { - responseHandler = originalResponseHandler; - } - - if (requestFilter != null) { - InterceptingResponseHandler interceptingResponseHandler = new InterceptingResponseHandler(responseHandler); - requestFilter.filter(HttpRequest.class.cast(request), interceptingResponseHandler); - if (interceptingResponseHandler.hasProducedResponse()) { - return COMPLETING_CONTENT_CHANNEL; - } - } - - 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(ResponseHandler delegate, ResponseFilter responseFilter, Request request) { - this.delegate = Objects.requireNonNull(delegate); - this.responseFilter = Objects.requireNonNull(responseFilter); - this.request = request; - } - - @Override - public ContentChannel handleResponse(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(ResponseHandler delegate) { - this.delegate = Objects.requireNonNull(delegate); - } - - @Override - public ContentChannel handleResponse(Response response) { - 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 deleted file mode 100644 index 38f84438526..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.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; -import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; - -/** - * 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 bakksjo - * $Id$ - */ -class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel { - - 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(); - } - - @SuppressWarnings("try") - @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> 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> parseFormParameters(final String formContent) { - if (formContent.isEmpty()) { - return Collections.emptyMap(); - } - - final Map> 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 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> source, - final Map> destination) { - for (Map.Entry> entry : source.entrySet()) { - final List 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/HealthCheckProxyHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java deleted file mode 100644 index 0f7ce77e4cd..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HealthCheckProxyHandler.java +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.concurrent.DaemonThreadFactory; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.security.SslContextBuilder; -import com.yahoo.security.tls.TransportSecurityOptions; -import com.yahoo.security.tls.TransportSecurityUtils; -import com.yahoo.security.tls.TrustAllX509TrustManager; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.conn.ssl.NoopHostnameVerifier; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; -import org.eclipse.jetty.server.DetectorConnectionFactory; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import javax.net.ssl.SSLContext; -import javax.servlet.AsyncContext; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; - -/** - * A handler that proxies status.html health checks - * - * @author bjorncs - */ -class HealthCheckProxyHandler extends HandlerWrapper { - - private static final Logger log = Logger.getLogger(HealthCheckProxyHandler.class.getName()); - - private static final String HEALTH_CHECK_PATH = "/status.html"; - - private final Executor executor = Executors.newSingleThreadExecutor(new DaemonThreadFactory("health-check-proxy-client-")); - private final Map portToProxyTargetMapping; - - HealthCheckProxyHandler(List connectors) { - this.portToProxyTargetMapping = createPortToProxyTargetMapping(connectors); - } - - private static Map createPortToProxyTargetMapping(List connectors) { - var mapping = new HashMap(); - for (JDiscServerConnector connector : connectors) { - ConnectorConfig.HealthCheckProxy proxyConfig = connector.connectorConfig().healthCheckProxy(); - if (proxyConfig.enable()) { - Duration targetTimeout = Duration.ofMillis((int) (proxyConfig.clientTimeout() * 1000)); - mapping.put(connector.listenPort(), createProxyTarget(proxyConfig.port(), targetTimeout, connectors)); - log.info(String.format("Port %1$d is configured as a health check proxy for port %2$d. " + - "HTTP requests to '%3$s' on %1$d are proxied as HTTPS to %2$d.", - connector.listenPort(), proxyConfig.port(), HEALTH_CHECK_PATH)); - } - } - return mapping; - } - - private static ProxyTarget createProxyTarget(int targetPort, Duration targetTimeout, List connectors) { - JDiscServerConnector targetConnector = connectors.stream() - .filter(connector -> connector.listenPort() == targetPort) - .findAny() - .orElseThrow(() -> new IllegalArgumentException("Could not find any connector with listen port " + targetPort)); - SslContextFactory.Server sslContextFactory = - Optional.ofNullable(targetConnector.getConnectionFactory(SslConnectionFactory.class)) - .or(() -> Optional.ofNullable(targetConnector.getConnectionFactory(DetectorConnectionFactory.class)) - .map(detectorConnFactory -> detectorConnFactory.getBean(SslConnectionFactory.class))) - .map(connFactory -> (SslContextFactory.Server) connFactory.getSslContextFactory()) - .orElseThrow(() -> new IllegalArgumentException("Health check proxy can only target https port")); - return new ProxyTarget(targetPort, targetTimeout, sslContextFactory); - } - - @Override - public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { - int localPort = getConnectorLocalPort(servletRequest); - ProxyTarget proxyTarget = portToProxyTargetMapping.get(localPort); - if (proxyTarget != null) { - AsyncContext asyncContext = servletRequest.startAsync(); - ServletOutputStream out = servletResponse.getOutputStream(); - if (servletRequest.getRequestURI().equals(HEALTH_CHECK_PATH)) { - executor.execute(new ProxyRequestTask(asyncContext, proxyTarget, servletResponse, out)); - } else { - servletResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); - asyncContext.complete(); - } - request.setHandled(true); - } else { - _handler.handle(target, request, servletRequest, servletResponse); - } - } - - @Override - protected void doStop() throws Exception { - for (ProxyTarget target : portToProxyTargetMapping.values()) { - target.close(); - } - super.doStop(); - } - - private static class ProxyRequestTask implements Runnable { - - final AsyncContext asyncContext; - final ProxyTarget target; - final HttpServletResponse servletResponse; - final ServletOutputStream output; - - ProxyRequestTask(AsyncContext asyncContext, ProxyTarget target, HttpServletResponse servletResponse, ServletOutputStream output) { - this.asyncContext = asyncContext; - this.target = target; - this.servletResponse = servletResponse; - this.output = output; - } - - @Override - public void run() { - StatusResponse statusResponse = target.requestStatusHtml(); - servletResponse.setStatus(statusResponse.statusCode); - if (statusResponse.contentType != null) { - servletResponse.setHeader("Content-Type", statusResponse.contentType); - } - servletResponse.setHeader("Vespa-Health-Check-Proxy-Target", Integer.toString(target.port)); - output.setWriteListener(new WriteListener() { - @Override - public void onWritePossible() throws IOException { - if (output.isReady()) { - if (statusResponse.content != null) { - output.write(statusResponse.content); - } - asyncContext.complete(); - } - } - - @Override - public void onError(Throwable t) { - log.log(Level.FINE, t, () -> "Failed to write status response: " + t.getMessage()); - asyncContext.complete(); - } - }); - } - } - - private static class ProxyTarget implements AutoCloseable { - final int port; - final Duration timeout; - final SslContextFactory.Server sslContextFactory; - volatile CloseableHttpClient client; - volatile StatusResponse lastResponse; - - ProxyTarget(int port, Duration timeout, SslContextFactory.Server sslContextFactory) { - this.port = port; - this.timeout = timeout; - this.sslContextFactory = sslContextFactory; - } - - StatusResponse requestStatusHtml() { - StatusResponse response = lastResponse; - if (response != null && !response.isExpired()) { - return response; - } - return this.lastResponse = getStatusResponse(); - } - - private StatusResponse getStatusResponse() { - try (CloseableHttpResponse clientResponse = client().execute(new HttpGet("https://localhost:" + port + HEALTH_CHECK_PATH))) { - int statusCode = clientResponse.getStatusLine().getStatusCode(); - HttpEntity entity = clientResponse.getEntity(); - if (entity != null) { - Header contentTypeHeader = entity.getContentType(); - String contentType = contentTypeHeader != null ? contentTypeHeader.getValue() : null; - byte[] content = EntityUtils.toByteArray(entity); - return new StatusResponse(statusCode, contentType, content); - } else { - return new StatusResponse(statusCode, null, null); - } - } catch (Exception e) { - log.log(Level.FINE, e, () -> "Proxy request failed" + e.getMessage()); - return new StatusResponse(500, "text/plain", e.getMessage().getBytes()); - } - } - - // Client construction must be delayed to ensure that the SslContextFactory is started before calling getSslContext(). - private CloseableHttpClient client() { - if (client == null) { - synchronized (this) { - if (client == null) { - int timeoutMillis = (int) timeout.toMillis(); - client = HttpClientBuilder.create() - .disableAutomaticRetries() - .setMaxConnPerRoute(4) - .setSSLContext(getSslContext(sslContextFactory)) - .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) // Certificate may not match "localhost" - .setUserTokenHandler(context -> null) // https://stackoverflow.com/a/42112034/1615280 - .setUserAgent("health-check-proxy-client") - .setDefaultRequestConfig( - RequestConfig.custom() - .setConnectTimeout(timeoutMillis) - .setConnectionRequestTimeout(timeoutMillis) - .setSocketTimeout(timeoutMillis) - .build()) - .build(); - } - } - } - return client; - } - - private SSLContext getSslContext(SslContextFactory.Server sslContextFactory) { - // A client certificate is only required if the server connector's ssl context factory is configured with "need-auth". - if (sslContextFactory.getNeedClientAuth()) { - log.info(String.format("Port %d requires client certificate - client will provide its node certificate", port)); - // We should ideally specify the client certificate through connector config, but the model has currently no knowledge of node certificate location on disk. - // Instead we assume that the server connector will accept its own node certificate. This will work for the current hosted use-case. - // The Vespa TLS config will provide us the location of certificate and key. - TransportSecurityOptions options = TransportSecurityUtils.getOptions() - .orElseThrow(() -> - new IllegalStateException("Vespa TLS configuration is required when using health check proxy to a port with client auth 'need'")); - return new SslContextBuilder() - .withKeyStore(options.getPrivateKeyFile().get(), options.getCertificatesFile().get()) - .withTrustManager(new TrustAllX509TrustManager()) - .build(); - } else { - log.info(String.format( - "Port %d does not require a client certificate - client will not provide a certificate", port)); - return new SslContextBuilder() - .withTrustManager(new TrustAllX509TrustManager()) - .build(); - } - } - - @Override - public void close() throws IOException { - synchronized (this) { - if (client != null) { - client.close(); - client = null; - } - } - } - } - - private static class StatusResponse { - final long createdAt = System.nanoTime(); - final int statusCode; - final String contentType; - final byte[] content; - - StatusResponse(int statusCode, String contentType, byte[] content) { - this.statusCode = statusCode; - this.contentType = contentType; - this.content = content; - } - - boolean isExpired() { return System.nanoTime() - createdAt > Duration.ofSeconds(1).toNanos(); } - } -} 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 deleted file mode 100644 index 05715b13d10..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestDispatch.java +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.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.ConnectorConfig; -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.HttpRequest; -import org.eclipse.jetty.io.EofException; -import org.eclipse.jetty.server.HttpConnection; -import org.eclipse.jetty.server.Request; - -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.time.Instant; -import java.util.Arrays; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -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.HttpServletRequestUtils.getConnection; -import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; -import static com.yahoo.yolean.Exceptions.throwUnchecked; - -/** - * @author Simon Thoresen Hult - * @author bjorncs - */ -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 Request jettyRequest; - - private final ServletResponseController servletResponseController; - private final RequestHandler requestHandler; - private final RequestMetricReporter metricReporter; - - public HttpRequestDispatch(JDiscContext jDiscContext, - AccessLogEntry accessLogEntry, - Context metricContext, - HttpServletRequest servletRequest, - HttpServletResponse servletResponse) throws IOException { - this.jDiscContext = jDiscContext; - - requestHandler = newRequestHandler(jDiscContext, accessLogEntry, servletRequest); - - this.jettyRequest = (Request) servletRequest; - this.metricReporter = new RequestMetricReporter(jDiscContext.metric, metricContext, jettyRequest.getTimeStamp()); - this.servletResponseController = new ServletResponseController(servletRequest, - servletResponse, - jDiscContext.janitor, - metricReporter, - jDiscContext.developerMode()); - markConnectionAsNonPersistentIfThresholdReached(servletRequest); - this.async = servletRequest.startAsync(); - async.setTimeout(0); - metricReporter.uriLength(jettyRequest.getOriginalURI().length()); - } - - public void dispatch() throws IOException { - 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 final BiConsumer completeRequestCallback; - { - AtomicBoolean completeRequestCalled = new AtomicBoolean(false); - HttpRequestDispatch parent = this; //used to avoid binding uninitialized variables - - completeRequestCallback = (result, error) -> { - 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; - } - - boolean reportedError = false; - - if (error != null) { - if (isErrorOfType(error, EofException.class, IOException.class)) { - log.log(Level.FINE, - error, - () -> "Network connection was unexpectedly terminated: " + parent.jettyRequest.getRequestURI()); - parent.metricReporter.prematurelyClosed(); - } else if (!isErrorOfType(error, OverloadException.class, BindingNotFoundException.class, RequestException.class)) { - log.log(Level.WARNING, "Request failed: " + parent.jettyRequest.getRequestURI(), error); - } - reportedError = true; - parent.metricReporter.failedResponse(); - } else { - parent.metricReporter.successfulResponse(); - } - - try { - parent.async.complete(); - log.finest(() -> "Request completed successfully: " + parent.jettyRequest.getRequestURI()); - } catch (Throwable throwable) { - Level level = reportedError ? Level.FINE: Level.WARNING; - log.log(level, "Async.complete failed", throwable); - } - }; - } - - private static void markConnectionAsNonPersistentIfThresholdReached(HttpServletRequest request) { - ConnectorConfig connectorConfig = getConnector(request).connectorConfig(); - int maxRequestsPerConnection = connectorConfig.maxRequestsPerConnection(); - if (maxRequestsPerConnection > 0) { - HttpConnection connection = getConnection(request); - if (connection.getMessagesIn() >= maxRequestsPerConnection) { - connection.getGenerator().setPersistent(false); - } - } - double maxConnectionLifeInSeconds = connectorConfig.maxConnectionLife(); - if (maxConnectionLifeInSeconds > 0) { - HttpConnection connection = getConnection(request); - Instant expireAt = Instant.ofEpochMilli((long)(connection.getCreatedTimeStamp() + maxConnectionLifeInSeconds * 1000)); - if (Instant.now().isAfter(expireAt)) { - connection.getGenerator().setPersistent(false); - } - } - } - - @SafeVarargs - @SuppressWarnings("varargs") - private static boolean isErrorOfType(Throwable throwable, Class... handledTypes) { - return Arrays.stream(handledTypes) - .anyMatch( - exceptionType -> exceptionType.isInstance(throwable) - || throwable instanceof CompletionException && exceptionType.isInstance(throwable.getCause())); - } - - @SuppressWarnings("try") - private ServletRequestReader handleRequest() throws IOException { - HttpRequest jdiscRequest = HttpRequestFactory.newJDiscRequest(jDiscContext.container, jettyRequest); - ContentChannel requestContentChannel; - - try (ResourceReference ref = References.fromResource(jdiscRequest)) { - HttpRequestFactory.copyHeaders(jettyRequest, jdiscRequest); - requestContentChannel = requestHandler.handleRequest(jdiscRequest, servletResponseController.responseHandler); - } - - ServletInputStream servletInputStream = jettyRequest.getInputStream(); - - ServletRequestReader servletRequestReader = new ServletRequestReader(servletInputStream, - requestContentChannel, - jDiscContext.janitor, - metricReporter); - - servletInputStream.setReadListener(servletRequestReader); - return servletRequestReader; - } - - private static void onError(CompletableFuture future, Consumer errorHandler) { - future.whenComplete((result, exception) -> { - if (exception != null) { - errorHandler.accept(exception); - } - }); - } - - ContentChannel handleRequestFilterResponse(Response response) { - try { - jettyRequest.getInputStream().close(); - ContentChannel responseContentChannel = servletResponseController.responseHandler.handleResponse(response); - servletResponseController.finishedFuture().whenComplete(completeRequestCallback); - return responseContentChannel; - } catch (IOException e) { - throw throwUnchecked(e); - } - } - - - private static RequestHandler newRequestHandler(JDiscContext context, - AccessLogEntry accessLogEntry, - HttpServletRequest servletRequest) { - RequestHandler requestHandler = wrapHandlerIfFormPost( - new FilteringRequestHandler(context.filterResolver, servletRequest), - servletRequest, context.serverConfig.removeRawPostBodyForWwwUrlEncodedPost()); - - return new AccessLoggingRequestHandler(requestHandler, accessLogEntry); - } - - private static RequestHandler wrapHandlerIfFormPost(RequestHandler requestHandler, - HttpServletRequest servletRequest, - boolean removeBodyForFormPost) { - if (!servletRequest.getMethod().equals("POST")) { - return requestHandler; - } - 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(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 deleted file mode 100644 index e8d37cfadb5..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpRequestFactory.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.servlet.ServletRequest; -import com.yahoo.jdisc.service.CurrentContainer; -import org.eclipse.jetty.util.Utf8Appendable; - -import javax.servlet.http.HttpServletRequest; -import java.net.InetSocketAddress; -import java.net.URI; -import java.security.cert.X509Certificate; -import java.util.Enumeration; - -import static com.yahoo.jdisc.Response.Status.BAD_REQUEST; -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; - -/** - * @author Simon Thoresen Hult - * @author bjorncs - */ -class HttpRequestFactory { - - public static HttpRequest newJDiscRequest(CurrentContainer container, HttpServletRequest servletRequest) { - try { - HttpRequest httpRequest = HttpRequest.newServerRequest( - container, - getUri(servletRequest), - HttpRequest.Method.valueOf(servletRequest.getMethod()), - HttpRequest.Version.fromString(servletRequest.getProtocol()), - new InetSocketAddress(servletRequest.getRemoteAddr(), servletRequest.getRemotePort()), - getConnection(servletRequest).getCreatedTimeStamp()); - httpRequest.context().put(ServletRequest.JDISC_REQUEST_X509CERT, getCertChain(servletRequest)); - return httpRequest; - } catch (Utf8Appendable.NotUtf8Exception e) { - throw createBadQueryException(e); - } - } - - // Implementation based on org.eclipse.jetty.server.Request.getRequestURL(), but with the connector's local port instead - public static URI getUri(HttpServletRequest servletRequest) { - try { - String scheme = servletRequest.getScheme(); - String host = servletRequest.getServerName(); - int port = getConnectorLocalPort(servletRequest); - String path = servletRequest.getRequestURI(); - String query = servletRequest.getQueryString(); - - URI uri = URI.create(scheme + "://" + - host + ":" + port + - (path != null ? path : "") + - (query != null ? "?" + query : "")); - - validateSchemeHostPort(scheme, host, port, uri); - return uri; - } - catch (IllegalArgumentException e) { - throw createBadQueryException(e); - } - } - - private static void validateSchemeHostPort(String scheme, String host, int port, URI uri) { - if ( ! scheme.equals(uri.getScheme())) - throw new IllegalArgumentException("Bad scheme: " + scheme); - - if ( ! host.equals(uri.getHost()) || port != uri.getPort()) - throw new IllegalArgumentException("Bad authority: " + uri.getRawAuthority() + " != " + host + ":" + port); - } - - private static RequestException createBadQueryException(IllegalArgumentException e) { - return new RequestException(BAD_REQUEST, "URL violates RFC 2396: " + e.getMessage(), e); - } - - public static void copyHeaders(HttpServletRequest from, HttpRequest to) { - for (Enumeration it = from.getHeaderNames(); it.hasMoreElements(); ) { - String key = it.nextElement(); - for (Enumeration value = from.getHeaders(key); value.hasMoreElements(); ) { - to.headers().add(key, value.nextElement()); - } - } - } - - private static X509Certificate[] getCertChain(HttpServletRequest servletRequest) { - return (X509Certificate[]) servletRequest.getAttribute("javax.servlet.request.X509Certificate"); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java deleted file mode 100644 index 82c445c7ca9..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpResponseStatisticsCollector.java +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.HttpRequest; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.server.AsyncContextEvent; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpChannelState; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.FutureCallback; -import org.eclipse.jetty.util.component.Graceful; - -import javax.servlet.AsyncEvent; -import javax.servlet.AsyncListener; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.LongAdder; - -/** - * HttpResponseStatisticsCollector collects statistics about HTTP response types aggregated by category - * (1xx, 2xx, etc). It is similar to {@link org.eclipse.jetty.server.handler.StatisticsHandler} - * with the distinction that this class collects response type statistics grouped - * by HTTP method and only collects the numbers that are reported as metrics from Vespa. - * - * @author ollivir - */ -public class HttpResponseStatisticsCollector extends HandlerWrapper implements Graceful { - - static final String requestTypeAttribute = "requestType"; - - private final AtomicReference shutdown = new AtomicReference<>(); - private final List monitoringHandlerPaths; - private final List searchHandlerPaths; - - public enum HttpMethod { - GET, PATCH, POST, PUT, DELETE, OPTIONS, HEAD, OTHER - } - - public enum HttpScheme { - HTTP, HTTPS, OTHER - } - - private static final String[] HTTP_RESPONSE_GROUPS = { - MetricDefinitions.RESPONSES_1XX, - MetricDefinitions.RESPONSES_2XX, - MetricDefinitions.RESPONSES_3XX, - MetricDefinitions.RESPONSES_4XX, - MetricDefinitions.RESPONSES_5XX, - MetricDefinitions.RESPONSES_401, - MetricDefinitions.RESPONSES_403 - }; - - private final AtomicLong inFlight = new AtomicLong(); - private final LongAdder[][][][] statistics; - - public HttpResponseStatisticsCollector(List monitoringHandlerPaths, List searchHandlerPaths) { - this.monitoringHandlerPaths = monitoringHandlerPaths; - this.searchHandlerPaths = searchHandlerPaths; - statistics = new LongAdder[HttpScheme.values().length][HttpMethod.values().length][][]; - for (int scheme = 0; scheme < HttpScheme.values().length; ++scheme) { - for (int method = 0; method < HttpMethod.values().length; method++) { - statistics[scheme][method] = new LongAdder[HTTP_RESPONSE_GROUPS.length][]; - for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { - statistics[scheme][method][group] = new LongAdder[HttpRequest.RequestType.values().length]; - for (int requestType = 0; requestType < HttpRequest.RequestType.values().length; requestType++) { - statistics[scheme][method][group][requestType] = new LongAdder(); - } - } - } - } - } - - private final AsyncListener completionWatcher = new AsyncListener() { - - @Override - public void onTimeout(AsyncEvent event) { } - - @Override - public void onStartAsync(AsyncEvent event) { - event.getAsyncContext().addListener(this); - } - - @Override - public void onError(AsyncEvent event) { } - - @Override - public void onComplete(AsyncEvent event) throws IOException { - HttpChannelState state = ((AsyncContextEvent) event).getHttpChannelState(); - Request request = state.getBaseRequest(); - - observeEndOfRequest(request, null); - } - }; - - @Override - public void handle(String path, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - inFlight.incrementAndGet(); - - try { - Handler handler = getHandler(); - if (handler != null && shutdown.get() == null && isStarted()) { - handler.handle(path, baseRequest, request, response); - } else if ( ! baseRequest.isHandled()) { - baseRequest.setHandled(true); - response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); - } - } finally { - HttpChannelState state = baseRequest.getHttpChannelState(); - if (state.isSuspended()) { - if (state.isInitial()) { - state.addListener(completionWatcher); - } - } else if (state.isInitial()) { - observeEndOfRequest(baseRequest, response); - } - } - } - - private void observeEndOfRequest(Request request, HttpServletResponse flushableResponse) throws IOException { - int group = groupIndex(request); - if (group >= 0) { - HttpScheme scheme = getScheme(request); - HttpMethod method = getMethod(request); - HttpRequest.RequestType requestType = getRequestType(request); - - statistics[scheme.ordinal()][method.ordinal()][group][requestType.ordinal()].increment(); - if (group == 5 || group == 6) { // if 401/403, also increment 4xx - statistics[scheme.ordinal()][method.ordinal()][3][requestType.ordinal()].increment(); - } - } - - long live = inFlight.decrementAndGet(); - FutureCallback shutdownCb = shutdown.get(); - if (shutdownCb != null) { - if (flushableResponse != null) { - flushableResponse.flushBuffer(); - } - if (live == 0) { - shutdownCb.succeeded(); - } - } - } - - private int groupIndex(Request request) { - int index = request.getResponse().getStatus(); - if (index == 401) { - return 5; - } - if (index == 403) { - return 6; - } - - index = index / 100 - 1; // 1xx = 0, 2xx = 1 etc. - if (index < 0 || index >= statistics[0].length) { - return -1; - } else { - return index; - } - } - - private HttpScheme getScheme(Request request) { - switch (request.getScheme()) { - case "http": - return HttpScheme.HTTP; - case "https": - return HttpScheme.HTTPS; - default: - return HttpScheme.OTHER; - } - } - - private HttpMethod getMethod(Request request) { - switch (request.getMethod()) { - case "GET": - return HttpMethod.GET; - case "PATCH": - return HttpMethod.PATCH; - case "POST": - return HttpMethod.POST; - case "PUT": - return HttpMethod.PUT; - case "DELETE": - return HttpMethod.DELETE; - case "OPTIONS": - return HttpMethod.OPTIONS; - case "HEAD": - return HttpMethod.HEAD; - default: - return HttpMethod.OTHER; - } - } - - private HttpRequest.RequestType getRequestType(Request request) { - HttpRequest.RequestType requestType = (HttpRequest.RequestType)request.getAttribute(requestTypeAttribute); - if (requestType != null) return requestType; - - // Deduce from path and method: - String path = request.getRequestURI(); - for (String monitoringHandlerPath : monitoringHandlerPaths) { - if (path.startsWith(monitoringHandlerPath)) return HttpRequest.RequestType.MONITORING; - } - for (String searchHandlerPath : searchHandlerPaths) { - if (path.startsWith(searchHandlerPath)) return HttpRequest.RequestType.READ; - } - if ("GET".equals(request.getMethod())) { - return HttpRequest.RequestType.READ; - } else { - return HttpRequest.RequestType.WRITE; - } - } - - public List takeStatistics() { - var ret = new ArrayList(); - for (HttpScheme scheme : HttpScheme.values()) { - int schemeIndex = scheme.ordinal(); - for (HttpMethod method : HttpMethod.values()) { - int methodIndex = method.ordinal(); - for (int group = 0; group < HTTP_RESPONSE_GROUPS.length; group++) { - for (HttpRequest.RequestType type : HttpRequest.RequestType.values()) { - long value = statistics[schemeIndex][methodIndex][group][type.ordinal()].sumThenReset(); - if (value > 0) { - ret.add(new StatisticsEntry(scheme.name().toLowerCase(), method.name(), HTTP_RESPONSE_GROUPS[group], type.name().toLowerCase(), value)); - } - } - } - } - } - return ret; - } - - @Override - protected void doStart() throws Exception { - shutdown.set(null); - super.doStart(); - } - - @Override - protected void doStop() throws Exception { - super.doStop(); - FutureCallback shutdownCb = shutdown.get(); - if ( ! shutdownCb.isDone()) { - shutdownCb.failed(new TimeoutException()); - } - } - - @Override - public Future shutdown() { - FutureCallback shutdownCb = new FutureCallback(false); - shutdown.compareAndSet(null, shutdownCb); - shutdownCb = shutdown.get(); - if (inFlight.get() == 0) { - shutdownCb.succeeded(); - } - return shutdownCb; - } - - @Override - public boolean isShutdown() { - FutureCallback futureCallback = shutdown.get(); - return futureCallback != null && futureCallback.isDone(); - } - - public static class StatisticsEntry { - - public final String scheme; - public final String method; - public final String name; - public final String requestType; - public final long value; - - public StatisticsEntry(String scheme, String method, String name, String requestType, long value) { - this.scheme = scheme; - this.method = method; - this.name = name; - this.requestType = requestType; - this.value = value; - } - - @Override - public String toString() { - return "scheme: " + scheme + - ", method: " + method + - ", name: " + name + - ", requestType: " + requestType + - ", value: " + value; - } - - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java deleted file mode 100644 index e7b9f459d2e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/HttpServletRequestUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import org.eclipse.jetty.server.HttpConnection; - -import javax.servlet.http.HttpServletRequest; - -/** - * @author bjorncs - */ -public class HttpServletRequestUtils { - private HttpServletRequestUtils() {} - - public static HttpConnection getConnection(HttpServletRequest request) { - return (HttpConnection)request.getAttribute("org.eclipse.jetty.server.HttpConnection"); - } - - /** - * Note: {@link HttpServletRequest#getLocalPort()} may return the local port of the load balancer / reverse proxy if proxy-protocol is enabled. - * @return the actual local port of the underlying Jetty connector - */ - public static int getConnectorLocalPort(HttpServletRequest request) { - JDiscServerConnector connector = (JDiscServerConnector) getConnection(request).getConnector(); - int actualLocalPort = connector.getLocalPort(); - int localPortIfConnectorUnopened = -1; - int localPortIfConnectorClosed = -2; - if (actualLocalPort == localPortIfConnectorUnopened || actualLocalPort == localPortIfConnectorClosed) { - int configuredLocalPort = connector.listenPort(); - int localPortEphemeralPort = 0; - if (configuredLocalPort == localPortEphemeralPort) { - throw new IllegalStateException("Unable to determine connector's listen port"); - } - return configuredLocalPort; - } - return actualLocalPort; - } - -} 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 deleted file mode 100644 index b37a7352dc6..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscContext.java +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.http.ServerConfig; -import com.yahoo.jdisc.service.CurrentContainer; - -import java.util.concurrent.Executor; - -public class JDiscContext { - final FilterResolver filterResolver; - final CurrentContainer container; - final Executor janitor; - final Metric metric; - final ServerConfig serverConfig; - - public JDiscContext(FilterBindings filterBindings, - CurrentContainer container, - Executor janitor, - Metric metric, - ServerConfig serverConfig) { - - this.filterResolver = new FilterResolver(filterBindings, metric, serverConfig.strictFiltering()); - 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 deleted file mode 100644 index a89c115a1c2..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscFilterInvokerFilter.java +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.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.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; - -import static com.yahoo.jdisc.http.server.jetty.JDiscHttpServlet.getConnector; -import static com.yahoo.yolean.Exceptions.throwUnchecked; - -/** - * Runs JDisc security filters for Servlets - * This component is split in two: - * 1) JDiscFilterInvokerFilter, which uses package private methods to support JDisc APIs - * 2) SecurityFilterInvoker, which is intended for use in a servlet context. - * - * @author Tony Vaagenes - */ -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; - try { - uri = HttpRequestFactory.getUri(httpRequest); - } catch (RequestException e) { - httpResponse.sendError(e.getResponseStatus(), e.getMessage()); - return; - } - - AtomicReference 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 responseFilterInvoker = - jDiscContext.filterResolver.resolveResponseFilter(request, uri) - .map(responseFilter -> - new OneTimeRunnable(() -> - filterInvoker.invokeResponseFilterChain(responseFilter, uri, request, response))); - - - HttpServletResponse responseForServlet = responseFilterInvoker - .map(invoker -> - new FilterInvokingResponseWrapper(response, invoker)) - .orElse(response); - - HttpServletRequest requestForServlet = responseFilterInvoker - .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 responseReturned, URI uri, HttpServletRequest request, HttpServletResponse response) throws IOException { - try { - RequestFilter requestFilter = jDiscContext.filterResolver.resolveRequestFilter(request, uri).orElse(null); - 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 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).createRequestMetricContext(request, Map.of()), - 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 createListener(Class 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 deleted file mode 100644 index 41a1ffc2709..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscHttpServlet.java +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.handler.OverloadException; -import com.yahoo.jdisc.http.HttpRequest.Method; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Enumeration; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; - -/** - * @author Simon Thoresen Hult - * @author bjorncs - */ -@WebServlet(asyncSupported = true, description = "Bridge between Servlet and JDisc APIs") -class JDiscHttpServlet extends HttpServlet { - - 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; - - private static final Set servletSupportedMethods = - Stream.of(Method.OPTIONS, Method.GET, Method.HEAD, Method.POST, Method.PUT, Method.DELETE, Method.TRACE) - .map(Method::name) - .collect(Collectors.toSet()); - - public JDiscHttpServlet(JDiscContext context) { - this.context = context; - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - @Override - protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws IOException { - dispatchHttpRequest(request, response); - } - - /** - * 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(MetricDefinitions.NUM_REQUESTS, 1, metricContext); - context.metric.add(MetricDefinitions.JDISC_HTTP_REQUESTS, 1, metricContext); - - String method = request.getMethod().toUpperCase(); - if (servletSupportedMethods.contains(method)) { - super.service(request, response); - } else if (method.equals(Method.PATCH.name())) { - // PATCH method is not handled by the Servlet spec - dispatchHttpRequest(request, response); - } else { - // Divergence from HTTP / Servlet spec: JDisc returns 405 for both unknown and known (but unsupported) methods. - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); - } - } - - static JDiscServerConnector getConnector(HttpServletRequest request) { - return (JDiscServerConnector)getConnection(request).getConnector(); - } - - private void dispatchHttpRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { - AccessLogEntry accessLogEntry = new 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 static Metric.Context getMetricContext(HttpServletRequest request) { - return JDiscServerConnector.fromRequest(request).createRequestMetricContext(request, Map.of()); - } - - private static String formatAttributes(final HttpServletRequest request) { - StringBuilder out = new StringBuilder(); - out.append("attributes = {"); - for (Enumeration 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/JDiscServerConnector.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java deleted file mode 100644 index 99d0c5c8d8c..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.http.ConnectorConfig; -import org.eclipse.jetty.io.ConnectionStatistics; -import org.eclipse.jetty.server.ConnectionFactory; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; - -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import java.net.Socket; -import java.net.SocketException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * @author bjorncs - */ -class JDiscServerConnector extends ServerConnector { - - public static final String REQUEST_ATTRIBUTE = JDiscServerConnector.class.getName(); - private final Metric.Context metricCtx; - private final ConnectionStatistics statistics; - private final ConnectorConfig config; - private final boolean tcpKeepAlive; - private final boolean tcpNoDelay; - private final Metric metric; - private final String connectorName; - private final int listenPort; - - JDiscServerConnector(ConnectorConfig config, Metric metric, Server server, JettyConnectionLogger connectionLogger, ConnectionFactory... factories) { - super(server, factories); - this.config = config; - this.tcpKeepAlive = config.tcpKeepAliveEnabled(); - this.tcpNoDelay = config.tcpNoDelay(); - this.metric = metric; - this.connectorName = config.name(); - this.listenPort = config.listenPort(); - this.metricCtx = metric.createContext(createConnectorDimensions(listenPort, connectorName)); - - this.statistics = new ConnectionStatistics(); - addBean(statistics); - ConnectorConfig.Throttling throttlingConfig = config.throttling(); - if (throttlingConfig.enabled()) { - new ConnectionThrottler(this, throttlingConfig).registerWithConnector(); - } - addBean(connectionLogger); - } - - @Override - protected void configure(final Socket socket) { - super.configure(socket); - try { - socket.setKeepAlive(tcpKeepAlive); - socket.setTcpNoDelay(tcpNoDelay); - } catch (SocketException ignored) { - } - } - - public ConnectionStatistics getStatistics() { - return statistics; - } - - public Metric.Context getConnectorMetricContext() { - return metricCtx; - } - - public Metric.Context createRequestMetricContext(HttpServletRequest request, Map extraDimensions) { - String method = request.getMethod(); - String scheme = request.getScheme(); - boolean clientAuthenticated = request.getAttribute(com.yahoo.jdisc.http.servlet.ServletRequest.SERVLET_REQUEST_X509CERT) != null; - Map dimensions = createConnectorDimensions(listenPort, connectorName); - dimensions.put(MetricDefinitions.METHOD_DIMENSION, method); - dimensions.put(MetricDefinitions.SCHEME_DIMENSION, scheme); - dimensions.put(MetricDefinitions.CLIENT_AUTHENTICATED_DIMENSION, Boolean.toString(clientAuthenticated)); - String serverName = Optional.ofNullable(request.getServerName()).orElse("unknown"); - dimensions.put(MetricDefinitions.REQUEST_SERVER_NAME_DIMENSION, serverName); - dimensions.putAll(extraDimensions); - return metric.createContext(dimensions); - } - - public static JDiscServerConnector fromRequest(ServletRequest request) { - return (JDiscServerConnector) request.getAttribute(REQUEST_ATTRIBUTE); - } - - ConnectorConfig connectorConfig() { - return config; - } - - int listenPort() { - return listenPort; - } - - private static Map createConnectorDimensions(int listenPort, String connectorName) { - Map props = new HashMap<>(); - props.put(MetricDefinitions.NAME_DIMENSION, connectorName); - props.put(MetricDefinitions.PORT_DIMENSION, listenPort); - return props; - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java deleted file mode 100644 index cd1ca490f61..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.ConnectionLog; -import com.yahoo.container.logging.ConnectionLogEntry; -import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry; -import com.yahoo.io.HexDump; -import com.yahoo.jdisc.http.ServerConfig; -import org.eclipse.jetty.io.Connection; -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.io.SocketChannelEndPoint; -import org.eclipse.jetty.io.ssl.SslConnection; -import org.eclipse.jetty.io.ssl.SslHandshakeListener; -import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.HttpConnection; -import org.eclipse.jetty.server.ProxyConnectionFactory; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.util.component.AbstractLifeCycle; - -import javax.net.ssl.ExtendedSSLSession; -import javax.net.ssl.SNIHostName; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.StandardConstants; -import java.net.InetSocketAddress; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Objects; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Jetty integration for jdisc connection log ({@link ConnectionLog}). - * - * @author bjorncs - */ -class JettyConnectionLogger extends AbstractLifeCycle implements Connection.Listener, HttpChannel.Listener, SslHandshakeListener { - - static final String CONNECTION_ID_REQUEST_ATTRIBUTE = "jdisc.request.connection.id"; - - private static final Logger log = Logger.getLogger(JettyConnectionLogger.class.getName()); - - private final ConcurrentMap, ConnectionInfo> connectionInfo = new ConcurrentHashMap<>(); - private final ConcurrentMap, ConnectionInfo> sslToConnectionInfo = new ConcurrentHashMap<>(); - - private final boolean enabled; - private final ConnectionLog connectionLog; - - JettyConnectionLogger(ServerConfig.ConnectionLog config, ConnectionLog connectionLog) { - this.enabled = config.enabled(); - this.connectionLog = connectionLog; - log.log(Level.FINE, () -> "Jetty connection logger is " + (config.enabled() ? "enabled" : "disabled")); - } - - // - // AbstractLifeCycle methods start - // - @Override - protected void doStop() { - handleListenerInvocation("AbstractLifeCycle", "doStop", "", List.of(), () -> { - log.log(Level.FINE, () -> "Jetty connection logger is stopped"); - }); - } - - @Override - protected void doStart() { - handleListenerInvocation("AbstractLifeCycle", "doStart", "", List.of(), () -> { - log.log(Level.FINE, () -> "Jetty connection logger is started"); - }); - } - // - // AbstractLifeCycle methods stop - // - - // - // Connection.Listener methods start - // - @Override - public void onOpened(Connection connection) { - handleListenerInvocation("Connection.Listener", "onOpened", "%h", List.of(connection), () -> { - SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint()); - var endpointKey = IdentityKey.of(endpoint); - ConnectionInfo info = connectionInfo.get(endpointKey); - if (info == null) { - info = ConnectionInfo.from(endpoint); - connectionInfo.put(IdentityKey.of(endpoint), info); - } - if (connection instanceof SslConnection) { - SSLEngine sslEngine = ((SslConnection) connection).getSSLEngine(); - sslToConnectionInfo.put(IdentityKey.of(sslEngine), info); - } - if (connection.getEndPoint() instanceof ProxyConnectionFactory.ProxyEndPoint) { - InetSocketAddress remoteAddress = connection.getEndPoint().getRemoteAddress(); - info.setRemoteAddress(remoteAddress); - } - }); - } - - @Override - public void onClosed(Connection connection) { - handleListenerInvocation("Connection.Listener", "onClosed", "%h", List.of(connection), () -> { - SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint()); - var endpointKey = IdentityKey.of(endpoint); - ConnectionInfo info = connectionInfo.get(endpointKey); - if (info == null) return; // Closed connection already handled - if (connection instanceof HttpConnection) { - info.setHttpBytes(connection.getBytesIn(), connection.getBytesOut()); - } - if (!endpoint.isOpen()) { - info.setClosedAt(System.currentTimeMillis()); - connectionLog.log(info.toLogEntry()); - connectionInfo.remove(endpointKey); - } - }); - } - // - // Connection.Listener methods end - // - - // - // HttpChannel.Listener methods start - // - @Override - public void onRequestBegin(Request request) { - handleListenerInvocation("HttpChannel.Listener", "onRequestBegin", "%h", List.of(request), () -> { - SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint()); - ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint))); - info.incrementRequests(); - request.setAttribute(CONNECTION_ID_REQUEST_ATTRIBUTE, info.uuid()); - }); - } - - @Override - public void onResponseBegin(Request request) { - handleListenerInvocation("HttpChannel.Listener", "onResponseBegin", "%h", List.of(request), () -> { - SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint()); - ConnectionInfo info = Objects.requireNonNull(connectionInfo.get(IdentityKey.of(endpoint))); - info.incrementResponses(); - }); - } - // - // HttpChannel.Listener methods end - // - - // - // SslHandshakeListener methods start - // - @Override - public void handshakeSucceeded(Event event) { - SSLEngine sslEngine = event.getSSLEngine(); - handleListenerInvocation("SslHandshakeListener", "handshakeSucceeded", "sslEngine=%h", List.of(sslEngine), () -> { - ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine)); - info.setSslSessionDetails(sslEngine.getSession()); - }); - } - - @Override - public void handshakeFailed(Event event, Throwable failure) { - SSLEngine sslEngine = event.getSSLEngine(); - handleListenerInvocation("SslHandshakeListener", "handshakeFailed", "sslEngine=%h,failure=%s", List.of(sslEngine, failure), () -> { - log.log(Level.FINE, failure, failure::toString); - ConnectionInfo info = sslToConnectionInfo.remove(IdentityKey.of(sslEngine)); - info.setSslHandshakeFailure((SSLHandshakeException)failure); - }); - } - // - // SslHandshakeListener methods end - // - - private void handleListenerInvocation( - String listenerType, String methodName, String methodArgumentsFormat, List methodArguments, ListenerHandler handler) { - if (!enabled) return; - try { - log.log(Level.FINE, () -> String.format(listenerType + "." + methodName + "(" + methodArgumentsFormat + ")", methodArguments.toArray())); - handler.run(); - } catch (Exception e) { - log.log(Level.WARNING, String.format("Exception in %s.%s listener: %s", listenerType, methodName, e.getMessage()), e); - } - } - - /** - * Protocol layers are connected through each {@link Connection}'s {@link EndPoint} reference. - * This methods iterates through the endpoints recursively to find the underlying socket endpoint. - */ - private static SocketChannelEndPoint findUnderlyingSocketEndpoint(EndPoint endpoint) { - if (endpoint instanceof SocketChannelEndPoint) { - return (SocketChannelEndPoint) endpoint; - } else if (endpoint instanceof SslConnection.DecryptedEndPoint) { - var decryptedEndpoint = (SslConnection.DecryptedEndPoint) endpoint; - return findUnderlyingSocketEndpoint(decryptedEndpoint.getSslConnection().getEndPoint()); - } else if (endpoint instanceof ProxyConnectionFactory.ProxyEndPoint) { - var proxyEndpoint = (ProxyConnectionFactory.ProxyEndPoint) endpoint; - return findUnderlyingSocketEndpoint(proxyEndpoint.unwrap()); - } else { - throw new IllegalArgumentException("Unknown connection endpoint type: " + endpoint.getClass().getName()); - } - } - - @FunctionalInterface private interface ListenerHandler { void run() throws Exception; } - - private static class ConnectionInfo { - private final UUID uuid; - private final long createdAt; - private final InetSocketAddress localAddress; - private final InetSocketAddress peerAddress; - - private long closedAt = 0; - private long httpBytesReceived = 0; - private long httpBytesSent = 0; - private long requests = 0; - private long responses = 0; - private InetSocketAddress remoteAddress; - private byte[] sslSessionId; - private String sslProtocol; - private String sslCipherSuite; - private String sslPeerSubject; - private Date sslPeerNotBefore; - private Date sslPeerNotAfter; - private List sslSniServerNames; - private SSLHandshakeException sslHandshakeException; - - private ConnectionInfo(UUID uuid, long createdAt, InetSocketAddress localAddress, InetSocketAddress peerAddress) { - this.uuid = uuid; - this.createdAt = createdAt; - this.localAddress = localAddress; - this.peerAddress = peerAddress; - } - - static ConnectionInfo from(SocketChannelEndPoint endpoint) { - return new ConnectionInfo( - UUID.randomUUID(), - endpoint.getCreatedTimeStamp(), - endpoint.getLocalAddress(), - endpoint.getRemoteAddress()); - } - - synchronized UUID uuid() { return uuid; } - - synchronized ConnectionInfo setClosedAt(long closedAt) { - this.closedAt = closedAt; - return this; - } - - synchronized ConnectionInfo setHttpBytes(long received, long sent) { - this.httpBytesReceived = received; - this.httpBytesSent = sent; - return this; - } - - synchronized ConnectionInfo incrementRequests() { ++this.requests; return this; } - - synchronized ConnectionInfo incrementResponses() { ++this.responses; return this; } - - synchronized ConnectionInfo setRemoteAddress(InetSocketAddress remoteAddress) { - this.remoteAddress = remoteAddress; - return this; - } - - synchronized ConnectionInfo setSslSessionDetails(SSLSession session) { - this.sslCipherSuite = session.getCipherSuite(); - this.sslProtocol = session.getProtocol(); - this.sslSessionId = session.getId(); - if (session instanceof ExtendedSSLSession) { - ExtendedSSLSession extendedSession = (ExtendedSSLSession) session; - this.sslSniServerNames = extendedSession.getRequestedServerNames(); - } - try { - this.sslPeerSubject = session.getPeerPrincipal().getName(); - X509Certificate peerCertificate = (X509Certificate) session.getPeerCertificates()[0]; - this.sslPeerNotBefore = peerCertificate.getNotBefore(); - this.sslPeerNotAfter = peerCertificate.getNotAfter(); - } catch (SSLPeerUnverifiedException e) { - // Throw if peer is not authenticated (e.g when client auth is disabled) - // JSSE provides no means of checking for client authentication without catching this exception - } - return this; - } - - synchronized ConnectionInfo setSslHandshakeFailure(SSLHandshakeException exception) { - this.sslHandshakeException = exception; - return this; - } - - synchronized ConnectionLogEntry toLogEntry() { - ConnectionLogEntry.Builder builder = ConnectionLogEntry.builder(uuid, Instant.ofEpochMilli(createdAt)); - if (closedAt > 0) { - builder.withDuration((closedAt - createdAt) / 1000D); - } - if (httpBytesReceived > 0) { - builder.withHttpBytesReceived(httpBytesReceived); - } - if (httpBytesSent > 0) { - builder.withHttpBytesSent(httpBytesSent); - } - if (requests > 0) { - builder.withRequests(requests); - } - if (responses > 0) { - builder.withResponses(responses); - } - if (peerAddress != null) { - builder.withPeerAddress(peerAddress.getHostString()) - .withPeerPort(peerAddress.getPort()); - } - if (localAddress != null) { - builder.withLocalAddress(localAddress.getHostString()) - .withLocalPort(localAddress.getPort()); - } - if (remoteAddress != null) { - builder.withRemoteAddress(remoteAddress.getHostString()) - .withRemotePort(remoteAddress.getPort()); - } - if (sslProtocol != null && sslCipherSuite != null && sslSessionId != null) { - builder.withSslProtocol(sslProtocol) - .withSslCipherSuite(sslCipherSuite) - .withSslSessionId(HexDump.toHexString(sslSessionId)); - } - if (sslSniServerNames != null) { - sslSniServerNames.stream() - .filter(name -> name instanceof SNIHostName && name.getType() == StandardConstants.SNI_HOST_NAME) - .map(name -> ((SNIHostName) name).getAsciiName()) - .findAny() - .ifPresent(builder::withSslSniServerName); - } - if (sslPeerSubject != null && sslPeerNotAfter != null && sslPeerNotBefore != null) { - builder.withSslPeerSubject(sslPeerSubject) - .withSslPeerNotAfter(sslPeerNotAfter.toInstant()) - .withSslPeerNotBefore(sslPeerNotBefore.toInstant()); - } - if (sslHandshakeException != null) { - List exceptionChain = new ArrayList<>(); - Throwable cause = sslHandshakeException; - while (cause != null) { - exceptionChain.add(new ExceptionEntry(cause.getClass().getName(), cause.getMessage())); - cause = cause.getCause(); - } - String type = SslHandshakeFailure.fromSslHandshakeException(sslHandshakeException) - .map(SslHandshakeFailure::failureType) - .orElse("UNKNOWN"); - builder.withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure(type, exceptionChain)); - } - return builder.build(); - } - - } - - private static class IdentityKey { - final T instance; - - IdentityKey(T instance) { this.instance = instance; } - - static IdentityKey of(T instance) { return new IdentityKey<>(instance); } - - @Override public int hashCode() { return System.identityHashCode(instance); } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!(obj instanceof IdentityKey)) return false; - IdentityKey other = (IdentityKey) obj; - return this.instance == other.instance; - } - } -} 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 deleted file mode 100644 index 510c561c10f..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyHttpServer.java +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.inject.Inject; -import com.yahoo.component.ComponentId; -import com.yahoo.component.provider.ComponentRegistry; -import com.yahoo.concurrent.DaemonThreadFactory; -import com.yahoo.container.logging.AccessLog; -import com.yahoo.container.logging.ConnectionLog; -import com.yahoo.container.logging.RequestLog; -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ServerConfig; -import com.yahoo.jdisc.http.ServletPathsConfig; -import com.yahoo.jdisc.service.AbstractServerProvider; -import com.yahoo.jdisc.service.CurrentContainer; -import org.eclipse.jetty.http.HttpField; -import org.eclipse.jetty.jmx.ConnectorServer; -import org.eclipse.jetty.jmx.MBeanContainer; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.server.handler.gzip.GzipHttpOutputInterceptor; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.log.JavaUtilLog; -import org.eclipse.jetty.util.log.Log; -import org.eclipse.jetty.util.thread.QueuedThreadPool; - -import javax.management.remote.JMXServiceURL; -import javax.servlet.DispatcherType; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.net.BindException; -import java.net.MalformedURLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import static java.util.stream.Collectors.toList; - -/** - * @author Simon Thoresen Hult - * @author bjorncs - */ -public class JettyHttpServer extends AbstractServerProvider { - - private final static Logger log = Logger.getLogger(JettyHttpServer.class.getName()); - - private final ExecutorService janitor; - - private final Server server; - private final List listenedPorts = new ArrayList<>(); - private final ServerMetricReporter metricsReporter; - - @Inject - public JettyHttpServer(CurrentContainer container, - Metric metric, - ServerConfig serverConfig, - ServletPathsConfig servletPathsConfig, - FilterBindings filterBindings, - ComponentRegistry connectorFactories, - ComponentRegistry servletHolders, - FilterInvoker filterInvoker, - RequestLog requestLog, - ConnectionLog connectionLog) { - super(container); - if (connectorFactories.allComponents().isEmpty()) - throw new IllegalArgumentException("No connectors configured."); - - initializeJettyLogging(); - - server = new Server(); - server.setStopTimeout((long)(serverConfig.stopTimeout() * 1000.0)); - server.setRequestLog(new AccessLogRequestLog(requestLog, serverConfig.accessLog())); - setupJmx(server, serverConfig); - configureJettyThreadpool(server, serverConfig); - JettyConnectionLogger connectionLogger = new JettyConnectionLogger(serverConfig.connectionLog(), connectionLog); - - for (ConnectorFactory connectorFactory : connectorFactories.allComponents()) { - ConnectorConfig connectorConfig = connectorFactory.getConnectorConfig(); - server.addConnector(connectorFactory.createConnector(metric, server, connectionLogger)); - listenedPorts.add(connectorConfig.listenPort()); - } - - janitor = newJanitor(); - - JDiscContext jDiscContext = new JDiscContext(filterBindings, - container, - janitor, - metric, - serverConfig); - - ServletHolder jdiscServlet = new ServletHolder(new JDiscHttpServlet(jDiscContext)); - FilterHolder jDiscFilterInvokerFilter = new FilterHolder(new JDiscFilterInvokerFilter(jDiscContext, filterInvoker)); - - List connectors = Arrays.stream(server.getConnectors()) - .map(JDiscServerConnector.class::cast) - .collect(toList()); - - server.setHandler(getHandlerCollection(serverConfig, - servletPathsConfig, - connectors, - jdiscServlet, - servletHolders, - jDiscFilterInvokerFilter)); - this.metricsReporter = new ServerMetricReporter(metric, server); - } - - private static void initializeJettyLogging() { - // Note: Jetty is logging stderr if no logger is explicitly configured - try { - Log.setLog(new JavaUtilLog()); - } catch (Exception e) { - throw new RuntimeException("Unable to initialize logging framework for Jetty"); - } - } - - private static void setupJmx(Server server, ServerConfig serverConfig) { - if (serverConfig.jmx().enabled()) { - System.setProperty("java.rmi.server.hostname", "localhost"); - server.addBean(new MBeanContainer(ManagementFactory.getPlatformMBeanServer())); - server.addBean(new ConnectorServer(createJmxLoopbackOnlyServiceUrl(serverConfig.jmx().listenPort()), - "org.eclipse.jetty.jmx:name=rmiconnectorserver")); - } - } - - private static void configureJettyThreadpool(Server server, ServerConfig config) { - QueuedThreadPool pool = (QueuedThreadPool) server.getThreadPool(); - pool.setMaxThreads(config.maxWorkerThreads()); - pool.setMinThreads(config.minWorkerThreads()); - } - - private static JMXServiceURL createJmxLoopbackOnlyServiceUrl(int port) { - try { - return new JMXServiceURL("rmi", "localhost", port, "/jndi/rmi://localhost:" + port + "/jmxrmi"); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - private HandlerCollection getHandlerCollection(ServerConfig serverConfig, - ServletPathsConfig servletPathsConfig, - List connectors, - ServletHolder jdiscServlet, - ComponentRegistry servletHolders, - FilterHolder jDiscFilterInvokerFilter) { - 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, "/*"); - - List connectorConfigs = connectors.stream().map(JDiscServerConnector::connectorConfig).collect(toList()); - var secureRedirectHandler = new SecuredRedirectHandler(connectorConfigs); - secureRedirectHandler.setHandler(servletContextHandler); - - var proxyHandler = new HealthCheckProxyHandler(connectors); - proxyHandler.setHandler(secureRedirectHandler); - - var authEnforcer = new TlsClientAuthenticationEnforcer(connectorConfigs); - authEnforcer.setHandler(proxyHandler); - - GzipHandler gzipHandler = newGzipHandler(serverConfig); - gzipHandler.setHandler(authEnforcer); - - HttpResponseStatisticsCollector statisticsCollector = - new HttpResponseStatisticsCollector(serverConfig.metric().monitoringHandlerPaths(), - serverConfig.metric().searchHandlerPaths()); - statisticsCollector.setHandler(gzipHandler); - - StatisticsHandler statisticsHandler = newStatisticsHandler(); - statisticsHandler.setHandler(statisticsCollector); - - HandlerCollection handlerCollection = new HandlerCollection(); - handlerCollection.setHandlers(new Handler[] { statisticsHandler }); - return handlerCollection; - } - - private static String getServletPath(ServletPathsConfig servletPathsConfig, ComponentId id) { - return "/" + servletPathsConfig.servlets(id.stringValue()).path(); - } - - private ServletContextHandler createServletContextHandler() { - ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SECURITY | ServletContextHandler.NO_SESSIONS); - servletContextHandler.setContextPath("/"); - servletContextHandler.setDisplayName(getDisplayName(listenedPorts)); - return servletContextHandler; - } - - private static String getDisplayName(List ports) { - return ports.stream().map(Object::toString).collect(Collectors.joining(":")); - } - - // Separate threadpool for tasks that cannot be executed on the jdisc default threadpool due to risk of deadlock - private static ExecutorService newJanitor() { - int threadPoolSize = Math.max(1, Runtime.getRuntime().availableProcessors()/8); - log.info("Creating janitor executor with " + threadPoolSize + " threads"); - return Executors.newFixedThreadPool( - threadPoolSize, - new DaemonThreadFactory(JettyHttpServer.class.getName() + "-Janitor-")); - } - - @Override - public void start() { - try { - server.start(); - metricsReporter.start(); - logEffectiveSslConfiguration(); - } catch (final Exception e) { - if (e instanceof IOException && e.getCause() instanceof BindException) { - throw new RuntimeException("Failed to start server due to BindException. ListenPorts = " + listenedPorts.toString(), e.getCause()); - } - throw new RuntimeException("Failed to start server.", e); - } - } - - private void logEffectiveSslConfiguration() { - if (!server.isStarted()) throw new IllegalStateException(); - for (Connector connector : server.getConnectors()) { - ServerConnector serverConnector = (ServerConnector) connector; - int localPort = serverConnector.getLocalPort(); - var sslConnectionFactory = serverConnector.getConnectionFactory(SslConnectionFactory.class); - if (sslConnectionFactory != null) { - var sslContextFactory = sslConnectionFactory.getSslContextFactory(); - log.info(String.format("Enabled SSL cipher suites for port '%d': %s", - localPort, Arrays.toString(sslContextFactory.getSelectedCipherSuites()))); - log.info(String.format("Enabled SSL protocols for port '%d': %s", - localPort, Arrays.toString(sslContextFactory.getSelectedProtocols()))); - } - } - } - - @Override - public void close() { - try { - log.log(Level.INFO, String.format("Shutting down server (graceful=%b, timeout=%.1fs)", isGracefulShutdownEnabled(), server.getStopTimeout()/1000d)); - server.stop(); - log.log(Level.INFO, "Server shutdown completed"); - } catch (final Exception e) { - log.log(Level.SEVERE, "Server shutdown threw an unexpected exception.", e); - } - - metricsReporter.shutdown(); - janitor.shutdown(); - } - - private boolean isGracefulShutdownEnabled() { - return server.getChildHandlersByClass(StatisticsHandler.class).length > 0 && server.getStopTimeout() > 0; - } - - public int getListenPort() { - return ((ServerConnector)server.getConnectors()[0]).getLocalPort(); - } - - Server server() { return server; } - - private StatisticsHandler newStatisticsHandler() { - StatisticsHandler statisticsHandler = new StatisticsHandler(); - statisticsHandler.statsReset(); - return statisticsHandler; - } - - private GzipHandler newGzipHandler(ServerConfig serverConfig) { - GzipHandler gzipHandler = new GzipHandlerWithVaryHeaderFixed(); - gzipHandler.setCompressionLevel(serverConfig.responseCompressionLevel()); - gzipHandler.setInflateBufferSize(8 * 1024); - gzipHandler.setIncludedMethods("GET", "POST", "PUT", "PATCH"); - return gzipHandler; - } - - /** A subclass which overrides Jetty's default behavior of including user-agent in the vary field */ - private static class GzipHandlerWithVaryHeaderFixed extends GzipHandler { - - @Override - public HttpField getVaryField() { - return GzipHttpOutputInterceptor.VARY_ACCEPT_ENCODING; - } - - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java deleted file mode 100644 index 5e953179b53..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/MetricDefinitions.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -/** - * Name and dimensions for jdisc/container metrics - * - * @author bjorncs - */ -class MetricDefinitions { - static final String NAME_DIMENSION = "serverName"; - static final String PORT_DIMENSION = "serverPort"; - static final String METHOD_DIMENSION = "httpMethod"; - static final String SCHEME_DIMENSION = "scheme"; - static final String REQUEST_TYPE_DIMENSION = "requestType"; - static final String CLIENT_IP_DIMENSION = "clientIp"; - static final String CLIENT_AUTHENTICATED_DIMENSION = "clientAuthenticated"; - static final String REQUEST_SERVER_NAME_DIMENSION = "requestServerName"; - static final String FILTER_CHAIN_ID_DIMENSION = "chainId"; - - static final String NUM_OPEN_CONNECTIONS = "serverNumOpenConnections"; - static final String NUM_CONNECTIONS_OPEN_MAX = "serverConnectionsOpenMax"; - static final String CONNECTION_DURATION_MAX = "serverConnectionDurationMax"; - static final String CONNECTION_DURATION_MEAN = "serverConnectionDurationMean"; - static final String CONNECTION_DURATION_STD_DEV = "serverConnectionDurationStdDev"; - static final String NUM_PREMATURELY_CLOSED_CONNECTIONS = "jdisc.http.request.prematurely_closed"; - - static final String NUM_BYTES_RECEIVED = "serverBytesReceived"; - static final String NUM_BYTES_SENT = "serverBytesSent"; - - static final String NUM_CONNECTIONS = "serverNumConnections"; - - /* For historical reasons, these are all aliases for the same metric. 'jdisc.http' should ideally be the only one. */ - static final String JDISC_HTTP_REQUESTS = "jdisc.http.requests"; - static final String NUM_REQUESTS = "serverNumRequests"; - - static final String NUM_SUCCESSFUL_RESPONSES = "serverNumSuccessfulResponses"; - static final String NUM_FAILED_RESPONSES = "serverNumFailedResponses"; - static final String NUM_SUCCESSFUL_WRITES = "serverNumSuccessfulResponseWrites"; - static final String NUM_FAILED_WRITES = "serverNumFailedResponseWrites"; - - static final String TOTAL_SUCCESSFUL_LATENCY = "serverTotalSuccessfulResponseLatency"; - static final String TOTAL_FAILED_LATENCY = "serverTotalFailedResponseLatency"; - static final String TIME_TO_FIRST_BYTE = "serverTimeToFirstByte"; - - static final String RESPONSES_1XX = "http.status.1xx"; - static final String RESPONSES_2XX = "http.status.2xx"; - static final String RESPONSES_3XX = "http.status.3xx"; - static final String RESPONSES_4XX = "http.status.4xx"; - static final String RESPONSES_5XX = "http.status.5xx"; - static final String RESPONSES_401 = "http.status.401"; - static final String RESPONSES_403 = "http.status.403"; - - static final String STARTED_MILLIS = "serverStartedMillis"; - - static final String URI_LENGTH = "jdisc.http.request.uri_length"; - static final String CONTENT_SIZE = "jdisc.http.request.content_size"; - - static final String SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.missing_client_cert"; - static final String SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.expired_client_cert"; - static final String SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT = "jdisc.http.ssl.handshake.failure.invalid_client_cert"; - static final String SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS = "jdisc.http.ssl.handshake.failure.incompatible_protocols"; - static final String SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS = "jdisc.http.ssl.handshake.failure.incompatible_ciphers"; - static final String SSL_HANDSHAKE_FAILURE_UNKNOWN = "jdisc.http.ssl.handshake.failure.unknown"; - - static final String JETTY_THREADPOOL_MAX_THREADS = "jdisc.http.jetty.threadpool.thread.max"; - static final String JETTY_THREADPOOL_MIN_THREADS = "jdisc.http.jetty.threadpool.thread.min"; - static final String JETTY_THREADPOOL_RESERVED_THREADS = "jdisc.http.jetty.threadpool.thread.reserved"; - static final String JETTY_THREADPOOL_BUSY_THREADS = "jdisc.http.jetty.threadpool.thread.busy"; - static final String JETTY_THREADPOOL_IDLE_THREADS = "jdisc.http.jetty.threadpool.thread.idle"; - static final String JETTY_THREADPOOL_TOTAL_THREADS = "jdisc.http.jetty.threadpool.thread.total"; - static final String JETTY_THREADPOOL_QUEUE_SIZE = "jdisc.http.jetty.threadpool.queue.size"; - - static final String FILTERING_REQUEST_HANDLED = "jdisc.http.filtering.request.handled"; - static final String FILTERING_REQUEST_UNHANDLED = "jdisc.http.filtering.request.unhandled"; - static final String FILTERING_RESPONSE_HANDLED = "jdisc.http.filtering.response.handled"; - static final String FILTERING_RESPONSE_UNHANDLED = "jdisc.http.filtering.response.unhandled"; - - private MetricDefinitions() {} -} 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 deleted file mode 100644 index eb83d3d7d03..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/OneTimeRunnable.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * @author Tony Vaagenes - */ -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 deleted file mode 100644 index f2bf5b56d5c..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ReferenceCountingRequestHandler.java +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.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 bakksjo - */ -@SuppressWarnings("try") -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 deleted file mode 100644 index eea69cd7f74..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestException.java +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -/** - * 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 bakksjo - */ -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/RequestMetricReporter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java deleted file mode 100644 index 7596be0415a..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/RequestMetricReporter.java +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.Metric.Context; - -import java.util.concurrent.atomic.AtomicBoolean; - - -/** - * Responsible for metric reporting for JDisc http request handler support. - * @author Tony Vaagenes - */ -class RequestMetricReporter { - private final Metric metric; - private final Context context; - - private final long requestStartTime; - - //TODO: rename - private final AtomicBoolean firstSetOfTimeToFirstByte = new AtomicBoolean(true); - - - RequestMetricReporter(Metric metric, Context context, long requestStartTime) { - this.metric = metric; - this.context = context; - this.requestStartTime = requestStartTime; - } - - void successfulWrite(int numBytes) { - setTimeToFirstByteFirstTime(); - - metric.add(MetricDefinitions.NUM_SUCCESSFUL_WRITES, 1, context); - metric.set(MetricDefinitions.NUM_BYTES_SENT, numBytes, context); - } - - private void setTimeToFirstByteFirstTime() { - boolean isFirstWrite = firstSetOfTimeToFirstByte.getAndSet(false); - if (isFirstWrite) { - long timeToFirstByte = getRequestLatency(); - metric.set(MetricDefinitions.TIME_TO_FIRST_BYTE, timeToFirstByte, context); - } - } - - void failedWrite() { - metric.add(MetricDefinitions.NUM_FAILED_WRITES, 1, context); - } - - void successfulResponse() { - setTimeToFirstByteFirstTime(); - - long requestLatency = getRequestLatency(); - - metric.set(MetricDefinitions.TOTAL_SUCCESSFUL_LATENCY, requestLatency, context); - - metric.add(MetricDefinitions.NUM_SUCCESSFUL_RESPONSES, 1, context); - } - - void failedResponse() { - setTimeToFirstByteFirstTime(); - - metric.set(MetricDefinitions.TOTAL_FAILED_LATENCY, getRequestLatency(), context); - metric.add(MetricDefinitions.NUM_FAILED_RESPONSES, 1, context); - } - - void prematurelyClosed() { - metric.add(MetricDefinitions.NUM_PREMATURELY_CLOSED_CONNECTIONS, 1, context); - } - - void successfulRead(int bytes_received) { - metric.set(MetricDefinitions.NUM_BYTES_RECEIVED, bytes_received, context); - } - - private long getRequestLatency() { - return System.currentTimeMillis() - requestStartTime; - } - - void uriLength(int length) { - metric.set(MetricDefinitions.URI_LENGTH, length, context); - } - - void contentSize(int size) { - metric.set(MetricDefinitions.CONTENT_SIZE, size, context); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java deleted file mode 100644 index e32c9d46deb..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SecuredRedirectHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.ConnectorConfig; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.URIUtil; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; - -/** - * A secure redirect handler inspired by {@link org.eclipse.jetty.server.handler.SecuredRedirectHandler}. - * - * @author bjorncs - */ -class SecuredRedirectHandler extends HandlerWrapper { - - private static final String HEALTH_CHECK_PATH = "/status.html"; - - private final Map redirectMap; - - SecuredRedirectHandler(List connectorConfigs) { - this.redirectMap = createRedirectMap(connectorConfigs); - } - - @Override - public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { - int localPort = getConnectorLocalPort(servletRequest); - if (!redirectMap.containsKey(localPort)) { - _handler.handle(target, request, servletRequest, servletResponse); - return; - } - servletResponse.setContentLength(0); - if (!servletRequest.getRequestURI().equals(HEALTH_CHECK_PATH)) { - servletResponse.sendRedirect( - URIUtil.newURI("https", request.getServerName(), redirectMap.get(localPort), request.getRequestURI(), request.getQueryString())); - } - request.setHandled(true); - } - - private static Map createRedirectMap(List connectorConfigs) { - var redirectMap = new HashMap(); - for (ConnectorConfig connectorConfig : connectorConfigs) { - if (connectorConfig.secureRedirect().enabled()) { - redirectMap.put(connectorConfig.listenPort(), connectorConfig.secureRedirect().port()); - } - } - return redirectMap; - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java deleted file mode 100644 index ba3694ffc2f..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServerMetricReporter.java +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.concurrent.DaemonThreadFactory; -import com.yahoo.jdisc.Metric; -import org.eclipse.jetty.io.ConnectionStatistics; -import org.eclipse.jetty.server.Connector; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AbstractHandlerContainer; -import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.util.thread.QueuedThreadPool; - -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** - * Reports server/connector specific metrics for Jdisc and Jetty - * - * @author bjorncs - */ -class ServerMetricReporter { - - private final ScheduledExecutorService executor = - Executors.newScheduledThreadPool(1, new DaemonThreadFactory("jdisc-jetty-metric-reporter-")); - private final Metric metric; - private final Server jetty; - - ServerMetricReporter(Metric metric, Server jetty) { - this.metric = metric; - this.jetty = jetty; - } - - void start() { - executor.scheduleAtFixedRate(new ReporterTask(), 0, 2, TimeUnit.SECONDS); - } - - void shutdown() { - try { - executor.shutdownNow(); - executor.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - private class ReporterTask implements Runnable { - - private final Instant timeStarted = Instant.now(); - - @Override - public void run() { - HttpResponseStatisticsCollector statisticsCollector = ((AbstractHandlerContainer) jetty.getHandler()) - .getChildHandlerByClass(HttpResponseStatisticsCollector.class); - if (statisticsCollector != null) { - setServerMetrics(statisticsCollector); - } - - // reset statisticsHandler to preserve earlier behavior - StatisticsHandler statisticsHandler = ((AbstractHandlerContainer) jetty.getHandler()) - .getChildHandlerByClass(StatisticsHandler.class); - if (statisticsHandler != null) { - statisticsHandler.statsReset(); - } - - for (Connector connector : jetty.getConnectors()) { - setConnectorMetrics((JDiscServerConnector)connector); - } - - setJettyThreadpoolMetrics(); - } - - private void setServerMetrics(HttpResponseStatisticsCollector statisticsCollector) { - long timeSinceStarted = System.currentTimeMillis() - timeStarted.toEpochMilli(); - metric.set(MetricDefinitions.STARTED_MILLIS, timeSinceStarted, null); - - addResponseMetrics(statisticsCollector); - } - - private void addResponseMetrics(HttpResponseStatisticsCollector statisticsCollector) { - for (var metricEntry : statisticsCollector.takeStatistics()) { - Map dimensions = new HashMap<>(); - dimensions.put(MetricDefinitions.METHOD_DIMENSION, metricEntry.method); - dimensions.put(MetricDefinitions.SCHEME_DIMENSION, metricEntry.scheme); - dimensions.put(MetricDefinitions.REQUEST_TYPE_DIMENSION, metricEntry.requestType); - metric.add(metricEntry.name, metricEntry.value, metric.createContext(dimensions)); - } - } - - private void setJettyThreadpoolMetrics() { - QueuedThreadPool threadpool = (QueuedThreadPool) jetty.getThreadPool(); - metric.set(MetricDefinitions.JETTY_THREADPOOL_MAX_THREADS, threadpool.getMaxThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_MIN_THREADS, threadpool.getMinThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_RESERVED_THREADS, threadpool.getReservedThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_BUSY_THREADS, threadpool.getBusyThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_IDLE_THREADS, threadpool.getIdleThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_TOTAL_THREADS, threadpool.getThreads(), null); - metric.set(MetricDefinitions.JETTY_THREADPOOL_QUEUE_SIZE, threadpool.getQueueSize(), null); - } - - private void setConnectorMetrics(JDiscServerConnector connector) { - ConnectionStatistics statistics = connector.getStatistics(); - metric.set(MetricDefinitions.NUM_CONNECTIONS, statistics.getConnectionsTotal(), connector.getConnectorMetricContext()); - metric.set(MetricDefinitions.NUM_OPEN_CONNECTIONS, statistics.getConnections(), connector.getConnectorMetricContext()); - metric.set(MetricDefinitions.NUM_CONNECTIONS_OPEN_MAX, statistics.getConnectionsMax(), connector.getConnectorMetricContext()); - metric.set(MetricDefinitions.CONNECTION_DURATION_MAX, statistics.getConnectionDurationMax(), connector.getConnectorMetricContext()); - metric.set(MetricDefinitions.CONNECTION_DURATION_MEAN, statistics.getConnectionDurationMean(), connector.getConnectorMetricContext()); - metric.set(MetricDefinitions.CONNECTION_DURATION_STD_DEV, statistics.getConnectionDurationStdDev(), connector.getConnectorMetricContext()); - } - - } -} 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 deleted file mode 100644 index b4d03385c3b..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletOutputStreamWriter.java +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.handler.CompletionHandler; - -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.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; - -/** - * @author Tony Vaagenes - * @author bjorncs - */ -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()); - - // If so, application code could fake a close by writing such a byte buffer. - // The problem can be solved by filtering out zero-length byte buffers from application code. - // Other ways to express this are also possible, e.g. with a 'closed' state checked when queue goes empty. - 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 responseContentQueue = new ArrayDeque<>(); - - private final RequestMetricReporter 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 finishedFuture = new CompletableFuture<>(); - - - public ServletOutputStreamWriter(ServletOutputStream outputStream, Executor executor, RequestMetricReporter metricReporter) { - this.outputStream = outputStream; - this.executor = executor; - this.metricReporter = metricReporter; - } - - public void sendErrorContentAndCloseAsync(ByteBuffer errorContent) { - synchronized (monitor) { - // Assert that no content has been written as it is too late to write error response if the response is committed. - assertStateIs(state, State.NOT_STARTED); - queueErrorContent_holdingLock(errorContent); - state = State.WAITING_FOR_WRITE_POSSIBLE_CALLBACK; - outputStream.setWriteListener(writeListener); - } - } - - private void queueErrorContent_holdingLock(ByteBuffer errorContent) { - responseContentQueue.addLast(new ResponseContentPart(errorContent, NOOP_COMPLETION_HANDLER)); - responseContentQueue.addLast(new ResponseContentPart(CLOSE_STREAM_BUFFER, NOOP_COMPLETION_HANDLER)); - } - - public void writeBuffer(ByteBuffer buf, CompletionHandler handler) { - boolean thisThreadShouldWrite = false; - - synchronized (monitor) { - if (state == State.FINISHED_OR_ERROR) { - 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); - } - - public void close() { - close(NOOP_COMPLETION_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) { - callCompletionHandlerWhenDone(contentPart.handler, outputStream::close); - setFinished(Optional.empty()); - return; - } else { - writeBufferToOutputStream(contentPart); - } - } catch (Throwable e) { - setFinished(Optional.of(e)); - return; - } - } - } - - private void setFinished(Optional 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 failedParts = new ArrayList<>(responseContentQueue); - responseContentQueue.clear(); - - @SuppressWarnings("ThrowableInstanceNeverThrown") - RuntimeException failReason = new RuntimeException("Failing due to earlier ServletOutputStream write failure", e); - - Consumer failCompletionHandler = responseContentPart -> - runCompletionHandler_logOnExceptions( - () -> responseContentPart.handler.failed(failReason)); - - executor.execute( - () -> failedParts.forEach(failCompletionHandler)); - } - - 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; - } - }); - } - - private static void callCompletionHandlerWhenDone(CompletionHandler handler, IORunnable runnable) throws Exception { - try { - runnable.run(); - } catch (Throwable e) { - runCompletionHandler_logOnExceptions(() -> handler.failed(e)); - throw e; - } - handler.completed(); //Might throw an exception, handling in the enclosing scope. - } - - private static void runCompletionHandler_logOnExceptions(Runnable runnable) { - try { - runnable.run(); - } catch (Throwable e) { - log.log(Level.WARNING, "Unexpected exception from CompletionHandler.", e); - } - } - - private static 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)); - } - }; - - private static class ResponseContentPart { - public final ByteBuffer buf; - public final CompletionHandler handler; - - public ResponseContentPart(ByteBuffer buf, CompletionHandler handler) { - this.buf = buf; - this.handler = handler; - } - } - - @FunctionalInterface - private interface IORunnable { - void run() throws IOException; - } -} 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 deleted file mode 100644 index 1882448757a..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.google.common.base.Preconditions; -import com.yahoo.jdisc.handler.CompletionHandler; -import com.yahoo.jdisc.handler.ContentChannel; - -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 BUFFER_SIZE_BYTES = 8 * 1024; - - private final Object monitor = new Object(); - - private final ServletInputStream servletInputStream; - private final ContentChannel requestContentChannel; - - private final Executor executor; - private final RequestMetricReporter metricReporter; - - private int bytesRead; - - /** - * 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 finishedFuture = new CompletableFuture<>(); - - public ServletRequestReader( - ServletInputStream servletInputStream, - ContentChannel requestContentChannel, - Executor executor, - RequestMetricReporter 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 byte[] buffer = new byte[BUFFER_SIZE_BYTES]; - int numBytesRead; - - synchronized (monitor) { - numBytesRead = servletInputStream.read(buffer); - if (numBytesRead < 0) { - // End of stream; there should be no more data available, ever. - return; - } - 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; - bytesRead += numBytesRead; - } - - try { - requestContentChannel.write(ByteBuffer.wrap(buffer, 0, numBytesRead), writeCompletionHandler); - metricReporter.successfulRead(numBytesRead); - } - catch (Throwable t) { - finishedFuture.completeExceptionally(t); - } - finally { - //decrease due to this method completing. - decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally(); - } - } - } - - private void decreaseOutstandingUserCallsAndCloseRequestContentChannelConditionally() { - 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; - - int bytesRead; - synchronized (monitor) { - if (state != State.READING) { - return; - } - - state = State.ALL_DATA_READ; - - shouldCloseRequestContentChannel = numberOfOutstandingUserCalls == 0; - if (shouldCloseRequestContentChannel) { - state = State.REQUEST_CONTENT_CLOSED; - } - bytesRead = this.bytesRead; - } - - if (shouldCloseRequestContentChannel) { - closeCompletionHandler_noThrow(); - } - - metricReporter.contentSize(bytesRead); - } - - 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 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 deleted file mode 100644 index 60b7878156f..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletResponseController.java +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.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.HttpHeaders; -import com.yahoo.jdisc.http.HttpResponse; -import com.yahoo.jdisc.service.BindingSetNotFoundException; -import org.eclipse.jetty.http.MimeTypes; - -import javax.servlet.http.HttpServletRequest; -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.Executor; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMPLETION_HANDLER; - -/** - * @author Tony Vaagenes - * @author bjorncs - */ -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 HttpServletRequest servletRequest; - private final HttpServletResponse servletResponse; - private final boolean developerMode; - private final ErrorResponseContentCreator errorResponseContentCreator = new ErrorResponseContentCreator(); - - //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( - HttpServletRequest servletRequest, - HttpServletResponse servletResponse, - Executor executor, - RequestMetricReporter metricReporter, - boolean developerMode) throws IOException { - - this.servletRequest = servletRequest; - 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; - try { - synchronized (monitor) { - String reasonPhrase = getReasonPhrase(t, developerMode); - int statusCode = getStatusCode(t); - responseWasCommitted = responseCommitted; - if (!responseCommitted) { - responseCommitted = true; - sendErrorAsync(statusCode, reasonPhrase); - } - } - } catch (Throwable e) { - servletOutputStreamWriter.fail(t); - return; - } - - //Must be evaluated after state transition for test purposes(See ConformanceTestException) - //Done outside the monitor since it causes a callback in tests. - 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(); - } - - } - - /** - * Async version of {@link org.eclipse.jetty.server.Response#sendError(int, String)}. - */ - private void sendErrorAsync(int statusCode, String reasonPhrase) { - servletResponse.setHeader(HttpHeaders.Names.EXPIRES, null); - servletResponse.setHeader(HttpHeaders.Names.LAST_MODIFIED, null); - servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, null); - servletResponse.setHeader(HttpHeaders.Names.CONTENT_TYPE, null); - servletResponse.setHeader(HttpHeaders.Names.CONTENT_LENGTH, null); - setStatus(servletResponse, statusCode, Optional.of(reasonPhrase)); - - // If we are allowed to have a body - if (statusCode != HttpServletResponse.SC_NO_CONTENT && - statusCode != HttpServletResponse.SC_NOT_MODIFIED && - statusCode != HttpServletResponse.SC_PARTIAL_CONTENT && - statusCode >= HttpServletResponse.SC_OK) { - servletResponse.setHeader(HttpHeaders.Names.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); - servletResponse.setContentType(MimeTypes.Type.TEXT_HTML_8859_1.toString()); - byte[] errorContent = errorResponseContentCreator - .createErrorContent(servletRequest.getRequestURI(), statusCode, Optional.ofNullable(reasonPhrase)); - servletResponse.setContentLength(errorContent.length); - servletOutputStreamWriter.sendErrorContentAndCloseAsync(ByteBuffer.wrap(errorContent)); - } else { - servletResponse.setContentLength(0); - servletOutputStreamWriter.close(); - } - } - - /** - * 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 finishedFuture() { - return servletOutputStreamWriter.finishedFuture; - } - - private void setResponse(Response jdiscResponse) { - synchronized (monitor) { - servletRequest.setAttribute(HttpResponseStatisticsCollector.requestTypeAttribute, jdiscResponse.getRequestType()); - 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(); - return; - } - - setStatus_holdingLock(jdiscResponse, servletResponse); - setHeaders_holdingLock(jdiscResponse, servletResponse); - } - } - - private static void setHeaders_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { - for (final Map.Entry entry : jdiscResponse.headers().entries()) { - servletResponse.addHeader(entry.getKey(), entry.getValue()); - } - - if (servletResponse.getContentType() == null) { - servletResponse.setContentType("text/plain;charset=utf-8"); - } - } - - private static void setStatus_holdingLock(Response jdiscResponse, HttpServletResponse servletResponse) { - if (jdiscResponse instanceof HttpResponse) { - setStatus(servletResponse, jdiscResponse.getStatus(), Optional.ofNullable(((HttpResponse) jdiscResponse).getMessage())); - } else { - setStatus(servletResponse, jdiscResponse.getStatus(), getErrorMessage(jdiscResponse)); - } - } - - @SuppressWarnings("deprecation") - private static void setStatus(HttpServletResponse response, int statusCode, Optional reasonPhrase) { - if (reasonPhrase.isPresent()) { - // Sets the status line: a status code along with a custom message. - // Using a custom status message is deprecated in the Servlet API. No alternative exist. - response.setStatus(statusCode, reasonPhrase.get()); // DEPRECATED - } else { - response.setStatus(statusCode); - } - } - - private static Optional 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, handlerOrNoopHandler(handler)); - } - - @Override - public void close(CompletionHandler handler) { - commitResponse(); - servletOutputStreamWriter.close(handlerOrNoopHandler(handler)); - } - - private CompletionHandler handlerOrNoopHandler(CompletionHandler handler) { - return handler != null ? handler : NOOP_COMPLETION_HANDLER; - } - }; -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java deleted file mode 100644 index 822e1c2ffb8..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailedListener.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2020 Oath 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 org.eclipse.jetty.io.ssl.SslHandshakeListener; - -import javax.net.ssl.SSLHandshakeException; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * A {@link SslHandshakeListener} that reports metrics for SSL handshake failures. - * - * @author bjorncs - */ -class SslHandshakeFailedListener implements SslHandshakeListener { - - private final static Logger log = Logger.getLogger(SslHandshakeFailedListener.class.getName()); - - private final Metric metric; - private final String connectorName; - private final int listenPort; - - SslHandshakeFailedListener(Metric metric, String connectorName, int listenPort) { - this.metric = metric; - this.connectorName = connectorName; - this.listenPort = listenPort; - } - - @Override - public void handshakeFailed(Event event, Throwable throwable) { - log.log(Level.FINE, throwable, () -> "Ssl handshake failed: " + throwable.getMessage()); - String metricName = SslHandshakeFailure.fromSslHandshakeException((SSLHandshakeException) throwable) - .map(SslHandshakeFailure::metricName) - .orElse(MetricDefinitions.SSL_HANDSHAKE_FAILURE_UNKNOWN); - metric.add(metricName, 1L, metric.createContext(createDimensions(event))); - } - - private Map createDimensions(Event event) { - Map dimensions = new HashMap<>(); - dimensions.put(MetricDefinitions.NAME_DIMENSION, connectorName); - dimensions.put(MetricDefinitions.PORT_DIMENSION, listenPort); - Optional.ofNullable(event.getSSLEngine().getPeerHost()) - .ifPresent(clientIp -> dimensions.put(MetricDefinitions.CLIENT_IP_DIMENSION, clientIp)); - return Map.copyOf(dimensions); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java deleted file mode 100644 index 64f70564137..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/SslHandshakeFailure.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import javax.net.ssl.SSLHandshakeException; -import java.util.Optional; -import java.util.function.Predicate; -import java.util.regex.Pattern; - -/** - * Categorizes instances of {@link SSLHandshakeException} - * - * @author bjorncs - */ -enum SslHandshakeFailure { - INCOMPATIBLE_PROTOCOLS( - MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_PROTOCOLS, - "INCOMPATIBLE_CLIENT_PROTOCOLS", - "(Client requested protocol \\S+? is not enabled or supported in server context" + - "|The client supported protocol versions \\[.+?\\] are not accepted by server preferences \\[.+?\\])"), - INCOMPATIBLE_CIPHERS( - MetricDefinitions.SSL_HANDSHAKE_FAILURE_INCOMPATIBLE_CIPHERS, - "INCOMPATIBLE_CLIENT_CIPHER_SUITES", - "no cipher suites in common"), - MISSING_CLIENT_CERT( - MetricDefinitions.SSL_HANDSHAKE_FAILURE_MISSING_CLIENT_CERT, - "MISSING_CLIENT_CERTIFICATE", - "Empty (server|client) certificate chain"), - EXPIRED_CLIENT_CERTIFICATE( - MetricDefinitions.SSL_HANDSHAKE_FAILURE_EXPIRED_CLIENT_CERT, - "EXPIRED_CLIENT_CERTIFICATE", - // Note: this pattern will match certificates with too late notBefore as well - "PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed"), - INVALID_CLIENT_CERT( - MetricDefinitions.SSL_HANDSHAKE_FAILURE_INVALID_CLIENT_CERT, // Includes mismatch of client certificate and private key - "INVALID_CLIENT_CERTIFICATE", - "(PKIX path (building|validation) failed: .+)|(Invalid CertificateVerify signature)"); - - private final String metricName; - private final String failureType; - private final Predicate messageMatcher; - - SslHandshakeFailure(String metricName, String failureType, String messagePattern) { - this.metricName = metricName; - this.failureType = failureType; - this.messageMatcher = Pattern.compile(messagePattern).asMatchPredicate(); - } - - String metricName() { return metricName; } - String failureType() { return failureType; } - - static Optional fromSslHandshakeException(SSLHandshakeException exception) { - String message = exception.getMessage(); - if (message == null || message.isBlank()) return Optional.empty(); - for (SslHandshakeFailure failure : values()) { - if (failure.messageMatcher.test(message)) { - return Optional.of(failure); - } - } - return Optional.empty(); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java deleted file mode 100644 index 10a6c4702b5..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/TlsClientAuthenticationEnforcer.java +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.servlet.ServletRequest; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.HandlerWrapper; - -import javax.servlet.DispatcherType; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnectorLocalPort; - -/** - * A Jetty handler that enforces TLS client authentication with configurable white list. - * - * @author bjorncs - */ -class TlsClientAuthenticationEnforcer extends HandlerWrapper { - - private final Map> portToWhitelistedPathsMapping; - - TlsClientAuthenticationEnforcer(List connectorConfigs) { - portToWhitelistedPathsMapping = createWhitelistMapping(connectorConfigs); - } - - @Override - public void handle(String target, Request request, HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException { - if (isHttpsRequest(request) - && !isRequestToWhitelistedBinding(servletRequest) - && !isClientAuthenticated(servletRequest)) { - servletResponse.sendError( - Response.Status.UNAUTHORIZED, - "Client did not present a x509 certificate, " + - "or presented a certificate not issued by any of the CA certificates in trust store."); - } else { - _handler.handle(target, request, servletRequest, servletResponse); - } - } - - private static Map> createWhitelistMapping(List connectorConfigs) { - var mapping = new HashMap>(); - for (ConnectorConfig connectorConfig : connectorConfigs) { - var enforcerConfig = connectorConfig.tlsClientAuthEnforcer(); - if (enforcerConfig.enable()) { - mapping.put(connectorConfig.listenPort(), enforcerConfig.pathWhitelist()); - } - } - return mapping; - } - - private boolean isHttpsRequest(Request request) { - return request.getDispatcherType() == DispatcherType.REQUEST && request.getScheme().equalsIgnoreCase("https"); - } - - private boolean isRequestToWhitelistedBinding(HttpServletRequest servletRequest) { - int localPort = getConnectorLocalPort(servletRequest); - List whiteListedPaths = getWhitelistedPathsForPort(localPort); - if (whiteListedPaths == null) { - return true; // enforcer not enabled - } - // Note: Same path definition as HttpRequestFactory.getUri() - return whiteListedPaths.contains(servletRequest.getRequestURI()); - } - - private List getWhitelistedPathsForPort(int localPort) { - if (portToWhitelistedPathsMapping.containsKey(0) && portToWhitelistedPathsMapping.size() == 1) { - return portToWhitelistedPathsMapping.get(0); // for unit tests which uses 0 for listen port - } - return portToWhitelistedPathsMapping.get(localPort); - } - - private boolean isClientAuthenticated(HttpServletRequest servletRequest) { - return servletRequest.getAttribute(ServletRequest.SERVLET_REQUEST_X509CERT) != null; - } -} 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 deleted file mode 100644 index ce52bccf52d..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/UnsupportedFilterInvoker.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.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 Tony Vaagenes - */ -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/VoidConnectionLog.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java deleted file mode 100644 index 5d33cc0835e..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidConnectionLog.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. - -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.ConnectionLog; -import com.yahoo.container.logging.ConnectionLogEntry; - -/** - * @author mortent - */ -public class VoidConnectionLog implements ConnectionLog { - - @Override - public void log(ConnectionLogEntry connectionLogEntry) { - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java deleted file mode 100644 index 9db5ba99115..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/VoidRequestLog.java +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.RequestLog; -import com.yahoo.container.logging.RequestLogEntry; - -/** - * @author bjorncs - */ -public class VoidRequestLog implements RequestLog { - - @Override public void log(RequestLogEntry entry) {} - -} 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 deleted file mode 100644 index 189751aa9c0..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/server/jetty/package-info.java +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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/servlet/ServletOrJdiscHttpRequest.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java deleted file mode 100644 index eaac2b1c415..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.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; -import java.util.concurrent.TimeUnit; - -/** - * Common interface for JDisc and servlet http requests. - */ -public interface ServletOrJdiscHttpRequest { - - void copyHeaders(HeaderFields target); - - Map> parameters(); - - URI getUri(); - - HttpRequest.Version getVersion(); - - String getRemoteHostAddress(); - String getRemoteHostName(); - int getRemotePort(); - - void setRemoteAddress(SocketAddress remoteAddress); - - Map context(); - - List decodeCookieHeader(); - - void encodeCookieHeader(List cookies); - - long getConnectedAt(TimeUnit unit); -} 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 deleted file mode 100644 index a24ada05b3d..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletOrJdiscHttpResponse.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.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 context(); - - public List 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 deleted file mode 100644 index c945dc6d8b6..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletRequest.java +++ /dev/null @@ -1,272 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.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.security.Principal; -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; -import java.util.concurrent.TimeUnit; - -import static com.yahoo.jdisc.http.server.jetty.HttpServletRequestUtils.getConnection; - -/** - * Mutable wrapper to use a {@link javax.servlet.http.HttpServletRequest} - * with JDisc security filters. - *

- * 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". - */ -public class ServletRequest extends HttpServletRequestWrapper implements ServletOrJdiscHttpRequest { - - public static final String JDISC_REQUEST_PRINCIPAL = "jdisc.request.principal"; - public static final String JDISC_REQUEST_X509CERT = "jdisc.request.X509Certificate"; - public static final String JDISC_REQUEST_CHAIN = "jdisc.request.chain"; - public static final String JDISC_RESPONSE_CHAIN = "jdisc.response.chain"; - public static final String SERVLET_REQUEST_X509CERT = "javax.servlet.request.X509Certificate"; - public static final String SERVLET_REQUEST_SSL_SESSION_ID = "javax.servlet.request.ssl_session_id"; - public static final String SERVLET_REQUEST_CIPHER_SUITE = "javax.servlet.request.cipher_suite"; - - private final HttpServletRequest request; - private final HeaderFields headerFields; - private final Set removedHeaders = new HashSet<>(); - private final Map context = new HashMap<>(); - private final Map> parameters = new HashMap<>(); - private final long connectedAt; - - 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(); - connectedAt = getConnection(request).getCreatedTimeStamp(); - - headerFields = new HeaderFields(); - Enumeration parentHeaders = request.getHeaderNames(); - while (parentHeaders.hasMoreElements()) { - String name = parentHeaders.nextElement(); - Enumeration values = request.getHeaders(name); - while (values.hasMoreElements()) { - headerFields.add(name, values.nextElement()); - } - } - } - - public HttpServletRequest getRequest() { - return request; - } - - @Override - public Map> 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 getParameterMap() { - Map 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 getParameterNames() { - return Collections.enumeration(parameters.keySet()); - } - - @Override - public String[] getParameterValues(String name) { - List 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 getHeaders(String name) { - if (removedHeaders.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 headerFields = this.headerFields.get(name); - return headerFields == null || headerFields.isEmpty() ? - super.getHeaders(name) : - Collections.enumeration(headerFields); - } - - @Override - public String getHeader(String name) { - if (removedHeaders.contains(name)) - return null; - - String headerField = headerFields.getFirst(name); - return headerField != null ? - headerField : - super.getHeader(name); - } - - @Override - public Enumeration getHeaderNames() { - Set names = new HashSet<>(Collections.list(super.getHeaderNames())); - names.addAll(headerFields.keySet()); - names.removeAll(removedHeaders); - return Collections.enumeration(names); - } - - public void addHeader(String name, String value) { - headerFields.add(name, value); - removedHeaders.remove(name); - } - - public void setHeaders(String name, String value) { - headerFields.put(name, value); - removedHeaders.remove(name); - } - - public void setHeaders(String name, List values) { - headerFields.put(name, values); - removedHeaders.remove(name); - } - - public void removeHeaders(String name) { - headerFields.remove(name); - removedHeaders.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 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 decodeCookieHeader() { - Enumeration cookies = getHeaders(HttpHeaders.Names.COOKIE); - if (cookies == null) - return Collections.emptyList(); - - List ret = new LinkedList<>(); - while(cookies.hasMoreElements()) - ret.addAll(Cookie.fromCookieHeader(cookies.nextElement())); - - return ret; - } - - @Override - public void encodeCookieHeader(List cookies) { - setHeaders(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies)); - } - - @Override - public long getConnectedAt(TimeUnit unit) { - return unit.convert(connectedAt, TimeUnit.MILLISECONDS); - } - - @Override - public Principal getUserPrincipal() { - // NOTE: The principal from the underlying servlet request is ignored. JDisc filters are the source-of-truth. - return (Principal) request.getAttribute(JDISC_REQUEST_PRINCIPAL); - } - - public void setUserPrincipal(Principal principal) { - request.setAttribute(JDISC_REQUEST_PRINCIPAL, principal); - } -} 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 deleted file mode 100644 index 48c8f577de9..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/ServletResponse.java +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.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. - */ -public class ServletResponse extends HttpServletResponseWrapper implements ServletOrJdiscHttpResponse { - - private final HttpServletResponse response; - private final Map 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 context() { - return context; - } - - @Override - public void copyHeaders(HeaderFields target) { - response.getHeaderNames().forEach( header -> - target.add(header, new ArrayList<>(response.getHeaders(header))) - ); - } - - @Override - public List decodeSetCookieHeader() { - Collection cookies = getHeaders(HttpHeaders.Names.SET_COOKIE); - if (cookies == null) { - return Collections.emptyList(); - } - List ret = new LinkedList<>(); - for (String cookie : cookies) { - ret.add(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 deleted file mode 100644 index 0120f164cae..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/servlet/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2017 Yahoo Holdings. 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/SslContextFactoryProvider.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactoryProvider.java deleted file mode 100644 index c364116e0af..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/SslContextFactoryProvider.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl; - -import org.eclipse.jetty.util.ssl.SslContextFactory; - -/** - * A provider that is used to configure SSL connectors in JDisc - * - * @author bjorncs - */ -public interface SslContextFactoryProvider extends AutoCloseable { - - /** - * This method is called once for each SSL connector. - * - * @return returns an instance of {@link SslContextFactory} for a given JDisc http server - */ - SslContextFactory getInstance(String containerId, int port); - - @Override default void close() {} -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java deleted file mode 100644 index 90848f1dfd4..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/ConfiguredSslContextFactoryProvider.java +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl.impl; - -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ConnectorConfig.Ssl.ClientAuth; -import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.SslContextBuilder; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.security.tls.AutoReloadingX509KeyManager; -import com.yahoo.security.tls.TlsContext; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import static com.yahoo.jdisc.http.ssl.impl.SslContextFactoryUtils.setEnabledCipherSuites; -import static com.yahoo.jdisc.http.ssl.impl.SslContextFactoryUtils.setEnabledProtocols; - -/** - * An implementation of {@link SslContextFactoryProvider} that uses the {@link ConnectorConfig} to construct a {@link SslContextFactory}. - * - * @author bjorncs - */ -public class ConfiguredSslContextFactoryProvider implements SslContextFactoryProvider { - - private volatile AutoReloadingX509KeyManager keyManager; - private final ConnectorConfig connectorConfig; - - public ConfiguredSslContextFactoryProvider(ConnectorConfig connectorConfig) { - validateConfig(connectorConfig.ssl()); - this.connectorConfig = connectorConfig; - } - - @Override - public SslContextFactory getInstance(String containerId, int port) { - ConnectorConfig.Ssl sslConfig = connectorConfig.ssl(); - if (!sslConfig.enabled()) throw new IllegalStateException(); - - SslContextBuilder builder = new SslContextBuilder(); - if (sslConfig.certificateFile().isBlank() || sslConfig.privateKeyFile().isBlank()) { - PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey(getPrivateKey(sslConfig)); - List certificates = X509CertificateUtils.certificateListFromPem(getCertificate(sslConfig)); - builder.withKeyStore(privateKey, certificates); - } else { - keyManager = AutoReloadingX509KeyManager.fromPemFiles(Paths.get(sslConfig.privateKeyFile()), Paths.get(sslConfig.certificateFile())); - builder.withKeyManager(keyManager); - } - List caCertificates = getCaCertificates(sslConfig) - .map(X509CertificateUtils::certificateListFromPem) - .orElse(List.of()); - builder.withTrustStore(caCertificates); - - SSLContext sslContext = builder.build(); - - SslContextFactory.Server factory = new SslContextFactory.Server(); - factory.setSslContext(sslContext); - - factory.setNeedClientAuth(sslConfig.clientAuth() == ClientAuth.Enum.NEED_AUTH); - factory.setWantClientAuth(sslConfig.clientAuth() == ClientAuth.Enum.WANT_AUTH); - - List protocols = !sslConfig.enabledProtocols().isEmpty() - ? sslConfig.enabledProtocols() - : new ArrayList<>(TlsContext.getAllowedProtocols(sslContext)); - setEnabledProtocols(factory, sslContext, protocols); - - List ciphers = !sslConfig.enabledCipherSuites().isEmpty() - ? sslConfig.enabledCipherSuites() - : new ArrayList<>(TlsContext.getAllowedCipherSuites(sslContext)); - setEnabledCipherSuites(factory, sslContext, ciphers); - - return factory; - } - - @Override - public void close() { - if (keyManager != null) { - keyManager.close(); - } - } - - private static void validateConfig(ConnectorConfig.Ssl config) { - if (!config.enabled()) return; - - if(hasBoth(config.certificate(), config.certificateFile())) - throw new IllegalArgumentException("Specified both certificate and certificate file."); - - if(hasBoth(config.privateKey(), config.privateKeyFile())) - throw new IllegalArgumentException("Specified both private key and private key file."); - - if(hasNeither(config.certificate(), config.certificateFile())) - throw new IllegalArgumentException("Specified neither certificate or certificate file."); - - if(hasNeither(config.privateKey(), config.privateKeyFile())) - throw new IllegalArgumentException("Specified neither private key or private key file."); - } - - private static boolean hasBoth(String a, String b) { return !a.isBlank() && !b.isBlank(); } - private static boolean hasNeither(String a, String b) { return a.isBlank() && b.isBlank(); } - - private static Optional getCaCertificates(ConnectorConfig.Ssl sslConfig) { - if (!sslConfig.caCertificate().isBlank()) { - return Optional.of(sslConfig.caCertificate()); - } else if (!sslConfig.caCertificateFile().isBlank()) { - return Optional.of(readToString(sslConfig.caCertificateFile())); - } else { - return Optional.empty(); - } - } - - private static String getPrivateKey(ConnectorConfig.Ssl config) { - if(!config.privateKey().isBlank()) return config.privateKey(); - return readToString(config.privateKeyFile()); - } - - private static String getCertificate(ConnectorConfig.Ssl config) { - if(!config.certificate().isBlank()) return config.certificate(); - return readToString(config.certificateFile()); - } - - private static String readToString(String filename) { - try { - return Files.readString(Paths.get(filename), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/DefaultSslContextFactoryProvider.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/DefaultSslContextFactoryProvider.java deleted file mode 100644 index 7395d2307af..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/DefaultSslContextFactoryProvider.java +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl.impl; - -import com.google.inject.Inject; -import com.yahoo.component.AbstractComponent; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider; -import com.yahoo.security.tls.ConfigFileBasedTlsContext; -import com.yahoo.security.tls.PeerAuthentication; -import com.yahoo.security.tls.TlsContext; -import com.yahoo.security.tls.TransportSecurityUtils; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import java.nio.file.Path; - -/** - * The default implementation of {@link SslContextFactoryProvider} to be injected into connectors without explicit ssl configuration. - * - * @author bjorncs - */ -public class DefaultSslContextFactoryProvider extends AbstractComponent implements SslContextFactoryProvider { - - private final SslContextFactoryProvider instance; - - @Inject - public DefaultSslContextFactoryProvider(ConnectorConfig connectorConfig) { - this.instance = TransportSecurityUtils.getConfigFile() - .map(configFile -> createTlsContextBasedProvider(connectorConfig, configFile)) - .orElseGet(ThrowingSslContextFactoryProvider::new); - } - - private static SslContextFactoryProvider createTlsContextBasedProvider(ConnectorConfig connectorConfig, Path configFile) { - return new StaticTlsContextBasedProvider( - new ConfigFileBasedTlsContext( - configFile, TransportSecurityUtils.getInsecureAuthorizationMode(), getPeerAuthenticationMode(connectorConfig))); - } - - /** - * Allows white-listing of user provided uri paths. - * JDisc will delegate the enforcement of peer authentication from the TLS to the HTTP layer if {@link ConnectorConfig.TlsClientAuthEnforcer#enable()} is true. - */ - private static PeerAuthentication getPeerAuthenticationMode(ConnectorConfig connectorConfig) { - return connectorConfig.tlsClientAuthEnforcer().enable() - ? PeerAuthentication.WANT - : PeerAuthentication.NEED; - } - - @Override - public SslContextFactory getInstance(String containerId, int port) { - return instance.getInstance(containerId, port); - } - - @Override - public void deconstruct() { - instance.close(); - } - - private static class ThrowingSslContextFactoryProvider implements SslContextFactoryProvider { - @Override - public SslContextFactory getInstance(String containerId, int port) { - throw new UnsupportedOperationException(); - } - } - - private static class StaticTlsContextBasedProvider extends TlsContextBasedProvider { - final TlsContext tlsContext; - - StaticTlsContextBasedProvider(TlsContext tlsContext) { - this.tlsContext = tlsContext; - } - - @Override - protected TlsContext getTlsContext(String containerId, int port) { - return tlsContext; - } - - @Override public void deconstruct() { tlsContext.close(); } - } -} \ No newline at end of file diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/JDiscSslContextFactory.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/JDiscSslContextFactory.java deleted file mode 100644 index 006a282e1e0..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/JDiscSslContextFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl.impl; - -import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.security.CertificateUtils; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import java.security.KeyStore; -import java.util.Objects; - -/** - * A modified {@link SslContextFactory} that allows passwordless truststore in combination with password protected keystore. - * - * @author bjorncs - */ -class JDiscSslContextFactory extends SslContextFactory.Server { - - private String trustStorePassword; - - @Override - public void setTrustStorePassword(String password) { - super.setTrustStorePassword(password); - this.trustStorePassword = password; - } - - - // Overriden to stop Jetty from using the keystore password if no truststore password is specified. - @Override - protected KeyStore loadTrustStore(Resource resource) throws Exception { - return CertificateUtils.getKeyStore( - resource != null ? resource : getKeyStoreResource(), - Objects.toString(getTrustStoreType(), getKeyStoreType()), - Objects.toString(getTrustStoreProvider(), getKeyStoreProvider()), - trustStorePassword); - } - -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/SslContextFactoryUtils.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/SslContextFactoryUtils.java deleted file mode 100644 index a0172668cbb..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/SslContextFactoryUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl.impl; - -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import javax.net.ssl.SSLContext; -import java.util.Arrays; -import java.util.List; - -/** - * @author bjorncs - */ -class SslContextFactoryUtils { - - static void setEnabledCipherSuites(SslContextFactory factory, SSLContext sslContext, List enabledCiphers) { - String[] supportedCiphers = sslContext.getSupportedSSLParameters().getCipherSuites(); - factory.setIncludeCipherSuites(enabledCiphers.toArray(String[]::new)); - factory.setExcludeCipherSuites(createExclusionList(enabledCiphers, supportedCiphers)); - } - - static void setEnabledProtocols(SslContextFactory factory, SSLContext sslContext, List enabledProtocols) { - String[] supportedProtocols = sslContext.getSupportedSSLParameters().getProtocols(); - factory.setIncludeProtocols(enabledProtocols.toArray(String[]::new)); - factory.setExcludeProtocols(createExclusionList(enabledProtocols, supportedProtocols)); - } - - private static String[] createExclusionList(List enabledValues, String[] supportedValues) { - return Arrays.stream(supportedValues) - .filter(supportedValue -> !enabledValues.contains(supportedValue)) - .toArray(String[]::new); - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProvider.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProvider.java deleted file mode 100644 index 93d4f1dca3f..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/TlsContextBasedProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.ssl.impl; - -import com.yahoo.component.AbstractComponent; -import com.yahoo.jdisc.http.ssl.SslContextFactoryProvider; -import com.yahoo.security.tls.TlsContext; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import java.util.List; - -import static com.yahoo.jdisc.http.ssl.impl.SslContextFactoryUtils.setEnabledCipherSuites; -import static com.yahoo.jdisc.http.ssl.impl.SslContextFactoryUtils.setEnabledProtocols; - -/** - * A {@link SslContextFactoryProvider} that creates {@link SslContextFactory} instances from {@link TlsContext} instances. - * - * @author bjorncs - */ -public abstract class TlsContextBasedProvider extends AbstractComponent implements SslContextFactoryProvider { - - protected abstract TlsContext getTlsContext(String containerId, int port); - - @Override - public final SslContextFactory getInstance(String containerId, int port) { - TlsContext tlsContext = getTlsContext(containerId, port); - SSLContext sslContext = tlsContext.context(); - SSLParameters parameters = tlsContext.parameters(); - - SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setSslContext(sslContext); - - sslContextFactory.setNeedClientAuth(parameters.getNeedClientAuth()); - sslContextFactory.setWantClientAuth(parameters.getWantClientAuth()); - - setEnabledProtocols(sslContextFactory, sslContext, List.of(parameters.getProtocols())); - setEnabledCipherSuites(sslContextFactory, sslContext, List.of(parameters.getCipherSuites())); - - return sslContextFactory; - } -} diff --git a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/package-info.java b/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/package-info.java deleted file mode 100644 index f337e9d010b..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/impl/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@ExportPackage -package com.yahoo.jdisc.http.ssl.impl; - -import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file 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 deleted file mode 100644 index 085e9dedf20..00000000000 --- a/jdisc_http_service/src/main/java/com/yahoo/jdisc/http/ssl/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author bjorncs - */ -@PublicApi -@ExportPackage -package com.yahoo.jdisc.http.ssl; - -import com.yahoo.api.annotations.PublicApi; -import com.yahoo.osgi.annotation.ExportPackage; diff --git a/jdisc_http_service/src/main/resources/configdefinitions/container.logging.connection-log.def b/jdisc_http_service/src/main/resources/configdefinitions/container.logging.connection-log.def deleted file mode 100644 index 39841dba6f2..00000000000 --- a/jdisc_http_service/src/main/resources/configdefinitions/container.logging.connection-log.def +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=container.logging - -# Name of the cluster -cluster string - -# Log directory name -logDirectoryName string default="qrs" - -# Max queue length of file handler -queueSize int default=10000 diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def deleted file mode 100644 index 8f99fccec94..00000000000 --- a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.client.jdisc.http.client.http-client.def +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=jdisc.http.client - -userAgent string default = "JDisc/1.0" -chunkedEncodingEnabled bool default = false -compressionEnabled bool default = false -connectionPoolEnabled bool default = true -followRedirects bool default = false -removeQueryParamsOnRedirect bool default = true -sslConnectionPoolEnabled bool default = true -proxyServer string default = "" -useProxyProperties bool default = false -useRawUri bool default = false -compressionLevel int default = -1 -maxNumConnections int default = -1 -maxNumConnectionsPerHost int default = -1 -maxNumRedirects int default = 5 -maxNumRetries int default = 0 -connectionTimeout double default = 60 -idleConnectionInPoolTimeout double default = 60 -idleConnectionTimeout double default = 60 -idleWebSocketTimeout double default = 15 -requestTimeout double default = 60 - -ssl.enabled bool default = false -ssl.keyStoreType string default = "JKS" - -# Vespa home is prepended is path is relative -ssl.keyStorePath string default = "jdisc_container/keyStore.jks" - -# Vespa home is prepended is path is relative -ssl.trustStorePath string default = "conf/jdisc_container/trustStore.jks" - -ssl.keyDBKey string default = "jdisc_container" -ssl.algorithm string default = "SunX509" -ssl.protocol string default = "TLS" diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def deleted file mode 100644 index 055e5ad62d2..00000000000 --- a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=jdisc.http - -# The TCP port to listen to for this connector. -listenPort int default=0 - -# The connector name -name string default="default" - -# The header field cache size. -headerCacheSize int default=512 - -# The size of the buffer into which response content is aggregated before being sent to the client. -outputBufferSize int default=65536 - -# The maximum size of a request header. -requestHeaderSize int default=65536 - -# The maximum size of a response header. -responseHeaderSize int default=65536 - -# The accept queue size (also known as accept backlog). -acceptQueueSize int default=0 - -# Whether the server socket reuses addresses. -reuseAddress bool default=true - -# The maximum idle time for a connection, which roughly translates to the Socket.setSoTimeout(int). -idleTimeout double default=180.0 - -# DEPRECATED - Ignored, no longer in use -stopTimeout double default = 30.0 -# TODO Vespa 8 Remove stop timeout - -# Whether or not to have socket keep alive turned on. -tcpKeepAliveEnabled bool default=false - -# Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm). -tcpNoDelay bool default=true - -# Whether to enable connection throttling. New connections will be dropped when a threshold is exceeded. -throttling.enabled bool default=false - -# Max number of connections. -throttling.maxConnections int default=-1 - -# Max memory utilization as a value between 0 and 1. -throttling.maxHeapUtilization double default=-1.0 - -# Max connection accept rate per second. -throttling.maxAcceptRate int default=-1 - -# Idle timeout in seconds applied to endpoints when a threshold is exceeded. -throttling.idleTimeout double default=-1.0 - -# Whether to enable TLS on connector when Vespa is configured with TLS. -# The connector will implicitly enable TLS if set to 'true' and Vespa TLS is enabled. -implicitTlsEnabled bool default=true - -# Whether to enable SSL for this connector. -ssl.enabled bool default=false - -# File with private key in PEM format. Specify either this or privateKey, but not both -ssl.privateKeyFile string default="" - -# Private key in PEM format. Specify either this or privateKeyFile, but not both -ssl.privateKey string default="" - -# File with certificate in PEM format. Specify either this or certificate, but not both -ssl.certificateFile string default="" - -# Certificate in PEM format. Specify either this or certificateFile, but not both -ssl.certificate string default="" - -# with trusted CA certificates in PEM format. Used to verify clients -# - this is the name of a file on the local container file system -# - only one of caCertificateFile and caCertificate -ssl.caCertificateFile string default="" - -# with trusted CA certificates in PEM format. Used to verify clients -# - this is the actual certificates instead of a pointer to the file -# - only one of caCertificateFile and caCertificate -ssl.caCertificate string default="" - -# Client authentication mode. See SSLEngine.getNeedClientAuth()/getWantClientAuth() for details. -ssl.clientAuth enum { DISABLED, WANT_AUTH, NEED_AUTH } default=DISABLED - -# List of enabled cipher suites. JDisc will use Vespa default if empty. -ssl.enabledCipherSuites[] string - -# List of enabled TLS protocol versions. JDisc will use Vespa default if empty. -ssl.enabledProtocols[] string - -# Enforce TLS client authentication for https requests at the http layer. -# Intended to be used with connectors with optional client authentication enabled. -# 401 status code is returned for requests from non-authenticated clients. -tlsClientAuthEnforcer.enable bool default=false - -# Paths where client authentication should not be enforced. To be used in combination with WANT_AUTH. Typically used for health checks. -tlsClientAuthEnforcer.pathWhitelist[] string - -# Use connector only for proxying '/status.html' health checks. Any ssl configuration will be ignored if this option is enabled. -healthCheckProxy.enable bool default=false - -# Which port to proxy -healthCheckProxy.port int default=8080 - -# Low-level timeout for proxy client (socket connect, socket read, connection pool). Aggregate timeout will be longer. -healthCheckProxy.clientTimeout double default=1.0 - -# Enable PROXY protocol V1/V2 support (only for https connectors). -proxyProtocol.enabled bool default=false - -# Allow https in parallel with proxy protocol -proxyProtocol.mixedMode bool default=false - -# Redirect all requests to https port -secureRedirect.enabled bool default=false - -# Target port for redirect -secureRedirect.port int default=443 - -# Maximum number of request per connection before server marks connections as non-persistent. Set to '0' to disable. -maxRequestsPerConnection int default=0 - -# Maximum number of seconds a connection can live before it's marked as non-persistent. Set to '0' to disable. -maxConnectionLife double default=0.0 diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def deleted file mode 100644 index 049080dedbd..00000000000 --- a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.server.def +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=jdisc.http - -# Whether to enable developer mode, where stack traces etc are visible in response bodies. -developerMode bool default=false - -# The gzip compression level to use, if compression is enabled in a request. -responseCompressionLevel int default=6 - -# DEPRECATED - Ignored, no longer in use. -httpKeepAliveEnabled bool default=true -# TODO Vespa 8 Remove httpKeepAliveEnabled - -# Maximum number of request per http connection before server will hangup. -# Naming taken from apache http server. -# 0 means never hangup. -# DEPRECATED - Ignored, no longer in use. Use similar parameter in connector config instead. -maxKeepAliveRequests int default=0 -# TODO Vespa 8 Remove maxKeepAliveRequests - -# Whether the request body of POSTed forms should be removed (form parameters are available as request parameters). -removeRawPostBodyForWwwUrlEncodedPost bool default=false - -# The component ID of a filter -filter[].id string - -# The binding of a filter -filter[].binding string - -# Filter id for a default filter (chain) -defaultFilters[].filterId string - -# The local port which the default filter should be applied to -defaultFilters[].localPort int - -# Reject all requests not handled by a request filter (chain) -strictFiltering bool default = false - -# Max number of threads in underlying Jetty pool -maxWorkerThreads int default = 200 - -# Min number of threads in underlying Jetty pool -minWorkerThreads int default = 8 - -# Stop timeout in seconds. The maximum allowed time to process in-flight requests during server shutdown. Setting it to 0 disable graceful shutdown. -stopTimeout double default = 30.0 - -# Enable embedded JMX server. Note: only accessible through the loopback interface. -jmx.enabled bool default = false - -# Listen port for the JMX server. -jmx.listenPort int default = 1099 - -# Paths that should be reported with monitoring dimensions where applicable -metric.monitoringHandlerPaths[] string - -# Paths that should be reported with search dimensions where applicable -metric.searchHandlerPaths[] string - -# HTTP request headers that contain remote address -accessLog.remoteAddressHeaders[] string - -# HTTP request headers that contain remote port -accessLog.remotePortHeaders[] string - -# Whether to enable jdisc connection log -connectionLog.enabled bool default=false diff --git a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def b/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def deleted file mode 100644 index 86707b027be..00000000000 --- a/jdisc_http_service/src/main/resources/configdefinitions/jdisc.http.jdisc.http.servlet-paths.def +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -namespace=jdisc.http - -# path by servlet componentId -servlets{}.path string diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java deleted file mode 100644 index 5d9509eb045..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/CircularArrayAccessLogKeeperTest.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsCollectionContaining.hasItem; -import static org.hamcrest.core.IsNot.not; -import static org.junit.Assert.assertThat; - -public class CircularArrayAccessLogKeeperTest { - private CircularArrayAccessLogKeeper circularArrayAccessLogKeeper = new CircularArrayAccessLogKeeper(); - - @Test - public void testSizeIsCroppedCorrectly() { - for (int i = 0; i < CircularArrayAccessLogKeeper.SIZE - 1; i++) { - circularArrayAccessLogKeeper.addUri(String.valueOf(i)); - } - assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE -1)); - circularArrayAccessLogKeeper.addUri("foo"); - assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE)); - circularArrayAccessLogKeeper.addUri("bar"); - assertThat(circularArrayAccessLogKeeper.getUris().size(), is(CircularArrayAccessLogKeeper.SIZE)); - assertThat(circularArrayAccessLogKeeper.getUris(), hasItems("1", "2", "3", "foo", "bar")); - assertThat(circularArrayAccessLogKeeper.getUris(), not(hasItem("0"))); - } - - @Test - public void testEmpty() { - assertThat(circularArrayAccessLogKeeper.getUris().size(), is(0)); - } - - @Test - public void testSomeItems() { - circularArrayAccessLogKeeper.addUri("a"); - circularArrayAccessLogKeeper.addUri("b"); - circularArrayAccessLogKeeper.addUri("b"); - assertThat(circularArrayAccessLogKeeper.getUris(), contains("a", "b", "b")); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java deleted file mode 100644 index cb3d1d0a12f..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/JSONLogTestCase.java +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.yolean.trace.TraceNode; -import org.junit.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; - -import static com.yahoo.test.json.JsonTestHelper.assertJsonEquals; - - -/** - * @author frodelu - */ -public class JSONLogTestCase { - - private static String ipAddress = "152.200.54.243"; - - private RequestLogEntry.Builder newRequestLogEntry(final String query) { - return newRequestLogEntry(query, new Coverage(100,100,100,0)); - } - private RequestLogEntry.Builder newRequestLogEntry(final String query, Coverage coverage) { - return new RequestLogEntry.Builder() - .rawQuery("query=" + query) - .rawPath("") - .peerAddress(ipAddress) - .httpMethod("GET") - .httpVersion("HTTP/1.1") - .userAgent("Mozilla/4.05 [en] (Win95; I)") - .hitCounts(new HitCounts(0, 10, 1234, 0, 10, coverage)) - .hostString("localhost") - .statusCode(200) - .timestamp(Instant.ofEpochMilli(920880005023L)) - .duration(Duration.ofMillis(122)) - .contentSize(9875) - .localPort(0) - .peerPort(0); - } - - @Test - public void test_json_log_entry() { - RequestLogEntry entry = newRequestLogEntry("test").build(); - - String expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":100,\"documents\":100}" + - "}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - } - @Test - public void test_json_of_trace() { - TraceNode root = new TraceNode("root", 7); - RequestLogEntry entry = newRequestLogEntry("test") - .traceNode(root) - .build(); - - String expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"trace\":{\"timestamp\":0,\"message\":\"root\"}," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":100,\"documents\":100}" + - "}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - } - @Test - public void test_with_keyvalues() { - RequestLogEntry entry = newRequestLogEntry("test") - .addExtraAttribute("singlevalue", "value1") - .addExtraAttribute("multivalue", "value2") - .addExtraAttribute("multivalue", "value3") - .build(); - - String expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":100,\"documents\":100}" + - "}," + - "\"attributes\":{" + - "\"singlevalue\":\"value1\"," + - "\"multivalue\":[\"value2\",\"value3\"]}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - - } - - @Test - public void test_with_remoteaddrport() throws Exception { - RequestLogEntry entry = newRequestLogEntry("test") - .remoteAddress("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") - .build(); - - String expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"remoteaddr\":\"FE80:0000:0000:0000:0202:B3FF:FE1E:8329\"," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":100,\"documents\":100}" + - "}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - - // Add remote port and verify - entry = newRequestLogEntry("test") - .remoteAddress("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") - .remotePort(1234) - .build(); - - expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"remoteaddr\":\"FE80:0000:0000:0000:0202:B3FF:FE1E:8329\"," + - "\"remoteport\":1234," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":100,\"documents\":100}" + - "}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - } - - @Test - public void test_remote_address_same_as_ip_address() throws Exception { - RequestLogEntry entry = newRequestLogEntry("test").build(); - RequestLogEntry entrywithremote = newRequestLogEntry("test") - .remoteAddress(entry.peerAddress().get()) - .build(); - JSONFormatter formatter = new JSONFormatter(); - assertJsonEquals(formatEntry(entry), formatEntry(entrywithremote)); - } - - @Test - public void test_useragent_with_quotes() { - RequestLogEntry entry = new RequestLogEntry.Builder() - .rawQuery("query=test") - .rawPath("") - .peerAddress(ipAddress) - .httpMethod("GET") - .httpVersion("HTTP/1.1") - .userAgent("Mozilla/4.05 [en] (Win95; I; \"Best Browser Ever\")") - .hitCounts(new HitCounts(0, 10, 1234, 0, 10, new Coverage(100, 200, 200, 0))) - .hostString("localhost") - .statusCode(200) - .timestamp(Instant.ofEpochMilli(920880005023L)) - .duration(Duration.ofMillis(122)) - .contentSize(9875) - .localPort(0) - .peerPort(0) - .build(); - - String expectedOutput = - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I; \\\"Best Browser Ever\\\")\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - "\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"non-ideal-state\":true}}" + - "}" + - "}"; - - assertJsonEquals(formatEntry(entry), expectedOutput); - } - - private void verifyCoverage(String coverage, RequestLogEntry entry) { - assertJsonEquals(formatEntry(entry), - "{\"ip\":\"152.200.54.243\"," + - "\"peeraddr\":\"152.200.54.243\"," + - "\"time\":920880005.023," + - "\"duration\":0.122," + - "\"responsesize\":9875," + - "\"code\":200," + - "\"method\":\"GET\"," + - "\"uri\":\"?query=test\"," + - "\"version\":\"HTTP/1.1\"," + - "\"agent\":\"Mozilla/4.05 [en] (Win95; I)\"," + - "\"host\":\"localhost\"," + - "\"scheme\":null," + - "\"localport\":0," + - "\"search\":{" + - "\"totalhits\":1234," + - "\"hits\":0," + - coverage + - "}" + - "}"); - } - - @Test - public void test_with_coverage_degradation() { - verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"non-ideal-state\":true}}", - newRequestLogEntry("test", new Coverage(100,200,200,0)).build()); - verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"match-phase\":true}}", - newRequestLogEntry("test", new Coverage(100,200,200,1)).build()); - verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"timeout\":true}}", - newRequestLogEntry("test", new Coverage(100,200,200,2)).build()); - verifyCoverage("\"coverage\":{\"coverage\":50,\"documents\":100,\"degraded\":{\"adaptive-timeout\":true}}", - newRequestLogEntry("test", new Coverage(100,200,200,4)).build()); - } - - private String formatEntry(RequestLogEntry entry) { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - new JSONFormatter().write(entry, outputStream); - return outputStream.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java deleted file mode 100644 index 33ecb664af5..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; -import com.yahoo.test.json.JsonTestHelper; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.util.List; -import java.util.UUID; - -/** - * @author bjorncs - */ -class JsonConnectionLogWriterTest { - - @Test - void test_serialization() throws IOException { - var id = UUID.randomUUID(); - var instant = Instant.parse("2021-01-13T12:12:12Z"); - ConnectionLogEntry entry = ConnectionLogEntry.builder(id, instant) - .withPeerPort(1234) - .withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure("UNKNOWN", - List.of( - new ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry("javax.net.ssl.SSLHandshakeException", "message"), - new ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry("java.io.IOException", "cause message")))) - .build(); - String expectedJson = "{" + - "\"id\":\""+id.toString()+"\"," + - "\"timestamp\":\"2021-01-13T12:12:12Z\"," + - "\"peerPort\":1234," + - "\"ssl\":{\"handshake-failure\":{\"exception\":[" + - "{\"cause\":\"javax.net.ssl.SSLHandshakeException\",\"message\":\"message\"}," + - "{\"cause\":\"java.io.IOException\",\"message\":\"cause message\"}" + - "],\"type\":\"UNKNOWN\"}}}"; - - JsonConnectionLogWriter writer = new JsonConnectionLogWriter(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - writer.write(entry, out); - String actualJson = out.toString(StandardCharsets.UTF_8); - JsonTestHelper.assertJsonEquals(actualJson, expectedJson); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java deleted file mode 100644 index dad8f5e3f90..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/LogFileHandlerTestCase.java +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging; - -import com.yahoo.compress.ZstdCompressor; -import com.yahoo.container.logging.LogFileHandler.Compression; -import com.yahoo.io.IOUtils; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.function.BiFunction; -import java.util.logging.Formatter; -import java.util.logging.Level; -import java.util.logging.LogRecord; -import java.util.zip.GZIPInputStream; - -import static com.yahoo.yolean.Exceptions.uncheck; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertNotEquals; - -/** - * @author Bob Travis - * @author bjorncs - */ -public class LogFileHandlerTestCase { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void testIt() throws IOException { - File root = temporaryFolder.newFolder("logfilehandlertest"); - - String pattern = root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S"; - long[] rTimes = {1000, 2000, 10000}; - LogFileHandler h = new LogFileHandler<>(Compression.NONE, pattern, rTimes, null, 2048, "thread-name", new StringLogWriter()); - long now = System.currentTimeMillis(); - long millisPerDay = 60*60*24*1000; - long tomorrowDays = (now / millisPerDay) +1; - long tomorrowMillis = tomorrowDays * millisPerDay; - - assertThat(tomorrowMillis+1000).isEqualTo(h.logThread.getNextRotationTime(tomorrowMillis)); - assertThat(tomorrowMillis+10000).isEqualTo(h.logThread.getNextRotationTime(tomorrowMillis+3000)); - String message = "test"; - h.publish(message); - h.publish( "another test"); - h.rotateNow(); - h.publish(message); - h.flush(); - h.shutdown(); - } - - @Test - public void testSimpleLogging() throws IOException { - File logFile = temporaryFolder.newFile("testLogFileG1.txt"); - - //create logfilehandler - LogFileHandler h = new LogFileHandler<>(Compression.NONE, logFile.getAbsolutePath(), "0 5 ...", null, 2048, "thread-name", new StringLogWriter()); - - //write log - h.publish("testDeleteFileFirst1"); - h.flush(); - h.shutdown(); - } - - @Test - public void testDeleteFileDuringLogging() throws IOException { - File logFile = temporaryFolder.newFile("testLogFileG2.txt"); - - //create logfilehandler - LogFileHandler h = new LogFileHandler<>(Compression.NONE, logFile.getAbsolutePath(), "0 5 ...", null, 2048, "thread-name", new StringLogWriter()); - - //write log - h.publish("testDeleteFileDuringLogging1"); - h.flush(); - - //delete log file - logFile.delete(); - - //write log again - h.publish("testDeleteFileDuringLogging2"); - h.flush(); - h.shutdown(); - } - - @Test(timeout = /*5 minutes*/300_000) - public void testSymlink() throws IOException, InterruptedException { - File root = temporaryFolder.newFolder("testlogforsymlinkchecking"); - Formatter formatter = new Formatter() { - public String format(LogRecord r) { - DateFormat df = new SimpleDateFormat("yyyy.MM.dd:HH:mm:ss.SSS"); - String timeStamp = df.format(new Date(r.getMillis())); - return ("[" + timeStamp + "]" + " " + formatMessage(r)); - } - }; - LogFileHandler handler = new LogFileHandler<>( - Compression.NONE, root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); - - String message = formatter.format(new LogRecord(Level.INFO, "test")); - handler.publishAndWait(message); - String firstFile = handler.getFileName(); - handler.rotateNow(); - String secondFileName = handler.getFileName(); - assertNotEquals(firstFile, secondFileName); - - String longMessage = formatter.format(new LogRecord(Level.INFO, "string which is way longer than the word test")); - handler.publish(longMessage); - handler.flush(); - assertThat(Files.size(Paths.get(firstFile))).isEqualTo(31); - final long expectedSecondFileLength = 72; - - long symlinkFileLength = Files.size(root.toPath().resolve("symlink")); - assertThat(symlinkFileLength).isEqualTo(expectedSecondFileLength); - handler.shutdown(); - } - - @Test(timeout = /*5 minutes*/300_000) - public void compresses_previous_log_file() throws InterruptedException, IOException { - File root = temporaryFolder.newFolder("compressespreviouslogfile"); - LogFileHandler firstHandler = new LogFileHandler<>( - Compression.ZSTD, root.getAbsolutePath() + "/compressespreviouslogfile.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); - firstHandler.publishAndWait("test"); - firstHandler.shutdown(); - - assertThat(Files.size(Paths.get(firstHandler.getFileName()))).isEqualTo(5); - assertThat(root.toPath().resolve("symlink").toRealPath().toString()).isEqualTo(firstHandler.getFileName()); - - LogFileHandler secondHandler = new LogFileHandler<>( - Compression.ZSTD, root.getAbsolutePath() + "/compressespreviouslogfile.%Y%m%d%H%M%S%s", new long[]{0}, "symlink", 2048, "thread-name", new StringLogWriter()); - secondHandler.publishAndWait("test"); - secondHandler.rotateNow(); - - assertThat(root.toPath().resolve("symlink").toRealPath().toString()).isEqualTo(secondHandler.getFileName()); - - while (Files.exists(root.toPath().resolve(firstHandler.getFileName()))) Thread.sleep(1); - - assertThat(Files.exists(Paths.get(firstHandler.getFileName() + ".zst"))).isTrue(); - secondHandler.shutdown(); - } - - @Test(timeout = /*5 minutes*/300_000) - public void testcompression_gzip() throws InterruptedException, IOException { - testcompression( - Compression.GZIP, "gz", - (compressedFile, __) -> uncheck(() -> new String(new GZIPInputStream(Files.newInputStream(compressedFile)).readAllBytes()))); - } - - @Test(timeout = /*5 minutes*/300_000) - public void testcompression_zstd() throws InterruptedException, IOException { - testcompression( - Compression.ZSTD, "zst", - (compressedFile, uncompressedSize) -> uncheck(() -> { - ZstdCompressor zstdCompressor = new ZstdCompressor(); - byte[] uncompressedBytes = new byte[uncompressedSize]; - byte[] compressedBytes = Files.readAllBytes(compressedFile); - zstdCompressor.decompress(compressedBytes, 0, compressedBytes.length, uncompressedBytes, 0, uncompressedBytes.length); - return new String(uncompressedBytes); - })); - } - - private void testcompression(Compression compression, - String fileExtension, - BiFunction decompressor) throws IOException, InterruptedException { - File root = temporaryFolder.newFolder("testcompression" + compression.name()); - - LogFileHandler h = new LogFileHandler<>( - compression, root.getAbsolutePath() + "/logfilehandlertest.%Y%m%d%H%M%S%s", new long[]{0}, null, 2048, "thread-name", new StringLogWriter()); - int logEntries = 10000; - for (int i = 0; i < logEntries; i++) { - h.publish("test"); - } - h.flush(); - String f1 = h.getFileName(); - assertThat(f1).startsWith(root.getAbsolutePath() + "/logfilehandlertest."); - File uncompressed = new File(f1); - File compressed = new File(f1 + "." + fileExtension); - assertThat(uncompressed).exists(); - assertThat(compressed).doesNotExist(); - String content = IOUtils.readFile(uncompressed); - assertThat(content).hasLineCount(logEntries); - h.rotateNow(); - while (uncompressed.exists()) { - Thread.sleep(1); - } - assertThat(compressed).exists(); - String uncompressedContent = decompressor.apply(compressed.toPath(), content.getBytes().length); - assertThat(uncompressedContent).isEqualTo(content); - h.shutdown(); - } - - static class StringLogWriter implements LogWriter { - - @Override - public void write(String record, OutputStream outputStream) throws IOException { - outputStream.write(record.getBytes(StandardCharsets.UTF_8)); - } - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java deleted file mode 100644 index ecacf95d100..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/test/LogFormatterTestCase.java +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.logging.test; - -import com.yahoo.container.logging.LogFormatter; -import org.junit.Test; - -import java.util.Date; - -import static org.junit.Assert.assertEquals; - -/** - * @author Bob Travis - */ -public class LogFormatterTestCase { - - @Test - public void testIt() { - java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("UTC")); - @SuppressWarnings("deprecation") - long time = new Date(103,7,25,13,30,35).getTime(); - String result = LogFormatter.insertDate("test%Y%m%d%H%M%S%x",time); - assertEquals("test20030825133035Aug",result); - result = LogFormatter.insertDate("test%s%T",time); - assertEquals("test000"+time, result); - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java deleted file mode 100644 index dbdce5c704e..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/CookieTestCase.java +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import org.junit.Test; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author Simon Thoresen Hult - * @author bjorncs - */ -public class CookieTestCase { - - @Test - public void requireThatDefaultValuesAreSane() { - Cookie cookie = new Cookie("foo", "bar"); - assertEquals("foo", cookie.getName()); - assertEquals("bar", cookie.getValue()); - assertEquals(null, cookie.getDomain()); - assertEquals(Integer.MIN_VALUE, cookie.getMaxAge(TimeUnit.SECONDS)); - assertEquals(null, cookie.getPath()); - assertEquals(false, cookie.isHttpOnly()); - assertEquals(false, cookie.isSecure()); - } - - @Test - public void requireThatAccessorsWork() { - final Cookie cookie = new Cookie(); - cookie.setName("foo"); - assertEquals("foo", cookie.getName()); - cookie.setName("bar"); - assertEquals("bar", cookie.getName()); - - cookie.setValue("foo"); - assertEquals("foo", cookie.getValue()); - cookie.setValue("bar"); - assertEquals("bar", cookie.getValue()); - - cookie.setDomain("foo"); - assertEquals("foo", cookie.getDomain()); - cookie.setDomain("bar"); - assertEquals("bar", cookie.getDomain()); - - cookie.setPath("foo"); - assertEquals("foo", cookie.getPath()); - cookie.setPath("bar"); - assertEquals("bar", cookie.getPath()); - - cookie.setMaxAge(69, TimeUnit.DAYS); - assertEquals(69, cookie.getMaxAge(TimeUnit.DAYS)); - assertEquals(TimeUnit.DAYS.toHours(69), cookie.getMaxAge(TimeUnit.HOURS)); - - cookie.setSecure(true); - assertTrue(cookie.isSecure()); - cookie.setSecure(false); - assertFalse(cookie.isSecure()); - - cookie.setHttpOnly(true); - assertTrue(cookie.isHttpOnly()); - cookie.setHttpOnly(false); - assertFalse(cookie.isHttpOnly()); - } - - @Test - public void requireThatCopyConstructorWorks() { - final Cookie lhs = newSetCookie("foo"); - final Cookie rhs = new Cookie(lhs); - assertEquals(rhs.getName(), rhs.getName()); - assertEquals(rhs.getValue(), rhs.getValue()); - assertEquals(rhs.getDomain(), rhs.getDomain()); - assertEquals(rhs.getPath(), rhs.getPath()); - assertEquals(rhs.getMaxAge(TimeUnit.MILLISECONDS), rhs.getMaxAge(TimeUnit.MILLISECONDS)); - assertEquals(rhs.isSecure(), rhs.isSecure()); - assertEquals(rhs.isHttpOnly(), rhs.isHttpOnly()); - } - - @Test - public void requireThatHashCodeIsImplemented() { - final Cookie cookie = newCookie("foo"); - assertFalse(cookie.hashCode() == new Cookie().hashCode()); - assertEquals(cookie.hashCode(), cookie.hashCode()); - assertEquals(cookie.hashCode(), new Cookie(cookie).hashCode()); - } - - @Test - public void requireThatEqualsIsImplemented() { - final Cookie cookie = newCookie("foo"); - assertFalse(cookie.equals(new Cookie())); - assertEquals(cookie, cookie); - assertEquals(cookie, new Cookie(cookie)); - } - - @Test - public void requireThatCookieCanBeEncoded() { - assertEncodeCookie( - "foo.name=foo.value", - List.of(newCookie("foo"))); - assertEncodeCookie( - "foo.name=foo.value;bar.name=bar.value", - List.of(newCookie("foo"), newCookie("bar"))); - } - - @Test - public void requireThatSetCookieCanBeEncoded() { - assertEncodeSetCookie( - List.of("foo.name=foo.value; Path=path; Domain=domain; Secure; HttpOnly", - "foo.name=foo.value; Path=path; Domain=domain; Secure; HttpOnly; SameSite=None"), - List.of(newSetCookie("foo"), - newSetCookie("foo").setSameSite(Cookie.SameSite.NONE))); - } - - @Test - public void requireThatCookieCanBeDecoded() { - final Cookie foo = new Cookie(); - foo.setName("foo.name"); - foo.setValue("foo.value"); - assertDecodeCookie(List.of(newCookie("foo")), "foo.name=foo.value"); - - final Cookie bar = new Cookie(); - bar.setName("bar.name"); - bar.setValue("bar.value"); - assertDecodeCookie(List.of(foo, bar),"foo.name=foo.value; bar.name=bar.value"); - } - - @Test - public void requireThatSetCookieCanBeDecoded() { - final Cookie foo = new Cookie(); - foo.setName("foo.name"); - foo.setValue("foo.value"); - foo.setPath("path"); - foo.setDomain("domain"); - foo.setMaxAge(0, TimeUnit.SECONDS); - foo.setSecure(true); - foo.setHttpOnly(true); - assertDecodeSetCookie(foo, "foo.name=foo.value;Max-Age=0;Path=path;Domain=domain;Secure;HTTPOnly;"); - - final Cookie bar = new Cookie(); - bar.setName("bar.name"); - bar.setValue("bar.value"); - bar.setPath("path"); - bar.setDomain("domain"); - bar.setMaxAge(0, TimeUnit.SECONDS); - assertDecodeSetCookie(bar, "bar.name=bar.value;Max-Age=0;Path=path;Domain=domain;"); - } - - @Test - public void requireThatCookieDecoderWorksForGenericValidCookies() { - Cookie.fromCookieHeader("Y=v=1&n=8es5opih9ljtk&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + - "us&np=1; T=z=h.nzPBhSP4PBVd5JqacVnIbNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=YAE&sk=DAALShmNQ" + - "vhoZV&ks=EAABsibvMK6ejwn0uUoS4rC9w--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + - "NU5nLS0BYQFZQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAWNOUnZIbEc3ZHZoVHlWZ" + - "0NoXzEwYkxhOVdzcy0Bb2sBWlcwLQF0aXABWUhwTmVDAXp6AWgubnpQQkE3RQ--"); - } - - @Test - public void requireThatCookieDecoderWorksForYInvalidCookies() { - Cookie.fromCookieHeader("Y=v=1&n=77nkr5t7o4nqn&l=og0_iedeh0qqvqqr/o&p=m2g2rs6012000000&r=pv&lg=en-US&intl=" + - "us&np=1; T=z=05nzPB0NP4PBN/n0gwc1AWGNjU1NAY2TjYzNzVOTjYzNzM0Mj&a=QAE&sk=DAA4R2svo" + - "osjIa&ks=EAAj3nBQFkN4ZmuhqFxJdNoaQ--~E&d=c2wBTVRJeU13RXhPVEUwTURJNU9URTBNRFF6TlRJ" + - "NU5nLS0BYQFRQUUBZwE1VkNHT0w3VUVDTklJVEdRR1FXT0pOSkhEQQFzY2lkAUpPalRXOEVsUDZrR3RHT" + - "VZkX29CWk53clJIQS0BdGlwAVlIcE5lQwF6egEwNW56UEJBN0U-"); - } - - @Test - public void requireThatCookieDecoderWorksForYValidCookies() { - Cookie.fromCookieHeader("Y=v=1&n=3767k6te5aj2s&l=1v4u3001uw2ys00q0rw0qrw34q0x5s3u/o&p=030vvit012000000&iz=" + - "&r=pu&lg=en-US,it-IT,it&intl=it&np=1; T=z=m38yPBmLk3PBWvehTPBhBHYNU5OBjQ3NE5ONU5P" + - "NDY0NzU0M0&a=IAE&sk=DAAAx5URYgbhQ6&ks=EAA4rTgdlAGeMQmdYeM_VehGg--~E&d=c2wBTWprNUF" + - "UTXdNems1TWprNE16RXpNREl6TkRneAFhAUlBRQFnAUVJSlNMSzVRM1pWNVNLQVBNRkszQTRaWDZBAXNj" + - "aWQBSUlyZW5paXp4NS4zTUZMMDVlSVhuMjZKYUcwLQFvawFaVzAtAWFsAW1hcmlvYXByZWFAeW1haWwuY" + - "29tAXp6AW0zOHlQQkE3RQF0aXABaXRZOFRE"); - } - - @Test - public void requireThatCookieDecoderWorksForGenericInvalidCookies() { - Cookie.fromCookieHeader("Y=v=1&n=e92s5cq8qbs6h&l=3kdb0f.3@i126be10b.d4j/o&p=m1f2qgmb13000107&r=g5&lg=en-US" + - "&intl=us; T=z=TXp3OBTrQ8OBFMcj3GBpFSyNk83TgY2MjMwN04zMDMw&a=YAE&sk=DAAVfaNwLeISrX" + - "&ks=EAAOeNNgY8c5hV8YzPYmnrW7w--~E&d=c2wBTVRnd09RRXhOVFEzTURrME56UTMBYQFZQUUBZwFMQ" + - "U5NT0Q2UjY2Q0I1STY0R0tKSUdVQVlRRQFvawFaVzAtAXRpcAFMTlRUdkMBenoBVFhwM09CQTdF&af=QU" + - "FBQ0FDQURBd0FCMUNCOUFJQUJBQ0FEQU1IME1nTWhNbiZ0cz0xMzIzMjEwMTk1JnBzPVA1d3NYakh0aVk" + - "2UDMuUGZ6WkdTT2ctLQ--"); - } - - @Test - public void requireMappingBetweenSameSiteAndJettySameSite() { - for (var jdiscSameSite : Cookie.SameSite.values()) { - assertEquals(jdiscSameSite, Cookie.SameSite.fromJettySameSite(jdiscSameSite.jettySameSite())); - } - - for (var jettySameSite : org.eclipse.jetty.http.HttpCookie.SameSite.values()) { - assertEquals(jettySameSite, Cookie.SameSite.fromJettySameSite(jettySameSite).jettySameSite()); - } - } - - private static void assertEncodeCookie(String expectedResult, List cookies) { - String actual = Cookie.toCookieHeader(cookies); - String expectedResult1 = expectedResult; - assertThat(actual, equalTo(expectedResult1)); - } - - private static void assertEncodeSetCookie(List expectedResult, List cookies) { - assertThat(Cookie.toSetCookieHeaders(cookies), containsInAnyOrder(expectedResult.toArray())); - } - - private static void assertDecodeCookie(List expected, String toDecode) { - assertThat(Cookie.fromCookieHeader(toDecode), containsInAnyOrder(expected.toArray())); - } - - private static void assertDecodeSetCookie(final Cookie expected, String toDecode) { - assertThat(Cookie.fromSetCookieHeader(toDecode), equalTo(expected)); - } - - private static Cookie newCookie(final String name) { - final Cookie cookie = new Cookie(); - cookie.setName(name + ".name"); - cookie.setValue(name + ".value"); - return cookie; - } - - private static Cookie newSetCookie(String name) { - final Cookie cookie = new Cookie(); - cookie.setName(name + ".name"); - cookie.setValue(name + ".value"); - cookie.setDomain("domain"); - cookie.setPath("path"); - cookie.setSecure(true); - cookie.setHttpOnly(true); - return cookie; - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java deleted file mode 100644 index d8ce4a6da0c..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpHeadersTestCase.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * @author Simon Thoresen Hult - */ -public class HttpHeadersTestCase { - - @Test - public void requireThatHeadersDoNotChange() { - assertEquals("X-JDisc-Disable-Chunking", HttpHeaders.Names.X_DISABLE_CHUNKING); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java deleted file mode 100644 index a3cb31d5ecb..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpRequestTestCase.java +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import com.yahoo.jdisc.Container; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.service.CurrentContainer; -import org.junit.Test; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Simon Thoresen Hult - */ -public class HttpRequestTestCase { - - @Test - public void requireThatSimpleServerConstructorsUseReasonableDefaults() { - URI uri = URI.create("http://localhost/"); - HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri); - assertTrue(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.GET, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); - - request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST); - assertTrue(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.POST, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); - - request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); - assertTrue(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.POST, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); - } - - @Test - public void requireThatSimpleClientConstructorsUseReasonableDefaults() { - Request parent = new Request(mockContainer(), URI.create("http://localhost/")); - - URI uri = URI.create("http://remotehost/"); - HttpRequest request = HttpRequest.newClientRequest(parent, uri); - assertFalse(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.GET, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); - - request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST); - assertFalse(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.POST, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); - - request = HttpRequest.newClientRequest(parent, uri, HttpRequest.Method.POST, HttpRequest.Version.HTTP_1_0); - assertFalse(request.isServerRequest()); - assertEquals(uri, request.getUri()); - assertEquals(HttpRequest.Method.POST, request.getMethod()); - assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); - } - - @Test - public void requireThatAccessorsWork() { - URI uri = URI.create("http://localhost/path?foo=bar&foo=baz&cox=69"); - InetSocketAddress address = new InetSocketAddress("remotehost", 69); - final HttpRequest request = HttpRequest.newServerRequest(mockContainer(), uri, HttpRequest.Method.GET, - HttpRequest.Version.HTTP_1_1, address, 1L); - assertEquals(uri, request.getUri()); - request.setUri(uri = URI.create("http://remotehost/")); - assertEquals(uri, request.getUri()); - - assertEquals(HttpRequest.Method.GET, request.getMethod()); - request.setMethod(HttpRequest.Method.CONNECT); - assertEquals(HttpRequest.Method.CONNECT, request.getMethod()); - - assertEquals(HttpRequest.Version.HTTP_1_1, request.getVersion()); - request.setVersion(HttpRequest.Version.HTTP_1_0); - assertEquals(HttpRequest.Version.HTTP_1_0, request.getVersion()); - - assertEquals(address, request.getRemoteAddress()); - request.setRemoteAddress(address = new InetSocketAddress("localhost", 96)); - assertEquals(address, request.getRemoteAddress()); - - final URI proxy = URI.create("http://proxyhost/"); - request.setProxyServer(proxy); - assertEquals(proxy, request.getProxyServer()); - - assertNull(request.getConnectionTimeout(TimeUnit.MILLISECONDS)); - request.setConnectionTimeout(1, TimeUnit.SECONDS); - assertEquals(Long.valueOf(1000), request.getConnectionTimeout(TimeUnit.MILLISECONDS)); - - assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); - assertEquals(Collections.singletonList("69"), request.parameters().get("cox")); - request.parameters().put("cox", Arrays.asList("6", "9")); - assertEquals(Arrays.asList("bar", "baz"), request.parameters().get("foo")); - assertEquals(Arrays.asList("6", "9"), request.parameters().get("cox")); - - assertEquals(1L, request.getConnectedAt(TimeUnit.MILLISECONDS)); - } - - @Test - public void requireThatHttp10EncodingIsNeverChunked() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - assertFalse(request.isChunked()); - request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); - assertFalse(request.isChunked()); - } - - @Test - public void requireThatHttp11EncodingIsNotChunkedByDefault() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); - assertFalse(request.isChunked()); - } - - @Test - public void requireThatHttp11EncodingCanBeChunked() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); - request.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED); - assertTrue(request.isChunked()); - } - - @Test - public void requireThatHttp10ConnectionIsAlwaysClose() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - assertFalse(request.isKeepAlive()); - request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE); - assertTrue(request.isKeepAlive()); - } - - @Test - public void requireThatHttp11ConnectionIsKeepAliveByDefault() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); - assertTrue(request.isKeepAlive()); - } - - @Test - public void requireThatHttp11ConnectionCanBeClose() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); - request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE); - assertFalse(request.isKeepAlive()); - } - - @Test - public void requireThatHttp10NeverHasChunkedResponse() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - assertFalse(request.hasChunkedResponse()); - } - - @Test - public void requireThatHttp11HasDefaultChunkedResponse() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_1); - assertTrue(request.hasChunkedResponse()); - } - - @Test - public void requireThatHttp11CanDisableChunkedResponse() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - request.headers().add(com.yahoo.jdisc.http.HttpHeaders.Names.X_DISABLE_CHUNKING, "true"); - assertFalse(request.hasChunkedResponse()); - } - - @Test - public void requireThatCookieHeaderCanBeEncoded() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - final List cookies = Collections.singletonList(new Cookie("foo", "bar")); - request.encodeCookieHeader(cookies); - final List headers = request.headers().get(com.yahoo.jdisc.http.HttpHeaders.Names.COOKIE); - assertEquals(1, headers.size()); - assertEquals(Cookie.toCookieHeader(cookies), headers.get(0)); - } - - @Test - public void requireThatCookieHeaderCanBeDecoded() throws Exception { - final HttpRequest request = newRequest(HttpRequest.Version.HTTP_1_0); - final List cookies = Collections.singletonList(new Cookie("foo", "bar")); - request.encodeCookieHeader(cookies); - assertEquals(cookies, request.decodeCookieHeader()); - } - - private static HttpRequest newRequest(final HttpRequest.Version version) throws Exception { - return HttpRequest.newServerRequest( - mockContainer(), - new URI("http://localhost:1234/status.html"), - HttpRequest.Method.GET, - version); - } - - private static CurrentContainer mockContainer() { - final CurrentContainer currentContainer = mock(CurrentContainer.class); - when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); - return currentContainer; - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java deleted file mode 100644 index 61499200f3c..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/HttpResponseTestCase.java +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http; - -import com.yahoo.jdisc.Container; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.service.CurrentContainer; -import org.junit.Test; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Simon Thoresen Hult - */ -public class HttpResponseTestCase { - - @Test - public void requireThatAccessorsWork() throws Exception { - final HttpResponse response = newResponse(6, "foo"); - assertEquals(6, response.getStatus()); - assertEquals("foo", response.getMessage()); - assertNull(response.getError()); - assertTrue(response.isChunkedEncodingEnabled()); - - response.setStatus(9); - assertEquals(9, response.getStatus()); - - response.setMessage("bar"); - assertEquals("bar", response.getMessage()); - - final Throwable err = new Throwable(); - response.setError(err); - assertSame(err, response.getError()); - - response.setChunkedEncodingEnabled(false); - assertFalse(response.isChunkedEncodingEnabled()); - } - - @Test - public void requireThatStatusCodesDoNotChange() { - assertEquals(HttpResponse.Status.CREATED, 201); - assertEquals(HttpResponse.Status.ACCEPTED, 202); - assertEquals(HttpResponse.Status.NON_AUTHORITATIVE_INFORMATION, 203); - assertEquals(HttpResponse.Status.NO_CONTENT, 204); - assertEquals(HttpResponse.Status.RESET_CONTENT, 205); - assertEquals(HttpResponse.Status.PARTIAL_CONTENT, 206); - - assertEquals(HttpResponse.Status.MULTIPLE_CHOICES, 300); - assertEquals(HttpResponse.Status.SEE_OTHER, 303); - assertEquals(HttpResponse.Status.NOT_MODIFIED, 304); - assertEquals(HttpResponse.Status.USE_PROXY, 305); - - assertEquals(HttpResponse.Status.PAYMENT_REQUIRED, 402); - assertEquals(HttpResponse.Status.PROXY_AUTHENTICATION_REQUIRED, 407); - assertEquals(HttpResponse.Status.CONFLICT, 409); - assertEquals(HttpResponse.Status.GONE, 410); - assertEquals(HttpResponse.Status.LENGTH_REQUIRED, 411); - assertEquals(HttpResponse.Status.PRECONDITION_FAILED, 412); - assertEquals(HttpResponse.Status.REQUEST_ENTITY_TOO_LARGE, 413); - assertEquals(HttpResponse.Status.REQUEST_URI_TOO_LONG, 414); - assertEquals(HttpResponse.Status.UNSUPPORTED_MEDIA_TYPE, 415); - assertEquals(HttpResponse.Status.REQUEST_RANGE_NOT_SATISFIABLE, 416); - assertEquals(HttpResponse.Status.EXPECTATION_FAILED, 417); - - assertEquals(HttpResponse.Status.BAD_GATEWAY, 502); - assertEquals(HttpResponse.Status.GATEWAY_TIMEOUT, 504); - } - - @Test - public void requireThat5xxIsServerError() { - for (int i = 0; i < 999; ++i) { - assertEquals(i >= 500 && i < 600, HttpResponse.isServerError(new Response(i))); - } - } - - @Test - public void requireThatCookieHeaderCanBeEncoded() throws Exception { - final HttpResponse response = newResponse(69, "foo"); - final List cookies = Collections.singletonList(new Cookie("foo", "bar")); - response.encodeSetCookieHeader(cookies); - final List headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); - assertEquals(1, headers.size()); - assertEquals(Cookie.toSetCookieHeaders(cookies), headers); - } - - @Test - public void requireThatMultipleCookieHeadersCanBeEncoded() throws Exception { - final HttpResponse response = newResponse(69, "foo"); - final List cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); - response.encodeSetCookieHeader(cookies); - final List headers = response.headers().get(HttpHeaders.Names.SET_COOKIE); - assertEquals(2, headers.size()); - assertEquals(Cookie.toSetCookieHeaders(Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox"))), - headers); - } - - @Test - public void requireThatCookieHeaderCanBeDecoded() throws Exception { - final HttpResponse response = newResponse(69, "foo"); - final List cookies = Collections.singletonList(new Cookie("foo", "bar")); - response.encodeSetCookieHeader(cookies); - assertEquals(cookies, response.decodeSetCookieHeader()); - } - - @Test - public void requireThatMultipleCookieHeadersCanBeDecoded() throws Exception { - final HttpResponse response = newResponse(69, "foo"); - final List cookies = Arrays.asList(new Cookie("foo", "bar"), new Cookie("baz", "cox")); - response.encodeSetCookieHeader(cookies); - assertEquals(cookies, response.decodeSetCookieHeader()); - } - - private static HttpResponse newResponse(final int status, final String message) throws Exception { - final Request request = HttpRequest.newServerRequest( - mockContainer(), - new URI("http://localhost:1234/status.html"), - HttpRequest.Method.GET, - HttpRequest.Version.HTTP_1_1); - return HttpResponse.newInstance(status, message); - } - - private static CurrentContainer mockContainer() { - final CurrentContainer currentContainer = mock(CurrentContainer.class); - when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); - return currentContainer; - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java deleted file mode 100644 index 1c05a3f3db2..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterRequestTest.java +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.HeaderFields; -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.HttpRequest.Version; -import com.yahoo.jdisc.test.TestDriver; -import org.junit.Assert; -import org.junit.Test; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; - -import static org.junit.Assert.assertTrue; - -public class DiscFilterRequestTest { - - private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { - InetSocketAddress address = new InetSocketAddress("example.yahoo.com", 69); - TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); - driver.activateContainer(driver.newContainerBuilder()); - HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); - request.release(); - assertTrue(driver.close()); - return request; - } - - @Test - public void testRequestConstruction(){ - URI uri = URI.create("http://localhost:8080/test?param1=abc"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); - httpReq.headers().add("X-Custom-Header", "custom_header"); - List cookies = new ArrayList(); - cookies.add(new Cookie("XYZ", "value")); - cookies.add(new Cookie("ABC", "value")); - httpReq.encodeCookieHeader(cookies); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertSame(request.getParentRequest(), httpReq); - Assert.assertEquals(request.getHeader("X-Custom-Header"),"custom_header"); - Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); - - List c = request.getCookies(); - Assert.assertNotNull(c); - Assert.assertEquals(c.size(), 2); - - Assert.assertEquals(request.getParameter("param1"),"abc"); - Assert.assertNull(request.getParameter("param2")); - Assert.assertEquals(request.getVersion(),Version.HTTP_1_1); - Assert.assertEquals(request.getProtocol(),Version.HTTP_1_1.name()); - Assert.assertNull(request.getRequestedSessionId()); - } - - @Test - public void testRequestConstruction2() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add("some-header", "some-value"); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - request.addHeader("some-header", "some-value"); - String value = request.getUntreatedHeaders().get("some-header").get(0); - Assert.assertEquals(value,"some-value"); - } - - @Test - public void testRequestAttributes() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.setAttribute("some_attr", "some_value"); - - Assert.assertEquals(request.containsAttribute("some_attr"),true); - - Assert.assertEquals(request.getAttribute("some_attr"),"some_value"); - - } - - @Test - public void testGetAttributeNames() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.setAttribute("some_attr_1", "some_value1"); - request.setAttribute("some_attr_2", "some_value2"); - - Enumeration e = request.getAttributeNames(); - List attrList = Collections.list(e); - Assert.assertEquals(2, attrList.size()); - Assert.assertEquals(attrList.contains("some_attr_1"), true); - Assert.assertEquals(attrList.contains("some_attr_2"), true); - - } - - @Test - public void testRemoveAttribute() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.setAttribute("some_attr", "some_value"); - - Assert.assertEquals(request.containsAttribute("some_attr"),true); - - request.removeAttribute("some_attr"); - - Assert.assertEquals(request.containsAttribute("some_attr"),false); - } - - @Test - public void testGetIntHeader() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(-1, request.getIntHeader("int_header")); - - request.addHeader("int_header", String.valueOf(5)); - - Assert.assertEquals(5, request.getIntHeader("int_header")); - } - - @Test - public void testDateHeader() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - - Assert.assertEquals(-1, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); - - request.addHeader(HttpHeaders.Names.IF_MODIFIED_SINCE, "Sat, 29 Oct 1994 19:43:31 GMT"); - - Assert.assertEquals(783459811000L, request.getDateHeader(HttpHeaders.Names.IF_MODIFIED_SINCE)); - } - - @Test - public void testParameterAPIsAsList() { - URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertEquals(request.getParameter("param1"),"abc"); - - List values = request.getParameterValuesAsList("param2"); - Assert.assertEquals(values.get(0),"xyz"); - Assert.assertEquals(values.get(1),"pqr"); - - List paramNames = request.getParameterNamesAsList(); - Assert.assertEquals(paramNames.size(), 2); - - } - - @Test - public void testParameterAPI(){ - URI uri = URI.create("http://example.yahoo.com:8080/test?param1=abc¶m2=xyz¶m2=pqr"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertEquals(request.getParameter("param1"),"abc"); - - Enumeration values = request.getParameterValues("param2"); - List valuesList = Collections.list(values); - Assert.assertEquals(valuesList.get(0),"xyz"); - Assert.assertEquals(valuesList.get(1),"pqr"); - - Enumeration paramNames = request.getParameterNames(); - List paramNamesList = Collections.list(paramNames); - Assert.assertEquals(paramNamesList.size(), 2); - } - - @Test - public void testGetHeaderNamesAsList() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); - httpReq.headers().add("header_1", "value1"); - httpReq.headers().add("header_2", "value2"); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); - Assert.assertEquals(request.getHeaderNamesAsList().size(), 3); - } - - @Test - public void testGetHeadersAsList() { - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(request.getHeaderNamesAsList() instanceof List, true); - Assert.assertEquals(request.getHeaderNamesAsList().size(), 0); - - httpReq.headers().add("header_1", "value1"); - httpReq.headers().add("header_1", "value2"); - - Assert.assertEquals(request.getHeadersAsList("header_1").size(), 2); - } - - @Test - public void testIsMultipart() { - - URI uri = URI.create("http://localhost:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "multipart/form-data"); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(true,DiscFilterRequest.isMultipart(request)); - - httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); - request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); - - Assert.assertEquals(DiscFilterRequest.isMultipart(null),false); - - - httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - request = new JdiscFilterRequest(httpReq); - Assert.assertEquals(DiscFilterRequest.isMultipart(request),false); - } - - @Test - public void testGetRemotePortLocalPort() { - - URI uri = URI.create("http://example.yahoo.com:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - Assert.assertEquals(69, request.getRemotePort()); - Assert.assertEquals(8080, request.getLocalPort()); - - if (request.getRemoteHost() != null) // if we have network - Assert.assertEquals("example.yahoo.com", request.getRemoteHost()); - - request.setRemoteAddr("1.1.1.1"); - - Assert.assertEquals("1.1.1.1",request.getRemoteAddr()); - } - - @Test - public void testCharacterEncoding() throws Exception { - URI uri = URI.create("http://example.yahoo.com:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html;charset=UTF-8"); - - Assert.assertEquals(request.getCharacterEncoding(), "UTF-8"); - - httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - request = new JdiscFilterRequest(httpReq); - request.setHeaders(HttpHeaders.Names.CONTENT_TYPE, "text/html"); - request.setCharacterEncoding("UTF-8"); - - Assert.assertEquals(request.getCharacterEncoding(),"UTF-8"); - - Assert.assertEquals(request.getHeader(HttpHeaders.Names.CONTENT_TYPE),"text/html;charset=UTF-8"); - } - - @Test - public void testSetScheme() throws Exception { - URI uri = URI.create("https://example.yahoo.com:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - - request.setScheme("http", true); - System.out.println(request.getUri().toString()); - Assert.assertEquals(request.getUri().toString(), "http://example.yahoo.com:8080/test"); - } - - @Test - public void testGetServerPort() throws Exception { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertEquals(request.getServerPort(), 80); - - request.setUri(URI.create("https://example.yahoo.com/test")); - Assert.assertEquals(request.getServerPort(), 443); - - } - - @Test - public void testIsSecure() throws Exception { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertEquals(request.isSecure(), false); - - request.setUri(URI.create("https://example.yahoo.com/test")); - Assert.assertEquals(request.isSecure(), true); - - } - - @Test - public void requireThatUnresolvableRemoteAddressesAreSupported() { - URI uri = URI.create("http://doesnotresolve.zzz:8080/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - Assert.assertNull(request.getLocalAddr()); - } - - @Test - public void testGetUntreatedHeaders() { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().add("key1", "value1"); - httpReq.headers().add("key2", Arrays.asList("value1","value2")); - - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - HeaderFields headers = request.getUntreatedHeaders(); - Assert.assertEquals(headers.keySet().size(), 2); - Assert.assertEquals(headers.get("key1").get(0), "value1" ); - Assert.assertEquals(headers.get("key2").get(0), "value1" ); - Assert.assertEquals(headers.get("key2").get(1), "value2" ); - } - - @Test - public void testClearCookies() throws Exception { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.clearCookies(); - Assert.assertNull(request.getHeader(HttpHeaders.Names.COOKIE)); - } - - @Test - public void testGetWrapedCookies() throws Exception { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - httpReq.headers().put(HttpHeaders.Names.COOKIE, "XYZ=value"); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - JDiscCookieWrapper[] wrappers = request.getWrappedCookies(); - Assert.assertEquals(wrappers.length ,1); - Assert.assertEquals(wrappers[0].getName(), "XYZ"); - Assert.assertEquals(wrappers[0].getValue(), "value"); - } - - @Test - public void testAddCookie() { - URI uri = URI.create("http://example.yahoo.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterRequest request = new JdiscFilterRequest(httpReq); - request.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); - - List cookies = request.getCookies(); - Assert.assertEquals(cookies.size(), 1); - Assert.assertEquals(cookies.get(0).getName(), "name"); - Assert.assertEquals(cookies.get(0).getValue(), "value"); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java deleted file mode 100644 index b349cb8d803..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/DiscFilterResponseTest.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.HttpResponse; -import com.yahoo.jdisc.test.TestDriver; -import org.junit.Assert; -import org.junit.Test; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertTrue; - -public class DiscFilterResponseTest { - - private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { - InetSocketAddress address = new InetSocketAddress("localhost", 69); - TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); - driver.activateContainer(driver.newContainerBuilder()); - HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); - request.release(); - assertTrue(driver.close()); - return request; - } - - public static HttpResponse newResponse(Request request, int status) { - return HttpResponse.newInstance(status); - } - - @Test - public void testGetSetStatus() { - HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), - HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); - - Assert.assertEquals(response.getStatus(), HttpResponse.Status.OK); - response.setStatus(HttpResponse.Status.REQUEST_TIMEOUT); - Assert.assertEquals(response.getStatus(), HttpResponse.Status.REQUEST_TIMEOUT); - } - - @Test - public void testAttributes() { - HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), - HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); - response.setAttribute("attr_1", "value1"); - Assert.assertEquals(response.getAttribute("attr_1"), "value1"); - List list = Collections.list(response.getAttributeNames()); - Assert.assertEquals(list.get(0), "attr_1"); - response.removeAttribute("attr_1"); - Assert.assertNull(response.getAttribute("attr_1")); - } - - @Test - public void testAddHeader() { - HttpRequest request = newRequest(URI.create("http://localhost:8080/echo"), - HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - DiscFilterResponse response = new JdiscFilterResponse(HttpResponse.newInstance(HttpResponse.Status.OK)); - response.addHeader("header1", "value1"); - Assert.assertEquals(response.getHeader("header1"), "value1"); - } - - @Test - public void testAddCookie() { - URI uri = URI.create("http://example.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - HttpResponse httpResp = newResponse(httpReq, 200); - DiscFilterResponse response = new JdiscFilterResponse(httpResp); - response.addCookie(JDiscCookieWrapper.wrap(new Cookie("name", "value"))); - - List cookies = response.getCookies(); - Assert.assertEquals(cookies.size(),1); - Assert.assertEquals(cookies.get(0).getName(),"name"); - } - - @Test - public void testSetCookie() { - URI uri = URI.create("http://example.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - HttpResponse httpResp = newResponse(httpReq, 200); - DiscFilterResponse response = new JdiscFilterResponse(httpResp); - response.setCookie("name", "value"); - List cookies = response.getCookies(); - Assert.assertEquals(cookies.size(),1); - Assert.assertEquals(cookies.get(0).getName(),"name"); - - } - - @Test - public void testSetHeader() { - URI uri = URI.create("http://example.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - HttpResponse httpResp = newResponse(httpReq, 200); - DiscFilterResponse response = new JdiscFilterResponse(httpResp); - response.setHeader("name", "value"); - Assert.assertEquals(response.getHeader("name"), "value"); - } - - @Test - public void testGetParentResponse() { - URI uri = URI.create("http://example.com/test"); - HttpRequest httpReq = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - HttpResponse httpResp = newResponse(httpReq, 200); - DiscFilterResponse response = new JdiscFilterResponse(httpResp); - Assert.assertSame(response.getParentResponse(), httpResp); - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java deleted file mode 100644 index f4418e74169..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyRequestFilterTestCase.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.Container; -import com.yahoo.jdisc.handler.ResponseHandler; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.filter.chain.EmptyRequestFilter; -import com.yahoo.jdisc.service.CurrentContainer; -import org.junit.Test; - -import java.net.URI; -import java.util.concurrent.TimeUnit; - -import static com.yahoo.jdisc.http.HttpRequest.Method; -import static com.yahoo.jdisc.http.HttpRequest.Version; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Simon Thoresen Hult - */ -public class EmptyRequestFilterTestCase { - - @Test - public void requireThatEmptyFilterDoesNothing() throws Exception { - final HttpRequest lhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); - final HttpRequest rhs = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); - - EmptyRequestFilter.INSTANCE.filter(rhs, mock(ResponseHandler.class)); - - assertEquals(lhs.headers(), rhs.headers()); - assertEquals(lhs.context(), rhs.context()); - assertEquals(lhs.getTimeout(TimeUnit.MILLISECONDS), rhs.getTimeout(TimeUnit.MILLISECONDS)); - assertEquals(lhs.parameters(), rhs.parameters()); - assertEquals(lhs.getMethod(), rhs.getMethod()); - assertEquals(lhs.getVersion(), rhs.getVersion()); - assertEquals(lhs.getRemoteAddress(), rhs.getRemoteAddress()); - } - - private static HttpRequest newRequest( - final Method method, final String uri, final Version version) { - final CurrentContainer currentContainer = mock(CurrentContainer.class); - when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); - return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java deleted file mode 100644 index e6d7259ea41..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/EmptyResponseFilterTestCase.java +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.Container; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.HttpResponse; -import com.yahoo.jdisc.http.filter.chain.EmptyResponseFilter; -import com.yahoo.jdisc.service.CurrentContainer; -import org.junit.Test; - -import java.net.URI; - -import static com.yahoo.jdisc.http.HttpRequest.Method; -import static com.yahoo.jdisc.http.HttpRequest.Version; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Simon Thoresen Hult - */ -public class EmptyResponseFilterTestCase { - - @Test - public void requireThatEmptyFilterDoesNothing() throws Exception { - final HttpRequest request = newRequest(Method.GET, "/status.html", Version.HTTP_1_1); - final HttpResponse lhs = HttpResponse.newInstance(Response.Status.OK); - final HttpResponse rhs = HttpResponse.newInstance(Response.Status.OK); - - EmptyResponseFilter.INSTANCE.filter(lhs, null); - - assertEquals(lhs.headers(), rhs.headers()); - assertEquals(lhs.context(), rhs.context()); - assertEquals(lhs.getError(), rhs.getError()); - assertEquals(lhs.getMessage(), rhs.getMessage()); - } - - private static HttpRequest newRequest(final Method method, final String uri, final Version version) { - final CurrentContainer currentContainer = mock(CurrentContainer.class); - when(currentContainer.newReference(any(URI.class))).thenReturn(mock(Container.class)); - return HttpRequest.newServerRequest(currentContainer, URI.create(uri), method, version); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java deleted file mode 100644 index 9948e5bfe7f..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/JDiscCookieWrapperTest.java +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.Cookie; -import org.junit.Assert; -import org.junit.Test; - -import java.util.concurrent.TimeUnit; - -public class JDiscCookieWrapperTest { - - @Test - public void requireThatWrapWorks() { - Cookie cookie = new Cookie("name", "value"); - JDiscCookieWrapper wrapper = JDiscCookieWrapper.wrap(cookie); - - wrapper.setDomain("yahoo.com"); - wrapper.setMaxAge(10); - wrapper.setPath("/path"); - - Assert.assertEquals(wrapper.getName(), cookie.getName()); - Assert.assertEquals(wrapper.getValue(), cookie.getValue()); - Assert.assertEquals(wrapper.getDomain(), cookie.getDomain()); - Assert.assertEquals(wrapper.getMaxAge(), cookie.getMaxAge(TimeUnit.SECONDS)); - Assert.assertEquals(wrapper.getPath(), cookie.getPath()); - Assert.assertEquals(wrapper.getSecure(), cookie.isSecure()); - - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java deleted file mode 100644 index ec0e0a33d35..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/RequestViewImplTest.java +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.google.common.collect.Lists; -import com.yahoo.jdisc.HeaderFields; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.http.filter.SecurityResponseFilterChain.RequestViewImpl; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author gjoranv - */ -public class RequestViewImplTest { - - @Test - public void header_from_the_parent_request_is_available() throws Exception { - final String HEADER = "single-header"; - - HeaderFields parentHeaders = new HeaderFields(); - parentHeaders.add(HEADER, "value"); - - RequestView requestView = newRequestView(parentHeaders); - - assertEquals(requestView.getFirstHeader(HEADER).get(), "value"); - assertEquals(requestView.getHeaders(HEADER).size(), 1); - assertEquals(requestView.getHeaders(HEADER).get(0), "value"); - } - - - @Test - public void multi_value_header_from_the_parent_request_is_available() throws Exception { - final String HEADER = "list-header"; - - HeaderFields parentHeaders = new HeaderFields(); - parentHeaders.add(HEADER, Lists.newArrayList("one", "two")); - - RequestView requestView = newRequestView(parentHeaders); - - assertEquals(requestView.getHeaders(HEADER).size(), 2); - assertEquals(requestView.getHeaders(HEADER).get(0), "one"); - assertEquals(requestView.getHeaders(HEADER).get(1), "two"); - - assertEquals(requestView.getFirstHeader(HEADER).get(), "one"); - } - - private static RequestView newRequestView(HeaderFields parentHeaders) { - Request request = mock(Request.class); - when(request.headers()).thenReturn(parentHeaders); - - return new RequestViewImpl(request); - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java deleted file mode 100644 index 3855c3a494b..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ResponseHeaderFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.AbstractResource; -import com.yahoo.jdisc.Request; -import com.yahoo.jdisc.Response; - -/** - * @author Simon Thoresen Hult - */ -public class ResponseHeaderFilter extends AbstractResource implements ResponseFilter { - - private final String key; - private final String val; - - public ResponseHeaderFilter(String key, String val) { - this.key = key; - this.val = val; - } - - @Override - public void filter(Response response, Request request) { - response.headers().add(key, val); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java deleted file mode 100644 index be19313dee2..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityRequestFilterChainTest.java +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.AbstractResource; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.handler.CompletionHandler; -import com.yahoo.jdisc.handler.ContentChannel; -import com.yahoo.jdisc.handler.ResponseDispatch; -import com.yahoo.jdisc.handler.ResponseHandler; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.test.TestDriver; -import org.junit.Assert; -import org.junit.Test; - -import java.net.InetSocketAddress; -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -/** - * @author bjorncs - */ -public class SecurityRequestFilterChainTest { - - - private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { - InetSocketAddress address = new InetSocketAddress("java.corp.yahoo.com", 69); - TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); - driver.activateContainer(driver.newContainerBuilder()); - HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); - request.release(); - Assert.assertTrue(driver.close()); - return request; - } - - @Test - public void testFilterChainConstruction() { - SecurityRequestFilterChain chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(); - assertEquals(chain.getFilters().size(),0); - - List requestFilters = new ArrayList(); - chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(); - - chain = (SecurityRequestFilterChain)SecurityRequestFilterChain.newInstance(new RequestHeaderFilter("abc", "xyz"), - new RequestHeaderFilter("pqr", "def")); - - assertEquals(chain instanceof SecurityRequestFilterChain, true); - } - - - @Test - public void testFilterChainRun() { - RequestFilter chain = SecurityRequestFilterChain.newInstance(new RequestHeaderFilter("abc", "xyz"), - new RequestHeaderFilter("pqr", "def")); - - assertEquals(chain instanceof SecurityRequestFilterChain, true); - ResponseHandler handler = newResponseHandler(); - HttpRequest request = newRequest(URI.create("http://test/test"), HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - chain.filter(request, handler); - Assert.assertTrue(request.headers().contains("abc", "xyz")); - Assert.assertTrue(request.headers().contains("pqr", "def")); - } - - @Test - public void testFilterChainResponds() { - RequestFilter chain = SecurityRequestFilterChain.newInstance( - new MyFilter(), - new RequestHeaderFilter("abc", "xyz"), - new RequestHeaderFilter("pqr", "def")); - - assertEquals(chain instanceof SecurityRequestFilterChain, true); - ResponseHandler handler = newResponseHandler(); - HttpRequest request = newRequest(URI.create("http://test/test"), HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - chain.filter(request, handler); - Response response = getResponse(handler); - Assert.assertNotNull(response); - Assert.assertTrue(!request.headers().contains("abc", "xyz")); - Assert.assertTrue(!request.headers().contains("pqr", "def")); - } - - private class RequestHeaderFilter extends AbstractResource implements SecurityRequestFilter { - - private final String key; - private final String val; - - public RequestHeaderFilter(String key, String val) { - this.key = key; - this.val = val; - } - - @Override - public void filter(DiscFilterRequest request, ResponseHandler handler) { - request.setHeaders(key, val); - } - } - - private class MyFilter extends AbstractResource implements SecurityRequestFilter { - - @Override - public void filter(DiscFilterRequest request, ResponseHandler handler) { - ResponseDispatch.newInstance(Response.Status.FORBIDDEN).dispatch(handler); - } - } - - private static ResponseHandler newResponseHandler() { - return new NonWorkingResponseHandler(); - } - - private static Response getResponse(ResponseHandler handler) { - return ((NonWorkingResponseHandler) handler).getResponse(); - } - - private static class NonWorkingResponseHandler implements ResponseHandler { - - private Response response = null; - - @Override - public ContentChannel handleResponse(Response response) { - this.response = response; - return new NonWorkingContentChannel(); - } - - public Response getResponse() { - return response; - } - } - - private static class NonWorkingContentChannel implements ContentChannel { - - @Override - public void close(CompletionHandler handler) { - - } - - @Override - public void write(ByteBuffer buf, CompletionHandler handler) { - - } - - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java deleted file mode 100644 index 25291de5cc1..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/SecurityResponseFilterChainTest.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.AbstractResource; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.http.HttpRequest; -import com.yahoo.jdisc.http.HttpResponse; -import com.yahoo.jdisc.test.TestDriver; -import org.junit.Test; - -import java.net.InetSocketAddress; -import java.net.URI; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -/** - * @author bjorncs - */ -public class SecurityResponseFilterChainTest { - private static HttpRequest newRequest(URI uri, HttpRequest.Method method, HttpRequest.Version version) { - InetSocketAddress address = new InetSocketAddress("java.corp.yahoo.com", 69); - TestDriver driver = TestDriver.newSimpleApplicationInstanceWithoutOsgi(); - driver.activateContainer(driver.newContainerBuilder()); - HttpRequest request = HttpRequest.newServerRequest(driver, uri, method, version, address); - request.release(); - assertTrue(driver.close()); - return request; - } - - @Test - public void testFilterChainConstruction() { - SecurityResponseFilterChain chain = (SecurityResponseFilterChain)SecurityResponseFilterChain.newInstance(); - assertEquals(chain.getFilters().size(),0); - - chain = (SecurityResponseFilterChain)SecurityResponseFilterChain.newInstance(new ResponseHeaderFilter("abc", "xyz"), - new ResponseHeaderFilter("pqr", "def")); - - assertEquals(chain instanceof SecurityResponseFilterChain, true); - } - - @Test - public void testFilterChainRun() { - URI uri = URI.create("http://localhost:8080/echo"); - HttpRequest request = newRequest(uri, HttpRequest.Method.GET, HttpRequest.Version.HTTP_1_1); - Response response = HttpResponse.newInstance(Response.Status.OK); - - ResponseFilter chain = SecurityResponseFilterChain.newInstance(new ResponseHeaderFilter("abc", "xyz"), - new ResponseHeaderFilter("pqr", "def")); - chain.filter(response, null); - assertTrue(response.headers().contains("abc", "xyz")); - assertTrue(response.headers().contains("pqr", "def")); - } - - private class ResponseHeaderFilter extends AbstractResource implements SecurityResponseFilter { - - private final String key; - private final String val; - - public ResponseHeaderFilter(String key, String val) { - this.key = key; - this.val = val; - } - - @Override - public void filter(DiscFilterResponse response, RequestView request) { - response.setHeaders(key, val); - } - - } - - - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java deleted file mode 100644 index 3052902f174..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterRequestTest.java +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.servlet.ServletRequest; -import org.eclipse.jetty.server.HttpConnection; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mockito; -import org.springframework.mock.web.MockHttpServletRequest; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static com.yahoo.jdisc.http.HttpRequest.Version; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; - -/** - * Test the parts of the DiscFilterRequest API that are implemented - * by ServletFilterRequest, both directly and indirectly via - * {@link com.yahoo.jdisc.http.servlet.ServletRequest}. - * - * @author gjoranv - * @since 5.27 - */ -public class ServletFilterRequestTest { - - private final String host = "host1"; - private final int port = 8080; - private final String path = "/path1"; - private final String paramName = "param1"; - private final String paramValue = "p1"; - private final String listParamName = "listParam"; - private final String[] listParamValue = new String[]{"1", "2"}; - private final String headerName = "header1"; - private final String headerValue = "h1"; - private final String attributeName = "attribute1"; - private final String attributeValue = "a1"; - - private URI uri; - private DiscFilterRequest filterRequest; - private ServletRequest parentRequest; - - @Before - public void init() throws Exception { - uri = new URI("http", null, host, port, path, paramName + "=" + paramValue, null); - - filterRequest = new ServletFilterRequest(newServletRequest()); - parentRequest = ((ServletFilterRequest)filterRequest).getServletRequest(); - } - - private ServletRequest newServletRequest() throws Exception { - MockHttpServletRequest parent = new MockHttpServletRequest("GET", uri.toString()); - parent.setProtocol(Version.HTTP_1_1.toString()); - parent.setRemoteHost(host); - parent.setRemotePort(port); - parent.setParameter(paramName, paramValue); - parent.setParameter(listParamName, listParamValue); - parent.addHeader(headerName, headerValue); - parent.setAttribute(attributeName, attributeValue); - HttpConnection connection = Mockito.mock(HttpConnection.class); - when(connection.getCreatedTimeStamp()).thenReturn(System.currentTimeMillis()); - parent.setAttribute("org.eclipse.jetty.server.HttpConnection", connection); - return new ServletRequest(parent, uri); - } - - @Test - public void parent_properties_are_propagated_to_disc_filter_request() throws Exception { - assertEquals(filterRequest.getVersion(), Version.HTTP_1_1); - assertEquals(filterRequest.getMethod(), "GET"); - assertEquals(filterRequest.getUri(), uri); - assertEquals(filterRequest.getRemoteHost(), host); - assertEquals(filterRequest.getRemotePort(), port); - assertEquals(filterRequest.getRequestURI(), path); // getRequestUri return only the path by design - - assertEquals(filterRequest.getParameter(paramName), paramValue); - assertEquals(filterRequest.getParameterMap().get(paramName), - Collections.singletonList(paramValue)); - assertEquals(filterRequest.getParameterValuesAsList(listParamName), Arrays.asList(listParamValue)); - - assertEquals(filterRequest.getHeader(headerName), headerValue); - assertEquals(filterRequest.getAttribute(attributeName), attributeValue); - } - - @Test - public void untreatedHeaders_is_populated_from_the_parent_request() { - assertEquals(filterRequest.getUntreatedHeaders().getFirst(headerName), headerValue); - } - - @Test - public void uri_can_be_set() throws Exception { - URI newUri = new URI("http", null, host, port + 1, path, paramName + "=" + paramValue, null); - filterRequest.setUri(newUri); - - assertEquals(filterRequest.getUri(), newUri); - assertEquals(parentRequest.getUri(), newUri); - } - - @Test - public void attributes_can_be_set() throws Exception { - String name = "newAttribute"; - String value = name + "Value"; - filterRequest.setAttribute(name, value); - - assertEquals(filterRequest.getAttribute(name), value); - assertEquals(parentRequest.getAttribute(name), value); - } - - @Test - public void attributes_can_be_removed() { - filterRequest.removeAttribute(attributeName); - - assertEquals(filterRequest.getAttribute(attributeName), null); - assertEquals(parentRequest.getAttribute(attributeName), null); - } - - @Test - public void headers_can_be_set() throws Exception { - String name = "myHeader"; - String value = name + "Value"; - filterRequest.setHeaders(name, value); - - assertEquals(filterRequest.getHeader(name), value); - assertEquals(parentRequest.getHeader(name), value); - } - - @Test - public void headers_can_be_removed() throws Exception { - filterRequest.removeHeaders(headerName); - - assertEquals(filterRequest.getHeader(headerName), null); - assertEquals(parentRequest.getHeader(headerName), null); - } - - @Test - public void headers_can_be_added() { - String value = "h2"; - filterRequest.addHeader(headerName, value); - - List expected = Arrays.asList(headerValue, value); - assertEquals(filterRequest.getHeadersAsList(headerName), expected); - assertEquals(Collections.list(parentRequest.getHeaders(headerName)), expected); - } - - @Test - public void cookies_can_be_added_and_removed() { - Cookie cookie = new Cookie("name", "value"); - filterRequest.addCookie(JDiscCookieWrapper.wrap(cookie)); - - assertEquals(filterRequest.getCookies(), Collections.singletonList(cookie)); - assertEquals(parentRequest.getCookies().length, 1); - - javax.servlet.http.Cookie servletCookie = parentRequest.getCookies()[0]; - assertEquals(servletCookie.getName(), cookie.getName()); - assertEquals(servletCookie.getValue(), cookie.getValue()); - - filterRequest.clearCookies(); - assertTrue(filterRequest.getCookies().isEmpty()); - assertEquals(parentRequest.getCookies().length, 0); - } - - @Test - public void character_encoding_can_be_set() throws Exception { - // ContentType must be non-null before setting character encoding - filterRequest.setHeaders(HttpHeaders.Names.CONTENT_TYPE, ""); - - String encoding = "myEncoding"; - filterRequest.setCharacterEncoding(encoding); - - assertTrue(filterRequest.getCharacterEncoding().contains(encoding)); - assertTrue(parentRequest.getCharacterEncoding().contains(encoding)); - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java deleted file mode 100644 index a2bc2badea3..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/filter/ServletFilterResponseTest.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.filter; - -import com.yahoo.jdisc.http.Cookie; -import com.yahoo.jdisc.http.HttpHeaders; -import com.yahoo.jdisc.http.servlet.ServletResponse; -import org.junit.Before; -import org.junit.Test; - -import java.util.Arrays; - -import static org.junit.Assert.assertEquals; - -/** - * @author gjoranv - * @since 5.27 - */ -public class ServletFilterResponseTest { - - private final String headerName = "header1"; - private final String headerValue = "h1"; - - private DiscFilterResponse filterResponse; - private ServletResponse parentResponse; - - @Before - public void init() throws Exception { - filterResponse = new ServletFilterResponse(newServletResponse()); - parentResponse = ((ServletFilterResponse)filterResponse).getServletResponse(); - - } - - private ServletResponse newServletResponse() throws Exception { - MockServletResponse parent = new MockServletResponse(); - parent.addHeader(headerName, headerValue); - return new ServletResponse(parent); - } - - - @Test - public void headers_can_be_set() throws Exception { - String name = "myHeader"; - String value = name + "Value"; - filterResponse.setHeaders(name, value); - - assertEquals(filterResponse.getHeader(name), value); - assertEquals(parentResponse.getHeader(name), value); - } - - @Test - public void headers_can_be_added() throws Exception { - String newValue = "h2"; - filterResponse.addHeader(headerName, newValue); - - // The DiscFilterResponse has no getHeaders() - assertEquals(filterResponse.getHeader(headerName), newValue); - - assertEquals(parentResponse.getHeaders(headerName), Arrays.asList(headerValue, newValue)); - } - - @Test - public void headers_can_be_removed() throws Exception { - filterResponse.removeHeaders(headerName); - - assertEquals(filterResponse.getHeader(headerName), null); - assertEquals(parentResponse.getHeader(headerName), null); - } - - @Test - public void set_cookie_overwrites_old_values() { - Cookie to_be_removed = new Cookie("to-be-removed", ""); - Cookie to_keep = new Cookie("to-keep", ""); - filterResponse.setCookie(to_be_removed.getName(), to_be_removed.getValue()); - filterResponse.setCookie(to_keep.getName(), to_keep.getValue()); - - assertEquals(filterResponse.getCookies(), Arrays.asList(to_keep)); - assertEquals(parentResponse.getHeaders(HttpHeaders.Names.SET_COOKIE), Arrays.asList(to_keep.toString())); - } - - - private static class MockServletResponse extends org.eclipse.jetty.server.Response { - private MockServletResponse() { - super(null, null); - } - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java deleted file mode 100644 index cc2a00c08c6..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ConnectorFactoryRegistryModule.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.guiceModules; - -import com.google.inject.Binder; -import com.google.inject.Module; -import com.google.inject.Provides; -import com.yahoo.component.ComponentId; -import com.yahoo.component.provider.ComponentRegistry; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ConnectorConfig.Builder; - -import com.yahoo.jdisc.http.server.jetty.ConnectorFactory; -import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; - -/** - * Guice module for test ConnectorFactories - * - * @author Tony Vaagenes - */ -public class ConnectorFactoryRegistryModule implements Module { - - private final Builder connectorConfigBuilder; - - public ConnectorFactoryRegistryModule(Builder connectorConfigBuilder) { - this.connectorConfigBuilder = connectorConfigBuilder; - } - - public ConnectorFactoryRegistryModule() { - this(new Builder()); - } - - @Provides - public ComponentRegistry connectorFactoryComponentRegistry() { - ComponentRegistry registry = new ComponentRegistry<>(); - registry.register(ComponentId.createAnonymousComponentId("connector-factory"), - new StaticKeyDbConnectorFactory(new ConnectorConfig(connectorConfigBuilder))); - - registry.freeze(); - return registry; - } - - @Override - public void configure(Binder binder) { - } - - private static class StaticKeyDbConnectorFactory extends ConnectorFactory { - - public StaticKeyDbConnectorFactory(ConnectorConfig connectorConfig) { - super(connectorConfig, new ConfiguredSslContextFactoryProvider(connectorConfig)); - } - - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java deleted file mode 100644 index dd6511d1f88..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/guiceModules/ServletModule.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.guiceModules; - -import com.google.inject.Binder; -import com.google.inject.Module; -import com.google.inject.Provides; -import com.yahoo.component.provider.ComponentRegistry; - -import org.eclipse.jetty.servlet.ServletHolder; - -/** - * @author Tony Vaagenes - */ -public class ServletModule implements Module { - @Override - public void configure(Binder binder) { - } - - @Provides - public ComponentRegistry servletHolderComponentRegistry() { - return new ComponentRegistry<>(); - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java deleted file mode 100644 index 6370912af48..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/AccessLogRequestLogTest.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.AccessLogEntry; -import com.yahoo.container.logging.RequestLog; -import com.yahoo.container.logging.RequestLogEntry; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ServerConfig; -import org.eclipse.jetty.http.MetaData; -import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.HttpConnection; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; -import org.junit.Test; - -import java.util.List; -import java.util.Optional; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Oyvind Bakksjo - * @author bjorncs - */ -public class AccessLogRequestLogTest { - @Test - public void requireThatQueryWithUnquotedSpecialCharactersIsHandled() { - final Request jettyRequest = createRequestMock(); - when(jettyRequest.getRequestURI()).thenReturn("/search/"); - when(jettyRequest.getQueryString()).thenReturn("query=year:>2010"); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - - assertThat(entry.rawPath().get(), is(not(nullValue()))); - assertTrue(entry.rawQuery().isPresent()); - } - - @Test - public void requireThatDoubleQuotingIsNotPerformed() { - final Request jettyRequest = createRequestMock(); - final String path = "/search/"; - when(jettyRequest.getRequestURI()).thenReturn(path); - final String query = "query=year%252010+%3B&customParameter=something"; - when(jettyRequest.getQueryString()).thenReturn(query); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - - assertThat(entry.rawPath().get(), is(path)); - assertThat(entry.rawQuery().get(), is(query)); - - } - - @Test - public void raw_path_and_query_are_set_from_request() { - Request jettyRequest = createRequestMock(); - String rawPath = "//search/"; - when(jettyRequest.getRequestURI()).thenReturn(rawPath); - String rawQuery = "q=%%2"; - when(jettyRequest.getQueryString()).thenReturn(rawQuery); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - assertThat(entry.rawPath().get(), is(rawPath)); - Optional actualRawQuery = entry.rawQuery(); - assertThat(actualRawQuery.isPresent(), is(true)); - assertThat(actualRawQuery.get(), is(rawQuery)); - } - - @Test - public void verify_x_forwarded_for_precedence () { - Request jettyRequest = createRequestMock(); - when(jettyRequest.getRequestURI()).thenReturn("//search/"); - when(jettyRequest.getQueryString()).thenReturn("q=%%2"); - when(jettyRequest.getHeader("x-forwarded-for")).thenReturn("1.2.3.4"); - when(jettyRequest.getHeader("y-ra")).thenReturn("2.3.4.5"); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - assertThat(entry.remoteAddress().get(), is("1.2.3.4")); - } - - @Test - public void verify_x_forwarded_port_precedence () { - Request jettyRequest = createRequestMock(); - when(jettyRequest.getRequestURI()).thenReturn("//search/"); - when(jettyRequest.getQueryString()).thenReturn("q=%%2"); - when(jettyRequest.getHeader("X-Forwarded-Port")).thenReturn("80"); - when(jettyRequest.getHeader("y-rp")).thenReturn("8080"); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - assertThat(entry.remotePort().getAsInt(), is(80)); - } - - @Test - public void defaults_to_peer_port_if_remote_port_header_is_invalid() { - final Request jettyRequest = createRequestMock(); - when(jettyRequest.getRequestURI()).thenReturn("/search/"); - when(jettyRequest.getHeader("X-Forwarded-Port")).thenReturn("8o8o"); - when(jettyRequest.getRemotePort()).thenReturn(80); - - InMemoryRequestLog requestLog = new InMemoryRequestLog(); - doAccessLoggingOfRequest(requestLog, jettyRequest); - RequestLogEntry entry = requestLog.entries().get(0); - assertFalse(entry.remotePort().isPresent()); - assertThat(entry.peerPort().getAsInt(), is(80)); - } - - private void doAccessLoggingOfRequest(RequestLog requestLog, Request jettyRequest) { - ServerConfig.AccessLog config = new ServerConfig.AccessLog( - new ServerConfig.AccessLog.Builder() - .remoteAddressHeaders(List.of("x-forwarded-for", "y-ra")) - .remotePortHeaders(List.of("X-Forwarded-Port", "y-rp"))); - new AccessLogRequestLog(requestLog, config).log(jettyRequest, createResponseMock()); - } - - private static Request createRequestMock() { - JDiscServerConnector serverConnector = mock(JDiscServerConnector.class); - int localPort = 1234; - when(serverConnector.connectorConfig()).thenReturn(new ConnectorConfig(new ConnectorConfig.Builder().listenPort(localPort))); - when(serverConnector.getLocalPort()).thenReturn(localPort); - HttpConnection httpConnection = mock(HttpConnection.class); - when(httpConnection.getConnector()).thenReturn(serverConnector); - Request request = mock(Request.class); - when(request.getMethod()).thenReturn("GET"); - when(request.getRemoteAddr()).thenReturn("localhost"); - when(request.getRemotePort()).thenReturn(12345); - when(request.getProtocol()).thenReturn("HTTP/1.1"); - when(request.getScheme()).thenReturn("http"); - when(request.getTimeStamp()).thenReturn(0L); - when(request.getAttribute(JDiscHttpServlet.ATTRIBUTE_NAME_ACCESS_LOG_ENTRY)).thenReturn(new AccessLogEntry()); - when(request.getAttribute("org.eclipse.jetty.server.HttpConnection")).thenReturn(httpConnection); - return request; - } - - private Response createResponseMock() { - Response response = mock(Response.class); - when(response.getHttpChannel()).thenReturn(mock(HttpChannel.class)); - when(response.getCommittedMetaData()).thenReturn(mock(MetaData.Response.class)); - return response; - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java deleted file mode 100644 index c1a2bea8ac4..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/BlockingQueueRequestLog.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.container.logging.RequestLog; -import com.yahoo.container.logging.RequestLogEntry; - -import java.time.Duration; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; - -/** - * @author bjorncs - */ -class BlockingQueueRequestLog implements RequestLog { - - private final BlockingQueue entries = new LinkedBlockingDeque<>(); - - @Override public void log(RequestLogEntry entry) { entries.offer(entry); } - - RequestLogEntry poll(Duration timeout) throws InterruptedException { - return entries.poll(timeout.toMillis(), TimeUnit.MILLISECONDS); - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java deleted file mode 100644 index 65eb7e1c145..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectionThrottlerTest.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.http.ConnectorConfig; -import org.eclipse.jetty.server.AbstractConnector; -import org.eclipse.jetty.util.component.AbstractLifeCycle; -import org.eclipse.jetty.util.statistic.RateStatistic; -import org.eclipse.jetty.util.thread.Scheduler; -import org.junit.Test; - -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.internal.verification.VerificationModeFactory.times; - -/** - * @author bjorncs - */ -public class ConnectionThrottlerTest { - - @Test - public void throttles_when_any_resource_check_exceeds_configured_threshold() { - Runtime runtime = mock(Runtime.class); - when(runtime.maxMemory()).thenReturn(100l); - RateStatistic rateStatistic = new RateStatistic(1, TimeUnit.HOURS); - MockScheduler scheduler = new MockScheduler(); - ConnectorConfig.Throttling config = new ConnectorConfig.Throttling(new ConnectorConfig.Throttling.Builder() - .maxHeapUtilization(0.8) - .maxAcceptRate(1)); - - AbstractConnector connector = mock(AbstractConnector.class); - - ConnectionThrottler throttler = new ConnectionThrottler(runtime, rateStatistic, scheduler, connector, config); - - // Heap utilization above configured threshold, but connection rate below threshold. - when(runtime.freeMemory()).thenReturn(10l); - when(connector.isAccepting()).thenReturn(true); - throttler.onAccepting(null); - assertNotNull(scheduler.task); - verify(connector).setAccepting(false); - - // Heap utilization below threshold, but connection rate above threshold. - when(runtime.freeMemory()).thenReturn(80l); - rateStatistic.record(); - rateStatistic.record(); // above accept rate limit (2 > 1) - scheduler.task.run(); // run unthrottleIfBelowThresholds() - verify(connector, times(1)).setAccepting(anyBoolean()); // verify setAccepting has not been called any mores times - - // Both heap utilization and accept rate below threshold - when(runtime.freeMemory()).thenReturn(80l); - when(connector.isAccepting()).thenReturn(false); - rateStatistic.reset(); - scheduler.task.run(); // run unthrottleIfBelowThresholds() - verify(connector).setAccepting(true); - - // Both heap utilization and accept rate below threshold - when(connector.isAccepting()).thenReturn(true); - when(runtime.freeMemory()).thenReturn(80l); - rateStatistic.record(); - throttler.onAccepting(null); - verify(connector, times(2)).setAccepting(anyBoolean()); // verify setAccepting has not been called any mores times - } - - private static class MockScheduler extends AbstractLifeCycle implements Scheduler { - Runnable task; - - @Override - public Task schedule(Runnable task, long delay, TimeUnit units) { - this.task = task; - return () -> false; - } - } - -} \ No newline at end of file diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java deleted file mode 100644 index df794c7ecb8..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ConnectorFactoryTest.java +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - -import com.yahoo.jdisc.Metric; -import com.yahoo.jdisc.http.ConnectorConfig; -import com.yahoo.jdisc.http.ServerConfig; -import com.yahoo.jdisc.http.ssl.impl.ConfiguredSslContextFactoryProvider; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.junit.Test; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Map; - -import static org.hamcrest.CoreMatchers.equalTo; - -/** - * @author Einar M R Rosenvinge - */ -public class ConnectorFactoryTest { - - @Test - public void requireThatServerCanBindChannel() throws Exception { - Server server = new Server(); - try { - ConnectorConfig config = new ConnectorConfig(new ConnectorConfig.Builder()); - ConnectorFactory factory = createConnectorFactory(config); - JettyConnectionLogger connectionLogger = new JettyConnectionLogger( - new ServerConfig.ConnectionLog.Builder().enabled(false).build(), - new VoidConnectionLog()); - JDiscServerConnector connector = - (JDiscServerConnector)factory.createConnector(new DummyMetric(), server, connectionLogger); - server.addConnector(connector); - server.setHandler(new HelloWorldHandler()); - server.start(); - - SimpleHttpClient client = new SimpleHttpClient(null, connector.getLocalPort(), false); - SimpleHttpClient.RequestExecutor ex = client.newGet("/blaasdfnb"); - SimpleHttpClient.ResponseValidator val = ex.execute(); - val.expectContent(equalTo("Hello world")); - } finally { - try { - server.stop(); - } catch (Exception e) { - //ignore - } - } - } - - private static ConnectorFactory createConnectorFactory(ConnectorConfig config) { - return new ConnectorFactory(config, new ConfiguredSslContextFactoryProvider(config)); - } - - private static class HelloWorldHandler extends AbstractHandler { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - response.getWriter().write("Hello world"); - response.getWriter().flush(); - response.getWriter().close(); - baseRequest.setHandled(true); - } - } - - private static class DummyMetric implements Metric { - @Override - public void set(String key, Number val, Context ctx) { } - - @Override - public void add(String key, Number val, Context ctx) { } - - @Override - public Context createContext(Map properties) { - return new DummyContext(); - } - } - - private static class DummyContext implements Metric.Context { - } - -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java deleted file mode 100644 index d66f22801f7..00000000000 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/ErrorResponseContentCreatorTest.java +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.jdisc.http.server.jetty; - - -import org.junit.Test; - -import javax.servlet.http.HttpServletResponse; -import java.nio.charset.StandardCharsets; -import java.util.Optional; - -import static org.junit.Assert.assertEquals; - - -/** - * @author bjorncs - */ -public class ErrorResponseContentCreatorTest { - - @Test - public void response_content_matches_expected_string() { - String expectedHtml = - "\n" + - "\n" + - "\n" + - "Error 200\n" + - "\n" + - "\n" + - "

HTTP ERROR: 200

\n" + - "

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

    My custom error message

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