diff --git a/Jenkinsfile b/Jenkinsfile index c241984244f..bfbf28a8a79 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -89,7 +89,6 @@ pipeline { } } - def slackNotif() { script { try { @@ -108,7 +107,6 @@ def slackNotif() { } } - /** * To other developers, if you are using this method above, please use the following syntax. * @@ -135,4 +133,5 @@ def mavenBuild(jdk, cmdline, mvnName, junitPublishDisabled) { } } + // vim: et:ts=2:sw=2:ft=groovy diff --git a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java index d3ac32dafbd..473fae972ab 100644 --- a/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java +++ b/examples/embedded/src/main/java/org/eclipse/jetty/embedded/WebSocketJsrServer.java @@ -26,7 +26,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.websocket.javax.server.JavaxWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer; /** * Example of setting up a javax.websocket server with Jetty embedded diff --git a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java index b3975b1c4cb..3ff372e22ee 100644 --- a/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java +++ b/jetty-annotations/src/test/java/org/eclipse/jetty/annotations/TestSecurityAnnotationConversions.java @@ -173,7 +173,7 @@ public class TestSecurityAnnotationConversions public void testMethodAnnotation() throws Exception { //ServletSecurity annotation with HttpConstraint of TransportGuarantee.CONFIDENTIAL, and a list of rolesAllowed, and - //a HttpMethodConstraint for GET method that permits all and has TransportGuarantee.NONE (ie is default) + //an HttpMethodConstraint for GET method that permits all and has TransportGuarantee.NONE (ie is default) WebAppContext wac = makeWebAppContext(Method1Servlet.class.getCanonicalName(), "method1Servlet", new String[]{ "/foo/*", "*.foo" diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java index 248403bba03..d1648eb70d0 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpClientTransport.java @@ -28,7 +28,7 @@ import org.eclipse.jetty.io.ClientConnectionFactory; * in order to plug-in a different transport for {@link HttpClient}. *
* While the {@link HttpClient} APIs define the HTTP semantic (request, response, headers, etc.) - * how a HTTP exchange is carried over the network depends on implementations of this class. + * how an HTTP exchange is carried over the network depends on implementations of this class. *
* The default implementation uses the HTTP protocol to carry over the network the HTTP exchange, * but the HTTP exchange may also be carried using the FCGI protocol, the HTTP/2 protocol or, diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java index efb5e983e4d..adb02176f0e 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpContent.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.log.Logger; /** * {@link HttpContent} is a stateful, linear representation of the request content provided * by a {@link ContentProvider} that can be traversed one-way to obtain content buffers to - * send to a HTTP server. + * send to an HTTP server. *
* {@link HttpContent} offers the notion of a one-way cursor to traverse the content. * The cursor starts in a virtual "before" position and can be advanced using {@link #advance()} diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java index daf6a32dca0..1f073351179 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/HttpReceiver.java @@ -50,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger; *
{@link Request} represents a HTTP request, and offers a fluent interface to customize + *
{@link Request} represents an HTTP request, and offers a fluent interface to customize * various attributes such as the path, the headers, the content, etc.
*You can create {@link Request} objects via {@link HttpClient#newRequest(String)} and * you can send them using either {@link #send()} for a blocking semantic, or diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java index 95605aad404..07e4d9b9a35 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Response.java @@ -29,7 +29,7 @@ import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.util.Callback; /** - *
{@link Response} represents a HTTP response and offers methods to retrieve status code, HTTP version + *
{@link Response} represents an HTTP response and offers methods to retrieve status code, HTTP version * and headers.
*{@link Response} objects are passed as parameters to {@link Response.Listener} callbacks, or as * future result of {@link Request#send()}.
diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java index 9febba733de..d0a153c6c6f 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpReceiverOverHTTP.java @@ -161,7 +161,7 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res } /** - * Parses a HTTP response in the receivers buffer. + * Parses an HTTP response in the receivers buffer. * * @return true to indicate that parsing should be interrupted (and will be resumed by another thread). */ diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java index de20a500547..d04acf7dea1 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpConnectionLifecycleTest.java @@ -485,7 +485,7 @@ public class HttpConnectionLifecycleTest extends AbstractHttpClientServerTest ContentResponse response = request .onResponseBegin(response1 -> { - // Simulate a HTTP 1.0 response has been received. + // Simulate an HTTP 1.0 response has been received. ((HttpResponse)response1).version(HttpVersion.HTTP_1_0); }) .send(); diff --git a/jetty-deploy/src/test/resources/jetty-http.xml b/jetty-deploy/src/test/resources/jetty-http.xml index 9526b56890a..137da33293f 100644 --- a/jetty-deploy/src/test/resources/jetty-http.xml +++ b/jetty-deploy/src/test/resources/jetty-http.xml @@ -3,13 +3,13 @@ - +- * This servlet accepts a HTTP request and transforms it into a FastCGI request + * This servlet accepts an HTTP request and transforms it into a FastCGI request * that is sent to the FastCGI server specified in the {@code proxyTo} * init-param. *
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java
index a5a4ac64952..d0ca77ba9d4 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/CookieCutter.java
@@ -64,15 +64,14 @@ public abstract class CookieCutter
boolean inQuoted = false;
boolean quoted = false;
boolean escaped = false;
+ boolean reject = false;
int tokenstart = -1;
int tokenend = -1;
for (int i = 0, length = hdr.length(); i <= length; i++)
{
char c = i == length ? 0 : hdr.charAt(i);
- // System.err.printf("i=%d/%d c=%s v=%b q=%b/%b e=%b u=%s s=%d e=%d \t%s=%s%n" ,i,length,c==0?"|":(""+c),invalue,inQuoted,quoted,escaped,unquoted,tokenstart,tokenend,name,value);
-
- // Handle quoted values for name or value
+ // Handle quoted values for value
if (inQuoted)
{
if (escaped)
@@ -119,7 +118,7 @@ public abstract class CookieCutter
// Handle name and value state machines
if (invalue)
{
- // parse the value
+ // parse the cookie-value
switch (c)
{
case ' ':
@@ -193,7 +192,11 @@ public abstract class CookieCutter
// This is a new cookie, so add the completed last cookie if we have one
if (cookieName != null)
{
- addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment);
+ if (!reject)
+ {
+ addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment);
+ reject = false;
+ }
cookieDomain = null;
cookiePath = null;
cookieComment = null;
@@ -234,6 +237,15 @@ public abstract class CookieCutter
quoted = false;
continue;
}
+
+ if (_complianceMode == CookieCompliance.RFC6265)
+ {
+ if (isRFC6265RejectedCharacter(inQuoted, c))
+ {
+ reject = true;
+ }
+ }
+
if (tokenstart < 0)
tokenstart = i;
tokenend = i;
@@ -242,13 +254,26 @@ public abstract class CookieCutter
}
else
{
- // parse the name
+ // parse the cookie-name
switch (c)
{
+ case 0:
case ' ':
case '\t':
continue;
+ case '"':
+ // Quoted name is not allowed in any version of the Cookie spec
+ reject = true;
+ break;
+
+ case ';':
+ // a cookie terminated with no '=' sign.
+ tokenstart = -1;
+ invalue = false;
+ reject = false;
+ continue;
+
case '=':
if (quoted)
{
@@ -272,6 +297,15 @@ public abstract class CookieCutter
quoted = false;
continue;
}
+
+ if (_complianceMode == CookieCompliance.RFC6265)
+ {
+ if (isRFC6265RejectedCharacter(inQuoted, c))
+ {
+ reject = true;
+ }
+ }
+
if (tokenstart < 0)
tokenstart = i;
tokenend = i;
@@ -281,7 +315,7 @@ public abstract class CookieCutter
}
}
- if (cookieName != null)
+ if (cookieName != null && !reject)
addCookie(cookieName, cookieValue, cookieDomain, cookiePath, cookieVersion, cookieComment);
}
}
@@ -295,4 +329,31 @@ public abstract class CookieCutter
}
protected abstract void addCookie(String cookieName, String cookieValue, String cookieDomain, String cookiePath, int cookieVersion, String cookieComment);
+
+ protected boolean isRFC6265RejectedCharacter(boolean inQuoted, char c)
+ {
+ if (inQuoted)
+ {
+ // We only reject if a Control Character is encountered
+ if (Character.isISOControl(c))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ /* From RFC6265 - Section 4.1.1 - Syntax
+ * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
+ * ; US-ASCII characters excluding CTLs,
+ * ; whitespace DQUOTE, comma, semicolon,
+ * ; and backslash
+ */
+ return Character.isISOControl(c) || // control characters
+ c > 127 || // 8-bit characters
+ c == ',' || // comma
+ c == ';'; // semicolon
+ }
+
+ return false;
+ }
}
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java
index 2379bcef99e..4b6128182fc 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HostPortHttpField.java
@@ -21,7 +21,7 @@ package org.eclipse.jetty.http;
import org.eclipse.jetty.util.HostPort;
/**
- * A HttpField holding a preparsed Host and port number
+ * An HttpField holding a preparsed Host and port number
*
* @see HostPort
*/
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java
index e69af07893f..21425329ded 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpField.java
@@ -23,7 +23,7 @@ import java.util.Objects;
import org.eclipse.jetty.util.StringUtil;
/**
- * A HTTP Field
+ * An HTTP Field
*/
public class HttpField
{
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
index 741be954df5..016f3dcbbee 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpMethod.java
@@ -48,7 +48,7 @@ public enum HttpMethod
* @param bytes Array containing ISO-8859-1 characters
* @param position The first valid index
* @param limit The first non valid index
- * @return A HttpMethod if a match or null if no easy match.
+ * @return An HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(byte[] bytes, final int position, int limit)
{
@@ -110,7 +110,7 @@ public enum HttpMethod
* Optimized lookup to find a method name and trailing space in a byte array.
*
* @param buffer buffer containing ISO-8859-1 characters, it is not modified.
- * @return A HttpMethod if a match or null if no easy match.
+ * @return An HttpMethod if a match or null if no easy match.
*/
public static HttpMethod lookAheadGet(ByteBuffer buffer)
{
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
index 26ef2ca2337..1093feae375 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
@@ -442,7 +442,7 @@ public class HttpParser
return t;
}
- /* Quick lookahead for the start state looking for a request method or a HTTP version,
+ /* Quick lookahead for the start state looking for a request method or an HTTP version,
* otherwise skip white space until something else to parse.
*/
private boolean quickStart(ByteBuffer buffer)
@@ -1834,14 +1834,14 @@ public class HttpParser
boolean messageComplete();
/**
- * This is the method called by parser when a HTTP Header name and value is found
+ * This is the method called by parser when an HTTP Header name and value is found
*
* @param field The field parsed
*/
void parsedHeader(HttpField field);
/**
- * This is the method called by parser when a HTTP Trailer name and value is found
+ * This is the method called by parser when an HTTP Trailer name and value is found
*
* @param field The field parsed
*/
@@ -1851,7 +1851,7 @@ public class HttpParser
/**
* Called to signal that an EOF was received unexpectedly
- * during the parsing of a HTTP message
+ * during the parsing of an HTTP message
*/
void earlyEOF();
diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
index 58d2a98f4bd..e48b37d84fc 100644
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java
@@ -314,6 +314,20 @@ public class HttpStatus
}
}
+ public static boolean hasNoBody(int status)
+ {
+ switch (status)
+ {
+ case NO_CONTENT_204:
+ case NOT_MODIFIED_304:
+ case PARTIAL_CONTENT_206:
+ return true;
+
+ default:
+ return status < OK_200;
+ }
+ }
+
/**
* Simple test against an code to determine if it falls into the
* A HttpField that will be cached and used many times can be created as
+ * An HttpField that will be cached and used many times can be created as
* a {@link PreEncodedHttpField}, which will use the {@link HttpFieldPreEncoder}
* instances discovered by the {@link ServiceLoader} to pre-encode the header
* for each version of HTTP in use. This will save garbage
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java
index e5bf0672a16..d60c7b9c46c 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterLenientTest.java
@@ -40,6 +40,7 @@ public class CookieCutterLenientTest
{
return Stream.of(
// Simple test to verify behavior
+ Arguments.of("x=y", "x", "y"),
Arguments.of("key=value", "key", "value"),
// Tests that conform to RFC2109
@@ -62,12 +63,17 @@ public class CookieCutterLenientTest
// quoted-string = ( <"> *(qdtext) <"> )
// qdtext = Informational
message category as defined in the http://user@host:port/path/info;param?query#fragment
* this class will split it into the following undecoded optional elements:
*
=&{()}", "rToken", "F_TOKEN''!--\"=&{()}"),
// Commas that were not commas
diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java
index 22516a5c024..202d7a961da 100644
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/CookieCutterTest.java
@@ -212,6 +212,34 @@ public class CookieCutterTest
assertThat("Cookies.length", cookies.length, is(0));
}
+ @Test
+ public void testMultipleCookies()
+ {
+ String rawCookie = "testcookie; server.id=abcd; server.detail=cfg";
+
+ // The first cookie "testcookie" should be ignored, per RFC6265, as it's missing the "=" sign.
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "server.id", "abcd", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "server.detail", "cfg", 0, null);
+ }
+
+ @Test
+ public void testExcessiveSemicolons()
+ {
+ char[] excessive = new char[65535];
+ Arrays.fill(excessive, ';');
+ String rawCookie = "foo=bar; " + excessive + "; xyz=pdq";
+
+ Cookie[] cookies = parseCookieHeaders(CookieCompliance.RFC6265, rawCookie);
+
+ assertThat("Cookies.length", cookies.length, is(2));
+ assertCookie("Cookies[0]", cookies[0], "foo", "bar", 0, null);
+ assertCookie("Cookies[1]", cookies[1], "xyz", "pdq", 0, null);
+ }
+
static class Cookie
{
String name;
@@ -282,6 +310,4 @@ public class CookieCutterTest
super.parseFields(Arrays.asList(fields));
}
}
-
- ;
}
diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java
index a5a57fa2666..7eff31981e2 100644
--- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java
+++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/BufferingFlowControlStrategy.java
@@ -170,7 +170,7 @@ public class BufferingFlowControlStrategy extends AbstractFlowControlStrategy
// and here we keep track of its max value.
// Updating the max session recv window is done here
- // so that if a peer decides to send an unilateral
+ // so that if a peer decides to send a unilateral
// window update to enlarge the session window,
// without the corresponding data consumption, here
// we can track it.
diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java
index dfe91e6c7c9..a164baf8b14 100644
--- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java
+++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ErrorCode.java
@@ -40,7 +40,7 @@ public enum ErrorCode
*/
INTERNAL_ERROR(2),
/**
- * Indicates a HTTP/2 flow control violation.
+ * Indicates an HTTP/2 flow control violation.
*/
FLOW_CONTROL_ERROR(3),
/**
@@ -68,7 +68,7 @@ public enum ErrorCode
*/
COMPRESSION_ERROR(9),
/**
- * Indicates that the connection established by a HTTP CONNECT was abnormally closed.
+ * Indicates that the connection established by an HTTP CONNECT was abnormally closed.
*/
HTTP_CONNECT_ERROR(10),
/**
diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java
index 4e4d483009f..d11beb1e6af 100644
--- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java
+++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/ISession.java
@@ -30,7 +30,7 @@ import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
/**
- *
The SPI interface for implementing a HTTP/2 session.
+ *The SPI interface for implementing an HTTP/2 session.
*This class extends {@link Session} by adding the methods required to * implement the HTTP/2 session functionalities.
*/ 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 cca02086afb..b59d09c3238 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 @@ -25,7 +25,7 @@ import org.eclipse.jetty.http2.frames.Frame; import org.eclipse.jetty.util.Callback; /** - *The SPI interface for implementing a HTTP/2 stream.
+ *The SPI interface for implementing an HTTP/2 stream.
*This class extends {@link Stream} by adding the methods required to * implement the HTTP/2 stream functionalities.
*/ diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java index 91a492ea1b3..d5d63a95477 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/api/Session.java @@ -33,7 +33,7 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Promise; /** - *A {@link Session} represents the client-side endpoint of a HTTP/2 connection to a single origin server.
+ *A {@link Session} represents the client-side endpoint of an HTTP/2 connection to a single origin server.
*Once a {@link Session} has been obtained, it can be used to open HTTP/2 streams:
** Session session = ...; @@ -140,7 +140,7 @@ public interface Session /** *A {@link Listener} is the passive counterpart of a {@link Session} and - * receives events happening on a HTTP/2 connection.
+ * receives events happening on an HTTP/2 connection. * * @see Session */ @@ -164,9 +164,9 @@ public interface Session /** *Callback method invoked when a new stream is being created upon - * receiving a HEADERS frame representing a HTTP request.
+ * receiving a HEADERS frame representing an HTTP request. *Applications should implement this method to process HTTP requests, - * typically providing a HTTP response via + * typically providing an HTTP response via * {@link Stream#headers(HeadersFrame, Callback)}.
*Applications can detect whether request DATA frames will be arriving * by testing {@link HeadersFrame#isEndStream()}. If the application is 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 4725c2198c5..fe49ffb4e00 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 @@ -29,8 +29,8 @@ import org.eclipse.jetty.util.Promise; *
A {@link Stream} represents a bidirectional exchange of data on top of a {@link Session}.
*Differently from socket streams, where the input and output streams are permanently associated * with the socket (and hence with the connection that the socket represents), there can be multiple - * HTTP/2 streams present concurrently for a HTTP/2 session.
- *A {@link Stream} maps to a HTTP request/response cycle, and after the request/response cycle is + * HTTP/2 streams present concurrently for an HTTP/2 session.
+ *A {@link Stream} maps to an HTTP request/response cycle, and after the request/response cycle is * completed, the stream is closed and removed from the session.
*Like {@link Session}, {@link Stream} is the active part and by calling its API applications * can generate events on the stream; conversely, {@link Stream.Listener} is the passive part, and @@ -51,7 +51,7 @@ public interface Stream public Session getSession(); /** - *
Sends the given HEADERS {@code frame} representing a HTTP response.
+ *Sends the given HEADERS {@code frame} representing an HTTP response.
* * @param frame the HEADERS frame to send * @param callback the callback that gets notified when the frame has been sent @@ -139,7 +139,7 @@ public interface Stream /** *A {@link Stream.Listener} is the passive counterpart of a {@link Stream} and receives - * events happening on a HTTP/2 stream.
+ * events happening on an HTTP/2 stream. *HTTP/2 data is flow controlled - this means that only a finite number of data events * are delivered, until the flow control window is exhausted.
*Applications control the delivery of data events by requesting them via @@ -147,7 +147,7 @@ public interface Stream * events must be explicitly demanded.
*Applications control the HTTP/2 flow control by completing the callback associated * with data events - this allows the implementation to recycle the data buffer and - * eventually enlarges the flow control window so that the sender can send more data.
+ * eventually to enlarge the flow control window so that the sender can send more data. * * @see Stream */ @@ -182,8 +182,6 @@ public interface Stream /** *Callback method invoked when a DATA frame has been received.
- *When this method is called, the {@link #demand(long) demand} has - * already been incremented by 1.
* * @param stream the stream * @param frame the DATA frame received @@ -203,8 +201,8 @@ public interface Stream */ public default void onDataDemanded(Stream stream, DataFrame frame, Callback callback) { - stream.demand(1); onData(stream, frame, callback); + stream.demand(1); } /** diff --git a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java index 3a6058878a4..741bba433a7 100644 --- a/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java +++ b/jetty-http2/http2-common/src/main/java/org/eclipse/jetty/http2/parser/PrefaceParser.java @@ -42,7 +42,7 @@ public class PrefaceParser *Advances this parser after the {@link PrefaceFrame#PREFACE_PREAMBLE_BYTES}.
*This allows the HTTP/1.1 parser to parse the preamble of the preface, * which is a legal HTTP/1.1 request, and this parser will parse the remaining - * bytes, that are not parseable by a HTTP/1.1 parser.
+ * bytes, that are not parseable by an HTTP/1.1 parser. */ protected void directUpgrade() { diff --git a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java index a067f510fcd..1d46593a28d 100644 --- a/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java +++ b/jetty-http2/http2-http-client-transport/src/test/java/org/eclipse/jetty/http2/client/http/HttpClientTransportOverHTTP2Test.java @@ -195,7 +195,7 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest .onRequestBegin(request -> { if (request.getVersion() != HttpVersion.HTTP_2) - request.abort(new Exception("Not a HTTP/2 request")); + request.abort(new Exception("Not an HTTP/2 request")); }) .send(); diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml index 16984b9bbdf..ad51713fd9c 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2.xml @@ -2,7 +2,7 @@ - +diff --git a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml index 08e5d62911f..2f5398b4d97 100644 --- a/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml +++ b/jetty-http2/http2-server/src/main/config/etc/jetty-http2c.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java index cc0b9925311..4ed23cf2fea 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2CServerConnectionFactory.java @@ -39,8 +39,8 @@ import org.eclipse.jetty.util.log.Logger; * * diff --git a/jetty-server/src/main/config/modules/http.mod b/jetty-server/src/main/config/modules/http.mod index e1f89b00dbe..a0b26e12663 100644 --- a/jetty-server/src/main/config/modules/http.mod +++ b/jetty-server/src/main/config/modules/http.mod @@ -1,7 +1,7 @@ DO NOT EDIT - See: https://www.eclipse.org/jetty/documentation/current/startup-modules.html [description] -Enables a HTTP connector on the server. +Enables an HTTP connector on the server. By default HTTP/1 is support, but HTTP2C can be added to the connector with the http2c module. diff --git a/jetty-server/src/main/config/modules/session-cache-null.mod b/jetty-server/src/main/config/modules/session-cache-null.mod index abdf2d7e076..6069c8f8168 100644 --- a/jetty-server/src/main/config/modules/session-cache-null.mod +++ b/jetty-server/src/main/config/modules/session-cache-null.mod @@ -18,3 +18,4 @@ etc/sessions/session-cache-null.xml [ini-template] #jetty.session.saveOnCreate=false #jetty.session.removeUnloadableSessions=false +#jetty.session.writeThroughMode=ON_EXIT diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java index 46c18701b7e..1723bcc73df 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -45,6 +45,7 @@ import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; +import org.eclipse.jetty.util.component.Container; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Graceful; @@ -115,7 +116,7 @@ import org.eclipse.jetty.util.thread.ThreadPoolBudget; * {@link ConnectionFactory}s may also create temporary {@link org.eclipse.jetty.io.Connection} instances that will exchange bytes * over the connection to determine what is the next protocol to use. For example the ALPN protocol is an extension * of SSL to allow a protocol to be specified during the SSL handshake. ALPN is used by the HTTP/2 protocol to - * negotiate the protocol that the client and server will speak. Thus to accept a HTTP/2 connection, the + * negotiate the protocol that the client and server will speak. Thus to accept an HTTP/2 connection, the * connector will be configured with {@link ConnectionFactory}s for "SSL-ALPN", "h2", "http/1.1" * with the default protocol being "SSL-ALPN". Thus a newly accepted connection uses "SSL-ALPN", which specifies a * SSLConnectionFactory with "ALPN" as the next protocol. Thus an SSL connection instance is created chained to an ALPN @@ -154,6 +155,7 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co private final SetIf used in combination with a {@link HttpConnectionFactory} as the * default protocol, this factory can support the non-standard direct - * update mechanism, where a HTTP1 request of the form "PRI * HTTP/2.0" - * is used to trigger a switch to a HTTP2 connection. This approach + * update mechanism, where an HTTP1 request of the form "PRI * HTTP/2.0" + * is used to trigger a switch to an HTTP2 connection. This approach * allows a single port to accept either HTTP/1 or HTTP/2 direct * connections. */ diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java index 2dd10e670e6..ed374cbd12d 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HTTP2ServerConnection.java @@ -66,7 +66,7 @@ import org.eclipse.jetty.util.TypeUtil; public class HTTP2ServerConnection extends HTTP2Connection implements Connection.UpgradeTo { /** - * @param protocol A HTTP2 protocol variant + * @param protocol An HTTP2 protocol variant * @return True if the protocol version is supported */ public static boolean isSupportedProtocol(String protocol) @@ -376,10 +376,9 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection } @Override - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { - if (isTunnel()) - getHttpTransport().prepareUpgrade(); + return isTunnel() && getHttpTransport().prepareUpgrade(); } @Override diff --git a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java index 1f53feba058..0dac59d2f06 100644 --- a/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java +++ b/jetty-http2/http2-server/src/main/java/org/eclipse/jetty/http2/server/HttpTransportOverHTTP2.java @@ -322,7 +322,7 @@ public class HttpTransportOverHTTP2 implements HttpTransport return transportCallback.onIdleTimeout(failure); } - void prepareUpgrade() + boolean prepareUpgrade() { HttpChannelOverHTTP2 channel = (HttpChannelOverHTTP2)stream.getAttachment(); Request request = channel.getRequest(); @@ -331,7 +331,8 @@ public class HttpTransportOverHTTP2 implements HttpTransport endPoint.upgrade(connection); stream.setAttachment(endPoint); if (request.getHttpInput().hasContent()) - channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return channel.sendErrorOrAbort("Unexpected content in CONNECT request"); + return false; } @Override diff --git a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java index 134639d9429..ff8a8c482af 100644 --- a/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java +++ b/jetty-http2/http2-server/src/test/java/org/eclipse/jetty/http2/server/HTTP2CServerTest.java @@ -188,7 +188,7 @@ public class HTTP2CServerTest extends AbstractServerTest assertThat(content, containsString("Hello from Jetty using HTTP/1.1")); assertThat(content, containsString("uri=/one")); - // Send a HTTP/2 request. + // Send an HTTP/2 request. headersRef.set(null); dataRef.set(null); latchRef.set(new CountDownLatch(2)); @@ -319,7 +319,7 @@ public class HTTP2CServerTest extends AbstractServerTest connector.setDefaultProtocol(connectionFactory.getProtocol()); connector.start(); - // Now send a HTTP/2 direct request, which + // Now send an HTTP/2 direct request, which // will have the PRI * HTTP/2.0 preface. byteBufferPool = new MappedByteBufferPool(); @@ -336,7 +336,7 @@ public class HTTP2CServerTest extends AbstractServerTest output.write(BufferUtil.toArray(buffer)); } - // We sent a HTTP/2 preface, but the server has no "h2c" connection + // We sent an HTTP/2 preface, but the server has no "h2c" connection // factory so it does not know how to handle this request. InputStream input = client.getInputStream(); diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java new file mode 100644 index 00000000000..fc68e4714b9 --- /dev/null +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ByteBufferOutputStream.java @@ -0,0 +1,62 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; + +/** + * Simple wrapper of a ByteBuffer as an OutputStream. + * The buffer does not grow and this class will throw an + * {@link java.nio.BufferOverflowException} if the buffer capacity is exceeded. + */ +public class ByteBufferOutputStream extends OutputStream +{ + final ByteBuffer _buffer; + + public ByteBufferOutputStream(ByteBuffer buffer) + { + _buffer = buffer; + } + + public void close() + { + } + + public void flush() + { + } + + public void write(byte[] b) + { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) + { + BufferUtil.append(_buffer, b, off, len); + } + + public void write(int b) + { + BufferUtil.append(_buffer, (byte)b); + } +} diff --git a/jetty-maven-plugin/.gitignore b/jetty-maven-plugin/.gitignore deleted file mode 100644 index 929d903c7d3..00000000000 --- a/jetty-maven-plugin/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.classpath -.project -.settings -target -*.swp diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml index c5925ef6b77..303ada127b6 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty-http.xml @@ -3,13 +3,13 @@ - +
- *- + diff --git a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml index 17f38f2776d..70c174072ab 100644 --- a/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml +++ b/jetty-osgi/jetty-osgi-boot/jettyhome/etc/jetty.xml @@ -96,7 +96,7 @@ - org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml index cd907934b86..e5baeae8106 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-context-as-service.xml @@ -3,13 +3,13 @@ - +- + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml index df4f9a29512..3f034f1334b 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-webapp-as-service.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml index fafa0ecdd18..ccb14c09de9 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-annotations.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml index 856a577d329..6f28312891c 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-javax-websocket.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml index aeda97879e8..5ae769fca57 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-jsp.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml index 3c45c45a368..20bb83b2765 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http-boot-with-websocket.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml index 6384118ee3b..8995bfc70ce 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http.xml @@ -3,13 +3,13 @@ - + - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml index 16984b9bbdf..ad51713fd9c 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2-jdk9.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml index a48b216e4ff..2bf9d1051b1 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-http2.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml index 7b408b6dc4d..41fb957e7f0 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-https.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml index 9cc5f6f72e7..6c2b9d68082 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty-with-custom-class.xml @@ -82,7 +82,7 @@ - + diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java index ab3008edd5c..c5040271992 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConfigurableSpnegoLoginService.java @@ -51,7 +51,7 @@ import org.ietf.jgss.Oid; * of the {@link #getServiceName() service name} and the {@link #getHostName() host name}, * for example {@code HTTP/wonder.com}, using a {@code keyTab} file as the service principal * credentials.- org.eclipse.jetty.plus.webapp.EnvConfiguration
- org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
diff --git a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml index 291fccd1873..ad2b008c33a 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml +++ b/jetty-osgi/test-jetty-osgi/src/test/config/etc/jetty.xml @@ -85,7 +85,7 @@- org.eclipse.jetty.webapp.JmxConfiguration
- org.eclipse.jetty.osgi.annotations.AnnotationConfiguration
- org.eclipse.jetty.websocket.server.config.JettyWebSocketConfiguration
-- org.eclipse.jetty.websocket.javax.server.JavaxWebSocketConfiguration
+- org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketConfiguration
- org.eclipse.jetty.osgi.boot.OSGiWebInfConfiguration
- org.eclipse.jetty.osgi.boot.OSGiMetaInfConfiguration
diff --git a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java index bf7dd3cec3a..a50db15003c 100644 --- a/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java +++ b/jetty-proxy/src/main/java/org/eclipse/jetty/proxy/AbstractProxyServlet.java @@ -693,6 +693,14 @@ public abstract class AbstractProxyServlet extends HttpServlet catch (Exception e) { _log.ignore(e); + try + { + proxyResponse.sendError(-1); + } + catch (Exception e2) + { + _log.ignore(e2); + } } finally { diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index 86abf843ef4..79d80dfaab2 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -22,6 +22,8 @@ import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.annotation.Name; /** diff --git a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml index 90dc198b4f3..dedac35bc32 100644 --- a/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml +++ b/jetty-rewrite/src/test/resources/org.mortbay.jetty.rewrite.handler/jetty-rewrite.xml @@ -43,7 +43,7 @@Upon receiving a HTTP request, the server tries to authenticate the client + *
Upon receiving an HTTP request, the server tries to authenticate the client * calling {@link #login(String, Object, ServletRequest)} where the GSS APIs are used to * verify client tokens and (perhaps after a few round-trips) a {@code GSSContext} is * established.
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java index 26aede1f640..411594f32f1 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java @@ -814,7 +814,7 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr { //an exact method name if (!hasOmissions) - //a http-method does not have http-method-omission to cover the other method names + //an http-method does not have http-method-omission to cover the other method names uncoveredPaths.add(path); } } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java index eaa66a0d81a..8b99979b18b 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java @@ -39,7 +39,7 @@ import org.eclipse.jetty.util.log.Logger; * * When a user has been successfully authenticated with some types * of Authenticator, the Authenticator stashes a SessionAuthentication - * into a HttpSession to remember that the user is authenticated. + * into an HttpSession to remember that the user is authenticated. */ public class SessionAuthentication extends AbstractUserAuthentication implements Serializable, HttpSessionActivationListener, HttpSessionBindingListener diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java index 2e6e82253c3..84c81f922dd 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java @@ -423,7 +423,7 @@ public class ConstraintTest assertEquals(1, uncoveredPaths.size()); assertThat("/user/*", is(in(uncoveredPaths))); - //Test an explicitly named method with a http-method-omission to cover all other methods + //Test an explicitly named method with an http-method-omission to cover all other methods Constraint constraint2a = new Constraint(); constraint2a.setAuthenticate(true); constraint2a.setName("forbid constraint"); @@ -437,7 +437,7 @@ public class ConstraintTest assertNotNull(uncoveredPaths); assertEquals(0, uncoveredPaths.size()); - //Test a http-method-omission only + //Test an http-method-omission only Constraint constraint3 = new Constraint(); constraint3.setAuthenticate(true); constraint3.setName("omit constraint"); diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java index 234646c807f..2a483729ed1 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -44,6 +44,7 @@ import org.junit.jupiter.api.Test; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -322,7 +323,8 @@ public class SpecExampleConstraintTest response = _connector.getResponse("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + "Authorization: Basic " + encodedChris + "\r\n" + "\r\n"); - assertThat(response, startsWith("HTTP/1.1 403 ")); + assertThat(response, startsWith("HTTP/1.1 403 Forbidden")); + assertThat(response, containsString("!Secure")); //a user in role HOMEOWNER can do a GET response = _connector.getResponse("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + 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 60f7fe510cd..25b6dbe6235 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 @@ -18,14 +18,17 @@ package org.eclipse.jetty.security.authentication; +import java.io.IOException; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.server.AbstractConnector; 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.HttpOutput; import org.eclipse.jetty.server.Request; @@ -34,6 +37,8 @@ import org.eclipse.jetty.server.Server; 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.junit.jupiter.api.Assertions.assertEquals; public class SpnegoAuthenticatorTest @@ -49,27 +54,34 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithNoAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); MetaData.Request metadata = new MetaData.Request(new HttpFields()); metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); @@ -78,23 +90,29 @@ public class SpnegoAuthenticatorTest @Test public void testChallengeSentWithUnhandledAuthorization() throws Exception { - HttpChannel channel = new HttpChannel(null, new HttpConfiguration(), null, null) + HttpChannel channel = new HttpChannel(new MockConnector(), new HttpConfiguration(), null, null) { @Override public Server getServer() { return null; } - }; - Request req = new Request(channel, null); - HttpOutput out = new HttpOutput(channel) - { + @Override - public void close() + protected HttpOutput newHttpOutput() { + return new HttpOutput(this) + { + @Override + public void close() {} + + @Override + public void flush() throws IOException {} + }; } }; - Response res = new Response(channel, out); + Request req = channel.getRequest(); + Response res = channel.getResponse(); HttpFields http_fields = new HttpFields(); // Create a bogus Authorization header. We don't care about the actual credentials. http_fields.add(HttpHeader.AUTHORIZATION, "Basic asdf"); @@ -102,8 +120,34 @@ public class SpnegoAuthenticatorTest metadata.setURI(new HttpURI("http://localhost")); req.setMetaData(metadata); + assertThat(channel.getState().handling(), is(HttpChannelState.Action.DISPATCH)); assertEquals(Authentication.SEND_CONTINUE, _authenticator.validateRequest(req, res, true)); assertEquals(HttpHeader.NEGOTIATE.asString(), res.getHeader(HttpHeader.WWW_AUTHENTICATE.asString())); assertEquals(HttpServletResponse.SC_UNAUTHORIZED, res.getStatus()); } + } + + class MockConnector extends AbstractConnector + { + public MockConnector() + { + super(new Server() , null, null, null, 0); + } + + @Override + protected void accept(int acceptorID) throws IOException, InterruptedException + { + } + + @Override + public Object getTransport() + { + return null; + } + + @Override + public String dumpSelf() + { + return null; + } } diff --git a/jetty-server/src/main/config/etc/jetty-http.xml b/jetty-server/src/main/config/etc/jetty-http.xml index ccf02559439..17aa2453587 100644 --- a/jetty-server/src/main/config/etc/jetty-http.xml +++ b/jetty-server/src/main/config/etc/jetty-http.xml @@ -3,13 +3,13 @@ - +- + diff --git a/jetty-server/src/main/config/etc/jetty-https.xml b/jetty-server/src/main/config/etc/jetty-https.xml index bbeaa5a6cc7..a71de579531 100644 --- a/jetty-server/src/main/config/etc/jetty-https.xml +++ b/jetty-server/src/main/config/etc/jetty-https.xml @@ -2,7 +2,7 @@ - + diff --git a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml index 84d26c24ef7..466402975aa 100644 --- a/jetty-server/src/main/config/etc/sessions/session-cache-null.xml +++ b/jetty-server/src/main/config/etc/sessions/session-cache-null.xml @@ -12,6 +12,11 @@ + + + ++ _endpoints = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set _immutableEndPoints = Collections.unmodifiableSet(_endpoints); private final Graceful.Shutdown _shutdown = new Graceful.Shutdown(); + private HttpChannel.Listener _httpChannelListeners = HttpChannel.NOOP_LISTENER; private CountDownLatch _stopping; private long _idleTimeout = 30000; private String _defaultProtocol; @@ -188,6 +190,23 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co pool = _server.getBean(ByteBufferPool.class); _byteBufferPool = pool != null ? pool : new ArrayByteBufferPool(); + addEventListener(new Container.Listener() + { + @Override + public void beanAdded(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + + @Override + public void beanRemoved(Container parent, Object bean) + { + if (bean instanceof HttpChannel.Listener) + _httpChannelListeners = new HttpChannelListeners(getBeans(HttpChannel.Listener.class)); + } + }); + addBean(_server, false); addBean(_executor); if (executor == null) @@ -208,6 +227,24 @@ public abstract class AbstractConnector extends ContainerLifeCycle implements Co _acceptors = new Thread[acceptors]; } + /** + * Get the {@link HttpChannel.Listener}s added to the connector + * as a single combined Listener. + * This is equivalent to a listener that iterates over the individual + * listeners returned from getBeans(HttpChannel.Listener.class);
, + * except that:+ *
+ * @see #getBeans(Class) + * @return An unmodifiable list of EventListener beans + */ + public HttpChannel.Listener getHttpChannelListeners() + { + return _httpChannelListeners; + } + @Override public Server getServer() { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java index 0cd93ff67be..52520ffd71f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncContextEvent.java @@ -160,7 +160,7 @@ public class AsyncContextEvent extends AsyncEvent implements Runnable Scheduler.Task task = _timeoutTask; _timeoutTask = null; if (task != null) - _state.getHttpChannel().execute(() -> _state.onTimeout()); + _state.timeout(); } public void addThrowable(Throwable e) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java index ffc6153bc12..0a1d87bc37b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ConnectionFactory.java @@ -35,11 +35,11 @@ import org.eclipse.jetty.io.EndPoint; * A ConnectionFactory has a protocol name that represents the protocol of the Connections * created. Example of protocol names include: *- The result is precomputed, so it is more efficient
+ *- The result is ordered by the order added.
+ *- The result is immutable.
+ *- *
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 9d8088b4ef1..9a474a30fc0 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.DebugHandler; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.log.Log; @@ -42,8 +43,6 @@ public class Dispatcher implements RequestDispatcher { private static final Logger LOG = Log.getLogger(Dispatcher.class); - public static final String __ERROR_DISPATCH = "org.eclipse.jetty.server.Dispatcher.ERROR"; - /** * Dispatch include attribute names */ @@ -77,15 +76,7 @@ public class Dispatcher implements RequestDispatcher public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException { - try - { - request.setAttribute(__ERROR_DISPATCH, Boolean.TRUE); - forward(request, response, DispatcherType.ERROR); - } - finally - { - request.setAttribute(__ERROR_DISPATCH, null); - } + forward(request, response, DispatcherType.ERROR); } @Override 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 144d69cc2f6..55d155601ad 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 @@ -22,10 +22,10 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.EventListener; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -33,12 +33,12 @@ import java.util.function.Function; import java.util.function.Supplier; import javax.servlet.DispatcherType; import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpGenerator; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpVersion; @@ -51,6 +51,7 @@ import org.eclipse.jetty.io.QuietException; import org.eclipse.jetty.server.HttpChannelState.Action; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.handler.ErrorHandler.ErrorPageMapper; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.SharedBlockingCallback.Blocker; @@ -60,7 +61,7 @@ import org.eclipse.jetty.util.thread.Scheduler; /** * HttpChannel represents a single endpoint for HTTP semantic processing. - * The HttpChannel is both a HttpParser.RequestHandler, where it passively receives events from + * The HttpChannel is both an HttpParser.RequestHandler, where it passively receives events from * an incoming HTTP request, and a Runnable, where it actively takes control of the request/response * life cycle and calls the application (perhaps suspending and resuming with multiple calls to run). * The HttpChannel signals the switch from passive mode to active mode by returning true to one of the @@ -69,10 +70,9 @@ import org.eclipse.jetty.util.thread.Scheduler; */ public class HttpChannel implements Runnable, HttpOutput.Interceptor { + public static Listener NOOP_LISTENER = new Listener(){}; private static final Logger LOG = Log.getLogger(HttpChannel.class); - private final AtomicBoolean _committed = new AtomicBoolean(); - private final AtomicBoolean _responseCompleted = new AtomicBoolean(); private final AtomicLong _requests = new AtomicLong(); private final Connector _connector; private final Executor _executor; @@ -82,9 +82,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private final HttpChannelState _state; private final Request _request; private final Response _response; + private final HttpChannel.Listener _combinedListener; + @Deprecated + private final List- http
- Creates a HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1
- *- h2
- Creates a HTTP/2 connection that handles the HTTP/2 protocol
+ *- http
- Creates an HTTP connection that can handle multiple versions of HTTP from 0.9 to 1.1
+ *- h2
- Creates an HTTP/2 connection that handles the HTTP/2 protocol
*- SSL-XYZ
- Create an SSL connection chained to a connection obtained from a connection factory * with a protocol "XYZ".
- *- SSL-http
- Create an SSL connection chained to a HTTP connection (aka https)
+ *- SSL-http
- Create an SSL connection chained to an HTTP connection (aka https)
*- SSL-ALPN
- Create an SSL connection chained to a ALPN connection, that uses a negotiation with * the client to determine the next protocol.
*_transientListeners = new ArrayList<>(); private HttpFields _trailers; private final Supplier _trailerSupplier = () -> _trailers; - private final List _listeners; private MetaData.Response _committedMetaData; private RequestLog _requestLog; private long _oldIdleTimeout; @@ -105,13 +107,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request = new Request(this, newHttpInput(_state)); _response = new Response(this, newHttpOutput()); - _executor = connector == null ? null : connector.getServer().getThreadPool(); - _requestLog = connector == null ? null : connector.getServer().getRequestLog(); - - List listeners = new ArrayList<>(); - if (connector != null) - listeners.addAll(connector.getBeans(Listener.class)); - _listeners = listeners; + _executor = connector.getServer().getThreadPool(); + _requestLog = connector.getServer().getRequestLog(); + _combinedListener = (connector instanceof AbstractConnector) + ? ((AbstractConnector)connector).getHttpChannelListeners() + : NOOP_LISTENER; if (LOG.isDebugEnabled()) LOG.debug("new {} -> {},{},{}", @@ -121,6 +121,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _state); } + public boolean isSendError() + { + return _state.isSendError(); + } + protected HttpInput newHttpInput(HttpChannelState state) { return new HttpInput(state); @@ -136,14 +141,32 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return _state; } + /** + * Add a transient Listener to the HttpChannel. + * Listeners added by this method will only be notified + * if the HttpChannel has been constructed with an instance of + * {@link TransientListeners} as an {@link AbstractConnector} + * provided listener
+ *Transient listeners are removed after every request cycle
+ * @param listener + * @return true if the listener was added. + */ + @Deprecated public boolean addListener(Listener listener) { - return _listeners.add(listener); + return _transientListeners.add(listener); } + @Deprecated public boolean removeListener(Listener listener) { - return _listeners.remove(listener); + return _transientListeners.remove(listener); + } + + @Deprecated + public ListgetTransientListeners() + { + return _transientListeners; } public long getBytesWritten() @@ -284,8 +307,6 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void recycle() { - _committed.set(false); - _responseCompleted.set(false); _request.recycle(); _response.recycle(); _committedMetaData = null; @@ -293,6 +314,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _written = 0; _trailers = null; _oldIdleTimeout = 0; + _transientListeners.clear(); } public void onAsyncWaitForContent() @@ -320,7 +342,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean handle() { if (LOG.isDebugEnabled()) - LOG.debug("{} handle {} ", this, _request.getHttpURI()); + LOG.debug("handle {} {} ", _request.getHttpURI(), this); HttpChannelState.Action action = _state.handling(); @@ -334,19 +356,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor try { if (LOG.isDebugEnabled()) - LOG.debug("{} action {}", this, action); + LOG.debug("action {} {}", action, this); switch (action) { case TERMINATED: + onCompleted(); + break loop; + case WAIT: // break loop without calling unhandle break loop; - case NOOP: - // do nothing other than call unhandle - break; - case DISPATCH: { if (!_request.hasMetaData()) @@ -354,35 +375,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try + dispatch(DispatcherType.REQUEST, () -> { - _request.setDispatcherType(DispatcherType.REQUEST); - notifyBeforeDispatch(_request); - - List customizers = _configuration.getCustomizers(); - if (!customizers.isEmpty()) + for (HttpConfiguration.Customizer customizer : _configuration.getCustomizers()) { - for (HttpConfiguration.Customizer customizer : customizers) - { - customizer.customize(getConnector(), _configuration, _request); - if (_request.isHandled()) - break; - } + customizer.customize(getConnector(), _configuration, _request); + if (_request.isHandled()) + return; } + getServer().handle(HttpChannel.this); + }); - if (!_request.isHandled()) - getServer().handle(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } break; } @@ -391,70 +394,70 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setHandled(false); _response.getHttpOutput().reopen(); - try - { - _request.setDispatcherType(DispatcherType.ASYNC); - notifyBeforeDispatch(_request); - getServer().handleAsync(this); - } - catch (Throwable x) - { - notifyDispatchFailure(_request, x); - throw x; - } - finally - { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); - } + dispatch(DispatcherType.ASYNC,() -> getServer().handleAsync(this)); break; } - case ERROR_DISPATCH: + case ASYNC_TIMEOUT: + _state.onTimeout(); + break; + + case SEND_ERROR: { try { - _response.reset(true); - Integer icode = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - int code = icode != null ? icode : HttpStatus.INTERNAL_SERVER_ERROR_500; - _response.setStatus(code); - _request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); + // Get ready to send an error response _request.setHandled(false); + _response.resetContent(); _response.getHttpOutput().reopen(); - try + // the following is needed as you cannot trust the response code and reason + // as those could have been modified after calling sendError + Integer code = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + _response.setStatus(code != null ? code : HttpStatus.INTERNAL_SERVER_ERROR_500); + + ContextHandler.Context context = (ContextHandler.Context)_request.getAttribute(ErrorHandler.ERROR_CONTEXT); + ErrorHandler errorHandler = ErrorHandler.getErrorHandler(getServer(), context == null ? null : context.getContextHandler()); + + // If we can't have a body, then create a minimal error response. + if (HttpStatus.hasNoBody(_response.getStatus()) || errorHandler == null || !errorHandler.errorPageForMethod(_request.getMethod())) { - _request.setDispatcherType(DispatcherType.ERROR); - notifyBeforeDispatch(_request); - getServer().handle(this); + sendResponseAndComplete(); + break; } - catch (Throwable x) + + // Look for an error page dispatcher + String errorPage = (errorHandler instanceof ErrorPageMapper) ? ((ErrorPageMapper)errorHandler).getErrorPage(_request) : null; + Dispatcher errorDispatcher = errorPage != null ? (Dispatcher)context.getRequestDispatcher(errorPage) : null; + if (errorDispatcher == null) { - notifyDispatchFailure(_request, x); - throw x; + // Allow ErrorHandler to generate response + errorHandler.handle(null, _request, _request, _response); + _request.setHandled(true); } - finally + else { - notifyAfterDispatch(_request); - _request.setDispatcherType(null); + // Do the error page dispatch + dispatch(DispatcherType.ERROR,() -> errorDispatcher.error(_request, _response)); } } catch (Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("Could not perform ERROR dispatch, aborting", x); - Throwable failure = (Throwable)_request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - if (failure == null) - { - minimalErrorResponse(x); - } + if (_state.isResponseCommitted()) + abort(x); else { - if (x != failure) - failure.addSuppressed(x); - minimalErrorResponse(failure); + _response.resetContent(); + sendResponseAndComplete(); } } + finally + { + // clean up the context that was set in Response.sendError + _request.removeAttribute(ErrorHandler.ERROR_CONTEXT); + } break; } @@ -463,6 +466,12 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw _state.getAsyncContextEvent().getThrowable(); } + case READ_REGISTER: + { + onAsyncWaitForContent(); + break; + } + case READ_PRODUCE: { _request.getHttpInput().asyncReadProduce(); @@ -491,41 +500,36 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor case COMPLETE: { - try + if (!_response.isCommitted() && !_request.isHandled() && !_response.getHttpOutput().isClosed()) { - if (!_response.isCommitted() && !_request.isHandled()) - { - _response.sendError(HttpStatus.NOT_FOUND_404); - } - else - { - // RFC 7230, section 3.3. - int status = _response.getStatus(); - boolean hasContent = !(_request.isHead() || - HttpMethod.CONNECT.is(_request.getMethod()) && status == HttpStatus.OK_200 || - HttpStatus.isInformational(status) || - status == HttpStatus.NO_CONTENT_204 || - status == HttpStatus.NOT_MODIFIED_304); - if (hasContent && !_response.isContentComplete(_response.getHttpOutput().getWritten())) - sendErrorOrAbort("Insufficient content written"); - } - checkAndPrepareUpgrade(); - _response.closeOutput(); - } - finally - { - _request.setHandled(true); - _state.onComplete(); - onCompleted(); + _response.sendError(HttpStatus.NOT_FOUND_404); + break; } - break loop; + // RFC 7230, section 3.3. + if (!_request.isHead() && !_response.isContentComplete(_response.getHttpOutput().getWritten())) + { + if (sendErrorOrAbort("Insufficient content written")) + break; + } + + // Check if an update is done (if so, do not close) + if (checkAndPrepareUpgrade()) + break; + + // TODO Currently a blocking/aborting consumeAll is done in the handling of the TERMINATED + // TODO Action triggered by the completed callback below. It would be possible to modify the + // TODO callback to do a non-blocking consumeAll at this point and only call completed when + // TODO that is done. + + // Set a close callback on the HttpOutput to make it an async callback + _response.closeOutput(Callback.from(_state::completed)); + + break; } default: - { - throw new IllegalStateException("state=" + _state); - } + throw new IllegalStateException(this.toString()); } } catch (Throwable failure) @@ -540,42 +544,50 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } if (LOG.isDebugEnabled()) - LOG.debug("{} handle exit, result {}", this, action); + LOG.debug("!handle {} {}", action, this); boolean suspended = action == Action.WAIT; return !suspended; } - public void sendErrorOrAbort(String message) + public boolean sendErrorOrAbort(String message) { try { if (isCommitted()) + { abort(new IOException(message)); - else - _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return false; + } + + _response.sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, message); + return true; } catch (Throwable x) { LOG.ignore(x); abort(x); } + return false; } - protected void sendError(int code, String reason) + private void dispatch(DispatcherType type, Dispatchable dispatchable) throws IOException, ServletException { try { - _response.sendError(code, reason); + _request.setDispatcherType(type); + _combinedListener.onBeforeDispatch(_request); + dispatchable.dispatch(); } catch (Throwable x) { - if (LOG.isDebugEnabled()) - LOG.debug("Could not send error " + code + " " + reason, x); + _combinedListener.onDispatchFailure(_request, x); + throw x; } finally { - _state.errorComplete(); + _combinedListener.onAfterDispatch(_request); + _request.setDispatcherType(null); } } @@ -603,27 +615,19 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor { // No stack trace unless there is debug turned on if (LOG.isDebugEnabled()) - LOG.debug(_request.getRequestURI(), failure); + LOG.warn("handleException " + _request.getRequestURI(), failure); else - LOG.warn("{} {}", _request.getRequestURI(), noStack.toString()); + LOG.warn("handleException {} {}", _request.getRequestURI(), noStack.toString()); } else { LOG.warn(_request.getRequestURI(), failure); } - try - { + if (isCommitted()) + abort(failure); + else _state.onError(failure); - } - catch (Throwable e) - { - if (e != failure) - failure.addSuppressed(e); - LOG.warn("ERROR dispatch failed", failure); - // Try to send a minimal response. - minimalErrorResponse(failure); - } } /** @@ -647,32 +651,17 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor return null; } - private void minimalErrorResponse(Throwable failure) + public void sendResponseAndComplete() { try { - int code = 500; - Integer status = (Integer)_request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); - if (status != null) - { - code = status.intValue(); - } - else - { - Throwable cause = unwrap(failure, BadMessageException.class); - if (cause instanceof BadMessageException) - code = ((BadMessageException)cause).getCode(); - } - - _response.reset(true); - _response.setStatus(code); - _response.flushBuffer(); + _request.setHandled(true); + _state.completing(); + sendResponse(null, _response.getHttpOutput().getBuffer(), true, Callback.from(_state::completed)); } catch (Throwable x) { - if (x != failure) - failure.addSuppressed(x); - abort(failure); + abort(x); } } @@ -690,11 +679,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public String toString() { long timeStamp = _request.getTimeStamp(); - return String.format("%s@%x{r=%s,c=%b,c=%b/%b,a=%s,uri=%s,age=%d}", + return String.format("%s@%x{s=%s,r=%s,c=%b/%b,a=%s,uri=%s,age=%d}", getClass().getSimpleName(), hashCode(), + _state, _requests, - _committed.get(), isRequestCompleted(), isResponseCompleted(), _state.getState(), @@ -720,7 +709,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor _request.setSecure(HttpScheme.HTTPS.is(request.getURI().getScheme())); - notifyRequestBegin(_request); + _combinedListener.onRequestBegin(_request); if (LOG.isDebugEnabled()) LOG.debug("REQUEST for {} on {}{}{} {} {}{}{}", request.getURIString(), this, System.lineSeparator(), @@ -731,33 +720,33 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean onContent(HttpInput.Content content) { if (LOG.isDebugEnabled()) - LOG.debug("{} onContent {}", this, content); - notifyRequestContent(_request, content.getByteBuffer()); + LOG.debug("onContent {} {}", this, content); + _combinedListener.onRequestContent(_request, content.getByteBuffer()); return _request.getHttpInput().addContent(content); } public boolean onContentComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onContentComplete", this); - notifyRequestContentEnd(_request); + LOG.debug("onContentComplete {}", this); + _combinedListener.onRequestContentEnd(_request); return false; } public void onTrailers(HttpFields trailers) { if (LOG.isDebugEnabled()) - LOG.debug("{} onTrailers {}", this, trailers); + LOG.debug("onTrailers {} {}", this, trailers); _trailers = trailers; - notifyRequestTrailers(_request); + _combinedListener.onRequestTrailers(_request); } public boolean onRequestComplete() { if (LOG.isDebugEnabled()) - LOG.debug("{} onRequestComplete", this); + LOG.debug("onRequestComplete {}", this); boolean result = _request.getHttpInput().eof(); - notifyRequestEnd(_request); + _combinedListener.onRequestEnd(_request); return result; } @@ -767,15 +756,18 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * response is sent back to the client. * This avoids a race where the server is unprepared if the client sends * data immediately after having received the upgrade response.
+ * @return true if the channel is not complete and more processing is required, + * typically because sendError has been called. */ - protected void checkAndPrepareUpgrade() + protected boolean checkAndPrepareUpgrade() { + return false; } public void onCompleted() { if (LOG.isDebugEnabled()) - LOG.debug("COMPLETE for {} written={}", getRequest().getRequestURI(), getBytesWritten()); + LOG.debug("onCompleted for {} written={}", getRequest().getRequestURI(), getBytesWritten()); if (_requestLog != null) _requestLog.log(_request, _response); @@ -785,7 +777,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor setIdleTimeout(_oldIdleTimeout); _request.onCompleted(); - notifyComplete(_request); + _combinedListener.onComplete(_request); _transport.onCompleted(); } @@ -797,11 +789,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void onBadMessage(BadMessageException failure) { int status = failure.getCode(); - String message = failure.getReason(); - if (status < 400 || status > 599) - failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, message, failure); + String reason = failure.getReason(); + if (status < HttpStatus.BAD_REQUEST_400 || status > 599) + failure = new BadMessageException(HttpStatus.BAD_REQUEST_400, reason, failure); - notifyRequestFailure(_request, failure); + _combinedListener.onRequestFailure(_request, failure); Action action; try @@ -825,7 +817,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor ErrorHandler handler = getServer().getBean(ErrorHandler.class); if (handler != null) - content = handler.badMessageError(status, message, fields); + content = handler.badMessageError(status, reason, fields); sendResponse(new MetaData.Response(HttpVersion.HTTP_1_1, status, null, fields, BufferUtil.length(content)), content, true); } @@ -850,7 +842,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor protected boolean sendResponse(MetaData.Response response, ByteBuffer content, boolean complete, final Callback callback) { - boolean committing = _committed.compareAndSet(false, true); + boolean committing = _state.commitResponse(); if (LOG.isDebugEnabled()) LOG.debug("sendResponse info={} content={} complete={} committing={} callback={}", @@ -867,11 +859,13 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor response = _response.newResponseMetaData(); commit(response); - // Wrap the callback to process 1xx responses. - Callback committed = HttpStatus.isInformational(response.getStatus()) - ? new Send100Callback(callback) : new SendCallback(callback, content, true, complete); + // wrap callback to process 100 responses + final int status = response.getStatus(); + final Callback committed = (status < HttpStatus.OK_200 && status >= HttpStatus.CONTINUE_100) + ? new Send100Callback(callback) + : new SendCallback(callback, content, true, complete); - notifyResponseBegin(_request); + _combinedListener.onResponseBegin(_request); // committing write _transport.send(_request.getMetaData(), response, content, complete, committed); @@ -916,7 +910,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public boolean isCommitted() { - return _committed.get(); + return _state.isResponseCommitted(); } /** @@ -932,7 +926,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public boolean isResponseCompleted() { - return _responseCompleted.get(); + return _state.isResponseCompleted(); } public boolean isPersistent() @@ -995,8 +989,11 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor */ public void abort(Throwable failure) { - notifyResponseFailure(_request, failure); - _transport.abort(failure); + if (_state.abortResponse()) + { + _combinedListener.onResponseFailure(_request, failure); + _transport.abort(failure); + } } public boolean isTunnellingSupported() @@ -1009,84 +1006,9 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor throw new UnsupportedOperationException("Tunnelling not supported"); } - private void notifyRequestBegin(Request request) - { - notifyEvent1(listener -> listener::onRequestBegin, request); - } - - private void notifyBeforeDispatch(Request request) - { - notifyEvent1(listener -> listener::onBeforeDispatch, request); - } - - private void notifyDispatchFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onDispatchFailure, request, failure); - } - - private void notifyAfterDispatch(Request request) - { - notifyEvent1(listener -> listener::onAfterDispatch, request); - } - - private void notifyRequestContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onRequestContent, request, content); - } - - private void notifyRequestContentEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestContentEnd, request); - } - - private void notifyRequestTrailers(Request request) - { - notifyEvent1(listener -> listener::onRequestTrailers, request); - } - - private void notifyRequestEnd(Request request) - { - notifyEvent1(listener -> listener::onRequestEnd, request); - } - - private void notifyRequestFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onRequestFailure, request, failure); - } - - private void notifyResponseBegin(Request request) - { - notifyEvent1(listener -> listener::onResponseBegin, request); - } - - private void notifyResponseCommit(Request request) - { - notifyEvent1(listener -> listener::onResponseCommit, request); - } - - private void notifyResponseContent(Request request, ByteBuffer content) - { - notifyEvent2(listener -> listener::onResponseContent, request, content); - } - - private void notifyResponseEnd(Request request) - { - notifyEvent1(listener -> listener::onResponseEnd, request); - } - - private void notifyResponseFailure(Request request, Throwable failure) - { - notifyEvent2(listener -> listener::onResponseFailure, request, failure); - } - - private void notifyComplete(Request request) - { - notifyEvent1(listener -> listener::onComplete, request); - } - private void notifyEvent1(Function> function, Request request) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1101,7 +1023,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function > function, Request request, ByteBuffer content) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { ByteBuffer view = content.slice(); try @@ -1117,7 +1039,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor private void notifyEvent2(Function > function, Request request, Throwable failure) { - for (Listener listener : _listeners) + for (Listener listener : _transientListeners) { try { @@ -1130,10 +1052,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor } } + interface Dispatchable + { + void dispatch() throws IOException, ServletException; + } + /** * Listener for {@link HttpChannel} events.
*HttpChannel will emit events for the various phases it goes through while - * processing a HTTP request and response.
+ * processing an HTTP request and response. *Implementations of this interface may listen to those events to track * timing and/or other values such as request URI, etc.
*The events parameters, especially the {@link Request} object, may be @@ -1148,15 +1075,20 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor *
Listener methods are invoked synchronously from the thread that is * performing the request processing, and they should not call blocking code * (otherwise the request processing will be blocked as well).
+ *Listener instances that are set as a bean on the {@link Connector} are + * efficiently added to {@link HttpChannel}. If additional listeners are added + * using the deprecated {@link HttpChannel#addListener(Listener)}
method, + * then an instance of {@link TransientListeners} must be added to the connector + * in order for them to be invoked. */ - public interface Listener + public interface Listener extends EventListener { /** * Invoked just after the HTTP request line and headers have been parsed. * * @param request the request object */ - public default void onRequestBegin(Request request) + default void onRequestBegin(Request request) { } @@ -1165,7 +1097,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onBeforeDispatch(Request request) + default void onBeforeDispatch(Request request) { } @@ -1175,7 +1107,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the exception thrown by the application */ - public default void onDispatchFailure(Request request, Throwable failure) + default void onDispatchFailure(Request request, Throwable failure) { } @@ -1184,7 +1116,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onAfterDispatch(Request request) + default void onAfterDispatch(Request request) { } @@ -1195,7 +1127,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the request content chunk */ - public default void onRequestContent(Request request, ByteBuffer content) + default void onRequestContent(Request request, ByteBuffer content) { } @@ -1204,7 +1136,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestContentEnd(Request request) + default void onRequestContentEnd(Request request) { } @@ -1213,7 +1145,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestTrailers(Request request) + default void onRequestTrailers(Request request) { } @@ -1222,7 +1154,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onRequestEnd(Request request) + default void onRequestEnd(Request request) { } @@ -1232,7 +1164,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the request failure */ - public default void onRequestFailure(Request request, Throwable failure) + default void onRequestFailure(Request request, Throwable failure) { } @@ -1241,7 +1173,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseBegin(Request request) + default void onResponseBegin(Request request) { } @@ -1252,7 +1184,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseCommit(Request request) + default void onResponseCommit(Request request) { } @@ -1262,7 +1194,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param content a {@link ByteBuffer#slice() slice} of the response content chunk */ - public default void onResponseContent(Request request, ByteBuffer content) + default void onResponseContent(Request request, ByteBuffer content) { } @@ -1271,7 +1203,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onResponseEnd(Request request) + default void onResponseEnd(Request request) { } @@ -1281,7 +1213,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * @param request the request object * @param failure the response failure */ - public default void onResponseFailure(Request request, Throwable failure) + default void onResponseFailure(Request request, Throwable failure) { } @@ -1290,7 +1222,7 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor * * @param request the request object */ - public default void onComplete(Request request) + default void onComplete(Request request) { } } @@ -1315,16 +1247,15 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor public void succeeded() { _written += _length; + if (_complete) + _response.getHttpOutput().closed(); super.succeeded(); if (_commit) - notifyResponseCommit(_request); + _combinedListener.onResponseCommit(_request); if (_length > 0) - notifyResponseContent(_request, _content); - if (_complete) - { - _responseCompleted.set(true); - notifyResponseEnd(_request); - } + _combinedListener.onResponseContent(_request, _content); + if (_complete && _state.completeResponse()) + _combinedListener.onResponseEnd(_request); } @Override @@ -1340,13 +1271,14 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - super.failed(x); _response.getHttpOutput().closed(); + super.failed(x); } @Override public void failed(Throwable th) { + _response.getHttpOutput().closed(); abort(x); super.failed(x); } @@ -1370,10 +1302,108 @@ public class HttpChannel implements Runnable, HttpOutput.Interceptor @Override public void succeeded() { - if (_committed.compareAndSet(true, false)) + if (_state.partialResponse()) super.succeeded(); else super.failed(new IllegalStateException()); } } + + /** + * A Listener instance that can be added as a bean to {@link AbstractConnector} so that + * the listeners obtained from HttpChannel{@link #getTransientListeners()} + */ + @Deprecated + public static class TransientListeners implements Listener + { + @Override + public void onRequestBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestBegin, request); + } + + @Override + public void onBeforeDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onBeforeDispatch, request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onDispatchFailure, request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onAfterDispatch, request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestContent, request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestContentEnd, request); + } + + @Override + public void onRequestTrailers(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestTrailers, request); + } + + @Override + public void onRequestEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onRequestEnd, request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onRequestFailure, request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseBegin, request); + } + + @Override + public void onResponseCommit(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseCommit, request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseContent, request, content); + } + + @Override + public void onResponseEnd(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onResponseEnd, request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + request.getHttpChannel().notifyEvent2(listener -> listener::onResponseFailure, request, failure); + } + + @Override + public void onComplete(Request request) + { + request.getHttpChannel().notifyEvent1(listener -> listener::onComplete, request); + } + } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java new file mode 100644 index 00000000000..281c7f5eb04 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpChannelListeners.java @@ -0,0 +1,286 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.nio.ByteBuffer; +import java.util.Collection; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * A {@link HttpChannel.Listener} that holds a collection of + * other {@link HttpChannel.Listener} instances that are efficiently + * invoked without iteration. + * @see AbstractConnector + */ +public class HttpChannelListeners implements HttpChannel.Listener +{ + static final Logger LOG = Log.getLogger(HttpChannel.class); + public static HttpChannel.Listener NOOP = new HttpChannel.Listener() {}; + + private final NotifyRequest onRequestBegin; + private final NotifyRequest onBeforeDispatch; + private final NotifyFailure onDispatchFailure; + private final NotifyRequest onAfterDispatch; + private final NotifyContent onRequestContent; + private final NotifyRequest onRequestContentEnd; + private final NotifyRequest onRequestTrailers; + private final NotifyRequest onRequestEnd; + private final NotifyFailure onRequestFailure; + private final NotifyRequest onResponseBegin; + private final NotifyRequest onResponseCommit; + private final NotifyContent onResponseContent; + private final NotifyRequest onResponseEnd; + private final NotifyFailure onResponseFailure; + private final NotifyRequest onComplete; + + public HttpChannelListeners(Collectionlisteners) + { + try + { + NotifyRequest onRequestBegin = NotifyRequest.NOOP; + NotifyRequest onBeforeDispatch = NotifyRequest.NOOP; + NotifyFailure onDispatchFailure = NotifyFailure.NOOP; + NotifyRequest onAfterDispatch = NotifyRequest.NOOP; + NotifyContent onRequestContent = NotifyContent.NOOP; + NotifyRequest onRequestContentEnd = NotifyRequest.NOOP; + NotifyRequest onRequestTrailers = NotifyRequest.NOOP; + NotifyRequest onRequestEnd = NotifyRequest.NOOP; + NotifyFailure onRequestFailure = NotifyFailure.NOOP; + NotifyRequest onResponseBegin = NotifyRequest.NOOP; + NotifyRequest onResponseCommit = NotifyRequest.NOOP; + NotifyContent onResponseContent = NotifyContent.NOOP; + NotifyRequest onResponseEnd = NotifyRequest.NOOP; + NotifyFailure onResponseFailure = NotifyFailure.NOOP; + NotifyRequest onComplete = NotifyRequest.NOOP; + + for (HttpChannel.Listener listener : listeners) + { + if (!listener.getClass().getMethod("onRequestBegin", Request.class).isDefault()) + onRequestBegin = combine(onRequestBegin, listener::onRequestBegin); + if (!listener.getClass().getMethod("onBeforeDispatch", Request.class).isDefault()) + onBeforeDispatch = combine(onBeforeDispatch, listener::onBeforeDispatch); + if (!listener.getClass().getMethod("onDispatchFailure", Request.class, Throwable.class).isDefault()) + onDispatchFailure = combine(onDispatchFailure, listener::onDispatchFailure); + if (!listener.getClass().getMethod("onAfterDispatch", Request.class).isDefault()) + onAfterDispatch = combine(onAfterDispatch, listener::onAfterDispatch); + if (!listener.getClass().getMethod("onRequestContent", Request.class, ByteBuffer.class).isDefault()) + onRequestContent = combine(onRequestContent, listener::onRequestContent); + if (!listener.getClass().getMethod("onRequestContentEnd", Request.class).isDefault()) + onRequestContentEnd = combine(onRequestContentEnd, listener::onRequestContentEnd); + if (!listener.getClass().getMethod("onRequestTrailers", Request.class).isDefault()) + onRequestTrailers = combine(onRequestTrailers, listener::onRequestTrailers); + if (!listener.getClass().getMethod("onRequestEnd", Request.class).isDefault()) + onRequestEnd = combine(onRequestEnd, listener::onRequestEnd); + if (!listener.getClass().getMethod("onRequestFailure", Request.class, Throwable.class).isDefault()) + onRequestFailure = combine(onRequestFailure, listener::onRequestFailure); + if (!listener.getClass().getMethod("onResponseBegin", Request.class).isDefault()) + onResponseBegin = combine(onResponseBegin, listener::onResponseBegin); + if (!listener.getClass().getMethod("onResponseCommit", Request.class).isDefault()) + onResponseCommit = combine(onResponseCommit, listener::onResponseCommit); + if (!listener.getClass().getMethod("onResponseContent", Request.class, ByteBuffer.class).isDefault()) + onResponseContent = combine(onResponseContent, listener::onResponseContent); + if (!listener.getClass().getMethod("onResponseEnd", Request.class).isDefault()) + onResponseEnd = combine(onResponseEnd, listener::onResponseEnd); + if (!listener.getClass().getMethod("onResponseFailure", Request.class, Throwable.class).isDefault()) + onResponseFailure = combine(onResponseFailure, listener::onResponseFailure); + if (!listener.getClass().getMethod("onComplete", Request.class).isDefault()) + onComplete = combine(onComplete, listener::onComplete); + } + + this.onRequestBegin = onRequestBegin; + this.onBeforeDispatch = onBeforeDispatch; + this.onDispatchFailure = onDispatchFailure; + this.onAfterDispatch = onAfterDispatch; + this.onRequestContent = onRequestContent; + this.onRequestContentEnd = onRequestContentEnd; + this.onRequestTrailers = onRequestTrailers; + this.onRequestEnd = onRequestEnd; + this.onRequestFailure = onRequestFailure; + this.onResponseBegin = onResponseBegin; + this.onResponseCommit = onResponseCommit; + this.onResponseContent = onResponseContent; + this.onResponseEnd = onResponseEnd; + this.onResponseFailure = onResponseFailure; + this.onComplete = onComplete; + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + @Override + public void onRequestBegin(Request request) + { + onRequestBegin.onRequest(request); + } + + @Override + public void onBeforeDispatch(Request request) + { + onBeforeDispatch.onRequest(request); + } + + @Override + public void onDispatchFailure(Request request, Throwable failure) + { + onDispatchFailure.onFailure(request, failure); + } + + @Override + public void onAfterDispatch(Request request) + { + onAfterDispatch.onRequest(request); + } + + @Override + public void onRequestContent(Request request, ByteBuffer content) + { + onRequestContent.onContent(request, content); + } + + @Override + public void onRequestContentEnd(Request request) + { + onRequestContentEnd.onRequest(request); + } + + @Override + public void onRequestTrailers(Request request) + { + onRequestTrailers.onRequest(request); + } + + @Override + public void onRequestEnd(Request request) + { + onRequestEnd.onRequest(request); + } + + @Override + public void onRequestFailure(Request request, Throwable failure) + { + onRequestFailure.onFailure(request, failure); + } + + @Override + public void onResponseBegin(Request request) + { + onResponseBegin.onRequest(request); + } + + @Override + public void onResponseCommit(Request request) + { + onResponseCommit.onRequest(request); + } + + @Override + public void onResponseContent(Request request, ByteBuffer content) + { + onResponseContent.onContent(request, content); + } + + @Override + public void onResponseEnd(Request request) + { + onResponseEnd.onRequest(request); + } + + @Override + public void onResponseFailure(Request request, Throwable failure) + { + onResponseFailure.onFailure(request, failure); + } + + @Override + public void onComplete(Request request) + { + onComplete.onRequest(request); + } + + private interface NotifyRequest + { + void onRequest(Request request); + + NotifyRequest NOOP = request -> + { + }; + } + + private interface NotifyFailure + { + void onFailure(Request request, Throwable failure); + + NotifyFailure NOOP = (request, failure) -> + { + }; + } + + private interface NotifyContent + { + void onContent(Request request, ByteBuffer content); + + NotifyContent NOOP = (request, content) -> + { + }; + } + + private static NotifyRequest combine(NotifyRequest first, NotifyRequest second) + { + if (first == NotifyRequest.NOOP) + return second; + if (second == NotifyRequest.NOOP) + return first; + return request -> + { + first.onRequest(request); + second.onRequest(request); + }; + } + + private static NotifyFailure combine(NotifyFailure first, NotifyFailure second) + { + if (first == NotifyFailure.NOOP) + return second; + if (second == NotifyFailure.NOOP) + return first; + return (request, throwable) -> + { + first.onFailure(request, throwable); + second.onFailure(request, throwable); + }; + } + + private static NotifyContent combine(NotifyContent first, NotifyContent second) + { + if (first == NotifyContent.NOOP) + return (request, content) -> second.onContent(request, content.slice()); + if (second == NotifyContent.NOOP) + return (request, content) -> first.onContent(request, content.slice()); + return (request, content) -> + { + content = content.slice(); + first.onContent(request, content); + second.onContent(request, content); + }; + } +} 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 55243378512..c09d71b7893 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 @@ -44,7 +44,7 @@ import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /** - * A HttpChannel customized to be transported over the HTTP/1 protocol + * An HttpChannel customized to be transported over the HTTP/1 protocol */ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.RequestHandler, ComplianceViolation.Listener { @@ -408,7 +408,7 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque } /** - * Attempts to perform a HTTP/1.1 upgrade.
+ *Attempts to perform an HTTP/1.1 upgrade.
*The upgrade looks up a {@link ConnectionFactory.Upgrading} from the connector * matching the protocol specified in the {@code Upgrade} header.
*The upgrade may succeed, be ignored (which can allow a later handler to implement) 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 75e37a8763a..e55f1878498 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 @@ -21,9 +21,7 @@ package org.eclipse.jetty.server; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; import javax.servlet.AsyncListener; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletResponse; import javax.servlet.UnavailableException; @@ -32,14 +30,16 @@ import org.eclipse.jetty.http.BadMessageException; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -import org.eclipse.jetty.util.thread.Locker; import org.eclipse.jetty.util.thread.Scheduler; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION; import static javax.servlet.RequestDispatcher.ERROR_EXCEPTION_TYPE; import static javax.servlet.RequestDispatcher.ERROR_MESSAGE; +import static javax.servlet.RequestDispatcher.ERROR_REQUEST_URI; +import static javax.servlet.RequestDispatcher.ERROR_SERVLET_NAME; import static javax.servlet.RequestDispatcher.ERROR_STATUS_CODE; /** @@ -51,21 +51,77 @@ public class HttpChannelState private static final long DEFAULT_TIMEOUT = Long.getLong("org.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT", 30000L); - /** + /* * The state of the HttpChannel,used to control the overall lifecycle. + *
+ * IDLE <-----> HANDLING ----> WAITING + * | ^ / + * | \ / + * v \ v + * UPGRADED WOKEN + **/ public enum State { - IDLE, // Idle request - DISPATCHED, // Request dispatched to filter/servlet - THROWN, // Exception thrown while DISPATCHED - ASYNC_WAIT, // Suspended and waiting - ASYNC_WOKEN, // Dispatch to handle from ASYNC_WAIT - ASYNC_IO, // Dispatched for async IO - ASYNC_ERROR, // Async error from ASYNC_WAIT - COMPLETING, // Response is completable - COMPLETED, // Response is completed - UPGRADED // Request upgraded the connection + IDLE, // Idle request + HANDLING, // Request dispatched to filter/servlet or Async IO callback + WAITING, // Suspended and waiting + WOKEN, // Dispatch to handle from ASYNC_WAIT + UPGRADED // Request upgraded the connection + } + + /* + * The state of the request processing lifecycle. + *+ * BLOCKING <----> COMPLETING ---> COMPLETED + * ^ | ^ ^ + * / | \ | + * | | DISPATCH | + * | | ^ ^ | + * | v / | | + * | ASYNC -------> COMPLETE + * | | | ^ + * | v | | + * | EXPIRE | | + * \ | / | + * \ v / | + * EXPIRING ----------+ + *+ */ + private enum RequestState + { + BLOCKING, // Blocking request dispatched + ASYNC, // AsyncContext.startAsync() has been called + DISPATCH, // AsyncContext.dispatch() has been called + EXPIRE, // AsyncContext timeout has happened + EXPIRING, // AsyncListeners are being called + COMPLETE, // AsyncContext.complete() has been called + COMPLETING, // Request is being closed (maybe asynchronously) + COMPLETED // Response is completed + } + + /* + * The input readiness state, which works together with {@link HttpInput.State} + */ + 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 + } + + /* + * The output committed state, which works together with {@link HttpOutput.State} + */ + private enum OutputState + { + OPEN, + COMMITTED, + COMPLETED, + ABORTED, } /** @@ -73,51 +129,28 @@ public class HttpChannelState */ public enum Action { - NOOP, // No action DISPATCH, // handle a normal request dispatch ASYNC_DISPATCH, // handle an async request dispatch - ERROR_DISPATCH, // handle a normal error + SEND_ERROR, // Generate an error page or error dispatch 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 + COMPLETE, // Complete the response by closing output TERMINATED, // No further actions WAIT, // Wait for further events } - /** - * The state of the servlet async API. - */ - private enum Async - { - NOT_ASYNC, - STARTED, // AsyncContext.startAsync() has been called - DISPATCH, // AsyncContext.dispatch() has been called - COMPLETE, // AsyncContext.complete() has been called - EXPIRING, // AsyncContext timeout just happened - EXPIRED, // AsyncContext timeout has been processed - ERRORING, // An error just happened - ERRORED // The error has been processed - } - - private enum AsyncRead - { - 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 - } - - private final Locker _locker = new Locker(); private final HttpChannel _channel; private List_asyncListeners; - private State _state; - private Async _async; - private boolean _initial; - private AsyncRead _asyncRead = AsyncRead.IDLE; + private State _state = State.IDLE; + private RequestState _requestState = RequestState.BLOCKING; + private OutputState _outputState = OutputState.OPEN; + private InputState _inputState = InputState.IDLE; + private boolean _initial = true; + private boolean _sendError; private boolean _asyncWritePossible; private long _timeoutMs = DEFAULT_TIMEOUT; private AsyncContextEvent _event; @@ -125,14 +158,11 @@ public class HttpChannelState protected HttpChannelState(HttpChannel channel) { _channel = channel; - _state = State.IDLE; - _async = Async.NOT_ASYNC; - _initial = true; } public State getState() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state; } @@ -140,7 +170,7 @@ public class HttpChannelState public void addListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) _asyncListeners = new ArrayList<>(); @@ -150,7 +180,7 @@ public class HttpChannelState public boolean hasListener(AsyncListener listener) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (_asyncListeners == null) return false; @@ -167,9 +197,17 @@ public class HttpChannelState } } + public boolean isSendError() + { + synchronized (this) + { + return _sendError; + } + } + public void setTimeout(long ms) { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { _timeoutMs = ms; } @@ -177,7 +215,7 @@ public class HttpChannelState public long getTimeout() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _timeoutMs; } @@ -185,7 +223,7 @@ public class HttpChannelState public AsyncContextEvent getAsyncContextEvent() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _event; } @@ -194,43 +232,139 @@ public class HttpChannelState @Override public String toString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return toStringLocked(); } } - public String toStringLocked() + private String toStringLocked() { - return String.format("%s@%x{s=%s a=%s i=%b r=%s w=%b}", + return String.format("%s@%x{%s}", getClass().getSimpleName(), hashCode(), - _state, - _async, - _initial, - _asyncRead, - _asyncWritePossible); + getStatusStringLocked()); } private String getStatusStringLocked() { - return String.format("s=%s i=%b a=%s", _state, _initial, _async); + return String.format("s=%s rs=%s os=%s is=%s awp=%b se=%b i=%b al=%d", + _state, + _requestState, + _outputState, + _inputState, + _asyncWritePossible, + _sendError, + _initial, + _asyncListeners == null ? 0 : _asyncListeners.size()); } public String getStatusString() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return getStatusStringLocked(); } } + public boolean commitResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + _outputState = OutputState.COMMITTED; + return true; + + default: + return false; + } + } + } + + public boolean partialResponse() + { + synchronized (this) + { + switch (_outputState) + { + case COMMITTED: + _outputState = OutputState.OPEN; + return true; + + default: + return false; + } + } + } + + public boolean completeResponse() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + case COMMITTED: + _outputState = OutputState.COMPLETED; + return true; + + default: + return false; + } + } + } + + public boolean isResponseCommitted() + { + synchronized (this) + { + switch (_outputState) + { + case OPEN: + return false; + default: + return true; + } + } + } + + public boolean isResponseCompleted() + { + synchronized (this) + { + return _outputState == OutputState.COMPLETED; + } + } + + public boolean abortResponse() + { + synchronized (this) + { + switch (_outputState) + { + case ABORTED: + return false; + + case OPEN: + _channel.getResponse().setStatus(500); + _outputState = OutputState.ABORTED; + return true; + + default: + _outputState = OutputState.ABORTED; + return true; + } + } + } + /** * @return Next handling of the request should proceed */ - protected Action handling() + public Action handling() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("handling {}", toStringLocked()); @@ -238,90 +372,169 @@ public class HttpChannelState switch (_state) { case IDLE: + if (_requestState != RequestState.BLOCKING) + throw new IllegalStateException(getStatusStringLocked()); _initial = true; - _state = State.DISPATCHED; + _state = State.HANDLING; return Action.DISPATCH; - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case ASYNC_WOKEN: - switch (_asyncRead) + case WOKEN: + if (_event != null && _event.getThrowable() != null && !_sendError) { - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - case REGISTER: - case PRODUCING: - case IDLE: - case REGISTERED: - break; - default: - throw new IllegalStateException(getStatusStringLocked()); + _state = State.HANDLING; + return Action.ASYNC_ERROR; } - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } + Action action = nextAction(true); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(true) {} {}", action, toStringLocked()); + return action; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - return Action.COMPLETE; - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - case EXPIRED: - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - case STARTED: - case EXPIRING: - case ERRORING: - _state = State.ASYNC_WAIT; - return Action.NOOP; - case NOT_ASYNC: - default: - throw new IllegalStateException(getStatusStringLocked()); - } - - case ASYNC_ERROR: - return Action.ASYNC_ERROR; - - case ASYNC_IO: - case ASYNC_WAIT: - case DISPATCHED: - case UPGRADED: default: throw new IllegalStateException(getStatusStringLocked()); } } } + /** + * Signal that the HttpConnection has finished handling the request. + * For blocking connectors, this call may block if the request has + * been suspended (startAsync called). + * + * @return next actions + * be handled again (eg because of a resume that happened before unhandle was called) + */ + protected Action unhandle() + { + boolean readInterested = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("unhandle {}", toStringLocked()); + + if (_state != State.HANDLING) + throw new IllegalStateException(this.getStatusStringLocked()); + + _initial = false; + + Action action = nextAction(false); + if (LOG.isDebugEnabled()) + LOG.debug("nextAction(false) {} {}", action, toStringLocked()); + return action; + } + } + + private Action nextAction(boolean handling) + { + // Assume we can keep going, but exceptions are below + _state = State.HANDLING; + + if (_sendError) + { + switch (_requestState) + { + case BLOCKING: + case ASYNC: + case COMPLETE: + case DISPATCH: + case COMPLETING: + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + default: + break; + } + } + + switch (_requestState) + { + case BLOCKING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case ASYNC: + switch (_inputState) + { + case POSSIBLE: + _inputState = InputState.PRODUCING; + return Action.READ_PRODUCE; + 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()); + } + + if (_asyncWritePossible) + { + _asyncWritePossible = false; + return Action.WRITE_CALLBACK; + } + + Scheduler scheduler = _channel.getScheduler(); + if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) + _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); + _state = State.WAITING; + return Action.WAIT; + + case DISPATCH: + _requestState = RequestState.BLOCKING; + return Action.ASYNC_DISPATCH; + + case EXPIRE: + _requestState = RequestState.EXPIRING; + return Action.ASYNC_TIMEOUT; + + case EXPIRING: + if (handling) + throw new IllegalStateException(getStatusStringLocked()); + sendError(HttpStatus.INTERNAL_SERVER_ERROR_500, "AsyncContext timeout"); + // handle sendError immediately + _requestState = RequestState.BLOCKING; + _sendError = false; + return Action.SEND_ERROR; + + case COMPLETE: + _requestState = RequestState.COMPLETING; + return Action.COMPLETE; + + case COMPLETING: + _state = State.WAITING; + return Action.WAIT; + + case COMPLETED: + _state = State.IDLE; + return Action.TERMINATED; + + default: + throw new IllegalStateException(getStatusStringLocked()); + } + } + public void startAsync(AsyncContextEvent event) { final List lastAsyncListeners; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("startAsync {}", toStringLocked()); - if (_state != State.DISPATCHED || _async != Async.NOT_ASYNC) + if (_state != State.HANDLING || _requestState != RequestState.BLOCKING) throw new IllegalStateException(this.getStatusStringLocked()); - _async = Async.STARTED; + _requestState = RequestState.ASYNC; _event = event; lastAsyncListeners = _asyncListeners; _asyncListeners = null; @@ -359,219 +572,36 @@ public class HttpChannelState } } - public void asyncError(Throwable failure) - { - AsyncContextEvent event = null; - try (Locker.Lock lock = _locker.lock()) - { - switch (_state) - { - case IDLE: - case DISPATCHED: - case COMPLETING: - case COMPLETED: - case UPGRADED: - case ASYNC_IO: - case ASYNC_WOKEN: - case ASYNC_ERROR: - { - break; - } - case ASYNC_WAIT: - { - _event.addThrowable(failure); - _state = State.ASYNC_ERROR; - event = _event; - break; - } - default: - { - throw new IllegalStateException(getStatusStringLocked()); - } - } - } - - if (event != null) - { - cancelTimeout(event); - runInContext(event, _channel); - } - } - - /** - * Signal that the HttpConnection has finished handling the request. - * For blocking connectors, this call may block if the request has - * been suspended (startAsync called). - * - * @return next actions - * be handled again (eg because of a resume that happened before unhandle was called) - */ - protected Action unhandle() - { - boolean readInterested = false; - - try (Locker.Lock lock = _locker.lock()) - { - if (LOG.isDebugEnabled()) - LOG.debug("unhandle {}", toStringLocked()); - - switch (_state) - { - case COMPLETING: - case COMPLETED: - return Action.TERMINATED; - - case THROWN: - _state = State.DISPATCHED; - return Action.ERROR_DISPATCH; - - case DISPATCHED: - case ASYNC_IO: - case ASYNC_ERROR: - case ASYNC_WAIT: - break; - - default: - throw new IllegalStateException(this.getStatusStringLocked()); - } - - _initial = false; - switch (_async) - { - case COMPLETE: - _state = State.COMPLETING; - _async = Async.NOT_ASYNC; - return Action.COMPLETE; - - case DISPATCH: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ASYNC_DISPATCH; - - case STARTED: - switch (_asyncRead) - { - case READY: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.IDLE; - return Action.READ_CALLBACK; - - case POSSIBLE: - _state = State.ASYNC_IO; - _asyncRead = AsyncRead.PRODUCING; - return Action.READ_PRODUCE; - - case REGISTER: - case PRODUCING: - _asyncRead = AsyncRead.REGISTERED; - readInterested = true; - break; - - case IDLE: - case REGISTERED: - break; - - default: - throw new IllegalStateException(_asyncRead.toString()); - } - - if (_asyncWritePossible) - { - _state = State.ASYNC_IO; - _asyncWritePossible = false; - return Action.WRITE_CALLBACK; - } - else - { - _state = State.ASYNC_WAIT; - - Scheduler scheduler = _channel.getScheduler(); - if (scheduler != null && _timeoutMs > 0 && !_event.hasTimeoutTask()) - _event.setTimeoutTask(scheduler.schedule(_event, _timeoutMs, TimeUnit.MILLISECONDS)); - - return Action.WAIT; - } - - case EXPIRING: - // onTimeout callbacks still being called, so just WAIT - _state = State.ASYNC_WAIT; - return Action.WAIT; - - case EXPIRED: - // onTimeout handling is complete, but did not dispatch as - // we were handling. So do the error dispatch here - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case ERRORED: - _state = State.DISPATCHED; - _async = Async.NOT_ASYNC; - return Action.ERROR_DISPATCH; - - case NOT_ASYNC: - _state = State.COMPLETING; - return Action.COMPLETE; - - default: - _state = State.COMPLETING; - return Action.COMPLETE; - } - } - finally - { - if (readInterested) - _channel.onAsyncWaitForContent(); - } - } - public void dispatch(ServletContext context, String path) { boolean dispatch = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("dispatch {} -> {}", toStringLocked(), path); - boolean started = false; - event = _event; - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; + case ASYNC: case EXPIRING: - case ERRORING: - case ERRORED: break; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.DISPATCH; if (context != null) _event.setDispatchContext(context); if (path != null) _event.setDispatchPath(path); - if (started) + if (_requestState == RequestState.ASYNC && _state == State.WAITING) { - switch (_state) - { - case DISPATCHED: - case ASYNC_IO: - case ASYNC_WOKEN: - break; - case ASYNC_WAIT: - _state = State.ASYNC_WOKEN; - dispatch = true; - break; - default: - LOG.warn("async dispatched when complete {}", this); - break; - } + _state = State.WOKEN; + dispatch = true; } + _requestState = RequestState.DISPATCH; + event = _event; } cancelTimeout(event); @@ -579,23 +609,47 @@ public class HttpChannelState scheduleDispatch(); } + protected void timeout() + { + boolean dispatch = false; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("Timeout {}", toStringLocked()); + + if (_requestState != RequestState.ASYNC) + return; + _requestState = RequestState.EXPIRE; + + if (_state == State.WAITING) + { + _state = State.WOKEN; + dispatch = true; + } + } + + if (dispatch) + { + if (LOG.isDebugEnabled()) + LOG.debug("Dispatch after async timeout {}", this); + scheduleDispatch(); + } + } + protected void onTimeout() { final List listeners; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onTimeout {}", toStringLocked()); - - if (_async != Async.STARTED) - return; - _async = Async.EXPIRING; + if (_requestState != RequestState.EXPIRING || _state != State.HANDLING) + throw new IllegalStateException(toStringLocked()); event = _event; listeners = _asyncListeners; } - final AtomicReference error = new AtomicReference<>(); if (listeners != null) { Runnable task = new Runnable() @@ -613,11 +667,6 @@ public class HttpChannelState { LOG.warn(x + " while invoking onTimeout listener " + listener); LOG.debug(x); - Throwable failure = error.get(); - if (failure == null) - error.set(x); - else if (x != failure) - failure.addSuppressed(x); } } } @@ -631,86 +680,34 @@ public class HttpChannelState runInContext(event, task); } - - Throwable th = error.get(); - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case EXPIRING: - _async = th == null ? Async.EXPIRED : Async.ERRORING; - break; - - case COMPLETE: - case DISPATCH: - if (th != null) - { - LOG.ignore(th); - th = null; - } - break; - - default: - throw new IllegalStateException(); - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (th != null) - { - if (LOG.isDebugEnabled()) - LOG.debug("Error after async timeout {}", this, th); - onError(th); - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after async timeout {}", this); - scheduleDispatch(); - } } public void complete() { - - // just like resume, except don't set _dispatched=true; boolean handle = false; AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("complete {}", toStringLocked()); - boolean started = false; event = _event; - - switch (_async) + switch (_requestState) { - case STARTED: - started = true; - break; case EXPIRING: - case ERRORING: - case ERRORED: + case ASYNC: + _requestState = _sendError ? RequestState.BLOCKING : RequestState.COMPLETE; break; + case COMPLETE: return; default: throw new IllegalStateException(this.getStatusStringLocked()); } - _async = Async.COMPLETE; - - if (started && _state == State.ASYNC_WAIT) + if (_state == State.WAITING) { handle = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } @@ -719,31 +716,132 @@ public class HttpChannelState runInContext(event, _channel); } - public void errorComplete() + public void asyncError(Throwable failure) { - try (Locker.Lock lock = _locker.lock()) + // This method is called when an failure occurs asynchronously to + // normal handling. If the request is async, we arrange for the + // exception to be thrown from the normal handling loop and then + // actually handled by #thrownException + + AsyncContextEvent event = null; + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("error complete {}", toStringLocked()); + LOG.debug("asyncError " + toStringLocked(), failure); - _async = Async.COMPLETE; - _event.setDispatchContext(null); - _event.setDispatchPath(null); + if (_state == State.WAITING && _requestState == RequestState.ASYNC) + { + _state = State.WOKEN; + _event.addThrowable(failure); + event = _event; + } + else + { + LOG.warn(failure.toString()); + LOG.debug(failure); + } } - cancelTimeout(); + if (event != null) + { + cancelTimeout(event); + runInContext(event, _channel); + } } protected void onError(Throwable th) { - final List listeners; - final AsyncContextEvent event; - final Request baseRequest = _channel.getRequest(); + final AsyncContextEvent asyncEvent; + final List asyncListeners; + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("thrownException " + getStatusStringLocked(), th); - int code = HttpStatus.INTERNAL_SERVER_ERROR_500; - String message = null; + // This can only be called from within the handle loop + if (_state != State.HANDLING) + throw new IllegalStateException(getStatusStringLocked()); + + // If sendError has already been called, we can only handle one failure at a time! + if (_sendError) + { + LOG.warn("unhandled due to prior sendError", th); + return; + } + + // Check async state to determine type of handling + switch (_requestState) + { + case BLOCKING: + // handle the exception with a sendError + sendError(th); + return; + + case DISPATCH: // Dispatch has already been called but we ignore and handle exception below + case COMPLETE: // Complete has already been called but we ignore and handle exception below + case ASYNC: + if (_asyncListeners == null || _asyncListeners.isEmpty()) + { + sendError(th); + return; + } + asyncEvent = _event; + asyncEvent.addThrowable(th); + asyncListeners = _asyncListeners; + break; + + default: + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + return; + } + } + + // If we are async and have async listeners + // call onError + runInContext(asyncEvent, () -> + { + for (AsyncListener listener : asyncListeners) + { + try + { + listener.onError(asyncEvent); + } + catch (Throwable x) + { + LOG.warn(x + " while invoking onError listener " + listener); + LOG.debug(x); + } + } + }); + + // check the actions of the listeners + synchronized (this) + { + // If we are still async and nobody has called sendError + if (_requestState == RequestState.ASYNC && !_sendError) + // Then the listeners did not invoke API methods + // and the container must provide a default error dispatch. + sendError(th); + else + LOG.warn("unhandled in state " + _requestState, new IllegalStateException(th)); + } + } + + private void sendError(Throwable th) + { + // No sync as this is always called with lock held + + // Determine the actual details of the exception + final Request request = _channel.getRequest(); + final int code; + final String message; Throwable cause = _channel.unwrap(th, BadMessageException.class, UnavailableException.class); - if (cause instanceof BadMessageException) + if (cause == null) + { + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = th.toString(); + } + else if (cause instanceof BadMessageException) { BadMessageException bme = (BadMessageException)cause; code = bme.getCode(); @@ -751,196 +849,175 @@ public class HttpChannelState } else if (cause instanceof UnavailableException) { + message = cause.toString(); if (((UnavailableException)cause).isPermanent()) code = HttpStatus.NOT_FOUND_404; else code = HttpStatus.SERVICE_UNAVAILABLE_503; } - - try (Locker.Lock lock = _locker.lock()) + else { - if (LOG.isDebugEnabled()) - LOG.debug("onError {} {}", toStringLocked(), th); - - // Set error on request. - if (_event != null) - { - _event.addThrowable(th); - _event.getSuppliedRequest().setAttribute(ERROR_STATUS_CODE, code); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION, th); - _event.getSuppliedRequest().setAttribute(ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - _event.getSuppliedRequest().setAttribute(ERROR_MESSAGE, message); - } - else - { - Throwable error = (Throwable)baseRequest.getAttribute(ERROR_EXCEPTION); - if (error != null) - throw new IllegalStateException("Error already set", error); - baseRequest.setAttribute(ERROR_STATUS_CODE, code); - baseRequest.setAttribute(ERROR_EXCEPTION, th); - baseRequest.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, th == null ? null : th.getClass()); - baseRequest.setAttribute(ERROR_MESSAGE, message); - } - - // Are we blocking? - if (_async == Async.NOT_ASYNC) - { - // Only called from within HttpChannel Handling, so much be dispatched, let's stay dispatched! - if (_state == State.DISPATCHED) - { - _state = State.THROWN; - return; - } - throw new IllegalStateException(this.getStatusStringLocked()); - } - - // We are Async - _async = Async.ERRORING; - listeners = _asyncListeners; - event = _event; + code = HttpStatus.INTERNAL_SERVER_ERROR_500; + message = null; } - if (listeners != null) - { - Runnable task = new Runnable() - { - @Override - public void run() - { - for (AsyncListener listener : listeners) - { - try - { - listener.onError(event); - } - catch (Throwable x) - { - LOG.warn(x + " while invoking onError listener " + listener); - LOG.debug(x); - } - } - } + sendError(code, message); - @Override - public String toString() - { - return "onError"; - } - }; - runInContext(event, task); - } - - boolean dispatch = false; - try (Locker.Lock lock = _locker.lock()) - { - switch (_async) - { - case ERRORING: - { - // Still in this state ? The listeners did not invoke API methods - // and the container must provide a default error dispatch. - _async = Async.ERRORED; - break; - } - case DISPATCH: - case COMPLETE: - { - // The listeners called dispatch() or complete(). - break; - } - default: - { - throw new IllegalStateException(toString()); - } - } - - if (_state == State.ASYNC_WAIT) - { - _state = State.ASYNC_WOKEN; - dispatch = true; - } - } - - if (dispatch) - { - if (LOG.isDebugEnabled()) - LOG.debug("Dispatch after error {}", this); - scheduleDispatch(); - } + // No ISE, so good to modify request/state + request.setAttribute(ERROR_EXCEPTION, th); + request.setAttribute(ERROR_EXCEPTION_TYPE, th.getClass()); + // Ensure any async lifecycle is ended! + _requestState = RequestState.BLOCKING; } - protected void onComplete() + public void sendError(int code, String message) { - final List aListeners; - final AsyncContextEvent event; + // This method is called by Response.sendError to organise for an error page to be generated when it is possible: + // + The response is reset and temporarily closed. + // + The details of the error are saved as request attributes + // + The _sendError boolean is set to true so that an ERROR_DISPATCH action will be generated: + // - after unhandle for sync + // - after both unhandle and complete for async - try (Locker.Lock lock = _locker.lock()) + final Request request = _channel.getRequest(); + final Response response = _channel.getResponse(); + if (message == null) + message = HttpStatus.getMessage(code); + + synchronized (this) { if (LOG.isDebugEnabled()) - LOG.debug("onComplete {}", toStringLocked()); + LOG.debug("sendError {}", toStringLocked()); switch (_state) { - case COMPLETING: - aListeners = _asyncListeners; - event = _event; - _state = State.COMPLETED; - _async = Async.NOT_ASYNC; + case HANDLING: + case WOKEN: + case WAITING: break; - default: - throw new IllegalStateException(this.getStatusStringLocked()); + throw new IllegalStateException(getStatusStringLocked()); + } + if (_outputState != OutputState.OPEN) + throw new IllegalStateException("Response is " + _outputState); + + response.getHttpOutput().closedBySendError(); + response.setStatus(code); + + request.setAttribute(ErrorHandler.ERROR_CONTEXT, request.getErrorContext()); + request.setAttribute(ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(ERROR_SERVLET_NAME, request.getServletName()); + request.setAttribute(ERROR_STATUS_CODE, code); + request.setAttribute(ERROR_MESSAGE, message); + + _sendError = true; + if (_event != null) + { + Throwable cause = (Throwable)request.getAttribute(ERROR_EXCEPTION); + if (cause != null) + _event.addThrowable(cause); + } + } + } + + protected void completing() + { + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completing {}", toStringLocked()); + + switch (_requestState) + { + case COMPLETED: + throw new IllegalStateException(getStatusStringLocked()); + default: + _requestState = RequestState.COMPLETING; + } + } + } + + protected void completed() + { + final List aListeners; + final AsyncContextEvent event; + boolean handle = false; + + synchronized (this) + { + if (LOG.isDebugEnabled()) + LOG.debug("completed {}", toStringLocked()); + + if (_requestState != RequestState.COMPLETING) + throw new IllegalStateException(this.getStatusStringLocked()); + + if (_event == null) + { + _requestState = RequestState.COMPLETED; + aListeners = null; + event = null; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } + else + { + aListeners = _asyncListeners; + event = _event; } } if (event != null) { + cancelTimeout(event); if (aListeners != null) { - Runnable callback = new Runnable() + runInContext(event, () -> { - @Override - public void run() + for (AsyncListener listener : aListeners) { - for (AsyncListener listener : aListeners) + try { - try - { - listener.onComplete(event); - } - catch (Throwable e) - { - LOG.warn(e + " while invoking onComplete listener " + listener); - LOG.debug(e); - } + listener.onComplete(event); + } + catch (Throwable e) + { + LOG.warn(e + " while invoking onComplete listener " + listener); + LOG.debug(e); } } - - @Override - public String toString() - { - return "onComplete"; - } - }; - - runInContext(event, callback); + }); } event.completed(); + + synchronized (this) + { + _requestState = RequestState.COMPLETED; + if (_state == State.WAITING) + { + _state = State.WOKEN; + handle = true; + } + } } + + if (handle) + _channel.handle(); } protected void recycle() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("recycle {}", toStringLocked()); switch (_state) { - case DISPATCHED: - case ASYNC_IO: + case HANDLING: throw new IllegalStateException(getStatusStringLocked()); case UPGRADED: return; @@ -949,9 +1026,10 @@ public class HttpChannelState } _asyncListeners = null; _state = State.IDLE; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; + _outputState = OutputState.OPEN; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -961,7 +1039,7 @@ public class HttpChannelState public void upgrade() { cancelTimeout(); - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("upgrade {}", toStringLocked()); @@ -969,16 +1047,15 @@ public class HttpChannelState switch (_state) { case IDLE: - case COMPLETED: break; default: throw new IllegalStateException(getStatusStringLocked()); } _asyncListeners = null; _state = State.UPGRADED; - _async = Async.NOT_ASYNC; + _requestState = RequestState.BLOCKING; _initial = true; - _asyncRead = AsyncRead.IDLE; + _inputState = InputState.IDLE; _asyncWritePossible = false; _timeoutMs = DEFAULT_TIMEOUT; _event = null; @@ -993,7 +1070,7 @@ public class HttpChannelState protected void cancelTimeout() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1008,7 +1085,7 @@ public class HttpChannelState public boolean isIdle() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _state == State.IDLE; } @@ -1016,15 +1093,16 @@ public class HttpChannelState public boolean isExpired() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _async == Async.EXPIRED; + // TODO review + return _requestState == RequestState.EXPIRE || _requestState == RequestState.EXPIRING; } } public boolean isInitial() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { return _initial; } @@ -1032,51 +1110,35 @@ public class HttpChannelState public boolean isSuspended() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.ASYNC_WAIT || _state == State.DISPATCHED && _async == Async.STARTED; - } - } - - boolean isCompleting() - { - try (Locker.Lock lock = _locker.lock()) - { - return _state == State.COMPLETING; + return _state == State.WAITING || _state == State.HANDLING && _requestState == RequestState.ASYNC; } } boolean isCompleted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return _state == State.COMPLETED; + return _requestState == RequestState.COMPLETED; } } public boolean isAsyncStarted() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - if (_state == State.DISPATCHED) - return _async != Async.NOT_ASYNC; - return _async == Async.STARTED || _async == Async.EXPIRING; - } - } - - public boolean isAsyncComplete() - { - try (Locker.Lock lock = _locker.lock()) - { - return _async == Async.COMPLETE; + if (_state == State.HANDLING) + return _requestState != RequestState.BLOCKING; + return _requestState == RequestState.ASYNC || _requestState == RequestState.EXPIRING; } } public boolean isAsync() { - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { - return !_initial || _async != Async.NOT_ASYNC; + return !_initial || _requestState != RequestState.BLOCKING; } } @@ -1093,7 +1155,7 @@ public class HttpChannelState public ContextHandler getContextHandler() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1114,7 +1176,7 @@ public class HttpChannelState public ServletResponse getServletResponse() { final AsyncContextEvent event; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { event = _event; } @@ -1162,23 +1224,23 @@ public class HttpChannelState public void onReadUnready() { boolean interested = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadUnready {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { interested = true; - _asyncRead = AsyncRead.REGISTERED; + _inputState = InputState.REGISTERED; } else { - _asyncRead = AsyncRead.REGISTER; + _inputState = InputState.REGISTER; } break; @@ -1187,8 +1249,9 @@ public class HttpChannelState case POSSIBLE: case PRODUCING: break; + default: - throw new IllegalStateException(_asyncRead.toString()); + throw new IllegalStateException(toStringLocked()); } } @@ -1207,28 +1270,28 @@ public class HttpChannelState public boolean onContentAdded() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onContentAdded {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: case READY: break; case PRODUCING: - _asyncRead = AsyncRead.READY; + _inputState = InputState.READY; break; case REGISTER: case REGISTERED: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1250,19 +1313,19 @@ public class HttpChannelState public boolean onReadReady() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadReady {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case IDLE: - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1283,19 +1346,19 @@ public class HttpChannelState public boolean onReadPossible() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onReadPossible {}", toStringLocked()); - switch (_asyncRead) + switch (_inputState) { case REGISTERED: - _asyncRead = AsyncRead.POSSIBLE; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.POSSIBLE; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } break; @@ -1315,17 +1378,17 @@ public class HttpChannelState public boolean onReadEof() { boolean woken = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onEof {}", toStringLocked()); // Force read ready so onAllDataRead can be called - _asyncRead = AsyncRead.READY; - if (_state == State.ASYNC_WAIT) + _inputState = InputState.READY; + if (_state == State.WAITING) { woken = true; - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; } } return woken; @@ -1335,15 +1398,15 @@ public class HttpChannelState { boolean wake = false; - try (Locker.Lock lock = _locker.lock()) + synchronized (this) { if (LOG.isDebugEnabled()) LOG.debug("onWritePossible {}", toStringLocked()); _asyncWritePossible = true; - if (_state == State.ASYNC_WAIT) + if (_state == State.WAITING) { - _state = State.ASYNC_WOKEN; + _state = State.WOKEN; wake = true; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java index 3c2cb772238..619f5741a57 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConfiguration.java @@ -38,7 +38,7 @@ import org.eclipse.jetty.util.component.DumpableCollection; /** * HTTP Configuration. * This class is a holder of HTTP configuration for use by the - * {@link HttpChannel} class. Typically a HTTPConfiguration instance + * {@link HttpChannel} class. Typically an HTTPConfiguration instance * is instantiated and passed to a {@link ConnectionFactory} that can * create HTTP channels (e.g. HTTP, AJP or FCGI).
*The configuration held by this class is not for the wire protocol, @@ -183,19 +183,19 @@ public class HttpConfiguration implements Dumpable return _outputAggregationSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP request header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP request header") public int getRequestHeaderSize() { return _requestHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP response header") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP response header") public int getResponseHeaderSize() { return _responseHeaderSize; } - @ManagedAttribute("The maximum allowed size in bytes for a HTTP header field cache") + @ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache") public int getHeaderCacheSize() { return _headerCacheSize; @@ -226,20 +226,20 @@ public class HttpConfiguration implements Dumpable } /** - *
The max idle time is applied to a HTTP request for IO operations and + *
The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.
* * @return the max idle time in ms or if == 0 implies an infinite timeout, <0 * implies no HTTP channel timeout and the connection timeout is used instead. */ - @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of a HTTP request") + @ManagedAttribute("The idle timeout in ms for I/O operations during the handling of an HTTP request") public long getIdleTimeout() { return _idleTimeout; } /** - *The max idle time is applied to a HTTP request for IO operations and + *
The max idle time is applied to an HTTP request for IO operations and * delayed dispatch.
* * @param timeoutMs the max idle time in ms or if == 0 implies an infinite timeout, <0 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 8f5303c7737..9d8ac9edc35 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 @@ -274,18 +274,8 @@ public class HttpConnection extends AbstractConnection implements Runnable, Http } else if (filled < 0) { - switch (_channel.getState().getState()) - { - case COMPLETING: - case COMPLETED: - case IDLE: - case THROWN: - case ASYNC_ERROR: - getEndPoint().shutdownOutput(); - break; - default: - break; - } + if (_channel.getState().isIdle()) + getEndPoint().shutdownOutput(); break; } } 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 0b6a450d01c..151c1bc9467 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 @@ -274,7 +274,8 @@ public class HttpInput extends ServletInputStream implements Runnable { BadMessageException bad = new BadMessageException(HttpStatus.REQUEST_TIMEOUT_408, String.format("Request content data rate < %d B/s", minRequestDataRate)); - _channelState.getHttpChannel().abort(bad); + if (_channelState.isResponseCommitted()) + _channelState.getHttpChannel().abort(bad); throw bad; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java index a010a69e9a7..50089ae163c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -18,6 +18,7 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -62,8 +63,31 @@ import org.eclipse.jetty.util.log.Logger; public class HttpOutput extends ServletOutputStream implements Runnable { private static final String LSTRING_FILE = "javax.servlet.LocalStrings"; + private static final Callback BLOCKING_CLOSE_CALLBACK = new Callback() {}; private static ResourceBundle lStrings = ResourceBundle.getBundle(LSTRING_FILE); + /* + ACTION OPEN ASYNC READY PENDING UNREADY CLOSING CLOSED + -------------------------------------------------------------------------------------------------- + setWriteListener() READY->owp ise ise ise ise ise ise + write() OPEN ise PENDING wpe wpe eof eof + flush() OPEN ise PENDING wpe wpe eof eof + close() CLOSING CLOSING CLOSING CLOSED CLOSED CLOSING CLOSED + isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true CLOSED:true + write completed - - - ASYNC READY->owp CLOSED - + */ + enum State + { + OPEN, // Open in blocking mode + ASYNC, // Open in async mode + READY, // isReady() has returned true + PENDING, // write operating in progress + UNREADY, // write operating in progress, isReady has returned false + ERROR, // An error has occured + CLOSING, // Asynchronous close in progress + CLOSED // Closed + } + /** * The HttpOutput.Interceptor is a single intercept point for all * output written to the HttpOutput: via writer; via output stream; @@ -129,6 +153,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private static Logger LOG = Log.getLogger(HttpOutput.class); private static final ThreadLocal_encoder = new ThreadLocal<>(); + private final AtomicReference _state = new AtomicReference<>(State.OPEN); private final HttpChannel _channel; private final SharedBlockingCallback _writeBlocker; private Interceptor _interceptor; @@ -140,23 +165,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable private int _commitSize; private WriteListener _writeListener; private volatile Throwable _onError; - - /* - ACTION OPEN ASYNC READY PENDING UNREADY CLOSED - ------------------------------------------------------------------------------------------- - setWriteListener() READY->owp ise ise ise ise ise - write() OPEN ise PENDING wpe wpe eof - flush() OPEN ise PENDING wpe wpe eof - close() CLOSED CLOSED CLOSED CLOSED CLOSED CLOSED - isReady() OPEN:true READY:true READY:true UNREADY:false UNREADY:false CLOSED:true - write completed - - - ASYNC READY->owp - - */ - private enum OutputState - { - OPEN, ASYNC, READY, PENDING, UNREADY, ERROR, CLOSED - } - - private final AtomicReference _state = new AtomicReference<>(OutputState.OPEN); + private Callback _closeCallback; public HttpOutput(HttpChannel channel) { @@ -200,7 +209,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable public void reopen() { - _state.set(OutputState.OPEN); + _state.set(State.OPEN); } private boolean isLastContentToWrite(int len) @@ -225,28 +234,78 @@ public class HttpOutput extends ServletOutputStream implements Runnable _channel.abort(failure); } - @Override - public void close() + public void closedBySendError() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { + case OPEN: + case READY: + case ASYNC: + if (!_state.compareAndSet(state, State.CLOSED)) + continue; + return; + + default: + throw new IllegalStateException(state.toString()); + } + } + } + + public void close(Closeable wrapper, Callback callback) + { + _closeCallback = callback; + try + { + if (wrapper != null) + wrapper.close(); + if (!isClosed()) + close(); + } + catch (Throwable th) + { + closed(); + if (_closeCallback == null) + LOG.ignore(th); + else + callback.failed(th); + } + finally + { + if (_closeCallback != null) + callback.succeeded(); + _closeCallback = null; + } + } + + @Override + public void close() + { + Callback closeCallback = _closeCallback == null ? BLOCKING_CLOSE_CALLBACK : _closeCallback; + + while (true) + { + State state = _state.get(); + switch (state) + { + case CLOSING: case CLOSED: { + _closeCallback = null; + closeCallback.succeeded(); return; } case ASYNC: { // A close call implies a write operation, thus in asynchronous mode // a call to isReady() that returned true should have been made. - // However it is desirable to allow a close at any time, specially if - // complete is called. Thus we simulate a call to isReady here, assuming - // that we can transition to READY. - if (!_state.compareAndSet(state, OutputState.READY)) - continue; - break; + // However it is desirable to allow a close at any time, specially if + // complete is called. Thus we simulate a call to isReady here, by + // trying to move to READY state. Either way we continue. + _state.compareAndSet(state, State.READY); + continue; } case UNREADY: case PENDING: @@ -257,34 +316,45 @@ public class HttpOutput extends ServletOutputStream implements Runnable // complete is called. Because the prior write has not yet completed // and/or isReady has not been called, this close is allowed, but will // abort the response. - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) continue; IOException ex = new IOException("Closed while Pending/Unready"); LOG.warn(ex.toString()); LOG.debug(ex); abort(ex); + _closeCallback = null; + closeCallback.failed(ex); return; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSING)) continue; // Do a normal close by writing the aggregate buffer or an empty buffer. If we are // not including, then indicate this is the last write. try { - write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, !_channel.getResponse().isIncluding()); + ByteBuffer content = BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER; + if (closeCallback == BLOCKING_CLOSE_CALLBACK) + { + // Do a blocking close + write(content, !_channel.getResponse().isIncluding()); + _closeCallback = null; + closeCallback.succeeded(); + } + else + { + _closeCallback = null; + write(content, !_channel.getResponse().isIncluding(), closeCallback); + } } catch (IOException x) { LOG.ignore(x); // Ignore it, it's been already logged in write(). + _closeCallback = null; + closeCallback.failed(x); } - finally - { - releaseBuffer(); - } - // Return even if an exception is thrown by write(). return; } } @@ -295,11 +365,11 @@ public class HttpOutput extends ServletOutputStream implements Runnable * Called to indicate that the last write has been performed. * It updates the state and performs cleanup operations. */ - void closed() + public void closed() { while (true) { - OutputState state = _state.get(); + State state = _state.get(); switch (state) { case CLOSED: @@ -308,15 +378,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable } case UNREADY: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) _writeListener.onError(_onError == null ? new EofException("Async closed") : _onError); break; } default: { - if (!_state.compareAndSet(state, OutputState.CLOSED)) + if (!_state.compareAndSet(state, State.CLOSED)) break; + // Just make sure write and output stream really are closed try { _channel.getResponse().closeOutput(); @@ -338,6 +409,18 @@ public class HttpOutput extends ServletOutputStream implements Runnable } } + public ByteBuffer getBuffer() + { + return _aggregate; + } + + public ByteBuffer acquireBuffer() + { + if (_aggregate == null) + _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + return _aggregate; + } + private void releaseBuffer() { if (_aggregate != null) @@ -349,7 +432,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable public boolean isClosed() { - return _state.get() == OutputState.CLOSED; + switch (_state.get()) + { + case CLOSING: + case CLOSED: + return true; + default: + return false; + } } public boolean isAsync() @@ -371,7 +461,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: write(BufferUtil.hasContent(_aggregate) ? _aggregate : BufferUtil.EMPTY_BUFFER, false); @@ -381,25 +472,24 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; new AsyncFlush().iterate(); return; - case PENDING: - return; - case UNREADY: throw new WritePendingException(); case ERROR: throw new EofException(_onError); + case PENDING: + case CLOSING: case CLOSED: return; default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } } } @@ -441,7 +531,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -451,15 +542,14 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Should we aggregate? boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -467,8 +557,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // return if we are not complete, not full and filled all the content if (filled == len && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) - throw new IllegalStateException(); + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) + throw new IllegalStateException(_state.get().toString()); return; } @@ -488,11 +578,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -504,8 +595,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable boolean last = isLastContentToWrite(len); if (!last && len <= _commitSize) { - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(capacity, _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); // YES - fill the aggregate with content from the buffer int filled = BufferUtil.fill(_aggregate, b, off, len); @@ -554,9 +644,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable { write(BufferUtil.EMPTY_BUFFER, true); } - - if (last) - closed(); } public void write(ByteBuffer buffer) throws IOException @@ -566,7 +653,8 @@ public class HttpOutput extends ServletOutputStream implements Runnable // Async or Blocking ? while (true) { - switch (_state.get()) + State state = _state.get(); + switch (state) { case OPEN: // process blocking below @@ -576,7 +664,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(state, State.PENDING)) continue; // Do the asynchronous writing from the callback @@ -591,11 +679,12 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); default: - throw new IllegalStateException(); + throw new IllegalStateException(state.toString()); } break; } @@ -613,9 +702,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable write(buffer, last); else if (last) write(BufferUtil.EMPTY_BUFFER, true); - - if (last) - closed(); } @Override @@ -630,34 +716,28 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (complete || BufferUtil.isFull(_aggregate)) - { write(_aggregate, complete); - if (complete) - closed(); - } break; case ASYNC: throw new IllegalStateException("isReady() not called"); case READY: - if (!_state.compareAndSet(OutputState.READY, OutputState.PENDING)) + if (!_state.compareAndSet(State.READY, State.PENDING)) continue; - if (_aggregate == null) - _aggregate = _channel.getByteBufferPool().acquire(getBufferSize(), _interceptor.isOptimizedForDirectBuffers()); + acquireBuffer(); BufferUtil.append(_aggregate, (byte)b); // Check if all written or full if (!complete && !BufferUtil.isFull(_aggregate)) { - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) throw new IllegalStateException(); return; } @@ -673,6 +753,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable case ERROR: throw new EofException(_onError); + case CLOSING: case CLOSED: throw new EofException("Closed"); @@ -810,7 +891,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable _written += content.remaining(); write(content, true); - closed(); } /** @@ -966,7 +1046,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: - if (!_state.compareAndSet(OutputState.OPEN, OutputState.PENDING)) + if (!_state.compareAndSet(State.OPEN, State.PENDING)) continue; break; @@ -974,6 +1054,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable callback.failed(new EofException(_onError)); return; + case CLOSING: case CLOSED: callback.failed(new EofException("Closed")); return; @@ -1073,6 +1154,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable _onError = null; _firstByteTimeStamp = -1; _flushed = 0; + _closeCallback = null; reopen(); } @@ -1082,7 +1164,6 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (BufferUtil.hasContent(_aggregate)) BufferUtil.clear(_aggregate); _written = 0; - reopen(); } @Override @@ -1091,7 +1172,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable if (!_channel.getState().isAsync()) throw new IllegalStateException("!ASYNC"); - if (_state.compareAndSet(OutputState.OPEN, OutputState.READY)) + if (_state.compareAndSet(State.OPEN, State.READY)) { _writeListener = writeListener; if (_channel.getState().onWritePossible()) @@ -1109,30 +1190,25 @@ public class HttpOutput extends ServletOutputStream implements Runnable switch (_state.get()) { case OPEN: + case READY: + case ERROR: + case CLOSING: + case CLOSED: return true; case ASYNC: - if (!_state.compareAndSet(OutputState.ASYNC, OutputState.READY)) + if (!_state.compareAndSet(State.ASYNC, State.READY)) continue; return true; - case READY: - return true; - case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.UNREADY)) + if (!_state.compareAndSet(State.PENDING, State.UNREADY)) continue; return false; case UNREADY: return false; - case ERROR: - return true; - - case CLOSED: - return true; - default: throw new IllegalStateException(); } @@ -1144,12 +1220,13 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState state = _state.get(); + State state = _state.get(); if (_onError != null) { switch (state) { + case CLOSING: case CLOSED: case ERROR: { @@ -1158,7 +1235,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable } default: { - if (_state.compareAndSet(state, OutputState.ERROR)) + if (_state.compareAndSet(state, State.ERROR)) { Throwable th = _onError; _onError = null; @@ -1234,16 +1311,16 @@ public class HttpOutput extends ServletOutputStream implements Runnable { while (true) { - OutputState last = _state.get(); + State last = _state.get(); switch (last) { case PENDING: - if (!_state.compareAndSet(OutputState.PENDING, OutputState.ASYNC)) + if (!_state.compareAndSet(State.PENDING, State.ASYNC)) continue; break; case UNREADY: - if (!_state.compareAndSet(OutputState.UNREADY, OutputState.READY)) + if (!_state.compareAndSet(State.UNREADY, State.READY)) continue; if (_last) closed(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java index 71b40c0fa82..41118eb081d 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/OptionalSslConnectionFactory.java @@ -95,7 +95,7 @@ public class OptionalSslConnectionFactory extends AbstractConnectionFactory int byte2 = buffer.get(1) & 0xFF; if (byte1 == 'G' && byte2 == 'E') { - // Plain text HTTP to a HTTPS port, + // Plain text HTTP to an HTTPS port, // write a minimal response. String body = "\r\n" + 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 f4c996acc03..55444641720 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 @@ -212,6 +212,7 @@ public class Request implements HttpServletRequest private String _contentType; private String _characterEncoding; private ContextHandler.Context _context; + private ContextHandler.Context _errorContext; private Cookies _cookies; private DispatcherType _dispatcherType; private int _inputState = INPUT_NONE; @@ -759,6 +760,22 @@ public class Request implements HttpServletRequest return _context; } + /** + * @return The current {@link Context context} used for this error handling for this request. If the request is asynchronous, + * then it is the context that called async. Otherwise it is the last non-null context passed to #setContext + */ + public Context getErrorContext() + { + if (isAsyncStarted()) + { + ContextHandler handler = _channel.getState().getContextHandler(); + if (handler != null) + return handler.getServletContext(); + } + + return _errorContext; + } + /* * @see javax.servlet.http.HttpServletRequest#getContextPath() */ @@ -1846,6 +1863,7 @@ public class Request implements HttpServletRequest _remote = null; _sessions = null; _input.recycle(); + _requestAttributeListeners.clear(); } /* @@ -1983,6 +2001,7 @@ public class Request implements HttpServletRequest else { _context = context; + _errorContext = context; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java index 95a9d83e533..b6d58845de1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java @@ -32,7 +32,7 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; /** - * A HttpContent.Factory for transient content (not cached). The HttpContent's created by + * An HttpContent.Factory for transient content (not cached). The HttpContent's created by * this factory are not intended to be cached, so memory limits for individual * HttpOutput streams are enforced. */ diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index de8ec53ad74..d0f1f98da23 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -18,19 +18,19 @@ package org.eclipse.jetty.server; +import java.io.Closeable; import java.io.IOException; import java.io.PrintWriter; import java.nio.channels.IllegalSelectorException; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import java.util.List; +import java.util.Iterator; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; -import javax.servlet.RequestDispatcher; import javax.servlet.ServletOutputStream; import javax.servlet.ServletResponse; import javax.servlet.ServletResponseWrapper; @@ -58,9 +58,8 @@ import org.eclipse.jetty.http.MetaData; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.PreEncodedHttpField; import org.eclipse.jetty.io.RuntimeIOException; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.log.Log; @@ -379,71 +378,40 @@ public class Response implements HttpServletResponse sendError(sc, null); } + /** + * Send an error response. + * In addition to the servlet standard handling, this method supports some additional codes:
+ *+ *
+ * @param code The error code + * @param message The message + * @throws IOException If an IO problem occurred sending the error response. + */ @Override public void sendError(int code, String message) throws IOException { if (isIncluding()) return; - if (isCommitted()) - { - if (LOG.isDebugEnabled()) - LOG.debug("Aborting on sendError on committed response {} {}", code, message); - code = -1; - } - else - resetBuffer(); - switch (code) { case -1: - _channel.abort(new IOException()); - return; - case 102: + _channel.abort(new IOException(message)); + break; + case HttpStatus.PROCESSING_102: sendProcessing(); - return; + break; default: + _channel.getState().sendError(code, message); break; } - - _outputType = OutputType.NONE; - setContentType(null); - setCharacterEncoding(null); - setHeader(HttpHeader.EXPIRES, null); - setHeader(HttpHeader.LAST_MODIFIED, null); - setHeader(HttpHeader.CACHE_CONTROL, null); - setHeader(HttpHeader.CONTENT_TYPE, null); - setHeader(HttpHeader.CONTENT_LENGTH, null); - - setStatus(code); - - Request request = _channel.getRequest(); - Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); - _reason = HttpStatus.getMessage(code); - if (message == null) - message = cause == null ? _reason : cause.toString(); - - // If we are allowed to have a body, then produce the error page. - if (code != SC_NO_CONTENT && code != SC_NOT_MODIFIED && - code != SC_PARTIAL_CONTENT && code >= SC_OK) - { - ContextHandler.Context context = request.getContext(); - ContextHandler contextHandler = context == null ? _channel.getState().getContextHandler() : context.getContextHandler(); - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, code); - request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); - request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); - request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, request.getServletName()); - ErrorHandler errorHandler = ErrorHandler.getErrorHandler(_channel.getServer(), contextHandler); - if (errorHandler != null) - errorHandler.handle(null, request, request, this); - } - if (!request.isAsyncStarted()) - closeOutput(); } /** * Sends a 102-Processing response. - * If the connection is a HTTP connection, the version is 1.1 and the + * If the connection is an HTTP connection, the version is 1.1 and the * request has a Expect header starting with 102, then a 102 response is * sent. This indicates that the request still be processed and real response * can still be sent. This method is called by sendError if it is passed 102. @@ -650,8 +618,11 @@ public class Response implements HttpServletResponse throw new IllegalArgumentException(); if (!isIncluding()) { + // Null the reason only if the status is different. This allows + // a specific reason to be sent with setStatusWithReason followed by sendError. + if (_status != sc) + _reason = null; _status = sc; - _reason = null; } } @@ -714,6 +685,11 @@ public class Response implements HttpServletResponse return _outputType == OutputType.STREAM; } + public boolean isWritingOrStreaming() + { + return isWriting() || isStreaming(); + } + @Override public PrintWriter getWriter() throws IOException { @@ -822,21 +798,15 @@ public class Response implements HttpServletResponse public void closeOutput() throws IOException { - switch (_outputType) - { - case WRITER: - _writer.close(); - if (!_out.isClosed()) - _out.close(); - break; - case STREAM: - if (!_out.isClosed()) - getOutputStream().close(); - break; - default: - if (!_out.isClosed()) - _out.close(); - } + if (_outputType == OutputType.WRITER) + _writer.close(); + if (!_out.isClosed()) + _out.close(); + } + + public void closeOutput(Callback callback) + { + _out.close((_outputType == OutputType.WRITER) ? _writer : _out, callback); } public long getLongContentLength() @@ -1029,19 +999,20 @@ public class Response implements HttpServletResponse @Override public void reset() { - reset(false); - } - - public void reset(boolean preserveCookies) - { - resetForForward(); _status = 200; _reason = null; + _out.resetBuffer(); + _outputType = OutputType.NONE; _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; - List- 102
- Send a partial PROCESSING response and allow additional responses
+ *- -1
- Abort the HttpChannel and close the connection/stream
+ *cookies = preserveCookies ? _fields.getFields(HttpHeader.SET_COOKIE) : null; + // Clear all response headers _fields.clear(); + // recreate necessary connection related fields for (String value : _channel.getRequest().getHttpFields().getCSV(HttpHeader.CONNECTION, false)) { HttpHeaderValue cb = HttpHeaderValue.CACHE.get(value); @@ -1064,21 +1035,57 @@ public class Response implements HttpServletResponse } } - if (preserveCookies) - cookies.forEach(_fields::add); - else + // recreate session cookies + Request request = getHttpChannel().getRequest(); + HttpSession session = request.getSession(false); + if (session != null && session.isNew()) { - Request request = getHttpChannel().getRequest(); - HttpSession session = request.getSession(false); - if (session != null && session.isNew()) + SessionHandler sh = request.getSessionHandler(); + if (sh != null) { - SessionHandler sh = request.getSessionHandler(); - if (sh != null) - { - HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); - if (c != null) - addCookie(c); - } + HttpCookie c = sh.getSessionCookie(session, request.getContextPath(), request.isSecure()); + if (c != null) + addCookie(c); + } + } + } + + public void resetContent() + { + _out.resetBuffer(); + _outputType = OutputType.NONE; + _contentLength = -1; + _contentType = null; + _mimeType = null; + _characterEncoding = null; + _encodingFrom = EncodingFrom.NOT_SET; + + // remove the content related response headers and keep all others + for (Iterator i = getHttpFields().iterator(); i.hasNext(); ) + { + HttpField field = i.next(); + if (field.getHeader() == null) + continue; + + switch (field.getHeader()) + { + case CONTENT_TYPE: + case CONTENT_LENGTH: + case CONTENT_ENCODING: + case CONTENT_LANGUAGE: + case CONTENT_RANGE: + case CONTENT_MD5: + case CONTENT_LOCATION: + case TRANSFER_ENCODING: + case CACHE_CONTROL: + case LAST_MODIFIED: + case EXPIRES: + case ETAG: + case DATE: + case VARY: + i.remove(); + continue; + default: } } } @@ -1093,6 +1100,7 @@ public class Response implements HttpServletResponse public void resetBuffer() { _out.resetBuffer(); + _out.reopen(); } @Override @@ -1143,6 +1151,9 @@ public class Response implements HttpServletResponse @Override public boolean isCommitted() { + // If we are in sendError state, we pretend to be committed + if (_channel.isSendError()) + return true; return _channel.isCommitted(); } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java new file mode 100644 index 00000000000..805620745c4 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SameFileAliasChecker.java @@ -0,0 +1,78 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.PathResource; +import org.eclipse.jetty.util.resource.Resource; + +/** + * Alias checking for working with FileSystems that normalize access to the + * File System. + * + * The Java {@link Files#isSameFile(Path, Path)} method is used to determine + * if the requested file is the same as the alias file. + *
+ *+ * For File Systems that are case insensitive (eg: Microsoft Windows FAT32 and NTFS), + * the access to the file can be in any combination or style of upper and lowercase. + *
+ *+ * For File Systems that normalize UTF-8 access (eg: Mac OSX on HFS+ or APFS, + * or Linux on XFS) the the actual file could be stored using UTF-16, + * but be accessed using NFD UTF-8 or NFC UTF-8 for the same file. + *
+ */ +public class SameFileAliasChecker implements AliasCheck +{ + private static final Logger LOG = Log.getLogger(SameFileAliasChecker.class); + + @Override + public boolean check(String uri, Resource resource) + { + // Only support PathResource alias checking + if (!(resource instanceof PathResource)) + return false; + + try + { + PathResource pathResource = (PathResource)resource; + Path path = pathResource.getPath(); + Path alias = pathResource.getAliasPath(); + + if (Files.isSameFile(path, alias)) + { + if (LOG.isDebugEnabled()) + LOG.debug("Allow alias to same file {} --> {}", path, alias); + return true; + } + } + catch (IOException e) + { + LOG.ignore(e); + } + return false; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index 7568fdf00b0..282baf968c1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -514,10 +514,16 @@ public class Server extends HandlerWrapper implements Attributes if (HttpMethod.OPTIONS.is(request.getMethod()) || "*".equals(target)) { if (!HttpMethod.OPTIONS.is(request.getMethod())) + { + request.setHandled(true); response.sendError(HttpStatus.BAD_REQUEST_400); - handleOptions(request, response); - if (!request.isHandled()) - handle(target, request, request, response); + } + else + { + handleOptions(request, response); + if (!request.isHandled()) + handle(target, request, request, response); + } } else handle(target, request, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java index c9a8ef3c92c..6e50690ef7a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletRequestHttpWrapper.java @@ -35,7 +35,7 @@ import javax.servlet.http.Part; /** * ServletRequestHttpWrapper * - * Class to tunnel a ServletRequest via a HttpServletRequest + * Class to tunnel a ServletRequest via an HttpServletRequest */ public class ServletRequestHttpWrapper extends ServletRequestWrapper implements HttpServletRequest { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java index e35689fb072..3db98034fed 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ServletResponseHttpWrapper.java @@ -28,7 +28,7 @@ import javax.servlet.http.HttpServletResponse; /** * ServletResponseHttpWrapper * - * Wrapper to tunnel a ServletResponse via a HttpServletResponse + * Wrapper to tunnel a ServletResponse via an HttpServletResponse */ public class ServletResponseHttpWrapper extends ServletResponseWrapper implements HttpServletResponse { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java index c6186885d9a..c1dc168f9fa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java @@ -52,7 +52,7 @@ public class AllowSymLinkAliasChecker implements AliasCheck Path path = pathResource.getPath(); Path alias = pathResource.getAliasPath(); - if (path.equals(alias)) + if (PathResource.isSameName(alias, path)) return false; // Unknown why this is an alias if (hasSymbolicLink(path) && Files.isSameFile(path, alias)) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index a9066b6535b..4840dbeff33 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -864,7 +864,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu * insert additional handling (Eg configuration) before the call to super.doStart by this method will start contained handlers. * * @throws Exception if unable to start the context - * @see org.eclipse.jetty.server.handler.ContextHandler.Context + * @see ContextHandler.Context */ protected void startContext() throws Exception { @@ -1103,7 +1103,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu case UNAVAILABLE: baseRequest.setHandled(true); response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - return true; + return false; default: if ((DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) return false; @@ -1138,8 +1138,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (oldContext != _scontext) { // check the target. - if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch) || - DispatcherType.ERROR.equals(dispatch) && baseRequest.getHttpChannelState().isAsync()) + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) { if (_compactPath) target = URIUtil.compactPath(target); @@ -1276,29 +1275,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (new_context) requestInitialized(baseRequest, request); - switch (dispatch) + if (dispatch == DispatcherType.REQUEST && isProtectedTarget(target)) { - case REQUEST: - if (isProtectedTarget(target)) - { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - baseRequest.setHandled(true); - return; - } - break; - - case ERROR: - // If this is already a dispatch to an error page, proceed normally - if (Boolean.TRUE.equals(baseRequest.getAttribute(Dispatcher.__ERROR_DISPATCH))) - break; - - // We can just call doError here. If there is no error page, then one will - // be generated. If there is an error page, then a RequestDispatcher will be - // used to route the request through appropriate filters etc. - doError(target, baseRequest, request, response); - return; - default: - break; + baseRequest.setHandled(true); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } nextHandle(target, baseRequest, request, response); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java index a1a560ec9c0..c46b31e342c 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -19,28 +19,29 @@ package org.eclipse.jetty.server.handler; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.StringWriter; import java.io.Writer; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; -import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.QuotedQualityCSV; +import org.eclipse.jetty.io.ByteBufferOutputStream; import org.eclipse.jetty.server.Dispatcher; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.QuotedStringTokenizer; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -49,15 +50,18 @@ import org.eclipse.jetty.util.log.Logger; * Handler for Error pages * An ErrorHandler is registered with {@link ContextHandler#setErrorHandler(ErrorHandler)} or * {@link Server#setErrorHandler(ErrorHandler)}. - * It is called by the HttpResponse.sendError method to write a error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} + * It is called by the HttpResponse.sendError method to write an error page via {@link #handle(String, Request, HttpServletRequest, HttpServletResponse)} * or via {@link #badMessageError(int, String, HttpFields)} for bad requests for which a dispatch cannot be done. */ public class ErrorHandler extends AbstractHandler { + // TODO This classes API needs to be majorly refactored/cleanup in jetty-10 private static final Logger LOG = Log.getLogger(ErrorHandler.class); public static final String ERROR_PAGE = "org.eclipse.jetty.server.error_page"; + public static final String ERROR_CONTEXT = "org.eclipse.jetty.server.error_context"; boolean _showStacks = true; + boolean _disableStacks = false; boolean _showMessageInTitle = true; String _cacheControl = "must-revalidate,no-cache,no-store"; @@ -65,6 +69,19 @@ public class ErrorHandler extends AbstractHandler { } + public boolean errorPageForMethod(String method) + { + switch (method) + { + case "GET": + case "POST": + case "HEAD": + return true; + default: + return false; + } + } + /* * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) */ @@ -77,71 +94,13 @@ public class ErrorHandler extends AbstractHandler @Override public void doError(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { - String method = request.getMethod(); - if (!HttpMethod.GET.is(method) && !HttpMethod.POST.is(method) && !HttpMethod.HEAD.is(method)) - { - baseRequest.setHandled(true); - return; - } + String cacheControl = getCacheControl(); + if (cacheControl != null) + response.setHeader(HttpHeader.CACHE_CONTROL.asString(), cacheControl); - if (this instanceof ErrorPageMapper) - { - String errorPage = ((ErrorPageMapper)this).getErrorPage(request); - if (errorPage != null) - { - String oldErrorPage = (String)request.getAttribute(ERROR_PAGE); - ContextHandler.Context context = baseRequest.getContext(); - if (context == null) - context = ContextHandler.getCurrentContext(); - if (context == null) - { - LOG.warn("No ServletContext for error page {}", errorPage); - } - else if (oldErrorPage != null && oldErrorPage.equals(errorPage)) - { - LOG.warn("Error page loop {}", errorPage); - } - else - { - request.setAttribute(ERROR_PAGE, errorPage); - - Dispatcher dispatcher = (Dispatcher)context.getRequestDispatcher(errorPage); - try - { - if (LOG.isDebugEnabled()) - LOG.debug("error page dispatch {}->{}", errorPage, dispatcher); - if (dispatcher != null) - { - dispatcher.error(request, response); - return; - } - LOG.warn("No error page found " + errorPage); - } - catch (ServletException e) - { - LOG.warn(Log.EXCEPTION, e); - return; - } - } - } - else - { - if (LOG.isDebugEnabled()) - { - LOG.debug("No Error Page mapping for request({} {}) (using default)", request.getMethod(), request.getRequestURI()); - } - } - } - - if (_cacheControl != null) - response.setHeader(HttpHeader.CACHE_CONTROL.asString(), _cacheControl); - - String message = (String)request.getAttribute(RequestDispatcher.ERROR_MESSAGE); + String message = (String)request.getAttribute(Dispatcher.ERROR_MESSAGE); if (message == null) message = baseRequest.getResponse().getReason(); - if (message == null) - message = HttpStatus.getMessage(response.getStatus()); - generateAcceptableResponse(baseRequest, request, response, response.getStatus(), message); } @@ -151,7 +110,7 @@ public class ErrorHandler extends AbstractHandler * acceptable to the user-agent. The Accept header is evaluated in * quality order and the method * {@link #generateAcceptableResponse(Request, HttpServletRequest, HttpServletResponse, int, String, String)} - * is called for each mimetype until {@link Request#isHandled()} is true. + * is called for each mimetype until the response is written to or committed. * * @param baseRequest The base request * @param request The servlet request (may be wrapped) @@ -174,48 +133,10 @@ public class ErrorHandler extends AbstractHandler for (String mimeType : acceptable) { generateAcceptableResponse(baseRequest, request, response, code, message, mimeType); - if (response.isCommitted() || baseRequest.getResponse().isWriting() || baseRequest.getResponse().isStreaming()) + if (response.isCommitted() || baseRequest.getResponse().isWritingOrStreaming()) break; } } - baseRequest.getResponse().closeOutput(); - } - - /** - * Generate an acceptable error response for a mime type. - *This method is called for each mime type in the users agent's - *
Accept
header, until {@link Request#isHandled()} is true and a - * response of the appropriate type is generated. - * - * @param baseRequest The base request - * @param request The servlet request (may be wrapped) - * @param response The response (may be wrapped) - * @param code the http error code - * @param message the http error message - * @param mimeType The mimetype to generate (may be */*or other wildcard) - * @throws IOException if a response cannot be generated - */ - protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String mimeType) - throws IOException - { - switch (mimeType) - { - case "text/html": - case "text/*": - case "*/*": - { - baseRequest.setHandled(true); - Writer writer = getAcceptableWriter(baseRequest, request, response); - if (writer != null) - { - response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); - handleErrorPage(request, writer, code, message); - } - break; - } - default: - break; - } } /** @@ -236,6 +157,7 @@ public class ErrorHandler extends AbstractHandler * @return A {@link Writer} if there is a known acceptable charset or null * @throws IOException if a Writer cannot be returned */ + @Deprecated protected Writer getAcceptableWriter(Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -264,6 +186,139 @@ public class ErrorHandler extends AbstractHandler return null; } + /** + * Generate an acceptable error response for a mime type. + *This method is called for each mime type in the users agent's + *
+ *Accept
header, until {@link Request#isHandled()} is true and a + * response of the appropriate type is generated. + *The default implementation handles "text/html", "text/*" and "*/*". + * The method can be overridden to handle other types. Implementations must + * immediate produce a response and may not be async. + *
+ * + * @param baseRequest The base request + * @param request The servlet request (may be wrapped) + * @param response The response (may be wrapped) + * @param code the http error code + * @param message the http error message + * @param contentType The mimetype to generate (may be */*or other wildcard) + * @throws IOException if a response cannot be generated + */ + protected void generateAcceptableResponse(Request baseRequest, HttpServletRequest request, HttpServletResponse response, int code, String message, String contentType) + throws IOException + { + // We can generate an acceptable contentType, but can we generate an acceptable charset? + // TODO refactor this in jetty-10 to be done in the other calling loop + Charset charset = null; + Listacceptable = baseRequest.getHttpFields().getQualityCSV(HttpHeader.ACCEPT_CHARSET); + if (!acceptable.isEmpty()) + { + for (String name : acceptable) + { + if ("*".equals(name)) + { + charset = StandardCharsets.UTF_8; + break; + } + + try + { + charset = Charset.forName(name); + } + catch (Exception e) + { + LOG.ignore(e); + } + } + if (charset == null) + return; + } + + MimeTypes.Type type; + switch (contentType) + { + case "text/html": + case "text/*": + case "*/*": + type = MimeTypes.Type.TEXT_HTML; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + case "text/json": + case "application/json": + type = MimeTypes.Type.TEXT_JSON; + if (charset == null) + charset = StandardCharsets.UTF_8; + break; + + case "text/plain": + type = MimeTypes.Type.TEXT_PLAIN; + if (charset == null) + charset = StandardCharsets.ISO_8859_1; + break; + + default: + return; + } + + // write into the response aggregate buffer and flush it asynchronously. + while (true) + { + try + { + // TODO currently the writer used here is of fixed size, so a large + // TODO error page may cause a BufferOverflow. In which case we try + // TODO again with stacks disabled. If it still overflows, it is + // TODO written without a body. + ByteBuffer buffer = baseRequest.getResponse().getHttpOutput().acquireBuffer(); + ByteBufferOutputStream out = new ByteBufferOutputStream(buffer); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, charset)); + + switch (type) + { + case TEXT_HTML: + response.setContentType(MimeTypes.Type.TEXT_HTML.asString()); + response.setCharacterEncoding(charset.name()); + handleErrorPage(request, writer, code, message); + break; + case TEXT_JSON: + response.setContentType(contentType); + writeErrorJson(request, writer, code, message); + break; + case TEXT_PLAIN: + response.setContentType(MimeTypes.Type.TEXT_PLAIN.asString()); + response.setCharacterEncoding(charset.name()); + writeErrorPlain(request, writer, code, message); + break; + default: + throw new IllegalStateException(); + } + + writer.flush(); + break; + } + catch (BufferOverflowException e) + { + LOG.warn("Error page too large: {} {} {}", code, message, request); + if (LOG.isDebugEnabled()) + LOG.warn(e); + baseRequest.getResponse().resetContent(); + if (!_disableStacks) + { + LOG.info("Disabling showsStacks for " + this); + _disableStacks = true; + continue; + } + break; + } + } + + // Do an asynchronous completion. + baseRequest.getHttpChannel().sendResponseAndComplete(); + } + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { @@ -288,12 +343,13 @@ public class ErrorHandler extends AbstractHandler { writer.write("\n"); writer.write(" Error "); - writer.write(Integer.toString(code)); - - if (_showMessageInTitle) + // TODO this code is duplicated in writeErrorPageMessage + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) { writer.write(' '); - write(writer, message); + writer.write(StringUtil.sanitizeXmlString(message)); } writer.write(" \n"); } @@ -304,7 +360,7 @@ public class ErrorHandler extends AbstractHandler String uri = request.getRequestURI(); writeErrorPageMessage(request, writer, code, message, uri); - if (showStacks) + if (showStacks && !_disableStacks) writeErrorPageStacks(request, writer); Request.getBaseRequest(request).getHttpChannel().getHttpConfiguration() @@ -315,35 +371,103 @@ public class ErrorHandler extends AbstractHandler throws IOException { writer.write("HTTP ERROR "); + String status = Integer.toString(code); + writer.write(status); + if (message != null && !message.equals(status)) + { + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + } + writer.write("
\n"); + writer.write("\n"); + htmlRow(writer, "URI", uri); + htmlRow(writer, "STATUS", status); + htmlRow(writer, "MESSAGE", message); + htmlRow(writer, "SERVLET", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + htmlRow(writer, "CAUSED BY", cause); + cause = cause.getCause(); + } + writer.write("
\n"); + } + + private void htmlRow(Writer writer, String tag, Object value) + throws IOException + { + writer.write("\n"); + } + + private void writeErrorPlain(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer.write("HTTP ERROR "); writer.write(Integer.toString(code)); - writer.write("\n "); + writer.write(tag); + writer.write(": "); + if (value == null) + writer.write("-"); + else + writer.write(StringUtil.sanitizeXmlString(value.toString())); + writer.write(" Problem accessing "); - write(writer, uri); - writer.write(". Reason:\n
"); - write(writer, message); - writer.write(""); + writer.write(' '); + writer.write(StringUtil.sanitizeXmlString(message)); + writer.write("\n"); + writer.printf("URI: %s%n", request.getRequestURI()); + writer.printf("STATUS: %s%n", code); + writer.printf("MESSAGE: %s%n", message); + writer.printf("SERVLET: %s%n", request.getAttribute(Dispatcher.ERROR_SERVLET_NAME)); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + while (cause != null) + { + writer.printf("CAUSED BY %s%n", cause); + if (_showStacks && !_disableStacks) + cause.printStackTrace(writer); + cause = cause.getCause(); + } + } + + private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message) + { + writer + .append("{\n") + .append(" url: \"").append(request.getRequestURI()).append("\",\n") + .append(" status: \"").append(Integer.toString(code)).append("\",\n") + .append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n"); + Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME); + if (servlet != null) + writer.append("servlet: \"").append(servlet.toString()).append("\",\n"); + Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION); + int c = 0; + while (cause != null) + { + writer.append(" cause").append(Integer.toString(c++)).append(": ") + .append(QuotedStringTokenizer.quote(cause.toString())).append(",\n"); + cause = cause.getCause(); + } + writer.append("}"); } protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) throws IOException { Throwable th = (Throwable)request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); - while (th != null) + if (_showStacks && th != null) { - writer.write("Caused by:
"); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - th.printStackTrace(pw); - pw.flush(); - write(writer, sw.getBuffer().toString()); + PrintWriter pw = writer instanceof PrintWriter ? (PrintWriter)writer : new PrintWriter(writer); + pw.write(""); + while (th != null) + { + th.printStackTrace(pw); + th = th.getCause(); + } writer.write("\n"); - - th = th.getCause(); } } /** * Bad Message Error body - *Generate a error response body to be sent for a bad message. + *
Generate an error response body to be sent for a bad message. * In this case there is something wrong with the request, so either * a request cannot be built, or it is not safe to build a request. * This method allows for a simple error page body to be returned diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java index 947cf9ce5f2..f9baab903fa 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ShutdownHandler.java @@ -38,7 +38,7 @@ import org.eclipse.jetty.util.log.Logger; * A handler that shuts the server down on a valid request. Used to do "soft" restarts from Java. * If _exitJvm is set to true a hard System.exit() call is being made. * If _sendShutdownAtStart is set to true, starting the server will try to shut down an existing server at the same port. - * If _sendShutdownAtStart is set to true, make a http call to + * If _sendShutdownAtStart is set to true, make an http call to * "http://localhost:" + port + "/shutdown?token=" + shutdownCookie * in order to shut down the server. * @@ -93,7 +93,7 @@ public class ShutdownHandler extends HandlerWrapper /** * @param shutdownToken a secret password to avoid unauthorized shutdown attempts * @param exitJVM If true, when the shutdown is executed, the handler class System.exit() - * @param sendShutdownAtStart If true, a shutdown is sent as a HTTP post + * @param sendShutdownAtStart If true, a shutdown is sent as an HTTP post * during startup, which will shutdown any previously running instances of * this server with an identically configured ShutdownHandler */ @@ -190,8 +190,9 @@ public class ShutdownHandler extends HandlerWrapper connector.shutdown(); } - response.sendError(200, "Connectors closed, commencing full shutdown"); baseRequest.setHandled(true); + response.setStatus(200); + response.flushBuffer(); final Server server = getServer(); new Thread() diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java index 9fedb2b3938..c099e09abfd 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpInputInterceptor.java @@ -27,7 +27,7 @@ import org.eclipse.jetty.server.HttpInput.Content; import org.eclipse.jetty.util.component.Destroyable; /** - * A HttpInput Interceptor that inflates GZIP encoded request content. + * An HttpInput Interceptor that inflates GZIP encoded request content. */ public class GzipHttpInputInterceptor implements HttpInput.Interceptor, Destroyable { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java index b89ba6c060b..17ce67038ce 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionDataStore.java @@ -80,6 +80,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public SessionData load(String id) throws Exception { + if (!isStarted()) + throw new IllegalStateException ("Not started"); + final AtomicReference
reference = new AtomicReference (); final AtomicReference exception = new AtomicReference (); @@ -109,6 +112,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public void store(String id, SessionData data) throws Exception { + if (!isStarted()) + throw new IllegalStateException("Not started"); + if (data == null) return; @@ -126,7 +132,7 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem LOG.debug("Store: id={}, dirty={}, lsave={}, period={}, elapsed={}", id, data.isDirty(), data.getLastSaved(), savePeriodMs, (System.currentTimeMillis() - lastSave)); //save session if attribute changed or never been saved or time between saves exceeds threshold - if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) > savePeriodMs)) + if (data.isDirty() || (lastSave <= 0) || ((System.currentTimeMillis() - lastSave) >= savePeriodMs)) { //set the last saved time to now data.setLastSaved(System.currentTimeMillis()); @@ -156,6 +162,9 @@ public abstract class AbstractSessionDataStore extends ContainerLifeCycle implem @Override public Set getExpired(Set candidates) { + if (!isStarted()) + throw new IllegalStateException ("Not started"); + try { return doGetExpired(candidates); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java index 688340ddc3b..6809c785ae4 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java @@ -467,9 +467,9 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi } /** - * Get SessionManager for every context. + * Get SessionHandler for every context. * - * @return all session managers + * @return all SessionHandlers that are running */ @Override public Set getSessionHandlers() @@ -480,7 +480,8 @@ public class DefaultSessionIdManager extends ContainerLifeCycle implements Sessi { for (Handler h : tmp) { - handlers.add((SessionHandler)h); + if (h.isStarted()) + handlers.add((SessionHandler)h); } } return handlers; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java index 5d93456dd24..e22919eed7b 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCache.java @@ -18,7 +18,12 @@ package org.eclipse.jetty.server.session; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; /** * NullSessionCache @@ -30,6 +35,150 @@ import javax.servlet.http.HttpServletRequest; */ public class NullSessionCache extends AbstractSessionCache { + /** + * If the writethrough mode is ALWAYS or NEW, then use an + * attribute listener to ascertain when the attribute has changed. + * + */ + public class WriteThroughAttributeListener implements HttpSessionAttributeListener + { + Set _sessionsBeingWritten = ConcurrentHashMap.newKeySet(); + + @Override + public void attributeAdded(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + @Override + public void attributeRemoved(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + @Override + public void attributeReplaced(HttpSessionBindingEvent event) + { + doAttributeChanged(event); + } + + private void doAttributeChanged(HttpSessionBindingEvent event) + { + if (_writeThroughMode == WriteThroughMode.ON_EXIT) + return; + + Session session = (Session)event.getSession(); + + SessionDataStore store = getSessionDataStore(); + + if (store == null) + return; + + if (_writeThroughMode == WriteThroughMode.ALWAYS || (_writeThroughMode == WriteThroughMode.NEW && session.isNew())) + { + //ensure that a call to willPassivate doesn't result in a passivation + //listener removing an attribute, which would cause this listener to + //be called again + if (_sessionsBeingWritten.add(session)) + { + try + { + //should hold the lock on the session, but as sessions are never shared + //with the NullSessionCache, there can be no other thread modifying the + //same session at the same time (although of course there can be another + //request modifying its copy of the session data, so it is impossible + //to guarantee the order of writes). + if (store.isPassivating()) + session.willPassivate(); + store.store(session.getId(), session.getSessionData()); + if (store.isPassivating()) + session.didActivate(); + } + catch (Exception e) + { + LOG.warn("Write through of {} failed", e); + } + finally + { + _sessionsBeingWritten.remove(session); + } + } + } + } + } + + /** + * Defines the circumstances a session will be written to the backing store. + */ + public enum WriteThroughMode + { + /** + * ALWAYS means write through every attribute change. + */ + ALWAYS, + /** + * NEW means to write through every attribute change only + * while the session is freshly created, ie its id has not yet been returned to the client + */ + NEW, + /** + * ON_EXIT means write the session only when the request exits + * (which is the default behaviour of AbstractSessionCache) + */ + ON_EXIT + } + + private WriteThroughMode _writeThroughMode = WriteThroughMode.ON_EXIT; + protected WriteThroughAttributeListener _listener = null; + + /** + * @return the writeThroughMode + */ + public WriteThroughMode getWriteThroughMode() + { + return _writeThroughMode; + } + + /** + * @param writeThroughMode the writeThroughMode to set + */ + public void setWriteThroughMode(WriteThroughMode writeThroughMode) + { + if (getSessionHandler() == null) + throw new IllegalStateException("No SessionHandler"); + + //assume setting null is the same as ON_EXIT + if (writeThroughMode == null) + { + if (_listener != null) + getSessionHandler().removeEventListener(_listener); + _listener = null; + _writeThroughMode = WriteThroughMode.ON_EXIT; + return; + } + + switch (writeThroughMode) + { + case ON_EXIT: + { + if (_listener != null) + getSessionHandler().removeEventListener(_listener); + _listener = null; + break; + } + case NEW: + case ALWAYS: + { + if (_listener == null) + { + _listener = new WriteThroughAttributeListener(); + getSessionHandler().addEventListener(_listener); + } + break; + } + } + _writeThroughMode = writeThroughMode; + } /** * @param handler The SessionHandler related to this SessionCache diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java index 7d0886148d8..dd4a4cd0986 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/NullSessionCacheFactory.java @@ -27,6 +27,23 @@ public class NullSessionCacheFactory implements SessionCacheFactory { boolean _saveOnCreate; boolean _removeUnloadableSessions; + NullSessionCache.WriteThroughMode _writeThroughMode; + + /** + * @return the writeThroughMode + */ + public NullSessionCache.WriteThroughMode getWriteThroughMode() + { + return _writeThroughMode; + } + + /** + * @param writeThroughMode the writeThroughMode to set + */ + public void setWriteThroughMode(NullSessionCache.WriteThroughMode writeThroughMode) + { + _writeThroughMode = writeThroughMode; + } /** * @return the saveOnCreate @@ -69,6 +86,7 @@ public class NullSessionCacheFactory implements SessionCacheFactory NullSessionCache cache = new NullSessionCache(handler); cache.setSaveOnCreate(isSaveOnCreate()); cache.setRemoveUnloadableSessions(isRemoveUnloadableSessions()); + cache.setWriteThroughMode(_writeThroughMode); return cache; } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java index 06a85108e39..f33b4d83f1f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/Session.java @@ -42,7 +42,7 @@ import org.eclipse.jetty.util.thread.Locker.Lock; /** * Session * - * A heavy-weight Session object representing a HttpSession. Session objects + * A heavy-weight Session object representing an HttpSession. Session objects * relating to a context are kept in a {@link SessionCache}. The purpose of the * SessionCache is to keep the working set of Session objects in memory so that * they may be accessed quickly, and facilitate the sharing of a Session object @@ -476,6 +476,10 @@ public class Session implements SessionHandler.SessionIf { try (Lock lock = _lock.lock()) { + if (isInvalid()) + { + throw new IllegalStateException("Session not valid"); + } return _sessionData.getLastAccessed(); } } @@ -690,6 +694,7 @@ public class Session implements SessionHandler.SessionIf { try (Lock lock = _lock.lock()) { + checkValidForRead(); return _sessionData.getAttribute(name); } } 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 a675bece850..4f4dc81d758 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 @@ -554,7 +554,7 @@ public class SessionHandler extends ScopedHandler /** * @return same as SessionCookieConfig.getSecure(). If true, session * cookies are ALWAYS marked as secure. If false, a session cookie is - * ONLY marked as secure if _secureRequestOnly == true and it is a HTTPS request. + * ONLY marked as secure if _secureRequestOnly == true and it is an HTTPS request. */ @ManagedAttribute("if true, secure cookie flag is set on session cookies") public boolean getSecureCookies() diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java index d99a5b43b8f..e6a2b830e88 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AbstractHttpTest.java @@ -85,10 +85,10 @@ public abstract class AbstractHttpTest HttpTester.parseResponse(input, response); if (httpVersion.is("HTTP/1.1") && - response.isComplete() && - response.get("content-length") == null && - response.get("transfer-encoding") == null && - !__noBodyCodes.contains(response.getStatus())) + response.isComplete() && + response.get("content-length") == null && + response.get("transfer-encoding") == null && + !__noBodyCodes.contains(response.getStatus())) assertThat("If HTTP/1.1 response doesn't contain transfer-encoding or content-length headers, " + "it should contain connection:close", response.get("connection"), is("close")); return response; diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java new file mode 100644 index 00000000000..d97d8fb5efe --- /dev/null +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/AsyncCompletionTest.java @@ -0,0 +1,221 @@ +// +// ======================================================================== +// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +import org.eclipse.jetty.http.HttpCompliance; +import org.eclipse.jetty.http.tools.HttpTester; +import org.eclipse.jetty.io.ChannelEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.ManagedSelector; +import org.eclipse.jetty.io.SocketChannelEndPoint; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.thread.Scheduler; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + +/** + * Extended Server Tester. + */ +public class AsyncCompletionTest extends HttpServerTestFixture +{ + private static final Exchanger X = new Exchanger<>(); + private static final AtomicBoolean COMPLETE = new AtomicBoolean(); + + private static class DelayedCallback extends Callback.Nested + { + private CompletableFuture _delay = new CompletableFuture<>(); + + public DelayedCallback(Callback callback) + { + super(callback); + } + + @Override + public void succeeded() + { + _delay.complete(null); + } + + @Override + public void failed(Throwable x) + { + _delay.completeExceptionally(x); + } + + public void proceed() + { + try + { + _delay.get(10, TimeUnit.SECONDS); + getCallback().succeeded(); + } + catch(Throwable th) + { + th.printStackTrace(); + getCallback().failed(th); + } + } + } + + + @BeforeEach + public void init() throws Exception + { + COMPLETE.set(false); + + startServer(new ServerConnector(_server, new HttpConnectionFactory() + { + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) + { + return configure(new ExtendedHttpConnection(getHttpConfiguration(), connector, endPoint), connector, endPoint); + } + }) + { + @Override + protected ChannelEndPoint newEndPoint(SocketChannel channel, ManagedSelector selectSet, SelectionKey key) throws IOException + { + return new ExtendedEndPoint(channel, selectSet, key, getScheduler()); + } + }); + } + + private static class ExtendedEndPoint extends SocketChannelEndPoint + { + public ExtendedEndPoint(SocketChannel channel, ManagedSelector selector, SelectionKey key, Scheduler scheduler) + { + super(channel, selector, key, scheduler); + } + + @Override + public void write(Callback callback, ByteBuffer... buffers) throws IllegalStateException + { + DelayedCallback delay = new DelayedCallback(callback); + super.write(delay, buffers); + try + { + X.exchange(delay); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + } + } + + private static class ExtendedHttpConnection extends HttpConnection + { + public ExtendedHttpConnection(HttpConfiguration config, Connector connector, EndPoint endPoint) + { + super(config, connector, endPoint, false); + } + + @Override + public void onCompleted() + { + COMPLETE.compareAndSet(false,true); + super.onCompleted(); + } + } + + // Tests from here use these parameters + public static Stream tests() + { + List