diff --git a/VERSION.txt b/VERSION.txt index 3be5a9f1af7..26a8b5fb73a 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -23,6 +23,15 @@ jetty-10.0.0.beta3 - 21 October 2020 + 5475 Update to spifly 1.3.2 and asm 9 + 5480 NPE from WebInfConfiguration.deconfigure during WebAppContext shutdown +jetty-9.4.34.v20201102 - 02 November 2020 + + 5320 Using WebSocketClient with jetty-websocket-httpclient.xml in a Jetty + web application causes ClassCastException + + 5488 jetty-dir.css not found when using JPMS + + 5498 ServletHolder lifecycle correctness + + 5521 ResourceCollection NPE in list() + + 5535 Support regex in SslContextFactory include/exclude of protocols + + 5555 NPE for servlet with no mapping + jetty-9.4.33.v20201020 - 20 October 2020 + 5022 Cleanup ServletHandler, specifically with respect to making filter chains more extensible @@ -80,7 +89,7 @@ jetty-10.0.0.beta2 - 02 October 2020 be empty string, but is `"/"` + 5064 NotSerializableException for OpenIdConfiguration + 5069 HttpClientTimeoutTests can occasionally fail due to unreachable network - + 5079 :authority header for IPv6 address not having square brackets + + 5079 :authority header for IPv6 address not having square brackets + 5081 Review HouseKeeper locking + 5083 Convert synchronized usages to AutoLock + 5096 using JettyWebSocketServlet without having a WebSocketUpgradeFilter diff --git a/demos/demo-jndi-webapp/pom.xml b/demos/demo-jndi-webapp/pom.xml index f267041752b..4af94934dfb 100644 --- a/demos/demo-jndi-webapp/pom.xml +++ b/demos/demo-jndi-webapp/pom.xml @@ -21,14 +21,14 @@ generate-xml-files process-resources - + - + run diff --git a/demos/demo-spec/demo-spec-webapp/pom.xml b/demos/demo-spec/demo-spec-webapp/pom.xml index 9f035ffabb9..62af3267b9e 100644 --- a/demos/demo-spec/demo-spec-webapp/pom.xml +++ b/demos/demo-spec/demo-spec-webapp/pom.xml @@ -85,14 +85,14 @@ generate-xml-files process-resources - + - + run diff --git a/javadoc/pom.xml b/javadoc/pom.xml index 6b6e2625dcf..387f5c15da8 100644 --- a/javadoc/pom.xml +++ b/javadoc/pom.xml @@ -88,9 +88,9 @@ run - + - + 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 39089a1e801..bd476883a85 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 @@ -1377,9 +1377,12 @@ public class SslBytesServerTest extends SslBytesTest // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); - assertThat(sslFills.get(), Matchers.lessThan(100)); + // The new HttpInput impl tends to call fill and parse more often than the previous one + // b/c HttpChannel.needContent() does a fill and parse before doing a fill interested; + // this runs the parser an goes to the OS more often but requires less rescheduling. + assertThat(sslFills.get(), Matchers.lessThan(150)); assertThat(sslFlushes.get(), Matchers.lessThan(50)); - assertThat(httpParses.get(), Matchers.lessThan(100)); + assertThat(httpParses.get(), Matchers.lessThan(150)); assertNull(request.get(5, TimeUnit.SECONDS)); @@ -1399,9 +1402,12 @@ public class SslBytesServerTest extends SslBytesTest // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); - assertThat(sslFills.get(), Matchers.lessThan(100)); + // The new HttpInput impl tends to call fill and parse more often than the previous one + // b/c HttpChannel.needContent() does a fill and parse before doing a fill interested; + // this runs the parser an goes to the OS more often but requires less rescheduling. + assertThat(sslFills.get(), Matchers.lessThan(150)); assertThat(sslFlushes.get(), Matchers.lessThan(50)); - assertThat(httpParses.get(), Matchers.lessThan(100)); + assertThat(httpParses.get(), Matchers.lessThan(150)); closeClient(client); } @@ -1596,9 +1602,12 @@ public class SslBytesServerTest extends SslBytesTest // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); - assertThat(sslFills.get(), Matchers.lessThan(50)); + // The new HttpInput impl tends to call fill and parse more often than the previous one + // b/c HttpChannel.needContent() does a fill and parse before doing a fill interested; + // this runs the parser and goes to the OS more often but requires less rescheduling. + assertThat(sslFills.get(), Matchers.lessThan(70)); assertThat(sslFlushes.get(), Matchers.lessThan(20)); - assertThat(httpParses.get(), Matchers.lessThan(50)); + assertThat(httpParses.get(), Matchers.lessThan(70)); closeClient(client); } @@ -1743,9 +1752,12 @@ public class SslBytesServerTest extends SslBytesTest // Check that we did not spin TimeUnit.MILLISECONDS.sleep(500); - assertThat(sslFills.get(), Matchers.lessThan(50)); + // The new HttpInput impl tends to call fill and parse more often than the previous one + // b/c HttpChannel.needContent() does a fill and parse before doing a fill interested; + // this runs the parser and goes to the OS more often but requires less rescheduling. + assertThat(sslFills.get(), Matchers.lessThan(80)); assertThat(sslFlushes.get(), Matchers.lessThan(20)); - assertThat(httpParses.get(), Matchers.lessThan(100)); + assertThat(httpParses.get(), Matchers.lessThan(120)); closeClient(client); } diff --git a/jetty-documentation/src/main/asciidoc/operations-guide/begin/architecture.adoc b/jetty-documentation/src/main/asciidoc/operations-guide/begin/architecture.adoc index f816db219a3..a521be56079 100644 --- a/jetty-documentation/src/main/asciidoc/operations-guide/begin/architecture.adoc +++ b/jetty-documentation/src/main/asciidoc/operations-guide/begin/architecture.adoc @@ -21,10 +21,10 @@ There are two main concepts on which the Eclipse Jetty standalone server is based: -* the xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features -* the xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features, you need for your web applications +* The xref:og-begin-arch-modules[Jetty _module_ system], that provides the Jetty features +* The xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory], that provides a place where you configure the modules, and therefore the features you need for your web applications -After installing Jetty, you want to setup a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules]. +After installing Jetty, you will want to set up a xref:og-begin-arch-jetty-base[`$JETTY_BASE` directory] where you configure xref:og-begin-arch-modules[Jetty modules]. [[og-begin-arch-modules]] ===== Eclipse Jetty Architecture: Modules @@ -33,19 +33,19 @@ The Jetty standalone server is made of components that are assembled together, c A Jetty _module_ is made of one or more components that work together to provide typically one feature, although they may provide more than one feature. -A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files rather than using Java APIs. -What you can do in Java code to assemble Jetty components, it can be done using Jetty modules. +A Jetty module is nothing more than Jetty components assembled together like you would do using Java APIs, just done in a declarative way using configuration files. +What you can do in Java code to assemble Jetty components can be done using Jetty modules. -A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module, that in turn depends on the `threadpool` and `logging` Jetty modules. +A Jetty module may be dependent on other Jetty modules: for example, the `http` Jetty module depends on the `server` Jetty module which in turn depends on the `threadpool` and `logging` Jetty modules. -Every feature in a Jetty server is enabled by enabling correspondent Jetty modules. +Every feature in a Jetty server is enabled by enabling the corresponding Jetty module(s). For example, if you enable only the `http` Jetty module, then your Jetty standalone server will only be able to listen to a network port for clear-text HTTP requests. It will not be able to process secure HTTP (i.e. `https`) requests, it will not be able to process WebSocket, or HTTP/2 or any other protocol because the correspondent modules have not been enabled. You can even start a Jetty server _without_ listening on a network port -- for example because you have enabled a custom module you wrote that provides the features you need. -This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk that client use a module that you did not know was even there. +This allows the Jetty standalone server to be as small as necessary: modules that are not enabled are not loaded, don't waste memory, and you don't risk a client using a module that you did not know was even there. For more detailed information about the Jetty module system, see xref:og-modules[this section]. @@ -60,7 +60,7 @@ This separation between `$JETTY_HOME` and `$JETTY_BASE` allows upgrades without `$JETTY_HOME` contains the Jetty runtime and libraries and the default configuration, while a `$JETTY_BASE` contains your web applications and any override of the default configuration. For example, with the `$JETTY_HOME` installation the default value for the network port for clear-text HTTP is `8080`. -However, you want that port to be `6060`, for example because you are behind a load balancer that is configured to forward to the backend on port `6060`. +However, you want that port to be `6060`, because you are behind a load balancer that is configured to forward to the backend on port `6060`. Instead, you want to configure the clear-text HTTP port in your `$JETTY_BASE`. When you upgrade Jetty, you will upgrade only files in `$JETTY_HOME`, and all the configuration in `$JETTY_BASE` will remain unchanged. diff --git a/jetty-documentation/src/main/asciidoc/operations-guide/begin/download.adoc b/jetty-documentation/src/main/asciidoc/operations-guide/begin/download.adoc index 963dd601407..4a867a364ed 100644 --- a/jetty-documentation/src/main/asciidoc/operations-guide/begin/download.adoc +++ b/jetty-documentation/src/main/asciidoc/operations-guide/begin/download.adoc @@ -21,5 +21,5 @@ The Eclipse Jetty distribution is available for download from link:https://www.eclipse.org/jetty/download.html[] -The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operative systems. +The Eclipse Jetty distribution is available in both `zip` and `gzip` formats; download the one most appropriate for your system, typically `zip` for Windows and `gzip` for other operating systems. diff --git a/jetty-documentation/src/main/asciidoc/operations-guide/begin/install.adoc b/jetty-documentation/src/main/asciidoc/operations-guide/begin/install.adoc index 55a498062f5..cfa1d8ecf90 100644 --- a/jetty-documentation/src/main/asciidoc/operations-guide/begin/install.adoc +++ b/jetty-documentation/src/main/asciidoc/operations-guide/begin/install.adoc @@ -30,6 +30,6 @@ The rest of the instructions in this documentation will refer to this location a IMPORTANT: It is important that *only* stable release versions are used in production environments. Versions that have been deprecated or are released as Milestones (M), Alpha, Beta or Release Candidates (RC) are *not* suitable for production as they may contain security flaws or incomplete/non-functioning feature sets. -If you are new to Jetty, read the xref:og-begin-arch[Jetty architecture short section] to become familiar with the terms used in this document. -Otherwise, you can jump to the xref:og-begin-start[start Jetty section]. +If you are new to Jetty, you should read the xref:og-begin-arch[Jetty architecture section below] to become familiar with the terms used in this documentation. +Otherwise, you can jump to the xref:og-begin-start[section on starting Jetty]. diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpChannelOverFCGI.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpChannelOverFCGI.java index 0c7b168604b..5759121ca55 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpChannelOverFCGI.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/HttpChannelOverFCGI.java @@ -18,7 +18,11 @@ package org.eclipse.jetty.fcgi.server; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import java.util.Locale; +import java.util.Queue; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; @@ -34,8 +38,10 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpInput; import org.eclipse.jetty.server.HttpTransport; import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +49,9 @@ public class HttpChannelOverFCGI extends HttpChannel { private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class); + private final Queue _contentQueue = new LinkedList<>(); + private final AutoLock _lock = new AutoLock(); + private HttpInput.Content _specialContent; private final HttpFields.Mutable fields = HttpFields.build(); private final Dispatcher dispatcher; private String method; @@ -57,6 +66,101 @@ public class HttpChannelOverFCGI extends HttpChannel this.dispatcher = new Dispatcher(connector.getServer().getThreadPool(), this); } + @Override + public boolean onContent(HttpInput.Content content) + { + boolean b = super.onContent(content); + + Throwable failure; + try (AutoLock l = _lock.lock()) + { + failure = _specialContent == null ? null : _specialContent.getError(); + if (failure == null) + _contentQueue.offer(content); + } + if (failure != null) + content.failed(failure); + + return b; + } + + @Override + public boolean needContent() + { + try (AutoLock l = _lock.lock()) + { + boolean hasContent = _specialContent != null || !_contentQueue.isEmpty(); + if (LOG.isDebugEnabled()) + LOG.debug("needContent has content? {}", hasContent); + return hasContent; + } + } + + @Override + public HttpInput.Content produceContent() + { + HttpInput.Content content; + try (AutoLock l = _lock.lock()) + { + content = _contentQueue.poll(); + if (content == null) + content = _specialContent; + } + if (LOG.isDebugEnabled()) + LOG.debug("produceContent has produced {}", content); + return content; + } + + @Override + public boolean failAllContent(Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("failing all content with {}", (Object)failure); + List copy; + try (AutoLock l = _lock.lock()) + { + copy = new ArrayList<>(_contentQueue); + _contentQueue.clear(); + } + copy.forEach(c -> c.failed(failure)); + HttpInput.Content lastContent = copy.isEmpty() ? null : copy.get(copy.size() - 1); + boolean atEof = lastContent != null && lastContent.isEof(); + if (LOG.isDebugEnabled()) + LOG.debug("failed all content, EOF = {}", atEof); + return atEof; + } + + @Override + public boolean failed(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("failed " + x); + + try (AutoLock l = _lock.lock()) + { + Throwable error = _specialContent == null ? null : _specialContent.getError(); + + if (error != null && error != x) + error.addSuppressed(x); + else + _specialContent = new HttpInput.ErrorContent(x); + } + + return getRequest().getHttpInput().onContentProducible(); + } + + @Override + protected boolean eof() + { + if (LOG.isDebugEnabled()) + LOG.debug("received EOF"); + try (AutoLock l = _lock.lock()) + { + _specialContent = new HttpInput.EofContent(); + } + return getRequest().getHttpInput().onContentProducible(); + } + protected void header(HttpField field) { String name = field.getName(); @@ -127,12 +231,46 @@ public class HttpChannelOverFCGI extends HttpChannel public boolean onIdleTimeout(Throwable timeout) { - boolean handle = getRequest().getHttpInput().onIdleTimeout(timeout); + boolean handle = doOnIdleTimeout(timeout); if (handle) execute(this); return !handle; } + private boolean doOnIdleTimeout(Throwable x) + { + boolean neverDispatched = getState().isIdle(); + boolean waitingForContent; + HttpInput.Content specialContent; + try (AutoLock l = _lock.lock()) + { + waitingForContent = _contentQueue.isEmpty() || _contentQueue.peek().remaining() == 0; + specialContent = _specialContent; + } + if ((waitingForContent || neverDispatched) && specialContent == null) + { + x.addSuppressed(new Throwable("HttpInput idle timeout")); + try (AutoLock l = _lock.lock()) + { + _specialContent = new HttpInput.ErrorContent(x); + } + return getRequest().getHttpInput().onContentProducible(); + } + return false; + } + + @Override + public void recycle() + { + try (AutoLock l = _lock.lock()) + { + if (!_contentQueue.isEmpty()) + throw new AssertionError("unconsumed content: " + _contentQueue); + _specialContent = null; + } + super.recycle(); + } + private static class Dispatcher implements Runnable { private final AtomicReference state = new AtomicReference<>(State.IDLE); diff --git a/jetty-gcloud/jetty-gcloud-session-manager/pom.xml b/jetty-gcloud/jetty-gcloud-session-manager/pom.xml index d3b46391e75..a343dd013cf 100644 --- a/jetty-gcloud/jetty-gcloud-session-manager/pom.xml +++ b/jetty-gcloud/jetty-gcloud-session-manager/pom.xml @@ -122,10 +122,10 @@ run - + - + @@ -135,12 +135,12 @@ run - + - + diff --git a/jetty-home/pom.xml b/jetty-home/pom.xml index c98483bed25..0397b02b90c 100644 --- a/jetty-home/pom.xml +++ b/jetty-home/pom.xml @@ -514,9 +514,9 @@ run - + - + diff --git a/jetty-http-spi/pom.xml b/jetty-http-spi/pom.xml index 0fea1098871..9f2572a9d1a 100644 --- a/jetty-http-spi/pom.xml +++ b/jetty-http-spi/pom.xml @@ -53,7 +53,7 @@ com.sun.xml.ws jaxws-rt - 2.3.0.2 + 2.3.3 test diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java index 973c957d4c5..7040c82407a 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2Stream.java @@ -22,6 +22,7 @@ import java.io.EOFException; import java.io.IOException; import java.nio.channels.WritePendingException; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; @@ -235,6 +236,21 @@ public class HTTP2Stream extends IdleTimeout implements IStream, Callback, Dumpa return state == CloseState.REMOTELY_CLOSED || state == CloseState.CLOSING || state == CloseState.CLOSED; } + @Override + public boolean failAllData(Throwable x) + { + List copy; + try (AutoLock l = lock.lock()) + { + dataDemand = 0; + copy = new ArrayList<>(dataQueue); + dataQueue.clear(); + } + copy.forEach(dataEntry -> dataEntry.callback.failed(x)); + DataEntry lastDataEntry = copy.isEmpty() ? null : copy.get(copy.size() - 1); + return lastDataEntry != null && lastDataEntry.frame.isEndStream(); + } + public boolean isLocallyClosed() { return closeState.get() == CloseState.LOCALLY_CLOSED; diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java index 0251f720672..3f6fefc937e 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/HTTP2StreamEndPoint.java @@ -216,6 +216,9 @@ public abstract class HTTP2StreamEndPoint implements EndPoint else { entry.succeed(); + // WebSocket does not have a backpressure API so you must always demand + // the next frame after succeeding the previous one. + stream.demand(1); } return length; } diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java index 81a38ae9f7b..6b97474febf 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/IStream.java @@ -119,6 +119,14 @@ public interface IStream extends Stream, Attachable, Closeable */ boolean isRemotelyClosed(); + /** + * Fail all data queued in the stream and reset + * demand to 0. + * @param x the exception to fail the data with. + * @return true if the end of the stream was reached, false otherwise. + */ + boolean failAllData(Throwable x); + /** * @return whether this stream has been reset (locally or remotely) or has been failed * @see #isReset() diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java index bf41f0fe2d2..ac72f5333ad 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Stream.java @@ -242,7 +242,10 @@ public interface Stream * @param callback the callback to complete when the bytes of the DATA frame have been consumed * @see #onDataDemanded(Stream, DataFrame, Callback) */ - public void onData(Stream stream, DataFrame frame, Callback callback); + public default void onData(Stream stream, DataFrame frame, Callback callback) + { + callback.succeeded(); + } /** *

Callback method invoked when a DATA frame has been demanded.

diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ContentDemander_state.puml b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ContentDemander_state.puml new file mode 100644 index 00000000000..f9ee5b4af5a --- /dev/null +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/ContentDemander_state.puml @@ -0,0 +1,26 @@ +@startuml + +null: +content: +DEMANDING: +EOF: + +[*] --> null + +null --> DEMANDING : demand() +null --> EOF : eof() +null -left-> null : onTimeout() + +DEMANDING --> DEMANDING : demand() +DEMANDING --> content : onContent()\n onTimeout() +DEMANDING --> EOF : eof() + +EOF --> EOF : eof()\n onTimeout() + +note bottom of content: content1 -> content2 is only\nvalid if content1 is special +note top of content: content -> null only happens\nwhen content is not special +content --> content : onContent()\n onTimeout() +content --> null: take() +content --> EOF: eof() + +@enduml diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java index d9c49d60399..6b9202803ab 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnectionFactory.java @@ -157,7 +157,7 @@ public class HTTP2ServerConnectionFactory extends AbstractHTTP2ServerConnectionF } @Override - public void onData(Stream stream, DataFrame frame, Callback callback) + public void onDataDemanded(Stream stream, DataFrame frame, Callback callback) { getConnection().onData((IStream)stream, frame, callback); } diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java index a54e6769c55..f3b8100042c 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpChannelOverHTTP2.java @@ -21,6 +21,7 @@ package org.eclipse.jetty.http2.server; import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.eclipse.jetty.http.BadMessageException; @@ -57,10 +58,12 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ private boolean _expect100Continue; private boolean _delayedUntilContent; private boolean _useOutputDirectByteBuffers; + private final ContentDemander _contentDemander; public HttpChannelOverHTTP2(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransportOverHTTP2 transport) { super(connector, configuration, endPoint, transport); + _contentDemander = new ContentDemander(); } protected IStream getStream() @@ -131,9 +134,18 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ _delayedUntilContent = getHttpConfiguration().isDelayDispatchUntilContent() && !endStream && !_expect100Continue && !connect; - // Delay the demand of DATA frames for CONNECT with :protocol. - if (!connect || request.getProtocol() == null) - getStream().demand(1); + // Delay the demand of DATA frames for CONNECT with :protocol + // or for normal requests expecting 100 continue. + if (connect) + { + if (request.getProtocol() == null) + _contentDemander.demand(false); + } + else + { + if (_delayedUntilContent) + _contentDemander.demand(false); + } if (LOG.isDebugEnabled()) { @@ -204,6 +216,7 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ { _expect100Continue = false; _delayedUntilContent = false; + _contentDemander.recycle(); super.recycle(); getHttpTransport().recycle(); } @@ -224,26 +237,16 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ @Override public Runnable onData(DataFrame frame, Callback callback) { - return onRequestContent(frame, callback); - } - - public Runnable onRequestContent(DataFrame frame, final Callback callback) - { - Stream stream = getStream(); - if (stream.isReset()) - { - // Consume previously queued content to - // enlarge the session flow control window. - consumeInput(); - // Consume immediately this content. - callback.succeeded(); - return null; - } - ByteBuffer buffer = frame.getData(); int length = buffer.remaining(); - boolean handle = onContent(new HttpInput.Content(buffer) + HttpInput.Content content = new HttpInput.Content(buffer) { + @Override + public boolean isEof() + { + return frame.isEndStream(); + } + @Override public void succeeded() { @@ -261,23 +264,31 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ { return callback.getInvocationType(); } - }); + }; + boolean needed = _contentDemander.onContent(content); + boolean handle = onContent(content); boolean endStream = frame.isEndStream(); if (endStream) { boolean handleContent = onContentComplete(); + // This will generate EOF -> must happen before onContentProducible. boolean handleRequest = onRequestComplete(); handle |= handleContent | handleRequest; } + boolean woken = needed && getRequest().getHttpInput().onContentProducible(); + handle |= woken; if (LOG.isDebugEnabled()) { - LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, handle: {}", + Stream stream = getStream(); + LOG.debug("HTTP2 Request #{}/{}: {} bytes of {} content, woken: {}, needed: {}, handle: {}", stream.getId(), Integer.toHexString(stream.getSession().hashCode()), length, endStream ? "last" : "some", + woken, + needed, handle); } @@ -286,6 +297,326 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ return handle || wasDelayed ? this : null; } + /** + * Demanding content is a marker content that is used to remember that a demand was + * registered into the stream. The {@code needed} flag indicates if the demand originated + * from a call to {@link #produceContent()} when false or {@link #needContent()} + * when true, as {@link HttpInput#onContentProducible()} must only be called + * only when {@link #needContent()} was called. + * Instances of this class must never escape the scope of this channel impl, + * so {@link #produceContent()} must never return one. + */ + private static final class DemandingContent extends HttpInput.SpecialContent + { + private final boolean needed; + + private DemandingContent(boolean needed) + { + this.needed = needed; + } + } + + private static final HttpInput.Content EOF = new HttpInput.EofContent(); + private static final HttpInput.Content DEMANDING_NEEDED = new DemandingContent(true); + private static final HttpInput.Content DEMANDING_NOT_NEEDED = new DemandingContent(false); + + private class ContentDemander + { + private final AtomicReference _content = new AtomicReference<>(); + + public void recycle() + { + if (LOG.isDebugEnabled()) + LOG.debug("recycle {}", this); + HttpInput.Content c = _content.getAndSet(null); + if (c != null && !c.isSpecial()) + throw new AssertionError("unconsumed content: " + c); + } + + public HttpInput.Content poll() + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("poll, content = {}", c); + if (c == null || c.isSpecial() || _content.compareAndSet(c, c.isEof() ? EOF : null)) + { + if (LOG.isDebugEnabled()) + LOG.debug("returning current content"); + return c; + } + } + } + + public boolean demand(boolean needed) + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("demand({}), content = {}", needed, c); + if (c instanceof DemandingContent) + { + if (needed && !((DemandingContent)c).needed) + { + if (!_content.compareAndSet(c, DEMANDING_NEEDED)) + { + if (LOG.isDebugEnabled()) + LOG.debug("already demanding but switched needed flag to true"); + continue; + } + } + if (LOG.isDebugEnabled()) + LOG.debug("already demanding, returning false"); + return false; + } + if (c != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("content available, returning true"); + return true; + } + if (_content.compareAndSet(null, needed ? DEMANDING_NEEDED : DEMANDING_NOT_NEEDED)) + { + IStream stream = getStream(); + if (stream == null) + { + _content.set(null); + if (LOG.isDebugEnabled()) + LOG.debug("no content available, switched to demanding but stream is now null"); + return false; + } + if (LOG.isDebugEnabled()) + LOG.debug("no content available, demanding stream {}", stream); + stream.demand(1); + c = _content.get(); + boolean hasContent = !(c instanceof DemandingContent) && c != null; + if (LOG.isDebugEnabled()) + LOG.debug("has content now? {}", hasContent); + return hasContent; + } + } + } + + public boolean onContent(HttpInput.Content content) + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("content delivered by stream: {}, current content: {}", content, c); + if (c instanceof DemandingContent) + { + if (_content.compareAndSet(c, content)) + { + boolean needed = ((DemandingContent)c).needed; + if (LOG.isDebugEnabled()) + LOG.debug("replacing demand content with {} succeeded; returning {}", content, needed); + return needed; + } + } + else if (c == null) + { + if (!content.isSpecial()) + { + // This should never happen, consider as a bug. + content.failed(new IllegalStateException("Non special content without demand : " + content)); + return false; + } + if (_content.compareAndSet(null, content)) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing null content with {} succeeded", content); + return false; + } + } + else if (c.isEof() && content.isEof() && content.isEmpty()) + { + content.succeeded(); + return true; + } + else if (content.getError() != null) + { + if (c.getError() != null) + { + if (c.getError() != content.getError()) + c.getError().addSuppressed(content.getError()); + return true; + } + if (_content.compareAndSet(c, content)) + { + c.failed(content.getError()); + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with {} succeeded", content); + return true; + } + } + else if (c.getError() != null && content.remaining() == 0) + { + content.succeeded(); + return true; + } + else + { + // This should never happen, consider as a bug. + content.failed(new IllegalStateException("Cannot overwrite exiting content " + c + " with " + content)); + return false; + } + } + } + + public boolean onTimeout(Throwable failure) + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("onTimeout with current content: {} and failure = {}", c, failure); + if (!(c instanceof DemandingContent)) + return false; + if (_content.compareAndSet(c, new HttpInput.ErrorContent(failure))) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with error succeeded"); + return true; + } + } + } + + public void eof() + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("eof with current content: {}", c); + if (c instanceof DemandingContent) + { + if (_content.compareAndSet(c, EOF)) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with special EOF succeeded"); + return; + } + } + else if (c == null) + { + if (_content.compareAndSet(null, EOF)) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing null content with special EOF succeeded"); + return; + } + } + else if (c.isEof()) + { + if (LOG.isDebugEnabled()) + LOG.debug("current content already is EOF"); + return; + } + else if (c.remaining() == 0) + { + if (_content.compareAndSet(c, EOF)) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with special EOF succeeded"); + return; + } + } + else + { + // EOF may arrive with HEADERS frame (e.g. a trailer) that is not flow controlled, so we need to wrap the existing content. + // Covered by HttpTrailersTest.testRequestTrailersWithContent. + HttpInput.Content content = new HttpInput.WrappingContent(c, true); + if (_content.compareAndSet(c, content)) + { + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with {} succeeded", content); + return; + } + } + } + } + + public boolean failContent(Throwable failure) + { + while (true) + { + HttpInput.Content c = _content.get(); + if (LOG.isDebugEnabled()) + LOG.debug("failing current content: {} with failure: {}", c, failure); + if (c == null) + return false; + if (c.isSpecial()) + return c.isEof(); + if (_content.compareAndSet(c, null)) + { + c.failed(failure); + if (LOG.isDebugEnabled()) + LOG.debug("replacing current content with null succeeded"); + return false; + } + } + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "@" + hashCode() + " _content=" + _content; + } + } + + @Override + public boolean needContent() + { + boolean hasContent = _contentDemander.demand(true); + if (LOG.isDebugEnabled()) + LOG.debug("needContent has content? {}", hasContent); + return hasContent; + } + + @Override + public HttpInput.Content produceContent() + { + HttpInput.Content content = null; + if (_contentDemander.demand(false)) + content = _contentDemander.poll(); + if (LOG.isDebugEnabled()) + LOG.debug("produceContent produced {}", content); + return content; + } + + @Override + public boolean failAllContent(Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("failing all content with {}", (Object)failure); + boolean atEof = getStream().failAllData(failure); + atEof |= _contentDemander.failContent(failure); + if (LOG.isDebugEnabled()) + LOG.debug("failed all content, reached EOF? {}", atEof); + return atEof; + } + + @Override + public boolean failed(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("failed " + x); + + _contentDemander.onContent(new HttpInput.ErrorContent(x)); + + return getRequest().getHttpInput().onContentProducible(); + } + + @Override + protected boolean eof() + { + _contentDemander.eof(); + return false; + } + @Override public Runnable onTrailer(HeadersFrame frame) { @@ -301,7 +632,10 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ System.lineSeparator(), trailers); } + // This will generate EOF -> need to call onContentProducible. boolean handle = onRequestComplete(); + boolean woken = getRequest().getHttpInput().onContentProducible(); + handle |= woken; boolean wasDelayed = _delayedUntilContent; _delayedUntilContent = false; @@ -320,25 +654,30 @@ public class HttpChannelOverHTTP2 extends HttpChannel implements Closeable, Writ final boolean delayed = _delayedUntilContent; _delayedUntilContent = false; - boolean result = isIdle(); - if (result) + boolean reset = isIdle(); + if (reset) consumeInput(); getHttpTransport().onStreamTimeout(failure); - if (getRequest().getHttpInput().onIdleTimeout(failure) || delayed) + + failure.addSuppressed(new Throwable("HttpInput idle timeout")); + _contentDemander.onTimeout(failure); + boolean needed = getRequest().getHttpInput().onContentProducible(); + + if (needed || delayed) { consumer.accept(this::handleWithContext); - result = false; + reset = false; } - return result; + return reset; } @Override public Runnable onFailure(Throwable failure, Callback callback) { getHttpTransport().onStreamFailure(failure); - boolean handle = getRequest().getHttpInput().failed(failure); + boolean handle = failed(failure); consumeInput(); return new FailureTask(failure, callback, handle); } diff --git a/jetty-infinispan/infinispan-common/pom.xml b/jetty-infinispan/infinispan-common/pom.xml index 7569cfb7311..bff280121bd 100644 --- a/jetty-infinispan/infinispan-common/pom.xml +++ b/jetty-infinispan/infinispan-common/pom.xml @@ -43,7 +43,7 @@ org.infinispan.protostream protostream - 4.2.2.Final + ${infinispan.protostream.version} true provided diff --git a/jetty-infinispan/infinispan-embedded-query/pom.xml b/jetty-infinispan/infinispan-embedded-query/pom.xml index b90adbdd37b..f5d44da1a64 100644 --- a/jetty-infinispan/infinispan-embedded-query/pom.xml +++ b/jetty-infinispan/infinispan-embedded-query/pom.xml @@ -45,10 +45,10 @@ run - + - + @@ -58,12 +58,12 @@ run - + - + diff --git a/jetty-infinispan/infinispan-embedded/pom.xml b/jetty-infinispan/infinispan-embedded/pom.xml index 2916d720310..ce628795147 100644 --- a/jetty-infinispan/infinispan-embedded/pom.xml +++ b/jetty-infinispan/infinispan-embedded/pom.xml @@ -46,10 +46,10 @@ run - + - + @@ -59,12 +59,12 @@ run - + - + diff --git a/jetty-infinispan/infinispan-remote-query/pom.xml b/jetty-infinispan/infinispan-remote-query/pom.xml index 2d531981de4..c3b9dfa3eac 100644 --- a/jetty-infinispan/infinispan-remote-query/pom.xml +++ b/jetty-infinispan/infinispan-remote-query/pom.xml @@ -45,11 +45,11 @@ run - + - + @@ -59,12 +59,12 @@ run - + - + diff --git a/jetty-infinispan/infinispan-remote/pom.xml b/jetty-infinispan/infinispan-remote/pom.xml index 7b4e54cbbcd..e6f3d1ea91b 100644 --- a/jetty-infinispan/infinispan-remote/pom.xml +++ b/jetty-infinispan/infinispan-remote/pom.xml @@ -46,11 +46,11 @@ run - + - + @@ -60,12 +60,12 @@ run - + - + @@ -111,7 +111,7 @@ org.infinispan.protostream protostream - 4.2.2.Final + ${infinispan.protostream.version} provided diff --git a/jetty-osgi/jetty-osgi-boot/pom.xml b/jetty-osgi/jetty-osgi-boot/pom.xml index d786b0ba9fe..745a5c64973 100644 --- a/jetty-osgi/jetty-osgi-boot/pom.xml +++ b/jetty-osgi/jetty-osgi-boot/pom.xml @@ -46,14 +46,14 @@ process-resources - + - + run diff --git a/jetty-osgi/jetty-osgi-httpservice/pom.xml b/jetty-osgi/jetty-osgi-httpservice/pom.xml index de791c6cee6..658f91e52c4 100644 --- a/jetty-osgi/jetty-osgi-httpservice/pom.xml +++ b/jetty-osgi/jetty-osgi-httpservice/pom.xml @@ -43,11 +43,11 @@ process-resources - + - + run diff --git a/jetty-osgi/pom.xml b/jetty-osgi/pom.xml index 25b05e8b809..e84d255911a 100644 --- a/jetty-osgi/pom.xml +++ b/jetty-osgi/pom.xml @@ -12,7 +12,7 @@ pom - 3.6.0.v20100517 + 3.7.1 3.2.100.v20100503 1.0.0-v20070606 diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java index 5740c2da290..22d7c0e8950 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/authentication/SpnegoAuthenticatorTest.java @@ -30,6 +30,7 @@ import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannelState; import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpInput; import org.eclipse.jetty.server.HttpOutput; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; @@ -62,6 +63,36 @@ public class SpnegoAuthenticatorTest return null; } + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } + + @Override + public boolean needContent() + { + return false; + } + + @Override + public HttpInput.Content produceContent() + { + return null; + } + + @Override + public boolean failAllContent(Throwable failure) + { + return false; + } + @Override protected HttpOutput newHttpOutput() { @@ -97,6 +128,36 @@ public class SpnegoAuthenticatorTest return null; } + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } + + @Override + public boolean needContent() + { + return false; + } + + @Override + public HttpInput.Content produceContent() + { + return null; + } + + @Override + public boolean failAllContent(Throwable failure) + { + return false; + } + @Override protected HttpOutput newHttpOutput() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java new file mode 100644 index 00000000000..253097a40be --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContentProducer.java @@ -0,0 +1,354 @@ +// +// ======================================================================== +// 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.util.concurrent.TimeUnit; + +import org.eclipse.jetty.http.BadMessageException; +import org.eclipse.jetty.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Non-blocking {@link ContentProducer} implementation. Calling {@link #nextContent()} will never block + * but will return null when there is no available content. + */ +class AsyncContentProducer implements ContentProducer +{ + private static final Logger LOG = LoggerFactory.getLogger(AsyncContentProducer.class); + + private final HttpChannel _httpChannel; + private HttpInput.Interceptor _interceptor; + private HttpInput.Content _rawContent; + private HttpInput.Content _transformedContent; + private boolean _error; + private long _firstByteTimeStamp = Long.MIN_VALUE; + private long _rawContentArrived; + + AsyncContentProducer(HttpChannel httpChannel) + { + _httpChannel = httpChannel; + } + + @Override + public void recycle() + { + if (LOG.isDebugEnabled()) + LOG.debug("recycling {}", this); + _interceptor = null; + _rawContent = null; + _transformedContent = null; + _error = false; + _firstByteTimeStamp = Long.MIN_VALUE; + _rawContentArrived = 0L; + } + + @Override + public HttpInput.Interceptor getInterceptor() + { + return _interceptor; + } + + @Override + public void setInterceptor(HttpInput.Interceptor interceptor) + { + this._interceptor = interceptor; + } + + @Override + public int available() + { + HttpInput.Content content = nextTransformedContent(); + int available = content == null ? 0 : content.remaining(); + if (LOG.isDebugEnabled()) + LOG.debug("available = {}", available); + return available; + } + + @Override + public boolean hasContent() + { + boolean hasContent = _rawContent != null; + if (LOG.isDebugEnabled()) + LOG.debug("hasContent = {}", hasContent); + return hasContent; + } + + @Override + public boolean isError() + { + if (LOG.isDebugEnabled()) + LOG.debug("isError = {}", _error); + return _error; + } + + @Override + public void checkMinDataRate() + { + long minRequestDataRate = _httpChannel.getHttpConfiguration().getMinRequestDataRate(); + if (LOG.isDebugEnabled()) + LOG.debug("checkMinDataRate [m={},t={}]", minRequestDataRate, _firstByteTimeStamp); + if (minRequestDataRate > 0 && _firstByteTimeStamp != Long.MIN_VALUE) + { + long period = System.nanoTime() - _firstByteTimeStamp; + if (period > 0) + { + long minimumData = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1); + if (getRawContentArrived() < minimumData) + { + if (LOG.isDebugEnabled()) + LOG.debug("checkMinDataRate check failed"); + BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, + String.format("Request content data rate < %d B/s", minRequestDataRate)); + if (_httpChannel.getState().isResponseCommitted()) + { + if (LOG.isDebugEnabled()) + LOG.debug("checkMinDataRate aborting channel"); + _httpChannel.abort(bad); + } + failCurrentContent(bad); + throw bad; + } + } + } + } + + @Override + public long getRawContentArrived() + { + if (LOG.isDebugEnabled()) + LOG.debug("getRawContentArrived = {}", _rawContentArrived); + return _rawContentArrived; + } + + @Override + public boolean consumeAll(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("consumeAll [e={}]", (Object)x); + failCurrentContent(x); + // A specific HttpChannel mechanism must be used as the following code + // does not guarantee that the channel will synchronously deliver all + // content it already contains: + // while (true) + // { + // HttpInput.Content content = _httpChannel.produceContent(); + // ... + // } + // as the HttpChannel's produceContent() contract makes no such promise; + // for instance the H2 implementation calls Stream.demand() that may + // deliver the content asynchronously. Tests in StreamResetTest cover this. + boolean atEof = _httpChannel.failAllContent(x); + if (LOG.isDebugEnabled()) + LOG.debug("failed all content of http channel; at EOF? {}", atEof); + return atEof; + } + + private void failCurrentContent(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("failing currently held content [r={},t={}]", _rawContent, _transformedContent, x); + if (_transformedContent != null && !_transformedContent.isSpecial()) + { + if (_transformedContent != _rawContent) + { + _transformedContent.skip(_transformedContent.remaining()); + _transformedContent.failed(x); + } + _transformedContent = null; + } + + if (_rawContent != null && !_rawContent.isSpecial()) + { + _rawContent.skip(_rawContent.remaining()); + _rawContent.failed(x); + _rawContent = null; + } + } + + @Override + public boolean onContentProducible() + { + if (LOG.isDebugEnabled()) + LOG.debug("onContentProducible"); + return _httpChannel.getState().onReadReady(); + } + + @Override + public HttpInput.Content nextContent() + { + HttpInput.Content content = nextTransformedContent(); + if (LOG.isDebugEnabled()) + LOG.debug("nextContent = {}", content); + if (content != null) + _httpChannel.getState().onReadIdle(); + return content; + } + + @Override + public void reclaim(HttpInput.Content content) + { + if (LOG.isDebugEnabled()) + LOG.debug("reclaim {} [t={}]", content, _transformedContent); + if (_transformedContent == content) + { + content.succeeded(); + if (_transformedContent == _rawContent) + _rawContent = null; + _transformedContent = null; + } + } + + @Override + public boolean isReady() + { + HttpInput.Content content = nextTransformedContent(); + if (content == null) + { + _httpChannel.getState().onReadUnready(); + if (_httpChannel.needContent()) + { + content = nextTransformedContent(); + if (LOG.isDebugEnabled()) + LOG.debug("isReady got transformed content after needContent retry {}", content); + if (content != null) + _httpChannel.getState().onContentAdded(); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("isReady has no transformed content after needContent"); + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("isReady got transformed content {}", content); + _httpChannel.getState().onContentAdded(); + } + boolean ready = content != null; + if (LOG.isDebugEnabled()) + LOG.debug("isReady = {}", ready); + return ready; + } + + private HttpInput.Content nextTransformedContent() + { + if (LOG.isDebugEnabled()) + LOG.debug("nextTransformedContent [r={},t={}]", _rawContent, _transformedContent); + if (_rawContent == null) + { + _rawContent = produceRawContent(); + if (_rawContent == null) + return null; + } + + if (_transformedContent != null && _transformedContent.isEmpty()) + { + if (_transformedContent != _rawContent) + _transformedContent.succeeded(); + if (LOG.isDebugEnabled()) + LOG.debug("nulling depleted transformed content"); + _transformedContent = null; + } + + while (_transformedContent == null) + { + if (_rawContent.isSpecial()) + { + // TODO does EOF need to be passed to the interceptors? + + _error = _rawContent.getError() != null; + if (LOG.isDebugEnabled()) + LOG.debug("raw content is special (with error = {}), returning it", _error); + return _rawContent; + } + + if (_interceptor != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("using interceptor {} to transform raw content", _interceptor); + _transformedContent = _interceptor.readFrom(_rawContent); + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("null interceptor, transformed content = raw content"); + _transformedContent = _rawContent; + } + + if (_transformedContent != null && _transformedContent.isEmpty()) + { + if (_transformedContent != _rawContent) + _transformedContent.succeeded(); + if (LOG.isDebugEnabled()) + LOG.debug("nulling depleted transformed content"); + _transformedContent = null; + } + + if (_transformedContent == null) + { + if (_rawContent.isEmpty()) + { + _rawContent.succeeded(); + _rawContent = null; + if (LOG.isDebugEnabled()) + LOG.debug("nulling depleted raw content"); + _rawContent = produceRawContent(); + if (_rawContent == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("produced null raw content, returning null"); + return null; + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("raw content is not empty"); + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("transformed content is not empty"); + } + } + + if (LOG.isDebugEnabled()) + LOG.debug("returning transformed content {}", _transformedContent); + return _transformedContent; + } + + private HttpInput.Content produceRawContent() + { + HttpInput.Content content = _httpChannel.produceContent(); + if (content != null) + { + _rawContentArrived += content.remaining(); + if (_firstByteTimeStamp == Long.MIN_VALUE) + _firstByteTimeStamp = System.nanoTime(); + if (LOG.isDebugEnabled()) + LOG.debug("produceRawContent updated rawContentArrived to {} and firstByteTimeStamp to {}", _rawContentArrived, _firstByteTimeStamp); + } + if (LOG.isDebugEnabled()) + LOG.debug("produceRawContent produced {}", content); + return content; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingContentProducer.java new file mode 100644 index 00000000000..3dff5c88594 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/BlockingContentProducer.java @@ -0,0 +1,164 @@ +// +// ======================================================================== +// 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.util.concurrent.Semaphore; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Blocking implementation of {@link ContentProducer}. Calling {@link #nextContent()} will block when + * there is no available content but will never return null. + */ +class BlockingContentProducer implements ContentProducer +{ + private static final Logger LOG = LoggerFactory.getLogger(BlockingContentProducer.class); + + private final Semaphore _semaphore = new Semaphore(0); + private final AsyncContentProducer _asyncContentProducer; + + BlockingContentProducer(AsyncContentProducer delegate) + { + _asyncContentProducer = delegate; + } + + @Override + public void recycle() + { + if (LOG.isDebugEnabled()) + LOG.debug("recycling {}", this); + _asyncContentProducer.recycle(); + _semaphore.drainPermits(); + } + + @Override + public int available() + { + return _asyncContentProducer.available(); + } + + @Override + public boolean hasContent() + { + return _asyncContentProducer.hasContent(); + } + + @Override + public boolean isError() + { + return _asyncContentProducer.isError(); + } + + @Override + public void checkMinDataRate() + { + _asyncContentProducer.checkMinDataRate(); + } + + @Override + public long getRawContentArrived() + { + return _asyncContentProducer.getRawContentArrived(); + } + + @Override + public boolean consumeAll(Throwable x) + { + return _asyncContentProducer.consumeAll(x); + } + + @Override + public HttpInput.Content nextContent() + { + while (true) + { + HttpInput.Content content = _asyncContentProducer.nextContent(); + if (LOG.isDebugEnabled()) + LOG.debug("nextContent async producer returned {}", content); + if (content != null) + return content; + + // IFF isReady() returns false then HttpChannel.needContent() has been called, + // thus we know that eventually a call to onContentProducible will come. + if (_asyncContentProducer.isReady()) + { + if (LOG.isDebugEnabled()) + LOG.debug("nextContent async producer is ready, retrying"); + continue; + } + if (LOG.isDebugEnabled()) + LOG.debug("nextContent async producer is not ready, waiting on semaphore {}", _semaphore); + + try + { + _semaphore.acquire(); + } + catch (InterruptedException e) + { + return new HttpInput.ErrorContent(e); + } + } + } + + @Override + public void reclaim(HttpInput.Content content) + { + _asyncContentProducer.reclaim(content); + } + + @Override + public boolean isReady() + { + boolean ready = available() > 0; + if (LOG.isDebugEnabled()) + LOG.debug("isReady = {}", ready); + return ready; + } + + @Override + public HttpInput.Interceptor getInterceptor() + { + return _asyncContentProducer.getInterceptor(); + } + + @Override + public void setInterceptor(HttpInput.Interceptor interceptor) + { + _asyncContentProducer.setInterceptor(interceptor); + } + + @Override + public boolean onContentProducible() + { + // In blocking mode, the dispatched thread normally does not have to be rescheduled as it is normally in state + // DISPATCHED blocked on the semaphore that just needs to be released for the dispatched thread to resume. This is why + // this method always returns false. + // But async errors can occur while the dispatched thread is NOT blocked reading (i.e.: in state WAITING), + // so the WAITING to WOKEN transition must be done by the error-notifying thread which then has to reschedule the + // dispatched thread after HttpChannelState.asyncError() is called. + // Calling _asyncContentProducer.wakeup() changes the channel state from WAITING to WOKEN which would prevent the + // subsequent call to HttpChannelState.asyncError() from rescheduling the thread. + // AsyncServletTest.testStartAsyncThenClientStreamIdleTimeout() tests this. + if (LOG.isDebugEnabled()) + LOG.debug("onContentProducible releasing semaphore {}", _semaphore); + _semaphore.release(); + return false; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ContentProducer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ContentProducer.java new file mode 100644 index 00000000000..1a2a477001e --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ContentProducer.java @@ -0,0 +1,141 @@ +// +// ======================================================================== +// 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; + +/** + * ContentProducer is the bridge between {@link HttpInput} and {@link HttpChannel}. + * It wraps a {@link HttpChannel} and uses the {@link HttpChannel#needContent()}, + * {@link HttpChannel#produceContent()} and {@link HttpChannel#failAllContent(Throwable)} + * methods, tracks the current state of the channel's input by updating the + * {@link HttpChannelState} and provides the necessary mechanism to unblock + * the reader thread when using a blocking implementation or to know if the reader thread + * has to be rescheduled when using an async implementation. + */ +public interface ContentProducer +{ + /** + * Reset all internal state and clear any held resources. + */ + void recycle(); + + /** + * Fail all content currently available in this {@link ContentProducer} instance + * as well as in the underlying {@link HttpChannel}. + * + * This call is always non-blocking. + * Doesn't change state. + * @return true if EOF was reached. + */ + boolean consumeAll(Throwable x); + + /** + * Check if the current data rate consumption is above the minimal rate. + * Abort the channel, fail the content currently available and throw a + * BadMessageException(REQUEST_TIMEOUT_408) if the check fails. + */ + void checkMinDataRate(); + + /** + * Get the byte count produced by the underlying {@link HttpChannel}. + * + * This call is always non-blocking. + * Doesn't change state. + * @return the byte count produced by the underlying {@link HttpChannel}. + */ + long getRawContentArrived(); + + /** + * Get the byte count that can immediately be read from this + * {@link ContentProducer} instance or the underlying {@link HttpChannel}. + * + * This call is always non-blocking. + * Doesn't change state. + * @return the available byte count. + */ + int available(); + + /** + * Check if this {@link ContentProducer} instance contains some + * content without querying the underlying {@link HttpChannel}. + * + * This call is always non-blocking. + * Doesn't change state. + * Doesn't query the HttpChannel. + * @return true if this {@link ContentProducer} instance contains content, false otherwise. + */ + boolean hasContent(); + + /** + * Check if the underlying {@link HttpChannel} reached an error content. + * This call is always non-blocking. + * Doesn't change state. + * Doesn't query the HttpChannel. + * @return true if the underlying {@link HttpChannel} reached an error content, false otherwise. + */ + boolean isError(); + + /** + * Get the next content that can be read from or that describes the special condition + * that was reached (error, eof). + * This call may or may not block until some content is available, depending on the implementation. + * The returned content is decoded by the interceptor set with {@link #setInterceptor(HttpInput.Interceptor)} + * or left as-is if no intercept is set. + * After this call, state can be either of UNREADY or IDLE. + * @return the next content that can be read from or null if the implementation does not block + * and has no available content. + */ + HttpInput.Content nextContent(); + + /** + * Free up the content by calling {@link HttpInput.Content#succeeded()} on it + * and updating this instance' internal state. + */ + void reclaim(HttpInput.Content content); + + /** + * Check if this {@link ContentProducer} instance has some content that can be read without blocking. + * If there is some, the next call to {@link #nextContent()} will not block. + * If there isn't any and the implementation does not block, this method will trigger a + * {@link javax.servlet.ReadListener} callback once some content is available. + * This call is always non-blocking. + * After this call, state can be either of UNREADY or READY. + * @return true if some content is immediately available, false otherwise. + */ + boolean isReady(); + + /** + * Get the {@link org.eclipse.jetty.server.HttpInput.Interceptor}. + * @return The {@link org.eclipse.jetty.server.HttpInput.Interceptor}, or null if none set. + */ + HttpInput.Interceptor getInterceptor(); + + /** + * Set the interceptor. + * @param interceptor The interceptor to use. + */ + void setInterceptor(HttpInput.Interceptor interceptor); + + /** + * Wake up the thread that is waiting for the next content. + * After this call, state can be READY. + * @return true if the thread has to be rescheduled, false otherwise. + */ + boolean onContentProducible(); +} + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java index cf8e24d5516..ca40821980a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannel.java @@ -64,7 +64,7 @@ import org.slf4j.LoggerFactory; * HttpParser.RequestHandler callbacks. The completion of the active phase is signalled by a call to * HttpTransport.completed(). */ -public class HttpChannel implements Runnable, HttpOutput.Interceptor +public abstract class HttpChannel implements Runnable, HttpOutput.Interceptor { public static Listener NOOP_LISTENER = new Listener() {}; private static final Logger LOG = LoggerFactory.getLogger(HttpChannel.class); @@ -119,11 +119,53 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return _state.isSendError(); } - protected HttpInput newHttpInput(HttpChannelState state) + private HttpInput newHttpInput(HttpChannelState state) { return new HttpInput(state); } + /** + * Notify the channel that content is needed. If some content is immediately available, true is returned and + * {@link #produceContent()} has to be called and will return a non-null object. + * If no content is immediately available, {@link HttpInput#onContentProducible()} is called once some content arrives + * and {@link #produceContent()} can be called without returning null. + * If a failure happens, then {@link HttpInput#onContentProducible()} will be called and an error content will return the + * error on the next call to {@link #produceContent()}. + * @return true if content is immediately available. + */ + public abstract boolean needContent(); + + /** + * Produce a {@link HttpInput.Content} object with data currently stored within the channel. The produced content + * can be special (meaning calling {@link HttpInput.Content#isSpecial()} returns true) if the channel reached a special + * state, like EOF or an error. + * Once a special content has been returned, all subsequent calls to this method will always return a special content + * of the same kind and {@link #needContent()} will always return true. + * The returned content is "raw", i.e.: not decoded. + * @return a {@link HttpInput.Content} object if one is immediately available without blocking, null otherwise. + */ + public abstract HttpInput.Content produceContent(); + + /** + * Fail all content that is currently stored within the channel. + * @param failure the failure to fail the content with. + * @return true if EOF was reached while failing all content, false otherwise. + */ + public abstract boolean failAllContent(Throwable failure); + + /** + * Fail the channel's input. + * @param failure the failure. + * @return true if the channel needs to be rescheduled. + */ + public abstract boolean failed(Throwable failure); + + /** + * Mark the channel's input as EOF. + * @return true if the channel needs to be rescheduled. + */ + protected abstract boolean eof(); + protected HttpOutput newHttpOutput() { return new HttpOutput(this); @@ -303,19 +345,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _transientListeners.clear(); } - public void onAsyncWaitForContent() - { - } - - public void onBlockWaitForContent() - { - } - - public void onBlockWaitForContentFailure(Throwable failure) - { - getRequest().getHttpInput().failed(failure); - } - @Override public void run() { @@ -445,18 +474,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw _state.getAsyncContextEvent().getThrowable(); } - case READ_REGISTER: - { - onAsyncWaitForContent(); - break; - } - - case READ_PRODUCE: - { - _request.getHttpInput().asyncReadProduce(); - break; - } - case READ_CALLBACK: { ContextHandler handler = _state.getContextHandler(); @@ -706,7 +723,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor if (LOG.isDebugEnabled()) LOG.debug("onContent {} {}", this, content); _combinedListener.onRequestContent(_request, content.getByteBuffer()); - return _request.getHttpInput().addContent(content); + return false; } public boolean onContentComplete() @@ -729,7 +746,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { if (LOG.isDebugEnabled()) LOG.debug("onRequestComplete {}", this); - boolean result = _request.getHttpInput().eof(); + boolean result = eof(); _combinedListener.onRequestEnd(_request); return result; } @@ -765,11 +782,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _transport.onCompleted(); } - public boolean onEarlyEOF() - { - return _request.getHttpInput().earlyEOF(); - } - public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java index 3dfd130306d..f580e325d2b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelOverHttp.java @@ -40,6 +40,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,6 +51,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque { private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverHttp.class); private static final HttpField PREAMBLE_UPGRADE_H2C = new HttpField(HttpHeader.UPGRADE, "h2c"); + private static final HttpInput.Content EOF = new HttpInput.EofContent(); private final HttpConnection _httpConnection; private final RequestBuilder _requestBuilder = new RequestBuilder(); private MetaData.Request _metadata; @@ -61,6 +63,14 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque private boolean _expect102Processing = false; private List _complianceViolations; private HttpFields.Mutable _trailers; + // Field _content doesn't need to be volatile nor protected by a lock + // as it is always accessed by the same thread, i.e.: we get notified by onFillable + // that the socket contains new bytes and either schedule an onDataAvailable + // call that is going to read the socket or release the blocking semaphore to wake up + // the blocked reader and make it read the socket. The same logic is true for async + // events like timeout: we get notified and either schedule onError or release the + // blocking semaphore. + private HttpInput.Content _content; public HttpChannelOverHttp(HttpConnection httpConnection, Connector connector, HttpConfiguration config, EndPoint endPoint, HttpTransport transport) { @@ -75,6 +85,79 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque _httpConnection.getGenerator().setPersistent(false); } + @Override + public boolean needContent() + { + if (_content != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("needContent has content immediately available: {}", _content); + return true; + } + _httpConnection.parseAndFillForContent(); + if (_content != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("needContent has content after parseAndFillForContent: {}", _content); + return true; + } + + if (LOG.isDebugEnabled()) + LOG.debug("needContent has no content"); + _httpConnection.asyncReadFillInterested(); + return false; + } + + @Override + public HttpInput.Content produceContent() + { + if (_content == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("produceContent has no content, parsing and filling"); + _httpConnection.parseAndFillForContent(); + } + HttpInput.Content result = _content; + if (result != null && !result.isSpecial()) + _content = result.isEof() ? EOF : null; + if (LOG.isDebugEnabled()) + LOG.debug("produceContent produced {}", result); + return result; + } + + @Override + public boolean failAllContent(Throwable failure) + { + if (LOG.isDebugEnabled()) + LOG.debug("failing all content with {}", (Object)failure); + if (_content != null && !_content.isSpecial()) + { + _content.failed(failure); + _content = _content.isEof() ? EOF : null; + if (_content == EOF) + return true; + } + while (true) + { + HttpInput.Content c = produceContent(); + if (c == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("failed all content, EOF was not reached"); + return false; + } + c.skip(c.remaining()); + c.failed(failure); + if (c.isSpecial()) + { + boolean atEof = c.isEof(); + if (LOG.isDebugEnabled()) + LOG.debug("failed all content, EOF = {}", atEof); + return atEof; + } + } + } + @Override public void badMessage(BadMessageException failure) { @@ -85,7 +168,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque if (_metadata == null) _metadata = _requestBuilder.build(); onRequest(_metadata); - getRequest().getHttpInput().earlyEOF(); + markEarlyEOF(); } catch (Exception e) { @@ -96,12 +179,23 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque } @Override - public boolean content(ByteBuffer content) + public boolean content(ByteBuffer buffer) { - HttpInput.Content c = _httpConnection.newContent(content); - boolean handle = onContent(c) || _delayedForContent; - _delayedForContent = false; - return handle; + HttpInput.Content content = _httpConnection.newContent(buffer); + if (_content != null) + { + if (_content.isSpecial()) + content.failed(_content.getError()); + else + throw new AssertionError("Cannot overwrite exiting content " + _content + " with " + content); + } + else + { + _content = content; + onContent(_content); + _delayedForContent = false; + } + return true; } @Override @@ -147,12 +241,69 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque _httpConnection.getGenerator().setPersistent(false); // If we have no request yet, just close if (_metadata == null) - _httpConnection.close(); - else if (onEarlyEOF() || _delayedForContent) { - _delayedForContent = false; - handle(); + _httpConnection.close(); } + else + { + markEarlyEOF(); + if (_delayedForContent) + { + _delayedForContent = false; + handle(); + } + } + } + + private void markEarlyEOF() + { + if (LOG.isDebugEnabled()) + LOG.debug("received early EOF, content = {}", _content); + EofException failure = new EofException("Early EOF"); + if (_content != null) + _content.failed(failure); + _content = new HttpInput.ErrorContent(failure); + } + + @Override + protected boolean eof() + { + if (LOG.isDebugEnabled()) + LOG.debug("received EOF, content = {}", _content); + if (_content == null) + { + _content = EOF; + } + else + { + HttpInput.Content c = _content; + _content = new HttpInput.WrappingContent(c, true); + } + return false; + } + + @Override + public boolean failed(Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("failed {}, content = {}", x, _content); + + Throwable error = null; + if (_content != null && _content.isSpecial()) + error = _content.getError(); + + if (error != null && error != x) + { + error.addSuppressed(x); + } + else + { + if (_content != null) + _content.failed(x); + _content = new HttpInput.ErrorContent(x); + } + + return getRequest().getHttpInput().onContentProducible(); } @Override @@ -309,24 +460,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque return onRequestComplete(); } - @Override - public void onAsyncWaitForContent() - { - _httpConnection.asyncReadFillInterested(); - } - - @Override - public void onBlockWaitForContent() - { - _httpConnection.blockingReadFillInterested(); - } - - @Override - public void onBlockWaitForContentFailure(Throwable failure) - { - _httpConnection.blockingReadFailure(failure); - } - @Override public void onComplianceViolation(ComplianceViolation.Mode mode, ComplianceViolation violation, String details) { @@ -434,6 +567,9 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque _upgrade = null; _trailers = null; _metadata = null; + if (_content != null && !_content.isSpecial()) + throw new AssertionError("unconsumed content: " + _content); + _content = null; } @Override @@ -459,12 +595,6 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque super.handleException(x); } - @Override - protected HttpInput newHttpInput(HttpChannelState state) - { - return new HttpInputOverHTTP(state); - } - /** *

