diff --git a/Jenkinsfile b/Jenkinsfile index 2dfc916f2d1..0a91888affe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,51 +3,46 @@ pipeline { agent any // save some io during the build - options { durabilityHint('PERFORMANCE_OPTIMIZED') } + options { durabilityHint( 'PERFORMANCE_OPTIMIZED' ) } stages { - stage("Parallel Stage") { + stage( "Parallel Stage" ) { parallel { - stage("Build / Test - JDK11") { + stage( "Build / Test - JDK11" ) { agent { node { label 'linux' } } - options { timeout(time: 120, unit: 'MINUTES') } steps { - container('jetty-build') { - mavenBuild("jdk11", "-T3 -Pmongodb clean install", "maven3", true) // -Pautobahn - // Collect up the jacoco execution results (only on main build) - jacoco inclusionPattern: '**/org/eclipse/jetty/**/*.class', - exclusionPattern: '' + - // build tools - '**/org/eclipse/jetty/ant/**' + - ',**/org/eclipse/jetty/maven/**' + - ',**/org/eclipse/jetty/jspc/**' + - // example code / documentation - ',**/org/eclipse/jetty/embedded/**' + - ',**/org/eclipse/jetty/asyncrest/**' + - ',**/org/eclipse/jetty/demo/**' + - // special environments / late integrations - ',**/org/eclipse/jetty/gcloud/**' + - ',**/org/eclipse/jetty/infinispan/**' + - ',**/org/eclipse/jetty/osgi/**' + - ',**/org/eclipse/jetty/spring/**' + - ',**/org/eclipse/jetty/http/spi/**' + - // test classes - ',**/org/eclipse/jetty/tests/**' + - ',**/org/eclipse/jetty/test/**', - execPattern: '**/target/jacoco.exec', - classPattern: '**/target/classes', - sourcePattern: '**/src/main/java' - warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] - junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml' + container( 'jetty-build' ) { + timeout( time: 120, unit: 'MINUTES' ) { + mavenBuild( "jdk11", "-T3 -Pmongodb clean install", "maven3", true ) // -Pautobahn + // Collect up the jacoco execution results (only on main build) + jacoco inclusionPattern: '**/org/eclipse/jetty/**/*.class', + exclusionPattern: '' + + // build tools + '**/org/eclipse/jetty/ant/**' + ',**/org/eclipse/jetty/maven/**' + + ',**/org/eclipse/jetty/jspc/**' + + // example code / documentation + ',**/org/eclipse/jetty/embedded/**' + ',**/org/eclipse/jetty/asyncrest/**' + + ',**/org/eclipse/jetty/demo/**' + + // special environments / late integrations + ',**/org/eclipse/jetty/gcloud/**' + ',**/org/eclipse/jetty/infinispan/**' + + ',**/org/eclipse/jetty/osgi/**' + ',**/org/eclipse/jetty/spring/**' + + ',**/org/eclipse/jetty/http/spi/**' + + // test classes + ',**/org/eclipse/jetty/tests/**' + ',**/org/eclipse/jetty/test/**', + execPattern: '**/target/jacoco.exec', + classPattern: '**/target/classes', + sourcePattern: '**/src/main/java' + warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] + junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml' + } } } } - - stage("Build / Test - JDK14") { + stage( "Build / Test - JDK14" ) { agent { node { label 'linux' } } steps { - container('jetty-build') { + container( 'jetty-build' ) { timeout( time: 120, unit: 'MINUTES' ) { mavenBuild( "jdk14", "-T3 -Pmongodb clean install", "maven3", true ) warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] @@ -57,10 +52,10 @@ pipeline { } } - stage("Build Javadoc") { + stage( "Build Javadoc" ) { agent { node { label 'linux' } } steps { - container('jetty-build') { + container( 'jetty-build' ) { timeout( time: 30, unit: 'MINUTES' ) { mavenBuild( "jdk11", "package source:jar javadoc:jar javadoc:aggregate-jar -Peclipse-release -DskipTests -Dpmd.skip=true -Dcheckstyle.skip=true", @@ -70,6 +65,17 @@ pipeline { } } } + stage( "Build Compact3" ) { + agent { node { label 'linux' } } + steps { + container( 'jetty-build' ) { + timeout( time: 30, unit: 'MINUTES' ) { + mavenBuild( "jdk11", "-T3 -Pcompact3 clean install -DskipTests", "maven3", true ) + warnings consoleParsers: [[parserName: 'Maven'], [parserName: 'Java']] + } + } + } + } } } } @@ -86,10 +92,11 @@ pipeline { } } + def slackNotif() { script { try { - if (env.BRANCH_NAME == 'jetty-10.0.x' || env.BRANCH_NAME == 'jetty-9.4.x') { + if (env.BRANCH_NAME == 'jetty-10.0.x' || env.BRANCH_NAME == 'jetty-9.4.x' || env.BRANCH_NAME == 'jetty-11.0.x') { //BUILD_USER = currentBuild.rawBuild.getCause(Cause.UserIdCause).getUserId() // by ${BUILD_USER} COLOR_MAP = ['SUCCESS': 'good', 'FAILURE': 'danger', 'UNSTABLE': 'danger', 'ABORTED': 'danger'] @@ -126,7 +133,7 @@ def mavenBuild(jdk, cmdline, mvnName, junitPublishDisabled) { mavenOpts: mavenOpts, mavenLocalRepo: localRepo) { // Some common Maven command line + provided command line - sh "mvn -Pci -V -B -e -fae -Dmaven.test.failure.ignore=true -Djetty.testtracker.log=true $cmdline -Dunix.socket.tmp=" + env.JENKINS_HOME + sh "mvn -Premote-session-tests -Pci -V -B -e -fae -Dmaven.test.failure.ignore=true -Djetty.testtracker.log=true $cmdline -Dunix.socket.tmp=" + env.JENKINS_HOME } } diff --git a/aggregates/jetty-all/pom.xml b/aggregates/jetty-all/pom.xml index 6e60cc36d63..3382985141e 100644 --- a/aggregates/jetty-all/pom.xml +++ b/aggregates/jetty-all/pom.xml @@ -178,7 +178,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} diff --git a/aggregates/jetty-websocket-all/pom.xml b/aggregates/jetty-websocket-all/pom.xml index fec0ba1be95..c67746b7988 100644 --- a/aggregates/jetty-websocket-all/pom.xml +++ b/aggregates/jetty-websocket-all/pom.xml @@ -107,7 +107,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} provided diff --git a/examples/embedded/pom.xml b/examples/embedded/pom.xml index 22bc490c200..e70d07eeff1 100644 --- a/examples/embedded/pom.xml +++ b/examples/embedded/pom.xml @@ -66,7 +66,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} diff --git a/examples/embedded/src/test/java/org/eclipse/jetty/embedded/ManyHandlersTest.java b/examples/embedded/src/test/java/org/eclipse/jetty/embedded/ManyHandlersTest.java index 9f4ebcf0993..6b0b2d7fa24 100644 --- a/examples/embedded/src/test/java/org/eclipse/jetty/embedded/ManyHandlersTest.java +++ b/examples/embedded/src/test/java/org/eclipse/jetty/embedded/ManyHandlersTest.java @@ -23,6 +23,7 @@ import java.util.Map; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Server; @@ -59,7 +60,7 @@ public class ManyHandlersTest extends AbstractEmbeddedTest ContentResponse response = client.newRequest(uri) .method(HttpMethod.GET) - .header(HttpHeader.ACCEPT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.ACCEPT_ENCODING, HttpHeaderValue.GZIP)) .send(); assertThat("HTTP Response Status", response.getStatus(), is(HttpStatus.OK_200)); @@ -84,7 +85,7 @@ public class ManyHandlersTest extends AbstractEmbeddedTest URI uri = server.getURI().resolve("/hello"); ContentResponse response = client.newRequest(uri) .method(HttpMethod.GET) - .header(HttpHeader.ACCEPT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.ACCEPT_ENCODING, HttpHeaderValue.GZIP)) .send(); assertThat("HTTP Response Status", response.getStatus(), is(HttpStatus.OK_200)); diff --git a/examples/embedded/src/test/java/org/eclipse/jetty/embedded/SecuredHelloHandlerTest.java b/examples/embedded/src/test/java/org/eclipse/jetty/embedded/SecuredHelloHandlerTest.java index 5b569fbaae6..92bec6ec4dd 100644 --- a/examples/embedded/src/test/java/org/eclipse/jetty/embedded/SecuredHelloHandlerTest.java +++ b/examples/embedded/src/test/java/org/eclipse/jetty/embedded/SecuredHelloHandlerTest.java @@ -72,7 +72,7 @@ public class SecuredHelloHandlerTest extends AbstractEmbeddedTest String authEncoded = Base64.getEncoder().encodeToString("user:password".getBytes(UTF_8)); ContentResponse response = client.newRequest(uri) .method(HttpMethod.GET) - .header(HttpHeader.AUTHORIZATION, "Basic " + authEncoded) + .headers(headers -> headers.put(HttpHeader.AUTHORIZATION, "Basic " + authEncoded)) .send(); assertThat("HTTP Response Status", response.getStatus(), is(HttpStatus.OK_200)); diff --git a/jetty-bom/pom.xml b/jetty-bom/pom.xml index 4b073072d34..0a9c50a7b59 100644 --- a/jetty-bom/pom.xml +++ b/jetty-bom/pom.xml @@ -376,7 +376,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server 10.0.0-SNAPSHOT diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index 35067e47a26..494ba110379 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -256,7 +256,7 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler { HttpField field = oldRequest.getHeaders().getField(header); if (field != null && !newRequest.getHeaders().contains(header)) - newRequest.put(field); + newRequest.headers(headers -> headers.put(field)); } private void forwardSuccessComplete(HttpRequest request, Response response) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java index f5270c93dc0..51939e50163 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClient.java @@ -444,7 +444,7 @@ public class HttpClient extends ContainerLifeCycle protected Request copyRequest(HttpRequest oldRequest, URI newURI) { - Request newRequest = newHttpRequest(oldRequest.getConversation(), newURI); + HttpRequest newRequest = newHttpRequest(oldRequest.getConversation(), newURI); newRequest.method(oldRequest.getMethod()) .version(oldRequest.getVersion()) .body(oldRequest.getBody()) @@ -471,10 +471,8 @@ public class HttpClient extends ContainerLifeCycle HttpHeader.PROXY_AUTHORIZATION == header) continue; - String name = field.getName(); - String value = field.getValue(); - if (!newRequest.getHeaders().contains(name, value)) - newRequest.header(name, value); + if (!newRequest.getHeaders().contains(field)) + newRequest.addHeader(field); } return newRequest; } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java index 187a4f133e1..f603d9978f4 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpConnection.java @@ -122,9 +122,9 @@ public abstract class HttpConnection implements IConnection } } - protected void normalizeRequest(Request request) + protected void normalizeRequest(HttpRequest request) { - boolean normalized = ((HttpRequest)request).normalized(); + boolean normalized = request.normalized(); if (LOG.isDebugEnabled()) LOG.debug("Normalizing {} {}", !normalized, request); if (normalized) @@ -153,7 +153,7 @@ public abstract class HttpConnection implements IConnection if (version.getVersion() <= 11) { if (!headers.contains(HttpHeader.HOST)) - request.put(getHttpDestination().getHostField()); + request.addHeader(getHttpDestination().getHostField()); } // Add content headers @@ -167,22 +167,19 @@ public abstract class HttpConnection implements IConnection if (!headers.contains(HttpHeader.CONTENT_TYPE)) { String contentType = content.getContentType(); + if (contentType == null) + contentType = getHttpClient().getDefaultRequestContentType(); if (contentType != null) { - request.put(new HttpField(HttpHeader.CONTENT_TYPE, contentType)); - } - else - { - contentType = getHttpClient().getDefaultRequestContentType(); - if (contentType != null) - request.put(new HttpField(HttpHeader.CONTENT_TYPE, contentType)); + HttpField field = new HttpField(HttpHeader.CONTENT_TYPE, contentType); + request.addHeader(field); } } long contentLength = content.getLength(); if (contentLength >= 0) { if (!headers.contains(HttpHeader.CONTENT_LENGTH)) - request.put(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); + request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength)); } } @@ -195,7 +192,10 @@ public abstract class HttpConnection implements IConnection cookies = convertCookies(HttpCookieStore.matchPath(uri, cookieStore.get(uri)), null); cookies = convertCookies(request.getCookies(), cookies); if (cookies != null) - request.header(HttpHeader.COOKIE, cookies.toString()); + { + HttpField cookieField = new HttpField(HttpHeader.COOKIE, cookies.toString()); + request.addHeader(cookieField); + } } // Authentication diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java index 64a2812a024..82b1918428d 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpProxy.java @@ -200,9 +200,9 @@ public class HttpProxy extends ProxyConfiguration.Proxy Origin.Address proxyAddress = destination.getConnectAddress(); HttpClient httpClient = destination.getHttpClient(); Request connect = new TunnelRequest(httpClient, proxyAddress) - .method(HttpMethod.CONNECT) - .path(target) - .header(HttpHeader.HOST, target); + .method(HttpMethod.CONNECT) + .path(target) + .headers(headers -> headers.put(HttpHeader.HOST, target)); ProxyConfiguration.Proxy proxy = destination.getProxy(); if (proxy.isSecure()) connect.scheme(HttpScheme.HTTPS.asString()); @@ -262,8 +262,7 @@ public class HttpProxy extends ProxyConfiguration.Proxy } else { - HttpResponseException failure = new HttpResponseException("Unexpected " + response + - " for " + response.getRequest(), response); + HttpResponseException failure = new HttpResponseException("Unexpected " + response + " for " + response.getRequest(), response); tunnelFailed(endPoint, failure); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index c39d75b1717..85fe08b44d6 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -72,7 +72,7 @@ import org.slf4j.LoggerFactory; */ public abstract class HttpReceiver { - protected static final Logger LOG = LoggerFactory.getLogger(HttpReceiver.class); + private static final Logger LOG = LoggerFactory.getLogger(HttpReceiver.class); private final AtomicReference responseState = new AtomicReference<>(ResponseState.IDLE); private final HttpChannel channel; @@ -242,7 +242,7 @@ public abstract class HttpReceiver boolean process = notifier.notifyHeader(exchange.getConversation().getResponseListeners(), response, field); if (process) { - response.getHeaderFieldsMutable().add(field); + response.addHeader(field); HttpHeader fieldHeader = field.getHeader(); if (fieldHeader != null) { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java index 8d4e7a6eca8..c6c329d2507 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpRequest.java @@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.function.Supplier; @@ -306,34 +307,7 @@ public class HttpRequest implements Request } @Override - public Request set(HttpFields fields) - { - headers.clear().add(fields); - return this; - } - - @Override - public Request remove(HttpHeader header) - { - headers.remove(header); - return this; - } - - @Override - public Request put(HttpField field) - { - headers.put(field); - return this; - } - - @Override - public Request add(HttpField field) - { - headers.add(field); - return this; - } - - @Override + @Deprecated public Request header(String name, String value) { if (value == null) @@ -344,6 +318,7 @@ public class HttpRequest implements Request } @Override + @Deprecated public Request header(HttpHeader header, String value) { if (value == null) @@ -402,6 +377,19 @@ public class HttpRequest implements Request return headers; } + @Override + public Request headers(Consumer consumer) + { + consumer.accept(headers); + return this; + } + + public HttpRequest addHeader(HttpField header) + { + headers.add(header); + return this; + } + @Override @SuppressWarnings("unchecked") public List getRequestListeners(Class type) @@ -707,7 +695,7 @@ public class HttpRequest implements Request public Request content(ContentProvider content, String contentType) { if (contentType != null) - header(HttpHeader.CONTENT_TYPE, contentType); + headers.put(HttpHeader.CONTENT_TYPE, contentType); return body(ContentProvider.toRequestContent(content)); } @@ -886,7 +874,7 @@ public class HttpRequest implements Request * headers, etc.

* * @return whether this request was already normalized - * @see HttpConnection#normalizeRequest(Request) + * @see HttpConnection#normalizeRequest(HttpRequest) */ boolean normalized() { diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java index 3d941c92827..0eca051d6a0 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpResponse.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.client; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; @@ -91,9 +92,16 @@ public class HttpResponse implements Response return headers.asImmutable(); } - public HttpFields.Mutable getHeaderFieldsMutable() + public HttpResponse addHeader(HttpField header) { - return headers; + headers.add(header); + return this; + } + + public HttpResponse headers(Consumer consumer) + { + consumer.accept(headers); + return this; } @Override @@ -110,7 +118,7 @@ public class HttpResponse implements Response public HttpFields getTrailers() { - return trailers; + return trailers == null ? null : trailers.asImmutable(); } public HttpResponse trailer(HttpField trailer) diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java index 7b2d14edb27..c1831b869ac 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Request.java @@ -31,10 +31,10 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.util.InputStreamResponseListener; -import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -166,38 +166,22 @@ public interface Request */ HttpFields getHeaders(); - /** Set the headers, clearing any existing headers - * @param fields The fields to set - * @return this request object - */ - Request set(HttpFields fields); - /** - * @param header the header to remove + * Modifies the headers of this request. + * + * @param consumer the code that modifies the headers of this request * @return this request object */ - Request remove(HttpHeader header); - - /** - * @param field the field to add - * @return this request object - * @see #header(HttpHeader, String) - */ - Request add(HttpField field); - - /** - * @param field the field to put - * @return this request object - * @see #header(HttpHeader, String) - */ - Request put(HttpField field); + Request headers(Consumer consumer); /** * @param name the name of the header * @param value the value of the header * @return this request object * @see #header(HttpHeader, String) + * @deprecated use {@link #headers(Consumer)} instead */ + @Deprecated Request header(String name, String value); /** @@ -208,7 +192,9 @@ public interface Request * @param header the header name * @param value the value of the header * @return this request object + * @deprecated use {@link #headers(Consumer)} instead */ + @Deprecated Request header(HttpHeader header, String value); /** diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java index 1375d799458..f6bd5d11353 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpConnectionOverHTTP.java @@ -261,7 +261,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne @Override public SendFailure send(HttpExchange exchange) { - Request request = exchange.getRequest(); + HttpRequest request = exchange.getRequest(); normalizeRequest(request); // Save the old idle timeout to restore it. @@ -276,7 +276,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne } @Override - protected void normalizeRequest(Request request) + protected void normalizeRequest(HttpRequest request) { super.normalizeRequest(request); @@ -287,8 +287,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne .idleTimeout(2 * connectTimeout, TimeUnit.MILLISECONDS); } - HttpRequest httpRequest = (HttpRequest)request; - HttpConversation conversation = httpRequest.getConversation(); + HttpConversation conversation = request.getConversation(); HttpUpgrader upgrader = (HttpUpgrader)conversation.getAttribute(HttpUpgrader.class.getName()); if (upgrader == null) { @@ -296,7 +295,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne { upgrader = ((HttpUpgrader.Factory)request).newHttpUpgrader(HttpVersion.HTTP_1_1); conversation.setAttribute(HttpUpgrader.class.getName(), upgrader); - upgrader.prepare(httpRequest); + upgrader.prepare(request); } else { @@ -305,7 +304,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne { upgrader = new ProtocolHttpUpgrader(getHttpDestination(), protocol); conversation.setAttribute(HttpUpgrader.class.getName(), upgrader); - upgrader.prepare(httpRequest); + upgrader.prepare(request); } } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 4bd4307fed1..5047b482338 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -38,9 +38,13 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.ResponseHandler { + private static final Logger LOG = LoggerFactory.getLogger(HttpReceiverOverHTTP.class); + private final HttpParser parser; private RetainableByteBuffer networkBuffer; private boolean shutdown; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java index ed3665074f0..faa889a63f1 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/BasicAuthentication.java @@ -105,7 +105,7 @@ public class BasicAuthentication extends AbstractAuthentication public void apply(Request request) { if (!request.getHeaders().contains(header, value)) - request.header(header, value); + request.headers(headers -> headers.add(header, value)); } @Override diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java index 466ea8168b6..dac81b63102 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java @@ -204,7 +204,7 @@ public class DigestAuthentication extends AbstractAuthentication } value.append(", response=\"").append(hashA3).append("\""); - request.header(header, value.toString()); + request.headers(headers -> headers.add(header, value.toString())); } private String nextNonceCount() diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/FormContentProvider.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/FormContentProvider.java index 2bff3857e3a..356dfe75f6b 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/FormContentProvider.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/FormContentProvider.java @@ -18,11 +18,8 @@ package org.eclipse.jetty.client.util; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.charset.UnsupportedCharsetException; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.util.Fields; @@ -53,29 +50,6 @@ public class FormContentProvider extends StringContentProvider public static String convert(Fields fields, Charset charset) { - // Assume 32 chars between name and value. - StringBuilder builder = new StringBuilder(fields.getSize() * 32); - for (Fields.Field field : fields) - { - for (String value : field.getValues()) - { - if (builder.length() > 0) - builder.append("&"); - builder.append(encode(field.getName(), charset)).append("=").append(encode(value, charset)); - } - } - return builder.toString(); - } - - private static String encode(String value, Charset charset) - { - try - { - return URLEncoder.encode(value, charset.name()); - } - catch (UnsupportedEncodingException x) - { - throw new UnsupportedCharsetException(charset.name()); - } + return FormRequestContent.convert(fields, charset); } } diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java index 5f3d7742604..a3398393372 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/SPNEGOAuthentication.java @@ -307,7 +307,7 @@ public class SPNEGOAuthentication extends AbstractAuthentication @Override public void apply(Request request) { - request.header(header, value); + request.headers(headers -> headers.add(header, value)); } } diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java index 70cb83e408f..bd6c64dde53 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ClientConnectionCloseTest.java @@ -86,7 +86,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest var request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .body(new StringRequestContent("0")) .onRequestSuccess(r -> { @@ -126,7 +126,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest long idleTimeout = 1000; var request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) .onRequestSuccess(r -> { @@ -188,7 +188,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest CountDownLatch resultLatch = new CountDownLatch(1); var request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .body(content) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) .onRequestSuccess(r -> @@ -242,7 +242,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest var request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .onRequestSuccess(r -> { HttpDestination destination = (HttpDestination)client.resolveDestination(r); @@ -250,10 +250,7 @@ public class ClientConnectionCloseTest extends AbstractHttpClientServerTest HttpConnectionOverHTTP connection = (HttpConnectionOverHTTP)connectionPool.getActiveConnections().iterator().next(); assertFalse(connection.getEndPoint().isOutputShutdown()); }) - .onResponseHeaders(r -> - { - ((HttpResponse)r).getHeaderFieldsMutable().remove(HttpHeader.CONNECTION); - }); + .onResponseHeaders(r -> ((HttpResponse)r).headers(headers -> headers.remove(HttpHeader.CONNECTION))); ContentResponse response = request.send(); assertEquals(HttpStatus.OK_200, response.getStatus()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java index 08139dbf1d6..581d61a7c03 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ConnectionPoolTest.java @@ -19,7 +19,6 @@ package org.eclipse.jetty.client; import java.io.IOException; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -36,6 +35,7 @@ import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.client.util.FutureResponseListener; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Handler; @@ -45,7 +45,6 @@ import org.eclipse.jetty.util.IO; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -58,20 +57,10 @@ public class ConnectionPoolTest private ServerConnector connector; private HttpClient client; - public static Stream pools() + public static Stream pools() { - List pools = new ArrayList<>(); - pools.add(new Object[]{ - DuplexConnectionPool.class, - (ConnectionPool.Factory) - destination -> new DuplexConnectionPool(destination, 8, destination) - }); - pools.add(new Object[]{ - RoundRobinConnectionPool.class, - (ConnectionPool.Factory) - destination -> new RoundRobinConnectionPool(destination, 8, destination) - }); - return pools.stream().map(Arguments::of); + return Stream.of(destination -> new DuplexConnectionPool(destination, 8, destination), + destination -> new RoundRobinConnectionPool(destination, 8, destination)); } private void start(final ConnectionPool.Factory factory, Handler handler) throws Exception @@ -112,7 +101,7 @@ public class ConnectionPoolTest @ParameterizedTest(name = "[{index}] {0}") @MethodSource("pools") - public void test(Class connectionPoolClass, ConnectionPool.Factory factory) throws Exception + public void test(ConnectionPool.Factory factory) throws Exception { start(factory, new EmptyServerHandler() { @@ -202,17 +191,17 @@ public class ConnectionPoolTest .method(method); if (clientClose) - request.header(HttpHeader.CONNECTION, "close"); + request.headers(fields -> fields.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); else if (serverClose) - request.header("X-Close", "true"); + request.headers(fields -> fields.put("X-Close", "true")); switch (method) { case GET: - request.header("X-Download", String.valueOf(contentLength)); + request.headers(fields -> fields.put("X-Download", String.valueOf(contentLength))); break; case POST: - request.header(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength)); + request.headers(fields -> fields.put(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength))); request.body(new BytesRequestContent(new byte[contentLength])); break; default: diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCorrelationDataTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCorrelationDataTest.java index 3fa4680c475..71331cd8b12 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCorrelationDataTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientCorrelationDataTest.java @@ -54,7 +54,7 @@ public class HttpClientCorrelationDataTest extends AbstractHttpClientServerTest @Override public void onQueued(Request request) { - request.header(correlationName, correlation.get()); + request.headers(headers -> headers.put(correlationName, correlation.get())); } }); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java index bdf75fbbdb9..5d20037f480 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyProtocolTest.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Destination; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.EndPoint; @@ -248,7 +249,7 @@ public class HttpClientProxyProtocolTest // The proxy maps the client address, then sends the request. ContentResponse response = client.newRequest("localhost", serverPort) .tag(tag) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .send(); assertEquals(HttpStatus.OK_200, response.getStatus()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java index e223f8ba2e0..35959299f0a 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientProxyTest.java @@ -379,7 +379,7 @@ public class HttpClientProxyTest extends AbstractHttpClientServerTest // Make a request, expect 407 + 204. ContentResponse response1 = client.newRequest(serverHost, serverPort) .scheme(scenario.getScheme()) - .header(HttpHeader.AUTHORIZATION, "Basic foobar") + .headers(headers -> headers.put(HttpHeader.AUTHORIZATION, "Basic foobar")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -390,7 +390,7 @@ public class HttpClientProxyTest extends AbstractHttpClientServerTest requests.set(0); ContentResponse response2 = client.newRequest(serverHost, serverPort) .scheme(scenario.getScheme()) - .header(HttpHeader.AUTHORIZATION, "Basic foobar") + .headers(headers -> headers.put(HttpHeader.AUTHORIZATION, "Basic foobar")) .timeout(5, TimeUnit.SECONDS) .send(); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java index 2678e18778b..491df8bdbea 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTLSTest.java @@ -44,6 +44,7 @@ import javax.net.ssl.SSLSocket; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.ByteBufferPool; @@ -380,7 +381,7 @@ public class HttpClientTLSTest // First request primes the TLS session. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(HttpScheme.HTTPS.asString()) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .timeout(5, TimeUnit.SECONDS) .send(); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -416,7 +417,7 @@ public class HttpClientTLSTest // Second request should have the same session ID. response = client.newRequest("localhost", connector.getLocalPort()) .scheme(HttpScheme.HTTPS.asString()) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .timeout(5, TimeUnit.SECONDS) .send(); assertEquals(HttpStatus.OK_200, response.getStatus()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java index 79a377408b1..0c516e4e24b 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientTest.java @@ -907,7 +907,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest // If no exceptions the test passes. client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .send(); } @@ -938,8 +938,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) - .header(HttpHeader.USER_AGENT, null) - .header(HttpHeader.USER_AGENT, userAgent) + .headers(headers -> headers.put(HttpHeader.USER_AGENT, userAgent)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -985,7 +984,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest // User agent explicitly removed. response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) - .header(HttpHeader.USER_AGENT, null) + .headers(headers -> headers.remove(HttpHeader.USER_AGENT)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -1213,7 +1212,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest ContentResponse response = client.newRequest("http://127.0.0.1:" + connector.getLocalPort() + "/path") .scheme(scenario.getScheme()) - .header(HttpHeader.HOST, host) + .headers(headers -> headers.put(HttpHeader.HOST, host)) .send(); assertEquals(200, response.getStatus()); @@ -1239,7 +1238,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .version(HttpVersion.HTTP_1_0) - .header(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString())) .timeout(5, TimeUnit.SECONDS) .send(); @@ -1266,7 +1265,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest Request request = client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .version(HttpVersion.HTTP_1_0) - .header(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString())) .timeout(timeout, TimeUnit.MILLISECONDS); FuturePromise promise = new FuturePromise<>(); Destination destination = client.resolveDestination(request); @@ -1295,7 +1294,7 @@ public class HttpClientTest extends AbstractHttpClientServerTest ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .version(HttpVersion.HTTP_1_0) - .header(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE.asString())) .timeout(5, TimeUnit.SECONDS) .send(); @@ -1647,8 +1646,8 @@ public class HttpClientTest extends AbstractHttpClientServerTest .timeout(321, TimeUnit.SECONDS) .idleTimeout(2221, TimeUnit.SECONDS) .followRedirects(true) - .header(HttpHeader.CONTENT_TYPE, "application/json") - .header("X-Some-Custom-Header", "some-value")); + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, "application/json")) + .headers(headers -> headers.put("X-Some-Custom-Header", "some-value"))); assertCopyRequest(client.newRequest("https://example.com") .method(HttpMethod.POST) @@ -1657,30 +1656,30 @@ public class HttpClientTest extends AbstractHttpClientServerTest .timeout(123231, TimeUnit.SECONDS) .idleTimeout(232342, TimeUnit.SECONDS) .followRedirects(false) - .header(HttpHeader.ACCEPT, "application/json") - .header("X-Some-Other-Custom-Header", "some-other-value")); + .headers(headers -> headers.put(HttpHeader.ACCEPT, "application/json")) + .headers(headers -> headers.put("X-Some-Other-Custom-Header", "some-other-value"))); assertCopyRequest(client.newRequest("https://example.com") - .header(HttpHeader.ACCEPT, "application/json") - .header(HttpHeader.ACCEPT, "application/xml") - .header("x-same-name", "value1") - .header("x-same-name", "value2")); + .headers(headers -> headers.add(HttpHeader.ACCEPT, "application/json")) + .headers(headers -> headers.add(HttpHeader.ACCEPT, "application/xml")) + .headers(headers -> headers.add("x-same-name", "value1")) + .headers(headers -> headers.add("x-same-name", "value2"))); assertCopyRequest(client.newRequest("https://example.com") - .header(HttpHeader.ACCEPT, "application/json") - .header(HttpHeader.CONTENT_TYPE, "application/json")); + .headers(headers -> headers.put(HttpHeader.ACCEPT, "application/json")) + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, "application/json"))); assertCopyRequest(client.newRequest("https://example.com") - .header("Accept", "application/json") - .header("Content-Type", "application/json")); + .headers(headers -> headers.put("Accept", "application/json")) + .headers(headers -> headers.put("Content-Type", "application/json"))); assertCopyRequest(client.newRequest("https://example.com") - .header("X-Custom-Header-1", "value1") - .header("X-Custom-Header-2", "value2")); + .headers(headers -> headers.put("X-Custom-Header-1", "value1")) + .headers(headers -> headers.put("X-Custom-Header-2", "value2"))); assertCopyRequest(client.newRequest("https://example.com") - .header("X-Custom-Header-1", "value") - .header("X-Custom-Header-2", "value")); + .headers(headers -> headers.put("X-Custom-Header-1", "value")) + .headers(headers -> headers.put("X-Custom-Header-2", "value"))); } @ParameterizedTest diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index 5d622444395..a06fd83957d 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -189,7 +189,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest public void onBegin(Request request) { // Remove the host header, this will make the request invalid - request.header(HttpHeader.HOST, null); + request.headers(headers -> headers.remove(HttpHeader.HOST)); } @Override @@ -251,7 +251,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest public void onBegin(Request request) { // Remove the host header, this will make the request invalid - request.header(HttpHeader.HOST, null); + request.headers(headers -> headers.remove(HttpHeader.HOST)); } @Override diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java index 6699c7cb336..9b58e6afee1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpCookieTest.java @@ -198,7 +198,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -207,7 +207,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -259,7 +259,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo/bar") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -268,7 +268,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -320,7 +320,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -329,7 +329,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -381,7 +381,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo/bar") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -390,7 +390,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -444,7 +444,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -453,7 +453,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -514,7 +514,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -523,7 +523,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -587,7 +587,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -596,7 +596,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); @@ -648,7 +648,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse response = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path("/foo/bar") - .header(headerName, "0") + .headers(headers -> headers.put(headerName, "0")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -657,7 +657,7 @@ public class HttpCookieTest extends AbstractHttpClientServerTest ContentResponse r = send(client.newRequest("localhost", connector.getLocalPort()) .scheme(scenario.getScheme()) .path(path) - .header(headerName, "1") + .headers(headers -> headers.put(headerName, "1")) .timeout(5, TimeUnit.SECONDS)); assertEquals(HttpStatus.OK_200, r.getStatus()); }); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java index c49d3c2c946..7b866206883 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/NetworkTrafficListenerTest.java @@ -22,8 +22,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.ByteBuffer; -import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -44,7 +44,6 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.NetworkTrafficListener; import org.eclipse.jetty.io.NetworkTrafficSocketChannelEndPoint; -import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.NetworkTrafficServerConnector; @@ -189,7 +188,7 @@ public class NetworkTrafficListenerTest }); ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()) + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .send(); assertEquals(HttpStatus.OK_200, response.getStatus()); @@ -507,16 +506,9 @@ public class NetworkTrafficListenerTest super(new HttpClientTransportOverHTTP(new ClientConnector() { @Override - protected SelectorManager newSelectorManager() + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - return new ClientSelectorManager(getExecutor(), getScheduler(), getSelectors()) - { - @Override - protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) - { - return new NetworkTrafficSocketChannelEndPoint(channel, selector, selectionKey, getScheduler(), getIdleTimeout().toMillis(), listener.get()); - } - }; + return new NetworkTrafficSocketChannelEndPoint(channel, selector, selectionKey, getScheduler(), getIdleTimeout().toMillis(), listener.get()); } })); this.listener = listener; diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java index 81a06f8ec8f..0bc41c78721 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/api/Usage.java @@ -81,7 +81,7 @@ public class Usage .path("/uri") .version(HttpVersion.HTTP_1_1) .param("a", "b") - .header("X-Header", "Y-value") + .headers(headers -> headers.put("X-Header", "Y-value")) .agent("Jetty HTTP Client") .idleTimeout(5000, TimeUnit.MILLISECONDS) .timeout(20, TimeUnit.SECONDS); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java index 49e8c0c46a3..e2543bee6a3 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/http/HttpDestinationOverHTTPTest.java @@ -262,7 +262,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest int port = connector.getLocalPort(); Request request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); Destination destinationBefore = client.resolveDestination(request); ContentResponse response = request.send(); @@ -275,7 +275,7 @@ public class HttpDestinationOverHTTPTest extends AbstractHttpClientServerTest request = client.newRequest(host, port) .scheme(scenario.getScheme()) - .header(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString()); + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); response = request.send(); assertEquals(200, response.getStatus()); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java index c961343c3c5..f575938cd4c 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/ssl/SslBytesServerTest.java @@ -50,10 +50,10 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.ssl.SslBytesTest.TLSRecord.Type; import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpParser; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpConnection; @@ -189,11 +189,11 @@ public class SslBytesServerTest extends SslBytesTest ServerConnector connector = new ServerConnector(server, null, null, null, 1, 1, sslFactory, httpFactory) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException { - ChannelEndPoint endp = super.newEndPoint(channel, selectSet, key); - serverEndPoint.set(endp); - return endp; + SocketChannelEndPoint endPoint = super.newEndPoint(channel, selectSet, key); + serverEndPoint.set(endPoint); + return endPoint; } }; connector.setIdleTimeout(idleTimeout); diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java index cce0415b3a5..c2b5cb3b973 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/util/TypedContentProviderTest.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.client.util; import java.io.IOException; import java.nio.charset.StandardCharsets; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -57,7 +56,7 @@ public class TypedContentProviderTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { baseRequest.setHandled(true); assertEquals("POST", request.getMethod()); @@ -90,13 +89,13 @@ public class TypedContentProviderTest extends AbstractHttpClientServerTest Fields fields = new Fields(); fields.put(name1, value1); fields.add(name2, value2); - final String content = FormContentProvider.convert(fields); + final String content = FormRequestContent.convert(fields); final String contentType = "text/plain;charset=UTF-8"; start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); assertEquals("POST", request.getMethod()); @@ -109,7 +108,7 @@ public class TypedContentProviderTest extends AbstractHttpClientServerTest .scheme(scenario.getScheme()) .method(HttpMethod.POST) .body(new FormRequestContent(fields)) - .header(HttpHeader.CONTENT_TYPE, contentType) + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, contentType)) .send(); assertEquals(200, response.getStatus()); @@ -124,7 +123,7 @@ public class TypedContentProviderTest extends AbstractHttpClientServerTest start(scenario, new AbstractHandler() { @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { baseRequest.setHandled(true); assertEquals("GET", request.getMethod()); diff --git a/jetty-client/src/test/resources/jetty-logging.properties b/jetty-client/src/test/resources/jetty-logging.properties index e6bbf9a6ca0..2bbfa1a3add 100644 --- a/jetty-client/src/test/resources/jetty-logging.properties +++ b/jetty-client/src/test/resources/jetty-logging.properties @@ -1,6 +1,6 @@ # Jetty Logging using jetty-slf4j-impl #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG -#org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG +#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG diff --git a/jetty-documentation/pom.xml b/jetty-documentation/pom.xml index acf0277731c..21938da5bda 100644 --- a/jetty-documentation/pom.xml +++ b/jetty-documentation/pom.xml @@ -33,6 +33,26 @@ org.eclipse.jetty.toolchain jetty-servlet-api
+ + org.eclipse.jetty + jetty-alpn-server + ${project.version} + + + org.eclipse.jetty + jetty-client + ${project.version} + + + org.eclipse.jetty + jetty-jmx + ${project.version} + + + org.eclipse.jetty + jetty-rewrite + ${project.version} + org.eclipse.jetty jetty-server @@ -48,36 +68,21 @@ jetty-servlets ${project.version} - - org.eclipse.jetty - jetty-rewrite - ${project.version} - org.eclipse.jetty jetty-webapp ${project.version} - - org.eclipse.jetty - jetty-alpn-server - ${project.version} - - - org.eclipse.jetty.http2 - http2-server - ${project.version} - - - org.eclipse.jetty - jetty-client - ${project.version} - org.eclipse.jetty.fcgi fcgi-client ${project.version} + + org.eclipse.jetty.http2 + http2-server + ${project.version} + org.eclipse.jetty.http2 http2-http-client-transport diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/arch-bean.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/arch-bean.adoc index 0d48fc0e776..7aaf13699e6 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/arch-bean.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/arch-bean.adoc @@ -31,8 +31,9 @@ The end result is that an application based on the Jetty libraries is a _tree_ o In server application the root of the component tree is a `Server` instance, while in client applications the root of the component tree is an `HttpClient` instance. Having all the Jetty components in a tree is beneficial in a number of use cases. -It makes possible to register the components in the tree as JMX MBeans (TODO: xref the JMX section) so that a JMX console can look at the internal state of the components. -It also makes possible to dump the component tree (and therefore each component's internal state) to a log file or to the console for troubleshooting purposes (TODO: xref troubleshooting section). +It makes possible to register the components in the tree as xref:eg-arch-jmx[JMX MBeans] so that a JMX console can look at the internal state of the components. +It also makes possible to xref:eg-troubleshooting-component-dump[dump the component tree] (and therefore each component's internal state) to a log file or to the console for xref:eg-troubleshooting[troubleshooting purposes]. +// TODO: add a section on Dumpable? [[eg-arch-bean-lifecycle]] ==== Jetty Component Lifecycle @@ -40,7 +41,7 @@ It also makes possible to dump the component tree (and therefore each component' Jetty components typically have a life cycle: they can be started and stopped. The Jetty components that have a life cycle implement the `org.eclipse.jetty.util.component.LifeCycle` interface. -Jetty components that contain other components extend the `org.eclipse.jetty.util.component.ContainerLifeCycle` class. +Jetty components that contain other components implement the `org.eclipse.jetty.util.component.Container` interface and typically extend the `org.eclipse.jetty.util.component.ContainerLifeCycle` class. `ContainerLifeCycle` can contain these type of components, also called __bean__s: * _managed_ beans, `LifeCycle` instances whose life cycle is tied to the life cycle of their container @@ -94,8 +95,72 @@ include::{doc_code}/embedded/ComponentDocs.java[tags=restart] `Service` can be stopped independently of `Root`, and re-started. Starting and stopping a non-root component does not alter the structure of the component tree, just the state of the subtree starting from the component that has been stopped and re-started. +`Container` provides an API to find beans in the component tree: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/ComponentDocs.java[tags=getBeans] +---- + +You can add your own beans to the component tree at application startup time, and later find them from your application code to access their services. + +[TIP] +==== +The component tree should be used for long-lived or medium-lived components such as thread pools, web application contexts, etc. + +It is not recommended adding to, and removing from, the component tree short-lived objects such as HTTP requests or TCP connections, for performance reasons. + +If you need component tree features such as automatic xref:eg-arch-jmx[export to JMX] or xref:eg-troubleshooting-component-dump[dump capabilities] for short-lived objects, consider having a long-lived container in the component tree instead. +You can make the long-lived container efficient at adding/removing the short-lived components using a data structure that is not part of the component tree, and make the long-lived container handle the JMX and dump features for the short-lived components. +==== + [[eg-arch-bean-listener]] ==== Jetty Component Listeners -// TODO: LifeCycle.Listener -// TODO: Container.Listener + InheritedListener +A component that extends `AbstractLifeCycle` inherits the possibility to add/remove event _listeners_ for various events emitted by components. + +A component that implements `java.util.EventListener` that is added to a `ContainerLifeCycle` is also registered as an event listener. + +The following sections describe in details the various listeners available in the Jetty component architecture. + +[[eg-arch-bean-listener-lifecycle]] +===== LifeCycle.Listener + +A `LifeCycle.Listener` emits events for life cycle events such as starting, stopping and failures: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/ComponentDocs.java[tags=lifecycleListener] +---- + +For example, a life cycle listener attached to a `Server` instance could be used to create (for the _started_ event) and delete (for the _stopped_ event) a file containing the process ID of the JVM that runs the `Server`. + +[[eg-arch-bean-listener-container]] +===== Container.Listener + +A component that implements `Container` is a container for other components and `ContainerLifeCycle` is the typical implementation. + +A `Container` emits events when a component (also called _bean_) is added to or removed from the container: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/ComponentDocs.java[tags=containerListener] +---- + +A `Container.Listener` added as a bean will also be registered as a listener: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/ComponentDocs.java[tags=containerSiblings] +---- + +[[eg-arch-bean-listener-inherited]] +===== Container.InheritedListener + +A `Container.InheritedListener` is a listener that will be added to all descendants that are also ``Container``s. + +Listeners of this type may be added to the component tree root only, but will be notified of every descendant component that is added to or removed from the component tree (not only first level children). + +The primary use of `Container.InheritedListener` within the Jetty Libraries is `MBeanContainer` from the xref:eg-arch-jmx[Jetty JMX support]. + +`MBeanContainer` listens for every component added to the tree, converts it to an MBean and registers it to the MBeanServer; for every component removed from the tree, it unregisters the corresponding MBean from the MBeanServer. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/arch-jmx.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/arch-jmx.adoc new file mode 100644 index 00000000000..d99a4c82e7c --- /dev/null +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/arch-jmx.adoc @@ -0,0 +1,306 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +[[eg-arch-jmx]] +== Jetty JMX Support + +The Java Management Extensions (JMX) APIs are standard API for managing and monitoring resources such as applications, devices, services, and the Java Virtual Machine itself. + +The JMX API includes remote access, so a remote management console such as link:https://openjdk.java.net/projects/jmc/[Java Mission Control] can interact with a running application for these purposes. + +Jetty architecture is based on xref:eg-arch-bean[components] organized in a tree. Every time a component is added to or removed from the component tree, an event is emitted, and xref:eg-arch-bean-listener-container[Container.Listener] implementations can listen to those events and perform additional actions. + +`org.eclipse.jetty.jmx.MBeanContainer` listens to those events and registers/unregisters the Jetty components as MBeans into the platform MBeanServer. + +The Jetty components are annotated with xref:eg-arch-jmx-annotation[Jetty JMX annotations] so that they can provide specific JMX metadata such as attributes and operations that should be exposed via JMX. + +Therefore, when a component is added to the component tree, `MBeanContainer` is notified, it creates the MBean from the component POJO and registers it to the `MBeanServer`. +Similarly, when a component is removed from the tree, `MBeanContainer` is notified, and unregisters the MBean from the `MBeanServer`. + +The Maven coordinates for the Jetty JMX support are: + +[source,xml,subs=normal] +---- + + org.eclipse.jetty + jetty-jmx + {version} + +---- + +=== Enabling JMX Support + +Enabling JMX support is always recommended because it provides valuable information about the system, both for monitoring purposes and for troubleshooting purposes in case of problems. + +To enable JMX support on the server: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=server] +---- + +Similarly on the client: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=client] +---- + +[NOTE] +==== +The MBeans exported to the platform MBeanServer can only be accessed locally (from the same machine), not from remote machines. + +This means that this configuration is enough for development, where you have easy access (with graphical user interface) to the machine where Jetty runs, but it is typically not enough when the machine where Jetty runs is remote, or only accessible via SSH or otherwise without graphical user interface support. +In these cases, you have to enable xref:eg-arch-jmx-remote[JMX Remote Access]. +==== + +// TODO: add a section about how to expose logging once #4830 is fixed. + +[[eg-arch-jmx-remote]] +=== Enabling JMX Remote Access + +There are two ways of enabling remote connectivity so that JMC can connect to the remote JVM to visualize MBeans. + +* Use the `com.sun.management.jmxremote` system property on the command line. +Unfortunately, this solution does not work well with firewalls and is not flexible. +* Use Jetty's `ConnectorServer` class. + +`org.eclipse.jetty.jmx.ConnectorServer` will use by default RMI to allow connection from remote clients, and it is a wrapper around the standard JDK class `JMXConnectorServer`, which is the class that provides remote access to JMX clients. + +Connecting to the remote JVM is a two step process: + +* First, the client will connect to the RMI _registry_ to download the RMI stub for the `JMXConnectorServer`; this RMI stub contains the IP address and port to connect to the RMI server, i.e. the remote `JMXConnectorServer`. +* Second, the client uses the RMI stub to connect to the RMI _server_ (i.e. the remote `JMXConnectorServer`) typically on an address and port that may be different from the RMI registry address and port. + +The host and port configuration for the RMI registry and the RMI server is specified by a `JMXServiceURL`. +The string format of an RMI `JMXServiceURL` is: + +[source,screen] +---- +service:jmx:rmi://:/jndi/rmi://:/jmxrmi +---- + +Default values are: + +[source,screen] +---- +rmi_server_host = localhost +rmi_server_port = 1099 +rmi_registry_host = localhost +rmi_registry_port = 1099 +---- + +With the default configuration, only clients that are local to the server machine can connect to the RMI registry and RMI server - this is done for security reasons. +With this configuration it would still be possible to access the MBeans from remote using a xref:eg-arch-jmx-remote-ssh-tunnel[SSH tunnel]. + +By specifying an appropriate `JMXServiceURL`, you can fine tune the network interfaces the RMI registry and the RMI server bind to, and the ports that the RMI registry and the RMI server listen to. +The RMI server and RMI registry hosts and ports can be the same (as in the default configuration) because RMI is able to multiplex traffic arriving to a port to multiple RMI objects. + +If you need to allow JMX remote access through a firewall, you must open both the RMI registry and the RMI server ports. + +`JMXServiceURL` common examples: + +[source,screen] +---- +service:jmx:rmi:///jndi/rmi:///jmxrmi + rmi_server_host = local host address + rmi_server_port = randomly chosen + rmi_registry_host = local host address + rmi_registry_port = 1099 + +service:jmx:rmi://0.0.0.0:1099/jndi/rmi://0.0.0.0:1099/jmxrmi + rmi_server_host = any address + rmi_server_port = 1099 + rmi_registry_host = any address + rmi_registry_port = 1099 + +service:jmx:rmi://localhost:1100/jndi/rmi://localhost:1099/jmxrmi + rmi_server_host = loopback address + rmi_server_port = 1100 + rmi_registry_host = loopback address + rmi_registry_port = 1099 +---- + +[NOTE] +==== +When `ConnectorServer` is started, its RMI stub is exported to the RMI registry. +The RMI stub contains the IP address and port to connect to the RMI object, but the IP address is typically the machine host name, not the host specified in the `JMXServiceURL`. + +To control the IP address stored in the RMI stub you need to set the system property `java.rmi.server.hostname` with the desired value. +This is especially important when binding the RMI server host to the loopback address for security reasons. See also xref:eg-arch-jmx-remote-ssh-tunnel[JMX Remote Access via SSH Tunnel.] +==== + +To allow JMX remote access, create and configure a `ConnectorServer`: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=remote] +---- + +[[eg-arch-jmx-remote-authorization]] +==== JMX Remote Access Authorization + +The standard `JMXConnectorServer` provides several options to authorize access, for example via JAAS or via configuration files. +For a complete guide to controlling authentication and authorization in JMX, see https://docs.oracle.com/en/java/javase/11/management/[the official JMX documentation]. + +In the sections below we detail one way to setup JMX authentication and authorization, using configuration files for users, passwords and roles: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=remoteAuthorization] +---- + +The `users.access` file format is defined in the `$JAVA_HOME/conf/management/jmxremote.access` file. +A simplified version is the following: + +.users.access +[source,screen] +---- +user1 readonly +user2 readwrite +---- + +The `users.password` file format is defined in the `$JAVA_HOME/conf/management/jmxremote.password.template` file. +A simplified version is the following: + +.users.password +[source,screen] +---- +user1 password1 +user2 password2 +---- + +CAUTION: The `users.access` and `users.password` files are not standard `*.properties` files -- the user must be separated from the role or password by a space character. + +===== Securing JMX Remote Access with TLS + +The JMX communication via RMI happens by default in clear-text. + +It is possible to configure the `ConnectorServer` with a `SslContextFactory` so that the JMX communication via RMI is encrypted: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=tlsRemote] +---- + +It is possible to use the same `SslContextFactory.Server` used to configure the Jetty `ServerConnector` that supports TLS also for the JMX communication via RMI. + +The keystore must contain a valid certificate signed by a Certification Authority. + +The RMI mechanic is the usual one: the RMI client (typically a monitoring console) will connect first to the RMI registry (using TLS), download the RMI server stub that contains the address and port of the RMI server to connect to, then connect to the RMI server (using TLS). + +This also mean that if the RMI registry and the RMI server are on different hosts, the RMI client must have available the cryptographic material to validate both hosts. + +Having certificates signed by a Certification Authority simplifies by a lot the configuration needed to get the JMX communication over TLS working properly. + +If that is not the case (for example the certificate is self-signed), then you need to specify the required system properties that allow RMI (especially when acting as an RMI client) to retrieve the cryptographic material necessary to establish the TLS connection. + +For example, trying to connect using the JDK standard `JMXConnector` with both the RMI server and the RMI registry via TLS to `domain.com` with a self-signed certificate: + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=tlsJMXConnector] +---- + +Similarly, to launch JMC: + +[source,screen] +---- +$ jmc -vmargs -Djavax.net.ssl.trustStore=/path/to/trustStore -Djavax.net.ssl.trustStorePassword=secret +---- + +IMPORTANT: These system properties are required when launching the `ConnectorServer` too, on the server, because it acts as an RMI client with respect to the RMI registry. + +[[eg-arch-jmx-remote-ssh-tunnel]] +===== JMX Remote Access with Port Forwarding via SSH Tunnel + +You can access JMX MBeans on a remote machine when the RMI ports are not open, for example because of firewall policies, but you have SSH access to the machine using local port forwarding via an SSH tunnel. + +In this case you want to configure the `ConnectorServer` with a `JMXServiceURL` that binds the RMI server and the RMI registry to the loopback interface only: `service:jmx:rmi://localhost:1099/jndi/rmi://localhost:1099/jmxrmi`. + +Then you setup the local port forwarding with the SSH tunnel: + +[source,screen] +---- +$ ssh -L 1099:localhost:1099 @ +---- + +Now you can use JConsole or JMC to connect to `localhost:1099` on your local computer. +The traffic will be forwarded to `machine_host` and when there, SSH will forward the traffic to `localhost:1099`, which is exactly where the `ConnectorServer` listens. + +When you configure `ConnectorServer` in this way, you must set the system property `-Djava.rmi.server.hostname=localhost`, on the server. +This is required because when the RMI server is exported, its address and port are stored in the RMI stub. You want the address in the RMI stub to be `localhost` so that when the RMI stub is downloaded to the remote client, the RMI communication will go through the SSH tunnel. + +[[eg-arch-jmx-annotation]] +=== Jetty JMX Annotations + +The Jetty JMX support, and in particular `MBeanContainer`, is notified every time a bean is added to the component tree. + +The bean is scanned for Jetty JMX annotations to obtain JMX metadata: the JMX attributes and JMX operations. + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=jmxAnnotation] +---- + +The JMX metadata and the bean are wrapped by an instance of `org.eclipse.jetty.jmx.ObjectMBean` that exposes the JMX metadata and, upon request from JMX consoles, invokes methods on the bean to get/set attribute values and perform operations. + +You can provide a custom subclass of `ObjectMBean` to further customize how the bean is exposed to JMX. + +The custom `ObjectMBean` subclass must respect the following naming convention: `.jmx.MBean`. +For example, class `com.acme.Foo` may have a custom `ObjectMBean` subclass named `com.acme.**jmx**.Foo**MBean**`. + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=jmxCustomMBean] +---- + +The custom `ObjectMBean` subclass is also scanned for Jetty JMX annotations and overrides the JMX metadata obtained by scanning the bean class. +This allows to annotate only the custom `ObjectMBean` subclass and keep the bean class free of the Jetty JMX annotations. + +[source,java,indent=0] +---- +include::{doc_code}/embedded/JMXDocs.java[tags=jmxCustomMBeanOverride] +---- + +The scan for Jetty JMX annotations is performed on the bean class and all the interfaces implemented by the bean class, then on the super-class and all the interfaces implemented by the super-class and so on until `java.lang.Object` is reached. +For each type -- class or interface, the corresponding `+*.jmx.*MBean+` is looked up and scanned as well with the same algorithm. +For each type, the scan looks for the class-level annotation `@ManagedObject`. +If it is found, the scan looks for method-level `@ManagedAttribute` and `@ManagedOperation` annotations; otherwise it skips the current type and moves to the next type to scan. + +==== @ManagedObject + +The `@ManagedObject` annotation is used on a class at the top level to indicate that it should be exposed as an MBean. +It has only one attribute to it which is used as the description of the MBean. + +==== @ManagedAttribute + +The `@ManagedAttribute` annotation is used to indicate that a given method is exposed as a JMX attribute. +This annotation is placed always on the getter method of a given attribute. +Unless the `readonly` attribute is set to `true` in the annotation, a corresponding setter is looked up following normal naming conventions. +For example if this annotation is on a method called `String getFoo()` then a method called `void setFoo(String)` would be looked up, and if found wired as the setter for the JMX attribute. + +==== @ManagedOperation + +The `@ManagedOperation` annotation is used to indicate that a given method is exposed as a JMX operation. +A JMX operation has an _impact_ that can be `INFO` if the operation returns a value without modifying the object, `ACTION` if the operation does not return a value but modifies the object, and "ACTION_INFO" if the operation both returns a value and modifies the object. +If the _impact_ is not specified, it has the default value of `UNKNOWN`. + +==== @Name + +The `@Name` annotation is used to assign a name and description to parameters in method signatures so that when rendered by JMX consoles it is clearer what the parameter meaning is. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/arch.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/arch.adoc index 6ca0cbde4d5..11548f54359 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/arch.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/arch.adoc @@ -21,5 +21,6 @@ == Jetty Architecture include::arch-bean.adoc[] +include::arch-jmx.adoc[] include::arch-listener.adoc[] include::arch-io.adoc[] diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/server/http/server-http-handler-use.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/server/http/server-http-handler-use.adoc index 6354fbc0085..7c01051cb81 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/server/http/server-http-handler-use.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/server/http/server-http-handler-use.adoc @@ -245,8 +245,7 @@ Server * Number of responses grouped by HTTP code (i.e. how many `2xx` responses, how many `3xx` responses, etc.) * Total response content bytes -Server applications can read these values and use them internally, or expose them via some service, or export them via JMX. -// TODO: xref to the JMX section. +Server applications can read these values and use them internally, or expose them via some service, or xref:eg-arch-jmx[export them to JMX]. `StatisticsHandler` can be configured at the server level or at the context level. diff --git a/jetty-documentation/src/main/asciidoc/embedded-guide/troubleshooting.adoc b/jetty-documentation/src/main/asciidoc/embedded-guide/troubleshooting.adoc index 0bfc95b68a7..1390f30b1fc 100644 --- a/jetty-documentation/src/main/asciidoc/embedded-guide/troubleshooting.adoc +++ b/jetty-documentation/src/main/asciidoc/embedded-guide/troubleshooting.adoc @@ -20,6 +20,13 @@ [[eg-troubleshooting]] == Troubleshooting Jetty +TODO: introduction +// TODO: explain the process to troubleshoot Jetty: +// TODO: #1 enable JMX +// TODO: #2 enable GC logs +// TODO: #3 take jvm/component dumps +// TODO: #4 enable debug logging if you can + [[eg-troubleshooting-logging]] === Logging @@ -66,6 +73,24 @@ If you want to enable DEBUG logging but only for the HTTP/2 classes: java -Dorg.eclipse.jetty.http2.LEVEL=DEBUG --class-path ... ---- +[[eg-troubleshooting-thread-dump]] +=== JVM Thread Dump +TODO + +[[eg-troubleshooting-component-dump]] +=== Jetty Component Tree Dump + +Jetty components are organized in a xref:eg-arch-bean[component tree]. + +At the root of the component tree there is typically a `ContainerLifeCycle` instance -- typically a `Server` instance on the server and an `HttpClient` instance on the client. + +`ContainerLifeCycle` has built-in _dump_ APIs that can be invoked either directly or xref:eg-arch-jmx[via JMX]. + +// TODO: images from JMC? +// TODO: Command line JMX will be in JMX section. + +TIP: You can get more details from a Jetty's `QueuedThreadPool` dump by enabling detailed dumps via `queuedThreadPool.setDetailedDump(true)`. + [[eg-troubleshooting-debugging]] === Debugging diff --git a/jetty-documentation/src/main/java/embedded/ComponentDocs.java b/jetty-documentation/src/main/java/embedded/ComponentDocs.java index d08f3df83c2..dc7aee5b6bc 100644 --- a/jetty-documentation/src/main/java/embedded/ComponentDocs.java +++ b/jetty-documentation/src/main/java/embedded/ComponentDocs.java @@ -18,11 +18,19 @@ package embedded; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; + +import static java.lang.System.Logger.Level.INFO; @SuppressWarnings("unused") public class ComponentDocs @@ -140,4 +148,142 @@ public class ComponentDocs service.start(); // end::restart[] } + + public void getBeans() throws Exception + { + // tag::getBeans[] + class Root extends ContainerLifeCycle + { + } + + class Service extends ContainerLifeCycle + { + private ScheduledExecutorService scheduler; + + @Override + protected void doStart() throws Exception + { + scheduler = Executors.newSingleThreadScheduledExecutor(); + addBean(scheduler); + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + removeBean(scheduler); + scheduler.shutdown(); + } + } + + Root root = new Root(); + Service service = new Service(); + root.addBean(service); + + // Start the Root component. + root.start(); + + // Find all the direct children of root. + Collection children = root.getBeans(); + // children contains only service + + // Find all descendants of root that are instance of a particular class. + Collection schedulers = root.getContainedBeans(ScheduledExecutorService.class); + // schedulers contains the service scheduler. + // end::getBeans[] + } + + public void lifecycleListener() + { + // tag::lifecycleListener[] + Server server = new Server(); + + // Add an event listener of type LifeCycle.Listener. + server.addEventListener(new LifeCycle.Listener() + { + @Override + public void lifeCycleStarted(LifeCycle lifeCycle) + { + System.getLogger("server").log(INFO, "Server {0} has been started", lifeCycle); + } + + @Override + public void lifeCycleFailure(LifeCycle lifeCycle, Throwable failure) + { + System.getLogger("server").log(INFO, "Server {0} failed to start", lifeCycle, failure); + } + + @Override + public void lifeCycleStopped(LifeCycle lifeCycle) + { + System.getLogger("server").log(INFO, "Server {0} has been stopped", lifeCycle); + } + }); + // end::lifecycleListener[] + } + + public void containerListener() + { + // tag::containerListener[] + Server server = new Server(); + + // Add an event listener of type LifeCycle.Listener. + server.addEventListener(new Container.Listener() + { + @Override + public void beanAdded(Container parent, Object child) + { + System.getLogger("server").log(INFO, "Added bean {1} to {0}", parent, child); + } + + @Override + public void beanRemoved(Container parent, Object child) + { + System.getLogger("server").log(INFO, "Removed bean {1} from {0}", parent, child); + } + }); + // end::containerListener[] + } + + public void containerSiblings() + { + // tag::containerSiblings[] + class Parent extends ContainerLifeCycle + { + } + + class Child + { + } + + // The older child takes care of its siblings. + class OlderChild extends Child implements Container.Listener + { + private Set siblings = new HashSet<>(); + + @Override + public void beanAdded(Container parent, Object child) + { + siblings.add(child); + } + + @Override + public void beanRemoved(Container parent, Object child) + { + siblings.remove(child); + } + } + + Parent parent = new Parent(); + + Child older = new OlderChild(); + // The older child is a child bean _and_ a listener. + parent.addBean(older); + + Child younger = new Child(); + // Adding a younger child will notify the older child. + parent.addBean(younger); + // end::containerSiblings[] + } } diff --git a/jetty-documentation/src/main/java/embedded/JMXDocs.java b/jetty-documentation/src/main/java/embedded/JMXDocs.java new file mode 100644 index 00000000000..d59c219c095 --- /dev/null +++ b/jetty-documentation/src/main/java/embedded/JMXDocs.java @@ -0,0 +1,276 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package embedded; + +import java.lang.management.ManagementFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.jmx.ConnectorServer; +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.jmx.ObjectMBean; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.annotation.ManagedAttribute; +import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.annotation.ManagedOperation; +import org.eclipse.jetty.util.annotation.Name; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +@SuppressWarnings("unused") +public class JMXDocs +{ + public void server() + { + // tag::server[] + Server server = new Server(); + + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + + // Add MBeanContainer to the root component. + server.addBean(mbeanContainer); + // end::server[] + } + + public void client() + { + // tag::client[] + HttpClient httpClient = new HttpClient(); + + // Create an MBeanContainer with the platform MBeanServer. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + + // Add MBeanContainer to the root component. + httpClient.addBean(mbeanContainer); + // end::client[] + } + + public void remote() throws Exception + { + // tag::remote[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup ConnectorServer. + + // Bind the RMI server to the wildcard address and port 1999. + // Bind the RMI registry to the wildcard address and port 1099. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1999, "/jndi/rmi:///jmxrmi"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, "org.eclipse.jetty.jmx:name=rmiconnectorserver"); + + // Add ConnectorServer as a bean, so it is started + // with the Server and also exported as MBean. + server.addBean(jmxServer); + + server.start(); + // end::remote[] + } + + public static void main(String[] args) throws Exception + { + new JMXDocs().remote(); + } + + public void remoteAuthorization() throws Exception + { + // tag::remoteAuthorization[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup ConnectorServer. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi"); + Map env = new HashMap<>(); + env.put("com.sun.management.jmxremote.access.file", "/path/to/users.access"); + env.put("com.sun.management.jmxremote.password.file", "/path/to/users.password"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, env, "org.eclipse.jetty.jmx:name=rmiconnectorserver"); + server.addBean(jmxServer); + + server.start(); + // end::remoteAuthorization[] + } + + public void tlsRemote() throws Exception + { + // tag::tlsRemote[] + Server server = new Server(); + + // Setup Jetty JMX. + MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); + server.addBean(mbeanContainer); + + // Setup SslContextFactory. + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath("/path/to/keystore"); + sslContextFactory.setKeyStorePassword("secret"); + + // Setup ConnectorServer with SslContextFactory. + JMXServiceURL jmxURL = new JMXServiceURL("rmi", null, 1099, "/jndi/rmi:///jmxrmi"); + ConnectorServer jmxServer = new ConnectorServer(jmxURL, null, "org.eclipse.jetty.jmx:name=rmiconnectorserver", sslContextFactory); + server.addBean(jmxServer); + + server.start(); + // end::tlsRemote[] + } + + public void tlsJMXConnector() throws Exception + { + // tag::tlsJMXConnector[] + // System properties necessary for an RMI client to trust a self-signed certificate. + System.setProperty("javax.net.ssl.trustStore", "/path/to/trustStore"); + System.setProperty("javax.net.ssl.trustStorePassword", "secret"); + + JMXServiceURL jmxURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://domain.com:1100/jmxrmi"); + + Map clientEnv = new HashMap<>(); + // Required to connect to the RMI registry via TLS. + clientEnv.put(ConnectorServer.RMI_REGISTRY_CLIENT_SOCKET_FACTORY_ATTRIBUTE, new SslRMIClientSocketFactory()); + + try (JMXConnector client = JMXConnectorFactory.connect(jmxURL, clientEnv)) + { + Set names = client.getMBeanServerConnection().queryNames(null, null); + } + // end::tlsJMXConnector[] + } + + public void jmxAnnotation() throws Exception + { + // tag::jmxAnnotation[] + // Annotate the class with @ManagedObject and provide a description. + @ManagedObject("Services that provide useful features") + class Services + { + private final Map services = new ConcurrentHashMap<>(); + private boolean enabled = true; + + // A read-only attribute with description. + @ManagedAttribute(value = "The number of services", readonly = true) + public int getServiceCount() + { + return services.size(); + } + + // A read-write attribute with description. + // Only the getter is annotated. + @ManagedAttribute(value = "Whether the services are enabled") + public boolean isEnabled() + { + return enabled; + } + + // There is no need to annotate the setter. + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + // An operation with description and impact. + // The @Name annotation is used to annotate parameters + // for example to display meaningful parameter names. + @ManagedOperation(value = "Retrieves the service with the given name", impact = "INFO") + public Object getService(@Name(value = "serviceName") String n) + { + return services.get(n); + } + } + // end::jmxAnnotation[] + } + + public void jmxCustomMBean() + { + // tag::jmxCustomMBean[] + //package com.acme; + @ManagedObject + class Service + { + } + + //package com.acme.jmx; + class ServiceMBean extends ObjectMBean + { + ServiceMBean(Object service) + { + super(service); + } + } + // end::jmxCustomMBean[] + } + + public void jmxCustomMBeanOverride() + { + // tag::jmxCustomMBeanOverride[] + //package com.acme; + // No Jetty JMX annotations. + class CountService + { + private int count; + + public int getCount() + { + return count; + } + + public void addCount(int value) + { + count += value; + } + } + + //package com.acme.jmx; + @ManagedObject("the count service") + class CountServiceMBean extends ObjectMBean + { + public CountServiceMBean(Object service) + { + super(service); + } + + private CountService getCountService() + { + return (CountService)super.getManagedObject(); + } + + @ManagedAttribute("the current service count") + public int getCount() + { + return getCountService().getCount(); + } + + @ManagedOperation(value = "adds the given value to the service count", impact = "ACTION") + public void addCount(@Name("count delta") int value) + { + getCountService().addCount(value); + } + } + // end::jmxCustomMBeanOverride[] + } +} diff --git a/jetty-documentation/src/main/java/embedded/SelectorManagerDocs.java b/jetty-documentation/src/main/java/embedded/SelectorManagerDocs.java index 243f6b66ecb..334fb033ca6 100644 --- a/jetty-documentation/src/main/java/embedded/SelectorManagerDocs.java +++ b/jetty-documentation/src/main/java/embedded/SelectorManagerDocs.java @@ -34,6 +34,7 @@ import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +@SuppressWarnings("unused") public class SelectorManagerDocs { // tag::connect[] diff --git a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java index 60161cf3c21..3e788294e90 100644 --- a/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java +++ b/jetty-documentation/src/main/java/embedded/client/http/HTTPClientDocs.java @@ -739,7 +739,7 @@ public class HTTPClientDocs // end::dynamicDefault[] } - public void dynamicOneProtocol() throws Exception + public void dynamicOneProtocol() { // tag::dynamicOneProtocol[] ClientConnector connector = new ClientConnector(); @@ -798,9 +798,10 @@ public class HTTPClientDocs // Make a clear-text upgrade request from HTTP/1.1 to HTTP/2. // The request will start as HTTP/1.1, but the response will be HTTP/2. ContentResponse upgradedResponse = client.newRequest("host", 8080) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .send(); // end::dynamicClearText[] } diff --git a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java index c4720e32318..87b0885687a 100644 --- a/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java +++ b/jetty-fcgi/fcgi-client/src/main/java/org/eclipse/jetty/fcgi/client/http/HttpConnectionOverFCGI.java @@ -34,6 +34,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpConnection; import org.eclipse.jetty.client.HttpDestination; import org.eclipse.jetty.client.HttpExchange; +import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.IConnection; import org.eclipse.jetty.client.SendFailure; import org.eclipse.jetty.client.api.Connection; @@ -357,7 +358,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne @Override public SendFailure send(HttpExchange exchange) { - Request request = exchange.getRequest(); + HttpRequest request = exchange.getRequest(); normalizeRequest(request); // FCGI may be multiplexed, so one channel for each exchange. diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java index c04580d4f8b..2359ba5bc06 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java @@ -182,12 +182,14 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent // If the Host header is missing, add it. if (!proxyRequest.getHeaders().contains(HttpHeader.HOST)) { - String host = request.getServerName(); + String server = request.getServerName(); int port = request.getServerPort(); if (!getHttpClient().isDefaultPort(request.getScheme(), port)) - host += ":" + port; - proxyRequest.header(HttpHeader.HOST, host); - proxyRequest.header(HttpHeader.X_FORWARDED_HOST, host); + server += ":" + port; + String host = server; + proxyRequest.headers(headers -> headers + .put(HttpHeader.HOST, host) + .put(HttpHeader.X_FORWARDED_HOST, host)); } // PHP does not like multiple Cookie headers, coalesce into one. @@ -202,8 +204,7 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent String cookie = cookies.get(i); builder.append(cookie); } - proxyRequest.header(HttpHeader.COOKIE, null); - proxyRequest.header(HttpHeader.COOKIE, builder.toString()); + proxyRequest.headers(headers -> headers.put(HttpHeader.COOKIE, builder.toString())); } super.sendProxyRequest(request, proxyResponse, proxyRequest); diff --git a/jetty-gcloud/jetty-gcloud-session-manager/pom.xml b/jetty-gcloud/jetty-gcloud-session-manager/pom.xml index 5044d383aed..088b8001430 100644 --- a/jetty-gcloud/jetty-gcloud-session-manager/pom.xml +++ b/jetty-gcloud/jetty-gcloud-session-manager/pom.xml @@ -69,7 +69,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} test diff --git a/jetty-hazelcast/pom.xml b/jetty-hazelcast/pom.xml index 1bb1d405806..1385a3a7d61 100644 --- a/jetty-hazelcast/pom.xml +++ b/jetty-hazelcast/pom.xml @@ -59,18 +59,6 @@ ${project.version} test - - org.eclipse.jetty.websocket - websocket-servlet - ${project.version} - test - - - org.eclipse.jetty.websocket - websocket-jetty-server - ${project.version} - test - org.eclipse.jetty.toolchain jetty-test-helper diff --git a/jetty-hazelcast/src/test/java/org/eclipse/jetty/hazelcast/session/TestHazelcastSessions.java b/jetty-hazelcast/src/test/java/org/eclipse/jetty/hazelcast/session/TestHazelcastSessions.java index 88fe6a48db9..aae916d81c4 100644 --- a/jetty-hazelcast/src/test/java/org/eclipse/jetty/hazelcast/session/TestHazelcastSessions.java +++ b/jetty-hazelcast/src/test/java/org/eclipse/jetty/hazelcast/session/TestHazelcastSessions.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.hazelcast.session; import java.io.IOException; import java.io.PrintWriter; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,6 +28,7 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -42,17 +42,13 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; public class TestHazelcastSessions { - public static class TestServlet - extends HttpServlet + public static class TestServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String arg = req.getParameter("action"); if (arg == null) @@ -154,16 +150,17 @@ public class TestHazelcastSessions client.GET("http://localhost:" + port + contextPath + "?action=set&value=" + value); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); String sessionCookie = response.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); String resp = response.getContentAsString(); assertEquals(resp.trim(), String.valueOf(value)); // Be sure the session value is still there + HttpField cookie = new HttpField("Cookie", sessionCookie); Request request = client.newRequest("http://localhost:" + port + contextPath + "?action=get"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -172,13 +169,13 @@ public class TestHazelcastSessions //Delete the session request = client.newRequest("http://localhost:" + port + contextPath + "?action=del"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); //Check that the session is gone request = client.newRequest("http://localhost:" + port + contextPath + "?action=get"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); resp = response.getContentAsString(); diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml index f7bdb194a0a..eb6655b6962 100644 --- a/jetty-home/pom.xml +++ b/jetty-home/pom.xml @@ -663,7 +663,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/AbstractTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/AbstractTest.java index cf8d3ddc135..f9b2f3d987d 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/AbstractTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/AbstractTest.java @@ -20,6 +20,8 @@ package org.eclipse.jetty.http2.client; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + import javax.servlet.http.HttpServlet; import org.eclipse.jetty.http.HostPortHttpField; @@ -30,6 +32,7 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http2.FlowControlStrategy; import org.eclipse.jetty.http2.api.Session; import org.eclipse.jetty.http2.api.server.ServerSessionListener; +import org.eclipse.jetty.http2.server.AbstractHTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.http2.server.RawHTTP2ServerConnectionFactory; import org.eclipse.jetty.server.ConnectionFactory; @@ -69,10 +72,16 @@ public class AbstractTest } protected void start(ServerSessionListener listener) throws Exception + { + start(listener, x -> {}); + } + + protected void start(ServerSessionListener listener, Consumer configurator) throws Exception { RawHTTP2ServerConnectionFactory connectionFactory = new RawHTTP2ServerConnectionFactory(new HttpConfiguration(), listener); connectionFactory.setInitialSessionRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); connectionFactory.setInitialStreamRecvWindow(FlowControlStrategy.DEFAULT_WINDOW_SIZE); + configurator.accept(connectionFactory); prepareServer(connectionFactory); server.start(); diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConcurrentStreamCreationTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConcurrentStreamCreationTest.java new file mode 100644 index 00000000000..2b82eda8b27 --- /dev/null +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/ConcurrentStreamCreationTest.java @@ -0,0 +1,107 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.http2.client; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.api.server.ServerSessionListener; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.Promise; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConcurrentStreamCreationTest extends AbstractTest +{ + @Test + public void testConcurrentStreamCreation() throws Exception + { + int threads = 64; + int runs = 1; + int iterations = 1024; + int total = threads * runs * iterations; + CountDownLatch serverLatch = new CountDownLatch(total); + start(new ServerSessionListener.Adapter() + { + @Override + public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) + { + MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY); + HeadersFrame responseFrame = new HeadersFrame(stream.getId(), response, null, true); + stream.headers(responseFrame, Callback.NOOP); + serverLatch.countDown(); + return null; + } + }, h2 -> h2.setMaxConcurrentStreams(total)); + + Session session = newClient(new Session.Listener.Adapter()); + + CyclicBarrier barrier = new CyclicBarrier(threads); + CountDownLatch clientLatch = new CountDownLatch(total); + CountDownLatch responseLatch = new CountDownLatch(runs); + Promise promise = new Promise.Adapter() + { + @Override + public void succeeded(Stream stream) + { + clientLatch.countDown(); + } + }; + IntStream.range(0, threads).forEach(i -> new Thread(() -> + { + try + { + barrier.await(); + IntStream.range(0, runs).forEach(j -> + IntStream.range(0, iterations).forEach(k -> + { + MetaData.Request request = newRequest("GET", HttpFields.EMPTY); + HeadersFrame requestFrame = new HeadersFrame(request, null, true); + session.newStream(requestFrame, promise, new Stream.Listener.Adapter() + { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) + { + int status = ((MetaData.Response)frame.getMetaData()).getStatus(); + if (status == HttpStatus.OK_200 && frame.isEndStream()) + responseLatch.countDown(); + } + }); + })); + } + catch (Throwable x) + { + x.printStackTrace(); + } + }).start()); + assertTrue(clientLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing streams on client: %d/%d", clientLatch.getCount(), total)); + assertTrue(serverLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing streams on server: %d/%d", serverLatch.getCount(), total)); + assertTrue(responseLatch.await(total, TimeUnit.MILLISECONDS), String.format("Missing response on client: %d/%d", clientLatch.getCount(), total)); + } +} diff --git a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/StreamCloseTest.java b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/StreamCloseTest.java index 1b0423db1f3..af90b1f7bd6 100644 --- a/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/StreamCloseTest.java +++ b/jetty-http2/http2-client/src/test/java/org/eclipse/jetty/http2/client/StreamCloseTest.java @@ -196,7 +196,7 @@ public class StreamCloseTest extends AbstractTest @Override public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) { - PushPromiseFrame pushFrame = new PushPromiseFrame(stream.getId(), 0, newRequest("GET", HttpFields.EMPTY)); + PushPromiseFrame pushFrame = new PushPromiseFrame(stream.getId(), newRequest("GET", HttpFields.EMPTY)); stream.push(pushFrame, new Promise.Adapter() { @Override diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java index 6166aba962d..526244a97f0 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Session.java @@ -22,10 +22,12 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.ClosedChannelException; import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -78,6 +80,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio private static final Logger LOG = LoggerFactory.getLogger(HTTP2Session.class); private final ConcurrentMap streams = new ConcurrentHashMap<>(); + private final StreamCreator streamCreator = new StreamCreator(); private final AtomicBiInteger streamCount = new AtomicBiInteger(); // Hi = closed, Lo = stream count private final AtomicInteger localStreamIds = new AtomicInteger(); private final AtomicInteger lastRemoteStreamId = new AtomicInteger(); @@ -532,6 +535,8 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio @Override public void newStream(HeadersFrame frame, Promise promise, Stream.Listener listener) { + streamCreator.newStream(frame, promise, listener); +/* try { // Synchronization is necessary to atomically create @@ -555,6 +560,7 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio { promise.failed(x); } +*/ } /** @@ -569,18 +575,14 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio */ public IStream newLocalStream(HeadersFrame frameIn, HeadersFrame[] frameOut) { - HeadersFrame frame = frameIn; int streamId = frameIn.getStreamId(); if (streamId <= 0) - { streamId = localStreamIds.getAndAdd(2); - PriorityFrame priority = frameIn.getPriority(); - priority = priority == null ? null : new PriorityFrame(streamId, priority.getParentStreamId(), - priority.getWeight(), priority.isExclusive()); - frame = new HeadersFrame(streamId, frameIn.getMetaData(), priority, frameIn.isEndStream()); - } + + HeadersFrame frame = streamCreator.prepareHeadersFrame(streamId, frameIn); if (frameOut != null) frameOut[0] = frame; + return createLocalStream(streamId, (MetaData.Request)frame.getMetaData()); } @@ -592,45 +594,13 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio @Override public int priority(PriorityFrame frame, Callback callback) { - int streamId = frame.getStreamId(); - IStream stream = streams.get(streamId); - if (stream == null) - { - streamId = localStreamIds.getAndAdd(2); - frame = new PriorityFrame(streamId, frame.getParentStreamId(), - frame.getWeight(), frame.isExclusive()); - } - control(stream, callback, frame); - return streamId; + return streamCreator.priority(frame, callback); } @Override public void push(IStream stream, Promise promise, PushPromiseFrame frame, Stream.Listener listener) { - try - { - // Synchronization is necessary to atomically create - // the stream id and enqueue the frame to be sent. - boolean queued; - synchronized (this) - { - int streamId = localStreamIds.getAndAdd(2); - frame = new PushPromiseFrame(frame.getStreamId(), streamId, frame.getMetaData()); - - IStream pushStream = createLocalStream(streamId, frame.getMetaData()); - pushStream.setListener(listener); - - ControlEntry entry = new ControlEntry(frame, pushStream, new StreamPromiseCallback(promise, pushStream)); - queued = flusher.append(entry); - } - // Iterate outside the synchronized block. - if (queued) - flusher.iterate(); - } - catch (Throwable x) - { - promise.failed(x); - } + streamCreator.push(frame, promise, listener); } @Override @@ -1731,4 +1701,167 @@ public abstract class HTTP2Session extends ContainerLifeCycle implements ISessio terminate(failure); } } + + /** + * SPEC: It is required that stream ids are monotonically increasing. + * Here we use a queue to atomically create the stream id and + * claim the slot in the queue. Concurrent threads will only + * flush up to the slot with a non-null entry to make sure + * frames are sent strictly in their stream id order. + * See https://tools.ietf.org/html/rfc7540#section-5.1.1. + */ + private class StreamCreator + { + private final Queue slots = new ArrayDeque<>(); + private Thread flushing; + + private int priority(PriorityFrame frame, Callback callback) + { + Slot slot = new Slot(); + int currentStreamId = frame.getStreamId(); + int streamId = reserveSlot(slot, currentStreamId); + + if (currentStreamId <= 0) + frame = new PriorityFrame(streamId, frame.getParentStreamId(), frame.getWeight(), frame.isExclusive()); + + assignSlotAndFlush(slot, new ControlEntry(frame, null, callback)); + return streamId; + } + + private void newStream(HeadersFrame frame, Promise promise, Stream.Listener listener) + { + Slot slot = new Slot(); + int currentStreamId = frame.getStreamId(); + int streamId = reserveSlot(slot, currentStreamId); + + frame = prepareHeadersFrame(streamId, frame); + + try + { + IStream stream = HTTP2Session.this.createLocalStream(streamId, (MetaData.Request)frame.getMetaData()); + stream.setListener(listener); + stream.process(new PrefaceFrame(), Callback.NOOP); + assignSlotAndFlush(slot, new ControlEntry(frame, stream, new StreamPromiseCallback(promise, stream))); + } + catch (Throwable x) + { + releaseSlotFlushAndFail(slot, promise, x); + } + } + + private HeadersFrame prepareHeadersFrame(int streamId, HeadersFrame frame) + { + if (frame.getStreamId() <= 0) + { + PriorityFrame priority = frame.getPriority(); + priority = priority == null ? null : new PriorityFrame(streamId, priority.getParentStreamId(), priority.getWeight(), priority.isExclusive()); + frame = new HeadersFrame(streamId, frame.getMetaData(), priority, frame.isEndStream()); + } + return frame; + } + + private void push(PushPromiseFrame frame, Promise promise, Stream.Listener listener) + { + Slot slot = new Slot(); + int streamId = reserveSlot(slot, 0); + frame = new PushPromiseFrame(frame.getStreamId(), streamId, frame.getMetaData()); + + try + { + IStream stream = HTTP2Session.this.createLocalStream(streamId, frame.getMetaData()); + stream.setListener(listener); + assignSlotAndFlush(slot, new ControlEntry(frame, stream, new StreamPromiseCallback(promise, stream))); + } + catch (Throwable x) + { + releaseSlotFlushAndFail(slot, promise, x); + } + } + + private void assignSlotAndFlush(Slot slot, ControlEntry entry) + { + // Every time a slot entry is assigned, we must flush. + slot.entry = entry; + flush(); + } + + private int reserveSlot(Slot slot, int streamId) + { + if (streamId <= 0) + { + synchronized (this) + { + streamId = localStreamIds.getAndAdd(2); + slots.offer(slot); + } + } + else + { + synchronized (this) + { + slots.offer(slot); + } + } + return streamId; + } + + private void releaseSlotFlushAndFail(Slot slot, Promise promise, Throwable x) + { + synchronized (this) + { + slots.remove(slot); + } + flush(); + promise.failed(x); + } + + /** + * Flush goes over the entries of the slots queue to flush the entries, + * until either one of the following two conditions is true: + * - The queue is empty. + * - It reaches a slot with a null entry. + * When a slot with a null entry is encountered, this means a concurrent thread reserved a slot + * but hasn't set its entry yet. Since entries must be flushed in order, the thread encountering + * the null entry must bail out and it is up to the concurrent thread to finish up flushing. + * Note that only one thread can flush at any one time, if two threads happen to call flush + * concurrently, one will do the work while the other will bail out, so it is safe that all + * threads call flush after they're done reserving a slot and setting the entry. + */ + private void flush() + { + Thread thread = Thread.currentThread(); + boolean queued = false; + while (true) + { + ControlEntry entry; + synchronized (this) + { + if (flushing == null) + flushing = thread; + else if (flushing != thread) + return; // Another thread is flushing. + + Slot slot = slots.peek(); + entry = slot == null ? null : slot.entry; + + if (entry == null) + { + flushing = null; + // No more slots or null entry, so we may iterate on the flusher. + break; + } + + slots.poll(); + } + queued |= flusher.append(entry); + } + if (queued) + flusher.iterate(); + } + + private class Slot + { + private volatile ControlEntry entry; + } + } } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java index 4d7d9f2e6cd..ee19dffeb89 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/frames/PushPromiseFrame.java @@ -26,6 +26,11 @@ public class PushPromiseFrame extends Frame private final int promisedStreamId; private final MetaData.Request metaData; + public PushPromiseFrame(int streamId, MetaData.Request metaData) + { + this(streamId, 0, metaData); + } + public PushPromiseFrame(int streamId, int promisedStreamId, MetaData.Request metaData) { super(FrameType.PUSH_PROMISE); diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java index 3fec7112c12..8a4224c49ea 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpConnectionOverHTTP2.java @@ -37,7 +37,6 @@ import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.client.HttpUpgrader; import org.eclipse.jetty.client.SendFailure; -import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; @@ -61,7 +60,7 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicInteger sweeps = new AtomicInteger(); private final Session session; - private boolean recycleHttpChannels; + private boolean recycleHttpChannels = true; public HttpConnectionOverHTTP2(HttpDestination destination, Session session) { @@ -126,14 +125,14 @@ public class HttpConnectionOverHTTP2 extends HttpConnection implements Sweeper.S } @Override - protected void normalizeRequest(Request request) + protected void normalizeRequest(HttpRequest request) { super.normalizeRequest(request); if (request instanceof HttpUpgrader.Factory) { HttpUpgrader upgrader = ((HttpUpgrader.Factory)request).newHttpUpgrader(HttpVersion.HTTP_2); - ((HttpRequest)request).getConversation().setAttribute(HttpUpgrader.class.getName(), upgrader); - upgrader.prepare((HttpRequest)request); + request.getConversation().setAttribute(HttpUpgrader.class.getName(), upgrader); + upgrader.prepare(request); } } diff --git a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java index ea2e1f93a22..e2cfc1e8650 100644 --- a/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java +++ b/jetty-http2/http2-http-client-transport/src/main/java/org/eclipse/jetty/http2/client/http/HttpReceiverOverHTTP2.java @@ -50,9 +50,13 @@ import org.eclipse.jetty.http2.frames.ResetFrame; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.Client { + private static final Logger LOG = LoggerFactory.getLogger(HttpReceiverOverHTTP2.class); + private final ContentNotifier contentNotifier = new ContentNotifier(this); public HttpReceiverOverHTTP2(HttpChannel channel) diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/PushedResourcesTest.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/PushedResourcesTest.java index d5bd31e2726..e37c1ec0b6d 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/PushedResourcesTest.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/PushedResourcesTest.java @@ -64,7 +64,7 @@ public class PushedResourcesTest extends AbstractTest { HttpURI pushURI = HttpURI.from("http://localhost:" + connector.getLocalPort() + pushPath); MetaData.Request pushRequest = new MetaData.Request(HttpMethod.GET.asString(), pushURI, HttpVersion.HTTP_2, HttpFields.EMPTY); - stream.push(new PushPromiseFrame(stream.getId(), 0, pushRequest), new Promise.Adapter<>() + stream.push(new PushPromiseFrame(stream.getId(), pushRequest), new Promise.Adapter<>() { @Override public void succeeded(Stream pushStream) diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index 6c0de97c0f4..25ca2002064 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -94,7 +94,7 @@ public class HTTP2ServerConnection extends HTTP2Connection private final AtomicLong totalResponses = new AtomicLong(); private final ServerSessionListener listener; private final HttpConfiguration httpConfig; - private boolean recycleHttpChannels; + private boolean recycleHttpChannels = true; public HTTP2ServerConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, HttpConfiguration httpConfig, ServerParser parser, ISession session, int inputBufferSize, ServerSessionListener listener) { diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index fe7e62dfae8..4e2f6367cbf 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -272,7 +272,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport if (LOG.isDebugEnabled()) LOG.debug("HTTP/2 Push {}", request); - stream.push(new PushPromiseFrame(stream.getId(), 0, request), new Promise<>() + stream.push(new PushPromiseFrame(stream.getId(), request), new Promise<>() { @Override public void succeeded(Stream pushStream) diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java index 3d67bed2047..ecb818614bb 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2ServerTest.java @@ -34,7 +34,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -57,7 +56,6 @@ import org.eclipse.jetty.http2.frames.SettingsFrame; import org.eclipse.jetty.http2.generator.Generator; import org.eclipse.jetty.http2.parser.Parser; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.logging.StacklessLogging; @@ -118,7 +116,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void service(HttpServletRequest req, HttpServletResponse resp) { latch.countDown(); } @@ -175,7 +173,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { latch.countDown(); resp.getOutputStream().write(content); @@ -321,7 +319,7 @@ public class HTTP2ServerTest extends AbstractServerTest startServer(new HttpServlet() { @Override - protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException { try { @@ -340,7 +338,7 @@ public class HTTP2ServerTest extends AbstractServerTest ServerConnector connector2 = new ServerConnector(server, new HTTP2ServerConnectionFactory(new HttpConfiguration())) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java index 7cd08ad4542..b4483f703d2 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/AbstractEndPoint.java @@ -469,11 +469,11 @@ public abstract class AbstractEndPoint extends IdleTimeout implements EndPoint name = c.getSimpleName(); } - return String.format("%s@%h{%s<->%s,%s,fill=%s,flush=%s,to=%d/%d}", + return String.format("%s@%h{l=%s,r=%s,%s,fill=%s,flush=%s,to=%d/%d}", name, this, - getRemoteAddress(), getLocalAddress(), + getRemoteAddress(), _state.get(), _fillInterest.toStateString(), _writeFlusher.toStateString(), diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java deleted file mode 100644 index 2c89a173753..00000000000 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ChannelEndPoint.java +++ /dev/null @@ -1,429 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.io; - -import java.io.Closeable; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.GatheringByteChannel; -import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; - -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.thread.Invocable; -import org.eclipse.jetty.util.thread.Scheduler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Channel End Point. - *

Holds the channel and socket for an NIO endpoint. - */ -public abstract class ChannelEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable -{ - private static final Logger LOG = LoggerFactory.getLogger(ChannelEndPoint.class); - - private final ByteChannel _channel; - private final GatheringByteChannel _gather; - protected final ManagedSelector _selector; - protected final SelectionKey _key; - private boolean _updatePending; - - /** - * The current value for {@link SelectionKey#interestOps()}. - */ - protected int _currentInterestOps; - - /** - * The desired value for {@link SelectionKey#interestOps()}. - */ - protected int _desiredInterestOps; - - private abstract class RunnableTask implements Runnable, Invocable - { - final String _operation; - - protected RunnableTask(String op) - { - _operation = op; - } - - @Override - public String toString() - { - return String.format("CEP:%s:%s:%s", ChannelEndPoint.this, _operation, getInvocationType()); - } - } - - private abstract class RunnableCloseable extends RunnableTask implements Closeable - { - protected RunnableCloseable(String op) - { - super(op); - } - - @Override - public void close() - { - try - { - ChannelEndPoint.this.close(); - } - catch (Throwable x) - { - LOG.warn("Unable to close ChannelEndPoint", x); - } - } - } - - private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate() - { - @Override - public void update(Selector selector) - { - updateKey(); - } - }; - - private final Runnable _runFillable = new RunnableCloseable("runFillable") - { - @Override - public InvocationType getInvocationType() - { - return getFillInterest().getCallbackInvocationType(); - } - - @Override - public void run() - { - getFillInterest().fillable(); - } - }; - - private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite") - { - @Override - public InvocationType getInvocationType() - { - return getWriteFlusher().getCallbackInvocationType(); - } - - @Override - public void run() - { - getWriteFlusher().completeWrite(); - } - - @Override - public String toString() - { - return String.format("CEP:%s:%s:%s->%s", ChannelEndPoint.this, _operation, getInvocationType(), getWriteFlusher()); - } - }; - - private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable") - { - @Override - public InvocationType getInvocationType() - { - InvocationType fillT = getFillInterest().getCallbackInvocationType(); - InvocationType flushT = getWriteFlusher().getCallbackInvocationType(); - if (fillT == flushT) - return fillT; - - if (fillT == InvocationType.EITHER && flushT == InvocationType.NON_BLOCKING) - return InvocationType.EITHER; - - if (fillT == InvocationType.NON_BLOCKING && flushT == InvocationType.EITHER) - return InvocationType.EITHER; - - return InvocationType.BLOCKING; - } - - @Override - public void run() - { - getWriteFlusher().completeWrite(); - getFillInterest().fillable(); - } - }; - - public ChannelEndPoint(ByteChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) - { - super(scheduler); - _channel = channel; - _selector = selector; - _key = key; - _gather = (channel instanceof GatheringByteChannel) ? (GatheringByteChannel)channel : null; - } - - @Override - public boolean isOpen() - { - return _channel.isOpen(); - } - - @Override - public void doClose() - { - if (LOG.isDebugEnabled()) - LOG.debug("doClose {}", this); - try - { - _channel.close(); - } - catch (IOException e) - { - LOG.debug("Unable to close channel", e); - } - finally - { - super.doClose(); - } - } - - @Override - public void onClose(Throwable cause) - { - try - { - super.onClose(cause); - } - finally - { - if (_selector != null) - _selector.destroyEndPoint(this, cause); - } - } - - @Override - public int fill(ByteBuffer buffer) throws IOException - { - if (isInputShutdown()) - return -1; - - int pos = BufferUtil.flipToFill(buffer); - int filled; - try - { - filled = _channel.read(buffer); - if (filled > 0) - notIdle(); - else if (filled == -1) - shutdownInput(); - } - catch (IOException e) - { - LOG.debug("Unable to shutdown output", e); - shutdownInput(); - filled = -1; - } - finally - { - BufferUtil.flipToFlush(buffer, pos); - } - if (LOG.isDebugEnabled()) - LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer)); - return filled; - } - - @Override - public boolean flush(ByteBuffer... buffers) throws IOException - { - long flushed = 0; - try - { - if (buffers.length == 1) - flushed = _channel.write(buffers[0]); - else if (_gather != null && buffers.length > 1) - flushed = _gather.write(buffers, 0, buffers.length); - else - { - for (ByteBuffer b : buffers) - { - if (b.hasRemaining()) - { - int l = _channel.write(b); - if (l > 0) - flushed += l; - if (b.hasRemaining()) - break; - } - } - } - if (LOG.isDebugEnabled()) - LOG.debug("flushed {} {}", flushed, this); - } - catch (IOException e) - { - throw new EofException(e); - } - - if (flushed > 0) - notIdle(); - - for (ByteBuffer b : buffers) - { - if (!BufferUtil.isEmpty(b)) - return false; - } - - return true; - } - - public ByteChannel getChannel() - { - return _channel; - } - - @Override - public Object getTransport() - { - return _channel; - } - - @Override - protected void needsFillInterest() - { - changeInterests(SelectionKey.OP_READ); - } - - @Override - protected void onIncompleteFlush() - { - changeInterests(SelectionKey.OP_WRITE); - } - - @Override - public Runnable onSelected() - { - /** - * This method may run concurrently with {@link #changeInterests(int)}. - */ - - int readyOps = _key.readyOps(); - int oldInterestOps; - int newInterestOps; - synchronized (this) - { - _updatePending = true; - // Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both). - oldInterestOps = _desiredInterestOps; - newInterestOps = oldInterestOps & ~readyOps; - _desiredInterestOps = newInterestOps; - } - - boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; - boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; - - if (LOG.isDebugEnabled()) - LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this); - - // return task to complete the job - Runnable task = fillable - ? (flushable - ? _runCompleteWriteFillable - : _runFillable) - : (flushable - ? _runCompleteWrite - : null); - - if (LOG.isDebugEnabled()) - LOG.debug("task {}", task); - return task; - } - - @Override - public void updateKey() - { - /** - * This method may run concurrently with {@link #changeInterests(int)}. - */ - - try - { - int oldInterestOps; - int newInterestOps; - synchronized (this) - { - _updatePending = false; - oldInterestOps = _currentInterestOps; - newInterestOps = _desiredInterestOps; - if (oldInterestOps != newInterestOps) - { - _currentInterestOps = newInterestOps; - _key.interestOps(newInterestOps); - } - } - - if (LOG.isDebugEnabled()) - LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this); - } - catch (CancelledKeyException x) - { - LOG.debug("Ignoring key update for concurrently closed channel {}", this); - close(); - } - catch (Throwable x) - { - LOG.warn("Ignoring key update for " + this, x); - close(); - } - } - - private void changeInterests(int operation) - { - /** - * This method may run concurrently with - * {@link #updateKey()} and {@link #onSelected()}. - */ - - int oldInterestOps; - int newInterestOps; - boolean pending; - synchronized (this) - { - pending = _updatePending; - oldInterestOps = _desiredInterestOps; - newInterestOps = oldInterestOps | operation; - if (newInterestOps != oldInterestOps) - _desiredInterestOps = newInterestOps; - } - - if (LOG.isDebugEnabled()) - LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this); - - if (!pending && _selector != null) - _selector.submit(_updateKeyAction); - } - - @Override - public String toEndPointString() - { - // We do a best effort to print the right toString() and that's it. - return String.format("%s{io=%d/%d,kio=%d,kro=%d}", - super.toEndPointString(), - _currentInterestOps, - _desiredInterestOps, - ManagedSelector.safeInterestOps(_key), - ManagedSelector.safeReadyOps(_key)); - } -} diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index a997de63001..76fbcff5cd3 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -18,10 +18,10 @@ package org.eclipse.jetty.io; -import java.io.Closeable; import java.io.IOException; import java.net.SocketAddress; import java.net.SocketException; +import java.net.StandardSocketOptions; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; @@ -58,6 +58,7 @@ public class ClientConnector extends ContainerLifeCycle private Duration connectTimeout = Duration.ofSeconds(5); private Duration idleTimeout = Duration.ofSeconds(30); private SocketAddress bindAddress; + private boolean reuseAddress = true; public Executor getExecutor() { @@ -165,6 +166,16 @@ public class ClientConnector extends ContainerLifeCycle this.bindAddress = bindAddress; } + public boolean getReuseAddress() + { + return reuseAddress; + } + + public void setReuseAddress(boolean reuseAddress) + { + this.reuseAddress = reuseAddress; + } + @Override protected void doStart() throws Exception { @@ -219,8 +230,10 @@ public class ClientConnector extends ContainerLifeCycle SocketAddress bindAddress = getBindAddress(); if (bindAddress != null) { + boolean reuseAddress = getReuseAddress(); if (LOG.isDebugEnabled()) - LOG.debug("Binding to {} to connect to {}", bindAddress, address); + LOG.debug("Binding to {} to connect to {}{}", bindAddress, address, (reuseAddress ? " reusing address" : "")); + channel.setOption(StandardSocketOptions.SO_REUSEADDR, reuseAddress); channel.bind(bindAddress); } configure(channel); @@ -253,7 +266,7 @@ public class ClientConnector extends ContainerLifeCycle // exception is being thrown, so we attempt to provide a better error message. if (x.getClass() == SocketException.class) x = new SocketException("Could not connect to " + address).initCause(x); - safeClose(channel); + IO.close(channel); connectFailed(x, context); } } @@ -273,23 +286,23 @@ public class ClientConnector extends ContainerLifeCycle { if (LOG.isDebugEnabled()) LOG.debug("Could not accept {}", channel); - safeClose(channel); + IO.close(channel); Promise promise = (Promise)context.get(CONNECTION_PROMISE_CONTEXT_KEY); if (promise != null) promise.failed(failure); } } - protected void safeClose(Closeable closeable) - { - IO.close(closeable); - } - protected void configure(SocketChannel channel) throws IOException { channel.socket().setTcpNoDelay(true); } + protected EndPoint newEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey selectionKey) + { + return new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); + } + protected void connectFailed(Throwable failure, Map context) { if (LOG.isDebugEnabled()) @@ -309,7 +322,7 @@ public class ClientConnector extends ContainerLifeCycle @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - SocketChannelEndPoint endPoint = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); + EndPoint endPoint = ClientConnector.this.newEndPoint((SocketChannel)channel, selector, selectionKey); endPoint.setIdleTimeout(getIdleTimeout().toMillis()); return endPoint; } diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java index fbdd71ad77b..47651f816a0 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ManagedSelector.java @@ -80,7 +80,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } private final AtomicBoolean _started = new AtomicBoolean(false); - private boolean _selecting = false; + private boolean _selecting; private final SelectorManager _selectorManager; private final int _id; private final ExecutionStrategy _strategy; @@ -123,22 +123,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable start._started.await(); } - protected void onSelectFailed(Throwable cause) - { - // override to change behavior - } - - public int size() - { - Selector s = _selector; - if (s == null) - return 0; - Set keys = s.keys(); - if (keys == null) - return 0; - return keys.size(); - } - @Override protected void doStop() throws Exception { @@ -160,22 +144,119 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable super.doStop(); } + protected int nioSelect(Selector selector, boolean now) throws IOException + { + return now ? selector.selectNow() : selector.select(); + } + + protected int select(Selector selector) throws IOException + { + try + { + int selected = nioSelect(selector, false); + if (selected == 0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Selector {} woken with none selected", selector); + + if (Thread.interrupted() && !isRunning()) + throw new ClosedSelectorException(); + + if (FORCE_SELECT_NOW) + selected = nioSelect(selector, true); + } + return selected; + } + catch (ClosedSelectorException x) + { + throw x; + } + catch (Throwable x) + { + handleSelectFailure(selector, x); + return 0; + } + } + + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + LOG.info("Caught select() failure, trying to recover: {}", failure.toString()); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); + + Selector newSelector = _selectorManager.newSelector(); + for (SelectionKey oldKey : selector.keys()) + { + SelectableChannel channel = oldKey.channel(); + int interestOps = safeInterestOps(oldKey); + if (interestOps >= 0) + { + try + { + Object attachment = oldKey.attachment(); + SelectionKey newKey = channel.register(newSelector, interestOps, attachment); + if (attachment instanceof Selectable) + ((Selectable)attachment).replaceKey(newKey); + oldKey.cancel(); + if (LOG.isDebugEnabled()) + LOG.debug("Transferred {} iOps={} att={}", channel, interestOps, attachment); + } + catch (Throwable t) + { + if (LOG.isDebugEnabled()) + LOG.debug("Could not transfer {}", channel, t); + IO.close(channel); + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("Invalid interestOps for {}", channel); + IO.close(channel); + } + } + + IO.close(selector); + _selector = newSelector; + } + + protected void onSelectFailed(Throwable cause) + { + // override to change behavior + } + + public int size() + { + Selector s = _selector; + if (s == null) + return 0; + Set keys = s.keys(); + if (keys == null) + return 0; + return keys.size(); + } + /** * Submit an {@link SelectorUpdate} to be acted on between calls to {@link Selector#select()} * * @param update The selector update to apply at next wakeup */ public void submit(SelectorUpdate update) + { + submit(update, false); + } + + private void submit(SelectorUpdate update, boolean lazy) { if (LOG.isDebugEnabled()) - LOG.debug("Queued change {} on {}", update, this); + LOG.debug("Queued change lazy={} {} on {}", lazy, update, this); Selector selector = null; synchronized (ManagedSelector.this) { _updates.offer(update); - if (_selecting) + if (_selecting && !lazy) { selector = _selector; // To avoid the extra select wakeup. @@ -223,7 +304,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } } - private void processConnect(SelectionKey key, final Connect connect) + private void processConnect(SelectionKey key, Connect connect) { SelectableChannel channel = key.channel(); try @@ -271,7 +352,18 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Object context = selectionKey.attachment(); Connection connection = _selectorManager.newConnection(channel, endPoint, context); endPoint.setConnection(connection); - selectionKey.attach(endPoint); + submit(selector -> + { + SelectionKey key = selectionKey; + if (key.selector() != selector) + { + key = channel.keyFor(selector); + if (key != null && endPoint instanceof Selectable) + ((Selectable)endPoint).replaceKey(key); + } + if (key != null) + key.attach(endPoint); + }, true); endPoint.onOpen(); endPointOpened(endPoint); _selectorManager.connectionOpened(connection, context); @@ -279,7 +371,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable LOG.debug("Created {}", endPoint); } - public void destroyEndPoint(final EndPoint endPoint, Throwable cause) + void destroyEndPoint(EndPoint endPoint, Throwable cause) { // Waking up the selector is necessary to clean the // cancelled-key set and tell the TCP stack that the @@ -330,8 +422,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Selector selector = _selector; if (selector != null && selector.isOpen()) { - final DumpKeys dump = new DumpKeys(); - final String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); + DumpKeys dump = new DumpKeys(); + String updatesAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()); synchronized (ManagedSelector.this) { updates = new ArrayList<>(_updates); @@ -387,6 +479,14 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable * {@link ManagedSelector} for this endpoint have been processed. */ void updateKey(); + + /** + * Callback method invoked when the SelectionKey is replaced + * because the channel has been moved to a new selector. + * + * @param newKey the new SelectionKey + */ + void replaceKey(SelectionKey newKey); } private class SelectorProducer implements ExecutionStrategy.Producer @@ -434,9 +534,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable LOG.debug("update {}", update); update.update(_selector); } - catch (Throwable th) + catch (Throwable x) { - LOG.warn("Cannot update selector {}", _selector, th); + LOG.warn("Cannot update selector {}", ManagedSelector.this, x); } } _updateable.clear(); @@ -466,39 +566,33 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable try { Selector selector = _selector; - if (selector != null && selector.isOpen()) + if (selector != null) { if (LOG.isDebugEnabled()) LOG.debug("Selector {} waiting with {} keys", selector, selector.keys().size()); - int selected = selector.select(); - if (selected == 0) + int selected = ManagedSelector.this.select(selector); + // The selector may have been recreated. + selector = _selector; + if (selector != null) { if (LOG.isDebugEnabled()) - LOG.debug("Selector {} woken with none selected", selector); + LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size()); - if (Thread.interrupted() && !isRunning()) - throw new ClosedSelectorException(); + int updates; + synchronized (ManagedSelector.this) + { + // finished selecting + _selecting = false; + updates = _updates.size(); + } - if (FORCE_SELECT_NOW) - selected = selector.selectNow(); + _keys = selector.selectedKeys(); + _cursor = _keys.isEmpty() ? Collections.emptyIterator() : _keys.iterator(); + if (LOG.isDebugEnabled()) + LOG.debug("Selector {} processing {} keys, {} updates", selector, _keys.size(), updates); + + return true; } - if (LOG.isDebugEnabled()) - LOG.debug("Selector {} woken up from select, {}/{}/{} selected", selector, selected, selector.selectedKeys().size(), selector.keys().size()); - - int updates; - synchronized (ManagedSelector.this) - { - // finished selecting - _selecting = false; - updates = _updates.size(); - } - - _keys = selector.selectedKeys(); - _cursor = _keys.isEmpty() ? Collections.emptyIterator() : _keys.iterator(); - if (LOG.isDebugEnabled()) - LOG.debug("Selector {} processing {} keys, {} updates", selector, _keys.size(), updates); - - return true; } } catch (Throwable x) @@ -514,7 +608,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable else { LOG.warn(x.toString()); - LOG.debug("select() failure", x); + if (LOG.isDebugEnabled()) + LOG.debug("select() failure", x); } } return false; @@ -525,9 +620,10 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable while (_cursor.hasNext()) { SelectionKey key = _cursor.next(); + Object attachment = key.attachment(); + SelectableChannel channel = key.channel(); if (key.isValid()) { - Object attachment = key.attachment(); if (LOG.isDebugEnabled()) LOG.debug("selected {} {} {} ", safeReadyOps(key), key, attachment); try @@ -550,24 +646,21 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (CancelledKeyException x) { - LOG.debug("Ignoring cancelled key for channel {}", key.channel()); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring cancelled key for channel {}", channel); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } catch (Throwable x) { - LOG.warn("Could not process key for channel " + key.channel(), x); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + LOG.warn("Could not process key for channel {}", channel, x); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } } else { if (LOG.isDebugEnabled()) - LOG.debug("Selector loop ignoring invalid key for channel {}", key.channel()); - Object attachment = key.attachment(); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); + LOG.debug("Selector loop ignoring invalid key for channel {}", channel); + IO.close(attachment instanceof EndPoint ? (EndPoint)attachment : channel); } } return null; @@ -616,7 +709,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private static class DumpKeys implements SelectorUpdate { - private CountDownLatch latch = new CountDownLatch(1); + private final CountDownLatch latch = new CountDownLatch(1); private List keys; @Override @@ -652,9 +745,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private final SelectableChannel _channel; private SelectionKey _key; - public Acceptor(SelectableChannel channel) + Acceptor(SelectableChannel channel) { - this._channel = channel; + _channel = channel; } @Override @@ -662,31 +755,26 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { try { - if (_key == null) - { - _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); - } - + _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); if (LOG.isDebugEnabled()) - LOG.debug("{} acceptor={}", this, _key); + LOG.debug("{} acceptor={}", this, _channel); } catch (Throwable x) { IO.close(_channel); - LOG.warn("Unable to register OP_ACCEPT on selector", x); + LOG.warn("Unable to register OP_ACCEPT on selector for {}", _channel, x); } } @Override public Runnable onSelected() { - SelectableChannel server = _key.channel(); SelectableChannel channel = null; try { while (true) { - channel = _selectorManager.doAccept(server); + channel = _selectorManager.doAccept(_channel); if (channel == null) break; _selectorManager.accepted(channel); @@ -694,10 +782,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { + LOG.warn("Accept failed for channel {}", channel, x); IO.close(channel); - LOG.warn("Accept failed for channel " + channel, x); } - return null; } @@ -706,13 +793,18 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { } + @Override + public void replaceKey(SelectionKey newKey) + { + _key = newKey; + } + @Override public void close() throws IOException { - SelectionKey key = _key; - _key = null; - if (key != null && key.isValid()) - key.cancel(); + // May be called from any thread. + // Implements AbstractConnector.setAccepting(boolean). + submit(selector -> _key.cancel()); } } @@ -732,7 +824,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable @Override public void close() { - LOG.debug("closed accept of {}", channel); + if (LOG.isDebugEnabled()) + LOG.debug("closed accept of {}", channel); IO.close(channel); } @@ -748,7 +841,8 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { IO.close(channel); _selectorManager.onAcceptFailed(channel, x); - LOG.debug("Unable to register update for accept", x); + if (LOG.isDebugEnabled()) + LOG.debug("Could not register channel after accept {}", channel, x); } } @@ -762,7 +856,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } catch (Throwable x) { - LOG.debug("Unable to accept", x); failed(x); } } @@ -770,10 +863,17 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable protected void failed(Throwable failure) { IO.close(channel); - LOG.warn("ManagedSelector#Accept failure : {}", Objects.toString(failure)); - LOG.debug("ManagedSelector#Accept failure", failure); + LOG.warn("Could not accept {}: {}", channel, String.valueOf(failure)); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); _selectorManager.onAcceptFailed(channel, failure); } + + @Override + public String toString() + { + return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), channel); + } } class Connect implements SelectorUpdate, Runnable @@ -833,16 +933,15 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private class CloseConnections implements SelectorUpdate { - final Set _closed; - final CountDownLatch _noEndPoints = new CountDownLatch(1); - final CountDownLatch _complete = new CountDownLatch(1); + private final Set _closed; + private final CountDownLatch _complete = new CountDownLatch(1); - public CloseConnections() + private CloseConnections() { this(null); } - public CloseConnections(Set closed) + private CloseConnections(Set closed) { _closed = closed; } @@ -852,7 +951,6 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable { if (LOG.isDebugEnabled()) LOG.debug("Closing {} connections on {}", selector.keys().size(), ManagedSelector.this); - boolean zero = true; for (SelectionKey key : selector.keys()) { if (key != null && key.isValid()) @@ -861,14 +959,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable Object attachment = key.attachment(); if (attachment instanceof EndPoint) { - EndPoint endp = (EndPoint)attachment; - if (!endp.isOutputShutdown()) - zero = false; - Connection connection = endp.getConnection(); - if (connection != null) - closeable = connection; - else - closeable = endp; + EndPoint endPoint = (EndPoint)attachment; + Connection connection = endPoint.getConnection(); + closeable = Objects.requireNonNullElse(connection, endPoint); } if (closeable != null) @@ -885,30 +978,26 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable } } } - - if (zero) - _noEndPoints.countDown(); _complete.countDown(); } } private class StopSelector implements SelectorUpdate { - CountDownLatch _stopped = new CountDownLatch(1); + private final CountDownLatch _stopped = new CountDownLatch(1); @Override public void update(Selector selector) { for (SelectionKey key : selector.keys()) { - if (key != null && key.isValid()) - { - Object attachment = key.attachment(); - if (attachment instanceof EndPoint) - IO.close((EndPoint)attachment); - } + // Key may be null when using the UnixSocket selector. + if (key == null) + continue; + Object attachment = key.attachment(); + if (attachment instanceof Closeable) + IO.close((Closeable)attachment); } - _selector = null; IO.close(selector); _stopped.countDown(); @@ -936,8 +1025,9 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable catch (Throwable failure) { IO.close(_connect.channel); - LOG.warn("ManagedSelector#CreateEndpoint failure : {}", Objects.toString(failure)); - LOG.debug("ManagedSelector#CreateEndpoint failure", failure); + LOG.warn("Could not create EndPoint {}: {}", _connect.channel, String.valueOf(failure)); + if (LOG.isDebugEnabled()) + LOG.debug("", failure); _connect.failed(failure); } } @@ -945,7 +1035,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable @Override public String toString() { - return String.format("CreateEndPoint@%x{%s,%s}", hashCode(), _connect, _key); + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), _connect); } } @@ -954,7 +1044,7 @@ public class ManagedSelector extends ContainerLifeCycle implements Dumpable private final EndPoint endPoint; private final Throwable cause; - public DestroyEndPoint(EndPoint endPoint, Throwable cause) + private DestroyEndPoint(EndPoint endPoint, Throwable cause) { this.endPoint = endPoint; this.cause = cause; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java index 81fe1c04c88..272c0ecc0ac 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/NetworkTrafficSocketChannelEndPoint.java @@ -20,8 +20,8 @@ package org.eclipse.jetty.io; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; @@ -36,7 +36,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint private final NetworkTrafficListener listener; - public NetworkTrafficSocketChannelEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, NetworkTrafficListener listener) + public NetworkTrafficSocketChannelEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key, Scheduler scheduler, long idleTimeout, NetworkTrafficListener listener) { super(channel, selectSet, key, scheduler); setIdleTimeout(idleTimeout); @@ -80,7 +80,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.opened(getSocket()); + listener.opened(getChannel().socket()); } catch (Throwable x) { @@ -97,7 +97,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.closed(getSocket()); + listener.closed(getChannel().socket()); } catch (Throwable x) { @@ -113,7 +113,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint try { ByteBuffer view = buffer.asReadOnlyBuffer(); - listener.incoming(getSocket(), view); + listener.incoming(getChannel().socket(), view); } catch (Throwable x) { @@ -128,7 +128,7 @@ public class NetworkTrafficSocketChannelEndPoint extends SocketChannelEndPoint { try { - listener.outgoing(getSocket(), view); + listener.outgoing(getChannel().socket(), view); } catch (Throwable x) { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java index 1bcd0080ed8..81b034f4a2d 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java @@ -195,7 +195,7 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump */ public void accept(SelectableChannel channel, Object attachment) { - final ManagedSelector selector = chooseSelector(); + ManagedSelector selector = chooseSelector(); selector.submit(selector.new Accept(channel, attachment)); } @@ -210,7 +210,7 @@ public abstract class SelectorManager extends ContainerLifeCycle implements Dump */ public Closeable acceptor(SelectableChannel server) { - final ManagedSelector selector = chooseSelector(); + ManagedSelector selector = chooseSelector(); ManagedSelector.Acceptor acceptor = selector.new Acceptor(server); selector.submit(acceptor); return acceptor; diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java index 23809302b8c..e662c104b58 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/SocketChannelEndPoint.java @@ -18,53 +18,165 @@ package org.eclipse.jetty.io; +import java.io.Closeable; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; -import java.nio.channels.SelectableChannel; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; import java.nio.channels.SocketChannel; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.thread.Invocable; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SocketChannelEndPoint extends ChannelEndPoint +/** + * Channel End Point. + *

Holds the channel and socket for an NIO endpoint. + */ +public class SocketChannelEndPoint extends AbstractEndPoint implements ManagedSelector.Selectable { private static final Logger LOG = LoggerFactory.getLogger(SocketChannelEndPoint.class); - private final Socket _socket; - private final InetSocketAddress _local; - private final InetSocketAddress _remote; - public SocketChannelEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + private final SocketChannel _channel; + private final ManagedSelector _selector; + private SelectionKey _key; + private boolean _updatePending; + // The current value for interestOps. + private int _currentInterestOps; + // The desired value for interestOps. + private int _desiredInterestOps; + + private abstract class RunnableTask implements Runnable, Invocable { - this((SocketChannel)channel, selector, key, scheduler); + final String _operation; + + protected RunnableTask(String op) + { + _operation = op; + } + + @Override + public String toString() + { + return String.format("%s:%s:%s", SocketChannelEndPoint.this, _operation, getInvocationType()); + } } + private abstract class RunnableCloseable extends RunnableTask implements Closeable + { + protected RunnableCloseable(String op) + { + super(op); + } + + @Override + public void close() + { + try + { + SocketChannelEndPoint.this.close(); + } + catch (Throwable x) + { + LOG.warn("Unable to close {}", SocketChannelEndPoint.this, x); + } + } + } + + private final ManagedSelector.SelectorUpdate _updateKeyAction = this::updateKeyAction; + + private final Runnable _runFillable = new RunnableCloseable("runFillable") + { + @Override + public InvocationType getInvocationType() + { + return getFillInterest().getCallbackInvocationType(); + } + + @Override + public void run() + { + getFillInterest().fillable(); + } + }; + + private final Runnable _runCompleteWrite = new RunnableCloseable("runCompleteWrite") + { + @Override + public InvocationType getInvocationType() + { + return getWriteFlusher().getCallbackInvocationType(); + } + + @Override + public void run() + { + getWriteFlusher().completeWrite(); + } + + @Override + public String toString() + { + return String.format("%s:%s:%s->%s", SocketChannelEndPoint.this, _operation, getInvocationType(), getWriteFlusher()); + } + }; + + private final Runnable _runCompleteWriteFillable = new RunnableCloseable("runCompleteWriteFillable") + { + @Override + public InvocationType getInvocationType() + { + InvocationType fillT = getFillInterest().getCallbackInvocationType(); + InvocationType flushT = getWriteFlusher().getCallbackInvocationType(); + if (fillT == flushT) + return fillT; + + if (fillT == InvocationType.EITHER && flushT == InvocationType.NON_BLOCKING) + return InvocationType.EITHER; + + if (fillT == InvocationType.NON_BLOCKING && flushT == InvocationType.EITHER) + return InvocationType.EITHER; + + return InvocationType.BLOCKING; + } + + @Override + public void run() + { + getWriteFlusher().completeWrite(); + getFillInterest().fillable(); + } + }; + public SocketChannelEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) { - super(channel, selector, key, scheduler); - - _socket = channel.socket(); - _local = (InetSocketAddress)_socket.getLocalSocketAddress(); - _remote = (InetSocketAddress)_socket.getRemoteSocketAddress(); - } - - public Socket getSocket() - { - return _socket; + super(scheduler); + _channel = channel; + _selector = selector; + _key = key; } @Override public InetSocketAddress getLocalAddress() { - return _local; + return (InetSocketAddress)_channel.socket().getLocalSocketAddress(); } @Override public InetSocketAddress getRemoteAddress() { - return _remote; + return (InetSocketAddress)_channel.socket().getRemoteSocketAddress(); + } + + @Override + public boolean isOpen() + { + return _channel.isOpen(); } @Override @@ -72,12 +184,250 @@ public class SocketChannelEndPoint extends ChannelEndPoint { try { - if (!_socket.isOutputShutdown()) - _socket.shutdownOutput(); + Socket socket = _channel.socket(); + if (!socket.isOutputShutdown()) + socket.shutdownOutput(); + } + catch (IOException e) + { + LOG.debug("Could not shutdown output for {}", _channel, e); + } + } + + @Override + public void doClose() + { + if (LOG.isDebugEnabled()) + LOG.debug("doClose {}", this); + try + { + _channel.close(); + } + catch (IOException e) + { + LOG.debug("Unable to close channel", e); + } + finally + { + super.doClose(); + } + } + + @Override + public void onClose(Throwable cause) + { + try + { + super.onClose(cause); + } + finally + { + if (_selector != null) + _selector.destroyEndPoint(this, cause); + } + } + + @Override + public int fill(ByteBuffer buffer) throws IOException + { + if (isInputShutdown()) + return -1; + + int pos = BufferUtil.flipToFill(buffer); + int filled; + try + { + filled = _channel.read(buffer); + if (filled > 0) + notIdle(); + else if (filled == -1) + shutdownInput(); } catch (IOException e) { LOG.debug("Unable to shutdown output", e); + shutdownInput(); + filled = -1; + } + finally + { + BufferUtil.flipToFlush(buffer, pos); + } + if (LOG.isDebugEnabled()) + LOG.debug("filled {} {}", filled, BufferUtil.toDetailString(buffer)); + return filled; + } + + @Override + public boolean flush(ByteBuffer... buffers) throws IOException + { + long flushed; + try + { + flushed = _channel.write(buffers); + if (LOG.isDebugEnabled()) + LOG.debug("flushed {} {}", flushed, this); + } + catch (IOException e) + { + throw new EofException(e); + } + + if (flushed > 0) + notIdle(); + + for (ByteBuffer b : buffers) + { + if (!BufferUtil.isEmpty(b)) + return false; + } + + return true; + } + + public SocketChannel getChannel() + { + return _channel; + } + + @Override + public Object getTransport() + { + return _channel; + } + + @Override + protected void needsFillInterest() + { + changeInterests(SelectionKey.OP_READ); + } + + @Override + protected void onIncompleteFlush() + { + changeInterests(SelectionKey.OP_WRITE); + } + + @Override + public Runnable onSelected() + { + // This method runs from the selector thread, + // possibly concurrently with changeInterests(int). + + int readyOps = _key.readyOps(); + int oldInterestOps; + int newInterestOps; + synchronized (this) + { + _updatePending = true; + // Remove the readyOps, that here can only be OP_READ or OP_WRITE (or both). + oldInterestOps = _desiredInterestOps; + newInterestOps = oldInterestOps & ~readyOps; + _desiredInterestOps = newInterestOps; + } + + boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; + boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; + + if (LOG.isDebugEnabled()) + LOG.debug("onSelected {}->{} r={} w={} for {}", oldInterestOps, newInterestOps, fillable, flushable, this); + + // return task to complete the job + Runnable task = fillable + ? (flushable + ? _runCompleteWriteFillable + : _runFillable) + : (flushable + ? _runCompleteWrite + : null); + + if (LOG.isDebugEnabled()) + LOG.debug("task {}", task); + return task; + } + + private void updateKeyAction(Selector selector) + { + updateKey(); + } + + @Override + public void updateKey() + { + // This method runs from the selector thread, + // possibly concurrently with changeInterests(int). + + try + { + int oldInterestOps; + int newInterestOps; + synchronized (this) + { + _updatePending = false; + oldInterestOps = _currentInterestOps; + newInterestOps = _desiredInterestOps; + if (oldInterestOps != newInterestOps) + { + _currentInterestOps = newInterestOps; + _key.interestOps(newInterestOps); + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("Key interests updated {} -> {} on {}", oldInterestOps, newInterestOps, this); + } + catch (CancelledKeyException x) + { + if (LOG.isDebugEnabled()) + LOG.debug("Ignoring key update for cancelled key {}", this, x); + close(); + } + catch (Throwable x) + { + LOG.warn("Ignoring key update for {}", this, x); + close(); } } + + @Override + public void replaceKey(SelectionKey newKey) + { + _key = newKey; + } + + private void changeInterests(int operation) + { + // This method runs from any thread, possibly + // concurrently with updateKey() and onSelected(). + + int oldInterestOps; + int newInterestOps; + boolean pending; + synchronized (this) + { + pending = _updatePending; + oldInterestOps = _desiredInterestOps; + newInterestOps = oldInterestOps | operation; + if (newInterestOps != oldInterestOps) + _desiredInterestOps = newInterestOps; + } + + if (LOG.isDebugEnabled()) + LOG.debug("changeInterests p={} {}->{} for {}", pending, oldInterestOps, newInterestOps, this); + + if (!pending && _selector != null) + _selector.submit(_updateKeyAction); + } + + @Override + public String toEndPointString() + { + // We do a best effort to print the right toString() and that's it. + return String.format("%s{io=%d/%d,kio=%d,kro=%d}", + super.toEndPointString(), + _currentInterestOps, + _desiredInterestOps, + ManagedSelector.safeInterestOps(_key), + ManagedSelector.safeReadyOps(_key)); + } } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java index 5d3ca7e2772..9890de59d17 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SelectorManagerTest.java @@ -72,11 +72,11 @@ public class SelectorManagerTest SelectorManager selectorManager = new SelectorManager(executor, scheduler) { @Override - protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) throws IOException + protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()); - endp.setIdleTimeout(connectTimeout / 2); - return endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); + endPoint.setIdleTimeout(connectTimeout / 2); + return endPoint; } @Override @@ -96,7 +96,7 @@ public class SelectorManagerTest } @Override - public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) throws IOException + public Connection newConnection(SelectableChannel channel, EndPoint endpoint, Object attachment) { ((Callback)attachment).succeeded(); return new AbstractConnection(endpoint, executor) diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java index 3ee35a3f160..94f79c12d45 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointInterestsTest.java @@ -69,7 +69,7 @@ public class SocketChannelEndPointInterestsTest @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()) + SocketChannelEndPoint endp = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()) { @Override protected void onIncompleteFlush() diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java index 537174b8625..595ddb52115 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/SocketChannelEndPointTest.java @@ -465,10 +465,10 @@ public class SocketChannelEndPointTest @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, selectionKey, getScheduler()); - _lastEndPoint = endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, selectionKey, getScheduler()); + _lastEndPoint = endPoint; _lastEndPointLatch.countDown(); - return endp; + return endPoint; } @Override @@ -580,11 +580,11 @@ public class SocketChannelEndPointTest protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endp = new SocketChannelEndPoint(channel, selector, key, getScheduler()); - endp.setIdleTimeout(60000); - _lastEndPoint = endp; + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); + endPoint.setIdleTimeout(60000); + _lastEndPoint = endPoint; _lastEndPointLatch.countDown(); - return endp; + return endPoint; } @Override @@ -743,7 +743,7 @@ public class SocketChannelEndPointTest return; } - EndPoint endp = getEndPoint(); + EndPoint endPoint = getEndPoint(); try { _last = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); @@ -756,17 +756,17 @@ public class SocketChannelEndPointTest BufferUtil.compact(_in); if (BufferUtil.isFull(_in)) throw new IllegalStateException("FULL " + BufferUtil.toDetailString(_in)); - int filled = endp.fill(_in); + int filled = endPoint.fill(_in); if (filled > 0) progress = true; // If the tests wants to block, then block - while (_blockAt.get() > 0 && endp.isOpen() && _in.remaining() < _blockAt.get()) + while (_blockAt.get() > 0 && endPoint.isOpen() && _in.remaining() < _blockAt.get()) { FutureCallback future = _blockingRead = new FutureCallback(); fillInterested(); future.get(); - filled = endp.fill(_in); + filled = endPoint.fill(_in); progress |= filled > 0; } @@ -782,18 +782,18 @@ public class SocketChannelEndPointTest for (int i = 0; i < _writeCount.get(); i++) { FutureCallback blockingWrite = new FutureCallback(); - endp.write(blockingWrite, out.asReadOnlyBuffer()); + endPoint.write(blockingWrite, out.asReadOnlyBuffer()); blockingWrite.get(); } progress = true; } // are we done? - if (endp.isInputShutdown()) - endp.shutdownOutput(); + if (endPoint.isInputShutdown()) + endPoint.shutdownOutput(); } - if (endp.isOpen()) + if (endPoint.isOpen()) fillInterested(); } catch (ExecutionException e) @@ -802,9 +802,9 @@ public class SocketChannelEndPointTest try { FutureCallback blockingWrite = new FutureCallback(); - endp.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in))); + endPoint.write(blockingWrite, BufferUtil.toBuffer("EE: " + BufferUtil.toString(_in))); blockingWrite.get(); - endp.shutdownOutput(); + endPoint.shutdownOutput(); } catch (Exception e2) { diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java index 6a287c07226..6e885c1f5eb 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/ConnectorServer.java @@ -66,7 +66,7 @@ public class ConnectorServer extends AbstractLifeCycle private JMXServiceURL _jmxURL; private final Map _environment; private final String _objectName; - private final SslContextFactory _sslContextFactory; + private final SslContextFactory.Server _sslContextFactory; private int _registryPort; private int _rmiPort; private JMXConnectorServer _connectorServer; @@ -98,7 +98,7 @@ public class ConnectorServer extends AbstractLifeCycle this(svcUrl, environment, name, null); } - public ConnectorServer(JMXServiceURL svcUrl, Map environment, String name, SslContextFactory sslContextFactory) + public ConnectorServer(JMXServiceURL svcUrl, Map environment, String name, SslContextFactory.Server sslContextFactory) { this._jmxURL = svcUrl; this._environment = environment == null ? new HashMap<>() : new HashMap<>(environment); @@ -243,6 +243,7 @@ public class ConnectorServer extends AbstractLifeCycle if (_sslContextFactory == null) { ServerSocket server = new ServerSocket(); + server.setReuseAddress(true); server.bind(new InetSocketAddress(address, port)); return server; } diff --git a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java index f8db4c44bd2..f3200cc0cdc 100644 --- a/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java +++ b/jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java @@ -60,7 +60,7 @@ public class MBeanContainer implements Container.InheritedListener, Dumpable, De private final MBeanServer _mbeanServer; private final boolean _useCacheForOtherClassLoaders; - private final ConcurrentMap _metaData = new ConcurrentHashMap<>(); + private final ConcurrentMap, MetaData> _metaData = new ConcurrentHashMap<>(); private final ConcurrentMap _beans = new ConcurrentHashMap<>(); private final ConcurrentMap _mbeans = new ConcurrentHashMap<>(); private String _domain = null; diff --git a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java index 9a44308d6c3..ff762786792 100644 --- a/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java +++ b/jetty-jmx/src/test/java/org/eclipse/jetty/jmx/ConnectorServerTest.java @@ -231,7 +231,7 @@ public class ConnectorServerTest @Test public void testJMXOverTLS() throws Exception { - SslContextFactory sslContextFactory = new SslContextFactory.Server(); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); String keyStorePath = MavenTestingUtils.getTestResourcePath("keystore.p12").toString(); String keyStorePassword = "storepwd"; sslContextFactory.setKeyStorePath(keyStorePath); diff --git a/jetty-memcached/jetty-memcached-sessions/src/test/java/org/eclipse/jetty/memcached/session/TestMemcachedSessions.java b/jetty-memcached/jetty-memcached-sessions/src/test/java/org/eclipse/jetty/memcached/session/TestMemcachedSessions.java index 9debffeb235..0870135808b 100644 --- a/jetty-memcached/jetty-memcached-sessions/src/test/java/org/eclipse/jetty/memcached/session/TestMemcachedSessions.java +++ b/jetty-memcached/jetty-memcached-sessions/src/test/java/org/eclipse/jetty/memcached/session/TestMemcachedSessions.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.memcached.session; import java.io.IOException; import java.io.PrintWriter; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,6 +28,7 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.session.CachingSessionDataStore; @@ -40,7 +40,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * TestMemcachedSessions @@ -49,9 +48,8 @@ public class TestMemcachedSessions { public static class TestServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String arg = req.getParameter("action"); if (arg == null) @@ -117,16 +115,17 @@ public class TestMemcachedSessions ContentResponse response = client.GET("http://localhost:" + port + contextPath + "?action=set&value=" + value); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); String sessionCookie = response.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); String resp = response.getContentAsString(); assertEquals(resp.trim(), String.valueOf(value)); // Be sure the session value is still there + HttpField cookie = new HttpField("Cookie", sessionCookie); Request request = client.newRequest("http://localhost:" + port + contextPath + "?action=get"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -135,13 +134,13 @@ public class TestMemcachedSessions //Delete the session request = client.newRequest("http://localhost:" + port + contextPath + "?action=del"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); //Check that the session is gone request = client.newRequest("http://localhost:" + port + contextPath + "?action=get"); - request.header("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); resp = response.getContentAsString(); diff --git a/jetty-osgi/test-jetty-osgi/pom.xml b/jetty-osgi/test-jetty-osgi/pom.xml index 18f4c1d8c9b..04c634c8e5d 100644 --- a/jetty-osgi/test-jetty-osgi/pom.xml +++ b/jetty-osgi/test-jetty-osgi/pom.xml @@ -323,7 +323,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} runtime diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java index 6c604d0d2d8..15aaaa512e5 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java +++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java @@ -196,8 +196,8 @@ public class TestOSGiUtil res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-plus").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty").artifactId("jetty-annotations").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-core").versionAsInProject().start()); - res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-servlet").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-util").versionAsInProject().start()); + res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-util-server").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-api").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-server").versionAsInProject().start()); res.add(mavenBundle().groupId("org.eclipse.jetty.websocket").artifactId("websocket-jetty-client").versionAsInProject().start()); diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index d29dbad3b26..8e5669e1a26 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -499,7 +499,7 @@ public abstract class AbstractProxyServlet extends HttpServlet if (_hostHeader != null) newHeaders.add(HttpHeader.HOST, _hostHeader); - proxyRequest.set(newHeaders); + proxyRequest.headers(headers -> headers.clear().add(newHeaders)); } protected Set findConnectionHeaders(HttpServletRequest clientRequest) @@ -531,15 +531,19 @@ public abstract class AbstractProxyServlet extends HttpServlet protected void addViaHeader(Request proxyRequest) { - proxyRequest.header(HttpHeader.VIA, "http/1.1 " + getViaHost()); + proxyRequest.headers(headers -> headers.add(HttpHeader.VIA, "http/1.1 " + getViaHost())); } protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest) { - proxyRequest.header(HttpHeader.X_FORWARDED_FOR, clientRequest.getRemoteAddr()); - proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, clientRequest.getScheme()); - proxyRequest.header(HttpHeader.X_FORWARDED_HOST, clientRequest.getHeader(HttpHeader.HOST.asString())); - proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, clientRequest.getLocalName()); + proxyRequest.headers(headers -> headers.add(HttpHeader.X_FORWARDED_FOR, clientRequest.getRemoteAddr())); + proxyRequest.headers(headers -> headers.add(HttpHeader.X_FORWARDED_PROTO, clientRequest.getScheme())); + String hostHeader = clientRequest.getHeader(HttpHeader.HOST.asString()); + if (hostHeader != null) + proxyRequest.headers(headers -> headers.add(HttpHeader.X_FORWARDED_HOST, hostHeader)); + String localName = clientRequest.getLocalName(); + if (localName != null) + proxyRequest.headers(headers -> headers.add(HttpHeader.X_FORWARDED_SERVER, localName)); } protected void sendProxyRequest(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) @@ -633,13 +637,9 @@ public abstract class AbstractProxyServlet extends HttpServlet } builder.append(System.lineSeparator()); } - _log.debug("{} proxying to downstream:{}{}{}{}{}", + _log.debug("{} proxying to downstream:{}{}", getRequestId(clientRequest), System.lineSeparator(), - serverResponse, - System.lineSeparator(), - serverResponse.getHeaders().toString().trim(), - System.lineSeparator(), builder); } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AsyncMiddleManServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AsyncMiddleManServlet.java index cd5c5e2fbad..d66a93a1eda 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AsyncMiddleManServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AsyncMiddleManServlet.java @@ -396,7 +396,7 @@ public class AsyncMiddleManServlet extends AbstractProxyServlet clientRequest.setAttribute(PROXY_REQUEST_CONTENT_COMMITTED_ATTRIBUTE, true); if (!expects100Continue) { - proxyRequest.header(HttpHeader.CONTENT_LENGTH, null); + proxyRequest.headers(headers -> headers.remove(HttpHeader.CONTENT_LENGTH)); sendProxyRequest(clientRequest, proxyResponse, proxyRequest); } } diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java index 9cd10814def..29aba54a82c 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/ConnectHandler.java @@ -506,7 +506,7 @@ public class ConnectHandler extends HandlerWrapper @Override protected EndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey key) { - SocketChannelEndPoint endPoint = new SocketChannelEndPoint(channel, selector, key, getScheduler()); + SocketChannelEndPoint endPoint = new SocketChannelEndPoint((SocketChannel)channel, selector, key, getScheduler()); endPoint.setIdleTimeout(getIdleTimeout()); return endPoint; } diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java index 11bb328dd78..d520d3c8fa7 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/AsyncMiddleManServletTest.java @@ -226,7 +226,7 @@ public class AsyncMiddleManServletTest Request.Content gzipContent = new BytesRequestContent(gzipBytes); ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.CONTENT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .body(gzipContent) .timeout(5, TimeUnit.SECONDS) .send(); @@ -301,7 +301,7 @@ public class AsyncMiddleManServletTest startClient(); ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.CONTENT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .body(new BytesRequestContent(gzip(bytes))) .timeout(5, TimeUnit.SECONDS) .send(); @@ -348,7 +348,7 @@ public class AsyncMiddleManServletTest startClient(); ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.CONTENT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -393,7 +393,7 @@ public class AsyncMiddleManServletTest AsyncRequestContent content = new AsyncRequestContent(); Request request = client.newRequest("localhost", serverConnector.getLocalPort()); FutureResponseListener listener = new FutureResponseListener(request); - request.header(HttpHeader.CONTENT_ENCODING, "gzip") + request.headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .body(content) .send(listener); byte[] bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8); @@ -438,7 +438,7 @@ public class AsyncMiddleManServletTest byte[] bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8); ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.CONTENT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .body(new BytesRequestContent(gzip(bytes))) .timeout(5, TimeUnit.SECONDS) .send(); @@ -482,7 +482,7 @@ public class AsyncMiddleManServletTest startClient(); ContentResponse response = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.CONTENT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.CONTENT_ENCODING, HttpHeaderValue.GZIP)) .body(new BytesRequestContent(gzip(bytes))) .timeout(5, TimeUnit.SECONDS) .send(); diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java index 053de8b782a..41d12de38e8 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ForwardProxyTLSServerTest.java @@ -251,12 +251,13 @@ public class ForwardProxyTLSServerTest assertEquals(body, content); content = "body=" + body; + int contentLength = content.length(); ContentResponse response2 = httpClient.newRequest("localhost", serverConnector.getLocalPort()) .scheme(HttpScheme.HTTPS.asString()) .method(HttpMethod.POST) .path("/echo") - .header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString()) - .header(HttpHeader.CONTENT_LENGTH, String.valueOf(content.length())) + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString())) + .headers(headers -> headers.put(HttpHeader.CONTENT_LENGTH, String.valueOf(contentLength))) .body(new StringRequestContent(content)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -318,8 +319,8 @@ public class ForwardProxyTLSServerTest .scheme(HttpScheme.HTTPS.asString()) .method(HttpMethod.POST) .path("/echo") - .header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString()) - .header(HttpHeader.CONTENT_LENGTH, String.valueOf(body2.length())) + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString())) + .headers(headers -> headers.put(HttpHeader.CONTENT_LENGTH, String.valueOf(body2.length()))) .body(new StringRequestContent(body2)); // Make sure the second connection can send the exchange via the tunnel diff --git a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java index d39e94d9457..e944ec8c51b 100644 --- a/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java +++ b/jetty-proxy/src/test/java/org/eclipse/jetty/proxy/ProxyServletTest.java @@ -988,7 +988,7 @@ public class ProxyServletTest String value1 = "1"; ContentResponse response1 = client.newRequest("localhost", serverConnector.getLocalPort()) - .header(name, value1) + .headers(headers -> headers.put(name, value1)) .timeout(5, TimeUnit.SECONDS) .send(); assertEquals(200, response1.getStatus()); @@ -1003,7 +1003,7 @@ public class ProxyServletTest { String value2 = "2"; ContentResponse response2 = client2.newRequest("localhost", serverConnector.getLocalPort()) - .header(name, value2) + .headers(headers -> headers.put(name, value2)) .timeout(5, TimeUnit.SECONDS) .send(); assertEquals(200, response2.getStatus()); @@ -1236,10 +1236,7 @@ public class ProxyServletTest startClient(); Request request = client.newRequest("localhost", serverConnector.getLocalPort()); - for (Map.Entry entry : hopHeaders.entrySet()) - { - request.header(entry.getKey(), entry.getValue()); - } + hopHeaders.forEach((key, value) -> request.headers(headers -> headers.add(key, value))); ContentResponse response = request .timeout(5, TimeUnit.SECONDS) .send(); @@ -1283,7 +1280,7 @@ public class ProxyServletTest CountDownLatch contentLatch = new CountDownLatch(1); CountDownLatch clientLatch = new CountDownLatch(1); client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) .body(new BytesRequestContent(content)) .onRequestContent((request, buffer) -> contentLatch.countDown()) .send(new BufferingResponseListener() @@ -1340,7 +1337,7 @@ public class ProxyServletTest requestContent.offer(ByteBuffer.wrap(content, 0, chunk1)); CountDownLatch clientLatch = new CountDownLatch(1); client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) .body(requestContent) .send(new BufferingResponseListener() { @@ -1400,7 +1397,7 @@ public class ProxyServletTest requestContent.offer(ByteBuffer.wrap(content, 0, chunk1)); CountDownLatch clientLatch = new CountDownLatch(1); client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) .body(requestContent) .send(result -> { @@ -1448,7 +1445,7 @@ public class ProxyServletTest CountDownLatch contentLatch = new CountDownLatch(1); CountDownLatch clientLatch = new CountDownLatch(1); client.newRequest("localhost", serverConnector.getLocalPort()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString())) .body(new BytesRequestContent(content)) .onRequestContent((request, buffer) -> contentLatch.countDown()) .send(result -> diff --git a/jetty-server/pom.xml b/jetty-server/pom.xml index f37980beea8..b9900ccb068 100644 --- a/jetty-server/pom.xml +++ b/jetty-server/pom.xml @@ -37,6 +37,14 @@ org.eclipse.jetty.server.* + + maven-surefire-plugin + + + @{argLine} ${jetty.surefire.argLine} --add-opens org.eclipse.jetty.server/org.eclipse.jetty.server=ALL-UNNAMED + + + diff --git a/jetty-server/src/main/config/etc/jetty-http.xml b/jetty-server/src/main/config/etc/jetty-http.xml index 17aa2453587..ac0985b14cd 100644 --- a/jetty-server/src/main/config/etc/jetty-http.xml +++ b/jetty-server/src/main/config/etc/jetty-http.xml @@ -38,6 +38,10 @@ + + + + diff --git a/jetty-server/src/main/config/etc/jetty-ssl.xml b/jetty-server/src/main/config/etc/jetty-ssl.xml index e53ce12c81b..54ab9d81b20 100644 --- a/jetty-server/src/main/config/etc/jetty-ssl.xml +++ b/jetty-server/src/main/config/etc/jetty-ssl.xml @@ -31,6 +31,10 @@ + + + + diff --git a/jetty-server/src/main/config/modules/http.mod b/jetty-server/src/main/config/modules/http.mod index a0b26e12663..acc0c7be6f4 100644 --- a/jetty-server/src/main/config/modules/http.mod +++ b/jetty-server/src/main/config/modules/http.mod @@ -39,5 +39,20 @@ etc/jetty-http.xml ## Thread priority delta to give to acceptor threads # jetty.http.acceptorPriorityDelta=0 +## The requested maximum length of the queue of incoming connections. +# jetty.http.acceptQueueSize=0 + +## Enable/disable the SO_REUSEADDR socket option. +# jetty.http.reuseAddress=true + +## Enable/disable TCP_NODELAY on accepted sockets. +# jetty.http.acceptedTcpNoDelay=true + +## The SO_RCVBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value. +# jetty.http.acceptedReceiveBufferSize=-1 + +## The SO_SNDBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value. +# jetty.http.acceptedSendBufferSize=-1 + ## Connect Timeout in milliseconds # jetty.http.connectTimeout=15000 diff --git a/jetty-server/src/main/config/modules/ssl.mod b/jetty-server/src/main/config/modules/ssl.mod index bd64f9d7a7a..3395c7eda91 100644 --- a/jetty-server/src/main/config/modules/ssl.mod +++ b/jetty-server/src/main/config/modules/ssl.mod @@ -40,6 +40,21 @@ etc/jetty-ssl-context.xml ## Thread priority delta to give to acceptor threads # jetty.ssl.acceptorPriorityDelta=0 +## The requested maximum length of the queue of incoming connections. +# jetty.ssl.acceptQueueSize=0 + +## Enable/disable the SO_REUSEADDR socket option. +# jetty.ssl.reuseAddress=true + +## Enable/disable TCP_NODELAY on accepted sockets. +# jetty.ssl.acceptedTcpNoDelay=true + +## The SO_RCVBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value. +# jetty.ssl.acceptedReceiveBufferSize=-1 + +## The SO_SNDBUF option to set on accepted sockets. A value of -1 indicates that it is left to its default value. +# jetty.ssl.acceptedSendBufferSize=-1 + ## Connect Timeout in milliseconds # jetty.ssl.connectTimeout=15000 diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java index fef244df7fd..27ca81906a7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/NetworkTrafficServerConnector.java @@ -23,10 +23,10 @@ import java.nio.channels.SocketChannel; import java.util.concurrent.Executor; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.NetworkTrafficListener; import org.eclipse.jetty.io.NetworkTrafficSocketChannelEndPoint; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; @@ -81,7 +81,7 @@ public class NetworkTrafficServerConnector extends ServerConnector } @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new NetworkTrafficSocketChannelEndPoint(channel, selectSet, key, getScheduler(), getIdleTimeout(), getNetworkTrafficListener()); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java index ddba526aab3..3ffb77571c8 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyConnectionFactory.java @@ -641,6 +641,10 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory if (LOG.isDebugEnabled()) LOG.debug("Proxy v2 {} {}", getEndPoint(), proxyEndPoint.toString()); } + else + { + _buffer.position(_buffer.position() + _length); + } if (LOG.isDebugEnabled()) LOG.debug("Proxy v2 parsing dynamic packet part is now done, upgrading to {}", _nextProtocol); @@ -777,7 +781,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory { return _tlvs != null ? _tlvs.get(type) : null; } - + @Override public void close(Throwable cause) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java new file mode 100644 index 00000000000..43fa5583f07 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ProxyCustomizer.java @@ -0,0 +1,108 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletRequest; + +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.Attributes; + +/** + *

Customizer that extracts the real local and remote address:port pairs from a {@link ProxyConnectionFactory} + * and sets them on the request with {@link ServletRequest#setAttribute(String, Object)}. + */ +public class ProxyCustomizer implements HttpConfiguration.Customizer +{ + /** + * The remote address attribute name. + */ + public static final String REMOTE_ADDRESS_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.remote.address"; + + /** + * The remote port attribute name. + */ + public static final String REMOTE_PORT_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.remote.port"; + + /** + * The local address attribute name. + */ + public static final String LOCAL_ADDRESS_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.local.address"; + + /** + * The local port attribute name. + */ + public static final String LOCAL_PORT_ATTRIBUTE_NAME = "org.eclipse.jetty.proxy.local.port"; + + @Override + public void customize(Connector connector, HttpConfiguration channelConfig, Request request) + { + EndPoint endPoint = request.getHttpChannel().getEndPoint(); + if (endPoint instanceof ProxyConnectionFactory.ProxyEndPoint) + { + EndPoint underlyingEndpoint = ((ProxyConnectionFactory.ProxyEndPoint)endPoint).unwrap(); + request.setAttributes(new ProxyAttributes(underlyingEndpoint.getRemoteAddress(), underlyingEndpoint.getLocalAddress(), request.getAttributes())); + } + } + + private static class ProxyAttributes extends Attributes.Wrapper + { + private final InetSocketAddress remoteAddress; + private final InetSocketAddress localAddress; + + private ProxyAttributes(InetSocketAddress remoteAddress, InetSocketAddress localAddress, Attributes attributes) + { + super(attributes); + this.remoteAddress = remoteAddress; + this.localAddress = localAddress; + } + + @Override + public Object getAttribute(String name) + { + switch (name) + { + case REMOTE_ADDRESS_ATTRIBUTE_NAME: + return remoteAddress.getAddress().getHostAddress(); + case REMOTE_PORT_ATTRIBUTE_NAME: + return remoteAddress.getPort(); + case LOCAL_ADDRESS_ATTRIBUTE_NAME: + return localAddress.getAddress().getHostAddress(); + case LOCAL_PORT_ATTRIBUTE_NAME: + return localAddress.getPort(); + default: + return super.getAttribute(name); + } + } + + @Override + public Set getAttributeNameSet() + { + Set names = new HashSet<>(_attributes.getAttributeNameSet()); + names.add(REMOTE_ADDRESS_ATTRIBUTE_NAME); + names.add(REMOTE_PORT_ATTRIBUTE_NAME); + names.add(LOCAL_ADDRESS_ATTRIBUTE_NAME); + names.add(LOCAL_PORT_ATTRIBUTE_NAME); + return names; + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java index ab745646d88..61095d646c0 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/PushBuilderImpl.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.server; +import java.util.EnumSet; +import java.util.Objects; import java.util.Set; import javax.servlet.http.PushBuilder; @@ -27,6 +29,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +41,14 @@ public class PushBuilderImpl implements PushBuilder { private static final Logger LOG = LoggerFactory.getLogger(PushBuilderImpl.class); - private static final HttpField JettyPush = new HttpField("x-http2-push", "PushBuilder"); + private static final HttpField JETTY_PUSH = new HttpField("x-http2-push", "PushBuilder"); + private static EnumSet UNSAFE_METHODS = EnumSet.of( + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.DELETE, + HttpMethod.CONNECT, + HttpMethod.OPTIONS, + HttpMethod.TRACE); private final Request _request; private final HttpFields.Mutable _fields; @@ -56,7 +66,7 @@ public class PushBuilderImpl implements PushBuilder _method = method; _queryString = queryString; _sessionId = sessionId; - _fields.add(JettyPush); + _fields.add(JETTY_PUSH); if (LOG.isDebugEnabled()) LOG.debug("PushBuilder({} {}?{} s={} c={})", _method, _request.getRequestURI(), _queryString, _sessionId); } @@ -70,6 +80,10 @@ public class PushBuilderImpl implements PushBuilder @Override public PushBuilder method(String method) { + Objects.requireNonNull(method); + + if (StringUtil.isBlank(method) || UNSAFE_METHODS.contains(HttpMethod.fromString(method))) + throw new IllegalArgumentException("Method not allowed for push: " + method); _method = method; return this; } @@ -149,9 +163,6 @@ public class PushBuilderImpl implements PushBuilder @Override public void push() { - if (HttpMethod.POST.is(_method) || HttpMethod.PUT.is(_method)) - throw new IllegalStateException("Bad Method " + _method); - if (_path == null || _path.length() == 0) throw new IllegalStateException("Bad Path " + _path); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index d5426fbfc14..745290bb93a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -71,6 +71,7 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.ComplianceViolation; import org.eclipse.jetty.http.HostPortHttpField; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -393,6 +394,11 @@ public class Request implements HttpServletRequest HttpFields.Mutable fields = HttpFields.build(getHttpFields(), NOT_PUSHED_HEADERS); + HttpField authField = getHttpFields().getField(HttpHeader.AUTHORIZATION); + //TODO check what to do for digest etc etc + if (getUserPrincipal() != null && authField.getValue().startsWith("Basic")) + fields.add(authField); + String id; try { @@ -410,12 +416,47 @@ public class Request implements HttpServletRequest id = getRequestedSessionId(); } + Map cookies = new HashMap<>(); + Cookie[] existingCookies = getCookies(); + if (existingCookies != null) + { + for (Cookie c: getCookies()) + { + cookies.put(c.getName(), c.getValue()); + } + } + + //Any Set-Cookies that were set on the response must be set as Cookies on the + //PushBuilder, unless the max-age of the cookie is <= 0 + HttpFields responseFields = getResponse().getHttpFields(); + for (HttpField field : responseFields) + { + HttpHeader header = field.getHeader(); + if (header == HttpHeader.SET_COOKIE) + { + HttpCookie cookie = ((SetCookieHttpField)field).getHttpCookie(); + if (cookie.getMaxAge() > 0) + cookies.put(cookie.getName(), cookie.getValue()); + else + cookies.remove(cookie.getName()); + } + } + + if (!cookies.isEmpty()) + { + StringBuilder buff = new StringBuilder(); + for (Map.Entry entry : cookies.entrySet()) + { + if (buff.length() > 0) + buff.append("; "); + buff.append(entry.getKey()).append('=').append(entry.getValue()); + } + fields.add(new HttpField(HttpHeader.COOKIE, buff.toString())); + } + PushBuilder builder = new PushBuilderImpl(this, fields, getMethod(), getQueryString(), id); builder.addHeader("referer", getRequestURL().toString()); - // TODO process any set cookies - // TODO process any user_identity - return builder; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java index 5baaa6988ff..d3d4d265044 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServerConnector.java @@ -35,7 +35,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -83,6 +82,9 @@ public class ServerConnector extends AbstractNetworkConnector private volatile int _localPort = -1; private volatile int _acceptQueueSize = 0; private volatile boolean _reuseAddress = true; + private volatile boolean _acceptedTcpNoDelay = true; + private volatile int _acceptedReceiveBufferSize = -1; + private volatile int _acceptedSendBufferSize = -1; /** *

Construct a ServerConnector with a private instance of {@link HttpConnectionFactory} as the only factory.

@@ -397,7 +399,11 @@ public class ServerConnector extends AbstractNetworkConnector { try { - socket.setTcpNoDelay(true); + socket.setTcpNoDelay(_acceptedTcpNoDelay); + if (_acceptedReceiveBufferSize > -1) + socket.setReceiveBufferSize(_acceptedReceiveBufferSize); + if (_acceptedSendBufferSize > -1) + socket.setSendBufferSize(_acceptedSendBufferSize); } catch (SocketException e) { @@ -424,7 +430,7 @@ public class ServerConnector extends AbstractNetworkConnector return _localPort; } - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException { SocketChannelEndPoint endpoint = new SocketChannelEndPoint(channel, selectSet, key, getScheduler()); endpoint.setIdleTimeout(getIdleTimeout()); @@ -452,6 +458,7 @@ public class ServerConnector extends AbstractNetworkConnector * @return whether the server socket reuses addresses * @see ServerSocket#getReuseAddress() */ + @ManagedAttribute("Server Socket SO_REUSEADDR") public boolean getReuseAddress() { return _reuseAddress; @@ -466,6 +473,67 @@ public class ServerConnector extends AbstractNetworkConnector _reuseAddress = reuseAddress; } + /** + * @return whether the accepted socket gets {@link java.net.SocketOptions#TCP_NODELAY TCP_NODELAY} enabled. + * @see Socket#getTcpNoDelay() + */ + @ManagedAttribute("Accepted Socket TCP_NODELAY") + public boolean getAcceptedTcpNoDelay() + { + return _acceptedTcpNoDelay; + } + + /** + * @param tcpNoDelay whether {@link java.net.SocketOptions#TCP_NODELAY TCP_NODELAY} gets enabled on the the accepted socket. + * @see Socket#setTcpNoDelay(boolean) + */ + public void setAcceptedTcpNoDelay(boolean tcpNoDelay) + { + this._acceptedTcpNoDelay = tcpNoDelay; + } + + /** + * @return the {@link java.net.SocketOptions#SO_RCVBUF SO_RCVBUF} size to set onto the accepted socket. + * A value of -1 indicates that it is left to its default value. + * @see Socket#getReceiveBufferSize() + */ + @ManagedAttribute("Accepted Socket SO_RCVBUF") + public int getAcceptedReceiveBufferSize() + { + return _acceptedReceiveBufferSize; + } + + /** + * @param receiveBufferSize the {@link java.net.SocketOptions#SO_RCVBUF SO_RCVBUF} size to set onto the accepted socket. + * A value of -1 indicates that it is left to its default value. + * @see Socket#setReceiveBufferSize(int) + */ + public void setAcceptedReceiveBufferSize(int receiveBufferSize) + { + this._acceptedReceiveBufferSize = receiveBufferSize; + } + + /** + * @return the {@link java.net.SocketOptions#SO_SNDBUF SO_SNDBUF} size to set onto the accepted socket. + * A value of -1 indicates that it is left to its default value. + * @see Socket#getSendBufferSize() + */ + @ManagedAttribute("Accepted Socket SO_SNDBUF") + public int getAcceptedSendBufferSize() + { + return _acceptedSendBufferSize; + } + + /** + * @param sendBufferSize the {@link java.net.SocketOptions#SO_SNDBUF SO_SNDBUF} size to set onto the accepted socket. + * A value of -1 indicates that it is left to its default value. + * @see Socket#setSendBufferSize(int) + */ + public void setAcceptedSendBufferSize(int sendBufferSize) + { + this._acceptedSendBufferSize = sendBufferSize; + } + @Override public void setAccepting(boolean accepting) { @@ -511,9 +579,9 @@ public class ServerConnector extends AbstractNetworkConnector } @Override - protected ChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selectSet, SelectionKey selectionKey) throws IOException + protected SocketChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException { - return ServerConnector.this.newEndPoint((SocketChannel)channel, selectSet, selectionKey); + return ServerConnector.this.newEndPoint((SocketChannel)channel, selector, selectionKey); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java index 39f78844028..b9d6c1486a6 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SocketCustomizationListener.java @@ -59,18 +59,18 @@ public class SocketCustomizationListener implements Listener @Override public void onOpened(Connection connection) { - EndPoint endp = connection.getEndPoint(); + EndPoint endPoint = connection.getEndPoint(); boolean ssl = false; - if (_ssl && endp instanceof DecryptedEndPoint) + if (_ssl && endPoint instanceof DecryptedEndPoint) { - endp = ((DecryptedEndPoint)endp).getSslConnection().getEndPoint(); + endPoint = ((DecryptedEndPoint)endPoint).getSslConnection().getEndPoint(); ssl = true; } - if (endp instanceof SocketChannelEndPoint) + if (endPoint instanceof SocketChannelEndPoint) { - Socket socket = ((SocketChannelEndPoint)endp).getSocket(); + Socket socket = ((SocketChannelEndPoint)endPoint).getChannel().socket(); customize(socket, connection.getClass(), ssl); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java index 8b1a16d9c8c..d45008076e2 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java @@ -223,7 +223,15 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor LOG.debug("{} compressing {}", this, _deflater); _state.set(GZState.COMPRESSING); - gzip(content, complete, callback); + if (BufferUtil.isEmpty(content)) + { + // We are committing, but have no content to compress, so flush empty buffer to write headers. + _interceptor.write(BufferUtil.EMPTY_BUFFER, complete, callback); + } + else + { + gzip(content, complete, callback); + } } else callback.failed(new WritePendingException()); @@ -406,7 +414,7 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor @Override public String toString() { - return String.format("%s[content=%s last=%b copy=%s buffer=%s deflate=%s", + return String.format("%s[content=%s last=%b copy=%s buffer=%s deflate=%s %s]", super.toString(), BufferUtil.toDetailString(_content), _last, diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java index 60291739302..7cde76cdf40 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -45,7 +45,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -132,7 +131,7 @@ public class AsyncCompletionTest extends HttpServerTestFixture }) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java index 9afea19e7b5..d96a48b9dda 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ExtendedServerTest.java @@ -30,7 +30,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; @@ -61,7 +60,7 @@ public class ExtendedServerTest extends HttpServerTestBase }) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java index 93bf04a133b..c5ba725069a 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyConnectionTest.java @@ -151,6 +151,39 @@ public class ProxyConnectionTest assertThat(response, Matchers.containsString("remote=ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:12345")); } + @ParameterizedTest + @MethodSource("requestProcessors") + public void testLocalV2(RequestProcessor p) throws Exception + { + String proxy = + // Preamble + "0D0A0D0A000D0A515549540A" + + + // V2, LOCAL + "20" + + + // 0x1 : AF_INET 0x1 : STREAM. + "11" + + + // Address length is 16. + "0010" + + + // gibberish + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + ; + String http = "GET /path HTTP/1.1\n" + + "Host: server:80\n" + + "Connection: close\n" + + "\n"; + + String response = p.sendRequestWaitingForResponse(TypeUtil.fromHexString(proxy), http.getBytes(StandardCharsets.US_ASCII)); + + assertThat(response, Matchers.containsString("HTTP/1.1 200")); + assertThat(response, Matchers.containsString("pathInfo=/path")); + assertThat(response, Matchers.containsString("local=0.0.0.0:0")); + assertThat(response, Matchers.containsString("remote=0.0.0.0:0")); + } + @ParameterizedTest @MethodSource("requestProcessors") public void testMissingField(RequestProcessor p) throws Exception diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java new file mode 100644 index 00000000000..c5423a568c8 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java @@ -0,0 +1,178 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.util.TypeUtil; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class ProxyCustomizerTest +{ + private Server server; + + private ProxyResponse sendProxyRequest(String proxyAsHexString, String rawHttp) throws IOException + { + try (Socket socket = new Socket(server.getURI().getHost(), server.getURI().getPort())) + { + OutputStream output = socket.getOutputStream(); + output.write(TypeUtil.fromHexString(proxyAsHexString)); + output.write(rawHttp.getBytes(StandardCharsets.UTF_8)); + output.flush(); + socket.shutdownOutput(); + + StringBuilder sb = new StringBuilder(); + + InputStream input = socket.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + while (true) + { + String line = reader.readLine(); + if (line == null) + break; + sb.append(line).append("\r\n"); + } + + return new ProxyResponse((InetSocketAddress)socket.getLocalSocketAddress(), (InetSocketAddress)socket.getRemoteSocketAddress(), sb.toString()); + } + } + + private static class ProxyResponse + { + private final InetSocketAddress localSocketAddress; + private final InetSocketAddress remoteSocketAddress; + private final String httpResponse; + + public ProxyResponse(InetSocketAddress localSocketAddress, InetSocketAddress remoteSocketAddress, String httpResponse) + { + this.localSocketAddress = localSocketAddress; + this.remoteSocketAddress = remoteSocketAddress; + this.httpResponse = httpResponse; + } + } + + @BeforeEach + void setUp() throws Exception + { + Handler handler = new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + { + response.addHeader("preexisting.attribute", request.getAttribute("some.attribute").toString()); + ArrayList attributeNames = Collections.list(request.getAttributeNames()); + Collections.sort(attributeNames); + response.addHeader("attributeNames", String.join(",", attributeNames)); + + response.addHeader("localAddress", request.getLocalAddr() + ":" + request.getLocalPort()); + response.addHeader("remoteAddress", request.getRemoteAddr() + ":" + request.getRemotePort()); + Object localAddress = request.getAttribute(ProxyCustomizer.LOCAL_ADDRESS_ATTRIBUTE_NAME); + if (localAddress != null) + response.addHeader("proxyLocalAddress", localAddress.toString() + ":" + request.getAttribute(ProxyCustomizer.LOCAL_PORT_ATTRIBUTE_NAME)); + Object remoteAddress = request.getAttribute(ProxyCustomizer.REMOTE_ADDRESS_ATTRIBUTE_NAME); + if (remoteAddress != null) + response.addHeader("proxyRemoteAddress", remoteAddress.toString() + ":" + request.getAttribute(ProxyCustomizer.REMOTE_PORT_ATTRIBUTE_NAME)); + + baseRequest.setHandled(true); + } + }; + + server = new Server(); + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.addCustomizer((connector, channelConfig, request) -> request.setAttribute("some.attribute", "some value")); + httpConfiguration.addCustomizer(new ProxyCustomizer()); + ServerConnector connector = new ServerConnector(server, new ProxyConnectionFactory(), new HttpConnectionFactory(httpConfiguration)); + server.addConnector(connector); + server.setHandler(handler); + server.start(); + } + + @AfterEach + void tearDown() throws Exception + { + server.stop(); + server = null; + } + + @Test + void testProxyCustomizerWithProxyData() throws Exception + { + String proxy = + // Preamble + "0D0A0D0A000D0A515549540A" + + // V2, PROXY + "21" + + // 0x1 : AF_INET 0x1 : STREAM. Address length is 2*4 + 2*2 = 12 bytes. + "11" + + // length of remaining header (4+4+2+2 = 12) + "000C" + + // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port; + "01010001" + + "010100FE" + + "3039" + + "1F90"; + String http = "GET /1 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + + ProxyResponse response = sendProxyRequest(proxy, http); + + assertThat(response.httpResponse, Matchers.containsString("localAddress: 1.1.0.254:8080")); + assertThat(response.httpResponse, Matchers.containsString("remoteAddress: 1.1.0.1:12345")); + assertThat(response.httpResponse, Matchers.containsString("proxyLocalAddress: " + response.remoteSocketAddress.getAddress().getHostAddress() + ":" + response.remoteSocketAddress.getPort())); + assertThat(response.httpResponse, Matchers.containsString("proxyRemoteAddress: " + response.localSocketAddress.getAddress().getHostAddress() + ":" + response.localSocketAddress.getPort())); + assertThat(response.httpResponse, Matchers.containsString("preexisting.attribute: some value")); + assertThat(response.httpResponse, Matchers.containsString("attributeNames: org.eclipse.jetty.proxy.local.address,org.eclipse.jetty.proxy.local.port,org.eclipse.jetty.proxy.remote.address,org.eclipse.jetty.proxy.remote.port,some.attribute")); + } + + @Test + void testProxyCustomizerWithoutProxyData() throws Exception + { + String proxy = ""; + String http = "GET /1 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + + ProxyResponse response = sendProxyRequest(proxy, http); + + assertThat(response.httpResponse, Matchers.containsString("localAddress: " + response.remoteSocketAddress.getAddress().getHostAddress() + ":" + response.remoteSocketAddress.getPort())); + assertThat(response.httpResponse, Matchers.containsString("remoteAddress: " + response.localSocketAddress.getAddress().getHostAddress() + ":" + response.localSocketAddress.getPort())); + assertThat(response.httpResponse, Matchers.not(Matchers.containsString("proxyLocalAddress: "))); + assertThat(response.httpResponse, Matchers.not(Matchers.containsString("proxyRemoteAddress: "))); + assertThat(response.httpResponse, Matchers.containsString("preexisting.attribute: some value")); + assertThat(response.httpResponse, Matchers.containsString("attributeNames: some.attribute")); + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java index 45a536c7d19..25be0bd9631 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ProxyProtocolTest.java @@ -216,5 +216,81 @@ public class ProxyProtocolTest } } } - + + @Test + public void testProxyProtocolV2Local() throws Exception + { + start(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + } + }); + + try (Socket socket = new Socket("localhost", connector.getLocalPort())) + { + String proxy = + // Preamble + "0D0A0D0A000D0A515549540A" + + + // V2, LOCAL + "20" + + + // 0x1 : AF_INET 0x1 : STREAM. Address length is 2*4 + 2*2 = 12 bytes. + "11" + + + // length of remaining header (4+4+2+2+6+3 = 21) + "0015" + + + // uint32_t src_addr; uint32_t dst_addr; uint16_t src_port; uint16_t dst_port; + "C0A80001" + + "7f000001" + + "3039" + + "1F90" + + + // NOOP value 0 + "040000" + + + // NOOP value ABCDEF + "040003ABCDEF"; + + String request1 = + "GET /1 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + OutputStream output = socket.getOutputStream(); + output.write(TypeUtil.fromHexString(proxy)); + output.write(request1.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + InputStream input = socket.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + String response1 = reader.readLine(); + assertTrue(response1.startsWith("HTTP/1.1 200 ")); + while (true) + { + if (reader.readLine().isEmpty()) + break; + } + + // Send a second request to verify that the proxied IP is retained. + String request2 = + "GET /2 HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Connection: close\r\n" + + "\r\n"; + output.write(request2.getBytes(StandardCharsets.UTF_8)); + output.flush(); + + String response2 = reader.readLine(); + assertTrue(response2.startsWith("HTTP/1.1 200 ")); + while (true) + { + if (reader.readLine() == null) + break; + } + } + } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index e6c0a0ce660..a23c4c8aef4 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import java.security.Principal; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -45,10 +46,19 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import javax.servlet.http.Part; +import javax.servlet.http.PushBuilder; import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.http.tools.HttpTester; @@ -57,6 +67,9 @@ import org.eclipse.jetty.server.LocalConnector.LocalEndPoint; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.session.Session; +import org.eclipse.jetty.server.session.SessionData; +import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.BufferUtil; @@ -1860,11 +1873,128 @@ public class RequestTest assertNotNull(request.getParameterMap()); assertEquals(0, request.getParameterMap().size()); } + + @Test + public void testPushBuilder() throws Exception + { + String uri = "/foo/something"; + Request request = new TestRequest(null, null); + request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("good","thumbsup", 100), CookieCompliance.RFC6265)); + request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("bonza","bewdy", 1), CookieCompliance.RFC6265)); + request.getResponse().getHttpFields().add(new HttpCookie.SetCookieHttpField(new HttpCookie("bad", "thumbsdown", 0), CookieCompliance.RFC6265)); + HttpFields.Mutable fields = HttpFields.build(); + fields.add(HttpHeader.AUTHORIZATION, "Basic foo"); + request.setMetaData(new MetaData.Request("GET", HttpURI.from(uri), HttpVersion.HTTP_1_0, fields)); + assertTrue(request.isPushSupported()); + PushBuilder builder = request.newPushBuilder(); + assertNotNull(builder); + assertEquals("GET", builder.getMethod()); + assertThrows(NullPointerException.class, () -> + { + builder.method(null); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method(""); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method(" "); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("POST"); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("PUT"); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("DELETE"); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("CONNECT"); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("OPTIONS"); + }); + assertThrows(IllegalArgumentException.class, () -> + { + builder.method("TRACE"); + }); + assertEquals(TestRequest.TEST_SESSION_ID, builder.getSessionId()); + builder.path("/foo/something-else.txt"); + assertEquals("/foo/something-else.txt", builder.getPath()); + assertEquals("Basic foo", builder.getHeader("Authorization")); + assertThat(builder.getHeader("Cookie"), containsString("bonza")); + assertThat(builder.getHeader("Cookie"), containsString("good")); + assertThat(builder.getHeader("Cookie"), containsString("maxpos")); + assertThat(builder.getHeader("Cookie"), not(containsString("bad"))); + } interface RequestTester { boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException; } + + private class TestRequest extends Request + { + public static final String TEST_SESSION_ID = "abc123"; + Response _response = new Response(null, null); + Cookie c1; + Cookie c2; + + public TestRequest(HttpChannel channel, HttpInput input) + { + super(channel, input); + c1 = new Cookie("maxpos", "xxx"); + c1.setMaxAge(1); + c2 = new Cookie("maxneg", "yyy"); + c2.setMaxAge(-1); + } + + @Override + public boolean isPushSupported() + { + return true; + } + + @Override + public HttpSession getSession() + { + return new Session(new SessionHandler(), new SessionData(TEST_SESSION_ID, "", "0.0.0.0", 0, 0, 0, 300)); + } + + @Override + public Principal getUserPrincipal() + { + return new Principal() + { + + @Override + public String getName() + { + return "user"; + } + + }; + } + + @Override + public Response getResponse() + { + return _response; + } + + @Override + public Cookie[] getCookies() + { + return new Cookie[] {c1,c2}; + } + } private class RequestHandler extends AbstractHandler { diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java index 3fe424c2072..2f8bf9b9546 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ServerConnectorTest.java @@ -71,7 +71,7 @@ public class ServerConnectorTest EndPoint endPoint = baseRequest.getHttpChannel().getEndPoint(); assertThat("Endpoint", endPoint, instanceOf(SocketChannelEndPoint.class)); SocketChannelEndPoint channelEndPoint = (SocketChannelEndPoint)endPoint; - Socket socket = channelEndPoint.getSocket(); + Socket socket = channelEndPoint.getChannel().socket(); ServerConnector connector = (ServerConnector)baseRequest.getHttpChannel().getConnector(); PrintWriter out = response.getWriter(); @@ -214,7 +214,7 @@ public class ServerConnectorTest } @Test - public void testAddFirstConnectionFactory() throws Exception + public void testAddFirstConnectionFactory() { Server server = new Server(); ServerConnector connector = new ServerConnector(server); @@ -236,7 +236,7 @@ public class ServerConnectorTest public void testExceptionWhileAccepting() throws Exception { Server server = new Server(); - try (StacklessLogging stackless = new StacklessLogging(AbstractConnector.class)) + try (StacklessLogging ignored = new StacklessLogging(AbstractConnector.class)) { AtomicLong spins = new AtomicLong(); ServerConnector connector = new ServerConnector(server, 1, 1) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java index 668494a0566..6eb21fd2bbc 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SniSslConnectionFactoryTest.java @@ -300,6 +300,29 @@ public class SniSslConnectionFactoryTest assertThat(response.getStatus(), is(400)); } + @Test + public void testWrongSNIRejectedConnectionWithNonSNIKeystore() throws Exception + { + start(ssl -> + { + // Keystore has only one certificate, but we want to enforce SNI. + ssl.setKeyStorePath("src/test/resources/keystore.p12"); + ssl.setSniRequired(true); + }); + + // Wrong SNI host. + assertThrows(SSLHandshakeException.class, () -> getResponse("wrong.com", "wrong.com", null)); + + // No SNI host. + assertThrows(SSLHandshakeException.class, () -> getResponse(null, "wrong.com", null)); + + // Good SNI host. + HttpTester.Response response = HttpTester.parseResponse(getResponse("localhost", "localhost", null)); + + assertNotNull(response); + assertThat(response.getStatus(), is(200)); + } + @Test public void testSameConnectionRequestsForManyDomains() throws Exception { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/FormTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/FormTest.java index f6789252dbc..820b5d62dac 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/FormTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/FormTest.java @@ -136,7 +136,7 @@ public class FormTest ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .method(HttpMethod.POST) .path(contextPath + servletPath) - .header(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString()) + .headers(headers -> headers.put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.FORM_ENCODED.asString())) .body(content) .onRequestBegin(request -> { diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java index 2f6609016a5..2739bbe77d7 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerBreakEvenSizeTest.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.gzip.GzipHandler; @@ -85,7 +86,7 @@ public class GzipHandlerBreakEvenSizeTest { URI uri = server.getURI().resolve("/content?size=" + size); ContentResponse response = client.newRequest(uri) - .header(HttpHeader.ACCEPT_ENCODING, "gzip") + .headers(headers -> headers.put(HttpHeader.ACCEPT_ENCODING, HttpHeaderValue.GZIP)) .send(); assertThat("Status Code", response.getStatus(), is(200)); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java new file mode 100644 index 00000000000..ca7a4ee4e28 --- /dev/null +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/GzipHandlerCommitTest.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlet; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GzipHandlerCommitTest +{ + private Server server; + private HttpClient client; + + public void start(Servlet servlet) throws Exception + { + server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(0); + server.addConnector(connector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/"); + ServletHolder servletHolder = new ServletHolder(servlet); + contextHandler.addServlet(servletHolder, "/test/*"); + + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + client = new HttpClient(); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @Test + public void testImmediateFlushNoContent() throws Exception + { + CountDownLatch latch = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.flushBuffer(); + assertDoesNotThrow(() -> assertTrue(latch.await(1, TimeUnit.SECONDS))); + } + }); + + URI uri = server.getURI().resolve("/test/"); + Request request = client.newRequest(uri); + request.header(HttpHeader.CONNECTION, "Close"); + request.onResponseHeaders((r) -> latch.countDown()); + ContentResponse response = request.send(); + assertThat("Response status", response.getStatus(), is(200)); + } + + @Test + public void testImmediateFlushWithContent() throws Exception + { + int size = 8000; + CountDownLatch latch = new CountDownLatch(1); + start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + response.flushBuffer(); + assertDoesNotThrow(() -> assertTrue(latch.await(1, TimeUnit.SECONDS))); + response.getOutputStream(); + byte[] buf = new byte[size]; + Arrays.fill(buf, (byte)'a'); + response.getOutputStream().write(buf); + } + }); + + URI uri = server.getURI().resolve("/test/"); + Request request = client.newRequest(uri); + request.header(HttpHeader.CONNECTION, "Close"); + request.onResponseHeaders((r) -> latch.countDown()); + ContentResponse response = request.send(); + assertThat("Response status", response.getStatus(), is(200)); + assertThat("Response content size", response.getContent().length, is(size)); + } +} diff --git a/jetty-servlet/src/test/resources/jetty-logging.properties b/jetty-servlet/src/test/resources/jetty-logging.properties index 821cf3d2f35..bd3b391a3dc 100644 --- a/jetty-servlet/src/test/resources/jetty-logging.properties +++ b/jetty-servlet/src/test/resources/jetty-logging.properties @@ -1,8 +1,7 @@ # Jetty Logging using jetty-slf4j-impl -org.eclipse.jetty.LEVEL=INFO #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.server.LEVEL=DEBUG #org.eclipse.jetty.servlet.LEVEL=DEBUG -#org.eclipse.jetty.io.ChannelEndPoint.LEVEL=DEBUG +#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.server.DebugListener.LEVEL=DEBUG -#org.eclipse.jetty.server.HttpChannelState.LEVEL=DEBUG \ No newline at end of file +#org.eclipse.jetty.server.HttpChannelState.LEVEL=DEBUG diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java new file mode 100644 index 00000000000..0857532c93c --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractFileContentServlet.java @@ -0,0 +1,46 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.servlet.http.HttpServlet; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class AbstractFileContentServlet extends HttpServlet +{ + protected byte[] loadContentFileBytes(final String fileName) throws IOException + { + String relPath = fileName; + relPath = relPath.replaceFirst("^/context/", ""); + relPath = relPath.replaceFirst("^/", ""); + + String realPath = getServletContext().getRealPath(relPath); + assertNotNull(realPath, "Unable to find real path for " + relPath); + + Path realFile = Paths.get(realPath); + assertTrue(Files.exists(realFile), "Content File should exist: " + realFile); + + return Files.readAllBytes(realFile); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java new file mode 100644 index 00000000000..b47970ae58e --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AbstractGzipTest.java @@ -0,0 +1,159 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.IO; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.TypeUtil; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public abstract class AbstractGzipTest +{ + protected static final int DEFAULT_OUTPUT_BUFFER_SIZE = new HttpConfiguration().getOutputBufferSize(); + + protected Path workDir; + + public AbstractGzipTest() + { + workDir = MavenTestingUtils.getTargetTestingPath(this.getClass().getName()); + FS.ensureEmpty(workDir); + } + + protected FilterInputStream newContentEncodingFilterInputStream(String contentEncoding, InputStream inputStream) throws IOException + { + if (contentEncoding == null) + { + return new FilterInputStream(inputStream) {}; + } + else if (contentEncoding.contains(GzipHandler.GZIP)) + { + return new GZIPInputStream(inputStream); + } + else if (contentEncoding.contains(GzipHandler.DEFLATE)) + { + return new InflaterInputStream(inputStream, new Inflater(true)); + } + throw new RuntimeException("Unexpected response content-encoding: " + contentEncoding); + } + + protected UncompressedMetadata parseResponseContent(HttpTester.Response response) throws NoSuchAlgorithmException, IOException + { + UncompressedMetadata metadata = new UncompressedMetadata(); + metadata.contentLength = response.getContentBytes().length; + + String contentEncoding = response.get("Content-Encoding"); + MessageDigest digest = MessageDigest.getInstance("SHA1"); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(response.getContentBytes()); + FilterInputStream streamFilter = newContentEncodingFilterInputStream(contentEncoding, bais); + ByteArrayOutputStream uncompressedStream = new ByteArrayOutputStream(metadata.contentLength); + DigestOutputStream digester = new DigestOutputStream(uncompressedStream, digest)) + { + IO.copy(streamFilter, digester); + metadata.uncompressedContent = uncompressedStream.toByteArray(); + metadata.uncompressedSize = metadata.uncompressedContent.length; + // Odd toUpperCase is because TypeUtil.toHexString is mixed case results!?? + metadata.uncompressedSha1Sum = TypeUtil.toHexString(digest.digest()).toUpperCase(Locale.ENGLISH); + return metadata; + } + } + + protected Path createFile(Path contextDir, String fileName, int fileSize) throws IOException + { + Path destPath = contextDir.resolve(fileName); + byte[] content = generateContent(fileSize); + Files.write(destPath, content, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + return destPath; + } + + /** + * Generate semi-realistic text content of arbitrary length. + *

+ * Note: We don't just create a single string of repeating characters + * as that doesn't test the gzip behavior very well. (too efficient) + * We also don't just generate a random byte array as that is the opposite + * extreme of gzip handling (terribly inefficient). + *

+ * + * @param length the length of the content to generate. + * @return the content. + */ + private byte[] generateContent(int length) + { + StringBuilder builder = new StringBuilder(); + do + { + builder.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.\n"); + builder.append("Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque\n"); + builder.append("habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n"); + builder.append("Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam\n"); + builder.append("at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate\n"); + builder.append("velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.\n"); + builder.append("Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum\n"); + builder.append("eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa\n"); + builder.append("sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam\n"); + builder.append("consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.\n"); + builder.append("Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse\n"); + builder.append("et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.\n"); + } + while (builder.length() < length); + + // Make sure we are exactly at requested length. (truncate the extra) + if (builder.length() > length) + { + builder.setLength(length); + } + + return builder.toString().getBytes(UTF_8); + } + + public static class UncompressedMetadata + { + public byte[] uncompressedContent; + public int contentLength; + public String uncompressedSha1Sum; + public int uncompressedSize; + + public String getContentUTF8() + { + return new String(uncompressedContent, UTF_8); + } + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java index 7732af42f47..869f24e94a5 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncScheduledDispatchWrite.java @@ -30,7 +30,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial") -public abstract class AsyncScheduledDispatchWrite extends TestDirContentServlet +public abstract class AsyncScheduledDispatchWrite extends AbstractFileContentServlet { public static class Default extends AsyncScheduledDispatchWrite { @@ -102,7 +102,7 @@ public abstract class AsyncScheduledDispatchWrite extends TestDirContentServlet } else { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java index f2e1ebd3006..ab25a4c4e44 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutCompleteWrite.java @@ -43,7 +43,7 @@ import static org.hamcrest.Matchers.nullValue; * */ @SuppressWarnings("serial") -public abstract class AsyncTimeoutCompleteWrite extends TestDirContentServlet implements AsyncListener +public abstract class AsyncTimeoutCompleteWrite extends AbstractFileContentServlet implements AsyncListener { public static class Default extends AsyncTimeoutCompleteWrite { @@ -86,7 +86,7 @@ public abstract class AsyncTimeoutCompleteWrite extends TestDirContentServlet im // Pass Request & Response ctx = request.startAsync(request, response); } - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); request.setAttribute("filename", fileName); ctx.addListener(this); ctx.setTimeout(20); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java index 8ad05d75ead..567f86e259c 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/AsyncTimeoutDispatchWrite.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial") -public class AsyncTimeoutDispatchWrite extends TestDirContentServlet implements AsyncListener +public class AsyncTimeoutDispatchWrite extends AbstractFileContentServlet implements AsyncListener { public static class Default extends AsyncTimeoutDispatchWrite { @@ -77,7 +77,7 @@ public class AsyncTimeoutDispatchWrite extends TestDirContentServlet implements else { // second pass through, as result of timeout -> dispatch - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java index 618a8a3dba6..ab37d9d6afe 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthStreamTypeWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthStreamTypeWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletLengthStreamTypeWrite extends TestDirContentServlet +public class BlockingServletLengthStreamTypeWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java index 65c0184b133..9ff632ed7a9 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletLengthTypeStreamWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletLengthTypeStreamWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletLengthTypeStreamWrite extends TestDirContentServlet +public class BlockingServletLengthTypeStreamWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java index b83d56a3e67..063cb6a9a82 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamLengthTypeWrite extends TestDirContentServlet +public class BlockingServletStreamLengthTypeWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java similarity index 94% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java index 45901e5fe42..3fa9b757615 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamLengthTypeWriteWithFlush.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamLengthTypeWriteWithFlush.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamLengthTypeWriteWithFlush extends TestDirContentServlet +public class BlockingServletStreamLengthTypeWriteWithFlush extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java index 2ccc85c2d7d..7c793b8f25a 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletStreamTypeLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletStreamTypeLengthWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletStreamTypeLengthWrite extends TestDirContentServlet +public class BlockingServletStreamTypeLengthWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java index f3ee50fe588..0af3dc38eac 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeLengthStreamWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeLengthStreamWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletTypeLengthStreamWrite extends TestDirContentServlet +public class BlockingServletTypeLengthStreamWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); if (fileName.endsWith("txt")) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java similarity index 93% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java index 21ec41b19fc..0c622d54667 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletTypeStreamLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/BlockingServletTypeStreamLengthWrite.java @@ -42,12 +42,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletTypeStreamLengthWrite extends TestDirContentServlet +public class BlockingServletTypeStreamLengthWrite extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); if (fileName.endsWith("txt")) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java index fda091b8e7c..3b7134a56ae 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipContentLengthTest.java @@ -18,20 +18,27 @@ package org.eclipse.jetty.servlets; -import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import javax.servlet.Servlet; import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlets.GzipTester.ContentMetadata; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.extension.ExtendWith; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.hamcrest.Matcher; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -42,317 +49,178 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; /** - * Test the GzipHandler support for Content-Length setting variations. - * - * @see http://bugs.eclipse.org/354014 + * Test the {@code GzipHandler} support for the various ways that an App can set {@code Content-Length}. */ -@ExtendWith(WorkDirExtension.class) -public class GzipContentLengthTest +public class GzipContentLengthTest extends AbstractGzipTest { - public WorkDir workDir; - - private static final HttpConfiguration defaultHttp = new HttpConfiguration(); - private static final int LARGE = defaultHttp.getOutputBufferSize() * 8; - private static final int MEDIUM = defaultHttp.getOutputBufferSize(); - private static final int SMALL = defaultHttp.getOutputBufferSize() / 4; - private static final int TINY = GzipHandler.DEFAULT_MIN_GZIP_SIZE / 2; - private static final boolean EXPECT_COMPRESSED = true; + enum GzipMode + { + INTERNAL, EXTERNAL + } public static Stream scenarios() { - List ret = new ArrayList<>(); + // The list of servlets that implement various content sending behaviors + // some behaviors are more sane then others, but they are all real world scenarios + // that we have seen or had issues reported against Jetty. + List> servlets = new ArrayList<>(); - ret.add(new Scenario(0, "empty.txt", !EXPECT_COMPRESSED)); - ret.add(new Scenario(TINY, "file-tiny.txt", !EXPECT_COMPRESSED)); - ret.add(new Scenario(SMALL, "file-small.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(SMALL, "file-small.mp3", !EXPECT_COMPRESSED)); - ret.add(new Scenario(MEDIUM, "file-med.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(MEDIUM, "file-medium.mp3", !EXPECT_COMPRESSED)); - ret.add(new Scenario(LARGE, "file-large.txt", EXPECT_COMPRESSED)); - ret.add(new Scenario(LARGE, "file-large.mp3", !EXPECT_COMPRESSED)); + // AsyncContext create -> timeout -> onTimeout -> write-response -> complete + servlets.add(AsyncTimeoutCompleteWrite.Default.class); + servlets.add(AsyncTimeoutCompleteWrite.Passed.class); + // AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response + servlets.add(AsyncTimeoutDispatchWrite.Default.class); + servlets.add(AsyncTimeoutDispatchWrite.Passed.class); + // AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response + servlets.add(AsyncScheduledDispatchWrite.Default.class); + servlets.add(AsyncScheduledDispatchWrite.Passed.class); - return ret.stream().map(Arguments::of); - } + // HttpOutput usage scenario from http://bugs.eclipse.org/450873 + // 1. getOutputStream() + // 2. setHeader(content-type) + // 3. setHeader(content-length) + // 4. (unwrapped) HttpOutput.write(ByteBuffer) + servlets.add(HttpOutputWriteFileContentServlet.class); - private void testWithGzip(Scenario scenario, Class contentServlet) throws Exception - { - GzipTester tester = new GzipTester(workDir.getPath(), GzipHandler.GZIP); + // The following blocking scenarios are from http://bugs.eclipse.org/354014 + // Blocking + // 1. setHeader(content-length) + // 2. getOutputStream() + // 3. setHeader(content-type) + // 4. outputStream.write() + servlets.add(BlockingServletLengthStreamTypeWrite.class); + // Blocking + // 1. setHeader(content-length) + // 2. setHeader(content-type) + // 3. getOutputStream() + // 4. outputStream.write() + servlets.add(BlockingServletLengthTypeStreamWrite.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-length) + // 3. setHeader(content-type) + // 4. outputStream.write() + servlets.add(BlockingServletStreamLengthTypeWrite.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-length) + // 3. setHeader(content-type) + // 4. outputStream.write() (with frequent response flush) + servlets.add(BlockingServletStreamLengthTypeWriteWithFlush.class); + // Blocking + // 1. getOutputStream() + // 2. setHeader(content-type) + // 3. setHeader(content-length) + // 4. outputStream.write() + servlets.add(BlockingServletStreamTypeLengthWrite.class); + // Blocking + // 1. setHeader(content-type) + // 2. setHeader(content-length) + // 3. getOutputStream() + // 4. outputStream.write() + servlets.add(BlockingServletTypeLengthStreamWrite.class); + // Blocking + // 1. setHeader(content-type) + // 2. getOutputStream() + // 3. setHeader(content-length) + // 4. outputStream.write() + servlets.add(BlockingServletTypeStreamLengthWrite.class); - // Add AsyncGzip Configuration - tester.getGzipHandler().setIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setIncludedPaths("*.txt", "*.mp3"); + List scenarios = new ArrayList<>(); - // Add content servlet - tester.setContentServlet(contentServlet); - - try + for (Class servlet : servlets) { - String testFilename = String.format("%s-%s", contentServlet.getSimpleName(), scenario.fileName); - File testFile = tester.prepareServerFile(testFilename, scenario.fileSize); - - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/" + testFile.getName(), 5, TimeUnit.SECONDS); - - if (response.getStatus() != 200) - System.err.println("DANG!!!! " + response); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - - if (scenario.expectCompressed) + for (GzipMode gzipMode : GzipMode.values()) { - // Must be gzip compressed - assertThat("Content-Encoding", response.get("Content-Encoding"), containsString(GzipHandler.GZIP)); + // Not compressible (not large enough) + scenarios.add(Arguments.of(gzipMode, servlet, 0, "empty.txt", false)); + scenarios.add(Arguments.of(gzipMode, servlet, GzipHandler.DEFAULT_MIN_GZIP_SIZE / 2, "file-tiny.txt", false)); + + // Compressible. + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE / 2, "file-small.txt", true)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE, "file-medium.txt", true)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE * 4, "file-large.txt", true)); + + // Not compressible (not a matching Content-Type) + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE / 2, "file-small.mp3", false)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE, "file-medium.mp3", false)); + scenarios.add(Arguments.of(gzipMode, servlet, DEFAULT_OUTPUT_BUFFER_SIZE * 4, "file-large.mp3", false)); } - else - { - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(GzipHandler.GZIP))); - } - - // Uncompressed content Size - ContentMetadata content = tester.getResponseMetadata(response); - assertThat("(Uncompressed) Content Length", content.size, is((long)scenario.fileSize)); } - finally + + return scenarios.stream(); + } + + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @MethodSource("scenarios") + public void executeScenario(GzipMode gzipMode, Class contentServlet, int fileSize, String fileName, boolean compressible) throws Exception + { + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.addServlet(contentServlet, "/*"); + GzipHandler gzipHandler = new GzipHandler(); + + switch (gzipMode) { - tester.stop(); + case INTERNAL: + servletContextHandler.insertHandler(gzipHandler); + server.setHandler(servletContextHandler); + break; + case EXTERNAL: + gzipHandler.setHandler(servletContextHandler); + server.setHandler(gzipHandler); + break; } - } - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> write-response -> complete - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutCompleteWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutCompleteWrite.Default.class); - } + Path file = createFile(contextDir, fileName, fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> write-response -> complete - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutCompleteWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutCompleteWrite.Passed.class); - } + server.start(); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutDispatchWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutDispatchWrite.Default.class); - } + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/" + file.getFileName().toString()); - /** - * Test with content servlet that does: - * AsyncContext create -> timeout -> onTimeout -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncTimeoutDispatchWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncTimeoutDispatchWrite.Passed.class); - } + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); - /** - * Test with content servlet that does: - * AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncScheduledDispatchWriteDefault(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncScheduledDispatchWrite.Default.class); - } + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); - /** - * Test with content servlet that does: - * AsyncContext create -> no-timeout -> scheduler.schedule -> dispatch -> write-response - * - * @throws Exception on test failure - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testAsyncScheduledDispatchWritePassed(Scenario scenario) throws Exception - { - testWithGzip(scenario, AsyncScheduledDispatchWrite.Passed.class); - } + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - /** - * Test with content servlet that does: - * 1) setHeader(content-length) - * 2) getOutputStream() - * 3) setHeader(content-type) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletLengthStreamTypeWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletLengthStreamTypeWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-length) - * 2) setHeader(content-type) - * 3) getOutputStream() - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletLengthTypeStreamWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletLengthTypeStreamWrite.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-length) - * 3) setHeader(content-type) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamLengthTypeWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamLengthTypeWrite.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-length) - * 3) setHeader(content-type) - * 4) outputStream.write() (with frequent response flush) - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamLengthTypeWriteWithFlush(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamLengthTypeWriteWithFlush.class); - } - - /** - * Test with content servlet that does: - * 1) getOutputStream() - * 2) setHeader(content-type) - * 3) setHeader(content-length) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletStreamTypeLengthWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletStreamTypeLengthWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-type) - * 2) setHeader(content-length) - * 3) getOutputStream() - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see Eclipse Bug 354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletTypeLengthStreamWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletTypeLengthStreamWrite.class); - } - - /** - * Test with content servlet that does: - * 1) setHeader(content-type) - * 2) getOutputStream() - * 3) setHeader(content-length) - * 4) outputStream.write() - * - * @throws Exception on test failure - * @see http://bugs.eclipse.org/354014 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testServletTypeStreamLengthWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletTypeStreamLengthWrite.class); - } - - /** - * Test with content servlet that does: - * 2) getOutputStream() - * 1) setHeader(content-type) - * 3) setHeader(content-length) - * 4) (unwrapped) HttpOutput.write(ByteBuffer) - * - * This is done to demonstrate a bug with using HttpOutput.write() - * while also using GzipFilter - * - * @throws Exception on test failure - * @see Eclipse Bug 450873 - */ - @ParameterizedTest - @MethodSource("scenarios") - public void testHttpOutputWrite(Scenario scenario) throws Exception - { - testWithGzip(scenario, TestServletBufferTypeLengthWrite.class); - } - - public static class Scenario - { - final int fileSize; - final String fileName; - final boolean expectCompressed; - - public Scenario(int fileSize, String fileName, boolean expectCompressed) + // Response Content-Encoding check + Matcher contentEncodingMatcher = containsString(GzipHandler.GZIP); + if (!compressible) { - this.fileSize = fileSize; - this.fileName = fileName; - this.expectCompressed = expectCompressed; + contentEncodingMatcher = not(contentEncodingMatcher); } + assertThat("Content-Encoding", response.get("Content-Encoding"), contentEncodingMatcher); - @Override - public String toString() - { - return String.format("%s [%,d bytes, compressed=%b]", fileName, fileSize, expectCompressed); - } + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); } } diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java deleted file mode 100644 index 28226f3d69a..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultNoRecompressTest.java +++ /dev/null @@ -1,100 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.stream.Stream; - -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -/** - * Tests {@link GzipHandler} in combination with {@link DefaultServlet} for ability to configure {@link GzipHandler} to - * ignore recompress situations from upstream. - */ -@ExtendWith(WorkDirExtension.class) -public class GzipDefaultNoRecompressTest -{ - public static Stream data() - { - return Arrays.asList(new Object[][] - { - // Some already compressed files - {"test_quotes.gz", "application/gzip", GzipHandler.GZIP}, - {"test_quotes.br", "application/brotli", GzipHandler.GZIP}, - {"test_quotes.bz2", "application/bzip2", GzipHandler.GZIP}, - {"test_quotes.zip", "application/zip", GzipHandler.GZIP}, - {"test_quotes.rar", "application/x-rar-compressed", GzipHandler.GZIP}, - // Some images (common first) - {"jetty_logo.png", "image/png", GzipHandler.GZIP}, - {"jetty_logo.gif", "image/gif", GzipHandler.GZIP}, - {"jetty_logo.jpeg", "image/jpeg", GzipHandler.GZIP}, - {"jetty_logo.jpg", "image/jpeg", GzipHandler.GZIP}, - // Lesser encountered images (usually found being requested from non-browser clients) - {"jetty_logo.bmp", "image/bmp", GzipHandler.GZIP}, - {"jetty_logo.tif", "image/tiff", GzipHandler.GZIP}, - {"jetty_logo.tiff", "image/tiff", GzipHandler.GZIP}, - {"jetty_logo.xcf", "image/xcf", GzipHandler.GZIP}, - {"jetty_logo.jp2", "image/jpeg2000", GzipHandler.GZIP}, - //qvalue disables compression - {"test_quotes.txt", "text/plain", GzipHandler.GZIP + ";q=0"}, - {"test_quotes.txt", "text/plain", GzipHandler.GZIP + "; q = 0 "} - }).stream().map(Arguments::of); - } - - public WorkDir testingdir; - - @ParameterizedTest - @MethodSource("data") - public void testNotGzipHandleredDefaultAlreadyCompressed(String alreadyCompressedFilename, String expectedContentType, String compressionType) throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - copyTestFileToServer(alreadyCompressedFilename); - - tester.setContentServlet(TestStaticMimeTypeServlet.class); - - try - { - tester.start(); - tester.assertIsResponseNotGziped(alreadyCompressedFilename, alreadyCompressedFilename + ".sha1", expectedContentType); - } - finally - { - tester.stop(); - } - } - - private void copyTestFileToServer(String testFilename) throws IOException - { - File testFile = MavenTestingUtils.getTestResourceFile(testFilename); - File outFile = testingdir.getPathFile(testFilename).toFile(); - IO.copy(testFile, outFile); - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java new file mode 100644 index 00000000000..ad151c0654e --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletDeferredContentTypeTest.java @@ -0,0 +1,138 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +/** + * GzipHandler setting of headers when reset and/or not compressed. + * + * The GzipHandler now sets deferred headers (content-length and etag) when it decides not to commit. + * Also does not allow a reset after a decision to commit + * + * Originally from http://bugs.eclipse.org/408909 + */ +public class GzipDefaultServletDeferredContentTypeTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @Test + public void testIsNotGzipCompressedByDeferredContentType() throws Exception + { + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", new DefaultServlet() + { + @Override + public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException + { + String uri = req.getRequestURI(); + if (uri.endsWith(".deferred")) + { + // System.err.println("type for "+uri.substring(0,uri.length()-9)+" is "+getServletContext().getMimeType(uri.substring(0,uri.length()-9))); + resp.setContentType(getServletContext().getMimeType(uri.substring(0, uri.length() - 9))); + } + + doGet(req, resp); + } + }); + servletContextHandler.addServlet(holder, "/"); + + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setHandler(servletContextHandler); + server.setHandler(gzipHandler); + + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + + Path file = createFile(contextDir, "file.mp3.deferred", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.mp3.deferred"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + + // Response Vary check + assertThat("Response[Vary]", response.get("Vary"), is(emptyOrNullString())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java new file mode 100644 index 00000000000..958058db75c --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultServletTest.java @@ -0,0 +1,1182 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.CompressedContentFormat; +import org.eclipse.jetty.http.DateGenerator; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.startsWith; + +/** + * Test the GzipHandler support when working with the {@link DefaultServlet}. + */ +public class GzipDefaultServletTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @ValueSource(strings = {"POST", "WIBBLE", "GET", "HEAD"}) + public void testIsGzipByMethod(String method) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setIncludedMethods("POST", "WIBBLE", "GET", "HEAD"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", WibbleDefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 8; + + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod(method); // The point of this test + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); + // A HEAD request should have similar headers, but no body + if (method.equals("HEAD")) + { + assertThat("Response[Content-Length]", response.get("Content-Length"), is(not(nullValue()))); + } + else + { + assertThat("Response[Content-Length]", response.get("Content-Length"), is(nullValue())); + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + } + + public static class WibbleDefaultServlet extends DefaultServlet + { + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + switch (req.getMethod()) + { + case "WIBBLE": + // Disregard the method given, use GET instead. + doGet(req, resp); + return; + default: + super.service(req, resp); + } + } + } + + @Test + public void testIsGzipCompressedEmpty() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = 0; + createFile(contextDir, "file.txt", fileSize); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(0)); + } + + public static Stream compressibleSizes() + { + return Stream.of( + DEFAULT_OUTPUT_BUFFER_SIZE / 4, + DEFAULT_OUTPUT_BUFFER_SIZE, + DEFAULT_OUTPUT_BUFFER_SIZE * 4); + } + + @ParameterizedTest + @MethodSource("compressibleSizes") + public void testIsGzipCompressed(int fileSize) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @ParameterizedTest + @MethodSource("compressibleSizes") + public void testIsGzipCompressedIfModifiedSince(int fileSize) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + long fourSecondsAgo = TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000; + request.setHeader("If-Modified-Since", DateGenerator.formatDate(fourSecondsAgo)); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testGzippedIfSVG() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("image/svg+xml"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath("test.svg"); + Path file = contextDir.resolve("test.svg"); + IO.copy(testResource.toFile(), file.toFile()); + String expectedSha1Sum = Sha1Sum.calculate(testResource); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test.svg"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testNotGzipedIfNotModified() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + createFile(contextDir, "file.txt", fileSize); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + long fiveMinutesLater = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); + request.setHeader("If-Modified-Since", DateGenerator.formatDate(fiveMinutesLater)); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.NOT_MODIFIED_304)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[ETag]", response.get("ETag"), startsWith("W/")); + assertThat("Response[ETag]", response.get("ETag"), not(containsString(CompressedContentFormat.GZIP._etag))); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(0)); + } + + /** + * Gzip incorrectly gzips when {@code Accept-Encoding: gzip; q=0}. + * + *

+ * A quality of 0 results in no compression. + *

+ * + * See: http://bugs.eclipse.org/388072 + */ + @Test + public void testIsNotGzipCompressedWithZeroQ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE / 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip; q=0"); // TESTING THIS + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsGzipCompressedWithQ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE / 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "something; q=0.1, gzip; q=0.5"); // TESTING THIS + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), containsString("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByContentType() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.mp3", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.mp3"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByExcludedContentType() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addExcludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIsNotGzipCompressedByExcludedContentTypeWithCharset() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addExcludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.getMimeTypes().addMimeMapping("txt", "text/plain;charset=UTF-8"); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + holder.setInitParameter("etags", "true"); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "test_quotes.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test_quotes.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionNoUAProvided() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "foo"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + // INTENTIONALLY NOT SET - request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionUAMatch() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "foo"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionDefault() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "Some MSIE 6.0 user-agent"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testUserAgentExclusionByExcludedAgentPatterns() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns("bar", "fo.*"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setHeader("User-Agent", "foo"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testExcludePaths() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedPaths("*.txt"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + int fileSize = DEFAULT_OUTPUT_BUFFER_SIZE * 4; + Path file = createFile(contextDir, "file.txt", fileSize); + String expectedSha1Sum = Sha1Sum.calculate(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } + + @Test + public void testIncludedPaths() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setExcludedPaths("/bad.txt"); + gzipHandler.setIncludedPaths("*.txt"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path fileGood = createFile(contextDir, "file.txt", DEFAULT_OUTPUT_BUFFER_SIZE * 4); + Path fileBad = createFile(contextDir, "bad.txt", DEFAULT_OUTPUT_BUFFER_SIZE * 2); + String expectedGoodSha1Sum = Sha1Sum.calculate(fileGood); + String expectedBadSha1Sum = Sha1Sum.calculate(fileBad); + + server.start(); + + // Test Request 1 + { + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/file.txt"); + + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding, User-Agent")); + + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is((int)Files.size(fileGood))); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedGoodSha1Sum)); + } + + // Test Request 2 + { + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/bad.txt"); + + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + UncompressedMetadata metadata = parseResponseContent(response); + int fileSize = (int)Files.size(fileBad); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedBadSha1Sum)); + } + } + + @Test + public void testIsNotGzipCompressedSVGZ() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + ServletHolder holder = new ServletHolder("default", DefaultServlet.class); + servletContextHandler.addServlet(holder, "/"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath("test.svgz"); + Path file = contextDir.resolve("test.svgz"); + IO.copy(testResource.toFile(), file.toFile()); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/test.svgz"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Header checks + assertThat("Response[Content-Type]", response.get("Content-Type"), containsString("image/svg+xml")); + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java deleted file mode 100644 index a30150100c7..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipDefaultTest.java +++ /dev/null @@ -1,765 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.CompressedContentFormat; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.StringUtil; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -/** - * Test the GzipHandler support built into the {@link DefaultServlet} - */ -@ExtendWith(WorkDirExtension.class) -public class GzipDefaultTest -{ - private String compressionType; - - public GzipDefaultTest() - { - this.compressionType = GzipHandler.GZIP; - } - - @SuppressWarnings("serial") - public static class HttpStatusServlet extends HttpServlet - { - private int _status = 204; - - public HttpStatusServlet() - { - super(); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.setStatus(_status); - resp.setHeader("ETag", "W/\"204\""); - } - } - - @SuppressWarnings("serial") - public static class HttpErrorServlet extends HttpServlet - { - private int _status = 400; - - public HttpErrorServlet() - { - super(); - } - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.getOutputStream().write("error message".getBytes()); - resp.setStatus(_status); - } - } - - @SuppressWarnings("serial") - public static class HttpContentTypeWithEncoding extends HttpServlet - { - public static final String COMPRESSED_CONTENT = "

COMPRESSABLE CONTENT

" + - "This content must be longer than the default min gzip length, which is 256 bytes. " + - "The moon is blue to a fish in love. How now brown cow. The quick brown fox jumped over the lazy dog. A woman needs a man like a fish needs a bicycle!" + - ""; - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException - { - resp.setContentType("text/plain;charset=UTF8"); - resp.setStatus(200); - ServletOutputStream out = resp.getOutputStream(); - out.print(COMPRESSED_CONTENT); - } - } - - public WorkDir testingdir; - - @Test - public void testIsGzipByMethod() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setIncludedMethods("POST", "WIBBLE", "HEAD"); - - // Prepare Server File - int filesize = tester.getOutputBufferSize() * 2; - tester.prepareServerFile("file.txt", filesize); - - // Content Servlet - tester.setContentServlet(GetServlet.class); - - try - { - tester.start(); - HttpTester.Response response; - - //These methods have content bodies of the compressed response - tester.assertIsResponseGzipCompressed("POST", "file.txt"); - tester.assertIsResponseGzipCompressed("WIBBLE", "file.txt"); - - //A HEAD request should have similar headers, but no body - response = tester.executeRequest("HEAD", "/context/file.txt", 5, TimeUnit.SECONDS); - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("ETag", response.get("ETag"), containsString(CompressedContentFormat.GZIP._etag)); - assertThat("Content encoding", response.get("Content-Encoding"), containsString("gzip")); - assertNull(response.get("Content-Length"), "Content length"); - - response = tester.executeRequest("GET", "/context/file.txt", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content size", content.length(), is(filesize)); - String expectedContent = IO.readToString(testingdir.getPathFile("file.txt").toFile()); - assertThat("Response content", content, is(expectedContent)); - } - finally - { - tester.stop(); - } - } - - @SuppressWarnings("serial") - public static class GetServlet extends DefaultServlet - { - public GetServlet() - { - super(); - } - - @Override - public void service(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException - { - String uri = req.getRequestURI(); - if (uri.endsWith(".deferred")) - { - // System.err.println("type for "+uri.substring(0,uri.length()-9)+" is "+getServletContext().getMimeType(uri.substring(0,uri.length()-9))); - resp.setContentType(getServletContext().getMimeType(uri.substring(0, uri.length() - 9))); - } - - doGet(req, resp); - } - } - - @Test - public void testIsGzipCompressedEmpty() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - tester.prepareServerFile("empty.txt", 0); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - - HttpTester.Response response; - - response = tester.executeRequest("GET", "/context/empty.txt", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content size", content.length(), is(0)); - String expectedContent = IO.readToString(testingdir.getPathFile("empty.txt").toFile()); - assertThat("Response content", content, is(expectedContent)); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedTiny() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedLarge() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - tester.getGzipHandler().setExcludedAgentPatterns(); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzipedIfModified() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzippedIfSVG() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.copyTestServerFile("test.svg"); - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - tester.getGzipHandler().addIncludedMimeTypes("image/svg+xml"); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "test.svg", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) - 4000); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testNotGzipedIfNotModified() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - - try - { - tester.start(); - tester.assertIsResponseNotModified("GET", "file.txt", System.currentTimeMillis() + 4000); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedWithZeroQ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType + "; q=0"); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - assertThat("Response[Vary]", http.get("Vary"), containsString("Accept-Encoding")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsGzipCompressedWithQ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType, "something;q=0.1," + compressionType + ";q=0.5"); - - int filesize = tester.getOutputBufferSize() / 4; - tester.prepareServerFile("file.txt", filesize); - - tester.setContentServlet(org.eclipse.jetty.servlet.DefaultServlet.class); - tester.getGzipHandler().setExcludedAgentPatterns(); - - try - { - tester.start(); - HttpTester.Response http = tester.assertIsResponseGzipCompressed("GET", "file.txt"); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.mp3", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.mp3", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByExcludedContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addExcludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("test_quotes.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "test_quotes.txt", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByExcludedContentTypeWithCharset() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addExcludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("test_quotes.txt", filesize); - tester.addMimeType("txt", "text/plain;charset=UTF-8"); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "test_quotes.txt", filesize, HttpStatus.OK_200); - assertNull(http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testGzipCompressedByContentTypeWithEncoding() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setContentServlet(HttpContentTypeWithEncoding.class); - tester.getGzipHandler().setMinGzipSize(16); - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setExcludedAgentPatterns(); - try - { - tester.start(); - HttpTester.Response http = tester.assertNonStaticContentIsResponseGzipCompressed("GET", "xxx", HttpContentTypeWithEncoding.COMPRESSED_CONTENT); - assertEquals("Accept-Encoding", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedByDeferredContentType() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.mp3.deferred", filesize); - - // Add content servlet - tester.setContentServlet(GetServlet.class); - - try - { - tester.start(); - HttpTester.Response response = assertIsResponseNotGzipCompressed(tester, "GET", "file.mp3.deferred", filesize, HttpStatus.OK_200); - assertThat("Response[Vary]", response.get("Vary"), is(emptyOrNullString())); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedHttpStatus() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Test error code 204 - tester.setContentServlet(HttpStatusServlet.class); - - try - { - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.NO_CONTENT_204)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIsNotGzipCompressedHttpBadRequestStatus() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - - // Test error code 400 - tester.setContentServlet(HttpErrorServlet.class); - - try - { - tester.start(); - - HttpTester.Response response = tester.executeRequest("GET", "/context/", 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(HttpStatus.BAD_REQUEST_400)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - String content = tester.readResponse(response); - assertThat("Response content", content, is("error message")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusion() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setUserAgent("foo"); - - // Configure Gzip Handler - tester.getGzipHandler().addIncludedMimeTypes("text/plain"); - tester.getGzipHandler().setExcludedAgentPatterns("bar", "foo"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Add content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusionDefault() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setContentServlet(DefaultServlet.class); - tester.setUserAgent("Some MSIE 6.0 user-agent"); - - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - try - { - tester.start(); - HttpTester.Response http = assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - assertEquals("Accept-Encoding, User-Agent", http.get("Vary")); - } - finally - { - tester.stop(); - } - } - - @Test - public void testUserAgentExclusionByExcludedAgentPatterns() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - tester.setUserAgent("foo"); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedAgentPatterns("bar", "fo.*"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testExcludePaths() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedPaths("*.txt"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "file.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - @Test - public void testIncludedPaths() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - // Configure Gzip Handler - tester.getGzipHandler().setExcludedPaths(tester.getContextPath() + "/bad.txt"); - tester.getGzipHandler().setIncludedPaths("*.txt"); - - // Prepare server file - int filesize = tester.getOutputBufferSize() * 4; - tester.prepareServerFile("file.txt", filesize); - tester.prepareServerFile("bad.txt", filesize); - - // Set content servlet - tester.setContentServlet(DefaultServlet.class); - - try - { - tester.start(); - tester.assertIsResponseGzipCompressed("GET", "file.txt"); - } - finally - { - tester.stop(); - } - - try - { - tester.start(); - assertIsResponseNotGzipCompressed(tester, "GET", "bad.txt", filesize, HttpStatus.OK_200); - } - finally - { - tester.stop(); - } - } - - public HttpTester.Response assertIsResponseNotGzipCompressed(GzipTester tester, String method, String filename, int expectedFilesize, int status) - throws Exception - { - HttpTester.Response response = tester.executeRequest(method, "/context/" + filename, 5, TimeUnit.SECONDS); - - assertThat("Response status", response.getStatus(), is(status)); - assertThat("Content-Encoding", response.get("Content-Encoding"), not(containsString(compressionType))); - - assertResponseContent(tester, response, status, filename, expectedFilesize); - - return response; - } - - private void assertResponseContent(GzipTester tester, HttpTester.Response response, int status, String filename, int expectedFilesize) throws IOException, - UnsupportedEncodingException - { - if (expectedFilesize >= 0) - { - assertThat("filename", filename, notNullValue()); - assertThat("Response contentBytes.length", response.getContentBytes().length, is(expectedFilesize)); - String contentLength = response.get("Content-Length"); - if (StringUtil.isNotBlank(contentLength)) - { - assertThat("Content-Length", response.get("Content-Length"), is(Integer.toString(expectedFilesize))); - } - - if (status >= 200 && status < 300) - { - assertThat("ETag", response.get("ETAG"), startsWith("W/")); - } - - File serverFile = testingdir.getPathFile(filename).toFile(); - String expectedResponse = IO.readToString(serverFile); - - String actual = tester.readResponse(response); - assertEquals(expectedResponse, actual, "Expected response equals actual response"); - } - } - - @Test - public void testIsNotGzipCompressedSVGZ() throws Exception - { - GzipTester tester = new GzipTester(testingdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(DefaultServlet.class); - tester.copyTestServerFile("test.svgz"); - - try - { - tester.start(); - tester.assertIsResponseNotGzipFiltered("test.svgz", "test.svgz.sha1", "image/svg+xml", "gzip"); - } - finally - { - tester.stop(); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java new file mode 100644 index 00000000000..bfb00cc6a60 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerNoReCompressTest.java @@ -0,0 +1,147 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.toolchain.test.FS; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.toolchain.test.Sha1Sum; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.resource.PathResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests {@link GzipHandler} in combination with {@link DefaultServlet} for ability to configure {@link GzipHandler} to + * ignore recompress situations from upstream. + */ +public class GzipHandlerNoReCompressTest extends AbstractGzipTest +{ + public static Stream scenarios() + { + return Stream.of( + Arguments.of("test_quotes.gz", "application/gzip"), + Arguments.of("test_quotes.br", "application/brotli"), + Arguments.of("test_quotes.bz2", "application/bzip2"), + Arguments.of("test_quotes.zip", "application/zip"), + Arguments.of("test_quotes.rar", "application/x-rar-compressed"), + // Some images (common first) + Arguments.of("jetty_logo.png", "image/png"), + Arguments.of("jetty_logo.gif", "image/gif"), + Arguments.of("jetty_logo.jpeg", "image/jpeg"), + Arguments.of("jetty_logo.jpg", "image/jpeg"), + // Lesser encountered images (usually found being requested from non-browser clients) + Arguments.of("jetty_logo.bmp", "image/bmp"), + Arguments.of("jetty_logo.tif", "image/tiff"), + Arguments.of("jetty_logo.tiff", "image/tiff"), + Arguments.of("jetty_logo.xcf", "image/xcf"), + Arguments.of("jetty_logo.jp2", "image/jpeg2000") + ); + } + + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @ParameterizedTest + @MethodSource("scenarios") + public void testNotGzipAlreadyCompressed(String fileName, String expectedContentType) throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + Path contextDir = workDir.resolve("context"); + FS.ensureDirExists(contextDir); + + ServletContextHandler servletContextHandler = new ServletContextHandler(); + servletContextHandler.setContextPath("/context"); + servletContextHandler.setBaseResource(new PathResource(contextDir)); + servletContextHandler.addServlet(TestStaticMimeTypeServlet.class, "/*"); + servletContextHandler.insertHandler(gzipHandler); + + server.setHandler(servletContextHandler); + + // Prepare Server File + Path testResource = MavenTestingUtils.getTestResourcePath(fileName); + Path file = contextDir.resolve(fileName); + IO.copy(testResource.toFile(), file.toFile()); + String expectedSha1Sum = Sha1Sum.loadSha1(MavenTestingUtils.getTestResourceFile(fileName + ".sha1")).toUpperCase(Locale.ENGLISH); + int fileSize = (int)Files.size(file); + + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/" + fileName); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Headers check + assertThat("Response[Content-Type]", response.get("Content-Type"), is(expectedContentType)); + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response[Vary]", response.get("Vary"), is(nullValue())); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response Content Length", metadata.contentLength, is(fileSize)); + assertThat("(Uncompressed) Content Length", metadata.uncompressedSize, is(fileSize)); + assertThat("(Uncompressed) Content Hash", metadata.uncompressedSha1Sum, is(expectedSha1Sum)); + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java new file mode 100644 index 00000000000..980b254f672 --- /dev/null +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipHandlerTest.java @@ -0,0 +1,226 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.servlets; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class GzipHandlerTest extends AbstractGzipTest +{ + private Server server; + + @AfterEach + public void stopServer() + { + LifeCycle.stop(server); + } + + @Test + public void testGzipCompressedByContentTypeWithEncoding() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.setMinGzipSize(32); + gzipHandler.addIncludedMimeTypes("text/plain"); + gzipHandler.setExcludedAgentPatterns(); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpContentTypeWithEncodingServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.OK_200)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), containsString("gzip")); + assertThat("Response[Vary]", response.get("Vary"), is("Accept-Encoding")); + + // Response Content checks + UncompressedMetadata metadata = parseResponseContent(response); + assertThat("Response[Content] raw length vs uncompressed length", metadata.contentLength, not(is(metadata.uncompressedSize))); + assertThat("(Uncompressed) Content", metadata.getContentUTF8(), is(HttpContentTypeWithEncodingServlet.CONTENT)); + } + + public static class HttpContentTypeWithEncodingServlet extends HttpServlet + { + public static final String CONTENT = "

COMPRESSIBLE CONTENT

" + + "

" + + "This content must be longer than the default min gzip length, which is " + GzipHandler.DEFAULT_MIN_GZIP_SIZE + " bytes. " + + "The moon is blue to a fish in love.
" + + "How now brown cow.
" + + "The quick brown fox jumped over the lazy dog.
" + + "A woman needs a man like a fish needs a bicycle!" + + "

" + + ""; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setContentType("text/plain;charset=UTF8"); + resp.setStatus(200); + ServletOutputStream out = resp.getOutputStream(); + out.print(CONTENT); + } + } + + @Test + public void testIsNotGzipCompressedHttpStatus() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpStatusServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.NO_CONTENT_204)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + } + + public static class HttpStatusServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.setStatus(HttpServletResponse.SC_NO_CONTENT); + resp.setHeader("ETag", "W/\"204\""); + } + } + + @Test + public void testIsNotGzipCompressedHttpBadRequestStatus() throws Exception + { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMimeTypes("text/plain"); + + server = new Server(); + LocalConnector localConnector = new LocalConnector(server); + server.addConnector(localConnector); + + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.setContextPath("/context"); + contextHandler.addServlet(HttpErrorServlet.class, "/*"); + + gzipHandler.setHandler(contextHandler); + + server.setHandler(gzipHandler); + server.start(); + + // Setup request + HttpTester.Request request = HttpTester.newRequest(); + request.setMethod("GET"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "tester"); + request.setHeader("Connection", "close"); + request.setHeader("Accept-Encoding", "gzip"); + request.setURI("/context/xxx"); + + // Issue request + ByteBuffer rawResponse = localConnector.getResponse(request.generate(), 5, TimeUnit.SECONDS); + + // Parse response + HttpTester.Response response = HttpTester.parseResponse(rawResponse); + + assertThat("Response status", response.getStatus(), is(HttpStatus.BAD_REQUEST_400)); + + // Response Content-Encoding check + assertThat("Response[Content-Encoding]", response.get("Content-Encoding"), not(containsString("gzip"))); + assertThat("Response Content", response.getContent(), is("error message")); + } + + public static class HttpErrorServlet extends HttpServlet + { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + { + resp.getOutputStream().write("error message".getBytes()); + resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + } +} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java deleted file mode 100644 index 61bb4fab8be..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/GzipTester.java +++ /dev/null @@ -1,635 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.nio.file.Path; -import java.security.DigestOutputStream; -import java.security.MessageDigest; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import javax.servlet.DispatcherType; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.DateGenerator; -import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletTester; -import org.eclipse.jetty.toolchain.test.FS; -import org.eclipse.jetty.toolchain.test.IO; -import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.toolchain.test.Sha1Sum; -import org.hamcrest.Matchers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.emptyOrNullString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class GzipTester -{ - private static final Logger LOG = LoggerFactory.getLogger(GzipTester.class); - - public static class ContentMetadata - { - public final long size; - public final String sha1; - - public ContentMetadata(long size, String sha1checksum) - { - this.size = size; - this.sha1 = sha1checksum; - } - } - - private String encoding = "ISO8859_1"; - private String userAgent = null; - private final GzipHandler gzipHandler = new GzipHandler(); - private final ServletTester tester = new ServletTester("/context"); - private Path testdir; - private String accept; - private String compressionType; - - public GzipTester(Path testingdir, String compressionType) - { - this(testingdir, compressionType, compressionType); - } - - public GzipTester(Path testingdir, String compressionType, String accept) - { - this.testdir = testingdir; - this.compressionType = compressionType; - this.accept = accept; - this.tester.getServer().insertHandler(gzipHandler); - } - - public String getContextPath() - { - return tester.getContextPath(); - } - - public GzipHandler getGzipHandler() - { - return gzipHandler; - } - - public int getOutputBufferSize() - { - return tester.getConnector().getConnectionFactory(HttpConnectionFactory.class).getHttpConfiguration().getOutputBufferSize(); - } - - public ContentMetadata getResponseMetadata(HttpTester.Response response) throws Exception - { - long size = response.getContentBytes().length; - - String contentEncoding = response.get("Content-Encoding"); - - ByteArrayInputStream bais = null; - InputStream in = null; - DigestOutputStream digester = null; - ByteArrayOutputStream uncompressedStream = null; - try - { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - bais = new ByteArrayInputStream(response.getContentBytes()); - - if (contentEncoding == null) - { - LOG.debug("No response content-encoding"); - in = new PassThruInputStream(bais); - } - else if (contentEncoding.contains(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (contentEncoding.contains(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - else - { - assertThat("Unexpected response content-encoding", contentEncoding, is(emptyOrNullString())); - } - - uncompressedStream = new ByteArrayOutputStream((int)size); - - digester = new DigestOutputStream(uncompressedStream, digest); - IO.copy(in, digester); - - byte[] output = uncompressedStream.toByteArray(); - String actualSha1Sum = Hex.asHex(digest.digest()); - return new ContentMetadata(output.length, actualSha1Sum); - } - finally - { - IO.close(digester); - IO.close(in); - IO.close(bais); - IO.close(uncompressedStream); - } - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename) throws Exception - { - return assertIsResponseGzipCompressed(method, filename, filename, -1); - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String filename, long ifmodifiedsince) throws Exception - { - return assertIsResponseGzipCompressed(method, filename, filename, ifmodifiedsince); - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename) throws Exception - { - return assertIsResponseGzipCompressed(method, requestedFilename, serverFilename, -1); - } - - public HttpTester.Response assertNonStaticContentIsResponseGzipCompressed(String method, String path, String expected) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", accept); - - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + path); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - int qindex = compressionType.indexOf(";"); - if (qindex < 0) - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType)); - else - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType.substring(0, qindex))); - - ByteArrayInputStream bais = null; - InputStream in = null; - ByteArrayOutputStream out = null; - String actual = null; - - try - { - bais = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.startsWith(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (compressionType.startsWith(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - out = new ByteArrayOutputStream(); - IO.copy(in, out); - - actual = out.toString(encoding); - assertThat("Uncompressed contents", actual, equalTo(expected)); - } - finally - { - IO.close(out); - IO.close(in); - IO.close(bais); - } - - return response; - } - - public HttpTester.Response assertIsResponseGzipCompressed(String method, String requestedFilename, String serverFilename, long ifmodifiedsince) - throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (ifmodifiedsince > 0) - request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), DateGenerator.formatDate(ifmodifiedsince)); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - // Assert the response headers - // assertThat("Response.status",response.getStatus(),is(HttpServletResponse.SC_OK)); - - // Response headers should have either a Transfer-Encoding indicating chunked OR a Content-Length - /* - * TODO need to check for the 3rd option of EOF content. To do this properly you might need to look at both HTTP/1.1 and HTTP/1.0 requests String - * contentLength = response.get("Content-Length"); String transferEncoding = response.get("Transfer-Encoding"); - * - * boolean chunked = (transferEncoding != null) && (transferEncoding.indexOf("chunk") >= 0); if(!chunked) { - * assertThat("Response.header[Content-Length]",contentLength,notNullValue()); } else { - * assertThat("Response.header[Transfer-Encoding]",transferEncoding,notNullValue()); } - */ - - int qindex = compressionType.indexOf(";"); - if (qindex < 0) - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType)); - else - assertThat("Response.header[Content-Encoding]", response.get("Content-Encoding"), containsString(compressionType.substring(0, qindex))); - - assertThat(response.get("ETag"), Matchers.startsWith("W/")); - - // Assert that the decompressed contents are what we expect. - File serverFile = testdir.resolve(FS.separators(serverFilename)).toFile(); - String expected = IO.readToString(serverFile); - String actual = null; - - ByteArrayInputStream bais = null; - InputStream in = null; - ByteArrayOutputStream out = null; - try - { - bais = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.startsWith(GzipHandler.GZIP)) - { - in = new GZIPInputStream(bais); - } - else if (compressionType.startsWith(GzipHandler.DEFLATE)) - { - in = new InflaterInputStream(bais, new Inflater(true)); - } - out = new ByteArrayOutputStream(); - IO.copy(in, out); - - actual = out.toString(encoding); - assertThat("Uncompressed contents", actual, equalTo(expected)); - } - finally - { - IO.close(out); - IO.close(in); - IO.close(bais); - } - - return response; - } - - public HttpTester.Response assertIsResponseNotModified(String method, String requestedFilename, long ifmodifiedsince) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod(method); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (ifmodifiedsince > 0) - request.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), DateGenerator.formatDate(ifmodifiedsince)); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - assertThat(response.getStatus(), Matchers.equalTo(304)); - assertThat(response.get("ETag"), Matchers.startsWith("W/")); - - return response; - } - - /** - * Makes sure that the response contains an unfiltered file contents. - *

- * This is used to test exclusions and passthroughs in the GzipHandler. - *

- * An example is to test that it is possible to configure GzipFilter to not recompress content that shouldn't be compressed by the GzipFilter. - * - * @param requestedFilename the filename used to on the GET request,. - * @param testResourceSha1Sum the sha1sum file that contains the SHA1SUM checksum that will be used to verify that the response contents are what is intended. - * @param expectedContentType the expected content type - * @throws Exception on test failure - */ - public void assertIsResponseNotGziped(String requestedFilename, String testResourceSha1Sum, String expectedContentType) throws Exception - { - assertIsResponseNotGzipFiltered(requestedFilename, testResourceSha1Sum, expectedContentType, null); - } - - /** - * Makes sure that the response contains an unfiltered file contents. - *

- * This is used to test exclusions and passthroughs in the GzipHandler. - *

- * An example is to test that it is possible to configure GzipFilter to not recompress content that shouldn't be compressed by the GzipFilter. - * - * @param requestedFilename the filename used to on the GET request,. - * @param testResourceSha1Sum the sha1sum file that contains the SHA1SUM checksum that will be used to verify that the response contents are what is intended. - * @param expectedContentType the expected content type - * @param expectedContentEncoding can be non-null in some circumstances, eg when dealing with pre-gzipped .svgz files - * @throws Exception on test failure - */ - public void assertIsResponseNotGzipFiltered(String requestedFilename, String testResourceSha1Sum, String expectedContentType, String expectedContentEncoding) - throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - HttpTester.Response response; - - request.setMethod("GET"); - request.setVersion("HTTP/1.0"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", compressionType); - if (this.userAgent != null) - request.setHeader("User-Agent", this.userAgent); - request.setURI("/context/" + requestedFilename); - - // Issue the request - response = HttpTester.parseResponse(tester.getResponses(request.generate())); - - dumpHeaders(requestedFilename + " / Response Headers", response); - - // Assert the response headers - String prefix = requestedFilename + " / Response"; - assertThat(prefix + ".status", response.getStatus(), is(HttpServletResponse.SC_OK)); - assertThat(prefix + ".header[Content-Length]", response.get("Content-Length"), notNullValue()); - assertThat(prefix + ".header[Content-Encoding] (should not be recompressed by GzipHandler)", response.get("Content-Encoding"), - expectedContentEncoding == null ? nullValue() : notNullValue()); - if (expectedContentEncoding != null) - assertThat(prefix + ".header[Content-Encoding]", response.get("Content-Encoding"), is(expectedContentEncoding)); - assertThat(prefix + ".header[Content-Type] (should have a Content-Type associated with it)", response.get("Content-Type"), notNullValue()); - assertThat(prefix + ".header[Content-Type]", response.get("Content-Type"), is(expectedContentType)); - - assertThat(response.get("ETAG"), Matchers.startsWith("W/")); - - ByteArrayInputStream bais = null; - DigestOutputStream digester = null; - try - { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - bais = new ByteArrayInputStream(response.getContentBytes()); - digester = new DigestOutputStream(new NoOpOutputStream(), digest); - IO.copy(bais, digester); - - String actualSha1Sum = Hex.asHex(digest.digest()); - File sha1File = MavenTestingUtils.getTestResourceFile(testResourceSha1Sum); - String expectedSha1Sum = Sha1Sum.loadSha1(sha1File); - assertEquals(expectedSha1Sum, actualSha1Sum, requestedFilename + " / SHA1Sum of content"); - } - finally - { - IO.close(digester); - IO.close(bais); - } - } - - private void dumpHeaders(String prefix, HttpTester.Message message) - { - LOG.debug("dumpHeaders: {}", prefix); - Enumeration names = message.getFieldNames(); - while (names.hasMoreElements()) - { - String name = names.nextElement(); - String value = message.get(name); - LOG.debug("dumpHeaders: {} = {}", name, value); - } - } - - public HttpTester.Response executeRequest(String method, String path, int idleFor, TimeUnit idleUnit) throws Exception - { - HttpTester.Request request = HttpTester.newRequest(); - - request.setMethod(method); - request.setVersion("HTTP/1.1"); - request.setHeader("Host", "tester"); - request.setHeader("Accept-Encoding", accept); - request.setHeader("Connection", "close"); - - if (this.userAgent != null) - { - request.setHeader("User-Agent", this.userAgent); - } - - request.setURI(path); - - // Issue the request - return HttpTester.parseResponse(tester.getResponses(request.generate(), idleFor, idleUnit)); - } - - public String readResponse(HttpTester.Response response) throws IOException, UnsupportedEncodingException - { - String actual = null; - InputStream in = null; - ByteArrayOutputStream out = null; - try - { - byte[] content = response.getContentBytes(); - if (content != null) - actual = new String(response.getContentBytes(), encoding); - else - actual = ""; - } - finally - { - IO.close(out); - IO.close(in); - } - return actual; - } - - /** - * Generate string content of arbitrary length. - * - * @param length the length of the string to generate. - * @return the string content. - */ - public String generateContent(int length) - { - StringBuilder builder = new StringBuilder(); - do - { - builder.append("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc.\n"); - builder.append("Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque\n"); - builder.append("habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n"); - builder.append("Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam\n"); - builder.append("at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate\n"); - builder.append("velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum.\n"); - builder.append("Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum\n"); - builder.append("eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa\n"); - builder.append("sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam\n"); - builder.append("consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque.\n"); - builder.append("Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse\n"); - builder.append("et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque.\n"); - } - while (builder.length() < length); - - // Make sure we are exactly at requested length. (truncate the extra) - if (builder.length() > length) - { - builder.setLength(length); - } - - return builder.toString(); - } - - public String getEncoding() - { - return encoding; - } - - /** - * Create a file on the server resource path of a specified filename and size. - * - * @param filename the filename to create - * @param filesize the file size to create (Note: this isn't suitable for creating large multi-megabyte files) - * @return the prepared file - * @throws IOException if unable to create file - */ - public File prepareServerFile(String filename, int filesize) throws IOException - { - File dir = testdir.toFile(); - File testFile = new File(dir, filename); - // Make sure we have a uniq filename (to work around windows File.delete bug) - int i = 0; - while (testFile.exists()) - { - testFile = new File(dir, (i++) + "-" + filename); - } - - FileOutputStream fos = null; - ByteArrayInputStream in = null; - try - { - fos = new FileOutputStream(testFile, false); - in = new ByteArrayInputStream(generateContent(filesize).getBytes(encoding)); - IO.copy(in, fos); - return testFile; - } - finally - { - IO.close(in); - IO.close(fos); - } - } - - /** - * Copy a src/test/resource file into the server tree for eventual serving. - * - * @param filename the filename to look for in src/test/resources - * @throws IOException if unable to copy file - */ - public void copyTestServerFile(String filename) throws IOException - { - File srcFile = MavenTestingUtils.getTestResourceFile(filename); - File testFile = testdir.resolve(FS.separators(filename)).toFile(); - - IO.copy(srcFile, testFile); - } - - /** - * Set the servlet that provides content for the GzipHandler in being tested. - * - * @param servletClass the servlet that will provide content. - * @throws IOException if unable to set content servlet - */ - public void setContentServlet(Class servletClass) throws IOException - { - String resourceBase = testdir.toString(); - tester.setContextPath("/context"); - tester.setResourceBase(resourceBase); - ServletHolder servletHolder = tester.addServlet(servletClass, "/"); - servletHolder.setInitParameter("baseDir", resourceBase); - servletHolder.setInitParameter("etags", "true"); - } - - public void setEncoding(String encoding) - { - this.encoding = encoding; - } - - public void setUserAgent(String ua) - { - this.userAgent = ua; - } - - public void addMimeType(String extension, String mimetype) - { - this.tester.getContext().getMimeTypes().addMimeMapping(extension, mimetype); - } - - /** - * Add an arbitrary filter to the test case. - * - * @param holder the filter to add - * @param pathSpec the path spec for this filter - * @param dispatches the set of {@link DispatcherType} to associate with this filter - * @throws IOException if unable to add filter - */ - public void addFilter(FilterHolder holder, String pathSpec, EnumSet dispatches) throws IOException - { - tester.addFilter(holder, pathSpec, dispatches); - } - - public void start() throws Exception - { - assertThat("No servlet defined yet. Did you use #setContentServlet()?", tester, notNullValue()); - - if (LOG.isDebugEnabled()) - { - tester.dumpStdErr(); - } - tester.start(); - } - - public void stop() - { - // NOTE: Do not cleanup the workDir. Failures can't be diagnosed if you do that. - // IO.delete(workDir.getDir()): - try - { - tester.stop(); - } - catch (Exception e) - { - // Don't toss this out into Junit as this would be the last exception - // that junit will report as being the cause of the test failure. - // when in reality, the earlier setup issue is the real cause. - e.printStackTrace(System.err); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java deleted file mode 100644 index 1b491baae23..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/Hex.java +++ /dev/null @@ -1,76 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -public final class Hex -{ - private static final char[] hexcodes = "0123456789abcdef".toCharArray(); - - public static byte[] asByteArray(String id, int size) - { - if ((id.length() < 0) || (id.length() > (size * 2))) - { - throw new IllegalArgumentException(String.format("Invalid ID length of <%d> expected range of <0> to <%d>", id.length(), (size * 2))); - } - - byte[] buf = new byte[size]; - byte hex; - int len = id.length(); - - int idx = (int)Math.floor(((size * 2) - (double)len) / 2); - int i = 0; - if ((len % 2) != 0) - { // deal with odd numbered chars - i -= 1; - } - - for (; i < len; i++) - { - hex = 0; - if (i >= 0) - { - hex = (byte)(Character.digit(id.charAt(i), 16) << 4); - } - i++; - hex += (byte)(Character.digit(id.charAt(i), 16)); - - buf[idx] = hex; - idx++; - } - - return buf; - } - - public static String asHex(byte[] buf) - { - int len = buf.length; - char[] out = new char[len * 2]; - for (int i = 0; i < len; i++) - { - out[i * 2] = hexcodes[(buf[i] & 0xF0) >> 4]; - out[(i * 2) + 1] = hexcodes[(buf[i] & 0x0F)]; - } - return String.valueOf(out); - } - - private Hex() - { - /* prevent instantiation */ - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java similarity index 94% rename from jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java rename to jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java index 1d462fa8e6e..654dcdf78a2 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestServletBufferTypeLengthWrite.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/HttpOutputWriteFileContentServlet.java @@ -44,12 +44,12 @@ import org.eclipse.jetty.server.handler.gzip.GzipHandler; * @see http://bugs.eclipse.org/354014 */ @SuppressWarnings("serial") -public class TestServletBufferTypeLengthWrite extends TestDirContentServlet +public class HttpOutputWriteFileContentServlet extends AbstractFileContentServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); ServletOutputStream out = response.getOutputStream(); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java deleted file mode 100644 index b040a97e321..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipMinSizeTest.java +++ /dev/null @@ -1,95 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import javax.servlet.Servlet; - -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -/** - * Perform specific tests on the IncludableGzipHandler's ability to manage - * minGzipSize initialization parameter. - * - * @see http://bugs.eclipse.org/366106 - */ -@ExtendWith(WorkDirExtension.class) -public class IncludedGzipMinSizeTest -{ - public IncludedGzipMinSizeTest() - { - this.compressionType = GzipHandler.GZIP; - } - - public WorkDir testdir; - - private String compressionType; - private Class testServlet = TestMinGzipSizeServlet.class; - - @Test - public void testUnderMinSize() throws Exception - { - GzipTester tester = new GzipTester(testdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(testServlet); - // A valid mime type that we will never use in this test. - // configured here to prevent mimeType==null logic - tester.getGzipHandler().addIncludedMimeTypes("application/soap+xml"); - tester.getGzipHandler().setMinGzipSize(2048); - - tester.copyTestServerFile("small_script.js"); - - try - { - tester.start(); - tester.assertIsResponseNotGziped("small_script.js", - "small_script.js.sha1", - "text/javascript; charset=utf-8"); - } - finally - { - tester.stop(); - } - } - - @Test - public void testOverMinSize() throws Exception - { - GzipTester tester = new GzipTester(testdir.getEmptyPathDir(), compressionType); - - tester.setContentServlet(testServlet); - tester.getGzipHandler().addIncludedMimeTypes("application/soap+xml", "text/javascript", "application/javascript"); - tester.getGzipHandler().setMinGzipSize(2048); - - tester.copyTestServerFile("big_script.js"); - - try - { - tester.start(); - tester.assertIsResponseGzipCompressed("GET", "big_script.js"); - } - finally - { - tester.stop(); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java deleted file mode 100644 index 50d7526bf67..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/IncludedGzipTest.java +++ /dev/null @@ -1,134 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.tools.HttpTester; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.ServletTester; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; -import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.IO; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@ExtendWith(WorkDirExtension.class) -public class IncludedGzipTest -{ - public WorkDir testdir; - - private static String __content = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis felis nunc. " + - "Quisque suscipit mauris et ante auctor ornare rhoncus lacus aliquet. Pellentesque " + - "habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. " + - "Vestibulum sit amet felis augue, vel convallis dolor. Cras accumsan vehicula diam " + - "at faucibus. Etiam in urna turpis, sed congue mi. Morbi et lorem eros. Donec vulputate " + - "velit in risus suscipit lobortis. Aliquam id urna orci, nec sollicitudin ipsum. " + - "Cras a orci turpis. Donec suscipit vulputate cursus. Mauris nunc tellus, fermentum " + - "eu auctor ut, mollis at diam. Quisque porttitor ultrices metus, vitae tincidunt massa " + - "sollicitudin a. Vivamus porttitor libero eget purus hendrerit cursus. Integer aliquam " + - "consequat mauris quis luctus. Cras enim nibh, dignissim eu faucibus ac, mollis nec neque. " + - "Aliquam purus mauris, consectetur nec convallis lacinia, porta sed ante. Suspendisse " + - "et cursus magna. Donec orci enim, molestie a lobortis eu, imperdiet vitae neque."; - - private ServletTester tester; - private String compressionType; - - public IncludedGzipTest() - { - this.compressionType = GzipHandler.GZIP; - } - - @BeforeEach - public void setUp() throws Exception - { - testdir.ensureEmpty(); - - File testFile = testdir.getPathFile("file.txt").toFile(); - try (OutputStream testOut = new BufferedOutputStream(new FileOutputStream(testFile))) - { - ByteArrayInputStream testIn = new ByteArrayInputStream(__content.getBytes("ISO8859_1")); - IO.copy(testIn, testOut); - } - - tester = new ServletTester("/context"); - tester.getContext().setResourceBase(testdir.getPath().toString()); - tester.getContext().addServlet(org.eclipse.jetty.servlet.DefaultServlet.class, "/"); - - GzipHandler gzipHandler = new GzipHandler(); - gzipHandler.setMinGzipSize(16); - tester.getContext().insertHandler(gzipHandler); - tester.start(); - } - - @AfterEach - public void tearDown() throws Exception - { - tester.stop(); - } - - @Test - public void testGzip() throws Exception - { - // generated and parsed test - - ByteBuffer request = BufferUtil.toBuffer( - "GET /context/file.txt HTTP/1.0\r\n" + - "Host: tester\r\n" + - "Accept-Encoding: " + compressionType + "\r\n" + - "\r\n"); - - HttpTester.Response response = HttpTester.parseResponse(tester.getResponses(request)); - - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertEquals(compressionType, response.get("Content-Encoding")); - - InputStream testIn = null; - ByteArrayInputStream compressedResponseStream = new ByteArrayInputStream(response.getContentBytes()); - if (compressionType.equals(GzipHandler.GZIP)) - { - testIn = new GZIPInputStream(compressedResponseStream); - } - else if (compressionType.equals(GzipHandler.DEFLATE)) - { - testIn = new InflaterInputStream(compressedResponseStream, new Inflater(true)); - } - ByteArrayOutputStream testOut = new ByteArrayOutputStream(); - IO.copy(testIn, testOut); - - assertEquals(__content, testOut.toString("ISO8859_1")); - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java deleted file mode 100644 index 7c5f1a00178..00000000000 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestDirContentServlet.java +++ /dev/null @@ -1,73 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.servlets; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; - -import org.eclipse.jetty.toolchain.test.PathAssert; -import org.eclipse.jetty.util.IO; - -@SuppressWarnings("serial") -public class TestDirContentServlet extends HttpServlet -{ - private File basedir; - - @Override - public void init(ServletConfig config) throws ServletException - { - basedir = new File(config.getInitParameter("baseDir")); - } - - public File getTestFile(String filename) - { - File testfile = new File(basedir, filename); - PathAssert.assertFileExists("Content File should exist", testfile); - return testfile; - } - - protected byte[] loadContentFileBytes(final String fileName) throws IOException - { - String relPath = fileName; - relPath = relPath.replaceFirst("^/context/", ""); - relPath = relPath.replaceFirst("^/", ""); - - File contentFile = getTestFile(relPath); - - FileInputStream in = null; - ByteArrayOutputStream out = null; - try - { - in = new FileInputStream(contentFile); - out = new ByteArrayOutputStream(); - IO.copy(in, out); - return out.toByteArray(); - } - finally - { - IO.close(out); - IO.close(in); - } - } -} diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java index 8b24aaece49..87e60a377fb 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestMinGzipSizeServlet.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.http.MimeTypes; * Test servlet for testing against unusual minGzip configurable. */ @SuppressWarnings("serial") -public class TestMinGzipSizeServlet extends TestDirContentServlet +public class TestMinGzipSizeServlet extends AbstractFileContentServlet { private MimeTypes mimeTypes; diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java index d45ea8037bd..7d378f6f271 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/TestStaticMimeTypeServlet.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.http.MimeTypes; * Test servlet for testing against unusual MimeTypes and Content-Types. */ @SuppressWarnings("serial") -public class TestStaticMimeTypeServlet extends TestDirContentServlet +public class TestStaticMimeTypeServlet extends AbstractFileContentServlet { private MimeTypes mimeTypes; @@ -63,7 +63,7 @@ public class TestStaticMimeTypeServlet extends TestDirContentServlet @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - String fileName = request.getServletPath(); + String fileName = request.getPathInfo(); byte[] dataBytes = loadContentFileBytes(fileName); response.setContentLength(dataBytes.length); diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java index dd5db390cea..b97d52c9814 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/ThreadStarvationTest.java @@ -45,7 +45,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.logging.StacklessLogging; @@ -106,7 +105,7 @@ public class ThreadStarvationTest ServerConnector connector = new ServerConnector(_server, 0, 1) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { @@ -258,7 +257,7 @@ public class ThreadStarvationTest ServerConnector connector = new ServerConnector(_server, acceptors, selectors) { @Override - protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + protected SocketChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) { return new SocketChannelEndPoint(channel, selectSet, key, getScheduler()) { diff --git a/jetty-servlets/src/test/resources/test.svg.sha1 b/jetty-servlets/src/test/resources/test.svg.sha1 deleted file mode 100644 index 3b170f0b098..00000000000 --- a/jetty-servlets/src/test/resources/test.svg.sha1 +++ /dev/null @@ -1 +0,0 @@ -1ccb7a0b85585d0e9bdc3863ad093d4e53a9ea68 test.svg diff --git a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java index b4b20bd61cc..dad2b625e37 100644 --- a/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java +++ b/jetty-unixsocket/jetty-unixsocket-client/src/main/java/org/eclipse/jetty/unixsocket/client/HttpClientTransportOverUnixSockets.java @@ -47,6 +47,7 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SelectorManager; import org.eclipse.jetty.unixsocket.common.UnixSocketEndPoint; +import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -128,7 +129,7 @@ public class HttpClientTransportOverUnixSockets extends AbstractConnectorHttpCli } catch (Throwable x) { - safeClose(channel); + IO.close(channel); connectFailed(x, context); } } diff --git a/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java b/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java index 8add2ca82b3..a8f10b39001 100644 --- a/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java +++ b/jetty-unixsocket/jetty-unixsocket-client/src/test/java/org/eclipse/jetty/unixsocket/UnixSocketTest.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.unixsocket.server.UnixSocketConnector; import org.eclipse.jetty.util.StringUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.slf4j.Logger; @@ -146,6 +147,7 @@ public class UnixSocketTest assertThat(contentResponse.getContentAsString(), containsString("Hello World")); } + @Tag("external") @Test public void testNotLocal() throws Exception { diff --git a/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java b/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java index 29c3c11d5e9..ed4e4248605 100644 --- a/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java +++ b/jetty-unixsocket/jetty-unixsocket-common/src/main/java/org/eclipse/jetty/unixsocket/common/UnixSocketEndPoint.java @@ -23,22 +23,25 @@ import java.net.InetSocketAddress; import java.nio.channels.SelectionKey; import jnr.unixsocket.UnixSocketChannel; -import org.eclipse.jetty.io.ChannelEndPoint; import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.util.thread.Scheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class UnixSocketEndPoint extends ChannelEndPoint +public class UnixSocketEndPoint extends SocketChannelEndPoint { private static final Logger LOG = LoggerFactory.getLogger(UnixSocketEndPoint.class); - private final UnixSocketChannel _channel; - public UnixSocketEndPoint(UnixSocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) { super(channel, selector, key, scheduler); - _channel = channel; + } + + @Override + public UnixSocketChannel getChannel() + { + return (UnixSocketChannel)super.getChannel(); } @Override @@ -56,11 +59,9 @@ public class UnixSocketEndPoint extends ChannelEndPoint @Override protected void doShutdownOutput() { - if (LOG.isDebugEnabled()) - LOG.debug("oshut {}", this); try { - _channel.shutdownOutput(); + getChannel().shutdownOutput(); super.doShutdownOutput(); } catch (IOException e) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java index b8b8386b3a7..8fc304278e6 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -141,9 +141,9 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum private final Set _includeProtocols = new LinkedHashSet<>(); private final Set _excludeCipherSuites = new LinkedHashSet<>(); private final List _includeCipherSuites = new ArrayList<>(); - protected final Map _aliasX509 = new HashMap<>(); - protected final Map _certHosts = new HashMap<>(); - protected final Map _certWilds = new HashMap<>(); + private final Map _aliasX509 = new HashMap<>(); + private final Map _certHosts = new HashMap<>(); + private final Map _certWilds = new HashMap<>(); private String[] _selectedProtocols; private boolean _useCipherSuitesOrder = true; private Comparator _cipherComparator; @@ -453,6 +453,21 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum _certWilds.clear(); } + Map aliasCerts() + { + return _aliasX509; + } + + Map hostCerts() + { + return _certHosts; + } + + Map wildCerts() + { + return _certWilds; + } + @ManagedAttribute(value = "The selected TLS protocol versions", readonly = true) public String[] getSelectedProtocols() { @@ -2157,7 +2172,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum boolean hasSniX509ExtendedKeyManager = false; // Is SNI needed to select a certificate? - if (!_certWilds.isEmpty() || _certHosts.size() > 1 || (_certHosts.size() == 1 && _aliasX509.size() > 1)) + if (isSniRequired() || !wildCerts().isEmpty() || hostCerts().size() > 1 || (hostCerts().size() == 1 && aliasCerts().size() > 1)) { for (int idx = 0; idx < managers.length; idx++) { @@ -2201,7 +2216,7 @@ public abstract class SslContextFactory extends AbstractLifeCycle implements Dum if (sniHost == null) { // No SNI, so reject or delegate. - return _sniRequired ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE; + return isSniRequired() ? null : SniX509ExtendedKeyManager.SniSelector.DELEGATE; } else { diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java index 349ef807744..73f59a8cd3f 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ssl/X509.java @@ -80,8 +80,7 @@ public class X509 String cn = list.get(1).toString(); if (LOG.isDebugEnabled()) LOG.debug("Certificate SAN alias={} CN={} in {}", alias, cn, this); - if (cn != null) - addName(cn); + addName(cn); } } } @@ -95,19 +94,21 @@ public class X509 String cn = rdn.getValue().toString(); if (LOG.isDebugEnabled()) LOG.debug("Certificate CN alias={} CN={} in {}", alias, cn, this); - if (cn != null && cn.contains(".") && !cn.contains(" ")) - addName(cn); + addName(cn); } } } protected void addName(String cn) { - cn = StringUtil.asciiToLowerCase(cn); - if (cn.startsWith("*.")) - _wilds.add(cn.substring(2)); - else - _hosts.add(cn); + if (cn != null) + { + cn = StringUtil.asciiToLowerCase(cn); + if (cn.startsWith("*.")) + _wilds.add(cn.substring(2)); + else + _hosts.add(cn); + } } public String getAlias() diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java index 5bbf1e1118b..c38fa02a63b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/thread/ShutdownThread.java @@ -48,6 +48,7 @@ public class ShutdownThread extends Thread */ private ShutdownThread() { + super("JettyShutdownThread"); } private synchronized void hook() diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java index f6270c1eace..ed937fe7915 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ssl/SslContextFactoryTest.java @@ -282,7 +282,7 @@ public class SslContextFactoryTest assertTrue(cf.getX509("other").matches("www.example.com")); assertFalse(cf.getX509("other").matches("eclipse.org")); - assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("www.san.com", "m.san.com")); + assertThat(cf.getX509("san").getHosts(), containsInAnyOrder("san example", "www.san.com", "m.san.com")); assertTrue(cf.getX509("san").getWilds().isEmpty()); assertTrue(cf.getX509("san").matches("www.san.com")); assertTrue(cf.getX509("san").matches("m.san.com")); diff --git a/jetty-websocket/pom.xml b/jetty-websocket/pom.xml index b2952f150a8..3d74b9c9e91 100644 --- a/jetty-websocket/pom.xml +++ b/jetty-websocket/pom.xml @@ -15,8 +15,8 @@ websocket-core - websocket-servlet websocket-util + websocket-util-server websocket-jetty-api websocket-jetty-common diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP.java index 0e21f6104a2..54048a41692 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP.java @@ -54,16 +54,17 @@ public class HttpUpgraderOverHTTP implements HttpUpgrader public void prepare(HttpRequest request) { request.method(HttpMethod.GET).version(HttpVersion.HTTP_1_1) - .add(WS_VERSION_FIELD) - .add(WS_UPGRADE_FIELD) - .add(WS_CONNECTION_FIELD) - .header(HttpHeader.SEC_WEBSOCKET_KEY, generateRandomKey()) - // Per the hybi list: Add no-cache headers to avoid compatibility issue. - // There are some proxies that rewrite "Connection: upgrade" to - // "Connection: close" in the response if a request doesn't contain - // these headers. - .add(PRAGMA_NO_CACHE_FIELD) - .add(CACHE_CONTROL_NO_CACHE_FIELD); + .headers(headers -> headers + .put(WS_VERSION_FIELD) + .put(WS_UPGRADE_FIELD) + .put(WS_CONNECTION_FIELD) + .put(HttpHeader.SEC_WEBSOCKET_KEY, generateRandomKey()) + // Per the hybi list: Add no-cache headers to avoid compatibility issue. + // There are some proxies that rewrite "Connection: upgrade" to + // "Connection: close" in the response if a request doesn't contain + // these headers. + .put(PRAGMA_NO_CACHE_FIELD) + .put(CACHE_CONTROL_NO_CACHE_FIELD)); // Notify the UpgradeListeners now the headers are set. clientUpgradeRequest.requestComplete(); diff --git a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP2.java b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP2.java index e1904a0b419..422e4a14187 100644 --- a/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP2.java +++ b/jetty-websocket/websocket-core/src/main/java/org/eclipse/jetty/websocket/core/client/HttpUpgraderOverHTTP2.java @@ -41,9 +41,9 @@ public class HttpUpgraderOverHTTP2 implements HttpUpgrader @Override public void prepare(HttpRequest request) { - request.method(HttpMethod.CONNECT); - request.upgradeProtocol("websocket"); - request.add(WS_VERSION_FIELD); + request.upgradeProtocol("websocket") + .method(HttpMethod.CONNECT) + .headers(headers -> headers.put(WS_VERSION_FIELD)); // Notify the UpgradeListeners now the headers are set. clientUpgradeRequest.requestComplete(); diff --git a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java index 4cee6345c71..ae25d7bbe94 100644 --- a/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java +++ b/jetty-websocket/websocket-core/src/test/java/org/eclipse/jetty/websocket/core/WebSocketNegotiationTest.java @@ -387,7 +387,7 @@ public class WebSocketNegotiationTest extends WebSocketTester @Override public void onHandshakeRequest(HttpRequest request) { - request.header(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, "permessage-deflate"); + request.headers(headers -> headers.put(HttpHeader.SEC_WEBSOCKET_EXTENSIONS, "permessage-deflate")); } @Override diff --git a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java index bdf93ba6dcd..8202b43a44a 100644 --- a/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java +++ b/jetty-websocket/websocket-javax-client/src/main/java/org/eclipse/jetty/websocket/javax/client/internal/JavaxWebSocketClientContainer.java @@ -40,11 +40,14 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; +import org.eclipse.jetty.websocket.core.exception.UpgradeException; +import org.eclipse.jetty.websocket.core.exception.WebSocketTimeoutException; import org.eclipse.jetty.websocket.javax.common.ConfiguredEndpoint; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketExtensionConfig; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandler; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerFactory; +import org.eclipse.jetty.websocket.util.InvalidWebSocketException; /** * Container for Client use of the javax.websocket API. @@ -131,7 +134,7 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple { if (error != null) { - futureSession.completeExceptionally(error); + futureSession.completeExceptionally(convertCause(error)); return; } @@ -147,6 +150,18 @@ public class JavaxWebSocketClientContainer extends JavaxWebSocketContainer imple return futureSession; } + public static Throwable convertCause(Throwable error) + { + if (error instanceof UpgradeException || + error instanceof WebSocketTimeoutException) + return new IOException(error); + + if (error instanceof InvalidWebSocketException) + return new DeploymentException(error.getMessage(), error); + + return error; + } + private Session connect(ConfiguredEndpoint configuredEndpoint, URI destURI) throws IOException { Objects.requireNonNull(configuredEndpoint, "WebSocket configured endpoint cannot be null"); diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java index ab86f54b4bd..185c89715c8 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketAsyncRemote.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.util.TextUtil; +import org.eclipse.jetty.websocket.util.TextUtils; import org.eclipse.jetty.websocket.util.messages.MessageOutputStream; import org.eclipse.jetty.websocket.util.messages.MessageWriter; import org.slf4j.Logger; @@ -184,7 +184,7 @@ public class JavaxWebSocketAsyncRemote extends JavaxWebSocketRemoteEndpoint impl assertMessageNotNull(text); if (LOG.isDebugEnabled()) { - LOG.debug("sendText({})", TextUtil.hint(text)); + LOG.debug("sendText({})", TextUtils.hint(text)); } FutureCallback future = new FutureCallback(); sendFrame(new Frame(OpCode.TEXT).setPayload(text), future, batch); @@ -198,7 +198,7 @@ public class JavaxWebSocketAsyncRemote extends JavaxWebSocketRemoteEndpoint impl assertSendHandlerNotNull(handler); if (LOG.isDebugEnabled()) { - LOG.debug("sendText({},{})", TextUtil.hint(text), handler); + LOG.debug("sendText({},{})", TextUtils.hint(text), handler); } sendFrame(new Frame(OpCode.TEXT).setPayload(text), new SendHandlerCallback(handler), batch); } diff --git a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java index a119ebf1f9c..2519e4be72f 100644 --- a/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java +++ b/jetty-websocket/websocket-javax-common/src/main/java/org/eclipse/jetty/websocket/javax/common/JavaxWebSocketBasicRemote.java @@ -31,7 +31,7 @@ import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; -import org.eclipse.jetty.websocket.util.TextUtil; +import org.eclipse.jetty.websocket.util.TextUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -118,7 +118,7 @@ public class JavaxWebSocketBasicRemote extends JavaxWebSocketRemoteEndpoint impl assertMessageNotNull(text); if (LOG.isDebugEnabled()) { - LOG.debug("sendText({})", TextUtil.hint(text)); + LOG.debug("sendText({})", TextUtils.hint(text)); } FutureCallback b = new FutureCallback(); @@ -132,7 +132,7 @@ public class JavaxWebSocketBasicRemote extends JavaxWebSocketRemoteEndpoint impl assertMessageNotNull(partialMessage); if (LOG.isDebugEnabled()) { - LOG.debug("sendText({},{})", TextUtil.hint(partialMessage), isLast); + LOG.debug("sendText({},{})", TextUtils.hint(partialMessage), isLast); } Frame frame; diff --git a/jetty-websocket/websocket-javax-server/pom.xml b/jetty-websocket/websocket-javax-server/pom.xml index f33263ba474..d629de14878 100644 --- a/jetty-websocket/websocket-javax-server/pom.xml +++ b/jetty-websocket/websocket-javax-server/pom.xml @@ -22,7 +22,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} diff --git a/jetty-websocket/websocket-javax-server/src/main/config/modules/websocket-javax.mod b/jetty-websocket/websocket-javax-server/src/main/config/modules/websocket-javax.mod index 2f0ae30c75b..d46554bdfaf 100644 --- a/jetty-websocket/websocket-javax-server/src/main/config/modules/websocket-javax.mod +++ b/jetty-websocket/websocket-javax-server/src/main/config/modules/websocket-javax.mod @@ -12,8 +12,8 @@ annotations [lib] lib/websocket/websocket-core-${jetty.version}.jar -lib/websocket/websocket-servlet-${jetty.version}.jar lib/websocket/websocket-util-${jetty.version}.jar +lib/websocket/websocket-util-server-${jetty.version}.jar lib/websocket/jetty-javax-websocket-api-1.1.2.jar lib/websocket/websocket-javax-client-${jetty.version}.jar lib/websocket/websocket-javax-common-${jetty.version}.jar diff --git a/jetty-websocket/websocket-javax-server/src/main/java/module-info.java b/jetty-websocket/websocket-javax-server/src/main/java/module-info.java index 6d52f89a355..af5c88bbdd4 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/module-info.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/module-info.java @@ -31,7 +31,7 @@ module org.eclipse.jetty.websocket.javax.server requires transitive org.eclipse.jetty.webapp; requires transitive org.eclipse.jetty.websocket.javax.client; requires org.eclipse.jetty.websocket.javax.common; - requires org.eclipse.jetty.websocket.servlet; + requires org.eclipse.jetty.websocket.util.server; requires org.slf4j; provides ServletContainerInitializer with JavaxWebSocketServletContainerInitializer; diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java index 4c66e39abed..6075eeb66e3 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketConfiguration.java @@ -37,7 +37,7 @@ public class JavaxWebSocketConfiguration extends AbstractConfiguration { addDependencies(WebXmlConfiguration.class, MetaInfConfiguration.class, WebInfConfiguration.class, FragmentConfiguration.class); addDependents("org.eclipse.jetty.annotations.AnnotationConfiguration", WebAppConfiguration.class.getName()); - protectAndExpose("org.eclipse.jetty.websocket.servlet."); // For WebSocketUpgradeFilter + protectAndExpose("org.eclipse.jetty.websocket.util.server."); // For WebSocketUpgradeFilter protectAndExpose("org.eclipse.jetty.websocket.javax.server.config."); protectAndExpose("org.eclipse.jetty.websocket.javax.client.JavaxWebSocketClientContainerProvider"); hide("org.eclipse.jetty.websocket.javax.server.internal"); diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java index 797c9c1f1a8..87739c6a434 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/config/JavaxWebSocketServletContainerInitializer.java @@ -38,8 +38,8 @@ import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.thread.ThreadClassLoaderScope; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxServerUpgradeRequest.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxServerUpgradeRequest.java index 69624723ec8..b38d3779863 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxServerUpgradeRequest.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxServerUpgradeRequest.java @@ -22,7 +22,7 @@ import java.net.URI; import java.security.Principal; import org.eclipse.jetty.websocket.javax.common.UpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; public class JavaxServerUpgradeRequest implements UpgradeRequest { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java index de98982caac..e66f98c19ed 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketCreator.java @@ -37,9 +37,9 @@ import org.eclipse.jetty.websocket.javax.common.ConfiguredEndpoint; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketExtension; import org.eclipse.jetty.websocket.javax.common.ServerEndpointConfigWrapper; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketCreator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java index 5f7bd46f996..35a5ba4a86c 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerContainer.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient; import org.eclipse.jetty.websocket.core.exception.WebSocketException; import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientContainer; import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java index a31ea1741e6..bb81bbc3704 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JavaxWebSocketServerFrameHandlerFactory.java @@ -27,9 +27,9 @@ import org.eclipse.jetty.websocket.core.FrameHandler; import org.eclipse.jetty.websocket.javax.client.internal.JavaxWebSocketClientFrameHandlerFactory; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketContainer; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketFrameHandlerMetadata; -import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; public class JavaxWebSocketServerFrameHandlerFactory extends JavaxWebSocketClientFrameHandlerFactory implements FrameHandlerFactory { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java index be31158e1f2..dbb8e1f1fb3 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeRequest.java @@ -25,7 +25,7 @@ import java.util.Map; import javax.websocket.server.HandshakeRequest; import org.eclipse.jetty.http.pathmap.PathSpec; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; public class JsrHandshakeRequest implements HandshakeRequest { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeResponse.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeResponse.java index 8e25272dc36..80ec6a7502b 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeResponse.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/JsrHandshakeResponse.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Map; import javax.websocket.HandshakeResponse; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; public class JsrHandshakeResponse implements HandshakeResponse { diff --git a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamIdentifier.java b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamIdentifier.java index 3b26056b2cc..44410741311 100644 --- a/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamIdentifier.java +++ b/jetty-websocket/websocket-javax-server/src/main/java/org/eclipse/jetty/websocket/javax/server/internal/PathParamIdentifier.java @@ -22,6 +22,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.websocket.server.PathParam; +import org.eclipse.jetty.websocket.util.InvalidSignatureException; import org.eclipse.jetty.websocket.util.InvokerUtils; /** @@ -40,6 +41,7 @@ public class PathParamIdentifier implements InvokerUtils.ParamIdentifier { if (anno.annotationType().equals(PathParam.class)) { + validateType(paramType); PathParam pathParam = (PathParam)anno; return new InvokerUtils.Arg(paramType, pathParam.value()); } @@ -47,4 +49,22 @@ public class PathParamIdentifier implements InvokerUtils.ParamIdentifier } return new InvokerUtils.Arg(paramType); } + + /** + * The JSR356 rules for @PathParam only support + * String, Primitive Types (and their Boxed version) + */ + public static void validateType(Class type) + { + if (!String.class.isAssignableFrom(type) && + !Integer.TYPE.isAssignableFrom(type) && + !Long.TYPE.isAssignableFrom(type) && + !Short.TYPE.isAssignableFrom(type) && + !Float.TYPE.isAssignableFrom(type) && + !Double.TYPE.isAssignableFrom(type) && + !Boolean.TYPE.isAssignableFrom(type) && + !Character.TYPE.isAssignableFrom(type) && + !Byte.TYPE.isAssignableFrom(type)) + throw new InvalidSignatureException("Unsupported PathParam Type: " + type); + } } diff --git a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java index 01170fd2a8b..bc80b1a81ea 100644 --- a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java +++ b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/LocalServer.java @@ -27,7 +27,6 @@ import javax.websocket.OnMessage; import javax.websocket.server.ServerEndpoint; import org.eclipse.jetty.http.HttpVersion; -import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.MappedByteBufferPool; import org.eclipse.jetty.server.Handler; @@ -50,11 +49,6 @@ import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSession; import org.eclipse.jetty.websocket.javax.common.JavaxWebSocketSessionListener; import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerContainer; -import org.eclipse.jetty.websocket.javax.server.internal.JavaxWebSocketServerFrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -259,28 +253,6 @@ public class LocalServer extends ContainerLifeCycle implements LocalFuzzer.Provi servletContextHandler.addServlet(holder, urlPattern); } - public void registerWebSocket(String urlPattern, WebSocketCreator creator) - { - ServletHolder holder = new ServletHolder(new WebSocketServlet() - { - JavaxWebSocketServerFrameHandlerFactory factory = new JavaxWebSocketServerFrameHandlerFactory(JavaxWebSocketServerContainer.ensureContainer(getServletContext())); - - @Override - public void configure(WebSocketServletFactory factory) - { - PathSpec pathSpec = factory.parsePathSpec("/"); - factory.addMapping(pathSpec, creator); - } - - @Override - protected FrameHandlerFactory getFactory() - { - return factory; - } - }); - servletContextHandler.addServlet(holder, urlPattern); - } - public JavaxWebSocketServerContainer getServerContainer() { if (!servletContextHandler.isRunning()) diff --git a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java index 92a9fa0913b..a5682b72772 100644 --- a/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java +++ b/jetty-websocket/websocket-javax-tests/src/main/java/org/eclipse/jetty/websocket/javax/tests/WSServer.java @@ -50,112 +50,121 @@ import static org.hamcrest.Matchers.notNullValue; public class WSServer extends LocalServer implements LocalFuzzer.Provider { private static final Logger LOG = LoggerFactory.getLogger(WSServer.class); - private final Path contextDir; - private final String contextPath; - private ContextHandlerCollection contexts; - private Path webinf; - private Path classesDir; + private final Path testDir; + private ContextHandlerCollection contexts = new ContextHandlerCollection(); - public WSServer(File testdir, String contextName) + public WSServer(Path testDir) { - this(testdir.toPath(), contextName); + this.testDir = testDir; } - public WSServer(Path testdir, String contextName) + public WebApp createWebApp(String contextName) { - this.contextDir = testdir.resolve(contextName); - this.contextPath = "/" + contextName; - FS.ensureEmpty(contextDir); - } - - public void copyClass(Class clazz) throws Exception - { - ClassLoader cl = Thread.currentThread().getContextClassLoader(); - String endpointPath = TypeUtil.toClassReference(clazz); - URL classUrl = cl.getResource(endpointPath); - assertThat("Class URL for: " + clazz, classUrl, notNullValue()); - Path destFile = classesDir.resolve(endpointPath); - FS.ensureDirExists(destFile.getParent()); - File srcFile = new File(classUrl.toURI()); - IO.copy(srcFile, destFile.toFile()); - } - - public void copyEndpoint(Class endpointClass) throws Exception - { - copyClass(endpointClass); - } - - public void copyLib(Class clazz, String jarFileName) throws URISyntaxException, IOException - { - webinf = contextDir.resolve("WEB-INF"); - FS.ensureDirExists(webinf); - Path libDir = webinf.resolve("lib"); - FS.ensureDirExists(libDir); - Path jarFile = libDir.resolve(jarFileName); - - URL codeSourceURL = clazz.getProtectionDomain().getCodeSource().getLocation(); - assertThat("Class CodeSource URL is file scheme", codeSourceURL.getProtocol(), is("file")); - - File sourceCodeSourceFile = new File(codeSourceURL.toURI()); - if (sourceCodeSourceFile.isDirectory()) - { - LOG.info("Creating " + jarFile + " from " + sourceCodeSourceFile); - JAR.create(sourceCodeSourceFile, jarFile.toFile()); - } - else - { - LOG.info("Copying " + sourceCodeSourceFile + " to " + jarFile); - IO.copy(sourceCodeSourceFile, jarFile.toFile()); - } - } - - public void copyWebInf(String testResourceName) throws IOException - { - webinf = contextDir.resolve("WEB-INF"); - FS.ensureDirExists(webinf); - classesDir = webinf.resolve("classes"); - FS.ensureDirExists(classesDir); - Path webxml = webinf.resolve("web.xml"); - File testWebXml = MavenTestingUtils.getTestResourceFile(testResourceName); - IO.copy(testWebXml, webxml.toFile()); - } - - public WebAppContext createWebAppContext() throws IOException - { - WebAppContext context = new WebAppContext(); - context.setContextPath(this.contextPath); - context.setBaseResource(new PathResource(this.contextDir)); - context.setAttribute("org.eclipse.jetty.websocket.javax", Boolean.TRUE); - context.addConfiguration(new JavaxWebSocketConfiguration()); - return context; - } - - public void createWebInf() throws IOException - { - copyWebInf("empty-web.xml"); - } - - public void deployWebapp(WebAppContext webapp) throws Exception - { - contexts.addHandler(webapp); - contexts.manage(webapp); - webapp.setThrowUnavailableOnStartupException(true); - webapp.start(); - if (LOG.isDebugEnabled()) - { - LOG.debug("{}", webapp.dump()); - } - } - - public Path getWebAppDir() - { - return this.contextDir; + return new WebApp(contextName); } @Override - protected Handler createRootHandler(Server server) throws Exception + protected Handler createRootHandler(Server server) { - contexts = new ContextHandlerCollection(); return contexts; } + + public class WebApp + { + private final WebAppContext context; + private final Path contextDir; + private final Path webInf; + private final Path classesDir; + private final Path libDir; + + private WebApp(String contextName) + { + // Ensure context directory. + contextDir = testDir.resolve(contextName); + FS.ensureEmpty(contextDir); + + // Ensure WEB-INF directories. + webInf = contextDir.resolve("WEB-INF"); + FS.ensureDirExists(webInf); + classesDir = webInf.resolve("classes"); + FS.ensureDirExists(classesDir); + libDir = webInf.resolve("lib"); + FS.ensureDirExists(libDir); + + // Configure the WebAppContext. + context = new WebAppContext(); + context.setContextPath("/" + contextName); + context.setBaseResource(new PathResource(contextDir)); + context.setAttribute("org.eclipse.jetty.websocket.javax", Boolean.TRUE); + context.addConfiguration(new JavaxWebSocketConfiguration()); + } + + public WebAppContext getWebAppContext() + { + return context; + } + + public String getContextPath() + { + return context.getContextPath(); + } + + public Path getContextDir() + { + return contextDir; + } + + public void createWebInf() throws IOException + { + copyWebInf("empty-web.xml"); + } + + public void copyWebInf(String testResourceName) throws IOException + { + File testWebXml = MavenTestingUtils.getTestResourceFile(testResourceName); + Path webXml = webInf.resolve("web.xml"); + IO.copy(testWebXml, webXml.toFile()); + } + + public void copyClass(Class clazz) throws Exception + { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + String endpointPath = TypeUtil.toClassReference(clazz); + URL classUrl = cl.getResource(endpointPath); + assertThat("Class URL for: " + clazz, classUrl, notNullValue()); + Path destFile = classesDir.resolve(endpointPath); + FS.ensureDirExists(destFile.getParent()); + File srcFile = new File(classUrl.toURI()); + IO.copy(srcFile, destFile.toFile()); + } + + public void copyLib(Class clazz, String jarFileName) throws URISyntaxException, IOException + { + Path jarFile = libDir.resolve(jarFileName); + + URL codeSourceURL = clazz.getProtectionDomain().getCodeSource().getLocation(); + assertThat("Class CodeSource URL is file scheme", codeSourceURL.getProtocol(), is("file")); + + File sourceCodeSourceFile = new File(codeSourceURL.toURI()); + if (sourceCodeSourceFile.isDirectory()) + { + LOG.info("Creating " + jarFile + " from " + sourceCodeSourceFile); + JAR.create(sourceCodeSourceFile, jarFile.toFile()); + } + else + { + LOG.info("Copying " + sourceCodeSourceFile + " to " + jarFile); + IO.copy(sourceCodeSourceFile, jarFile.toFile()); + } + } + + public void deploy() + { + contexts.addHandler(context); + contexts.manage(context); + context.setThrowUnavailableOnStartupException(true); + if (LOG.isDebugEnabled()) + LOG.debug("{}", context.dump()); + } + } } diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/MessageReceivingTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/MessageReceivingTest.java index 5161bf2c05e..9699f2a3bc0 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/MessageReceivingTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/client/MessageReceivingTest.java @@ -47,7 +47,7 @@ import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.server.Negotiation; import org.eclipse.jetty.websocket.javax.tests.CoreServer; import org.eclipse.jetty.websocket.javax.tests.DataUtils; -import org.eclipse.jetty.websocket.util.TextUtil; +import org.eclipse.jetty.websocket.util.TextUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -328,7 +328,7 @@ public class MessageReceivingTest { if (LOG.isDebugEnabled()) { - LOG.debug("{}.onWholeText({})", EchoWholeMessageFrameHandler.class.getSimpleName(), TextUtil.hint(wholeMessage)); + LOG.debug("{}.onWholeText({})", EchoWholeMessageFrameHandler.class.getSimpleName(), TextUtils.hint(wholeMessage)); } sendText(wholeMessage, callback, false); diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java index e6c93a0ce17..31df6bdda45 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/AltFilterTest.java @@ -24,13 +24,13 @@ import java.util.List; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.javax.tests.Fuzzer; import org.eclipse.jetty.websocket.javax.tests.WSServer; import org.eclipse.jetty.websocket.javax.tests.server.sockets.echo.BasicEchoSocket; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,7 +39,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; /** - * Testing the use of an alternate {@link org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter} + * Testing the use of an alternate {@link WebSocketUpgradeFilter} * defined in the WEB-INF/web.xml */ @ExtendWith(WorkDirExtension.class) @@ -50,22 +50,21 @@ public class AltFilterTest @Test public void testEcho() throws Exception { - WSServer wsb = new WSServer(testdir.getPath(), "app"); - wsb.copyWebInf("alt-filter-web.xml"); + WSServer wsb = new WSServer(testdir.getPath()); + WSServer.WebApp app = wsb.createWebApp("app"); + app.copyWebInf("alt-filter-web.xml"); // the endpoint (extends javax.websocket.Endpoint) - wsb.copyClass(BasicEchoSocket.class); + app.copyClass(BasicEchoSocket.class); + app.deploy(); try { wsb.start(); - WebAppContext webapp = wsb.createWebAppContext(); - wsb.deployWebapp(webapp); - - FilterHolder filterWebXml = webapp.getServletHandler().getFilter("wsuf-test"); + FilterHolder filterWebXml = app.getWebAppContext().getServletHandler().getFilter("wsuf-test"); assertThat("Filter[wsuf-test]", filterWebXml, notNullValue()); - FilterHolder filterSCI = webapp.getServletHandler().getFilter("Jetty_WebSocketUpgradeFilter"); + FilterHolder filterSCI = app.getWebAppContext().getServletHandler().getFilter("Jetty_WebSocketUpgradeFilter"); assertThat("Filter[Jetty_WebSocketUpgradeFilter]", filterSCI, nullValue()); List send = new ArrayList<>(); diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java index 573ccd47b3c..82f3fd3f76c 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ConfiguratorTest.java @@ -115,7 +115,7 @@ public class ConfiguratorTest else { return "negotiatedExtensions=" + negotiatedExtensions.stream() - .map((ext) -> ext.getName()) + .map(Extension::getName) .collect(Collectors.joining(",", "[", "]")); } } @@ -198,7 +198,7 @@ public class ConfiguratorTest public static class UniqueUserPropsConfigurator extends ServerEndpointConfig.Configurator { - private AtomicInteger upgradeCount = new AtomicInteger(0); + private final AtomicInteger upgradeCount = new AtomicInteger(0); @Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) @@ -479,7 +479,7 @@ public class ConfiguratorTest FrameHandlerTracker clientSocket = new FrameHandlerTracker(); ClientUpgradeRequest upgradeRequest = ClientUpgradeRequest.from(client, wsUri, clientSocket); - upgradeRequest.header("X-Dummy", "Bogus"); + upgradeRequest.headers(headers -> headers.put("X-Dummy", "Bogus")); Future clientConnectFuture = client.connect(upgradeRequest); CoreSession coreSession = clientConnectFuture.get(Timeouts.CONNECT_MS, TimeUnit.MILLISECONDS); diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ContainerProviderServerTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ContainerProviderServerTest.java index 849d329c8d7..b11386ef4d5 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ContainerProviderServerTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/ContainerProviderServerTest.java @@ -28,7 +28,6 @@ import javax.websocket.WebSocketContainer; import javax.websocket.server.ServerEndpoint; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.javax.tests.EventSocket; import org.eclipse.jetty.websocket.javax.tests.WSServer; import org.junit.jupiter.api.AfterEach; @@ -61,12 +60,13 @@ public class ContainerProviderServerTest public void startServer() throws Exception { Path testdir = MavenTestingUtils.getTargetTestingPath(ContainerProviderServerTest.class.getName()); - server = new WSServer(testdir, "app"); - server.createWebInf(); - server.copyEndpoint(MySocket.class); + server = new WSServer(testdir); + WSServer.WebApp app = server.createWebApp("app"); + app.createWebInf(); + app.copyClass(MySocket.class); + app.deploy(); + server.start(); - WebAppContext webapp = server.createWebAppContext(); - server.deployWebapp(webapp); } @AfterEach diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentTest.java new file mode 100644 index 00000000000..2a30ea45c1e --- /dev/null +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/DeploymentTest.java @@ -0,0 +1,195 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.websocket.javax.tests.server; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import javax.websocket.CloseReason; +import javax.websocket.ContainerProvider; +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.WebSocketContainer; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +import org.eclipse.jetty.annotations.ServletContainerInitializersStarter; +import org.eclipse.jetty.logging.StacklessLogging; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.websocket.javax.tests.EventSocket; +import org.eclipse.jetty.websocket.javax.tests.WSServer; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.JRE; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DeploymentTest +{ + private WSServer server; + + @BeforeEach + public void startServer() throws Exception + { + Path testdir = MavenTestingUtils.getTargetTestingPath(DeploymentTest.class.getName()); + server = new WSServer(testdir); + } + + @AfterEach + public void stopServer() throws Exception + { + server.stop(); + } + + @Test + public void testBadPathParamSignature() throws Exception + { + WSServer.WebApp app1 = server.createWebApp("test1"); + app1.createWebInf(); + app1.copyClass(BadPathParamEndpoint.class); + app1.copyClass(DecodedString.class); + app1.copyClass(DeploymentTest.class); + app1.deploy(); + app1.getWebAppContext().setThrowUnavailableOnStartupException(false); + + try (StacklessLogging ignore = new StacklessLogging(ServletContainerInitializersStarter.class, WebAppContext.class)) + { + server.start(); + } + + WebSocketContainer client = ContainerProvider.getWebSocketContainer(); + EventSocket clientSocket = new EventSocket(); + + Throwable error = assertThrows(Throwable.class, () -> + client.connectToServer(clientSocket, server.getWsUri().resolve(app1.getContextPath() + "/badonclose/a"))); + assertThat(error, Matchers.instanceOf(IOException.class)); + assertThat(error.getMessage(), Matchers.containsString("503 Service Unavailable")); + } + + @Test + @DisabledOnJre(JRE.JAVA_14) // TODO: Waiting on JDK14 bug at https://bugs.openjdk.java.net/browse/JDK-8244090. + public void testDifferentWebAppsWithSameClassInSignature() throws Exception + { + WSServer.WebApp app1 = server.createWebApp("test1"); + app1.createWebInf(); + app1.copyClass(DecodedEndpoint.class); + app1.copyClass(StringDecoder.class); + app1.copyClass(DecodedString.class); + app1.copyClass(DeploymentTest.class); + app1.deploy(); + + WSServer.WebApp app2 = server.createWebApp("test2"); + app2.createWebInf(); + app2.copyClass(DecodedEndpoint.class); + app2.copyClass(StringDecoder.class); + app2.copyClass(DecodedString.class); + app2.copyClass(DeploymentTest.class); + app2.deploy(); + + server.start(); + WebSocketContainer client = ContainerProvider.getWebSocketContainer(); + EventSocket clientSocket = new EventSocket(); + + // Test echo and close to endpoint at /test1. + Session session = client.connectToServer(clientSocket, server.getWsUri().resolve("/test1")); + session.getAsyncRemote().sendText("hello world"); + assertThat(clientSocket.textMessages.poll(5, TimeUnit.SECONDS), is("hello world")); + session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientSocket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)); + + // Test echo and close to endpoint at /test2. + session = client.connectToServer(clientSocket, server.getWsUri().resolve("/test2")); + session.getAsyncRemote().sendText("hello world"); + assertThat(clientSocket.textMessages.poll(5, TimeUnit.SECONDS), is("hello world")); + session.close(); + assertTrue(clientSocket.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(clientSocket.closeReason.getCloseCode(), is(CloseReason.CloseCodes.NORMAL_CLOSURE)); + } + + @ServerEndpoint("/badonopen/{arg}") + public static class BadPathParamEndpoint + { + @OnOpen + public void onOpen(Session session, @PathParam("arg") DecodedString arg) + { + } + } + + @ServerEndpoint(value = "/", decoders = {StringDecoder.class}) + public static class DecodedEndpoint + { + @OnMessage + public void onMessage(Session session, DecodedString message) + { + session.getAsyncRemote().sendText(message.getString()); + } + } + + public static class DecodedString + { + public String string = ""; + + public DecodedString(String hold) + { + string = hold; + } + + public String getString() + { + return string; + } + } + + public static class StringDecoder implements Decoder.Text + { + @Override + public DecodedString decode(String s) throws DecodeException + { + return new DecodedString(s); + } + + @Override + public void init(EndpointConfig config) + { + } + + @Override + public void destroy() + { + } + + @Override + public boolean willDecode(String s) + { + return true; + } + } +} diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java index 57d9f839f37..b7e76497d18 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/EndpointViaConfigTest.java @@ -27,7 +27,6 @@ import com.acme.websocket.BasicEchoEndpointConfigContextListener; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -56,21 +55,20 @@ public class EndpointViaConfigTest @Test public void testEcho() throws Exception { - WSServer wsb = new WSServer(testdir.getPath(), "app"); - wsb.copyWebInf("basic-echo-endpoint-config-web.xml"); + WSServer wsb = new WSServer(testdir.getPath()); + WSServer.WebApp app = wsb.createWebApp("app"); + app.copyWebInf("basic-echo-endpoint-config-web.xml"); // the endpoint (extends javax.websocket.Endpoint) - wsb.copyClass(BasicEchoEndpoint.class); + app.copyClass(BasicEchoEndpoint.class); // the configuration (adds the endpoint) - wsb.copyClass(BasicEchoEndpointConfigContextListener.class); + app.copyClass(BasicEchoEndpointConfigContextListener.class); + app.deploy(); try { wsb.start(); URI uri = wsb.getWsUri(); - WebAppContext webapp = wsb.createWebAppContext(); - wsb.deployWebapp(webapp); - WebSocketCoreClient client = new WebSocketCoreClient(); try { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java index 4948dc66515..b4858f87b73 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/IdleTimeoutTest.java @@ -28,7 +28,6 @@ import com.acme.websocket.IdleTimeoutOnOpenEndpoint; import com.acme.websocket.IdleTimeoutOnOpenSocket; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CloseStatus; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -51,19 +50,18 @@ public class IdleTimeoutTest @BeforeAll public static void setupServer() throws Exception { - server = new WSServer(MavenTestingUtils.getTargetTestingPath(IdleTimeoutTest.class.getName()), "app"); - server.copyWebInf("idle-timeout-config-web.xml"); + server = new WSServer(MavenTestingUtils.getTargetTestingPath(IdleTimeoutTest.class.getName())); + WSServer.WebApp app = server.createWebApp("app"); + app.copyWebInf("idle-timeout-config-web.xml"); // the endpoint (extends javax.websocket.Endpoint) - server.copyClass(IdleTimeoutOnOpenEndpoint.class); + app.copyClass(IdleTimeoutOnOpenEndpoint.class); // the configuration that adds the endpoint - server.copyClass(IdleTimeoutContextListener.class); + app.copyClass(IdleTimeoutContextListener.class); // the annotated socket - server.copyClass(IdleTimeoutOnOpenSocket.class); + app.copyClass(IdleTimeoutOnOpenSocket.class); + app.deploy(); server.start(); - - WebAppContext webapp = server.createWebAppContext(); - server.deployWebapp(webapp); } @AfterAll diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java index 66d21d6a25d..f76b4683d1e 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeAnnotatedTest.java @@ -29,7 +29,6 @@ import javax.websocket.server.ServerEndpoint; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -63,18 +62,17 @@ public class LargeAnnotatedTest @Test public void testEcho() throws Exception { - WSServer wsb = new WSServer(testdir.getPath(), "app"); - wsb.createWebInf(); - wsb.copyEndpoint(LargeEchoConfiguredSocket.class); + WSServer wsb = new WSServer(testdir.getPath()); + WSServer.WebApp app = wsb.createWebApp("app"); + app.createWebInf(); + app.copyClass(LargeEchoConfiguredSocket.class); + app.deploy(); try { wsb.start(); URI uri = wsb.getWsUri(); - WebAppContext webapp = wsb.createWebAppContext(); - wsb.deployWebapp(webapp); - WebSocketCoreClient client = new WebSocketCoreClient(); try { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java index 541cc93b648..5025742f79c 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/LargeContainerTest.java @@ -28,7 +28,6 @@ import com.acme.websocket.LargeEchoDefaultSocket; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -53,18 +52,17 @@ public class LargeContainerTest @Test public void testEcho() throws Exception { - WSServer wsb = new WSServer(testdir.getPath(), "app"); - wsb.copyWebInf("large-echo-config-web.xml"); - wsb.copyEndpoint(LargeEchoDefaultSocket.class); + WSServer wsb = new WSServer(testdir.getPath()); + WSServer.WebApp app = wsb.createWebApp("app"); + app.copyWebInf("large-echo-config-web.xml"); + app.copyClass(LargeEchoDefaultSocket.class); + app.deploy(); try { wsb.start(); URI uri = wsb.getWsUri(); - WebAppContext webapp = wsb.createWebAppContext(); - wsb.deployWebapp(webapp); - WebSocketCoreClient client = new WebSocketCoreClient(); try { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java index 074205796b0..8da784ac364 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/OnMessageReturnTest.java @@ -31,7 +31,6 @@ import javax.websocket.server.ServerEndpoint; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -87,18 +86,17 @@ public class OnMessageReturnTest @Test public void testEchoReturn() throws Exception { - WSServer wsb = new WSServer(testdir.getPath(), "app"); - wsb.copyWebInf("empty-web.xml"); - wsb.copyClass(EchoReturnEndpoint.class); + WSServer wsb = new WSServer(testdir.getPath()); + WSServer.WebApp app = wsb.createWebApp("app"); + app.copyWebInf("empty-web.xml"); + app.copyClass(EchoReturnEndpoint.class); + app.deploy(); try { wsb.start(); URI uri = wsb.getWsUri(); - WebAppContext webapp = wsb.createWebAppContext(); - wsb.deployWebapp(webapp); - WebSocketCoreClient client = new WebSocketCoreClient(); try { diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java index 4d548a2f05b..0e63b4a9314 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/PingPongTest.java @@ -30,7 +30,6 @@ import com.acme.websocket.PongMessageEndpoint; import com.acme.websocket.PongSocket; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.Callback; -import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.OpCode; @@ -55,17 +54,16 @@ public class PingPongTest public static void startServer() throws Exception { Path testdir = MavenTestingUtils.getTargetTestingPath(PingPongTest.class.getName()); - server = new WSServer(testdir, "app"); - server.copyWebInf("pong-config-web.xml"); + server = new WSServer(testdir); - server.copyClass(PongContextListener.class); - server.copyClass(PongMessageEndpoint.class); - server.copyClass(PongSocket.class); + WSServer.WebApp app = server.createWebApp("app"); + app.copyWebInf("pong-config-web.xml"); + app.copyClass(PongContextListener.class); + app.copyClass(PongMessageEndpoint.class); + app.copyClass(PongSocket.class); + app.deploy(); server.start(); - - WebAppContext webapp = server.createWebAppContext(); - server.deployWebapp(webapp); } @BeforeAll diff --git a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebAppClassLoaderTest.java b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebAppClassLoaderTest.java index 9e40e26ab78..fc37d260962 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebAppClassLoaderTest.java +++ b/jetty-websocket/websocket-javax-tests/src/test/java/org/eclipse/jetty/websocket/javax/tests/server/WebAppClassLoaderTest.java @@ -93,12 +93,13 @@ public class WebAppClassLoaderTest public void startServer() throws Exception { Path testdir = MavenTestingUtils.getTargetTestingPath(WebAppClassLoaderTest.class.getName()); - server = new WSServer(testdir, "app"); - server.createWebInf(); - server.copyEndpoint(MySocket.class); + server = new WSServer(testdir); + WSServer.WebApp app = server.createWebApp("app"); + app.createWebInf(); + app.copyClass(MySocket.class); + app.deploy(); + webapp = app.getWebAppContext(); server.start(); - webapp = server.createWebAppContext(); - server.deployWebapp(webapp); } @AfterEach diff --git a/jetty-websocket/websocket-javax-tests/src/test/resources/alt-filter-web.xml b/jetty-websocket/websocket-javax-tests/src/test/resources/alt-filter-web.xml index f1723581a3a..986106295e3 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/resources/alt-filter-web.xml +++ b/jetty-websocket/websocket-javax-tests/src/test/resources/alt-filter-web.xml @@ -13,7 +13,7 @@ wsuf-test - org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter + org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter diff --git a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-alt-config-via-listener.xml b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-alt-config-via-listener.xml index 1479493aff3..647d0947663 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-alt-config-via-listener.xml +++ b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-alt-config-via-listener.xml @@ -12,7 +12,7 @@ wsuf-alt - org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter + org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter configAttributeKey alt.config diff --git a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-listener.xml b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-listener.xml index d9811f6e0e5..595de89a035 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-listener.xml +++ b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-listener.xml @@ -16,7 +16,7 @@ wsuf - org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter + org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter diff --git a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-servlet-init.xml b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-servlet-init.xml index f8c8fbf61e9..56a333bfee1 100644 --- a/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-servlet-init.xml +++ b/jetty-websocket/websocket-javax-tests/src/test/resources/wsuf-config-via-servlet-init.xml @@ -18,7 +18,7 @@ wsuf - org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter + org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter diff --git a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/EndPoints.java b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/EndPoints.java index 373e0dd7ccc..4ec9847e0af 100644 --- a/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/EndPoints.java +++ b/jetty-websocket/websocket-jetty-common/src/test/java/org/eclipse/jetty/websocket/common/EndPoints.java @@ -38,7 +38,7 @@ import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.core.CloseStatus; -import org.eclipse.jetty.websocket.util.TextUtil; +import org.eclipse.jetty.websocket.util.TextUtils; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; @@ -62,7 +62,7 @@ public class EndPoints @Override public void onWebSocketClose(int statusCode, String reason) { - events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtil.quote(reason)); + events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtils.quote(reason)); } @Override @@ -74,13 +74,13 @@ public class EndPoints @Override public void onWebSocketError(Throwable cause) { - events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtil.quote(cause.getMessage())); + events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtils.quote(cause.getMessage())); } @Override public void onWebSocketText(String message) { - events.add("onWebSocketText(%s)", TextUtil.quote(message)); + events.add("onWebSocketText(%s)", TextUtils.quote(message)); } } @@ -91,7 +91,7 @@ public class EndPoints @Override public void onWebSocketClose(int statusCode, String reason) { - events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtil.quote(reason)); + events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtils.quote(reason)); } @Override @@ -103,7 +103,7 @@ public class EndPoints @Override public void onWebSocketError(Throwable cause) { - events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtil.quote(cause.getMessage())); + events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtils.quote(cause.getMessage())); } @Override @@ -120,7 +120,7 @@ public class EndPoints @Override public void onWebSocketClose(int statusCode, String reason) { - events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtil.quote(reason)); + events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtils.quote(reason)); } @Override @@ -132,13 +132,13 @@ public class EndPoints @Override public void onWebSocketError(Throwable cause) { - events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtil.quote(cause.getMessage())); + events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtils.quote(cause.getMessage())); } @Override public void onWebSocketPartialText(String payload, boolean fin) { - events.add("onWebSocketPartialText(%s, %b)", TextUtil.quote(payload), fin); + events.add("onWebSocketPartialText(%s, %b)", TextUtils.quote(payload), fin); } @Override @@ -155,7 +155,7 @@ public class EndPoints @Override public void onWebSocketClose(int statusCode, String reason) { - events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtil.quote(reason)); + events.add("onWebSocketClose(%s, %s)", CloseStatus.codeString(statusCode), TextUtils.quote(reason)); } @Override @@ -167,7 +167,7 @@ public class EndPoints @Override public void onWebSocketError(Throwable cause) { - events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtil.quote(cause.getMessage())); + events.add("onWebSocketError((%s) %s)", cause.getClass().getSimpleName(), TextUtils.quote(cause.getMessage())); } @Override @@ -228,7 +228,7 @@ public class EndPoints @OnWebSocketClose public void onClose(int statusCode, String reason) { - events.add("onClose(%d, %s)", statusCode, TextUtil.quote(reason)); + events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason)); } @OnWebSocketConnect @@ -253,7 +253,7 @@ public class EndPoints @OnWebSocketClose public void onClose(int statusCode, String reason) { - events.add("onClose(%d, %s)", statusCode, TextUtil.quote(reason)); + events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason)); } @OnWebSocketConnect @@ -271,7 +271,7 @@ public class EndPoints @OnWebSocketClose public void onClose(int statusCode, String reason) { - events.add("onClose(%d, %s)", statusCode, TextUtil.quote(reason)); + events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason)); } @OnWebSocketConnect @@ -289,7 +289,7 @@ public class EndPoints @OnWebSocketMessage public void onText(String message) { - events.add("onText(%s)", TextUtil.quote(message)); + events.add("onText(%s)", TextUtils.quote(message)); } } @@ -301,7 +301,7 @@ public class EndPoints @OnWebSocketClose public void onClose(int statusCode, String reason) { - events.add("onClose(%d, %s)", statusCode, TextUtil.quote(reason)); + events.add("onClose(%d, %s)", statusCode, TextUtils.quote(reason)); } @OnWebSocketConnect diff --git a/jetty-websocket/websocket-jetty-server/pom.xml b/jetty-websocket/websocket-jetty-server/pom.xml index 811fa6c6f89..ade9576c6e7 100644 --- a/jetty-websocket/websocket-jetty-server/pom.xml +++ b/jetty-websocket/websocket-jetty-server/pom.xml @@ -48,7 +48,7 @@ org.eclipse.jetty.websocket - websocket-servlet + websocket-util-server ${project.version} diff --git a/jetty-websocket/websocket-jetty-server/src/main/config/modules/websocket-jetty.mod b/jetty-websocket/websocket-jetty-server/src/main/config/modules/websocket-jetty.mod index 1f3f6189a64..9a93574f2bf 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/config/modules/websocket-jetty.mod +++ b/jetty-websocket/websocket-jetty-server/src/main/config/modules/websocket-jetty.mod @@ -12,8 +12,8 @@ annotations [lib] lib/websocket/websocket-core-${jetty.version}.jar -lib/websocket/websocket-servlet-${jetty.version}.jar lib/websocket/websocket-util-${jetty.version}.jar +lib/websocket/websocket-util-server-${jetty.version}.jar lib/websocket/websocket-jetty-api-${jetty.version}.jar lib/websocket/websocket-jetty-common-${jetty.version}.jar lib/websocket/websocket-jetty-server-${jetty.version}.jar diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/module-info.java b/jetty-websocket/websocket-jetty-server/src/main/java/module-info.java index 9686cfd3f61..6156e8d5a30 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/module-info.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/module-info.java @@ -29,10 +29,10 @@ module org.eclipse.jetty.websocket.jetty.server requires jetty.servlet.api; requires org.eclipse.jetty.websocket.jetty.common; + requires org.eclipse.jetty.websocket.util.server; + requires org.slf4j; requires transitive org.eclipse.jetty.webapp; requires transitive org.eclipse.jetty.websocket.jetty.api; - requires transitive org.eclipse.jetty.websocket.servlet; - requires org.slf4j; // Only required if using JMX. requires static org.eclipse.jetty.jmx; diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java index 052b458c95f..13e0b330d61 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeRequest.java @@ -36,7 +36,7 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.common.JettyExtensionConfig; import org.eclipse.jetty.websocket.core.server.Negotiation; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; public class JettyServerUpgradeRequest { diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java index dcd144613a0..01e015302ed 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyServerUpgradeResponse.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.common.JettyExtensionConfig; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; public class JettyServerUpgradeResponse { diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java index faf36ac1a02..06a614efec2 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServerContainer.java @@ -41,8 +41,8 @@ import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.core.exception.WebSocketException; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.server.internal.JettyServerFrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; +import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java index b2880acbb31..448694577c5 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServlet.java @@ -18,23 +18,101 @@ package org.eclipse.jetty.websocket.server; -import org.eclipse.jetty.websocket.server.internal.JettyServerFrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.WebSocketServlet; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.time.Duration; +import java.util.Set; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; -public abstract class JettyWebSocketServlet extends WebSocketServlet +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.websocket.core.Configuration; +import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.server.internal.JettyServerFrameHandlerFactory; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketCreator; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract Servlet used to bridge the Servlet API to the WebSocket API. + *

+ * To use this servlet, you will be required to register your websockets with the {@link WebSocketMapping} so that it can create your websockets under the + * appropriate conditions. + *

+ *

The most basic implementation would be as follows:

+ *
+ * package my.example;
+ *
+ * import JettyWebSocketServlet;
+ * import JettyWebSocketServletFactory;
+ *
+ * public class MyEchoServlet extends JettyWebSocketServlet
+ * {
+ *     @Override
+ *     public void configure(JettyWebSocketServletFactory factory)
+ *     {
+ *       factory.setDefaultMaxFrameSize(4096);
+ *       factory.addMapping(factory.parsePathSpec("/"), (req,res)->new EchoSocket());
+ *     }
+ * }
+ * 
+ *

+ * Only request that conforms to a "WebSocket: Upgrade" handshake request will trigger the {@link WebSocketMapping} handling of creating + * WebSockets. All other requests are treated as normal servlet requests. The configuration defined by this servlet init parameters will + * be used as the customizer for any mappings created by {@link JettyWebSocketServletFactory#addMapping(String, JettyWebSocketCreator)} during + * {@link #configure(JettyWebSocketServletFactory)} calls. The request upgrade may be peformed by this servlet, or is may be performed by a + * {@link WebSocketUpgradeFilter} instance that will share the same {@link WebSocketMapping} instance. If the filter is used, then the + * filter configuraton is used as the default configuration prior to this servlets configuration being applied. + *

+ *

+ * Configuration / Init-Parameters: + *

+ *
+ *
idleTimeout
+ *
set the time in ms that a websocket may be idle before closing
+ *
maxTextMessageSize
+ *
set the size in UTF-8 bytes that a websocket may be accept as a Text Message before closing
+ *
maxBinaryMessageSize
+ *
set the size in bytes that a websocket may be accept as a Binary Message before closing
+ *
inputBufferSize
+ *
set the size in bytes of the buffer used to read raw bytes from the network layer
*
outputBufferSize
+ *
set the size in bytes of the buffer used to write bytes to the network layer
+ *
maxFrameSize
+ *
The maximum frame size sent or received.
+ *
autoFragment
+ *
If true, frames are automatically fragmented to respect the maximum frame size.
+ *
+ */ +public abstract class JettyWebSocketServlet extends HttpServlet { + private static final Logger LOG = LoggerFactory.getLogger(JettyWebSocketServlet.class); + private final CustomizedWebSocketServletFactory customizer = new CustomizedWebSocketServletFactory(); + + private WebSocketMapping mapping; + private WebSocketComponents components; + + /** + * Configure the JettyWebSocketServletFactory for this servlet instance by setting default + * configuration (which may be overriden by annotations) and mapping {@link JettyWebSocketCreator}s. + * This method assumes a single {@link FrameHandlerFactory} will be available as a bean on the + * {@link ContextHandler}, which in practise will mostly the the Jetty WebSocket API factory. + * + * @param factory the JettyWebSocketServletFactory + */ protected abstract void configure(JettyWebSocketServletFactory factory); - @Override - protected final void configure(WebSocketServletFactory factory) - { - configure(new JettyWebSocketServletFactory(factory)); - } - - @Override - protected FrameHandlerFactory getFactory() + /** + * @return the instance of {@link FrameHandlerFactory} to be used to create the FrameHandler + */ + private FrameHandlerFactory getFactory() { JettyServerFrameHandlerFactory frameHandlerFactory = JettyServerFrameHandlerFactory.getFactory(getServletContext()); @@ -43,4 +121,163 @@ public abstract class JettyWebSocketServlet extends WebSocketServlet return frameHandlerFactory; } + + @Override + public void init() throws ServletException + { + try + { + ServletContext servletContext = getServletContext(); + + components = WebSocketComponents.ensureWebSocketComponents(servletContext); + mapping = new WebSocketMapping(components); + + String max = getInitParameter("idleTimeout"); + if (max == null) + { + max = getInitParameter("maxIdleTime"); + if (max != null) + LOG.warn("'maxIdleTime' init param is deprecated, use 'idleTimeout' instead"); + } + if (max != null) + customizer.setIdleTimeout(Duration.ofMillis(Long.parseLong(max))); + + max = getInitParameter("maxTextMessageSize"); + if (max != null) + customizer.setMaxTextMessageSize(Long.parseLong(max)); + + max = getInitParameter("maxBinaryMessageSize"); + if (max != null) + customizer.setMaxBinaryMessageSize(Long.parseLong(max)); + + max = getInitParameter("inputBufferSize"); + if (max != null) + customizer.setInputBufferSize(Integer.parseInt(max)); + + max = getInitParameter("outputBufferSize"); + if (max != null) + customizer.setOutputBufferSize(Integer.parseInt(max)); + + max = getInitParameter("maxFrameSize"); + if (max == null) + max = getInitParameter("maxAllowedFrameSize"); + if (max != null) + customizer.setMaxFrameSize(Long.parseLong(max)); + + String autoFragment = getInitParameter("autoFragment"); + if (autoFragment != null) + customizer.setAutoFragment(Boolean.parseBoolean(autoFragment)); + + configure(customizer); // Let user modify customizer prior after init params + } + catch (Throwable x) + { + throw new ServletException(x); + } + } + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException + { + // provide a null default customizer the customizer will be on the negotiator in the mapping + if (mapping.upgrade(req, resp, null)) + return; + + // If we reach this point, it means we had an incoming request to upgrade + // but it was either not a proper websocket upgrade, or it was possibly rejected + // due to incoming request constraints (controlled by WebSocketCreator) + if (resp.isCommitted()) + return; + + // Handle normally + super.service(req, resp); + } + + private class CustomizedWebSocketServletFactory extends Configuration.ConfigurationCustomizer implements JettyWebSocketServletFactory + { + @Override + public Set getAvailableExtensionNames() + { + return components.getExtensionRegistry().getAvailableExtensionNames(); + } + + @Override + public void addMapping(String pathSpec, JettyWebSocketCreator creator) + { + mapping.addMapping(WebSocketMapping.parsePathSpec(pathSpec), new WrappedJettyCreator(creator), getFactory(), this); + } + + @Override + public void register(Class endpointClass) + { + Constructor constructor; + try + { + constructor = endpointClass.getDeclaredConstructor(); + } + catch (NoSuchMethodException e) + { + throw new RuntimeException(e); + } + + JettyWebSocketCreator creator = (req, resp) -> + { + try + { + return constructor.newInstance(); + } + catch (Throwable t) + { + t.printStackTrace(); + return null; + } + }; + + addMapping("/", creator); + } + + @Override + public void setCreator(JettyWebSocketCreator creator) + { + addMapping("/", creator); + } + + @Override + public JettyWebSocketCreator getMapping(String pathSpec) + { + WebSocketCreator creator = mapping.getMapping(WebSocketMapping.parsePathSpec(pathSpec)); + if (creator instanceof WrappedJettyCreator) + return ((WrappedJettyCreator)creator).getJettyWebSocketCreator(); + + return null; + } + + @Override + public boolean removeMapping(String pathSpec) + { + return mapping.removeMapping(WebSocketMapping.parsePathSpec(pathSpec)); + } + } + + private static class WrappedJettyCreator implements WebSocketCreator + { + private JettyWebSocketCreator creator; + + private WrappedJettyCreator(JettyWebSocketCreator creator) + { + this.creator = creator; + } + + private JettyWebSocketCreator getJettyWebSocketCreator() + { + return creator; + } + + @Override + public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) + { + return creator.createWebSocket(new JettyServerUpgradeRequest(req), new JettyServerUpgradeResponse(resp)); + } + } } diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java index c27d5245bca..bad07937441 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/JettyWebSocketServletFactory.java @@ -18,123 +18,15 @@ package org.eclipse.jetty.websocket.server; -import java.time.Duration; import java.util.Set; -import org.eclipse.jetty.http.pathmap.PathSpec; import org.eclipse.jetty.websocket.api.WebSocketBehavior; import org.eclipse.jetty.websocket.api.WebSocketPolicy; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; -import org.eclipse.jetty.websocket.servlet.WebSocketCreator; -import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; -public class JettyWebSocketServletFactory implements WebSocketPolicy +public interface JettyWebSocketServletFactory extends WebSocketPolicy { - private WebSocketServletFactory factory; - - JettyWebSocketServletFactory(WebSocketServletFactory factory) - { - this.factory = factory; - } - - public Set getAvailableExtensionNames() - { - return factory.getExtensionRegistry().getAvailableExtensionNames(); - } - - @Override - public WebSocketBehavior getBehavior() - { - return WebSocketBehavior.SERVER; - } - - @Override - public boolean isAutoFragment() - { - return factory.isAutoFragment(); - } - - @Override - public void setAutoFragment(boolean autoFragment) - { - factory.setAutoFragment(autoFragment); - } - - @Override - public long getMaxFrameSize() - { - return factory.getMaxFrameSize(); - } - - @Override - public void setMaxFrameSize(long maxFrameSize) - { - factory.setMaxFrameSize(maxFrameSize); - } - - @Override - public Duration getIdleTimeout() - { - return factory.getIdleTimeout(); - } - - @Override - public void setIdleTimeout(Duration duration) - { - factory.setIdleTimeout(duration); - } - - @Override - public int getInputBufferSize() - { - return factory.getInputBufferSize(); - } - - @Override - public void setInputBufferSize(int bufferSize) - { - factory.setInputBufferSize(bufferSize); - } - - @Override - public long getMaxBinaryMessageSize() - { - return factory.getMaxBinaryMessageSize(); - } - - @Override - public void setMaxBinaryMessageSize(long bufferSize) - { - factory.setMaxBinaryMessageSize(bufferSize); - } - - @Override - public long getMaxTextMessageSize() - { - return factory.getMaxTextMessageSize(); - } - - @Override - public void setMaxTextMessageSize(long bufferSize) - { - factory.setMaxTextMessageSize(bufferSize); - } - - @Override - public int getOutputBufferSize() - { - return factory.getOutputBufferSize(); - } - - @Override - public void setOutputBufferSize(int bufferSize) - { - factory.setOutputBufferSize(bufferSize); - } - /** - * add a WebSocket mapping to a provided {@link JettyWebSocketCreator}. + * Add a WebSocket mapping to a provided {@link JettyWebSocketCreator}. *

* If mapping is added before this configuration is started, then it is persisted through * stop/start of this configuration's lifecycle. Otherwise it will be removed when @@ -145,30 +37,21 @@ public class JettyWebSocketServletFactory implements WebSocketPolicy * @param creator the WebSocketCreator to use * @since 10.0 */ - public void addMapping(String pathSpec, JettyWebSocketCreator creator) - { - factory.addMapping(pathSpec, new WrappedCreator(creator)); - } + void addMapping(String pathSpec, JettyWebSocketCreator creator); /** * Add a WebSocket mapping at PathSpec "/" for a creator which creates the endpointClass * * @param endpointClass the WebSocket class to use */ - public void register(Class endpointClass) - { - factory.register(endpointClass); - } + void register(Class endpointClass); /** * Add a WebSocket mapping at PathSpec "/" for a creator * * @param creator the WebSocketCreator to use */ - public void setCreator(JettyWebSocketCreator creator) - { - factory.setCreator(new WrappedCreator(creator)); - } + void setCreator(JettyWebSocketCreator creator); /** * Returns the creator for the given path spec. @@ -176,14 +59,7 @@ public class JettyWebSocketServletFactory implements WebSocketPolicy * @param pathSpec the pathspec to respond on * @return the websocket creator if path spec exists, or null */ - public JettyWebSocketCreator getMapping(String pathSpec) - { - WebSocketCreator creator = factory.getMapping(parsePathSpec(pathSpec)); - if (creator instanceof WrappedCreator) - return ((WrappedCreator)creator).getCreator(); - - return null; - } + JettyWebSocketCreator getMapping(String pathSpec); /** * Removes the mapping based on the given path spec. @@ -191,40 +67,17 @@ public class JettyWebSocketServletFactory implements WebSocketPolicy * @param pathSpec the pathspec to respond on * @return true if underlying mapping were altered, false otherwise */ - public boolean removeMapping(String pathSpec) - { - return factory.removeMapping(parsePathSpec(pathSpec)); - } + boolean removeMapping(String pathSpec); /** - * Parse a PathSpec string into a PathSpec instance. - * - * @param rawSpec the raw path spec as String to parse. - * @return the {@link PathSpec} implementation for the rawSpec + * Get the names of all available WebSocket Extensions. + * @return a set the available extension names. */ - private PathSpec parsePathSpec(String rawSpec) + Set getAvailableExtensionNames(); + + @Override + default WebSocketBehavior getBehavior() { - return factory.parsePathSpec(rawSpec); - } - - private static class WrappedCreator implements WebSocketCreator - { - private JettyWebSocketCreator creator; - - private WrappedCreator(JettyWebSocketCreator creator) - { - this.creator = creator; - } - - public JettyWebSocketCreator getCreator() - { - return creator; - } - - @Override - public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp) - { - return creator.createWebSocket(new JettyServerUpgradeRequest(req), new JettyServerUpgradeResponse(resp)); - } + return WebSocketBehavior.SERVER; } } diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketConfiguration.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketConfiguration.java index c0a54dddef8..a2caa2480b2 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketConfiguration.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketConfiguration.java @@ -58,7 +58,8 @@ public class JettyWebSocketConfiguration extends AbstractConfiguration protectAndExpose( "org.eclipse.jetty.websocket.api.", - "org.eclipse.jetty.websocket.server."); + "org.eclipse.jetty.websocket.server.", + "org.eclipse.jetty.websocket.util.server."); // For WebSocketUpgradeFilter hide("org.eclipse.jetty.server.internal.", "org.eclipse.jetty.server.config."); diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java index 82c3f79383a..43b65eafd16 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/config/JettyWebSocketServletContainerInitializer.java @@ -27,8 +27,8 @@ import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.listener.ContainerInitializer; import org.eclipse.jetty.websocket.core.WebSocketComponents; import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; -import org.eclipse.jetty.websocket.servlet.WebSocketMapping; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyServerFrameHandlerFactory.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyServerFrameHandlerFactory.java index 6c3acd2ea91..ce4d9989c64 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyServerFrameHandlerFactory.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/JettyServerFrameHandlerFactory.java @@ -27,9 +27,9 @@ import org.eclipse.jetty.websocket.api.WebSocketContainer; import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandler; import org.eclipse.jetty.websocket.common.JettyWebSocketFrameHandlerFactory; import org.eclipse.jetty.websocket.core.FrameHandler; -import org.eclipse.jetty.websocket.servlet.FrameHandlerFactory; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.FrameHandlerFactory; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; public class JettyServerFrameHandlerFactory extends JettyWebSocketFrameHandlerFactory diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeRequestAdapter.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeRequestAdapter.java index fb72a261005..bddf712cf15 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeRequestAdapter.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeRequestAdapter.java @@ -28,7 +28,7 @@ import java.util.stream.Collectors; import org.eclipse.jetty.websocket.api.UpgradeRequest; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.common.JettyExtensionConfig; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeRequest; public class UpgradeRequestAdapter implements UpgradeRequest { diff --git a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java index 4e9eefed389..586d78e91c1 100644 --- a/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java +++ b/jetty-websocket/websocket-jetty-server/src/main/java/org/eclipse/jetty/websocket/server/internal/UpgradeResponseAdapter.java @@ -27,7 +27,7 @@ import java.util.stream.Collectors; import org.eclipse.jetty.websocket.api.UpgradeResponse; import org.eclipse.jetty.websocket.api.extensions.ExtensionConfig; import org.eclipse.jetty.websocket.common.JettyExtensionConfig; -import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse; +import org.eclipse.jetty.websocket.util.server.internal.ServletUpgradeResponse; public class UpgradeResponseAdapter implements UpgradeResponse { diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ErrorCloseTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ErrorCloseTest.java index 0e077b8d7ff..86dc8240721 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ErrorCloseTest.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/ErrorCloseTest.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.common.WebSocketSession; import org.eclipse.jetty.websocket.core.internal.WebSocketCoreSession; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java index aad53455047..744449a7741 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/WebSocketOverHTTP2Test.java @@ -64,7 +64,7 @@ import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.internal.UpgradeHttpServletRequest; +import org.eclipse.jetty.websocket.util.server.internal.UpgradeHttpServletRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java index bb6189b06d7..cfdef700f31 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/ClientConnectTest.java @@ -45,11 +45,11 @@ import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.eclipse.jetty.websocket.tests.EchoSocket; import org.eclipse.jetty.websocket.tests.GetAuthHeaderEndpoint; import org.eclipse.jetty.websocket.tests.SimpleStatusServlet; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; import org.hamcrest.Matcher; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java index 6b10557e870..3725abbb25b 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/client/WebSocketClientTest.java @@ -44,13 +44,13 @@ import org.eclipse.jetty.websocket.api.util.WSURI; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; import org.eclipse.jetty.websocket.tests.AnnoMaxMessageEndpoint; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; import org.eclipse.jetty.websocket.tests.ConnectMessageEndpoint; import org.eclipse.jetty.websocket.tests.EchoSocket; import org.eclipse.jetty.websocket.tests.ParamsEndpoint; import org.eclipse.jetty.websocket.tests.util.FutureWriteCallback; +import org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; diff --git a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/PartialListenerTest.java b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/PartialListenerTest.java index 01ee6ed0346..36bc27041e5 100644 --- a/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/PartialListenerTest.java +++ b/jetty-websocket/websocket-jetty-tests/src/test/java/org/eclipse/jetty/websocket/tests/server/PartialListenerTest.java @@ -47,7 +47,7 @@ import org.eclipse.jetty.websocket.server.JettyWebSocketServlet; import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.eclipse.jetty.websocket.tests.CloseTrackingEndpoint; -import org.eclipse.jetty.websocket.util.TextUtil; +import org.eclipse.jetty.websocket.util.TextUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -293,7 +293,7 @@ public class PartialListenerTest @Override public void onWebSocketPartialText(String payload, boolean fin) { - partialEvents.offer(String.format("TEXT[payload=%s, fin=%b]", TextUtil.maxStringLength(30, payload), fin)); + partialEvents.offer(String.format("TEXT[payload=%s, fin=%b]", TextUtils.maxStringLength(30, payload), fin)); } } } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java deleted file mode 100644 index 6a3e00a84bf..00000000000 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServlet.java +++ /dev/null @@ -1,263 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.websocket.servlet; - -import java.io.IOException; -import java.lang.reflect.Constructor; -import java.time.Duration; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.http.pathmap.PathSpec; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.websocket.core.Configuration; -import org.eclipse.jetty.websocket.core.WebSocketComponents; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Abstract Servlet used to bridge the Servlet API to the WebSocket API. - *

- * To use this servlet, you will be required to register your websockets with the {@link WebSocketMapping} so that it can create your websockets under the - * appropriate conditions. - *

- *

The most basic implementation would be as follows:

- *
- * package my.example;
- *
- * import WebSocketServlet;
- * import WebSocketServletFactory;
- *
- * public class MyEchoServlet extends WebSocketServlet
- * {
- *     @Override
- *     public void configure(WebSocketServletFactory factory)
- *     {
- *       factory.setDefaultMaxFrameSize(4096);
- *       factory.addMapping(factory.parsePathSpec("/"), (req,res)->new EchoSocket());
- *     }
- * }
- * 
- *

- * Only request that conforms to a "WebSocket: Upgrade" handshake request will trigger the {@link WebSocketMapping} handling of creating - * WebSockets. All other requests are treated as normal servlet requests. The configuration defined by this servlet init parameters will - * be used as the customizer for any mappings created by {@link WebSocketServletFactory#addMapping(PathSpec, WebSocketCreator)} during - * {@link #configure(WebSocketServletFactory)} calls. The request upgrade may be peformed by this servlet, or is may be performed by a - * {@link WebSocketUpgradeFilter} instance that will share the same {@link WebSocketMapping} instance. If the filter is used, then the - * filter configuraton is used as the default configuration prior to this servlets configuration being applied. - *

- *

- * Configuration / Init-Parameters: - *

- *
- *
idleTimeout
- *
set the time in ms that a websocket may be idle before closing
- *
maxTextMessageSize
- *
set the size in UTF-8 bytes that a websocket may be accept as a Text Message before closing
- *
maxBinaryMessageSize
- *
set the size in bytes that a websocket may be accept as a Binary Message before closing
- *
inputBufferSize
- *
set the size in bytes of the buffer used to read raw bytes from the network layer
*
outputBufferSize
- *
set the size in bytes of the buffer used to write bytes to the network layer
- *
maxFrameSize
- *
The maximum frame size sent or received.
- *
autoFragment
- *
If true, frames are automatically fragmented to respect the maximum frame size.
- *
- */ -@SuppressWarnings("serial") -public abstract class WebSocketServlet extends HttpServlet -{ - private static final Logger LOG = LoggerFactory.getLogger(WebSocketServlet.class); - private final CustomizedWebSocketServletFactory customizer = new CustomizedWebSocketServletFactory(); - - private WebSocketMapping mapping; - private WebSocketComponents components; - - /** - * Configure the WebSocketServletFactory for this servlet instance by setting default - * configuration (which may be overriden by annotations) and mapping {@link WebSocketCreator}s. - * This method assumes a single {@link FrameHandlerFactory} will be available as a bean on the - * {@link ContextHandler}, which in practise will mostly the the Jetty WebSocket API factory. - * - * @param factory the WebSocketServletFactory - */ - protected abstract void configure(WebSocketServletFactory factory); - - /** - * @return the instance of {@link FrameHandlerFactory} to be used to create the FrameHandler - */ - protected abstract FrameHandlerFactory getFactory(); - - @Override - public void init() throws ServletException - { - try - { - ServletContext servletContext = getServletContext(); - - components = WebSocketComponents.ensureWebSocketComponents(servletContext); - mapping = new WebSocketMapping(components); - - String max = getInitParameter("idleTimeout"); - if (max == null) - { - max = getInitParameter("maxIdleTime"); - if (max != null) - LOG.warn("'maxIdleTime' init param is deprecated, use 'idleTimeout' instead"); - } - if (max != null) - customizer.setIdleTimeout(Duration.ofMillis(Long.parseLong(max))); - - max = getInitParameter("maxTextMessageSize"); - if (max != null) - customizer.setMaxTextMessageSize(Long.parseLong(max)); - - max = getInitParameter("maxBinaryMessageSize"); - if (max != null) - customizer.setMaxBinaryMessageSize(Long.parseLong(max)); - - max = getInitParameter("inputBufferSize"); - if (max != null) - customizer.setInputBufferSize(Integer.parseInt(max)); - - max = getInitParameter("outputBufferSize"); - if (max != null) - customizer.setOutputBufferSize(Integer.parseInt(max)); - - max = getInitParameter("maxFrameSize"); - if (max == null) - max = getInitParameter("maxAllowedFrameSize"); - if (max != null) - customizer.setMaxFrameSize(Long.parseLong(max)); - - String autoFragment = getInitParameter("autoFragment"); - if (autoFragment != null) - customizer.setAutoFragment(Boolean.parseBoolean(autoFragment)); - - configure(customizer); // Let user modify customizer prior after init params - } - catch (Throwable x) - { - throw new ServletException(x); - } - } - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException - { - // provide a null default customizer the customizer will be on the negotiator in the mapping - if (mapping.upgrade(req, resp, null)) - return; - - // If we reach this point, it means we had an incoming request to upgrade - // but it was either not a proper websocket upgrade, or it was possibly rejected - // due to incoming request constraints (controlled by WebSocketCreator) - if (resp.isCommitted()) - return; - - // Handle normally - super.service(req, resp); - } - - private class CustomizedWebSocketServletFactory extends Configuration.ConfigurationCustomizer implements WebSocketServletFactory - { - @Override - public WebSocketExtensionRegistry getExtensionRegistry() - { - return components.getExtensionRegistry(); - } - - @Override - public void addMapping(String pathSpec, WebSocketCreator creator) - { - addMapping(WebSocketMapping.parsePathSpec(pathSpec), creator); - } - - @Override - public void addMapping(PathSpec pathSpec, WebSocketCreator creator) - { - mapping.addMapping(pathSpec, creator, getFactory(), this); - } - - @Override - public void register(Class endpointClass) - { - Constructor constructor; - try - { - constructor = endpointClass.getDeclaredConstructor(); - } - catch (NoSuchMethodException e) - { - throw new RuntimeException(e); - } - - WebSocketCreator creator = (req, resp) -> - { - try - { - return constructor.newInstance(); - } - catch (Throwable t) - { - t.printStackTrace(); - return null; - } - }; - - addMapping("/", creator); - } - - @Override - public void setCreator(WebSocketCreator creator) - { - addMapping("/", creator); - } - - @Override - public WebSocketCreator getMapping(PathSpec pathSpec) - { - return mapping.getMapping(pathSpec); - } - - @Override - public WebSocketCreator getMatch(String target) - { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeMapping(PathSpec pathSpec) - { - return mapping.removeMapping(pathSpec); - } - - @Override - public PathSpec parsePathSpec(String pathSpec) - { - return WebSocketMapping.parsePathSpec(pathSpec); - } - } -} diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java b/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java deleted file mode 100644 index f62a25056e5..00000000000 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketServletFactory.java +++ /dev/null @@ -1,101 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under -// the terms of the Eclipse Public License 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0 -// -// This Source Code may also be made available under the following -// Secondary Licenses when the conditions for such availability set -// forth in the Eclipse Public License, v. 2.0 are satisfied: -// the Apache License v2.0 which is available at -// https://www.apache.org/licenses/LICENSE-2.0 -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.websocket.servlet; - -import org.eclipse.jetty.http.pathmap.PathSpec; -import org.eclipse.jetty.websocket.core.Configuration; -import org.eclipse.jetty.websocket.core.WebSocketExtensionRegistry; - -public interface WebSocketServletFactory extends Configuration -{ - WebSocketExtensionRegistry getExtensionRegistry(); - - void addMapping(String pathSpec, WebSocketCreator creator); - - /** - * add a WebSocket mapping to a provided {@link WebSocketCreator}. - *

- * If mapping is added before this configuration is started, then it is persisted through - * stop/start of this configuration's lifecycle. Otherwise it will be removed when - * this configuration is stopped. - *

- * - * @param pathSpec the pathspec to respond on - * @param creator the WebSocketCreator to use - * @since 10.0 - */ - void addMapping(PathSpec pathSpec, WebSocketCreator creator); - - /** - * Add a WebSocket mapping at PathSpec "/" for a creator which creates the endpointClass - * - * @param endpointClass the WebSocket class to use - */ - void register(Class endpointClass); - - /** - * Add a WebSocket mapping at PathSpec "/" for a creator - * - * @param creator the WebSocketCreator to use - */ - void setCreator(WebSocketCreator creator); - - /** - * Returns the creator for the given path spec. - * - * @param pathSpec the pathspec to respond on - * @return the websocket creator if path spec exists, or null - */ - WebSocketCreator getMapping(PathSpec pathSpec); - - /** - * Get the MappedResource for the given target path. - * - * @param target the target path - * @return the MappedResource if matched, or null if not matched. - */ - WebSocketCreator getMatch(String target); - - /** - * Parse a PathSpec string into a PathSpec instance. - *

- * Recognized Path Spec syntaxes: - *

- *
- *
{@code /path/to} or {@code /} or {@code *.ext} or {@code servlet|{spec}}
- *
Servlet Syntax
- *
{@code ^{spec}} or {@code regex|{spec}}
- *
Regex Syntax
- *
{@code uri-template|{spec}}
- *
URI Template (see JSR356 and RFC6570 level 1)
- *
- * - * @param rawSpec the raw path spec as String to parse. - * @return the {@link PathSpec} implementation for the rawSpec - */ - PathSpec parsePathSpec(String rawSpec); - - /** - * Removes the mapping based on the given path spec. - * - * @param pathSpec the pathspec to respond on - * @return true if underlying mapping were altered, false otherwise - */ - boolean removeMapping(PathSpec pathSpec); -} diff --git a/jetty-websocket/websocket-servlet/pom.xml b/jetty-websocket/websocket-util-server/pom.xml similarity index 63% rename from jetty-websocket/websocket-servlet/pom.xml rename to jetty-websocket/websocket-util-server/pom.xml index beeeab4b0fc..c8b50273799 100644 --- a/jetty-websocket/websocket-servlet/pom.xml +++ b/jetty-websocket/websocket-util-server/pom.xml @@ -7,80 +7,19 @@ 4.0.0 - websocket-servlet - Jetty :: Websocket :: Servlet + websocket-util-server + Jetty :: Websocket :: org.eclipse.jetty.websocket :: Util-Server - ${project.groupId}.servlet + ${project.groupId}.util.server - - - org.eclipse.jetty.websocket - websocket-core - ${project.version} - - - org.eclipse.jetty - jetty-servlet - ${project.version} - - - org.slf4j - slf4j-api - - - org.eclipse.jetty - jetty-slf4j-impl - test - - - org.eclipse.jetty.toolchain - jetty-test-helper - test - - - org.eclipse.jetty.websocket - websocket-jetty-api - ${project.version} - test - - - - - org.apache.maven.plugins - maven-jar-plugin - - - artifact-jars - - test-jar - - - - org.apache.maven.plugins maven-enforcer-plugin - - ban-ws-apis - - enforce - - - - - - org.eclipse.jetty.websocket:websocket-jetty-api - javax.websocket - - - - - ban-java-servlet-api @@ -109,15 +48,13 @@ true + generate-manifest manifest - javax.websocket.servlet Implementation - - org.eclipse.jetty.websocket.servlet.*;version="${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.incrementalVersion}" - + *,org.eclipse.jetty.websocket.util.server.internal.* @@ -125,4 +62,32 @@ + + + + org.eclipse.jetty.websocket + websocket-core + ${project.version} + + + org.eclipse.jetty + jetty-servlet + ${project.version} + provided + + + org.slf4j + slf4j-api + + + org.eclipse.jetty + jetty-slf4j-impl + test + + + org.eclipse.jetty.toolchain + jetty-test-helper + test + + diff --git a/jetty-websocket/websocket-servlet/src/main/java/module-info.java b/jetty-websocket/websocket-util-server/src/main/java/module-info.java similarity index 78% rename from jetty-websocket/websocket-servlet/src/main/java/module-info.java rename to jetty-websocket/websocket-util-server/src/main/java/module-info.java index 93715204b9d..ae57cc58894 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/module-info.java +++ b/jetty-websocket/websocket-util-server/src/main/java/module-info.java @@ -16,11 +16,12 @@ // ======================================================================== // -module org.eclipse.jetty.websocket.servlet +module org.eclipse.jetty.websocket.util.server { - exports org.eclipse.jetty.websocket.servlet; - + exports org.eclipse.jetty.websocket.util.server; + exports org.eclipse.jetty.websocket.util.server.internal to org.eclipse.jetty.websocket.jetty.server, org.eclipse.jetty.websocket.javax.server; + + requires org.slf4j; requires transitive org.eclipse.jetty.servlet; requires transitive org.eclipse.jetty.websocket.core; - requires org.slf4j; } diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java similarity index 96% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java index 6829d6219a9..b0a9c030e90 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketUpgradeFilter.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server; import java.io.IOException; import java.time.Duration; @@ -35,11 +35,11 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.websocket.core.Configuration; import org.eclipse.jetty.websocket.core.WebSocketComponents; +import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,7 +123,7 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable return holder; } - public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "org.eclipse.jetty.websocket.servlet.WebSocketMapping.key"; + public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping.key"; private final Configuration.ConfigurationCustomizer defaultCustomizer = new Configuration.ConfigurationCustomizer(); private WebSocketMapping mapping; @@ -159,12 +159,6 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable Dumpable.dumpObjects(out, indent, this, mapping); } - @ManagedAttribute(value = "factory", readonly = true) - public WebSocketMapping getMapping() - { - return mapping; - } - @Override public void init(FilterConfig config) throws ServletException { diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/FrameHandlerFactory.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/FrameHandlerFactory.java similarity index 96% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/FrameHandlerFactory.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/FrameHandlerFactory.java index a895d9db690..bebcf7fe203 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/FrameHandlerFactory.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/FrameHandlerFactory.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server.internal; import org.eclipse.jetty.websocket.core.FrameHandler; diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeRequest.java similarity index 98% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeRequest.java index f327a424a95..a4418a9e6e7 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeRequest.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeRequest.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server.internal; import java.net.HttpCookie; import java.net.InetSocketAddress; @@ -42,7 +42,6 @@ import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.WebSocketConstants; import org.eclipse.jetty.websocket.core.server.Negotiation; -import org.eclipse.jetty.websocket.servlet.internal.UpgradeHttpServletRequest; /** * Holder of request data for a WebSocket upgrade request. diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeResponse.java similarity index 99% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeResponse.java index 04b5d490558..f78615f46c6 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/ServletUpgradeResponse.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/ServletUpgradeResponse.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server.internal; import java.io.IOException; import java.util.ArrayList; diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/UpgradeHttpServletRequest.java similarity index 99% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/UpgradeHttpServletRequest.java index f2f56dd0be5..72eafaff707 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/internal/UpgradeHttpServletRequest.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/UpgradeHttpServletRequest.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet.internal; +package org.eclipse.jetty.websocket.util.server.internal; import java.io.BufferedReader; import java.net.InetSocketAddress; diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketCreator.java similarity index 96% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketCreator.java index b4db96bd148..b991530193f 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketCreator.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketCreator.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server.internal; /** * Abstract WebSocket creator interface. diff --git a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java similarity index 97% rename from jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java rename to jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java index 30bf8098e2b..a96a55d315c 100644 --- a/jetty-websocket/websocket-servlet/src/main/java/org/eclipse/jetty/websocket/servlet/WebSocketMapping.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java @@ -16,7 +16,7 @@ // ======================================================================== // -package org.eclipse.jetty.websocket.servlet; +package org.eclipse.jetty.websocket.util.server.internal; import java.io.IOException; import java.util.concurrent.atomic.AtomicReference; @@ -45,8 +45,6 @@ import org.eclipse.jetty.websocket.core.server.WebSocketNegotiator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; - /** * Mapping of pathSpec to a tupple of {@link WebSocketCreator}, {@link FrameHandlerFactory} and * {@link Configuration.Customizer}. @@ -136,7 +134,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener throw new IllegalArgumentException("Unrecognized path spec syntax [" + rawSpec + "]"); } - public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.servlet.WebSocketMapping"; + public static final String DEFAULT_KEY = "org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping"; private final PathMappings mappings = new PathMappings<>(); private final WebSocketComponents components; @@ -281,7 +279,7 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener if (websocketPojo == null) { // no creation, sorry - upgradeResponse.sendError(SC_SERVICE_UNAVAILABLE, "WebSocket Endpoint Creation Refused"); + upgradeResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "WebSocket Endpoint Creation Refused"); return null; } diff --git a/jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtil.java b/jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtils.java similarity index 99% rename from jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtil.java rename to jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtils.java index 21a2a138d1a..20563f8af40 100644 --- a/jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtil.java +++ b/jetty-websocket/websocket-util/src/main/java/org/eclipse/jetty/websocket/util/TextUtils.java @@ -21,7 +21,7 @@ package org.eclipse.jetty.websocket.util; /** * Collection of utility methods for Text content */ -public final class TextUtil +public final class TextUtils { /** * Create a hint of what the text is like. diff --git a/jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java b/jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java similarity index 97% rename from jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java rename to jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java index 1cb9e174fc8..6569d8aed3b 100644 --- a/jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java +++ b/jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/PartialStringMessageSinkTest.java @@ -67,7 +67,7 @@ public class PartialStringMessageSinkTest List message = Objects.requireNonNull(endpoint.messages.poll(5, TimeUnit.SECONDS)); assertThat(message.size(), is(1)); - assertThat(message.get(0), is("\uD800\uDF48")); + assertThat(message.get(0), is("\uD800\uDF48")); // UTF-8 encoded payload. } @Test @@ -87,7 +87,7 @@ public class PartialStringMessageSinkTest List message = Objects.requireNonNull(endpoint.messages.poll(5, TimeUnit.SECONDS)); assertThat(message.size(), is(2)); assertThat(message.get(0), is("")); - assertThat(message.get(1), is("\uD800\uDF48")); + assertThat(message.get(1), is("\uD800\uDF48")); // UTF-8 encoded payload. } @Test diff --git a/jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java b/jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java similarity index 98% rename from jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java rename to jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java index f876c52a942..8696ec91565 100644 --- a/jetty-websocket/websocket-util/src/test/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java +++ b/jetty-websocket/websocket-util/src/test/java/org/eclipse/jetty/websocket/util/StringMessageSinkTest.java @@ -72,7 +72,7 @@ public class StringMessageSinkTest messageSink.accept(new Frame(OpCode.TEXT, utf8Payload).setFin(true), callback); callback.block(5, TimeUnit.SECONDS); - assertThat(endpoint.messages.poll(5, TimeUnit.SECONDS), is("\uD800\uDF48")); + assertThat(endpoint.messages.poll(5, TimeUnit.SECONDS), is("\uD800\uDF48")); // UTF-8 encoded payload. } @Test @@ -90,7 +90,7 @@ public class StringMessageSinkTest messageSink.accept(new Frame(OpCode.TEXT, continuationUtf8Payload).setFin(true), callback); callback.block(5, TimeUnit.SECONDS); - assertThat(endpoint.messages.poll(5, TimeUnit.SECONDS), is("\uD800\uDF48")); + assertThat(endpoint.messages.poll(5, TimeUnit.SECONDS), is("\uD800\uDF48")); // UTF-8 encoded payload. } @Test diff --git a/pom.xml b/pom.xml index 8602b3544db..620ebcffdeb 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ 1.21 benchmarks 1.4.0 - 5.5.1 + 5.6.2 3.6.0 1.3.1 3.1.2.Final @@ -41,7 +41,7 @@ false - -Dfile.encoding=UTF-8 -Duser.language=en -Duser.region=US -showversion -Xmx1g -Xms1g -Xlog:gc:stderr:time,level,tags + -Dfile.encoding=UTF-8 -Duser.language=en -Duser.region=US -showversion -Xmx2g -Xms2g -Xlog:gc:stderr:time,level,tags 3.0.0-M4 @@ -56,8 +56,7 @@ false - 5.3 - + 5.4 2.1.1.RELEASE @@ -1063,6 +1062,16 @@ junit-jupiter ${junit.version}
+ + org.testcontainers + testcontainers + 1.14.1 + + + org.testcontainers + junit-jupiter + 1.14.1 + org.eclipse.jetty.toolchain diff --git a/tests/test-distribution/pom.xml b/tests/test-distribution/pom.xml index 59abe41b08b..d7176621af5 100644 --- a/tests/test-distribution/pom.xml +++ b/tests/test-distribution/pom.xml @@ -113,6 +113,12 @@ ${project.version} test + + org.eclipse.jetty.websocket + websocket-jetty-client + ${project.version} + test + org.eclipse.jetty.tests test-felix-webapp @@ -120,6 +126,13 @@ test war + + org.eclipse.jetty.tests + test-websocket-webapp + ${project.version} + test + war + org.eclipse.jetty.tests test-bad-websocket-webapp diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 1d73e20f571..6afc56b7c6e 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -20,12 +20,14 @@ package org.eclipse.jetty.tests.distribution; import java.io.BufferedWriter; import java.io.File; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.client.HttpClient; @@ -37,10 +39,14 @@ import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; import org.eclipse.jetty.unixsocket.server.UnixSocketConnector; +import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.junit.jupiter.api.Disabled; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.WebSocketListener; +import org.eclipse.jetty.websocket.client.WebSocketClient; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnJre; import org.junit.jupiter.api.condition.JRE; @@ -49,6 +55,7 @@ import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -410,6 +417,7 @@ public class DistributionTests extends AbstractDistributionTest "", "--jpms", }) + @DisabledOnJre(JRE.JAVA_14) // TODO: Waiting on JDK14 bug at https://bugs.openjdk.java.net/browse/JDK-8244090. public void testSimpleWebAppWithWebsocket(String arg) throws Exception { String jettyVersion = System.getProperty("jettyVersion"); @@ -427,9 +435,13 @@ public class DistributionTests extends AbstractDistributionTest assertTrue(run1.awaitFor(5, TimeUnit.SECONDS)); assertEquals(0, run1.getExitValue()); - File war = distribution.resolveArtifact("org.eclipse.jetty.tests:test-bad-websocket-webapp:war:" + jettyVersion); - distribution.installWarFile(war, "test1"); - distribution.installWarFile(war, "test2"); + File webApp = distribution.resolveArtifact("org.eclipse.jetty.tests:test-websocket-webapp:war:" + jettyVersion); + File badWebApp = distribution.resolveArtifact("org.eclipse.jetty.tests:test-bad-websocket-webapp:war:" + jettyVersion); + + distribution.installWarFile(webApp, "test1"); + distribution.installWarFile(badWebApp, "test2"); + distribution.installWarFile(badWebApp, "test3"); + distribution.installWarFile(webApp, "test4"); int port = distribution.freePort(); String[] args2 = { @@ -437,26 +449,64 @@ public class DistributionTests extends AbstractDistributionTest "jetty.http.port=" + port//, //"jetty.server.dumpAfterStart=true" }; + try (DistributionTester.Run run2 = distribution.start(args2)) { assertTrue(run2.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS)); - // we do not test that anymore because it doesn't work for java14 - //assertFalse(run2.getLogs().stream().anyMatch(s -> s.contains("LinkageError"))); + assertFalse(run2.getLogs().stream().anyMatch(s -> s.contains("LinkageError"))); startHttpClient(); - ContentResponse response = client.GET("http://localhost:" + port + "/test1/index.jsp"); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Hello")); - assertThat(response.getContentAsString(), not(containsString("<%"))); + WebSocketClient wsClient = new WebSocketClient(client); + wsClient.start(); + URI serverUri = URI.create("ws://localhost:" + port); - client.GET("http://localhost:" + port + "/test2/index.jsp"); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getContentAsString(), containsString("Hello")); - assertThat(response.getContentAsString(), not(containsString("<%"))); + // Verify /test1 is able to establish a WebSocket connection. + WsListener webSocketListener = new WsListener(); + Session session = wsClient.connect(webSocketListener, serverUri.resolve("/test1")).get(5, TimeUnit.SECONDS); + session.getRemote().sendString("echo message"); + assertThat(webSocketListener.textMessages.poll(5, TimeUnit.SECONDS), is("echo message")); + session.close(); + assertTrue(webSocketListener.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(webSocketListener.closeCode, is(StatusCode.NO_CODE)); + + // Verify that /test2 and /test3 could not be started. + ContentResponse response = client.GET(serverUri.resolve("/test2/badonopen/a")); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); + client.GET("http://localhost:" + port + "/test3/badonopen/a"); + assertEquals(HttpStatus.SERVICE_UNAVAILABLE_503, response.getStatus()); + + // Verify /test4 is able to establish a WebSocket connection. + webSocketListener = new WsListener(); + session = wsClient.connect(webSocketListener, serverUri.resolve("/test4")).get(5, TimeUnit.SECONDS); + session.getRemote().sendString("echo message"); + assertThat(webSocketListener.textMessages.poll(5, TimeUnit.SECONDS), is("echo message")); + session.close(); + assertTrue(webSocketListener.closeLatch.await(5, TimeUnit.SECONDS)); + assertThat(webSocketListener.closeCode, is(StatusCode.NO_CODE)); } } } + public static class WsListener implements WebSocketListener + { + BlockingArrayQueue textMessages = new BlockingArrayQueue<>(); + private CountDownLatch closeLatch = new CountDownLatch(1); + private int closeCode; + + @Override + public void onWebSocketClose(int statusCode, String reason) + { + this.closeCode = statusCode; + closeLatch.countDown(); + } + + @Override + public void onWebSocketText(String message) + { + textMessages.add(message); + } + } + @Test public void testStartStopLog4j2Modules() throws Exception { diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java index 0504ded0f85..f2239a533b9 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/AsyncIOServletTest.java @@ -56,6 +56,7 @@ import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.client.util.InputStreamRequestContent; import org.eclipse.jetty.client.util.StringRequestContent; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http2.HTTP2Session; @@ -74,6 +75,7 @@ import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.FuturePromise; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; @@ -397,6 +399,7 @@ public class AsyncIOServletTest extends AbstractTest headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .body(new StringRequestContent(text)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -893,7 +896,7 @@ public class AsyncIOServletTest extends AbstractTest headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .body(new StringRequestContent("XYZ")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -949,7 +952,7 @@ public class AsyncIOServletTest extends AbstractTest headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .timeout(5, TimeUnit.SECONDS) .send(); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java index 47e3c28b362..2a4b3c508f7 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/ConnectionStatisticsTest.java @@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.ConnectionStatistics; @@ -101,7 +102,7 @@ public class ConnectionStatisticsTest extends AbstractTest byte[] content = new byte[3072]; long contentLength = content.length; ContentResponse response = scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .body(new BytesRequestContent(content)) .timeout(5, TimeUnit.SECONDS) .send(); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientContinueTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientContinueTest.java index 1fd46c84b6d..53e4f9ae8f8 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientContinueTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientContinueTest.java @@ -103,7 +103,7 @@ public class HttpClientContinueTest extends AbstractTest }); ContentResponse response = scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(contents)) .timeout(5, TimeUnit.SECONDS) .send(); @@ -144,7 +144,7 @@ public class HttpClientContinueTest extends AbstractTest byte[] content1 = new byte[10240]; byte[] content2 = new byte[16384]; ContentResponse response = scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content1, content2) { @Override @@ -202,7 +202,7 @@ public class HttpClientContinueTest extends AbstractTest byte[] content2 = new byte[16384]; CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content1, content2)) .send(new BufferingResponseListener() { @@ -254,7 +254,7 @@ public class HttpClientContinueTest extends AbstractTest scenario.client.newRequest(scenario.newURI()) .method(HttpMethod.POST) .path("/continue") - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content)) .send(new BufferingResponseListener() { @@ -304,7 +304,7 @@ public class HttpClientContinueTest extends AbstractTest scenario.client.newRequest(scenario.newURI()) .method(HttpMethod.POST) .path("/redirect") - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content)) .send(new BufferingResponseListener() { @@ -351,7 +351,7 @@ public class HttpClientContinueTest extends AbstractTest byte[] content = new byte[1024]; CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content)) .idleTimeout(idleTimeout, TimeUnit.MILLISECONDS) .send(new BufferingResponseListener() @@ -400,7 +400,7 @@ public class HttpClientContinueTest extends AbstractTest byte[] content = new byte[1024]; CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content)) .send(new BufferingResponseListener() { @@ -460,7 +460,7 @@ public class HttpClientContinueTest extends AbstractTest byte[] content = new byte[1024]; CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(content)) .send(new BufferingResponseListener() { @@ -504,7 +504,7 @@ public class HttpClientContinueTest extends AbstractTest CountDownLatch latch = new CountDownLatch(1); AsyncRequestContent content = new AsyncRequestContent(); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(content) .send(new BufferingResponseListener() { @@ -555,7 +555,7 @@ public class HttpClientContinueTest extends AbstractTest CountDownLatch latch = new CountDownLatch(1); AsyncRequestContent content = new AsyncRequestContent(ByteBuffer.wrap(chunk1)); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(content) .send(new BufferingResponseListener() { @@ -596,7 +596,7 @@ public class HttpClientContinueTest extends AbstractTest CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .onRequestHeaders(request -> { content.offer(ByteBuffer.wrap(data)); @@ -660,7 +660,7 @@ public class HttpClientContinueTest extends AbstractTest CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest(scenario.newURI()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(content) .send(new BufferingResponseListener() { @@ -694,7 +694,7 @@ public class HttpClientContinueTest extends AbstractTest CountDownLatch latch = new CountDownLatch(1); scenario.client.newRequest("localhost", server.getLocalPort()) - .header(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()) + .headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE)) .body(new BytesRequestContent(new byte[]{0})) .send(result -> { diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java index f25ef0fa80f..f03b5570f11 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.util.BytesRequestContent; import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool; @@ -224,23 +225,23 @@ public class HttpClientLoadTest extends AbstractTest headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)); else if (serverClose) - request.header("X-Close", "true"); + request.headers(headers -> headers.put("X-Close", "true")); if (clientTimeout > 0) { - request.header("X-Timeout", String.valueOf(clientTimeout)); + request.headers(headers -> headers.put("X-Timeout", String.valueOf(clientTimeout))); request.idleTimeout(clientTimeout, TimeUnit.MILLISECONDS); } switch (method) { case "GET": - request.header("X-Download", String.valueOf(contentLength)); + request.headers(headers -> headers.put("X-Download", String.valueOf(contentLength))); break; case "POST": - request.header("X-Upload", String.valueOf(contentLength)); + request.headers(headers -> headers.put("X-Upload", String.valueOf(contentLength))); request.body(new BytesRequestContent(new byte[contentLength])); break; } diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java index f1e679e9aa7..a48de6bbf96 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTest.java @@ -619,7 +619,7 @@ public class HttpClientTest extends AbstractTest ContentResponse response = scenario.client.newRequest(scenario.newURI()) .method(HttpMethod.HEAD) .path(scenario.servletPath) - .header(HttpHeader.ACCEPT, "*/*") + .headers(headers -> headers.put(HttpHeader.ACCEPT, "*/*")) .send(); assertEquals(status, response.getStatus()); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java index 4ce25b5eb2e..01b8d14eda6 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientTransportDynamicTest.java @@ -515,9 +515,10 @@ public class HttpClientTransportDynamicTest // Make an upgrade request from HTTP/1.1 to H2C. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -595,9 +596,10 @@ public class HttpClientTransportDynamicTest // Make an upgrade request from HTTP/1.1 to H2C. int serverPort = proxyPort + 1; // Any port will do. ContentResponse response = client.newRequest("localhost", serverPort) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -628,9 +630,10 @@ public class HttpClientTransportDynamicTest // Make an upgrade request from HTTP/1.1 to H2C. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) .scheme(HttpScheme.HTTPS.asString()) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -665,9 +668,10 @@ public class HttpClientTransportDynamicTest CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) .method(HttpMethod.POST) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .body(new BytesRequestContent(bytes)) .timeout(5, TimeUnit.SECONDS) .send(new BufferingResponseListener(bytes.length) @@ -698,8 +702,8 @@ public class HttpClientTransportDynamicTest // The upgrade request is missing the required HTTP2-Settings header. ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.CONNECTION, "Upgrade") + .headers(headers -> headers.add(HttpHeader.UPGRADE, "h2c")) + .headers(headers -> headers.add(HttpHeader.CONNECTION, "Upgrade")) .timeout(5, TimeUnit.SECONDS) .send(); @@ -725,9 +729,10 @@ public class HttpClientTransportDynamicTest // Make an upgrade request from HTTP/1.1 to H2C. CountDownLatch latch = new CountDownLatch(1); client.newRequest("localhost", connector.getLocalPort()) - .header(HttpHeader.UPGRADE, "h2c") - .header(HttpHeader.HTTP2_SETTINGS, "") - .header(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings") + .headers(headers -> headers + .put(HttpHeader.UPGRADE, "h2c") + .put(HttpHeader.HTTP2_SETTINGS, "") + .put(HttpHeader.CONNECTION, "Upgrade, HTTP2-Settings")) .send(result -> { if (result.isFailed()) diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/FailedSelectorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/FailedSelectorTest.java index a1aa817edd3..6aaa535a18e 100644 --- a/tests/test-integration/src/test/java/org/eclipse/jetty/test/FailedSelectorTest.java +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/FailedSelectorTest.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.io.EndPoint; @@ -180,7 +181,7 @@ public class FailedSelectorTest ContentResponse response = client.newRequest(dest) .method(HttpMethod.GET) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .send(); assertThat(dest + " status", response.getStatus(), is(HttpStatus.OK_200)); @@ -193,7 +194,7 @@ public class FailedSelectorTest LOG.info("Requesting GET on {}", dest); ContentResponse response = client.newRequest(dest) .method(HttpMethod.GET) - .header(HttpHeader.CONNECTION, "close") + .headers(headers -> headers.put(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE)) .send(); assertThat(dest + " status", response.getStatus(), is(HttpStatus.OK_200)); diff --git a/tests/test-integration/src/test/java/org/eclipse/jetty/test/RecoverFailedSelectorTest.java b/tests/test-integration/src/test/java/org/eclipse/jetty/test/RecoverFailedSelectorTest.java new file mode 100644 index 00000000000..9cbbc89c56f --- /dev/null +++ b/tests/test-integration/src/test/java/org/eclipse/jetty/test/RecoverFailedSelectorTest.java @@ -0,0 +1,379 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.test; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SelectorManager; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.Scheduler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RecoverFailedSelectorTest +{ + private Server server; + private ServerConnector connector; + + private void start(Function consumer) throws Exception + { + server = new Server(); + connector = consumer.apply(server); + server.addConnector(connector); + server.start(); + } + + @AfterEach + public void dispose() throws Exception + { + server.stop(); + } + + @Test + public void testSelectFailureBetweenReads() throws Exception + { + // There will be 3 calls to select(): one at start(), + // one to accept, and one to set read interest. + CountDownLatch selectLatch = new CountDownLatch(3); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + String request = "GET / HTTP/1.0\r\n\r\n"; + int split = request.length() / 2; + ByteBuffer chunk1 = StandardCharsets.UTF_8.encode(request.substring(0, split)); + ByteBuffer chunk2 = StandardCharsets.UTF_8.encode(request.substring(split)); + + // Wake up the selector and fail it. + fail.set(true); + client.write(chunk1); + + // Wait for the failure handling to be completed. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Write the rest of the request, the + // server should be able to continue. + client.write(chunk2); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testAcceptDuringSelectFailure() throws Exception + { + // There will be 3 calls to select(): one at start(), + // one to accept, and one to set read interest. + CountDownLatch selectLatch = new CountDownLatch(3); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + AtomicReference socketRef = new AtomicReference<>(); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + // Before handling the failure, connect with another socket. + SocketChannel socket = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())); + socketRef.set(socket); + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + String request = "GET / HTTP/1.0\r\n\r\n"; + ByteBuffer buffer = StandardCharsets.UTF_8.encode(request); + + // Wake up the selector and fail it. + fail.set(true); + client.write(buffer); + + // Wait for the failure handling to be completed. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + + // Verify that the newly created socket works well. + SocketChannel socket = socketRef.get(); + buffer.flip(); + socket.write(buffer); + response = HttpTester.parseResponse(HttpTester.from(socket)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testSelectFailureDuringEndPointCreation() throws Exception + { + // There will be 2 calls to select(): one at start(), one to accept. + CountDownLatch selectLatch = new CountDownLatch(2); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + CountDownLatch endPointLatch1 = new CountDownLatch(1); + CountDownLatch endPointLatch2 = new CountDownLatch(1); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + + @Override + protected SocketChannelEndPoint newEndPoint(SelectableChannel channel, ManagedSelector selector, SelectionKey selectionKey) throws IOException + { + try + { + SocketChannelEndPoint endPoint = super.newEndPoint(channel, selector, selectionKey); + endPointLatch1.countDown(); + assertTrue(endPointLatch2.await(5, TimeUnit.SECONDS)); + return endPoint; + } + catch (InterruptedException x) + { + throw new InterruptedIOException(); + } + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the server EndPoint instance is created. + assertTrue(endPointLatch1.await(5, TimeUnit.SECONDS)); + + // Wake up the selector and fail it. + fail.set(true); + SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())).close(); + + // Wait until the selector is replaced. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Continue the EndPoint creation. + endPointLatch2.countDown(); + + String request = "GET / HTTP/1.0\r\n\r\n"; + ByteBuffer buffer = StandardCharsets.UTF_8.encode(request); + client.write(buffer); + + HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client)); + assertNotNull(response); + assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus()); + } + } + + @Test + public void testSelectFailureDuringEndPointCreatedThenClosed() throws Exception + { + // There will be 2 calls to select(): one at start(), one to accept. + CountDownLatch selectLatch = new CountDownLatch(2); + CountDownLatch failureLatch = new CountDownLatch(1); + AtomicBoolean fail = new AtomicBoolean(); + CountDownLatch connectionLatch1 = new CountDownLatch(1); + CountDownLatch connectionLatch2 = new CountDownLatch(1); + start(server -> new ServerConnector(server, 1, 1) + { + @Override + protected SelectorManager newSelectorManager(Executor executor, Scheduler scheduler, int selectors) + { + return new ServerConnectorManager(executor, scheduler, selectors) + { + @Override + protected ManagedSelector newSelector(int id) + { + return new ManagedSelector(this, id) + { + @Override + protected int nioSelect(Selector selector, boolean now) throws IOException + { + selectLatch.countDown(); + if (fail.getAndSet(false)) + throw new IOException("explicit select() failure"); + return super.nioSelect(selector, now); + } + + @Override + protected void handleSelectFailure(Selector selector, Throwable failure) throws IOException + { + super.handleSelectFailure(selector, failure); + failureLatch.countDown(); + } + }; + } + + @Override + public Connection newConnection(SelectableChannel channel, EndPoint endPoint, Object attachment) throws IOException + { + try + { + Connection connection = super.newConnection(channel, endPoint, attachment); + endPoint.close(); + connectionLatch1.countDown(); + assertTrue(connectionLatch2.await(5, TimeUnit.SECONDS)); + return connection; + } + catch (InterruptedException e) + { + throw new InterruptedIOException(); + } + } + }; + } + }); + + try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort()))) + { + assertTrue(selectLatch.await(5, TimeUnit.SECONDS)); + + // Wait until the server EndPoint is closed. + assertTrue(connectionLatch1.await(5, TimeUnit.SECONDS)); + + // Wake up the selector and fail it. + fail.set(true); + SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())).close(); + + // Wait until the selector is replaced. + assertTrue(failureLatch.await(5, TimeUnit.SECONDS)); + + // Continue the server processing. + connectionLatch2.countDown(); + + // The channel has been closed on the server. + int read = client.read(ByteBuffer.allocate(1)); + assertTrue(read < 0); + } + } +} diff --git a/tests/test-integration/src/test/resources/jetty-logging.properties b/tests/test-integration/src/test/resources/jetty-logging.properties index c3b261ea39e..ba4789f545f 100644 --- a/tests/test-integration/src/test/resources/jetty-logging.properties +++ b/tests/test-integration/src/test/resources/jetty-logging.properties @@ -1,5 +1,3 @@ # Jetty Logging using jetty-slf4j-impl -## Jetty Logging using jetty-slf4j-impl -org.eclipse.jetty.LEVEL=WARN #org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.websocket.LEVEL=DEBUG diff --git a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java index 96131d3edc6..361fd148efa 100644 --- a/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java +++ b/tests/test-sessions/test-jdbc-sessions/src/test/java/org/eclipse/jetty/server/session/ClusteredSessionMigrationTest.java @@ -20,7 +20,6 @@ package org.eclipse.jetty.server.session; import java.io.IOException; import java.io.PrintWriter; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,11 +28,12 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * ClusteredSessionMigrationTest @@ -106,14 +106,15 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase ContentResponse response1 = request1.send(); assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); String sessionCookie = response1.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); // Perform a request to server2 using the session cookie from the previous request // This should migrate the session from server1 to server2. Request request2 = client.newRequest("http://localhost:" + port2 + contextPath + servletMapping.substring(1) + "?action=get"); - request2.header("Cookie", sessionCookie); + HttpField cookie = new HttpField("Cookie", sessionCookie); + request2.headers(headers -> headers.put(cookie)); ContentResponse response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); } @@ -140,13 +141,13 @@ public class ClusteredSessionMigrationTest extends AbstractTestBase private static long createTime = 0; @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { doPost(request, response); } @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { HttpSession session = request.getSession(false); diff --git a/tests/test-sessions/test-mongodb-sessions/pom.xml b/tests/test-sessions/test-mongodb-sessions/pom.xml index 11e301fe167..dee31766e2e 100644 --- a/tests/test-sessions/test-mongodb-sessions/pom.xml +++ b/tests/test-sessions/test-mongodb-sessions/pom.xml @@ -12,6 +12,8 @@ ${project.groupId}.sessions.mongo localhost + + 2.2.7 @@ -111,13 +113,28 @@ jetty-test-helper test + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + org.slf4j + slf4j-simple + test + - mongodb + remote-session-tests - mongodb.enabled + mongo.enabled true @@ -128,51 +145,11 @@ maven-surefire-plugin - ${embedmongo.port} - ${embedmongo.host} + ${mongo.docker.version} false - - com.github.joelittlejohn.embedmongo - embedmongo-maven-plugin - 0.4.1 - - - - true - - ${project.build.directory}/mongotest - - - file - - ${project.build.directory}/embedmongo.log - - - - false - https://jenkins.webtide.net/userContent/ - 2.2.1 - - - - start - process-test-classes - - start - - - - stop - test - - stop - - - - diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/AttributeNameTest.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/AttributeNameTest.java index 1d545059ea7..9b641c11ac4 100644 --- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/AttributeNameTest.java +++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/AttributeNameTest.java @@ -21,7 +21,6 @@ package org.eclipse.jetty.nosql.mongodb; import java.io.IOException; import java.io.PrintWriter; import java.util.concurrent.TimeUnit; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -29,6 +28,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.session.DefaultSessionCacheFactory; import org.eclipse.jetty.server.session.Session; @@ -40,7 +40,6 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * AttributeNameTest @@ -109,14 +108,15 @@ public class AttributeNameTest String sessionCookie = response.getHeaders().get(HttpHeader.SET_COOKIE); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); //Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); //Make a request to the 2nd server which will do a refresh, use TestServlet to ensure that the //session attribute with dotted name is not removed Request request2 = client.newRequest("http://localhost:" + port2 + contextPath + servletMapping + "?action=get"); - request2.header("Cookie", sessionCookie); + HttpField cookie = new HttpField("Cookie", sessionCookie); + request2.headers(headers -> headers.put(cookie)); ContentResponse response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); } @@ -135,7 +135,7 @@ public class AttributeNameTest public static class TestServlet extends HttpServlet { @Override - protected void doGet(HttpServletRequest request, HttpServletResponse httpServletResponse) throws ServletException, IOException + protected void doGet(HttpServletRequest request, HttpServletResponse httpServletResponse) throws IOException { String action = request.getParameter("action"); if ("init".equals(action)) diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreTest.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreTest.java index c53d73d6342..6380083e62e 100644 --- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreTest.java +++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoSessionDataStoreTest.java @@ -42,15 +42,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; */ public class MongoSessionDataStoreTest extends AbstractSessionDataStoreTest { + @BeforeEach - public void beforeClass() throws Exception + public void beforeEach() throws Exception { MongoTestHelper.dropCollection(); MongoTestHelper.createCollection(); } @AfterEach - public void afterClass() throws Exception + public void afterEach() throws Exception { MongoTestHelper.dropCollection(); } diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestHelper.java b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestHelper.java index 28c0ebe0ca5..d6aef0a6865 100644 --- a/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestHelper.java +++ b/tests/test-sessions/test-mongodb-sessions/src/test/java/org/eclipse/jetty/nosql/mongodb/MongoTestHelper.java @@ -35,6 +35,9 @@ import org.eclipse.jetty.util.ClassLoadingObjectInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -45,37 +48,71 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class MongoTestHelper { private static final Logger LOG = LoggerFactory.getLogger(MongoTestHelper.class); + private static final Logger MONGO_LOG = LoggerFactory.getLogger("org.eclipse.jetty.nosql.mongodb.MongoLogs"); + public static final String DB_NAME = "HttpSessions"; public static final String COLLECTION_NAME = "testsessions"; - static MongoClient _mongoClient; + static GenericContainer mongo = + new GenericContainer("mongo:" + System.getProperty("mongo.docker.version", "2.2.7")) + .withLogConsumer(new Slf4jLogConsumer(MONGO_LOG)); - static + static MongoClient mongoClient; + + public static void startMongo() { try { - _mongoClient = - new MongoClient(System.getProperty("embedmongo.host"), Integer.getInteger("embedmongoPort")); + long start = System.currentTimeMillis(); + mongo.start(); + String containerIpAddress = mongo.getContainerIpAddress(); + int mongoPort = mongo.getMappedPort(27017); + LOG.info("Mongo container started for {}:{} - {}ms", containerIpAddress, mongoPort, + System.currentTimeMillis() - start); + System.setProperty("embedmongoHost", containerIpAddress); + System.setProperty("embedmongoPort", Integer.toString(mongoPort)); } - catch (UnknownHostException e) + catch (Exception e) { - e.printStackTrace(); + LOG.error(e.getMessage(), e); + throw new RuntimeException(e); } } - public static void dropCollection() throws MongoException, UnknownHostException + public static void stopMongo() { - _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME).drop(); + mongo.stop(); + mongoClient = null; + } + + public static MongoClient getMongoClient() throws UnknownHostException + { + boolean restart = false; + if (mongo == null || !mongo.isRunning()) + { + startMongo(); + restart = true; + } + if (mongoClient == null || restart) + { + mongoClient = new MongoClient(System.getProperty("embedmongoHost"), Integer.getInteger("embedmongoPort")); + } + return mongoClient; + } + + public static void dropCollection() throws Exception + { + getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME).drop(); } public static void createCollection() throws UnknownHostException, MongoException { - _mongoClient.getDB(DB_NAME).createCollection(COLLECTION_NAME, null); + getMongoClient().getDB(DB_NAME).createCollection(COLLECTION_NAME, null); } public static DBCollection getCollection() throws UnknownHostException, MongoException { - return _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + return getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); } public static MongoSessionDataStoreFactory newSessionDataStoreFactory() @@ -91,7 +128,7 @@ public class MongoTestHelper public static boolean checkSessionExists(String id) throws Exception { - DBCollection collection = _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + DBCollection collection = getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); DBObject fields = new BasicDBObject(); fields.put(MongoSessionDataStore.__EXPIRY, 1); @@ -108,7 +145,7 @@ public class MongoTestHelper public static boolean checkSessionPersisted(SessionData data) throws Exception { - DBCollection collection = _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + DBCollection collection = getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); DBObject fields = new BasicDBObject(); @@ -116,7 +153,7 @@ public class MongoTestHelper if (sessionDocument == null) return false; //doesn't exist - LOG.info("{}", sessionDocument); + LOG.debug("{}", sessionDocument); Boolean valid = (Boolean)sessionDocument.get(MongoSessionDataStore.__VALID); @@ -182,7 +219,7 @@ public class MongoTestHelper Map attributes) throws Exception { - DBCollection collection = _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + DBCollection collection = getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -231,7 +268,7 @@ public class MongoTestHelper throws Exception { - DBCollection collection = _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + DBCollection collection = getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); @@ -277,7 +314,7 @@ public class MongoTestHelper throws Exception { //make old-style session to test if we can retrieve it - DBCollection collection = _mongoClient.getDB(DB_NAME).getCollection(COLLECTION_NAME); + DBCollection collection = getMongoClient().getDB(DB_NAME).getCollection(COLLECTION_NAME); // Form query for upsert BasicDBObject key = new BasicDBObject(MongoSessionDataStore.__ID, id); diff --git a/tests/test-sessions/test-mongodb-sessions/src/test/resources/simplelogger.properties b/tests/test-sessions/test-mongodb-sessions/src/test/resources/simplelogger.properties new file mode 100644 index 00000000000..5f859960729 --- /dev/null +++ b/tests/test-sessions/test-mongodb-sessions/src/test/resources/simplelogger.properties @@ -0,0 +1,3 @@ +org.slf4j.simpleLogger.defaultLogLevel=info +org.slf4j.simpleLogger.log.org.eclipse.jetty.nosql.mongodb.MongoLogs=error +org.slf4j.simpleLogger.log.org.eclipse.jetty.nosql.mongodb.MongoTestHelper=info diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredOrphanedSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredOrphanedSessionTest.java index 521086eb56d..c0bc2578da5 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredOrphanedSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractClusteredOrphanedSessionTest.java @@ -29,10 +29,12 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; /** * AbstractClusteredOrphanedSessionTest @@ -90,9 +92,9 @@ public abstract class AbstractClusteredOrphanedSessionTest extends AbstractTestB ContentResponse response1 = client.GET("http://localhost:" + port1 + contextPath + servletMapping.substring(1) + "?action=init"); assertEquals(HttpServletResponse.SC_OK, response1.getStatus()); String sessionCookie = response1.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); // Wait for the session to expire. // The first node does not do any scavenging, but the session @@ -101,7 +103,8 @@ public abstract class AbstractClusteredOrphanedSessionTest extends AbstractTestB // Perform one request to server2 to be sure that the session has been expired Request request = client.newRequest("http://localhost:" + port2 + contextPath + servletMapping.substring(1) + "?action=check"); - request.header("Cookie", sessionCookie); + HttpField cookie = new HttpField("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); ContentResponse response2 = request.send(); assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); } @@ -141,7 +144,7 @@ public abstract class AbstractClusteredOrphanedSessionTest extends AbstractTestB else if ("check".equals(action)) { HttpSession session = request.getSession(false); - assertTrue(session == null); + assertNull(session); } } } diff --git a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java index dbe70cce2c1..84302679316 100644 --- a/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/main/java/org/eclipse/jetty/server/session/AbstractWebAppObjectInSessionTest.java @@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.resource.Resource; @@ -35,7 +36,7 @@ import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; /** * AbstractWebAppObjectInSessionTest @@ -133,9 +134,9 @@ public abstract class AbstractWebAppObjectInSessionTest extends AbstractTestBase ContentResponse response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); String sessionCookie = response.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); // Mangle the cookie, replacing Path with $Path, etc. - sessionCookie = sessionCookie.replaceFirst("(\\W)(P|p)ath=", "$1\\$Path="); + sessionCookie = sessionCookie.replaceFirst("(\\W)([Pp])ath=", "$1\\$Path="); //ensure request has finished being handled synchronizer.await(5, TimeUnit.SECONDS); @@ -143,7 +144,8 @@ public abstract class AbstractWebAppObjectInSessionTest extends AbstractTestBase // Perform a request to server2 using the session cookie from the previous request Request request2 = client.newRequest("http://localhost:" + port2 + contextPath + servletMapping + "?action=get"); request2.method(HttpMethod.GET); - request2.header("Cookie", sessionCookie); + HttpField cookie = new HttpField("Cookie", sessionCookie); + request2.headers(headers -> headers.put(cookie)); ContentResponse response2 = request2.send(); assertEquals(HttpServletResponse.SC_OK, response2.getStatus()); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java index f6a6c6458ed..72473677ff7 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/ClientCrossContextSessionTest.java @@ -28,12 +28,14 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; /** * ClientCrossContextSessionTest @@ -81,12 +83,13 @@ public class ClientCrossContextSessionTest assertEquals(HttpServletResponse.SC_OK, response.getStatus()); String sessionCookie = response.getHeaders().get("Set-Cookie"); - assertTrue(sessionCookie != null); + assertNotNull(sessionCookie); String sessionId = TestServer.extractSessionId(sessionCookie); // Perform a request to contextB with the same session cookie Request request = client.newRequest("http://localhost:" + port + contextB + servletMapping); - request.header("Cookie", "JSESSIONID=" + sessionId); + HttpField cookie = new HttpField("Cookie", "JSESSIONID=" + sessionId); + request.headers(headers -> headers.put(cookie)); ContentResponse responseB = request.send(); assertEquals(HttpServletResponse.SC_OK, responseB.getStatus()); assertEquals(servletA.sessionId, servletB.sessionId); @@ -122,7 +125,7 @@ public class ClientCrossContextSessionTest // Check that we don't see things put in session by contextB Object objectB = session.getAttribute("B"); - assertTrue(objectB == null); + assertNull(objectB); } } @@ -145,7 +148,7 @@ public class ClientCrossContextSessionTest // Check that we don't see things put in session by contextA Object objectA = session.getAttribute("A"); - assertTrue(objectA == null); + assertNull(objectA); } } } diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java index 02c1e3edeee..9673fbd094e 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DeleteUnloadableSessionTest.java @@ -31,6 +31,7 @@ import javax.servlet.http.HttpSession; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -45,14 +46,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; */ public class DeleteUnloadableSessionTest { - /** * DelSessionDataStore */ public static class DelSessionDataStore extends AbstractSessionDataStore { - int count = 0; - Object o = new Object(); String unloadableId = null; @@ -64,7 +62,7 @@ public class DeleteUnloadableSessionTest } @Override - public boolean exists(String id) throws Exception + public boolean exists(String id) { return o != null; } @@ -77,7 +75,7 @@ public class DeleteUnloadableSessionTest } @Override - public boolean delete(String id) throws Exception + public boolean delete(String id) { if (id.equals(unloadableId)) { @@ -88,7 +86,7 @@ public class DeleteUnloadableSessionTest } @Override - public void doStore(String id, SessionData data, long lastSaveTime) throws Exception + public void doStore(String id, SessionData data, long lastSaveTime) { //pretend it was saved } @@ -102,9 +100,8 @@ public class DeleteUnloadableSessionTest public static class DelSessionDataStoreFactory extends AbstractSessionDataStoreFactory { - @Override - public SessionDataStore getSessionDataStore(SessionHandler handler) throws Exception + public SessionDataStore getSessionDataStore(SessionHandler handler) { return new DelSessionDataStore(); } @@ -141,8 +138,8 @@ public class DeleteUnloadableSessionTest DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory(); cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT); cacheFactory.setRemoveUnloadableSessions(true); - SessionDataStoreFactory storeFactory = new DelSessionDataStoreFactory(); - ((AbstractSessionDataStoreFactory)storeFactory).setGracePeriodSec(scavengePeriod); + AbstractSessionDataStoreFactory storeFactory = new DelSessionDataStoreFactory(); + storeFactory.setGracePeriodSec(scavengePeriod); TestServer server = new TestServer(0, inactivePeriod, scavengePeriod, cacheFactory, storeFactory); ServletContextHandler context = server.addContext(contextPath); @@ -154,7 +151,7 @@ public class DeleteUnloadableSessionTest ServletHolder holder = new ServletHolder(servlet); context.addServlet(holder, servletMapping); - try (StacklessLogging stackless = new StacklessLogging(DeleteUnloadableSessionTest.class.getPackage())) + try (StacklessLogging ignored = new StacklessLogging(DeleteUnloadableSessionTest.class.getPackage())) { server.start(); int port = server.getPort(); @@ -166,7 +163,8 @@ public class DeleteUnloadableSessionTest scopeListener.setExitSynchronizer(latch); String sessionCookie = "JSESSIONID=w0rm3zxpa6h1zg1mevtv76b3te00.w0;$Path=/"; Request request = client.newRequest("http://localhost:" + port + contextPath + servletMapping + "?action=test"); - request.header("Cookie", sessionCookie); + HttpField cookie = new HttpField("Cookie", sessionCookie); + request.headers(headers -> headers.put(cookie)); ContentResponse response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); diff --git a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DuplicateCookieTest.java b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DuplicateCookieTest.java index a254d2fe3cd..e6513c376ec 100644 --- a/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DuplicateCookieTest.java +++ b/tests/test-sessions/test-sessions-common/src/test/java/org/eclipse/jetty/server/session/DuplicateCookieTest.java @@ -61,7 +61,7 @@ public class DuplicateCookieTest server1.start(); int port1 = server1.getPort(); - try (StacklessLogging stackless = new StacklessLogging(DuplicateCookieTest.class.getPackage())) + try (StacklessLogging ignored = new StacklessLogging(DuplicateCookieTest.class.getPackage())) { //create a valid session createUnExpiredSession(contextHandler.getSessionHandler().getSessionCache(), @@ -73,8 +73,8 @@ public class DuplicateCookieTest //make a request with another session cookie in there that does not exist Request request = client.newRequest("http://localhost:" + port1 + contextPath + servletMapping + "?action=check"); - request.header("Cookie", "JSESSIONID=123"); //doesn't exist - request.header("Cookie", "JSESSIONID=4422"); //does exist + request.headers(headers -> headers.add("Cookie", "JSESSIONID=123")); //doesn't exist + request.headers(headers -> headers.add("Cookie", "JSESSIONID=4422")); //does exist ContentResponse response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertEquals("4422", response.getContentAsString()); @@ -104,7 +104,7 @@ public class DuplicateCookieTest server1.start(); int port1 = server1.getPort(); - try (StacklessLogging stackless = new StacklessLogging(DuplicateCookieTest.class.getPackage())) + try (StacklessLogging ignored = new StacklessLogging(DuplicateCookieTest.class.getPackage())) { //create a valid session createUnExpiredSession(contextHandler.getSessionHandler().getSessionCache(), @@ -120,8 +120,8 @@ public class DuplicateCookieTest //make a request with another session cookie in there that is not valid Request request = client.newRequest("http://localhost:" + port1 + contextPath + servletMapping + "?action=check"); - request.header("Cookie", "JSESSIONID=1122"); //is valid - request.header("Cookie", "JSESSIONID=2233"); //is invalid + request.headers(headers -> headers.add("Cookie", "JSESSIONID=1122")); //is valid + request.headers(headers -> headers.add("Cookie", "JSESSIONID=2233")); //is invalid ContentResponse response = request.send(); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertEquals("1122", response.getContentAsString()); @@ -151,7 +151,7 @@ public class DuplicateCookieTest server1.start(); int port1 = server1.getPort(); - try (StacklessLogging stackless = new StacklessLogging(DuplicateCookieTest.class.getPackage())) + try (StacklessLogging ignored = new StacklessLogging(DuplicateCookieTest.class.getPackage())) { //create some of unexpired sessions createUnExpiredSession(contextHandler.getSessionHandler().getSessionCache(), @@ -169,8 +169,8 @@ public class DuplicateCookieTest //make a request with multiple valid session ids Request request = client.newRequest("http://localhost:" + port1 + contextPath + servletMapping + "?action=check"); - request.header("Cookie", "JSESSIONID=1234"); - request.header("Cookie", "JSESSIONID=5678"); + request.headers(headers -> headers.add("Cookie", "JSESSIONID=1234")); + request.headers(headers -> headers.add("Cookie", "JSESSIONID=5678")); ContentResponse response = request.send(); assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); } diff --git a/tests/test-webapps/test-bad-websocket-webapp/pom.xml b/tests/test-webapps/test-bad-websocket-webapp/pom.xml index 4ae87c9a887..7e7726c941a 100644 --- a/tests/test-webapps/test-bad-websocket-webapp/pom.xml +++ b/tests/test-webapps/test-bad-websocket-webapp/pom.xml @@ -17,11 +17,13 @@ org.eclipse.jetty.toolchain jetty-javax-websocket-api + provided org.eclipse.jetty.websocket websocket-javax-common ${project.version} + provided org.eclipse.jetty.toolchain diff --git a/tests/test-webapps/test-websocket-webapp/pom.xml b/tests/test-webapps/test-websocket-webapp/pom.xml index d9a8d3cfb5d..8759869ec7d 100644 --- a/tests/test-webapps/test-websocket-webapp/pom.xml +++ b/tests/test-webapps/test-websocket-webapp/pom.xml @@ -25,11 +25,13 @@ org.eclipse.jetty.toolchain jetty-javax-websocket-api + provided org.eclipse.jetty.websocket websocket-javax-common ${project.version} + provided org.eclipse.jetty.toolchain diff --git a/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnOpenServerEndpoint.java b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/EchoEndpoint.java similarity index 71% rename from tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnOpenServerEndpoint.java rename to tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/EchoEndpoint.java index 32b79f14a67..fcbc80642fe 100644 --- a/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnOpenServerEndpoint.java +++ b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/EchoEndpoint.java @@ -18,8 +18,6 @@ package org.eclipse.jetty.tests.webapp.websocket; -import java.io.IOException; -import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; @@ -28,16 +26,15 @@ import javax.websocket.server.ServerEndpoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@ServerEndpoint("/onopen/{arg}") -public class OnOpenServerEndpoint +@ServerEndpoint(value = "/", decoders = {StringSequenceDecoder.class}) +public class EchoEndpoint { - private static final Logger LOGGER = LoggerFactory.getLogger(OnOpenServerEndpoint.class); - private static String open = ""; + private static final Logger LOGGER = LoggerFactory.getLogger(EchoEndpoint.class); @OnMessage - public String echo(String echo) + public String echo(StringSequence echo) { - return open + echo; + return echo.toString(); } @OnOpen @@ -45,12 +42,4 @@ public class OnOpenServerEndpoint { LOGGER.info("Session opened"); } - - @OnError - public void onError(Session session, Throwable t) - throws IOException - { - String message = "Error happened:" + t.getMessage(); - session.getBasicRemote().sendText(message); - } } diff --git a/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnCloseServerEndpoint.java b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequence.java similarity index 50% rename from tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnCloseServerEndpoint.java rename to tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequence.java index 11467a865f4..0c51c77ce1e 100644 --- a/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/OnCloseServerEndpoint.java +++ b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequence.java @@ -18,39 +18,37 @@ package org.eclipse.jetty.tests.webapp.websocket; -import java.io.IOException; -import javax.websocket.OnClose; -import javax.websocket.OnError; -import javax.websocket.OnMessage; -import javax.websocket.Session; -import javax.websocket.server.ServerEndpoint; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@ServerEndpoint("/onclose/{arg}") -public class OnCloseServerEndpoint +public class StringSequence + implements CharSequence { - private static final Logger LOGGER = LoggerFactory.getLogger(OnCloseServerEndpoint.class); - private static String close = ""; + public String stringBuffer; - @OnMessage - public String echo(String echo) + public StringSequence(String hold) { - return close + echo; + stringBuffer = hold; } - @OnClose - public void onClose(Session session) + @Override + public int length() { - LOGGER.info("Session close"); + return stringBuffer.length(); } - @OnError - public void onError(Session session, Throwable t) - throws IOException + @Override + public char charAt(int index) { - String message = "Error happened:" + t.getMessage(); - session.getBasicRemote().sendText(message); + return stringBuffer.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) + { + return stringBuffer.subSequence(start, end); + } + + @Override + public String toString() + { + return stringBuffer; } } diff --git a/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequenceDecoder.java b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequenceDecoder.java new file mode 100644 index 00000000000..fa0d28bc169 --- /dev/null +++ b/tests/test-webapps/test-websocket-webapp/src/main/java/org/eclipse/jetty/tests/webapp/websocket/StringSequenceDecoder.java @@ -0,0 +1,50 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under +// the terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0 +// +// This Source Code may also be made available under the following +// Secondary Licenses when the conditions for such availability set +// forth in the Eclipse Public License, v. 2.0 are satisfied: +// the Apache License v2.0 which is available at +// https://www.apache.org/licenses/LICENSE-2.0 +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.tests.webapp.websocket; + +import javax.websocket.DecodeException; +import javax.websocket.Decoder; +import javax.websocket.EndpointConfig; + +public class StringSequenceDecoder implements Decoder.Text +{ + @Override + public StringSequence decode(String s) throws DecodeException + { + return new StringSequence(s); + } + + @Override + public void init(EndpointConfig config) + { + + } + + @Override + public void destroy() + { + + } + + @Override + public boolean willDecode(String s) + { + return true; + } +}