Attempts to perform an HTTP/1.1 upgrade.

*

The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector @@ -534,13 +664,24 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque if (_delayedForContent) { _delayedForContent = false; - getRequest().getHttpInput().onIdleTimeout(timeout); + doOnIdleTimeout(timeout); execute(this); return false; } return true; } + private void doOnIdleTimeout(Throwable x) + { + boolean neverDispatched = getState().isIdle(); + boolean waitingForContent = _content == null || _content.remaining() == 0; + if ((waitingForContent || neverDispatched) && (_content == null || !_content.isSpecial())) + { + x.addSuppressed(new Throwable("HttpInput idle timeout")); + _content = new HttpInput.ErrorContent(x); + } + } + private static class RequestBuilder { private final HttpFields.Mutable _fieldsBuilder = HttpFields.build(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java index 3474a9dc271..c08291fbe07 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState.java @@ -107,12 +107,9 @@ public class HttpChannelState */ private enum InputState { - IDLE, // No isReady; No data - REGISTER, // isReady()==false handling; No data - REGISTERED, // isReady()==false !handling; No data - POSSIBLE, // isReady()==false async read callback called (http/1 only) - PRODUCING, // isReady()==false READ_PRODUCE action is being handled (http/1 only) - READY // isReady() was false, onContentAdded has been called + IDLE, // No isReady; No data + UNREADY, // isReady()==false; No data + READY // isReady() was false; data is available } /* @@ -137,8 +134,6 @@ public class HttpChannelState ASYNC_ERROR, // handle an async error ASYNC_TIMEOUT, // call asyncContext onTimeout WRITE_CALLBACK, // handle an IO write callback - READ_REGISTER, // Register for fill interest - READ_PRODUCE, // Check is a read is possible by parsing/filling READ_CALLBACK, // handle an IO read callback COMPLETE, // Complete the response by closing output TERMINATED, // No further actions @@ -465,19 +460,12 @@ public class HttpChannelState case ASYNC: switch (_inputState) { - case POSSIBLE: - _inputState = InputState.PRODUCING; - return Action.READ_PRODUCE; + case IDLE: + case UNREADY: + break; case READY: _inputState = InputState.IDLE; return Action.READ_CALLBACK; - case REGISTER: - case PRODUCING: - _inputState = InputState.REGISTERED; - return Action.READ_REGISTER; - case IDLE: - case REGISTERED: - break; default: throw new IllegalStateException(getStatusStringLocked()); @@ -1222,99 +1210,8 @@ public class HttpChannelState _channel.getRequest().setAttribute(name, attribute); } - /** - * Called to signal async read isReady() has returned false. - * This indicates that there is no content available to be consumed - * and that once the channel enters the ASYNC_WAIT state it will - * register for read interest by calling {@link HttpChannel#onAsyncWaitForContent()} - * either from this method or from a subsequent call to {@link #unhandle()}. - */ - public void onReadUnready() - { - boolean interested = false; - try (AutoLock l = lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("onReadUnready {}", toStringLocked()); - - switch (_inputState) - { - case IDLE: - case READY: - if (_state == State.WAITING) - { - interested = true; - _inputState = InputState.REGISTERED; - } - else - { - _inputState = InputState.REGISTER; - } - break; - - case REGISTER: - case REGISTERED: - case POSSIBLE: - case PRODUCING: - break; - - default: - throw new IllegalStateException(toStringLocked()); - } - } - - if (interested) - _channel.onAsyncWaitForContent(); - } - - /** - * Called to signal that content is now available to read. - * If the channel is in ASYNC_WAIT state and unready (ie isReady() has - * returned false), then the state is changed to ASYNC_WOKEN and true - * is returned. - * - * @return True IFF the channel was unready and in ASYNC_WAIT state - */ - public boolean onContentAdded() - { - boolean woken = false; - try (AutoLock l = lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("onContentAdded {}", toStringLocked()); - - switch (_inputState) - { - case IDLE: - case READY: - break; - - case PRODUCING: - _inputState = InputState.READY; - break; - - case REGISTER: - case REGISTERED: - _inputState = InputState.READY; - if (_state == State.WAITING) - { - woken = true; - _state = State.WOKEN; - } - break; - - default: - throw new IllegalStateException(toStringLocked()); - } - } - return woken; - } - /** * Called to signal that the channel is ready for a callback. - * This is similar to calling {@link #onReadUnready()} followed by - * {@link #onContentAdded()}, except that as content is already - * available, read interest is never set. * * @return true if woken */ @@ -1328,7 +1225,11 @@ public class HttpChannelState switch (_inputState) { + case READY: + _inputState = InputState.READY; + break; case IDLE: + case UNREADY: _inputState = InputState.READY; if (_state == State.WAITING) { @@ -1344,25 +1245,20 @@ public class HttpChannelState return woken; } - /** - * Called to indicate that more content may be available, - * but that a handling thread may need to produce (fill/parse) - * it. Typically called by the async read success callback. - * - * @return {@code true} if more content may be available - */ - public boolean onReadPossible() + public boolean onReadEof() { boolean woken = false; try (AutoLock l = lock()) { if (LOG.isDebugEnabled()) - LOG.debug("onReadPossible {}", toStringLocked()); + LOG.debug("onReadEof {}", toStringLocked()); switch (_inputState) { - case REGISTERED: - _inputState = InputState.POSSIBLE; + case IDLE: + case READY: + case UNREADY: + _inputState = InputState.READY; if (_state == State.WAITING) { woken = true; @@ -1377,29 +1273,72 @@ public class HttpChannelState return woken; } - /** - * Called to signal that a read has read -1. - * Will wake if the read was called while in ASYNC_WAIT state - * - * @return {@code true} if woken - */ - public boolean onReadEof() + public void onContentAdded() { - boolean woken = false; try (AutoLock l = lock()) { if (LOG.isDebugEnabled()) - LOG.debug("onEof {}", toStringLocked()); + LOG.debug("onContentAdded {}", toStringLocked()); - // Force read ready so onAllDataRead can be called - _inputState = InputState.READY; - if (_state == State.WAITING) + switch (_inputState) { - woken = true; - _state = State.WOKEN; + case IDLE: + case UNREADY: + case READY: + _inputState = InputState.READY; + break; + + default: + throw new IllegalStateException(toStringLocked()); + } + } + } + + public void onReadIdle() + { + try (AutoLock l = lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("onReadIdle {}", toStringLocked()); + + switch (_inputState) + { + case UNREADY: + case READY: + case IDLE: + _inputState = InputState.IDLE; + break; + + default: + throw new IllegalStateException(toStringLocked()); + } + } + } + + /** + * Called to indicate that more content may be available, + * but that a handling thread may need to produce (fill/parse) + * it. Typically called by the async read success callback. + */ + public void onReadUnready() + { + try (AutoLock l = lock()) + { + if (LOG.isDebugEnabled()) + LOG.debug("onReadUnready {}", toStringLocked()); + + switch (_inputState) + { + case IDLE: + case UNREADY: + case READY: // READY->UNREADY is needed by AsyncServletIOTest.testStolenAsyncRead + _inputState = InputState.UNREADY; + break; + + default: + throw new IllegalStateException(toStringLocked()); } } - return woken; } public boolean onWritePossible() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState_input.puml b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState_input.puml new file mode 100644 index 00000000000..13eb5dc325b --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelState_input.puml @@ -0,0 +1,84 @@ +@startuml +title HttpChannelState + +note top of onReadReady_inputState: onReadReady + +state "input state" as onReadReady_inputState { + state "IDLE" as onReadReady_IDLE + state "UNREADY" as onReadReady_UNREADY + state "READY" as onReadReady_READY + + state "channel state" as onReadReady_channelState { + state "WAITING" as onReadReady_WAITING + state "WOKEN" as onReadReady_WOKEN + onReadReady_WAITING --> onReadReady_WOKEN + } + + onReadReady_IDLE --> onReadReady_channelState + onReadReady_UNREADY --> onReadReady_channelState + + onReadReady_channelState --> onReadReady_READY + onReadReady_READY --> onReadReady_READY +} + + +note top of onReadEof_inputState: onReadEof + +state "input state" as onReadEof_inputState { + state "IDLE" as onReadEof_IDLE + state "UNREADY" as onReadEof_UNREADY + state "READY" as onReadEof_READY + + state "channel state" as onReadEof_channelState { + state "WAITING" as onReadEof_WAITING + state "WOKEN" as onReadEof_WOKEN + onReadEof_WAITING --> onReadEof_WOKEN + } + + onReadEof_IDLE --> onReadEof_channelState + onReadEof_UNREADY --> onReadEof_channelState + onReadEof_READY --> onReadEof_channelState + + onReadEof_channelState --> onReadEof_READY +} + + +note top of onReadIdle_inputState: onReadIdle + +state "input state" as onReadIdle_inputState { + state "IDLE" as onReadIdle_IDLE + state "UNREADY" as onReadIdle_UNREADY + state "READY" as onReadIdle_READY + + onReadIdle_IDLE --> onReadIdle_IDLE + onReadIdle_UNREADY --> onReadIdle_IDLE + onReadIdle_READY --> onReadIdle_IDLE +} + + +note top of onReadUnready_inputState: onReadUnready + +state "input state" as onReadUnready_inputState { + state "IDLE" as onReadUnready_IDLE + state "UNREADY" as onReadUnready_UNREADY + state "READY" as onReadUnready_READY + + onReadUnready_IDLE --> onReadUnready_UNREADY + onReadUnready_UNREADY --> onReadUnready_UNREADY + onReadUnready_READY --> onReadUnready_UNREADY +} + + +note top of onContentAdded_inputState: onContentAdded + +state "input state" as onContentAdded_inputState { + state "IDLE" as onContentAdded_IDLE + state "UNREADY" as onContentAdded_UNREADY + state "READY" as onContentAdded_READY + + onContentAdded_IDLE --> onContentAdded_READY + onContentAdded_UNREADY --> onContentAdded_READY + onContentAdded_READY --> onContentAdded_READY +} + +@enduml diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java index 9b8e1061607..add0c44b9d3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -33,7 +33,6 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpParser; -import org.eclipse.jetty.http.HttpParser.RequestHandler; import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.AbstractConnection; @@ -68,7 +67,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http private final HttpParser _parser; private final AtomicInteger _contentBufferReferences = new AtomicInteger(); private volatile ByteBuffer _requestBuffer = null; - private final BlockingReadCallback _blockingReadCallback = new BlockingReadCallback(); private final AsyncReadCallback _asyncReadCallback = new AsyncReadCallback(); private final SendCallback _sendCallback = new SendCallback(); private final boolean _recordHttpComplianceViolations; @@ -316,21 +314,20 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } /** - * Fill and parse data looking for content - * - * @return true if an {@link RequestHandler} method was called and it returned true; + * Parse and fill data, looking for content */ - protected boolean fillAndParseForContent() + void parseAndFillForContent() { - boolean handled = false; + // When fillRequestBuffer() is called, it must always be followed by a parseRequestBuffer() call otherwise this method + // doesn't trigger EOF/earlyEOF which breaks AsyncRequestReadTest.testPartialReadThenShutdown() + int filled = Integer.MAX_VALUE; while (_parser.inContentState()) { - int filled = fillRequestBuffer(); - handled = parseRequestBuffer(); - if (handled || filled <= 0 || _input.hasContent()) + boolean handled = parseRequestBuffer(); + if (handled || filled <= 0) break; + filled = fillRequestBuffer(); } - return handled; } private int fillRequestBuffer() @@ -600,25 +597,7 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http public void asyncReadFillInterested() { - getEndPoint().fillInterested(_asyncReadCallback); - } - - public void blockingReadFillInterested() - { - // We try fillInterested here because of SSL and - // spurious wakeups. With blocking reads, we read in a loop - // that tries to read/parse content and blocks waiting if there is - // none available. The loop can be woken up by incoming encrypted - // bytes, which due to SSL might not produce any decrypted bytes. - // Thus the loop needs to register fill interest again. However if - // the loop is woken up spuriously, then the register interest again - // can result in a pending read exception, unless we use tryFillInterested. - getEndPoint().tryFillInterested(_blockingReadCallback); - } - - public void blockingReadFailure(Throwable e) - { - _blockingReadCallback.failed(e); + getEndPoint().tryFillInterested(_asyncReadCallback); } @Override @@ -655,8 +634,15 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http @Override public void succeeded() { - if (_contentBufferReferences.decrementAndGet() == 0) + int counter = _contentBufferReferences.decrementAndGet(); + if (counter == 0) releaseRequestBuffer(); + // TODO: this should do something (warn? fail?) if _contentBufferReferences goes below 0 + if (counter < 0) + { + LOG.warn("Content reference counting went below zero: {}", counter); + _contentBufferReferences.incrementAndGet(); + } } @Override @@ -666,44 +652,30 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } } - private class BlockingReadCallback implements Callback - { - @Override - public void succeeded() - { - _input.unblock(); - } - - @Override - public void failed(Throwable x) - { - _input.failed(x); - } - - @Override - public InvocationType getInvocationType() - { - // This callback does not block, rather it wakes up the - // thread that is blocked waiting on the read. - return InvocationType.NON_BLOCKING; - } - } - private class AsyncReadCallback implements Callback { @Override public void succeeded() { - if (_channel.getState().onReadPossible()) + if (_channel.getRequest().getHttpInput().onContentProducible()) _channel.handle(); } @Override public void failed(Throwable x) { - if (_input.failed(x)) + if (_channel.failed(x)) _channel.handle(); } + + @Override + public InvocationType getInvocationType() + { + // This callback does not block when the HttpInput is in blocking mode, + // rather it wakes up the thread that is blocked waiting on the read; + // but it can if it is in async mode, hence the varying InvocationType. + return _channel.getRequest().getHttpInput().isAsync() ? InvocationType.BLOCKING : InvocationType.NON_BLOCKING; + } } private class SendCallback extends IteratingCallback diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java index 02c0be87ba4..d1aaa1d5c8f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -18,60 +18,349 @@ package org.eclipse.jetty.server; -import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayDeque; -import java.util.Deque; import java.util.Objects; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; -import org.eclipse.jetty.http.BadMessageException; -import org.eclipse.jetty.http.HttpStatus; -import org.eclipse.jetty.io.EofException; -import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.component.Destroyable; -import org.eclipse.jetty.util.thread.AutoLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * {@link HttpInput} provides an implementation of {@link ServletInputStream} for {@link HttpChannel}. - *

- * Content may arrive in patterns such as [content(), content(), messageComplete()] - * so that this class maintains two states: the content state that tells - * whether there is content to consume and the EOF state that tells whether an EOF has arrived. - * Only once the content has been consumed the content state is moved to the EOF state. - *

+ *

While this class is-a Runnable, it should never be dispatched in it's own thread. It is a runnable only so that the calling thread can use {@link + * ContextHandler#handle(Runnable)} to setup classloaders etc.

*/ public class HttpInput extends ServletInputStream implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(HttpInput.class); + + private final byte[] _oneByteBuffer = new byte[1]; + private final BlockingContentProducer _blockingContentProducer; + private final AsyncContentProducer _asyncContentProducer; + + private final HttpChannelState _channelState; + private ContentProducer _contentProducer; + private boolean _consumedEof; + private ReadListener _readListener; + + public HttpInput(HttpChannelState state) + { + _channelState = state; + _asyncContentProducer = new AsyncContentProducer(state.getHttpChannel()); + _blockingContentProducer = new BlockingContentProducer(_asyncContentProducer); + _contentProducer = _blockingContentProducer; + } + + /* HttpInput */ + + public void recycle() + { + if (LOG.isDebugEnabled()) + LOG.debug("recycle {}", this); + _blockingContentProducer.recycle(); + _contentProducer = _blockingContentProducer; + _consumedEof = false; + _readListener = null; + } + /** - * An interceptor for HTTP Request input. - *

- * Unlike InputStream wrappers that can be applied by filters, an interceptor - * is entirely transparent and works with async IO APIs. - *

- *

- * An Interceptor may consume data from the passed content and the interceptor - * will continue to be called for the same content until the interceptor returns - * null or an empty content. Thus even if the passed content is completely consumed - * the interceptor will be called with the same content until it can no longer - * produce more content. - *

- * - * @see HttpInput#setInterceptor(Interceptor) - * @see HttpInput#addInterceptor(Interceptor) + * @return The current Interceptor, or null if none set */ + public Interceptor getInterceptor() + { + return _contentProducer.getInterceptor(); + } + + /** + * Set the interceptor. + * + * @param interceptor The interceptor to use. + */ + public void setInterceptor(Interceptor interceptor) + { + if (LOG.isDebugEnabled()) + LOG.debug("setting interceptor to {}", interceptor); + _contentProducer.setInterceptor(interceptor); + } + + /** + * Set the {@link Interceptor}, chaining it to the existing one if + * an {@link Interceptor} is already set. + * + * @param interceptor the next {@link Interceptor} in a chain + */ + public void addInterceptor(Interceptor interceptor) + { + Interceptor currentInterceptor = _contentProducer.getInterceptor(); + if (currentInterceptor == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("adding single interceptor: {}", interceptor); + _contentProducer.setInterceptor(interceptor); + } + else + { + ChainedInterceptor chainedInterceptor = new ChainedInterceptor(currentInterceptor, interceptor); + if (LOG.isDebugEnabled()) + LOG.debug("adding chained interceptor: {}", chainedInterceptor); + _contentProducer.setInterceptor(chainedInterceptor); + } + } + + public long getContentReceived() + { + return _contentProducer.getRawContentArrived(); + } + + public boolean consumeAll() + { + if (LOG.isDebugEnabled()) + LOG.debug("consume all"); + boolean atEof = _contentProducer.consumeAll(new IOException("Unconsumed content")); + if (atEof) + _consumedEof = true; + + if (isFinished()) + return !isError(); + + return false; + } + + public boolean isError() + { + boolean error = _contentProducer.isError(); + if (LOG.isDebugEnabled()) + LOG.debug("isError = {}", error); + return error; + } + + public boolean isAsync() + { + if (LOG.isDebugEnabled()) + LOG.debug("isAsync read listener = " + _readListener); + return _readListener != null; + } + + /* ServletInputStream */ + + @Override + public boolean isFinished() + { + boolean finished = _consumedEof; + if (LOG.isDebugEnabled()) + LOG.debug("isFinished? {}", finished); + return finished; + } + + @Override + public boolean isReady() + { + boolean ready = _contentProducer.isReady(); + if (!ready) + { + if (LOG.isDebugEnabled()) + LOG.debug("isReady? false"); + return false; + } + + if (LOG.isDebugEnabled()) + LOG.debug("isReady? true"); + return true; + } + + @Override + public void setReadListener(ReadListener readListener) + { + if (LOG.isDebugEnabled()) + LOG.debug("setting read listener to {}", readListener); + if (_readListener != null) + throw new IllegalStateException("ReadListener already set"); + _readListener = Objects.requireNonNull(readListener); + //illegal if async not started + if (!_channelState.isAsyncStarted()) + throw new IllegalStateException("Async not started"); + + _contentProducer = _asyncContentProducer; + // trigger content production + if (isReady() && _channelState.onReadEof()) // onReadEof b/c we want to transition from WAITING to WOKEN + scheduleReadListenerNotification(); // this is needed by AsyncServletIOTest.testStolenAsyncRead + } + + public boolean onContentProducible() + { + return _contentProducer.onContentProducible(); + } + + @Override + public int read() throws IOException + { + int read = read(_oneByteBuffer, 0, 1); + if (read == 0) + throw new IOException("unready read=0"); + return read < 0 ? -1 : _oneByteBuffer[0] & 0xFF; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + // Calculate minimum request rate for DoS protection + _contentProducer.checkMinDataRate(); + + Content content = _contentProducer.nextContent(); + if (content == null) + throw new IllegalStateException("read on unready input"); + if (!content.isSpecial()) + { + int read = content.get(b, off, len); + if (LOG.isDebugEnabled()) + LOG.debug("read produced {} byte(s)", read); + if (content.isEmpty()) + _contentProducer.reclaim(content); + return read; + } + + Throwable error = content.getError(); + if (LOG.isDebugEnabled()) + LOG.debug("read error = " + error); + if (error != null) + { + if (error instanceof IOException) + throw (IOException)error; + throw new IOException(error); + } + + if (content.isEof()) + { + if (LOG.isDebugEnabled()) + LOG.debug("read at EOF, setting consumed EOF to true"); + _consumedEof = true; + // If EOF do we need to wake for allDataRead callback? + if (onContentProducible()) + scheduleReadListenerNotification(); + return -1; + } + + throw new AssertionError("no data, no error and not EOF"); + } + + private void scheduleReadListenerNotification() + { + HttpChannel channel = _channelState.getHttpChannel(); + channel.execute(channel); + } + + /** + * Check if this HttpInput instance has content stored internally, without fetching/parsing + * anything from the underlying channel. + * @return true if the input contains content, false otherwise. + */ + public boolean hasContent() + { + // Do not call _contentProducer.available() as it calls HttpChannel.produceContent() + // which is forbidden by this method's contract. + boolean hasContent = _contentProducer.hasContent(); + if (LOG.isDebugEnabled()) + LOG.debug("hasContent = {}", hasContent); + return hasContent; + } + + @Override + public int available() + { + int available = _contentProducer.available(); + if (LOG.isDebugEnabled()) + LOG.debug("available = {}", available); + return available; + } + + /* Runnable */ + + /* + *

While this class is-a Runnable, it should never be dispatched in it's own thread. It is a runnable only so that the calling thread can use {@link + * ContextHandler#handle(Runnable)} to setup classloaders etc.

+ */ + @Override + public void run() + { + Content content = _contentProducer.nextContent(); + if (LOG.isDebugEnabled()) + LOG.debug("running on content {}", content); + // The nextContent() call could return null if the transformer ate all + // the raw bytes without producing any transformed content. + if (content == null) + return; + + // This check is needed when a request is started async but no read listener is registered. + if (_readListener == null) + { + if (LOG.isDebugEnabled()) + LOG.debug("running without a read listener"); + onContentProducible(); + return; + } + + if (content.isSpecial()) + { + Throwable error = content.getError(); + if (error != null) + { + if (LOG.isDebugEnabled()) + LOG.debug("running has error: {}", (Object)error); + // TODO is this necessary to add here? + _channelState.getHttpChannel().getResponse().getHttpFields().add(HttpConnection.CONNECTION_CLOSE); + _readListener.onError(error); + } + else if (content.isEof()) + { + try + { + if (LOG.isDebugEnabled()) + LOG.debug("running at EOF"); + _readListener.onAllDataRead(); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("running failed onAllDataRead", x); + _readListener.onError(x); + } + } + } + else + { + if (LOG.isDebugEnabled()) + LOG.debug("running has content"); + try + { + _readListener.onDataAvailable(); + } + catch (Throwable x) + { + if (LOG.isDebugEnabled()) + LOG.debug("running failed onDataAvailable", x); + _readListener.onError(x); + } + } + } + + @Override + public String toString() + { + return getClass().getSimpleName() + "@" + hashCode() + + " cs=" + _channelState + + " cp=" + _contentProducer + + " eof=" + _consumedEof; + } + public interface Interceptor { /** - * @param content The content to be intercepted (may be empty or a {@link SentinelContent}. + * @param content The content to be intercepted. * The content will be modified with any data the interceptor consumes, but there is no requirement * that all the data is consumed by the interceptor. * @return The intercepted content or null if interception is completed for that content. @@ -85,23 +374,23 @@ public class HttpInput extends ServletInputStream implements Runnable * {@link Interceptor#readFrom(Content)} and then passes any {@link Content} returned * to the next {@link Interceptor}. */ - public static class ChainedInterceptor implements Interceptor, Destroyable + private static class ChainedInterceptor implements Interceptor, Destroyable { private final Interceptor _prev; private final Interceptor _next; - public ChainedInterceptor(Interceptor prev, Interceptor next) + ChainedInterceptor(Interceptor prev, Interceptor next) { _prev = prev; _next = next; } - public Interceptor getPrev() + Interceptor getPrev() { return _prev; } - public Interceptor getNext() + Interceptor getNext() { return _next; } @@ -109,7 +398,10 @@ public class HttpInput extends ServletInputStream implements Runnable @Override public Content readFrom(Content content) { - return getNext().readFrom(getPrev().readFrom(content)); + Content c = getPrev().readFrom(content); + if (c == null) + return null; + return getNext().readFrom(c); } @Override @@ -120,866 +412,22 @@ public class HttpInput extends ServletInputStream implements Runnable if (_next instanceof Destroyable) ((Destroyable)_next).destroy(); } - } - - private static final Logger LOG = LoggerFactory.getLogger(HttpInput.class); - static final Content EOF_CONTENT = new EofContent("EOF"); - static final Content EARLY_EOF_CONTENT = new EofContent("EARLY_EOF"); - - private final AutoLock.WithCondition _lock = new AutoLock.WithCondition(); - private final byte[] _oneByteBuffer = new byte[1]; - private Content _content; - private Content _intercepted; - private final Deque _inputQ = new ArrayDeque<>(); - private final HttpChannelState _channelState; - private ReadListener _listener; - private State _state = STREAM; - private long _firstByteTimeStamp = -1; - private long _contentArrived; - private long _contentConsumed; - private long _blockUntil; - private boolean _waitingForContent; - private Interceptor _interceptor; - - public HttpInput(HttpChannelState state) - { - _channelState = state; - } - - protected HttpChannelState getHttpChannelState() - { - return _channelState; - } - - public void recycle() - { - try (AutoLock l = _lock.lock()) - { - if (_content != null) - _content.failed(null); - _content = null; - Content item = _inputQ.poll(); - while (item != null) - { - item.failed(null); - item = _inputQ.poll(); - } - _listener = null; - _state = STREAM; - _contentArrived = 0; - _contentConsumed = 0; - _firstByteTimeStamp = -1; - _blockUntil = 0; - _waitingForContent = false; - if (_interceptor instanceof Destroyable) - ((Destroyable)_interceptor).destroy(); - _interceptor = null; - } - } - - /** - * @return The current Interceptor, or null if none set - */ - public Interceptor getInterceptor() - { - return _interceptor; - } - - /** - * Set the interceptor. - * - * @param interceptor The interceptor to use. - */ - public void setInterceptor(Interceptor interceptor) - { - _interceptor = interceptor; - } - - /** - * Set the {@link Interceptor}, using a {@link ChainedInterceptor} if - * an {@link Interceptor} is already set. - * - * @param interceptor the next {@link Interceptor} in a chain - */ - public void addInterceptor(Interceptor interceptor) - { - if (_interceptor == null) - _interceptor = interceptor; - else - _interceptor = new ChainedInterceptor(_interceptor, interceptor); - } - - @Override - public int available() - { - int available = 0; - boolean woken = false; - try (AutoLock l = _lock.lock()) - { - if (_content == null) - _content = _inputQ.poll(); - if (_content == null) - { - try - { - produceContent(); - } - catch (IOException e) - { - woken = failed(e); - } - if (_content == null) - _content = _inputQ.poll(); - } - - if (_content != null) - available = _content.remaining(); - } - - if (woken) - wake(); - return available; - } - - protected void wake() - { - HttpChannel channel = _channelState.getHttpChannel(); - Executor executor = channel.getConnector().getServer().getThreadPool(); - executor.execute(channel); - } - - @Override - public int read() throws IOException - { - int read = read(_oneByteBuffer, 0, 1); - if (read == 0) - throw new IllegalStateException("unready read=0"); - return read < 0 ? -1 : _oneByteBuffer[0] & 0xFF; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException - { - boolean wake = false; - int read; - try (AutoLock l = _lock.lock()) - { - // Calculate minimum request rate for DOS protection - long minRequestDataRate = _channelState.getHttpChannel().getHttpConfiguration().getMinRequestDataRate(); - if (minRequestDataRate > 0 && _firstByteTimeStamp != -1) - { - long period = System.nanoTime() - _firstByteTimeStamp; - if (period > 0) - { - long minimumData = minRequestDataRate * TimeUnit.NANOSECONDS.toMillis(period) / TimeUnit.SECONDS.toMillis(1); - if (_contentArrived < minimumData) - { - BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, - String.format("Request content data rate < %d B/s", minRequestDataRate)); - if (_channelState.isResponseCommitted()) - _channelState.getHttpChannel().abort(bad); - throw bad; - } - } - } - - // Consume content looking for bytes to read - while (true) - { - Content item = nextContent(); - if (item != null) - { - read = get(item, b, off, len); - if (LOG.isDebugEnabled()) - LOG.debug("{} read {} from {}", this, read, item); - - // Consume any following poison pills - if (item.isEmpty()) - nextInterceptedContent(); - break; - } - - // No content, so should we block? - if (!_state.blockForContent(this)) - { - // Not blocking, so what should we return? - read = _state.noContent(); - - if (read < 0) - // If EOF do we need to wake for allDataRead callback? - wake = _channelState.onReadEof(); - break; - } - } - } - - if (wake) - wake(); - return read; - } - - /** - * Called when derived implementations should attempt to produce more Content and add it via {@link #addContent(Content)}. For protocols that are constantly - * producing (eg HTTP2) this can be left as a noop; - * - * @throws IOException if unable to produce content - */ - protected void produceContent() throws IOException - { - } - - /** - * Called by channel when asynchronous IO needs to produce more content - * - * @throws IOException if unable to produce content - */ - public void asyncReadProduce() throws IOException - { - try (AutoLock l = _lock.lock()) - { - produceContent(); - } - } - - /** - * Get the next content from the inputQ, calling {@link #produceContent()} if need be. EOF is processed and state changed. - * - * @return the content or null if none available. - * @throws IOException if retrieving the content fails - */ - protected Content nextContent() throws IOException - { - Content content = nextNonSentinelContent(); - if (content == null && !isFinished()) - { - produceContent(); - content = nextNonSentinelContent(); - } - return content; - } - - /** - * Poll the inputQ for Content. Consumed buffers and {@link SentinelContent}s are removed and EOF state updated if need be. - * - * @return Content or null - */ - protected Content nextNonSentinelContent() - { - while (true) - { - // Get the next content (or EOF) - Content content = nextInterceptedContent(); - - // If it is EOF, consume it here - if (content instanceof SentinelContent) - { - // Consume the EOF content, either if it was original content - // or if it was produced by interception - consume(content); - continue; - } - - return content; - } - } - - /** - * Get the next readable from the inputQ, calling {@link #produceContent()} if need be. EOF is NOT processed and state is not changed. - * - * @return the content or EOF or null if none available. - * @throws IOException if retrieving the content fails - */ - protected Content produceNextContext() throws IOException - { - Content content = nextInterceptedContent(); - if (content == null && !isFinished()) - { - produceContent(); - content = nextInterceptedContent(); - } - return content; - } - - /** - * Poll the inputQ for Content or EOF. Consumed buffers and non EOF {@link SentinelContent}s are removed. EOF state is not updated. - * Interception is done within this method. - * - * @return Content with remaining, a {@link SentinelContent}, or null - */ - protected Content nextInterceptedContent() - { - // If we have a chunk produced by interception - if (_intercepted != null) - { - // Use it if it has any remaining content - if (_intercepted.hasContent()) - return _intercepted; - - // succeed the chunk - _intercepted.succeeded(); - _intercepted = null; - } - - // If we don't have a Content under consideration, get - // the next one off the input Q. - if (_content == null) - _content = _inputQ.poll(); - - // While we have content to consider. - while (_content != null) - { - // Are we intercepting? - if (_interceptor != null) - { - // Intercept the current content (may be called several - // times for the same content - _intercepted = _interceptor.readFrom(_content); - - // If interception produced new content - if (_intercepted != null && _intercepted != _content) - { - // if it is not empty use it - if (_intercepted.hasContent()) - return _intercepted; - _intercepted.succeeded(); - } - - // intercepted content consumed - _intercepted = null; - - // fall through so that the unintercepted _content is - // considered for any remaining content, for EOF and to - // succeed it if it is entirely consumed. - } - - // If the content has content or is an EOF marker, use it - if (_content.hasContent() || _content instanceof SentinelContent) - return _content; - - // The content is consumed, so get the next one. Note that EOF - // content is never consumed here, but in #pollContent - _content.succeeded(); - _content = _inputQ.poll(); - } - - return null; - } - - private void consume(Content content) - { - if (!isError() && content instanceof EofContent) - { - if (content == EARLY_EOF_CONTENT) - _state = EARLY_EOF; - else if (_listener == null) - _state = EOF; - else - _state = AEOF; - } - - // Consume the content, either if it was original content - // or if it was produced by interception - content.succeeded(); - if (_content == content) - _content = null; - else if (_intercepted == content) - _intercepted = null; - } - - /** - * Copies the given content into the given byte buffer. - * - * @param content the content to copy from - * @param buffer the buffer to copy into - * @param offset the buffer offset to start copying from - * @param length the space available in the buffer - * @return the number of bytes actually copied - */ - protected int get(Content content, byte[] buffer, int offset, int length) - { - int l = content.get(buffer, offset, length); - _contentConsumed += l; - return l; - } - - /** - * Consumes the given content. Calls the content succeeded if all content consumed. - * - * @param content the content to consume - * @param length the number of bytes to consume - */ - protected void skip(Content content, int length) - { - int l = content.skip(length); - - _contentConsumed += l; - if (l > 0 && content.isEmpty()) - nextNonSentinelContent(); // hungry succeed - } - - /** - * Blocks until some content or some end-of-file event arrives. - * - * @throws IOException if the wait is interrupted - */ - protected void blockForContent() throws IOException - { - assert _lock.isHeldByCurrentThread(); - try - { - _waitingForContent = true; - _channelState.getHttpChannel().onBlockWaitForContent(); - - boolean loop = false; - long timeout = 0; - while (true) - { - // This method is called from a loop, so we just - // need to check the timeout before and after waiting. - if (loop) - break; - - if (LOG.isDebugEnabled()) - LOG.debug("{} blocking for content timeout={}", this, timeout); - if (timeout > 0) - _lock.await(timeout, TimeUnit.MILLISECONDS); - else - _lock.await(); - - loop = true; - } - } - catch (Throwable x) - { - _channelState.getHttpChannel().onBlockWaitForContentFailure(x); - } - } - - /** - * Adds some content to this input stream. - * - * @param content the content to add - * @return true if content channel woken for read - */ - public boolean addContent(Content content) - { - try (AutoLock l = _lock.lock()) - { - _waitingForContent = false; - if (_firstByteTimeStamp == -1) - _firstByteTimeStamp = System.nanoTime(); - - if (isFinished()) - { - Throwable failure = isError() ? _state.getError() : new EOFException("Content after EOF"); - content.failed(failure); - return false; - } - else - { - _contentArrived += content.remaining(); - - if (_content == null && _inputQ.isEmpty()) - _content = content; - else - _inputQ.offer(content); - - if (LOG.isDebugEnabled()) - LOG.debug("{} addContent {}", this, content); - - if (nextInterceptedContent() != null) - return wakeup(); - else - return false; - } - } - } - - public boolean hasContent() - { - try (AutoLock l = _lock.lock()) - { - return _content != null || _inputQ.size() > 0; - } - } - - public void unblock() - { - try (AutoLock.WithCondition l = _lock.lock()) - { - l.signal(); - } - } - - public long getContentConsumed() - { - try (AutoLock l = _lock.lock()) - { - return _contentConsumed; - } - } - - public long getContentReceived() - { - synchronized (_inputQ) - { - return _contentArrived; - } - } - - /** - * This method should be called to signal that an EOF has been detected before all the expected content arrived. - *

- * Typically this will result in an EOFException being thrown from a subsequent read rather than a -1 return. - * - * @return true if content channel woken for read - */ - public boolean earlyEOF() - { - return addContent(EARLY_EOF_CONTENT); - } - - /** - * This method should be called to signal that all the expected content arrived. - * - * @return true if content channel woken for read - */ - public boolean eof() - { - return addContent(EOF_CONTENT); - } - - public boolean consumeAll() - { - try (AutoLock l = _lock.lock()) - { - try - { - while (true) - { - Content item = nextContent(); - if (item == null) - break; // Let's not bother blocking - - skip(item, item.remaining()); - } - if (isFinished()) - return !isError(); - - _state = EARLY_EOF; - return false; - } - catch (Throwable e) - { - if (LOG.isDebugEnabled()) - LOG.debug("Unable to consume all input", e); - _state = new ErrorState(e); - return false; - } - } - } - - public boolean isError() - { - try (AutoLock l = _lock.lock()) - { - return _state instanceof ErrorState; - } - } - - public boolean isAsync() - { - try (AutoLock l = _lock.lock()) - { - return _state == ASYNC; - } - } - - @Override - public boolean isFinished() - { - try (AutoLock l = _lock.lock()) - { - return _state instanceof EOFState; - } - } - - @Override - public boolean isReady() - { - try - { - try (AutoLock l = _lock.lock()) - { - if (_listener == null) - return true; - if (_state instanceof EOFState) - return true; - if (_waitingForContent) - return false; - if (produceNextContext() != null) - return true; - _channelState.onReadUnready(); - _waitingForContent = true; - } - return false; - } - catch (IOException e) - { - LOG.trace("IGNORED", e); - return true; - } - } - - @Override - public void setReadListener(ReadListener readListener) - { - boolean woken = false; - try - { - try (AutoLock l = _lock.lock()) - { - if (_listener != null) - throw new IllegalStateException("ReadListener already set"); - - _listener = Objects.requireNonNull(readListener); - - //illegal if async not started - if (!_channelState.isAsyncStarted()) - throw new IllegalStateException("Async not started"); - - if (isError()) - { - woken = _channelState.onReadReady(); - } - else - { - Content content = produceNextContext(); - if (content != null) - { - _state = ASYNC; - woken = _channelState.onReadReady(); - } - else if (_state == EOF) - { - _state = AEOF; - woken = _channelState.onReadEof(); - } - else - { - _state = ASYNC; - _channelState.onReadUnready(); - _waitingForContent = true; - } - } - } - } - catch (IOException e) - { - throw new RuntimeIOException(e); - } - - if (woken) - wake(); - } - - public boolean onIdleTimeout(Throwable x) - { - try (AutoLock l = _lock.lock()) - { - boolean neverDispatched = getHttpChannelState().isIdle(); - if ((_waitingForContent || neverDispatched) && !isError()) - { - x.addSuppressed(new Throwable("HttpInput idle timeout")); - _state = new ErrorState(x); - return wakeup(); - } - return false; - } - } - - public boolean failed(Throwable x) - { - try (AutoLock l = _lock.lock()) - { - // Errors may be reported multiple times, for example - // a local idle timeout and a remote I/O failure. - if (isError()) - { - if (LOG.isDebugEnabled()) - { - // Log both the original and current failure - // without modifying the original failure. - Throwable failure = new Throwable(_state.getError()); - failure.addSuppressed(x); - LOG.debug("HttpInput failure", failure); - } - } - else - { - // Add a suppressed throwable to capture this stack - // trace without wrapping/hiding the original failure. - x.addSuppressed(new Throwable("HttpInput failure")); - _state = new ErrorState(x); - } - return wakeup(); - } - } - - private boolean wakeup() - { - assert _lock.isHeldByCurrentThread(); - if (_listener != null) - return _channelState.onContentAdded(); - _lock.signal(); - return false; - } - - /* - *

While this class is-a Runnable, it should never be dispatched in it's own thread. It is a runnable only so that the calling thread can use {@link - * ContextHandler#handle(Runnable)} to setup classloaders etc.

- */ - @Override - public void run() - { - final ReadListener listener; - Throwable error; - boolean aeof = false; - - try (AutoLock l = _lock.lock()) - { - listener = _listener; - - if (_state == EOF) - return; - - if (_state == AEOF) - { - _state = EOF; - aeof = true; - } - - error = _state.getError(); - - if (!aeof && error == null) - { - Content content = nextInterceptedContent(); - if (content == null) - return; - - // Consume a directly received EOF without first calling onDataAvailable - // So -1 will never be read and only onAddDataRread or onError will be called - if (content instanceof EofContent) - { - consume(content); - if (_state == EARLY_EOF) - error = _state.getError(); - else if (_state == AEOF) - { - aeof = true; - _state = EOF; - } - } - } - } - - try - { - if (error != null) - { - // TODO is this necessary to add here? - _channelState.getHttpChannel().getResponse().getHttpFields().add(HttpConnection.CONNECTION_CLOSE); - listener.onError(error); - } - else if (aeof) - { - listener.onAllDataRead(); - } - else - { - listener.onDataAvailable(); - // If -1 was read, then HttpChannelState#onEOF will have been called and a subsequent - // unhandle will call run again so onAllDataRead() can be called. - } - } - catch (Throwable e) - { - if (LOG.isDebugEnabled()) - LOG.warn("Unable to notify listener", e); - else - LOG.warn("Unable to notify listener: {}", e.toString()); - try - { - if (aeof || error == null) - { - _channelState.getHttpChannel().getResponse().getHttpFields().add(HttpConnection.CONNECTION_CLOSE); - listener.onError(e); - } - } - catch (Throwable e2) - { - String msg = "Unable to notify error to listener"; - if (LOG.isDebugEnabled()) - LOG.warn(msg, e2); - else - LOG.warn("{}: {}", msg, e2.toString()); - throw new RuntimeIOException(msg, e2); - } - } - } - - @Override - public String toString() - { - State state; - long consumed; - int q; - Content content; - try (AutoLock l = _lock.lock()) - { - state = _state; - consumed = _contentConsumed; - q = _inputQ.size(); - content = _inputQ.peekFirst(); - } - return String.format("%s@%x[c=%d,q=%d,[0]=%s,s=%s]", - getClass().getSimpleName(), - hashCode(), - consumed, - q, - content, - state); - } - - /** - * A Sentinel Content, which has zero length content but - * indicates some other event in the input stream (eg EOF) - */ - public static class SentinelContent extends Content - { - private final String _name; - - public SentinelContent(String name) - { - super(BufferUtil.EMPTY_BUFFER); - _name = name; - } @Override public String toString() { - return _name; - } - } - - public static class EofContent extends SentinelContent - { - EofContent(String name) - { - super(name); + return getClass().getSimpleName() + "@" + hashCode() + " [p=" + _prev + ",n=" + _next + "]"; } } + /** + * A content represents the production of a {@link HttpChannel} returned by {@link HttpChannel#produceContent()}. + * There are two fundamental types of content: special and non-special. + * Non-special content always wraps a byte buffer that can be consumed and must be recycled once it is empty, either + * via {@link #succeeded()} or {@link #failed(Throwable)}. + * Special content indicates a special event, like EOF or an error and never wraps a byte buffer. Calling + * {@link #succeeded()} or {@link #failed(Throwable)} on those have no effect. + */ public static class Content implements Callback { protected final ByteBuffer _content; @@ -989,6 +437,10 @@ public class HttpInput extends ServletInputStream implements Runnable _content = content; } + /** + * Get the wrapped byte buffer. Throws {@link IllegalStateException} if the content is special. + * @return the wrapped byte buffer. + */ public ByteBuffer getByteBuffer() { return _content; @@ -1000,6 +452,13 @@ public class HttpInput extends ServletInputStream implements Runnable return InvocationType.NON_BLOCKING; } + /** + * Read the wrapped byte buffer. Throws {@link IllegalStateException} if the content is special. + * @param buffer The array into which bytes are to be written. + * @param offset The offset within the array of the first byte to be written. + * @param length The maximum number of bytes to be written to the given array. + * @return The amount of bytes read from the buffer. + */ public int get(byte[] buffer, int offset, int length) { length = Math.min(_content.remaining(), length); @@ -1007,6 +466,11 @@ public class HttpInput extends ServletInputStream implements Runnable return length; } + /** + * Skip some bytes from the buffer. Has no effect on a special content. + * @param length How many bytes to skip. + * @return How many bytes were skipped. + */ public int skip(int length) { length = Math.min(_content.remaining(), length); @@ -1014,55 +478,193 @@ public class HttpInput extends ServletInputStream implements Runnable return length; } + /** + * Check if there is at least one byte left in the buffer. + * Always false on a special content. + * @return true if there is at least one byte left in the buffer. + */ public boolean hasContent() { return _content.hasRemaining(); } + /** + * Get the number of bytes remaining in the buffer. + * Always 0 on a special content. + * @return the number of bytes remaining in the buffer. + */ public int remaining() { return _content.remaining(); } + /** + * Check if the buffer is empty. + * Always true on a special content. + * @return true if there is 0 byte left in the buffer. + */ public boolean isEmpty() { return !_content.hasRemaining(); } - @Override - public String toString() - { - return String.format("Content@%x{%s}", hashCode(), BufferUtil.toDetailString(_content)); - } - } - - protected abstract static class State - { - public boolean blockForContent(HttpInput in) throws IOException + /** + * Check if the content is special. + * @return true if the content is special, false otherwise. + */ + public boolean isSpecial() { return false; } - public int noContent() throws IOException + /** + * Check if EOF was reached. Both special and non-special content + * can have this flag set to true but in the case of non-special content, + * this can be interpreted as a hint as it is always going to be followed + * by another content that is both special and EOF. + * @return true if EOF was reached, false otherwise. + */ + public boolean isEof() { - return -1; + return false; } + /** + * Get the reported error. Only special contents can have an error. + * @return the error or null if there is none. + */ public Throwable getError() { return null; } + + @Override + public String toString() + { + return String.format("%s@%x{%s,spc=%s,eof=%s,err=%s}", getClass().getSimpleName(), hashCode(), + BufferUtil.toDetailString(_content), isSpecial(), isEof(), getError()); + } } - protected static class EOFState extends State + /** + * Simple non-special content wrapper allow overriding the EOF flag. + */ + public static class WrappingContent extends Content { + private final Content _delegate; + private final boolean _eof; + + public WrappingContent(Content delegate, boolean eof) + { + super(delegate.getByteBuffer()); + _delegate = delegate; + _eof = eof; + } + + @Override + public boolean isEof() + { + return _eof; + } + + @Override + public void succeeded() + { + _delegate.succeeded(); + } + + @Override + public void failed(Throwable x) + { + _delegate.failed(x); + } + + @Override + public InvocationType getInvocationType() + { + return _delegate.getInvocationType(); + } } - protected class ErrorState extends EOFState + /** + * Abstract class that implements the standard special content behavior. + */ + public abstract static class SpecialContent extends Content { - final Throwable _error; + public SpecialContent() + { + super(null); + } - ErrorState(Throwable error) + @Override + public final ByteBuffer getByteBuffer() + { + throw new IllegalStateException(this + " has no buffer"); + } + + @Override + public final int get(byte[] buffer, int offset, int length) + { + throw new IllegalStateException(this + " has no buffer"); + } + + @Override + public final int skip(int length) + { + return 0; + } + + @Override + public final boolean hasContent() + { + return false; + } + + @Override + public final int remaining() + { + return 0; + } + + @Override + public final boolean isEmpty() + { + return true; + } + + @Override + public final boolean isSpecial() + { + return true; + } + } + + /** + * EOF special content. + */ + public static final class EofContent extends SpecialContent + { + @Override + public boolean isEof() + { + return true; + } + + @Override + public String toString() + { + return getClass().getSimpleName(); + } + } + + /** + * Error special content. + */ + public static final class ErrorContent extends SpecialContent + { + private final Throwable _error; + + public ErrorContent(Throwable error) { _error = error; } @@ -1073,88 +675,10 @@ public class HttpInput extends ServletInputStream implements Runnable return _error; } - @Override - public int noContent() throws IOException - { - if (_error instanceof IOException) - throw (IOException)_error; - throw new IOException(_error); - } - @Override public String toString() { - return "ERROR:" + _error; + return getClass().getSimpleName() + " [" + _error + "]"; } } - - protected static final State STREAM = new State() - { - @Override - public boolean blockForContent(HttpInput input) throws IOException - { - input.blockForContent(); - return true; - } - - @Override - public String toString() - { - return "STREAM"; - } - }; - - protected static final State ASYNC = new State() - { - @Override - public int noContent() throws IOException - { - return 0; - } - - @Override - public String toString() - { - return "ASYNC"; - } - }; - - protected static final State EARLY_EOF = new EOFState() - { - @Override - public int noContent() throws IOException - { - throw getError(); - } - - @Override - public String toString() - { - return "EARLY_EOF"; - } - - @Override - public IOException getError() - { - return new EofException("Early EOF"); - } - }; - - protected static final State EOF = new EOFState() - { - @Override - public String toString() - { - return "EOF"; - } - }; - - protected static final State AEOF = new EOFState() - { - @Override - public String toString() - { - return "AEOF"; - } - }; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java deleted file mode 100644 index 6f646c4ce61..00000000000 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputOverHTTP.java +++ /dev/null @@ -1,35 +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.server; - -import java.io.IOException; - -public class HttpInputOverHTTP extends HttpInput -{ - public HttpInputOverHTTP(HttpChannelState state) - { - super(state); - } - - @Override - protected void produceContent() throws IOException - { - ((HttpConnection)getHttpChannelState().getHttpChannel().getEndPoint().getConnection()).fillAndParseForContent(); - } -} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputState.puml b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputState.puml new file mode 100644 index 00000000000..0ef1896b5fe --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInputState.puml @@ -0,0 +1,16 @@ +@startuml + +IDLE: +READY: +UNREADY: + +[*] --> IDLE + +IDLE --> UNREADY : isReady +IDLE -right->READY : isReady + +UNREADY -up-> READY : ASYNC onContentProducible + +READY -left->IDLE : nextContent + +@enduml diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_async.puml b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_async.puml new file mode 100644 index 00000000000..c520361faef --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_async.puml @@ -0,0 +1,114 @@ +@startuml +title "HttpInput" + +participant AsyncContentDelivery as "[async\ncontent\ndelivery]" +participant HttpChannel as "Http\nChannel\n" +participant HttpChannelState as "Http\nChannel\nState" +participant HttpInputInterceptor as "Http\nInput.\nInterceptor" +participant AsyncContentProducer as "Async\nContent\nProducer" +participant HttpInput as "Http\nInput\n" +participant Application as "\nApplication\n" + +autoactivate on + +== Async Read == + +Application->HttpInput: read +activate Application + HttpInput->AsyncContentProducer: nextContent + AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent + AsyncContentProducer->HttpChannel: produceContent + return raw content or null + alt if raw content is not null + AsyncContentProducer->HttpInputInterceptor: readFrom + return transformed content + end + return + alt if transformed content is not null + AsyncContentProducer->HttpChannelState: onReadIdle + return + end + return content or null + note over HttpInput + throw ISE + if content + is null + end note + HttpInput->AsyncContentProducer: reclaim + return +return +deactivate Application + +== isReady == + +Application->HttpInput: isReady +activate Application + HttpInput->AsyncContentProducer: isReady + AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent + AsyncContentProducer->HttpChannel: produceContent + return raw content or null + alt if raw content is not null + AsyncContentProducer->HttpInputInterceptor: readFrom + return transformed content + end + return + alt if transformed content is not null + AsyncContentProducer->HttpChannelState: onContentAdded + return + else transformed content is null + AsyncContentProducer->HttpChannelState: onReadUnready + return + AsyncContentProducer->HttpChannel: needContent + return + alt if needContent returns true + AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent + return + alt if transformed content is not null + AsyncContentProducer->HttpChannelState: onContentAdded + return + end + end + end + return boolean\n[transformed\ncontent is not null] +return +deactivate Application + +alt if content arrives + AsyncContentDelivery->HttpInput: onContentProducible + HttpInput->AsyncContentProducer: onContentProducible + alt if not at EOF + AsyncContentProducer->HttpChannelState: onReadReady + return true if woken + else if at EOF + AsyncContentProducer->HttpChannelState: onReadEof + return true if woken + end + return true if woken + return true if woken + alt onContentProducible returns true + AsyncContentDelivery->HttpChannel: execute(HttpChannel) + return + end +end + +||| + +== available == + +Application->HttpInput: available +activate Application + HttpInput->AsyncContentProducer: available + AsyncContentProducer->AsyncContentProducer: next\nTransformed\nContent + AsyncContentProducer->HttpChannel: produceContent + return raw content or null + alt if raw content is not null + AsyncContentProducer->HttpInputInterceptor: readFrom + return transformed content + end + return + return content size or\n0 if content is null +return +deactivate Application + +||| +@enduml diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_blocking.puml b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_blocking.puml new file mode 100644 index 00000000000..06cb82c4cfd --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput_blocking.puml @@ -0,0 +1,64 @@ +@startuml +title "HttpInput" + +participant AsyncContentDelivery as "[async\ncontent\ndelivery]" +participant HttpChannel as "Http\nChannel\n" +participant HttpChannelState as "Http\nChannel\nState" +participant AsyncContentProducer as "Async\nContent\nProducer" +participant Semaphore as "\nSemaphore\n" +participant BlockingContentProducer as "Blocking\nContent\nProducer" +participant HttpInput as "Http\nInput\n" +participant Application as "\nApplication\n" + +autoactivate on + +== Blocking Read == + +Application->HttpInput: read +activate Application + HttpInput->BlockingContentProducer: nextContent + loop + BlockingContentProducer->AsyncContentProducer: nextContent + AsyncContentProducer->AsyncContentProducer: nextTransformedContent + AsyncContentProducer->HttpChannel: produceContent + return + return + alt content is not null + AsyncContentProducer->HttpChannelState: onReadIdle + return + end + return content or null + alt content is null + BlockingContentProducer->HttpChannelState: onReadUnready + return + BlockingContentProducer->HttpChannel: needContent + return + alt needContent returns false + BlockingContentProducer->Semaphore: acquire + return + else needContent returns true + note over BlockingContentProducer + continue loop + end note + end + else content is not null + return non-null content + end + end + ' return from BlockingContentProducer: nextContent + HttpInput->BlockingContentProducer: reclaim + BlockingContentProducer->AsyncContentProducer: reclaim + return + return +return +deactivate Application + +alt if content arrives + AsyncContentDelivery->HttpInput: wakeup + HttpInput->BlockingContentProducer: wakeup + BlockingContentProducer->Semaphore: release + return + return false + return false +end +@enduml 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 a431184250a..934cd2e6a37 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 @@ -749,7 +749,7 @@ public class Request implements HttpServletRequest public long getContentRead() { - return _input.getContentConsumed(); + return _input.getContentReceived(); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java index 5c0efbf6695..d67c26027d7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java @@ -48,6 +48,7 @@ import javax.servlet.http.HttpSessionListener; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.Syntax; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.SessionIdManager; @@ -645,7 +646,7 @@ public class SessionHandler extends ScopedHandler HttpCookie cookie = null; cookie = new HttpCookie( - _cookieConfig.getName(), + getSessionCookieName(_cookieConfig), id, _cookieConfig.getDomain(), sessionPath, @@ -1334,6 +1335,13 @@ public class SessionHandler extends ScopedHandler public Session getSession(); } + public static String getSessionCookieName(SessionCookieConfig config) + { + if (config == null || config.getName() == null) + return __DefaultSessionCookie; + return config.getName(); + } + /** * CookieConfig * @@ -1423,6 +1431,10 @@ public class SessionHandler extends ScopedHandler { if (_context != null && _context.getContextHandler().isAvailable()) throw new IllegalStateException("CookieConfig cannot be set after ServletContext is started"); + if ("".equals(name)) + throw new IllegalArgumentException("Blank cookie name"); + if (name != null) + Syntax.requireValidRFC2616Token(name, "Bad Session cookie name"); _sessionCookie = name; } @@ -1596,12 +1608,12 @@ public class SessionHandler extends ScopedHandler Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { - final String sessionCookie = getSessionCookieConfig().getName(); - for (int i = 0; i < cookies.length; i++) + final String sessionCookie = getSessionCookieName(getSessionCookieConfig()); + for (Cookie cookie : cookies) { - if (sessionCookie.equalsIgnoreCase(cookies[i].getName())) + if (sessionCookie.equalsIgnoreCase(cookie.getName())) { - String id = cookies[i].getValue(); + String id = cookie.getValue(); requestedSessionIdFromCookie = true; if (LOG.isDebugEnabled()) LOG.debug("Got Session ID {} from cookie {}", id, sessionCookie); diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java new file mode 100644 index 00000000000..379a9386431 --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncContentProducerTest.java @@ -0,0 +1,340 @@ +// +// ======================================================================== +// 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.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.zip.GZIPOutputStream; + +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor; +import org.eclipse.jetty.util.compression.CompressionPool; +import org.eclipse.jetty.util.compression.InflaterPool; +import org.hamcrest.core.Is; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class AsyncContentProducerTest +{ + private ScheduledExecutorService scheduledExecutorService; + private InflaterPool inflaterPool; + + @BeforeEach + public void setUp() + { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + inflaterPool = new InflaterPool(-1, true); + } + + @AfterEach + public void tearDown() + { + scheduledExecutorService.shutdownNow(); + } + + @Test + public void testAsyncContentProducerNoInterceptor() throws Exception + { + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(buffers); + final String originalContentString = asString(buffers); + + CyclicBarrier barrier = new CyclicBarrier(2); + + ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier); + assertThat(error, nullValue()); + } + + @Test + public void testAsyncContentProducerNoInterceptorWithError() throws Exception + { + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(buffers); + final String originalContentString = asString(buffers); + final Throwable expectedError = new EofException("Early EOF"); + + CyclicBarrier barrier = new CyclicBarrier(2); + + ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier); + assertThat(error, Is.is(expectedError)); + } + + @Test + public void testAsyncContentProducerGzipInterceptor() throws Exception + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + CyclicBarrier barrier = new CyclicBarrier(2); + + ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier)); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier); + assertThat(error, nullValue()); + } + + @Test + public void testAsyncContentProducerGzipInterceptorWithTinyBuffers() throws Exception + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + CyclicBarrier barrier = new CyclicBarrier(2); + + ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, barrier)); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, totalContentBytesCount + buffers.length + 2, 25, 4, barrier); + assertThat(error, nullValue()); + } + + @Test + public void testBlockingContentProducerGzipInterceptorWithError() throws Exception + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + final Throwable expectedError = new Throwable("HttpInput idle timeout"); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + CyclicBarrier barrier = new CyclicBarrier(2); + + ContentProducer contentProducer = new AsyncContentProducer(new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, barrier)); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, contentProducer, (buffers.length + 1) * 2, 0, 4, barrier); + assertThat(error, Is.is(expectedError)); + } + + private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, ContentProducer contentProducer, int totalContentCount, int readyCount, int notReadyCount, CyclicBarrier barrier) throws InterruptedException, BrokenBarrierException, TimeoutException + { + int readBytes = 0; + String consumedString = ""; + int nextContentCount = 0; + int isReadyFalseCount = 0; + int isReadyTrueCount = 0; + Throwable error = null; + + while (true) + { + if (contentProducer.isReady()) + isReadyTrueCount++; + else + isReadyFalseCount++; + + HttpInput.Content content = contentProducer.nextContent(); + nextContentCount++; + if (content == null) + { + barrier.await(5, TimeUnit.SECONDS); + content = contentProducer.nextContent(); + nextContentCount++; + } + assertThat(content, notNullValue()); + + if (content.isSpecial()) + { + if (content.isEof()) + break; + error = content.getError(); + break; + } + + byte[] b = new byte[content.remaining()]; + readBytes += b.length; + content.getByteBuffer().get(b); + consumedString += new String(b, StandardCharsets.ISO_8859_1); + content.skip(content.remaining()); + } + + assertThat(nextContentCount, is(totalContentCount)); + assertThat(readBytes, is(totalContentBytesCount)); + assertThat(consumedString, is(originalContentString)); + assertThat(isReadyFalseCount, is(notReadyCount)); + assertThat(isReadyTrueCount, is(readyCount)); + return error; + } + + private static int countRemaining(ByteBuffer[] byteBuffers) + { + int total = 0; + for (ByteBuffer byteBuffer : byteBuffers) + { + total += byteBuffer.remaining(); + } + return total; + } + + private static String asString(ByteBuffer[] buffers) + { + StringBuilder sb = new StringBuilder(); + for (ByteBuffer buffer : buffers) + { + byte[] b = new byte[buffer.remaining()]; + buffer.duplicate().get(b); + sb.append(new String(b, StandardCharsets.ISO_8859_1)); + } + return sb.toString(); + } + + private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream output = new GZIPOutputStream(baos); + + byte[] b = new byte[uncompressedBuffer.remaining()]; + uncompressedBuffer.get(b); + output.write(b); + + output.close(); + return ByteBuffer.wrap(baos.toByteArray()); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + private static class ArrayDelayedHttpChannel extends HttpChannel + { + private final ByteBuffer[] byteBuffers; + private final HttpInput.Content finalContent; + private final ScheduledExecutorService scheduledExecutorService; + private final CyclicBarrier barrier; + private int counter; + private volatile HttpInput.Content nextContent; + + public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, CyclicBarrier barrier) + { + super(new MockConnector(), new HttpConfiguration(), null, null); + this.byteBuffers = new ByteBuffer[byteBuffers.length]; + this.finalContent = finalContent; + this.scheduledExecutorService = scheduledExecutorService; + this.barrier = barrier; + for (int i = 0; i < byteBuffers.length; i++) + { + this.byteBuffers[i] = byteBuffers[i].duplicate(); + } + } + + @Override + public boolean needContent() + { + if (nextContent != null) + return true; + scheduledExecutorService.schedule(() -> + { + if (byteBuffers.length > counter) + nextContent = new HttpInput.Content(byteBuffers[counter++]); + else + nextContent = finalContent; + try + { + barrier.await(5, TimeUnit.SECONDS); + } + catch (Exception e) + { + throw new AssertionError(e); + } + }, 50, TimeUnit.MILLISECONDS); + return false; + } + + @Override + public HttpInput.Content produceContent() + { + HttpInput.Content result = nextContent; + nextContent = null; + return result; + } + + @Override + public boolean failAllContent(Throwable failure) + { + nextContent = null; + counter = byteBuffers.length; + return false; + } + + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java new file mode 100644 index 00000000000..a8f8fdb7dfc --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/BlockingContentProducerTest.java @@ -0,0 +1,320 @@ +// +// ======================================================================== +// 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.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPOutputStream; + +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor; +import org.eclipse.jetty.util.compression.InflaterPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + +public class BlockingContentProducerTest +{ + private ScheduledExecutorService scheduledExecutorService; + private InflaterPool inflaterPool; + + @BeforeEach + public void setUp() + { + scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); + inflaterPool = new InflaterPool(-1, true); + } + + @AfterEach + public void tearDown() + { + scheduledExecutorService.shutdownNow(); + } + + @Test + public void testBlockingContentProducerNoInterceptor() + { + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(buffers); + final String originalContentString = asString(buffers); + + AtomicReference ref = new AtomicReference<>(); + ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible()); + ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel)); + ref.set(contentProducer); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer); + assertThat(error, nullValue()); + } + + @Test + public void testBlockingContentProducerNoInterceptorWithError() + { + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + buffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + buffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(buffers); + final String originalContentString = asString(buffers); + final Throwable expectedError = new EofException("Early EOF"); + + AtomicReference ref = new AtomicReference<>(); + ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible()); + ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel)); + ref.set(contentProducer); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer); + assertThat(error, is(expectedError)); + } + + @Test + public void testBlockingContentProducerGzipInterceptor() + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + AtomicReference ref = new AtomicReference<>(); + ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible()); + ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel)); + ref.set(contentProducer); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer); + assertThat(error, nullValue()); + } + + @Test + public void testBlockingContentProducerGzipInterceptorWithTinyBuffers() + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + AtomicReference ref = new AtomicReference<>(); + ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.EofContent(), scheduledExecutorService, () -> ref.get().onContentProducible()); + ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel)); + ref.set(contentProducer); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 1)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, totalContentBytesCount + 1, contentProducer); + assertThat(error, nullValue()); + } + + @Test + public void testBlockingContentProducerGzipInterceptorWithError() + { + ByteBuffer[] uncompressedBuffers = new ByteBuffer[3]; + uncompressedBuffers[0] = ByteBuffer.wrap("1 hello 1".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[1] = ByteBuffer.wrap("2 howdy 2".getBytes(StandardCharsets.ISO_8859_1)); + uncompressedBuffers[2] = ByteBuffer.wrap("3 hey ya 3".getBytes(StandardCharsets.ISO_8859_1)); + final int totalContentBytesCount = countRemaining(uncompressedBuffers); + final String originalContentString = asString(uncompressedBuffers); + final Throwable expectedError = new Throwable("HttpInput idle timeout"); + + ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = gzipByteBuffer(uncompressedBuffers[0]); + buffers[1] = gzipByteBuffer(uncompressedBuffers[1]); + buffers[2] = gzipByteBuffer(uncompressedBuffers[2]); + + AtomicReference ref = new AtomicReference<>(); + ArrayDelayedHttpChannel httpChannel = new ArrayDelayedHttpChannel(buffers, new HttpInput.ErrorContent(expectedError), scheduledExecutorService, () -> ref.get().onContentProducible()); + ContentProducer contentProducer = new BlockingContentProducer(new AsyncContentProducer(httpChannel)); + ref.set(contentProducer); + contentProducer.setInterceptor(new GzipHttpInputInterceptor(inflaterPool, new ArrayByteBufferPool(1, 1, 2), 32)); + + Throwable error = readAndAssertContent(totalContentBytesCount, originalContentString, buffers.length + 1, contentProducer); + assertThat(error, is(expectedError)); + } + + private Throwable readAndAssertContent(int totalContentBytesCount, String originalContentString, int totalContentCount, ContentProducer contentProducer) + { + int readBytes = 0; + int nextContentCount = 0; + String consumedString = ""; + Throwable error = null; + while (true) + { + HttpInput.Content content = contentProducer.nextContent(); + nextContentCount++; + + if (content.isSpecial()) + { + if (content.isEof()) + break; + error = content.getError(); + break; + } + + byte[] b = new byte[content.remaining()]; + content.getByteBuffer().get(b); + consumedString += new String(b, StandardCharsets.ISO_8859_1); + + readBytes += b.length; + } + assertThat(readBytes, is(totalContentBytesCount)); + assertThat(nextContentCount, is(totalContentCount)); + assertThat(consumedString, is(originalContentString)); + return error; + } + + private static int countRemaining(ByteBuffer[] byteBuffers) + { + int total = 0; + for (ByteBuffer byteBuffer : byteBuffers) + { + total += byteBuffer.remaining(); + } + return total; + } + + private static String asString(ByteBuffer[] buffers) + { + StringBuilder sb = new StringBuilder(); + for (ByteBuffer buffer : buffers) + { + byte[] b = new byte[buffer.remaining()]; + buffer.duplicate().get(b); + sb.append(new String(b, StandardCharsets.ISO_8859_1)); + } + return sb.toString(); + } + + private static ByteBuffer gzipByteBuffer(ByteBuffer uncompressedBuffer) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream output = new GZIPOutputStream(baos); + + byte[] b = new byte[uncompressedBuffer.remaining()]; + uncompressedBuffer.get(b); + output.write(b); + + output.close(); + return ByteBuffer.wrap(baos.toByteArray()); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + private interface ContentListener + { + void onContent(); + } + + private static class ArrayDelayedHttpChannel extends HttpChannel + { + private final ByteBuffer[] byteBuffers; + private final HttpInput.Content finalContent; + private final ScheduledExecutorService scheduledExecutorService; + private final ContentListener contentListener; + private int counter; + private volatile HttpInput.Content nextContent; + + public ArrayDelayedHttpChannel(ByteBuffer[] byteBuffers, HttpInput.Content finalContent, ScheduledExecutorService scheduledExecutorService, ContentListener contentListener) + { + super(new MockConnector(), new HttpConfiguration(), null, null); + this.byteBuffers = new ByteBuffer[byteBuffers.length]; + this.finalContent = finalContent; + this.scheduledExecutorService = scheduledExecutorService; + this.contentListener = contentListener; + for (int i = 0; i < byteBuffers.length; i++) + { + this.byteBuffers[i] = byteBuffers[i].duplicate(); + } + } + + @Override + public boolean needContent() + { + if (nextContent != null) + return true; + scheduledExecutorService.schedule(() -> + { + if (byteBuffers.length > counter) + nextContent = new HttpInput.Content(byteBuffers[counter++]); + else + nextContent = finalContent; + contentListener.onContent(); + }, 50, TimeUnit.MILLISECONDS); + return false; + } + + @Override + public HttpInput.Content produceContent() + { + HttpInput.Content result = nextContent; + nextContent = null; + return result; + } + + @Override + public boolean failAllContent(Throwable failure) + { + nextContent = null; + counter = byteBuffers.length; + return false; + } + + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } + } +} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java deleted file mode 100644 index 4401d4daa58..00000000000 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputAsyncStateTest.java +++ /dev/null @@ -1,735 +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.server; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; -import javax.servlet.ReadListener; - -import org.eclipse.jetty.server.HttpChannelState.Action; -import org.eclipse.jetty.server.HttpInput.Content; -import org.eclipse.jetty.util.BufferUtil; -import org.eclipse.jetty.util.thread.Scheduler; -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.eclipse.jetty.server.HttpInput.EARLY_EOF_CONTENT; -import static org.eclipse.jetty.server.HttpInput.EOF_CONTENT; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * this tests HttpInput and its interaction with HttpChannelState - */ - -public class HttpInputAsyncStateTest -{ - - private static final Queue __history = new LinkedBlockingQueue<>(); - private ByteBuffer _expected = BufferUtil.allocate(16 * 1024); - private boolean _eof; - private boolean _noReadInDataAvailable; - private boolean _completeInOnDataAvailable; - - private final ReadListener _listener = new ReadListener() - { - @Override - public void onError(Throwable t) - { - __history.add("onError:" + t); - } - - @Override - public void onDataAvailable() throws IOException - { - __history.add("onDataAvailable"); - if (!_noReadInDataAvailable && readAvailable() && _completeInOnDataAvailable) - { - __history.add("complete"); - _state.complete(); - } - } - - @Override - public void onAllDataRead() throws IOException - { - __history.add("onAllDataRead"); - } - }; - private HttpInput _in; - HttpChannelState _state; - - public static class TContent extends HttpInput.Content - { - public TContent(String content) - { - super(BufferUtil.toBuffer(content)); - } - } - - @BeforeEach - public void before() - { - _noReadInDataAvailable = false; - _in = new HttpInput(new HttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) - { - @Override - public void onAsyncWaitForContent() - { - __history.add("onAsyncWaitForContent"); - } - - @Override - public Scheduler getScheduler() - { - return null; - } - }) - { - @Override - public void onReadUnready() - { - super.onReadUnready(); - __history.add("onReadUnready"); - } - - @Override - public boolean onContentAdded() - { - boolean wake = super.onContentAdded(); - __history.add("onReadPossible " + wake); - return wake; - } - - @Override - public boolean onReadReady() - { - boolean wake = super.onReadReady(); - __history.add("onReadReady " + wake); - return wake; - } - }) - { - @Override - public void wake() - { - __history.add("wake"); - } - }; - - _state = _in.getHttpChannelState(); - __history.clear(); - } - - private void check(String... history) - { - if (history == null || history.length == 0) - assertThat(__history, empty()); - else - assertThat(__history.toArray(new String[__history.size()]), Matchers.arrayContaining(history)); - __history.clear(); - } - - private void wake() - { - handle(null); - } - - private void handle() - { - handle(null); - } - - private void handle(Runnable run) - { - Action action = _state.handling(); - loop: - while (true) - { - switch (action) - { - case DISPATCH: - if (run == null) - fail("Run is null during DISPATCH"); - run.run(); - break; - - case READ_CALLBACK: - _in.run(); - break; - - case TERMINATED: - case WAIT: - break loop; - - case COMPLETE: - __history.add("COMPLETE"); - break; - - case READ_REGISTER: - _state.getHttpChannel().onAsyncWaitForContent(); - break; - - default: - fail("Bad Action: " + action); - } - action = _state.unhandle(); - } - } - - private void deliver(Content... content) - { - if (content != null) - { - for (Content c : content) - { - if (c == EOF_CONTENT) - { - _in.eof(); - _eof = true; - } - else if (c == HttpInput.EARLY_EOF_CONTENT) - { - _in.earlyEOF(); - _eof = true; - } - else - { - _in.addContent(c); - BufferUtil.append(_expected, c.getByteBuffer().slice()); - } - } - } - } - - boolean readAvailable() throws IOException - { - int len = 0; - try - { - while (_in.isReady()) - { - int b = _in.read(); - - if (b < 0) - { - if (len > 0) - __history.add("read " + len); - __history.add("read -1"); - assertTrue(BufferUtil.isEmpty(_expected)); - assertTrue(_eof); - return true; - } - else - { - len++; - assertFalse(BufferUtil.isEmpty(_expected)); - int a = 0xff & _expected.get(); - assertThat(b, equalTo(a)); - } - } - __history.add("read " + len); - assertTrue(BufferUtil.isEmpty(_expected)); - } - catch (IOException e) - { - if (len > 0) - __history.add("read " + len); - __history.add("read " + e); - throw e; - } - return false; - } - - @AfterEach - public void after() - { - assertThat(__history.poll(), Matchers.nullValue()); - } - - @Test - public void testInitialEmptyListenInHandle() throws Exception - { - deliver(EOF_CONTENT); - check(); - - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadReady false"); - }); - - check("onAllDataRead"); - } - - @Test - public void testInitialEmptyListenAfterHandle() throws Exception - { - deliver(EOF_CONTENT); - - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onAllDataRead"); - } - - @Test - public void testListenInHandleEmpty() throws Exception - { - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(EOF_CONTENT); - check("onReadPossible true"); - handle(); - check("onAllDataRead"); - } - - @Test - public void testEmptyListenAfterHandle() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - deliver(EOF_CONTENT); - check(); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onAllDataRead"); - } - - @Test - public void testListenAfterHandleEmpty() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onAsyncWaitForContent", "onReadUnready"); - - deliver(EOF_CONTENT); - check("onReadPossible true"); - - handle(); - check("onAllDataRead"); - } - - @Test - public void testInitialEarlyEOFListenInHandle() throws Exception - { - deliver(EARLY_EOF_CONTENT); - check(); - - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadReady false"); - }); - - check("onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testInitialEarlyEOFListenAfterHandle() throws Exception - { - deliver(EARLY_EOF_CONTENT); - - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testListenInHandleEarlyEOF() throws Exception - { - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(EARLY_EOF_CONTENT); - check("onReadPossible true"); - handle(); - check("onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testEarlyEOFListenAfterHandle() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - deliver(EARLY_EOF_CONTENT); - check(); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testListenAfterHandleEarlyEOF() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onAsyncWaitForContent", "onReadUnready"); - - deliver(EARLY_EOF_CONTENT); - check("onReadPossible true"); - - handle(); - check("onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testInitialAllContentListenInHandle() throws Exception - { - deliver(new TContent("Hello"), EOF_CONTENT); - check(); - - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadReady false"); - }); - - check("onDataAvailable", "read 5", "read -1", "onAllDataRead"); - } - - @Test - public void testInitialAllContentListenAfterHandle() throws Exception - { - deliver(new TContent("Hello"), EOF_CONTENT); - - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onDataAvailable", "read 5", "read -1", "onAllDataRead"); - } - - @Test - public void testListenInHandleAllContent() throws Exception - { - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(new TContent("Hello"), EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - handle(); - check("onDataAvailable", "read 5", "read -1", "onAllDataRead"); - } - - @Test - public void testAllContentListenAfterHandle() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - deliver(new TContent("Hello"), EOF_CONTENT); - check(); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check("onDataAvailable", "read 5", "read -1", "onAllDataRead"); - } - - @Test - public void testListenAfterHandleAllContent() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onAsyncWaitForContent", "onReadUnready"); - - deliver(new TContent("Hello"), EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - - handle(); - check("onDataAvailable", "read 5", "read -1", "onAllDataRead"); - } - - @Test - public void testInitialIncompleteContentListenInHandle() throws Exception - { - deliver(new TContent("Hello"), EARLY_EOF_CONTENT); - check(); - - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadReady false"); - }); - - check( - "onDataAvailable", - "read 5", - "read org.eclipse.jetty.io.EofException: Early EOF", - "onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testInitialPartialContentListenAfterHandle() throws Exception - { - deliver(new TContent("Hello"), EARLY_EOF_CONTENT); - - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check( - "onDataAvailable", - "read 5", - "read org.eclipse.jetty.io.EofException: Early EOF", - "onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testListenInHandlePartialContent() throws Exception - { - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(new TContent("Hello"), EARLY_EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - handle(); - check( - "onDataAvailable", - "read 5", - "read org.eclipse.jetty.io.EofException: Early EOF", - "onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testPartialContentListenAfterHandle() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - deliver(new TContent("Hello"), EARLY_EOF_CONTENT); - check(); - - _in.setReadListener(_listener); - check("onReadReady true", "wake"); - wake(); - check( - "onDataAvailable", - "read 5", - "read org.eclipse.jetty.io.EofException: Early EOF", - "onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testListenAfterHandlePartialContent() throws Exception - { - handle(() -> - { - _state.startAsync(null); - check(); - }); - - _in.setReadListener(_listener); - check("onAsyncWaitForContent", "onReadUnready"); - - deliver(new TContent("Hello"), EARLY_EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - - handle(); - check( - "onDataAvailable", - "read 5", - "read org.eclipse.jetty.io.EofException: Early EOF", - "onError:org.eclipse.jetty.io.EofException: Early EOF"); - } - - @Test - public void testReadAfterOnDataAvailable() throws Exception - { - _noReadInDataAvailable = true; - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(new TContent("Hello"), EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - - handle(); - check("onDataAvailable"); - - readAvailable(); - check("wake", "read 5", "read -1"); - wake(); - check("onAllDataRead"); - } - - @Test - public void testReadOnlyExpectedAfterOnDataAvailable() throws Exception - { - _noReadInDataAvailable = true; - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(new TContent("Hello"), EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - - handle(); - check("onDataAvailable"); - - byte[] buffer = new byte[_expected.remaining()]; - assertThat(_in.read(buffer), equalTo(buffer.length)); - assertThat(new String(buffer), equalTo(BufferUtil.toString(_expected))); - BufferUtil.clear(_expected); - check(); - - assertTrue(_in.isReady()); - check(); - - assertThat(_in.read(), equalTo(-1)); - check("wake"); - - wake(); - check("onAllDataRead"); - } - - @Test - public void testReadAndCompleteInOnDataAvailable() throws Exception - { - _completeInOnDataAvailable = true; - handle(() -> - { - _state.startAsync(null); - _in.setReadListener(_listener); - check("onReadUnready"); - }); - - check("onAsyncWaitForContent"); - - deliver(new TContent("Hello"), EOF_CONTENT); - check("onReadPossible true", "onReadPossible false"); - - handle(() -> - { - __history.add(_state.getState().toString()); - }); - System.err.println(__history); - check( - "onDataAvailable", - "read 5", - "read -1", - "complete", - "COMPLETE" - ); - } -} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java deleted file mode 100644 index 833fb70352b..00000000000 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpInputTest.java +++ /dev/null @@ -1,614 +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.server; - -import java.io.EOFException; -import java.io.IOException; -import java.util.Queue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeoutException; -import javax.servlet.ReadListener; - -import org.eclipse.jetty.util.BufferUtil; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class HttpInputTest -{ - private final Queue _history = new LinkedBlockingQueue<>(); - private final Queue _fillAndParseSimulate = new LinkedBlockingQueue<>(); - private final ReadListener _listener = new ReadListener() - { - @Override - public void onError(Throwable t) - { - _history.add("l.onError:" + t); - } - - @Override - public void onDataAvailable() throws IOException - { - _history.add("l.onDataAvailable"); - } - - @Override - public void onAllDataRead() throws IOException - { - _history.add("l.onAllDataRead"); - } - }; - private HttpInput _in; - - public class TContent extends HttpInput.Content - { - private final String _content; - - public TContent(String content) - { - super(BufferUtil.toBuffer(content)); - _content = content; - } - - @Override - public void succeeded() - { - _history.add("Content succeeded " + _content); - super.succeeded(); - } - - @Override - public void failed(Throwable x) - { - _history.add("Content failed " + _content); - super.failed(x); - } - } - - public class TestHttpInput extends HttpInput - { - public TestHttpInput(HttpChannelState state) - { - super(state); - } - - @Override - protected void produceContent() throws IOException - { - _history.add("produceContent " + _fillAndParseSimulate.size()); - - for (String s = _fillAndParseSimulate.poll(); s != null; s = _fillAndParseSimulate.poll()) - { - if ("_EOF_".equals(s)) - _in.eof(); - else - _in.addContent(new TContent(s)); - } - } - - @Override - protected void blockForContent() throws IOException - { - _history.add("blockForContent"); - super.blockForContent(); - } - } - - public class TestHttpChannelState extends HttpChannelState - { - private boolean _fakeAsyncState; - - public TestHttpChannelState(HttpChannel channel) - { - super(channel); - } - - public boolean isFakeAsyncState() - { - return _fakeAsyncState; - } - - public void setFakeAsyncState(boolean fakeAsyncState) - { - _fakeAsyncState = fakeAsyncState; - } - - @Override - public boolean isAsyncStarted() - { - if (isFakeAsyncState()) - return true; - return super.isAsyncStarted(); - } - - @Override - public void onReadUnready() - { - _history.add("s.onReadUnready"); - super.onReadUnready(); - } - - @Override - public boolean onReadPossible() - { - _history.add("s.onReadPossible"); - return super.onReadPossible(); - } - - @Override - public boolean onContentAdded() - { - _history.add("s.onDataAvailable"); - return super.onContentAdded(); - } - - @Override - public boolean onReadReady() - { - _history.add("s.onReadReady"); - return super.onReadReady(); - } - } - - @BeforeEach - public void before() - { - _in = new TestHttpInput(new TestHttpChannelState(new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) - { - @Override - public void onAsyncWaitForContent() - { - _history.add("asyncReadInterested"); - } - }) - ); - } - - @AfterEach - public void after() - { - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testEmpty() throws Exception - { - assertThat(_in.available(), equalTo(0)); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.isReady(), equalTo(true)); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testRead() throws Exception - { - _in.addContent(new TContent("AB")); - _in.addContent(new TContent("CD")); - _fillAndParseSimulate.offer("EF"); - _fillAndParseSimulate.offer("GH"); - assertThat(_in.available(), equalTo(2)); - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.isReady(), equalTo(true)); - - assertThat(_in.getContentConsumed(), equalTo(0L)); - assertThat(_in.read(), equalTo((int)'A')); - assertThat(_in.getContentConsumed(), equalTo(1L)); - assertThat(_in.read(), equalTo((int)'B')); - assertThat(_in.getContentConsumed(), equalTo(2L)); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'C')); - assertThat(_in.read(), equalTo((int)'D')); - - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'E')); - assertThat(_in.read(), equalTo((int)'F')); - - assertThat(_history.poll(), equalTo("produceContent 2")); - assertThat(_history.poll(), equalTo("Content succeeded EF")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'G')); - assertThat(_in.read(), equalTo((int)'H')); - - assertThat(_history.poll(), equalTo("Content succeeded GH")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.getContentConsumed(), equalTo(8L)); - - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testBlockingRead() throws Exception - { - new Thread(() -> - { - try - { - Thread.sleep(500); - _in.addContent(new TContent("AB")); - } - catch (Throwable th) - { - th.printStackTrace(); - } - }).start(); - - assertThat(_in.read(), equalTo((int)'A')); - - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("blockForContent")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'B')); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testReadEOF() throws Exception - { - _in.addContent(new TContent("AB")); - _in.addContent(new TContent("CD")); - _in.eof(); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.available(), equalTo(2)); - assertThat(_in.isFinished(), equalTo(false)); - - assertThat(_in.read(), equalTo((int)'A')); - assertThat(_in.read(), equalTo((int)'B')); - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'C')); - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.read(), equalTo((int)'D')); - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), nullValue()); - assertThat(_in.isFinished(), equalTo(false)); - - assertThat(_in.read(), equalTo(-1)); - assertThat(_in.isFinished(), equalTo(true)); - - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testReadEarlyEOF() throws Exception - { - _in.addContent(new TContent("AB")); - _in.addContent(new TContent("CD")); - _in.earlyEOF(); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.available(), equalTo(2)); - assertThat(_in.isFinished(), equalTo(false)); - - assertThat(_in.read(), equalTo((int)'A')); - assertThat(_in.read(), equalTo((int)'B')); - - assertThat(_in.read(), equalTo((int)'C')); - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.read(), equalTo((int)'D')); - - assertThrows(EOFException.class, () -> _in.read()); - assertTrue(_in.isFinished()); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testBlockingEOF() throws Exception - { - new Thread(() -> - { - try - { - Thread.sleep(500); - _in.eof(); - } - catch (Throwable th) - { - th.printStackTrace(); - } - }).start(); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.read(), equalTo(-1)); - assertThat(_in.isFinished(), equalTo(true)); - - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("blockForContent")); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testAsyncEmpty() throws Exception - { - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testAsyncRead() throws Exception - { - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), nullValue()); - - _in.addContent(new TContent("AB")); - _fillAndParseSimulate.add("CD"); - - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - _in.run(); - assertThat(_history.poll(), equalTo("l.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.read(), equalTo((int)'A')); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.read(), equalTo((int)'B')); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_history.poll(), equalTo("produceContent 1")); - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo((int)'C')); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.read(), equalTo((int)'D')); - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testAsyncEOF() throws Exception - { - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - - _in.eof(); - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.read(), equalTo(-1)); - assertThat(_in.isFinished(), equalTo(true)); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testAsyncReadEOF() throws Exception - { - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), nullValue()); - - _in.addContent(new TContent("AB")); - _fillAndParseSimulate.add("_EOF_"); - - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - _in.run(); - assertThat(_history.poll(), equalTo("l.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.read(), equalTo((int)'A')); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_in.read(), equalTo((int)'B')); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.isReady(), equalTo(true)); - assertThat(_history.poll(), equalTo("produceContent 1")); - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isFinished(), equalTo(false)); - assertThat(_in.read(), equalTo(-1)); - assertThat(_in.isFinished(), equalTo(true)); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(true)); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testAsyncError() throws Exception - { - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), equalTo("s.onReadUnready")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(false)); - assertThat(_history.poll(), nullValue()); - - _in.failed(new TimeoutException()); - assertThat(_history.poll(), equalTo("s.onDataAvailable")); - assertThat(_history.poll(), nullValue()); - - _in.run(); - assertThat(_in.isFinished(), equalTo(true)); - assertThat(_history.poll(), equalTo("l.onError:java.util.concurrent.TimeoutException")); - assertThat(_history.poll(), nullValue()); - - assertThat(_in.isReady(), equalTo(true)); - - IOException e = assertThrows(IOException.class, () -> _in.read()); - assertThat(e.getCause(), instanceOf(TimeoutException.class)); - assertThat(_in.isFinished(), equalTo(true)); - - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testSetListenerWithNull() throws Exception - { - //test can't be null - assertThrows(NullPointerException.class, () -> - { - _in.setReadListener(null); - }); - } - - @Test - public void testSetListenerNotAsync() throws Exception - { - //test not async - assertThrows(IllegalStateException.class, () -> - { - _in.setReadListener(_listener); - }); - } - - @Test - public void testSetListenerAlreadySet() throws Exception - { - //set up a listener - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(true); - _in.setReadListener(_listener); - //throw away any events generated by setting the listener - _history.clear(); - ((TestHttpChannelState)_in.getHttpChannelState()).setFakeAsyncState(false); - //now test that you can't set another listener - assertThrows(IllegalStateException.class, () -> - { - _in.setReadListener(_listener); - }); - } - - @Test - public void testRecycle() throws Exception - { - testAsyncRead(); - _in.recycle(); - testAsyncRead(); - _in.recycle(); - testReadEOF(); - } - - @Test - public void testConsumeAll() throws Exception - { - _in.addContent(new TContent("AB")); - _in.addContent(new TContent("CD")); - _fillAndParseSimulate.offer("EF"); - _fillAndParseSimulate.offer("GH"); - assertThat(_in.read(), equalTo((int)'A')); - - assertFalse(_in.consumeAll()); - assertThat(_in.getContentConsumed(), equalTo(8L)); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), equalTo("produceContent 2")); - assertThat(_history.poll(), equalTo("Content succeeded EF")); - assertThat(_history.poll(), equalTo("Content succeeded GH")); - assertThat(_history.poll(), equalTo("produceContent 0")); - assertThat(_history.poll(), nullValue()); - } - - @Test - public void testConsumeAllEOF() throws Exception - { - _in.addContent(new TContent("AB")); - _in.addContent(new TContent("CD")); - _fillAndParseSimulate.offer("EF"); - _fillAndParseSimulate.offer("GH"); - _fillAndParseSimulate.offer("_EOF_"); - assertThat(_in.read(), equalTo((int)'A')); - - assertTrue(_in.consumeAll()); - assertThat(_in.getContentConsumed(), equalTo(8L)); - - assertThat(_history.poll(), equalTo("Content succeeded AB")); - assertThat(_history.poll(), equalTo("Content succeeded CD")); - assertThat(_history.poll(), equalTo("produceContent 3")); - assertThat(_history.poll(), equalTo("Content succeeded EF")); - assertThat(_history.poll(), equalTo("Content succeeded GH")); - assertThat(_history.poll(), nullValue()); - } -} diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java index 661d857b31d..c40e3d89109 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpWriterTest.java @@ -48,11 +48,41 @@ public class HttpWriterTest HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { + @Override + public boolean needContent() + { + return false; + } + + @Override + public HttpInput.Content produceContent() + { + return null; + } + + @Override + public boolean failAllContent(Throwable failure) + { + return false; + } + @Override public ByteBufferPool getByteBufferPool() { return pool; } + + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } }; _httpOut = new HttpOutput(channel) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index 8ff0b6cbc51..afb9d8210ad 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -175,7 +175,38 @@ public class ResponseTest { _channelError = failure; } - }); + }) + { + @Override + public boolean needContent() + { + return false; + } + + @Override + public HttpInput.Content produceContent() + { + return null; + } + + @Override + public boolean failAllContent(Throwable failure) + { + return false; + } + + @Override + public boolean failed(Throwable x) + { + return false; + } + + @Override + protected boolean eof() + { + return false; + } + }; } @AfterEach diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java index 2ba299dc49e..04e1ffc7d1e 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java @@ -488,21 +488,8 @@ public class ServletHandler extends ScopedHandler FilterChain chain = null; // find the servlet - if (target.startsWith("/")) - { - if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0) - chain = getFilterChain(baseRequest, target, servletHolder); - } - else - { - if (servletHolder != null) - { - if (_filterMappings != null && _filterMappings.length > 0) - { - chain = getFilterChain(baseRequest, null, servletHolder); - } - } - } + if (servletHolder != null && _filterMappings != null && _filterMappings.length > 0) + chain = getFilterChain(baseRequest, target.startsWith("/") ? target : null, servletHolder); if (LOG.isDebugEnabled()) LOG.debug("chain={}", chain); @@ -561,6 +548,7 @@ public class ServletHandler extends ScopedHandler private FilterChain getFilterChain(Request baseRequest, String pathInContext, ServletHolder servletHolder) { + Objects.requireNonNull(servletHolder); String key = pathInContext == null ? servletHolder.getName() : pathInContext; int dispatch = FilterMapping.dispatch(baseRequest.getDispatcherType()); @@ -576,7 +564,7 @@ public class ServletHandler extends ScopedHandler // The mappings lists have been reversed to make this simple and fast. FilterChain chain = null; - if (servletHolder != null && _filterNameMappings != null && !_filterNameMappings.isEmpty()) + if (_filterNameMappings != null && !_filterNameMappings.isEmpty()) { if (_wildFilterNameMappings != null) for (FilterMapping mapping : _wildFilterNameMappings) @@ -1658,6 +1646,7 @@ public class ServletHandler extends ScopedHandler ChainEnd(ServletHolder holder) { + Objects.requireNonNull(holder); _servletHolder = holder; } diff --git a/jetty-unixsocket/jetty-unixsocket-server/pom.xml b/jetty-unixsocket/jetty-unixsocket-server/pom.xml index 23b66f4451b..af7524abd31 100644 --- a/jetty-unixsocket/jetty-unixsocket-server/pom.xml +++ b/jetty-unixsocket/jetty-unixsocket-server/pom.xml @@ -76,7 +76,7 @@ run
- + @@ -88,7 +88,7 @@ - +
diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java index 736f605df70..43576a25c0f 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/JarFileResource.java @@ -144,7 +144,7 @@ public class JarFileResource extends JarResource String fileUrl = _urlString.substring(4, _urlString.length() - 2); try { - return newResource(fileUrl).exists(); + return _directory = newResource(fileUrl).exists(); } catch (Exception e) { @@ -236,15 +236,10 @@ public class JarFileResource extends JarResource return _exists; } - /** - * Returns true if the represented resource is a container/directory. - * If the resource is not a file, resources ending with "/" are - * considered directories. - */ @Override public boolean isDirectory() { - return _urlString.endsWith("/") || exists() && _directory; + return exists() && _directory; } /** diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java index 4aa95771539..657c7998eaa 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/Resource.java @@ -320,8 +320,6 @@ public abstract class Resource implements ResourceFactory, Closeable /** * @return true if the represented resource is a container/directory. - * if the resource is not a file, resources ending with "/" are - * considered directories. */ public abstract boolean isDirectory(); @@ -412,7 +410,7 @@ public abstract class Resource implements ResourceFactory, Closeable /** * Returns the resource contained inside the current resource with the - * given name. + * given name, which may or may not exist. * * @param path The path segment to add, which is not encoded * @return the Resource for the resolved path within this Resource, never null diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java index 4b7484081f1..4ef8d68c7a8 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/ResourceCollection.java @@ -62,8 +62,19 @@ public class ResourceCollection extends Resource * @param resources the resources to be added to collection */ public ResourceCollection(Resource... resources) + { + this(Arrays.asList(resources)); + } + + /** + * Instantiates a new resource collection. + * + * @param resources the resources to be added to collection + */ + public ResourceCollection(Collection resources) { _resources = new ArrayList<>(); + for (Resource r : resources) { if (r == null) @@ -82,17 +93,6 @@ public class ResourceCollection extends Resource } } - /** - * Instantiates a new resource collection. - * - * @param resources the resources to be added to collection - */ - public ResourceCollection(Collection resources) - { - _resources = new ArrayList<>(); - _resources.addAll(resources); - } - /** * Instantiates a new resource collection. * @@ -226,8 +226,16 @@ public class ResourceCollection extends Resource } /** + * Add a path to the resource collection. * @param path The path segment to add - * @return The contained resource (found first) in the collection of resources + * @return The resulting resource(s) : + *
    + *
  • is a file that exists in at least one of the collection, then the first one found is returned
  • + *
  • is a directory that exists in at exactly one of the collection, then that directory resource is returned
  • + *
  • is a directory that exists in several of the collection, then a ResourceCollection of those directories is returned
  • + *
  • do not exist in any of the collection, then a new non existent resource relative to the first in the collection is returned.
  • + *
+ * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed against any of the collection */ @Override public Resource addPath(String path) throws IOException @@ -247,27 +255,28 @@ public class ResourceCollection extends Resource ArrayList resources = null; // Attempt a simple (single) Resource lookup that exists + Resource addedResource = null; for (Resource res : _resources) { - Resource r = res.addPath(path); - if (!r.isDirectory() && r.exists()) - { - // Return simple (non-directory) Resource - return r; - } - + addedResource = res.addPath(path); + if (!addedResource.exists()) + continue; + if (!addedResource.isDirectory()) + return addedResource; // Return simple (non-directory) Resource if (resources == null) - { resources = new ArrayList<>(); - } + resources.add(addedResource); + } - resources.add(r); + if (resources == null) + { + if (addedResource != null) + return addedResource; // This will not exist + return EmptyResource.INSTANCE; } if (resources.size() == 1) - { return resources.get(0); - } return new ResourceCollection(resources); } @@ -384,7 +393,6 @@ public class ResourceCollection extends Resource public boolean isDirectory() { assertResourcesSet(); - return true; } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java index 3dbd48ffa02..557bf2fbfbb 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/resource/URLResource.java @@ -128,11 +128,6 @@ public class URLResource extends Resource return _in != null; } - /** - * Returns true if the represented resource is a container/directory. - * If the resource is not a file, resources ending with "/" are - * considered directories. - */ @Override public boolean isDirectory() { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java index fc39c573f47..cb5313fe8d0 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceCollectionTest.java @@ -35,7 +35,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.emptyArray; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -189,7 +188,7 @@ public class ResourceCollectionTest assertThat(Arrays.asList(rc1.list()), contains("1.txt", "2.txt", "3.txt", "dir/")); assertThat(Arrays.asList(rc1.addPath("dir").list()), contains("1.txt", "2.txt", "3.txt")); - assertThat(rc1.addPath("unknown").list(), emptyArray()); + assertThat(rc1.addPath("unknown").list(), nullValue()); } @Test diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java index ddb5153ff6b..d2a639ab411 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/resource/ResourceTest.java @@ -215,15 +215,15 @@ public class ResourceTest cases.addCase(new Scenario(tdata1, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")); cases.addCase(new Scenario(tdata2, "alphabet.txt", EXISTS, !DIR, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")); - cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, DIR)); - cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, DIR)); + cases.addCase(new Scenario("jar:file:/somejar.jar!/content/", !EXISTS, !DIR)); + cases.addCase(new Scenario("jar:file:/somejar.jar!/", !EXISTS, !DIR)); String urlRef = cases.uriRef.toASCIIString(); Scenario zdata = new Scenario("jar:" + urlRef + "TestData/test.zip!/", EXISTS, DIR); cases.addCase(zdata); cases.addCase(new Scenario(zdata, "Unknown", !EXISTS, !DIR)); - cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, DIR)); + cases.addCase(new Scenario(zdata, "/Unknown/", !EXISTS, !DIR)); cases.addCase(new Scenario(zdata, "subdir", EXISTS, DIR)); cases.addCase(new Scenario(zdata, "/subdir/", EXISTS, DIR)); diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java index 2e4a24d0c30..08933386287 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/StandardDescriptorProcessor.java @@ -38,6 +38,7 @@ import org.eclipse.jetty.http.pathmap.ServletPathSpec; import org.eclipse.jetty.security.ConstraintAware; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.authentication.FormAuthenticator; +import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.FilterMapping; @@ -745,7 +746,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor case WebFragment: { //a web-fragment set the value, all web-fragments must have the same value - if (!context.getSessionHandler().getSessionCookieConfig().getName().equals(name)) + if (!name.equals(SessionHandler.getSessionCookieName(context.getSessionHandler().getSessionCookieConfig()))) throw new IllegalStateException("Conflicting cookie-config name " + name + " in " + descriptor.getResource()); break; } @@ -821,7 +822,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor case WebFragment: { //a web-fragment set the value, all web-fragments must have the same value - if (!context.getSessionHandler().getSessionCookieConfig().getPath().equals(path)) + if (!path.equals(context.getSessionHandler().getSessionCookieConfig().getPath())) throw new IllegalStateException("Conflicting cookie-config path " + path + " in " + descriptor.getResource()); break; } diff --git a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java index dafd33f0a08..b2a6e553a4f 100644 --- a/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java +++ b/jetty-webapp/src/main/java/org/eclipse/jetty/webapp/WebAppContext.java @@ -31,7 +31,6 @@ import java.util.Collections; import java.util.EventListener; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflaterBufferSizeTest.java b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflaterBufferSizeTest.java index ccdbbb8329d..445249b9e9b 100644 --- a/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflaterBufferSizeTest.java +++ b/jetty-websocket/websocket-core-tests/src/test/java/org/eclipse/jetty/websocket/core/extensions/PerMessageDeflaterBufferSizeTest.java @@ -20,6 +20,7 @@ package org.eclipse.jetty.websocket.core.extensions; import java.io.IOException; import java.net.URI; +import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -28,10 +29,13 @@ import org.eclipse.jetty.client.HttpRequest; import org.eclipse.jetty.client.HttpResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.websocket.core.CoreSession; import org.eclipse.jetty.websocket.core.ExtensionConfig; import org.eclipse.jetty.websocket.core.Frame; import org.eclipse.jetty.websocket.core.FrameHandler; +import org.eclipse.jetty.websocket.core.OpCode; import org.eclipse.jetty.websocket.core.TestFrameHandler; import org.eclipse.jetty.websocket.core.WebSocketServer; import org.eclipse.jetty.websocket.core.client.CoreClientUpgradeRequest; @@ -291,7 +295,8 @@ public class PerMessageDeflaterBufferSizeTest // We should now only be able to send this message in multiple frames as it exceeds deflate_buffer_size. String message = "0123456789"; - clientHandler.sendText(message); + ByteBuffer payload = toBuffer(message, true); + clientHandler.getCoreSession().sendFrame(new Frame(OpCode.TEXT, payload), Callback.NOOP, false); // Verify the frame has been fragmented into multiple parts. int numFrames = 0; @@ -315,4 +320,18 @@ public class PerMessageDeflaterBufferSizeTest assertNull(serverHandler.getError()); assertNull(clientHandler.getError()); } + + public ByteBuffer toBuffer(String string, boolean direct) + { + ByteBuffer buffer = BufferUtil.allocate(string.length(), direct); + BufferUtil.clearToFill(buffer); + BufferUtil.put(BufferUtil.toBuffer(string), buffer); + BufferUtil.flipToFlush(buffer, 0); + + // Sanity checks. + assertThat(buffer.hasArray(), is(!direct)); + assertThat(BufferUtil.toString(buffer), is(string)); + + return buffer; + } } 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 986106295e3..ccf98c85030 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 @@ -14,6 +14,10 @@ wsuf-test org.eclipse.jetty.websocket.util.server.WebSocketUpgradeFilter + + jetty.websocket.WebSocketMapping + jetty.websocket.defaultMapping + 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 759e773bbd1..1729cd545f0 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 @@ -91,6 +91,7 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements private final ServletContextHandler contextHandler; private final WebSocketMapping webSocketMapping; + private final WebSocketComponents components; private final FrameHandlerFactory frameHandlerFactory; private final Executor executor; private final Configuration.ConfigurationCustomizer customizer = new Configuration.ConfigurationCustomizer(); @@ -102,14 +103,15 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements * Main entry point for {@link JettyWebSocketServletContainerInitializer}. * * @param webSocketMapping the {@link WebSocketMapping} that this container belongs to - * @param webSocketComponents the {@link WebSocketComponents} instance to use + * @param components the {@link WebSocketComponents} instance to use * @param executor the {@link Executor} to use */ - JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents webSocketComponents, Executor executor) + JettyWebSocketServerContainer(ServletContextHandler contextHandler, WebSocketMapping webSocketMapping, WebSocketComponents components, Executor executor) { this.contextHandler = contextHandler; this.webSocketMapping = webSocketMapping; this.executor = executor; + this.components = components; // Ensure there is a FrameHandlerFactory JettyServerFrameHandlerFactory factory = contextHandler.getBean(JettyServerFrameHandlerFactory.class); @@ -155,6 +157,11 @@ public class JettyWebSocketServerContainer extends ContainerLifeCycle implements }); } + public WebSocketComponents getWebSocketComponents() + { + return components; + } + @Override public Executor getExecutor() { 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 622d1f19f4d..bbe0e33bda9 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 @@ -92,11 +92,9 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain private static JettyWebSocketServerContainer initialize(ServletContextHandler context) { WebSocketComponents components = WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), context.getServletContext()); - WebSocketMapping mapping = WebSocketMapping.ensureMapping(context.getServletContext(), WebSocketMapping.DEFAULT_KEY); JettyWebSocketServerContainer container = JettyWebSocketServerContainer.ensureContainer(context.getServletContext()); - if (LOG.isDebugEnabled()) - LOG.debug("configureContext {} {} {}", container, mapping, components); + LOG.debug("initialize {} {}", container, components); return container; } @@ -105,6 +103,8 @@ public class JettyWebSocketServletContainerInitializer implements ServletContain public void onStartup(Set> c, ServletContext context) { ServletContextHandler contextHandler = ServletContextHandler.getServletContextHandler(context, "Jetty WebSocket SCI"); - JettyWebSocketServletContainerInitializer.initialize(contextHandler); + JettyWebSocketServerContainer container = JettyWebSocketServletContainerInitializer.initialize(contextHandler); + if (LOG.isDebugEnabled()) + LOG.debug("onStartup {}", container); } } 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 86dc8240721..b4a850bb5ca 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 @@ -22,11 +22,9 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import javax.servlet.DispatcherType; import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.server.Server; @@ -39,7 +37,6 @@ 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.util.server.WebSocketUpgradeFilter; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,22 +51,20 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class ErrorCloseTest { - private Server server = new Server(); - private WebSocketClient client = new WebSocketClient(); - private ThrowingSocket serverSocket = new ThrowingSocket(); - private CountDownLatch serverCloseListener = new CountDownLatch(1); - private ServerConnector connector; + private final Server server = new Server(); + private final WebSocketClient client = new WebSocketClient(); + private final ThrowingSocket serverSocket = new ThrowingSocket(); + private final CountDownLatch serverCloseListener = new CountDownLatch(1); private URI serverUri; @BeforeEach public void start() throws Exception { - connector = new ServerConnector(server); + ServerConnector connector = new ServerConnector(server); server.addConnector(connector); ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); contextHandler.setContextPath("/"); - contextHandler.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); JettyWebSocketServletContainerInitializer.configure(contextHandler, (context, container) -> { container.addMapping("/", (req, resp) -> serverSocket); @@ -140,7 +135,7 @@ public class ErrorCloseTest serverSocket.methodsToThrow.add("onOpen"); EventSocket clientSocket = new EventSocket(); - try (StacklessLogging stacklessLogging = new StacklessLogging(WebSocketCoreSession.class)) + try (StacklessLogging ignored = new StacklessLogging(WebSocketCoreSession.class)) { client.connect(clientSocket, serverUri).get(5, TimeUnit.SECONDS); assertTrue(serverSocket.closeLatch.await(5, TimeUnit.SECONDS)); 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 787c2c83261..72e0af5d204 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 @@ -25,7 +25,6 @@ import java.net.ServerSocket; import java.net.SocketTimeoutException; import java.net.URI; import java.time.Duration; -import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -33,7 +32,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import javax.servlet.DispatcherType; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -49,7 +47,6 @@ 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; @@ -73,7 +70,7 @@ public class ClientConnectTest { private Server server; private WebSocketClient client; - private CountDownLatch serverLatch = new CountDownLatch(1); + private final CountDownLatch serverLatch = new CountDownLatch(1); @SuppressWarnings("unchecked") private E assertExpectedError(ExecutionException e, CloseTrackingEndpoint wsocket, Matcher errorMatcher) @@ -143,8 +140,6 @@ public class ClientConnectTest }); }); - context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); - context.addServlet(new ServletHolder(new SimpleStatusServlet(404)), "/bogus"); context.addServlet(new ServletHolder(new SimpleStatusServlet(200)), "/a-okay"); context.addServlet(new ServletHolder(new InvalidUpgradeServlet()), "/invalid-upgrade/*"); 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 3725abbb25b..ee783272084 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 @@ -25,12 +25,10 @@ import java.nio.ByteBuffer; import java.time.Duration; import java.util.Arrays; import java.util.Collection; -import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import javax.servlet.DispatcherType; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -50,7 +48,6 @@ 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; @@ -104,10 +101,7 @@ public class WebSocketClientTest configuration.addMapping("/get-params", (req, resp) -> new ParamsEndpoint()); }); - context.addFilter(WebSocketUpgradeFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); - server.setHandler(context); - server.start(); } diff --git a/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java index d3cf9e1391b..176544be380 100644 --- a/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/WebSocketUpgradeFilter.java @@ -40,7 +40,6 @@ import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.thread.AutoLock; import org.eclipse.jetty.websocket.core.Configuration; -import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; import org.eclipse.jetty.websocket.util.server.internal.WebSocketMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,34 +77,40 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable private static final Logger LOG = LoggerFactory.getLogger(WebSocketUpgradeFilter.class); private static final AutoLock LOCK = new AutoLock(); + /** + * The init parameter name used to define {@link ServletContext} attribute used to share the {@link WebSocketMapping}. + */ + public static final String MAPPING_ATTRIBUTE_INIT_PARAM = "jetty.websocket.WebSocketMapping"; + + /** + * Return any {@link WebSocketUpgradeFilter} already present on the {@link ServletContext}. + * + * @param servletContext the {@link ServletContext} to use. + * @return the configured default {@link WebSocketUpgradeFilter} instance. + */ private static FilterHolder getFilter(ServletContext servletContext) { ContextHandler contextHandler = Objects.requireNonNull(ContextHandler.getContextHandler(servletContext)); ServletHandler servletHandler = contextHandler.getChildHandlerByClass(ServletHandler.class); - for (FilterHolder holder : servletHandler.getFilters()) { if (holder.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM) != null) return holder; } - return null; } /** - * Configure the default WebSocketUpgradeFilter. - * - *

- * This will return the default {@link WebSocketUpgradeFilter} on the - * provided {@link ServletContext}, creating the filter if necessary. + * Ensure a {@link WebSocketUpgradeFilter} is available on the provided {@link ServletContext}, + * a new filter will added if one does not already exist. *

*

* The default {@link WebSocketUpgradeFilter} is also available via * the {@link ServletContext} attribute named {@code org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter} *

* - * @param servletContext the {@link ServletContext} to use - * @return the configured default {@link WebSocketUpgradeFilter} instance + * @param servletContext the {@link ServletContext} to use. + * @return the configured default {@link WebSocketUpgradeFilter} instance. */ public static FilterHolder ensureFilter(ServletContext servletContext) { @@ -132,8 +137,6 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable } } - 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; @@ -174,10 +177,9 @@ public class WebSocketUpgradeFilter implements Filter, Dumpable final ServletContext context = config.getServletContext(); String mappingKey = config.getInitParameter(MAPPING_ATTRIBUTE_INIT_PARAM); - if (mappingKey != null) - mapping = WebSocketMapping.ensureMapping(context, mappingKey); - else - mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(context)); + if (mappingKey == null) + throw new ServletException("the WebSocketMapping init param must be set"); + mapping = WebSocketMapping.ensureMapping(context, mappingKey); String max = config.getInitParameter("idleTimeout"); if (max == null) diff --git a/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java index 5bef84d466f..e93c0c4c019 100644 --- a/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java +++ b/jetty-websocket/websocket-util-server/src/main/java/org/eclipse/jetty/websocket/util/server/internal/WebSocketMapping.java @@ -63,7 +63,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener public static WebSocketMapping getMapping(ServletContext servletContext, String mappingKey) { Object mappingObject = servletContext.getAttribute(mappingKey); - if (mappingObject != null) { if (mappingObject instanceof WebSocketMapping) @@ -86,7 +85,6 @@ public class WebSocketMapping implements Dumpable, LifeCycle.Listener public static WebSocketMapping ensureMapping(ServletContext servletContext, String mappingKey) { WebSocketMapping mapping = getMapping(servletContext, mappingKey); - if (mapping == null) { mapping = new WebSocketMapping(WebSocketServerComponents.getWebSocketComponents(servletContext)); @@ -135,7 +133,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.util.server.internal.WebSocketMapping"; + public static final String DEFAULT_KEY = "jetty.websocket.defaultMapping"; private final PathMappings mappings = new PathMappings<>(); private final WebSocketComponents components; diff --git a/pom.xml b/pom.xml index 257f18db06d..0834647a047 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,9 @@ 1.1.2 9.0.29 9.4.8.Final + 4.3.4.Final + 2.8.6 + 2.0.10 4.0.1 2.5.1 9.0 @@ -448,7 +451,7 @@ org.apache.maven.plugins maven-antrun-plugin - 1.8 + 3.0.0 org.apache.maven.plugins 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 f2239a533b9..098cf5bd729 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 @@ -36,6 +36,8 @@ import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPOutputStream; import javax.servlet.AsyncContext; import javax.servlet.DispatcherType; import javax.servlet.ReadListener; @@ -71,8 +73,11 @@ import org.eclipse.jetty.server.HttpInput.Content; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.server.handler.gzip.GzipHttpInputInterceptor; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.compression.CompressionPool; +import org.eclipse.jetty.util.compression.InflaterPool; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Disabled; @@ -87,6 +92,7 @@ import static org.eclipse.jetty.http.client.Transport.HTTP; import static org.eclipse.jetty.util.BufferUtil.toArray; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -776,10 +782,18 @@ public class AsyncIOServletTest extends AbstractTest= 0) + { + // System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b)); + response.getOutputStream().write(b); + } + else + return; + } + } + + @Override + public void onAllDataRead() throws IOException + { + asyncContext.complete(); + } + + @Override + public void onError(Throwable x) + { + } + }); + } + }); + + AsyncRequestContent contentProvider = new AsyncRequestContent(); + CountDownLatch clientLatch = new CountDownLatch(1); + + AtomicReference resultRef = new AtomicReference<>(); + scenario.client.newRequest(scenario.newURI()) + .method(HttpMethod.POST) + .path(scenario.servletPath) + .body(contentProvider) + .send(new BufferingResponseListener(16 * 1024 * 1024) + { + @Override + public void onComplete(Result result) + { + resultRef.set(result); + clientLatch.countDown(); + } + }); + + for (int i = 0; i < 1_000_000; i++) + { + contentProvider.offer(BufferUtil.toBuffer("S" + i)); + } + contentProvider.close(); + + assertTrue(clientLatch.await(30, TimeUnit.SECONDS)); + assertThat(resultRef.get().isSucceeded(), Matchers.is(true)); + assertThat(resultRef.get().getResponse().getStatus(), Matchers.equalTo(HttpStatus.OK_200)); + } + + @ParameterizedTest + @ArgumentsSource(TransportProvider.class) + public void testAsyncInterceptedTwice(Transport transport) throws Exception + { + init(transport); + scenario.start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + System.err.println("Service " + request); + + final HttpInput httpInput = ((Request)request).getHttpInput(); + httpInput.addInterceptor(new GzipHttpInputInterceptor(new InflaterPool(-1, true), ((Request)request).getHttpChannel().getByteBufferPool(), 1024)); + httpInput.addInterceptor(content -> + { + ByteBuffer byteBuffer = content.getByteBuffer(); + byte[] bytes = new byte[2]; + bytes[1] = byteBuffer.get(); + bytes[0] = byteBuffer.get(); + return new Content(wrap(bytes)); + }); + + AsyncContext asyncContext = request.startAsync(); + ServletInputStream input = request.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + input.setReadListener(new ReadListener() + { + @Override + public void onDataAvailable() throws IOException + { + while (input.isReady()) + { + int b = input.read(); + if (b > 0) + { + // System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b)); + out.write(b); + } + else if (b < 0) + return; + } + } + + @Override + public void onAllDataRead() throws IOException + { + response.getOutputStream().write(out.toByteArray()); + asyncContext.complete(); + } + + @Override + public void onError(Throwable x) + { + } + }); + } + }); + + AsyncRequestContent contentProvider = new AsyncRequestContent(); + CountDownLatch clientLatch = new CountDownLatch(1); + + String expected = + "0S" + + "1S" + + "2S" + + "3S" + + "4S" + + "5S" + + "6S"; + + scenario.client.newRequest(scenario.newURI()) + .method(HttpMethod.POST) + .path(scenario.servletPath) + .body(contentProvider) + .send(new BufferingResponseListener() + { + @Override + public void onComplete(Result result) + { + if (result.isSucceeded()) + { + Response response = result.getResponse(); + assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); + assertThat(getContentAsString(), Matchers.equalTo(expected)); + clientLatch.countDown(); + } + } + }); + + for (int i = 0; i < 7; i++) + { + contentProvider.offer(gzipToBuffer("S" + i)); + contentProvider.flush(); + } + contentProvider.close(); + + assertTrue(clientLatch.await(10, TimeUnit.SECONDS)); + } + + @ParameterizedTest + @ArgumentsSource(TransportProvider.class) + public void testAsyncInterceptedTwiceWithNulls(Transport transport) throws Exception + { + init(transport); + scenario.start(new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException + { + System.err.println("Service " + request); + + final HttpInput httpInput = ((Request)request).getHttpInput(); + httpInput.addInterceptor(content -> + { + if (content.isEmpty()) + return content; + + // skip contents with odd numbers + ByteBuffer duplicate = content.getByteBuffer().duplicate(); + duplicate.get(); + byte integer = duplicate.get(); + int idx = Character.getNumericValue(integer); + Content contentCopy = new Content(content.getByteBuffer().duplicate()); + content.skip(content.remaining()); + if (idx % 2 == 0) + return contentCopy; + return null; + }); + httpInput.addInterceptor(content -> + { + if (content.isEmpty()) + return content; + + // reverse the bytes + ByteBuffer byteBuffer = content.getByteBuffer(); + byte[] bytes = new byte[2]; + bytes[1] = byteBuffer.get(); + bytes[0] = byteBuffer.get(); + return new Content(wrap(bytes)); + }); + + AsyncContext asyncContext = request.startAsync(); + ServletInputStream input = request.getInputStream(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + input.setReadListener(new ReadListener() + { + @Override + public void onDataAvailable() throws IOException + { + while (input.isReady()) + { + int b = input.read(); + if (b > 0) + { + // System.err.printf("0x%2x %s %n", b, Character.isISOControl(b)?"?":(""+(char)b)); + out.write(b); + } + else if (b < 0) + return; + } + } + + @Override + public void onAllDataRead() throws IOException + { + response.getOutputStream().write(out.toByteArray()); + asyncContext.complete(); + } + + @Override + public void onError(Throwable x) + { + } + }); + } + }); + + AsyncRequestContent contentProvider = new AsyncRequestContent(); + CountDownLatch clientLatch = new CountDownLatch(1); + + String expected = + "0S" + + "2S" + + "4S" + + "6S"; + + scenario.client.newRequest(scenario.newURI()) + .method(HttpMethod.POST) + .path(scenario.servletPath) + .body(contentProvider) + .send(new BufferingResponseListener() + { + @Override + public void onComplete(Result result) + { + if (result.isSucceeded()) + { + Response response = result.getResponse(); + assertThat(response.getStatus(), Matchers.equalTo(HttpStatus.OK_200)); + assertThat(getContentAsString(), Matchers.equalTo(expected)); + clientLatch.countDown(); + } + } + }); + + contentProvider.offer(BufferUtil.toBuffer("S0")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S1")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S2")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S3")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S4")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S5")); + contentProvider.flush(); + contentProvider.offer(BufferUtil.toBuffer("S6")); + contentProvider.close(); + + assertTrue(clientLatch.await(10, TimeUnit.SECONDS)); + } + + private ByteBuffer gzipToBuffer(String s) throws IOException + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = new GZIPOutputStream(baos); + gzos.write(s.getBytes(StandardCharsets.ISO_8859_1)); + gzos.close(); + return BufferUtil.toBuffer(baos.toByteArray()); + } + @ParameterizedTest @ArgumentsSource(TransportProvider.class) public void testWriteListenerFromOtherThread(Transport transport) throws Exception @@ -1387,18 +1710,21 @@ public class AsyncIOServletTest extends AbstractTest org.infinispan.protostream protostream - 4.2.2.Final + ${infinispan.protostream.version} + test +
+ + com.google.code.gson + gson + ${gson.version} test diff --git a/tests/test-sessions/test-jdbc-sessions/pom.xml b/tests/test-sessions/test-jdbc-sessions/pom.xml index bc5ac917757..835edb1ff54 100644 --- a/tests/test-sessions/test-jdbc-sessions/pom.xml +++ b/tests/test-sessions/test-jdbc-sessions/pom.xml @@ -92,7 +92,8 @@ org.mariadb.jdbc mariadb-java-client - 2.6.2 + 2.7.0 + test