Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-10295-formAuthDispatch

This commit is contained in:
Lachlan Roberts 2023-08-18 11:43:31 +10:00
commit a418e0db71
83 changed files with 3270 additions and 1293 deletions

View File

@ -10,6 +10,9 @@ labels: Bug
**Jetty version(s)** **Jetty version(s)**
<!--[Jetty 9.x is now at End of Community Support](https://github.com/eclipse/jetty.project/issues/7958) --> <!--[Jetty 9.x is now at End of Community Support](https://github.com/eclipse/jetty.project/issues/7958) -->
**Jetty Environment**
<!-- Applicable for jetty-12 only, choose: core, ee8, ee9, ee10 -->
**Java version/vendor** `(use: java -version)` **Java version/vendor** `(use: java -version)`
**OS type/version** **OS type/version**

View File

@ -7,9 +7,12 @@ assignees: ''
--- ---
**Jetty version** **Jetty Version**
**Java version** **Jetty Environment**
<!-- Applicable only for jetty-12, choose: core, ee8, ee9, ee10 -->
**Java Version**
**Question** **Question**

View File

@ -55,7 +55,7 @@ This release process will produce releases:
- [ ] Promote staged releases. - [ ] Promote staged releases.
- [ ] Merge release branches back to main branches and delete release branches. - [ ] Merge release branches back to main branches and delete release branches.
- [ ] Verify release existence in Maven Central by triggering the Jenkins builds of CometD. - [ ] Verify release existence in Maven Central by triggering the Jenkins builds of CometD.
- [ ] Update Jetty versions on the web sites. - [ ] Update Jetty versions on the website ( follow instructions in [jetty-website](https://github.com/eclipse/jetty-website/blob/master/README.md) ).
+ [ ] Update (or check) [Download](https://eclipse.dev/jetty/download.php) page is updated. + [ ] Update (or check) [Download](https://eclipse.dev/jetty/download.php) page is updated.
+ [ ] Update (or check) documentation page(s) are updated. + [ ] Update (or check) documentation page(s) are updated.
- [ ] Publish GitHub Releases in the order of oldest (eg: 9) to newest (eg: 11) (to ensure that "latest" in github is truly the latest) - [ ] Publish GitHub Releases in the order of oldest (eg: 9) to newest (eg: 11) (to ensure that "latest" in github is truly the latest)

View File

@ -18,8 +18,14 @@ pipeline {
} }
steps { steps {
timeout( time: 120, unit: 'MINUTES' ) { timeout( time: 120, unit: 'MINUTES' ) {
mavenBuild( "jdk11", "-T3 clean install -Djacoco.skip=true -Pautobahn", "maven3", true ) // mavenBuild( "jdk11", "-T3 clean install -Djacoco.skip=true -pl :test-websocket-autobahn -am -Pautobahn -Dtest=AutobahnTests", "maven3" ) //
junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml' junit testResults: '**/target/surefire-reports/*.xml,**/target/invoker-reports/TEST*.xml,**/target/autobahntestsuite-reports/*.xml'
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/core/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Core Server', reportTitles: ''])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/core/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Core Client', reportTitles: ''])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/javax/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Javax Server', reportTitles: ''])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/javax/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Javax Client', reportTitles: ''])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/jetty/servers", reportFiles: 'index.html', reportName: 'Autobahn Report Jetty Server', reportTitles: ''])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "${env.WORKSPACE}/tests/test-websocket-autobahn/target/reports/jetty/clients", reportFiles: 'index.html', reportName: 'Autobahn Report Jetty Client', reportTitles: ''])
} }
} }
} }

View File

@ -97,8 +97,52 @@
[[pg-migration-11-to-12-servlet-to-handler]] [[pg-migration-11-to-12-servlet-to-handler]]
==== Migrate Servlets to Jetty Handlers ==== Migrate Servlets to Jetty Handlers
Web applications written using the Servlet APIs may be re-written using the Jetty `Handler` APIs.
The sections below outline the Jetty `Handler` APIs that correspond to the Servlet APIs.
To replace the functionalities of Servlet Filters, refer to xref:pg-server-http-handler[this section].
===== Handler Request APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=request]
----
===== Handler Request Content APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-string]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-buffer]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-stream]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source]
----
===== Handler Response APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=response]
----
===== Handler Response Content APIs
[source,java,indent=0]
----
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-implicit-status]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-explicit]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-content]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-string]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-echo]
include::../{doc_code}/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers]
----
[[pg-migration-11-to-12-api-changes]] [[pg-migration-11-to-12-api-changes]]
==== APIs Changes ==== APIs Changes

View File

@ -325,20 +325,28 @@ Jetty will send an HTTP `404` response anyway if `DefaultHandler` is not used.
`ServletContextHandler` is a `ContextHandler` that provides support for the Servlet APIs and implements the behaviors required by the Servlet specification. `ServletContextHandler` is a `ContextHandler` that provides support for the Servlet APIs and implements the behaviors required by the Servlet specification.
The Maven artifact coordinates are: The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are:
[source,xml,subs=normal] [source,xml,subs=normal]
---- ----
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty.{ee-all}</groupId>
<artifactId>jetty-servlet</artifactId> <artifactId>jetty-{ee-all}-servlet</artifactId>
<version>{version}</version> <version>{version}</version>
</dependency> </dependency>
---- ----
For example, for Jakarta {ee-current-caps} the coordinates are: `org.eclipse.jetty.ee10:jetty-ee10-servlet:{version}`.
Below you can find an example of how to setup a Jakarta {ee-current-caps} `ServletContextHandler`:
[source,java,indent=0] [source,java,indent=0]
---- ----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler] include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-servlet]
----
[source,java,indent=0]
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=servletContextHandler-setup]
---- ----
The `Handler` and Servlet components tree structure looks like the following: The `Handler` and Servlet components tree structure looks like the following:
@ -368,6 +376,17 @@ Server applications must be careful when creating the `Handler` tree to put ``Se
`WebAppContext` is a `ServletContextHandler` that auto configures itself by reading a `web.xml` Servlet configuration file. `WebAppContext` is a `ServletContextHandler` that auto configures itself by reading a `web.xml` Servlet configuration file.
The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are:
[source,xml,subs=normal]
----
<dependency>
<groupId>org.eclipse.jetty.{ee-all}</groupId>
<artifactId>jetty-{ee-all}-webapp</artifactId>
<version>{version}</version>
</dependency>
----
Server applications can specify a `+*.war+` file or a directory with the structure of a `+*.war+` file to `WebAppContext` to deploy a standard Servlet web application packaged as a `war` (as defined by the Servlet specification). Server applications can specify a `+*.war+` file or a directory with the structure of a `+*.war+` file to `WebAppContext` to deploy a standard Servlet web application packaged as a `war` (as defined by the Servlet specification).
Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s, `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading. Where server applications using `ServletContextHandler` must manually invoke methods to add ``Servlet``s and ``Filter``s, `WebAppContext` reads `WEB-INF/web.xml` to add ``Servlet``s and ``Filter``s, and also enforces a number of restrictions defined by the Servlet specification, in particular related to class loading.
@ -412,6 +431,19 @@ However, Jetty picks good defaults and allows server applications to customize _
If you have a xref:pg-server-http-handler-use-servlet-context[Servlet web application], you may want to use a `DefaultServlet` instead of `ResourceHandler`. If you have a xref:pg-server-http-handler-use-servlet-context[Servlet web application], you may want to use a `DefaultServlet` instead of `ResourceHandler`.
The features are similar, but `DefaultServlet` is more commonly used to serve static files for Servlet web applications. The features are similar, but `DefaultServlet` is more commonly used to serve static files for Servlet web applications.
The Maven artifact coordinates depend on the version of Jakarta EE you want to use, and they are:
[source,xml,subs=normal]
----
<dependency>
<groupId>org.eclipse.jetty.{ee-all}</groupId>
<artifactId>jetty-{ee-all}-servlet</artifactId>
<version>{version}</version>
</dependency>
----
Below you can find an example of how to setup `DefaultServlet`:
[source,java,indent=0] [source,java,indent=0]
---- ----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet] include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=defaultServlet]

View File

@ -0,0 +1,643 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.docs.programming.migration;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CompletableTask;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;
import static java.nio.charset.StandardCharsets.UTF_8;
@SuppressWarnings("unused")
public class ServletToHandlerDocs
{
@SuppressWarnings("InnerClassMayBeStatic")
// tag::request[]
public class RequestAPIs extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Gets the request method.
// Replaces:
// - servletRequest.getMethod();
String method = request.getMethod();
// Gets the request protocol name and version.
// Replaces:
// - servletRequest.getProtocol();
String protocol = request.getConnectionMetaData().getProtocol();
// Gets the full request URI.
// Replaces:
// - servletRequest.getRequestURL();
String fullRequestURI = request.getHttpURI().asString();
// Gets the request context.
// Replaces:
// - servletRequest.getServletContext()
Context context = request.getContext();
// Gets the context path.
// Replaces:
// - servletRequest.getContextPath()
String contextPath = context.getContextPath();
// Gets the request path.
// Replaces:
// - servletRequest.getRequestURI();
String requestPath = request.getHttpURI().getPath();
// Gets the request path after the context path.
// Replaces:
// - servletRequest.getServletPath() + servletRequest.getPathInfo()
String pathInContext = Request.getPathInContext(request);
// Gets the request query.
// Replaces:
// - servletRequest.getQueryString()
String queryString = request.getHttpURI().getQuery();
// Gets request parameters.
// Replaces:
// - servletRequest.getParameterNames();
// - servletRequest.getParameter(name);
// - servletRequest.getParameterValues(name);
// - servletRequest.getParameterMap();
Fields queryParameters = Request.extractQueryParameters(request, UTF_8);
Fields allParameters = Request.getParameters(request);
// Gets cookies.
// Replaces:
// - servletRequest.getCookies();
List<HttpCookie> cookies = Request.getCookies(request);
// Gets request HTTP headers.
// Replaces:
// - servletRequest.getHeaderNames()
// - servletRequest.getHeader(name)
// - servletRequest.getHeaders(name)
// - servletRequest.getDateHeader(name)
// - servletRequest.getIntHeader(name)
HttpFields requestHeaders = request.getHeaders();
// Gets the request Content-Type.
// Replaces:
// - servletRequest.getContentType()
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
// Gets the request Content-Length.
// Replaces:
// - servletRequest.getContentLength()
// - servletRequest.getContentLengthLong()
long contentLength = request.getHeaders().getLongField(HttpHeader.CONTENT_LENGTH);
// Gets the request locales.
// Replaces:
// - servletRequest.getLocale()
// - servletRequest.getLocales()
List<Locale> locales = Request.getLocales(request);
// Gets the request scheme.
// Replaces:
// - servletRequest.getScheme()
String scheme = request.getHttpURI().getScheme();
// Gets the server name.
// Replaces:
// - servletRequest.getServerName()
String serverName = Request.getServerName(request);
// Gets the server port.
// Replaces:
// - servletRequest.getServerPort()
int serverPort = Request.getServerPort(request);
// Gets the remote host/address.
// Replaces:
// - servletRequest.getRemoteAddr()
// - servletRequest.getRemoteHost()
String remoteAddress = Request.getRemoteAddr(request);
// Gets the remote port.
// Replaces:
// - servletRequest.getRemotePort()
int remotePort = Request.getRemotePort(request);
// Gets the local host/address.
// Replaces:
// - servletRequest.getLocalAddr()
// - servletRequest.getLocalHost()
String localAddress = Request.getLocalAddr(request);
// Gets the local port.
// Replaces:
// - servletRequest.getLocalPort()
int localPort = Request.getLocalPort(request);
// Gets the request attributes.
// Replaces:
// - servletRequest.getAttributeNames()
// - servletRequest.getAttribute(name)
// - servletRequest.setAttribute(name, value)
// - servletRequest.removeAttribute(name)
String name = "name";
Object value = "value";
Set<String> names = request.getAttributeNameSet();
Object attribute = request.getAttribute(name);
request.setAttribute(name, value);
request.removeAttribute(name);
request.clearAttributes();
Map<String, Object> map = request.asAttributeMap();
// Gets the request trailers.
// Replaces:
// - servletRequest.getTrailerFields()
HttpFields trailers = request.getTrailers();
// Gets the HTTP session.
// Replaces:
// - servletRequest.getSession()
// - servletRequest.getSession(create)
boolean create = true;
Session session = request.getSession(create);
callback.succeeded();
return false;
}
}
// end::request[]
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsString extends Handler.Abstract
{
// tag::requestContent-string[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Non-blocking read the request content as a String.
// Use with caution as the request content may be large.
Promise.Completable<String> completable = new Promise.Completable<>();
Content.Source.asString(request, UTF_8, completable);
completable.whenComplete((requestContent, failure) ->
{
if (failure == null)
{
// Process the request content here.
// Implicitly respond with status code 200 and no content.
callback.succeeded();
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::requestContent-string[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsByteBuffer extends Handler.Abstract
{
// tag::requestContent-buffer[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Non-blocking read the request content as a ByteBuffer.
// Use with caution as the request content may be large.
Promise.Completable<ByteBuffer> completable = new Promise.Completable<>();
Content.Source.asByteBuffer(request, completable);
completable.whenComplete((requestContent, failure) ->
{
if (failure == null)
{
// Process the request content here.
// Implicitly respond with status code 200 and no content.
callback.succeeded();
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::requestContent-buffer[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsInputStream extends Handler.Abstract
{
// tag::requestContent-stream[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Read the request content as an InputStream.
// Note that InputStream.read() may block.
try (InputStream inputStream = Content.Source.asInputStream(request))
{
while (true)
{
int read = inputStream.read();
// EOF was reached, stop reading.
if (read < 0)
break;
// Process the read byte here.
}
}
// Implicitly respond with status code 200 and no content.
callback.succeeded();
return true;
}
// end::requestContent-stream[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class RequestContentAPIsSource extends Handler.Abstract
{
// tag::requestContent-source[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
CompletableTask<Void> reader = new CompletableTask<>()
{
@Override
public void run()
{
// Read in a loop.
while (true)
{
// Read a chunk of content.
Content.Chunk chunk = request.read();
// If there is no content, demand to be
// called back when more content is available.
if (chunk == null)
{
request.demand(this);
return;
}
// If a failure is read, complete with a failure.
if (Content.Chunk.isFailure(chunk))
{
Throwable failure = chunk.getFailure();
completeExceptionally(failure);
return;
}
if (chunk instanceof Trailers trailers)
{
// Possibly process the request trailers here.
// Trailers have an empty ByteBuffer and are a last chunk.
}
// Process the request content chunk here.
// After the processing, the chunk MUST be released.
chunk.release();
// If the last chunk is read, complete normally.
if (chunk.isLast())
{
complete(null);
return;
}
// Not the last chunk of content, loop around to read more.
}
}
};
// Initiate the read of the request content.
reader.start();
// When the read is complete, complete the Handler callback.
callback.completeWith(reader);
return true;
}
// end::requestContent-source[]
}
@SuppressWarnings("InnerClassMayBeStatic")
// tag::response[]
public class ResponseAPIs extends Handler.Abstract
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Sets/Gets the response HTTP status.
// Replaces:
// - servletResponse.setStatus(code);
// - servletResponse.getStatus();
response.setStatus(HttpStatus.OK_200);
int status = response.getStatus();
// Gets the response HTTP headers.
// Replaces:
// - servletResponse.setHeader(name, value);
// - servletResponse.addHeader(name, value);
// - servletResponse.setDateHeader(name, date);
// - servletResponse.addDateHeader(name, date);
// - servletResponse.setIntHeader(name, value);
// - servletResponse.addIntHeader(name, value);
// - servletResponse.getHeaderNames()
// - servletResponse.getHeader(name)
// - servletResponse.getHeaders(name)
// - servletResponse.containsHeader(name)
HttpFields.Mutable responseHeaders = response.getHeaders();
// Sets an HTTP cookie.
// Replaces:
// - servletResponse.addCookie(cookie);
HttpCookie cookie = HttpCookie.build("name", "value")
.domain("example.org")
.path("/path")
.maxAge(Duration.ofDays(1).toSeconds())
.build();
Response.addCookie(response, cookie);
// Sets the response Content-Type.
// Replaces:
// - servletResponse.setContentType(type)
responseHeaders.put(HttpHeader.CONTENT_TYPE, "text/plain; charset=UTF-8");
// Sets the response Content-Length.
// Replaces:
// - servletResponse.setContentLength(length)
// - servletResponse.setContentLengthLong(length)
responseHeaders.put(HttpHeader.CONTENT_LENGTH, 1024L);
// Sets/Gets the response trailers.
// Replaces:
// - servletResponse.setTrailerFields(() -> trailers)
// - servletResponse.getTrailerFields()
HttpFields trailers = HttpFields.build().put("checksum", 0xCAFE);
response.setTrailersSupplier(trailers);
Supplier<HttpFields> trailersSupplier = response.getTrailersSupplier();
// Gets whether the response is committed.
// Replaces:
// - servletResponse.isCommitted()
boolean committed = response.isCommitted();
// Resets the response.
// Replaces:
// - servletResponse.reset();
response.reset();
// Sends a redirect response.
// Replaces:
// - servletResponse.encodeRedirectURL(location)
// - servletResponse.sendRedirect(location)
String location = Request.toRedirectURI(request, "/redirect");
Response.sendRedirect(request, response, callback, location);
// Sends an error response.
// Replaces:
// - servletResponse.sendError(code);
// - servletResponse.sendError(code, message);
Response.writeError(request, response, callback, HttpStatus.SERVICE_UNAVAILABLE_503, "Request Cannot be Processed");
callback.succeeded();
return true;
}
}
// end::response[]
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsImplicit extends Handler.Abstract
{
// tag::responseContent-implicit[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an implicit response with status code 200
// with no content when returning from this method.
// The Handler callback must be completed when returning true.
callback.succeeded();
return true;
}
// end::responseContent-implicit[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsImplicitWithStatus extends Handler.Abstract
{
// tag::responseContent-implicit-status[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an implicit response with status 204
// with no content when returning from this method.
response.setStatus(HttpStatus.NO_CONTENT_204);
// The Handler callback must be completed when returning true.
callback.succeeded();
return true;
}
// end::responseContent-implicit-status[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIsExplicit extends Handler.Abstract
{
// tag::responseContent-explicit[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
// Produces an explicit response with status 204 with no content.
response.setStatus(HttpStatus.NO_CONTENT_204);
// This explicit first write() writes the response status code and headers.
// It is also the last write (as specified by the first parameter)
// and writes an empty content (the second parameter, a null ByteBuffer).
// When this write completes, the Handler callback is completed.
response.write(true, null, callback);
return true;
}
// end::responseContent-explicit[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPISimpleContent extends Handler.Abstract
{
// tag::responseContent-content[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
ByteBuffer content = UTF_8.encode("Hello World");
// Explicit first write that writes the response status code, headers and content.
// When this write completes, the Handler callback is completed.
response.write(true, content, callback);
return true;
}
// end::responseContent-content[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIFlush extends Handler.Abstract
{
// tag::responseContent-content[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
ByteBuffer content = UTF_8.encode("Hello World");
response.getHeaders().put(HttpHeader.CONTENT_LENGTH, content.remaining());
// Flush the response status code and the headers (no content).
// This is the fist but non-last write.
Callback.Completable completable = new Callback.Completable();
response.write(false, null, completable);
// When the first write completes, perform the second (and last) write.
completable.whenComplete((ignored, failure) ->
{
if (failure == null)
{
// Now explicitly write the content as the last write.
// When this write completes, the Handler callback is completed.
response.write(true, content, callback);
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::responseContent-content[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIString extends Handler.Abstract
{
// tag::responseContent-string[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// Utility method to write UTF-8 string content.
// When this write completes, the Handler callback is completed.
Content.Sink.write(response, true, "Hello World", callback);
return true;
}
// end::responseContent-string[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPIEcho extends Handler.Abstract
{
// tag::responseContent-echo[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// Utility method to echo the content from the request to the response.
// When the echo completes, the Handler callback is completed.
Content.copy(request, response, callback);
return true;
}
// end::responseContent-echo[]
}
@SuppressWarnings("InnerClassMayBeStatic")
public class ResponseContentAPITrailers extends Handler.Abstract
{
// tag::responseContent-trailers[]
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.setStatus(HttpStatus.OK_200);
// The trailers must be set on the response before the first write.
HttpFields.Mutable trailers = HttpFields.build();
response.setTrailersSupplier(trailers);
// Explicit first write that writes the response status code, headers and content.
// The trailers have not been written yet; they will be written with the last write.
ByteBuffer content = UTF_8.encode("Hello World");
Callback.Completable completable = new Callback.Completable();
response.write(false, content, completable);
completable.whenComplete((ignored, failure) ->
{
if (failure == null)
{
// Update the trailers
trailers.put("Content-Checksum", 0xCAFE);
// Explicit last write to write the trailers
// and complete the Handler callback.
response.write(true, null, callback);
}
else
{
// Implicitly respond with status code 500.
callback.failed(failure);
}
});
return true;
}
// end::responseContent-trailers[]
}
}

View File

@ -658,10 +658,9 @@ public class HTTPServerDocs
// end::contextHandlerCollection[] // end::contextHandlerCollection[]
} }
public void servletContextHandler() throws Exception @SuppressWarnings("InnerClassMayBeStatic")
{ // tag::servletContextHandler-servlet[]
// tag::servletContextHandler[] public class ShopCartServlet extends HttpServlet
class ShopCartServlet extends HttpServlet
{ {
@Override @Override
protected void service(HttpServletRequest request, HttpServletResponse response) protected void service(HttpServletRequest request, HttpServletResponse response)
@ -669,7 +668,11 @@ public class HTTPServerDocs
// Implement the shop cart functionality. // Implement the shop cart functionality.
} }
} }
// end::servletContextHandler-servlet[]
public void servletContextHandler() throws Exception
{
// tag::servletContextHandler-setup[]
Server server = new Server(); Server server = new Server();
Connector connector = new ServerConnector(server); Connector connector = new ServerConnector(server);
server.addConnector(connector); server.addConnector(connector);
@ -692,7 +695,7 @@ public class HTTPServerDocs
server.setHandler(context); server.setHandler(context);
server.start(); server.start();
// end::servletContextHandler[] // end::servletContextHandler-setup[]
} }
public void webAppContextHandler() throws Exception public void webAppContextHandler() throws Exception

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.client;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import org.eclipse.jetty.client.transport.HttpDestination; import org.eclipse.jetty.client.transport.HttpDestination;
@ -23,6 +24,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue; import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Handler;
@ -33,6 +35,7 @@ import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -46,15 +49,18 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class HttpClientProxyProtocolTest public class HttpClientProxyProtocolTest
{ {
private ArrayByteBufferPool.Tracking serverBufferPool;
private Server server; private Server server;
private ServerConnector connector; private ServerConnector connector;
private ArrayByteBufferPool.Tracking clientBufferPool;
private HttpClient client; private HttpClient client;
private void startServer(Handler handler) throws Exception private void startServer(Handler handler) throws Exception
{ {
QueuedThreadPool serverThreads = new QueuedThreadPool(); QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server"); serverThreads.setName("server");
server = new Server(serverThreads); serverBufferPool = new ArrayByteBufferPool.Tracking();
server = new Server(serverThreads, null, serverBufferPool);
HttpConnectionFactory http = new HttpConnectionFactory(); HttpConnectionFactory http = new HttpConnectionFactory();
ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol()); ProxyConnectionFactory proxy = new ProxyConnectionFactory(http.getProtocol());
connector = new ServerConnector(server, 1, 1, proxy, http); connector = new ServerConnector(server, 1, 1, proxy, http);
@ -67,18 +73,22 @@ public class HttpClientProxyProtocolTest
{ {
QueuedThreadPool clientThreads = new QueuedThreadPool(); QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client"); clientThreads.setName("client");
clientBufferPool = new ArrayByteBufferPool.Tracking();
client = new HttpClient(); client = new HttpClient();
client.setExecutor(clientThreads); client.setExecutor(clientThreads);
client.setByteBufferPool(clientBufferPool);
client.start(); client.start();
} }
@AfterEach @AfterEach
public void dispose() throws Exception public void dispose() throws Exception
{ {
if (server != null) LifeCycle.stop(client);
server.stop(); LifeCycle.stop(server);
if (client != null) Set<ArrayByteBufferPool.Tracking.Buffer> serverLeaks = serverBufferPool.getLeaks();
client.stop(); assertEquals(0, serverLeaks.size(), serverBufferPool.dumpLeaks());
Set<ArrayByteBufferPool.Tracking.Buffer> clientLeaks = clientBufferPool.getLeaks();
assertEquals(0, clientLeaks.size(), clientBufferPool.dumpLeaks());
} }
@Test @Test

View File

@ -1,5 +1,6 @@
#org.eclipse.jetty.LEVEL=DEBUG #org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.client.LEVEL=DEBUG #org.eclipse.jetty.client.LEVEL=DEBUG
#org.eclipse.jetty.io.ArrayByteBufferPool$Tracking.LEVEL=DEBUG
#org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG #org.eclipse.jetty.io.SocketChannelEndPoint.LEVEL=DEBUG
#org.eclipse.jetty.io.ssl.LEVEL=DEBUG #org.eclipse.jetty.io.ssl.LEVEL=DEBUG
#org.eclipse.jetty.http.LEVEL=DEBUG #org.eclipse.jetty.http.LEVEL=DEBUG

View File

@ -197,8 +197,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements
} }
} }
if (LOG.isDebugEnabled()) LOG.warn("{} no environment for {}, ignoring", this, app);
LOG.debug("{} ignored {}", this, app);
return null; return null;
} }

View File

@ -13,45 +13,33 @@
package org.eclipse.jetty.fcgi.client.transport.internal; package org.eclipse.jetty.fcgi.client.transport.internal;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.Result; import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.client.transport.HttpChannel; import org.eclipse.jetty.client.transport.HttpChannel;
import org.eclipse.jetty.client.transport.HttpExchange; import org.eclipse.jetty.client.transport.HttpExchange;
import org.eclipse.jetty.client.transport.HttpReceiver; import org.eclipse.jetty.client.transport.HttpReceiver;
import org.eclipse.jetty.client.transport.HttpSender; import org.eclipse.jetty.client.transport.HttpSender;
import org.eclipse.jetty.fcgi.generator.Flusher;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.IdleTimeout;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpChannelOverFCGI extends HttpChannel public class HttpChannelOverFCGI extends HttpChannel
{ {
private static final Logger LOG = LoggerFactory.getLogger(HttpChannelOverFCGI.class);
private final HttpConnectionOverFCGI connection; private final HttpConnectionOverFCGI connection;
private final Flusher flusher;
private final HttpSenderOverFCGI sender; private final HttpSenderOverFCGI sender;
private final HttpReceiverOverFCGI receiver; private final HttpReceiverOverFCGI receiver;
private final FCGIIdleTimeout idle;
private int request; private int request;
private HttpVersion version; private HttpVersion version;
public HttpChannelOverFCGI(HttpConnectionOverFCGI connection, Flusher flusher, long idleTimeout) public HttpChannelOverFCGI(HttpConnectionOverFCGI connection)
{ {
super(connection.getHttpDestination()); super(connection.getHttpDestination());
this.connection = connection; this.connection = connection;
this.flusher = flusher;
this.sender = new HttpSenderOverFCGI(this); this.sender = new HttpSenderOverFCGI(this);
this.receiver = new HttpReceiverOverFCGI(this); this.receiver = new HttpReceiverOverFCGI(this);
this.idle = new FCGIIdleTimeout(connection, idleTimeout);
} }
public HttpConnectionOverFCGI getHttpConnection() public HttpConnectionOverFCGI getHttpConnection()
@ -81,28 +69,21 @@ public class HttpChannelOverFCGI extends HttpChannel
return receiver; return receiver;
} }
public boolean isFailed()
{
return sender.isFailed() || receiver.isFailed();
}
@Override @Override
public void send(HttpExchange exchange) public void send(HttpExchange exchange)
{ {
version = exchange.getRequest().getVersion(); version = exchange.getRequest().getVersion();
idle.onOpen();
sender.send(exchange); sender.send(exchange);
} }
@Override @Override
public void release() public void release()
{ {
connection.release(this); connection.release();
} }
protected void responseBegin(int code, String reason) protected void responseBegin(int code, String reason)
{ {
idle.notIdle();
HttpExchange exchange = getHttpExchange(); HttpExchange exchange = getHttpExchange();
if (exchange == null) if (exchange == null)
return; return;
@ -119,7 +100,6 @@ public class HttpChannelOverFCGI extends HttpChannel
protected void responseHeaders() protected void responseHeaders()
{ {
idle.notIdle();
HttpExchange exchange = getHttpExchange(); HttpExchange exchange = getHttpExchange();
if (exchange != null) if (exchange != null)
receiver.responseHeaders(exchange); receiver.responseHeaders(exchange);
@ -127,7 +107,6 @@ public class HttpChannelOverFCGI extends HttpChannel
protected void content(Content.Chunk chunk) protected void content(Content.Chunk chunk)
{ {
idle.notIdle();
HttpExchange exchange = getHttpExchange(); HttpExchange exchange = getHttpExchange();
if (exchange != null) if (exchange != null)
receiver.content(chunk); receiver.content(chunk);
@ -135,7 +114,6 @@ public class HttpChannelOverFCGI extends HttpChannel
protected void end() protected void end()
{ {
idle.notIdle();
HttpExchange exchange = getHttpExchange(); HttpExchange exchange = getHttpExchange();
if (exchange != null) if (exchange != null)
receiver.end(exchange); receiver.end(exchange);
@ -150,67 +128,33 @@ public class HttpChannelOverFCGI extends HttpChannel
promise.succeeded(false); promise.succeeded(false);
} }
void eof()
{
HttpExchange exchange = getHttpExchange();
if (exchange == null)
connection.close();
}
@Override @Override
public void exchangeTerminated(HttpExchange exchange, Result result) public void exchangeTerminated(HttpExchange exchange, Result result)
{ {
super.exchangeTerminated(exchange, result); super.exchangeTerminated(exchange, result);
idle.onClose();
HttpFields responseHeaders = result.getResponse().getHeaders(); HttpFields responseHeaders = result.getResponse().getHeaders();
if (result.isFailed()) if (result.isFailed())
connection.close(result.getFailure()); connection.close(result.getFailure());
else if (!connection.closeByHTTP(responseHeaders)) else if (connection.isShutdown() || connection.isCloseByHTTP(responseHeaders))
connection.close();
else
release(); release();
} }
protected void flush(ByteBufferPool.Accumulator accumulator, Callback callback) protected void flush(ByteBufferPool.Accumulator accumulator, Callback callback)
{ {
flusher.flush(accumulator, callback); connection.getFlusher().flush(accumulator, callback);
} }
void receive() void receive()
{ {
receiver.receive(); receiver.receive();
} }
private class FCGIIdleTimeout extends IdleTimeout
{
private final HttpConnectionOverFCGI connection;
private boolean open;
public FCGIIdleTimeout(HttpConnectionOverFCGI connection, long idleTimeout)
{
super(connection.getHttpDestination().getHttpClient().getScheduler());
this.connection = connection;
setIdleTimeout(idleTimeout >= 0 ? idleTimeout : connection.getEndPoint().getIdleTimeout());
}
@Override
public void onOpen()
{
open = true;
notIdle();
super.onOpen();
}
@Override
public void onClose()
{
super.onClose();
open = false;
}
@Override
protected void onIdleExpired(TimeoutException timeout)
{
if (LOG.isDebugEnabled())
LOG.debug("Idle timeout for request {}", request);
connection.abort(timeout);
}
@Override
public boolean isOpen()
{
return open;
}
}
} }

View File

@ -13,14 +13,13 @@
package org.eclipse.jetty.fcgi.client.transport.internal; package org.eclipse.jetty.fcgi.client.transport.internal;
import java.io.EOFException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException; import java.nio.channels.AsynchronousCloseException;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.client.Connection; import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.Destination; import org.eclipse.jetty.client.Destination;
@ -49,7 +48,6 @@ import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.util.Attachable; import org.eclipse.jetty.util.Attachable;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -58,18 +56,19 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
private static final Logger LOG = LoggerFactory.getLogger(HttpConnectionOverFCGI.class); private static final Logger LOG = LoggerFactory.getLogger(HttpConnectionOverFCGI.class);
private final ByteBufferPool networkByteBufferPool; private final ByteBufferPool networkByteBufferPool;
private final AutoLock lock = new AutoLock(); private final AtomicInteger requests = new AtomicInteger();
private final LinkedList<Integer> requests = new LinkedList<>();
private final AtomicBoolean closed = new AtomicBoolean(); private final AtomicBoolean closed = new AtomicBoolean();
private final HttpDestination destination; private final HttpDestination destination;
private final Promise<Connection> promise; private final Promise<Connection> promise;
private final Flusher flusher; private final Flusher flusher;
private final Delegate delegate; private final Delegate delegate;
private final ClientParser parser; private final ClientParser parser;
private HttpChannelOverFCGI channel; private final HttpChannelOverFCGI channel;
private RetainableByteBuffer networkBuffer; private RetainableByteBuffer networkBuffer;
private Object attachment; private Object attachment;
private Runnable action; private Runnable action;
private long idleTimeout;
private boolean shutdown;
public HttpConnectionOverFCGI(EndPoint endPoint, Destination destination, Promise<Connection> promise) public HttpConnectionOverFCGI(EndPoint endPoint, Destination destination, Promise<Connection> promise)
{ {
@ -79,7 +78,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
this.flusher = new Flusher(endPoint); this.flusher = new Flusher(endPoint);
this.delegate = new Delegate(destination); this.delegate = new Delegate(destination);
this.parser = new ClientParser(new ResponseListener()); this.parser = new ClientParser(new ResponseListener());
requests.addLast(0); this.channel = newHttpChannel();
HttpClient client = destination.getHttpClient(); HttpClient client = destination.getHttpClient();
this.networkByteBufferPool = client.getByteBufferPool(); this.networkByteBufferPool = client.getByteBufferPool();
} }
@ -207,13 +206,18 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
private void shutdown() private void shutdown()
{ {
// Close explicitly only if we are idle, since the request may still // Mark this receiver as shutdown, so that we can
// be in progress, otherwise close only if we can fail the responses. // close the connection when the exchange terminates.
HttpChannelOverFCGI channel = this.channel; // We cannot close the connection from here because
if (channel == null || channel.getRequest() == 0) // the request may still be in process.
close(); shutdown = true;
else if (!parser.eof())
failAndClose(new EOFException(String.valueOf(getEndPoint()))); channel.eof();
}
boolean isShutdown()
{
return shutdown;
} }
@Override @Override
@ -226,28 +230,12 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return false; return false;
} }
protected void release(HttpChannelOverFCGI channel) protected void release()
{ {
HttpChannelOverFCGI existing = this.channel; // Restore idle timeout
if (existing == channel) getEndPoint().setIdleTimeout(idleTimeout);
{
channel.setRequest(0);
// Recycle only non-failed channels.
if (channel.isFailed())
{
channel.destroy();
this.channel = null;
}
destination.release(this); destination.release(this);
} }
else
{
if (existing == null)
channel.destroy();
else
throw new UnsupportedOperationException("FastCGI Multiplex");
}
}
@Override @Override
public void close() public void close()
@ -260,9 +248,8 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
if (closed.compareAndSet(false, true)) if (closed.compareAndSet(false, true))
{ {
getHttpDestination().remove(this); getHttpDestination().remove(this);
abort(failure); abort(failure);
channel.destroy();
getEndPoint().shutdownOutput(); getEndPoint().shutdownOutput();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Shutdown {}", this); LOG.debug("Shutdown {}", this);
@ -290,62 +277,25 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return attachment; return attachment;
} }
protected boolean closeByHTTP(HttpFields fields) protected boolean isCloseByHTTP(HttpFields fields)
{ {
if (!fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString())) return fields.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
return false;
close();
return true;
} }
protected void abort(Throwable failure) protected void abort(Throwable failure)
{
HttpChannelOverFCGI channel = this.channel;
if (channel != null)
{ {
HttpExchange exchange = channel.getHttpExchange(); HttpExchange exchange = channel.getHttpExchange();
if (exchange != null) if (exchange != null)
exchange.getRequest().abort(failure); exchange.getRequest().abort(failure);
channel.destroy();
this.channel = null;
}
} }
private void failAndClose(Throwable failure) private void failAndClose(Throwable failure)
{
HttpChannelOverFCGI channel = this.channel;
if (channel != null)
{ {
channel.responseFailure(failure, Promise.from(failed -> channel.responseFailure(failure, Promise.from(failed ->
{ {
channel.destroy();
if (failed) if (failed)
close(failure); close(failure);
}, x -> }, x -> close(failure)));
{
channel.destroy();
close(failure);
}));
}
}
private int acquireRequest()
{
try (AutoLock ignored = lock.lock())
{
int last = requests.getLast();
int request = last + 1;
requests.addLast(request);
return request;
}
}
private void releaseRequest(int request)
{
try (AutoLock ignored = lock.lock())
{
requests.removeFirstOccurrence(request);
}
} }
private Runnable getAndSetAction(Runnable action) private Runnable getAndSetAction(Runnable action)
@ -355,17 +305,9 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return r; return r;
} }
protected HttpChannelOverFCGI acquireHttpChannel(int id, Request request) protected HttpChannelOverFCGI newHttpChannel()
{ {
if (channel == null) return new HttpChannelOverFCGI(this);
channel = newHttpChannel(request);
channel.setRequest(id);
return channel;
}
protected HttpChannelOverFCGI newHttpChannel(Request request)
{
return new HttpChannelOverFCGI(this, getFlusher(), request.getIdleTimeout());
} }
@Override @Override
@ -388,8 +330,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
@Override @Override
protected Iterator<HttpChannel> getHttpChannels() protected Iterator<HttpChannel> getHttpChannels()
{ {
HttpChannel channel = HttpConnectionOverFCGI.this.channel; return Collections.<HttpChannel>singleton(channel).iterator();
return channel == null ? Collections.emptyIterator() : Collections.singleton(channel).iterator();
} }
@Override @Override
@ -398,9 +339,14 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
HttpRequest request = exchange.getRequest(); HttpRequest request = exchange.getRequest();
normalizeRequest(request); normalizeRequest(request);
int id = acquireRequest(); // Save the old idle timeout to restore it.
HttpChannelOverFCGI channel = acquireHttpChannel(id, request); EndPoint endPoint = getEndPoint();
idleTimeout = endPoint.getIdleTimeout();
long requestIdleTimeout = request.getIdleTimeout();
if (requestIdleTimeout >= 0)
endPoint.setIdleTimeout(requestIdleTimeout);
channel.setRequest(requests.incrementAndGet());
return send(channel, exchange); return send(channel, exchange);
} }
@ -431,11 +377,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onBegin r={},c={},reason={}", request, code, reason); LOG.debug("onBegin r={},c={},reason={}", request, code, reason);
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel;
if (channel != null)
channel.responseBegin(code, reason); channel.responseBegin(code, reason);
else
noChannel(request);
} }
@Override @Override
@ -443,11 +385,7 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onHeader r={},f={}", request, field); LOG.debug("onHeader r={},f={}", request, field);
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel;
if (channel != null)
channel.responseHeader(field); channel.responseHeader(field);
else
noChannel(request);
} }
@Override @Override
@ -455,16 +393,10 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onHeaders r={} {}", request, networkBuffer); LOG.debug("onHeaders r={} {}", request, networkBuffer);
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel;
if (channel != null)
{
if (getAndSetAction(channel::responseHeaders) != null) if (getAndSetAction(channel::responseHeaders) != null)
throw new IllegalStateException(); throw new IllegalStateException();
return true; return true;
} }
noChannel(request);
return false;
}
@Override @Override
public boolean onContent(int request, FCGI.StreamType stream, ByteBuffer buffer) public boolean onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
@ -474,9 +406,6 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
switch (stream) switch (stream)
{ {
case STD_OUT -> case STD_OUT ->
{
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel;
if (channel != null)
{ {
// No need to call networkBuffer.retain() here, since we know // No need to call networkBuffer.retain() here, since we know
// that the action will be run before releasing the networkBuffer. // that the action will be run before releasing the networkBuffer.
@ -486,11 +415,6 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
return true; return true;
throw new IllegalStateException(); throw new IllegalStateException();
} }
else
{
noChannel(request);
}
}
case STD_ERR -> LOG.info(BufferUtil.toUTF8String(buffer)); case STD_ERR -> LOG.info(BufferUtil.toUTF8String(buffer));
default -> throw new IllegalArgumentException(); default -> throw new IllegalArgumentException();
} }
@ -502,42 +426,15 @@ public class HttpConnectionOverFCGI extends AbstractConnection implements IConne
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onEnd r={}", request); LOG.debug("onEnd r={}", request);
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel;
if (channel != null)
{
releaseRequest(request);
channel.end(); channel.end();
} }
else
{
noChannel(request);
}
}
@Override @Override
public void onFailure(int request, Throwable failure) public void onFailure(int request, Throwable failure)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("onFailure request={}", request, failure); LOG.debug("onFailure request={}", request, failure);
HttpChannelOverFCGI channel = HttpConnectionOverFCGI.this.channel; failAndClose(failure);
if (channel != null)
{
channel.responseFailure(failure, Promise.from(failed ->
{
if (failed)
releaseRequest(request);
}, x -> releaseRequest(request)));
}
else
{
noChannel(request);
}
}
private void noChannel(int request)
{
if (LOG.isDebugEnabled())
LOG.debug("Channel not found for request {}", request);
} }
} }
} }

View File

@ -61,6 +61,17 @@ public class HttpReceiverOverFCGI extends HttpReceiver
} }
} }
@Override
protected void dispose()
{
super.dispose();
if (chunk != null)
{
chunk.release();
chunk = null;
}
}
@Override @Override
public Content.Chunk read(boolean fillInterestIfNeeded) public Content.Chunk read(boolean fillInterestIfNeeded)
{ {

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.fcgi.parser; package org.eclipse.jetty.fcgi.parser;
import java.io.EOFException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.FCGI; import org.eclipse.jetty.fcgi.FCGI;
@ -60,7 +61,7 @@ public abstract class Parser
protected final HeaderParser headerParser = new HeaderParser(); protected final HeaderParser headerParser = new HeaderParser();
private final Listener listener; private final Listener listener;
private State state = State.HEADER; private State state = State.INITIAL;
private int padding; private int padding;
protected Parser(Listener listener) protected Parser(Listener listener)
@ -80,6 +81,12 @@ public abstract class Parser
{ {
switch (state) switch (state)
{ {
case INITIAL ->
{
if (!buffer.hasRemaining())
return false;
state = State.HEADER;
}
case HEADER -> case HEADER ->
{ {
if (!headerParser.parse(buffer)) if (!headerParser.parse(buffer))
@ -145,10 +152,19 @@ public abstract class Parser
protected abstract ContentParser findContentParser(FCGI.FrameType frameType); protected abstract ContentParser findContentParser(FCGI.FrameType frameType);
public boolean eof()
{
if (state == State.INITIAL)
return false;
Throwable failure = new EOFException();
listener.onFailure(headerParser.getRequest(), failure);
return true;
}
private void reset() private void reset()
{ {
headerParser.reset(); headerParser.reset();
state = State.HEADER; state = State.INITIAL;
padding = 0; padding = 0;
} }
@ -190,6 +206,6 @@ public abstract class Parser
private enum State private enum State
{ {
HEADER, CONTENT, PADDING INITIAL, HEADER, CONTENT, PADDING
} }
} }

View File

@ -36,7 +36,6 @@ import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -156,12 +155,6 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti
return getEndPoint().getLocalSocketAddress(); return getEndPoint().getLocalSocketAddress();
} }
@Override
public HostPort getServerAuthority()
{
return ConnectionMetaData.getServerAuthority(configuration, this);
}
@Override @Override
public Object removeAttribute(String name) public Object removeAttribute(String name)
{ {
@ -270,6 +263,8 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti
private void releaseInputBuffer() private void releaseInputBuffer()
{ {
if (networkBuffer == null)
return;
boolean released = networkBuffer.release(); boolean released = networkBuffer.release();
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("releaseInputBuffer {} {}", released, this); LOG.debug("releaseInputBuffer {} {}", released, this);
@ -327,6 +322,9 @@ public class ServerFCGIConnection extends AbstractConnection implements Connecti
@Override @Override
public boolean onIdleExpired(TimeoutException timeoutException) public boolean onIdleExpired(TimeoutException timeoutException)
{ {
HttpStreamOverFCGI stream = this.stream;
if (stream == null)
return true;
Runnable task = stream.getHttpChannel().onIdleTimeout(timeoutException); Runnable task = stream.getHttpChannel().onIdleTimeout(timeoutException);
if (task != null) if (task != null)
getExecutor().execute(task); getExecutor().execute(task);

View File

@ -32,7 +32,9 @@ import java.util.zip.GZIPOutputStream;
import org.eclipse.jetty.client.AsyncRequestContent; import org.eclipse.jetty.client.AsyncRequestContent;
import org.eclipse.jetty.client.BytesRequestContent; import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.ConnectionPool;
import org.eclipse.jetty.client.ContentResponse; import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.FutureResponseListener; import org.eclipse.jetty.client.FutureResponseListener;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.Request; import org.eclipse.jetty.client.Request;
@ -515,6 +517,35 @@ public class HttpClientTest extends AbstractHttpClientServerTest
@Test @Test
public void testConnectionIdleTimeout() throws Exception public void testConnectionIdleTimeout() throws Exception
{
long idleTimeout = 1000;
start(new Handler.Abstract()
{
@Override
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
callback.succeeded();
return true;
}
});
connector.setIdleTimeout(idleTimeout);
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.timeout(2 * idleTimeout, TimeUnit.MILLISECONDS)
.send();
assertNotNull(response);
assertEquals(200, response.getStatus());
Thread.sleep(2 * idleTimeout);
assertTrue(client.getDestinations().stream()
.map(Destination::getConnectionPool)
.allMatch(ConnectionPool::isEmpty));
}
@Test
public void testConnectionIdleTimeoutIgnored() throws Exception
{ {
long idleTimeout = 1000; long idleTimeout = 1000;
start(new Handler.Abstract() start(new Handler.Abstract()
@ -522,9 +553,11 @@ public class HttpClientTest extends AbstractHttpClientServerTest
@Override @Override
public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception public boolean handle(org.eclipse.jetty.server.Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception
{ {
// Handler says it will handle the idletimeout // Handler says it will handle the idle timeout by ignoring it.
request.addIdleTimeoutListener(t -> false); request.addIdleTimeoutListener(t -> false);
TimeUnit.MILLISECONDS.sleep(2 * idleTimeout); // Sleep an non-integral number of idle timeouts to avoid
// racing with the idle timeout ticking every idle period.
TimeUnit.MILLISECONDS.sleep(idleTimeout * 3 / 2);
callback.succeeded(); callback.succeeded();
return true; return true;
} }

View File

@ -774,7 +774,7 @@ public class HttpGenerator
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug(_endOfContent.toString()); LOG.debug("endOfContent {} content-Length {}", _endOfContent.toString(), contentLength);
// Add transfer encoding if it is not chunking // Add transfer encoding if it is not chunking
if (transferEncoding != null) if (transferEncoding != null)

View File

@ -656,7 +656,7 @@ public class MultiPart
@Override @Override
public long getLength() public long getLength()
{ {
// TODO: it is difficult to calculate the length because // TODO: #10307 it is difficult to calculate the length because
// we need to allow for customization of the headers from // we need to allow for customization of the headers from
// subclasses, and then serialize all the headers to get // subclasses, and then serialize all the headers to get
// their length (handling UTF-8 values) and we don't want // their length (handling UTF-8 values) and we don't want

View File

@ -51,7 +51,6 @@ import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -417,12 +416,6 @@ public class HTTP2ServerConnection extends HTTP2Connection implements Connection
return getEndPoint().getLocalSocketAddress(); return getEndPoint().getLocalSocketAddress();
} }
@Override
public HostPort getServerAuthority()
{
return ConnectionMetaData.getServerAuthority(httpConfig, this);
}
@Override @Override
public Object getAttribute(String name) public Object getAttribute(String name)
{ {

View File

@ -14,11 +14,17 @@
package org.eclipse.jetty.io; package org.eclipse.jetty.io;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.IntUnaryOperator; import java.util.function.IntUnaryOperator;
import java.util.stream.Collectors;
import org.eclipse.jetty.io.internal.CompoundPool; import org.eclipse.jetty.io.internal.CompoundPool;
import org.eclipse.jetty.io.internal.QueuedPool; import org.eclipse.jetty.io.internal.QueuedPool;
@ -564,4 +570,112 @@ public class ArrayByteBufferPool implements ByteBufferPool, Dumpable
); );
} }
} }
/**
* <p>A variant of {@link ArrayByteBufferPool} that tracks buffer
* acquires/releases, useful to identify buffer leaks.</p>
* <p>Use {@link #getLeaks()} when the system is idle to get
* the {@link Buffer}s that have been leaked, which contain
* the stack trace information of where the buffer was acquired.</p>
*/
public static class Tracking extends ArrayByteBufferPool
{
private static final Logger LOG = LoggerFactory.getLogger(Tracking.class);
private final Set<Buffer> buffers = ConcurrentHashMap.newKeySet();
public Tracking()
{
this(0, -1, Integer.MAX_VALUE);
}
public Tracking(int minCapacity, int maxCapacity, int maxBucketSize)
{
this(minCapacity, maxCapacity, maxBucketSize, -1L, -1L);
}
public Tracking(int minCapacity, int maxCapacity, int maxBucketSize, long maxHeapMemory, long maxDirectMemory)
{
super(minCapacity, -1, maxCapacity, maxBucketSize, maxHeapMemory, maxDirectMemory);
}
@Override
public RetainableByteBuffer acquire(int size, boolean direct)
{
RetainableByteBuffer buffer = super.acquire(size, direct);
Buffer wrapper = new Buffer(buffer, size);
if (LOG.isDebugEnabled())
LOG.debug("acquired {}", wrapper);
buffers.add(wrapper);
return wrapper;
}
public Set<Buffer> getLeaks()
{
return buffers;
}
public String dumpLeaks()
{
return getLeaks().stream()
.map(Buffer::dump)
.collect(Collectors.joining(System.lineSeparator()));
}
public class Buffer extends RetainableByteBuffer.Wrapper
{
private final int size;
private final Instant acquireInstant;
private final Throwable acquireStack;
private Buffer(RetainableByteBuffer wrapped, int size)
{
super(wrapped);
this.size = size;
this.acquireInstant = Instant.now();
this.acquireStack = new Throwable();
}
public int getSize()
{
return size;
}
public Instant getAcquireInstant()
{
return acquireInstant;
}
public Throwable getAcquireStack()
{
return acquireStack;
}
@Override
public boolean release()
{
boolean released = super.release();
if (released)
{
buffers.remove(this);
if (LOG.isDebugEnabled())
LOG.debug("released {}", this);
}
return released;
}
public String dump()
{
StringWriter w = new StringWriter();
getAcquireStack().printStackTrace(new PrintWriter(w));
return "%s of %d bytes on %s at %s".formatted(getClass().getSimpleName(), getSize(), getAcquireInstant(), w);
}
@Override
public String toString()
{
return "%s@%x[%s]".formatted(getClass().getSimpleName(), hashCode(), super.toString());
}
}
}
} }

View File

@ -42,6 +42,7 @@ import org.eclipse.jetty.io.content.PathContentSource;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CompletableTask;
import org.eclipse.jetty.util.FutureCallback; import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
@ -104,7 +105,9 @@ public class ContentSourceTest
return List.of(asyncSource, byteBufferSource, transformerSource, pathSource, inputSource, inputSource2); return List.of(asyncSource, byteBufferSource, transformerSource, pathSource, inputSource, inputSource2);
} }
/** Get the next chunk, blocking if necessary /**
* Get the next chunk, blocking if necessary
*
* @param source The source to get the next chunk from * @param source The source to get the next chunk from
* @return A non null chunk * @return A non null chunk
*/ */
@ -113,8 +116,7 @@ public class ContentSourceTest
Content.Chunk chunk = source.read(); Content.Chunk chunk = source.read();
if (chunk != null) if (chunk != null)
return chunk; return chunk;
FuturePromise<Content.Chunk> next = new FuturePromise<>(); CompletableTask<Content.Chunk> task = new CompletableTask<>()
Runnable getNext = new Runnable()
{ {
@Override @Override
public void run() public void run()
@ -122,18 +124,12 @@ public class ContentSourceTest
Content.Chunk chunk = source.read(); Content.Chunk chunk = source.read();
if (chunk == null) if (chunk == null)
source.demand(this); source.demand(this);
next.succeeded(chunk); else
complete(chunk);
} }
}; };
source.demand(getNext); source.demand(task);
try return task.join();
{
return next.get();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
} }
@ParameterizedTest @ParameterizedTest
@ -141,8 +137,7 @@ public class ContentSourceTest
public void testRead(Content.Source source) throws Exception public void testRead(Content.Source source) throws Exception
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
CountDownLatch eof = new CountDownLatch(1); var task = new CompletableTask<>()
source.demand(new Runnable()
{ {
@Override @Override
public void run() public void run()
@ -162,14 +157,14 @@ public class ContentSourceTest
if (chunk.isLast()) if (chunk.isLast())
{ {
eof.countDown(); complete(null);
break; break;
} }
} }
} }
}); };
source.demand(task);
assertTrue(eof.await(10, TimeUnit.SECONDS)); task.get(10, TimeUnit.SECONDS);
assertThat(builder.toString(), is("onetwo")); assertThat(builder.toString(), is("onetwo"));
} }

View File

@ -181,6 +181,12 @@ public class DeferredAuthenticationState implements AuthenticationState.Deferred
return true; return true;
} }
@Override
public boolean hasLastWrite()
{
return false;
}
@Override @Override
public boolean isCompletedSuccessfully() public boolean isCompletedSuccessfully()
{ {

View File

@ -67,20 +67,21 @@ public interface ConnectionMetaData extends Attributes
/** /**
* @return The URI authority that this server represents. By default, this is the address of the network socket on * @return The URI authority that this server represents. By default, this is the address of the network socket on
* which the connection was accepted, but it may be wrapped to represent a virtual address. * which the connection was accepted, but it may be configured to a specific address.
* @see HttpConfiguration#setServerAuthority(HostPort)
*/ */
HostPort getServerAuthority(); default HostPort getServerAuthority()
static HostPort getServerAuthority(HttpConfiguration httpConfiguration, ConnectionMetaData connectionMetaData)
{ {
HttpConfiguration httpConfiguration = getHttpConfiguration();
HostPort authority = httpConfiguration.getServerAuthority(); HostPort authority = httpConfiguration.getServerAuthority();
if (authority != null) if (authority != null)
return authority; return authority;
SocketAddress local = connectionMetaData.getLocalSocketAddress(); SocketAddress localSocketAddress = getLocalSocketAddress();
if (local instanceof InetSocketAddress inet) if (localSocketAddress instanceof InetSocketAddress inetSocketAddress)
return new HostPort(inet.getHostString(), inet.getPort()); return new HostPort(inetSocketAddress.getHostString(), inetSocketAddress.getPort());
else if (localSocketAddress != null)
return new HostPort(localSocketAddress.toString());
return null; return null;
} }

View File

@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory;
* <p>This factory can be placed in front of any other connection factory * <p>This factory can be placed in front of any other connection factory
* to process the proxy v1 or v2 line before the normal protocol handling</p> * to process the proxy v1 or v2 line before the normal protocol handling</p>
* *
* @see <a href="http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt">http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt</a> * @see <a href="https://www.haproxy.org/download/2.8/doc/proxy-protocol.txt">PROXY protocol</a>
*/ */
public class ProxyConnectionFactory extends DetectorConnectionFactory public class ProxyConnectionFactory extends DetectorConnectionFactory
{ {
@ -245,6 +245,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
_buffer.release(); _buffer.release();
return unconsumed; return unconsumed;
} }
_buffer.release();
return null; return null;
} }
@ -564,6 +565,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
_buffer.release(); _buffer.release();
return unconsumed; return unconsumed;
} }
_buffer.release();
return null; return null;
} }
@ -591,7 +593,7 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
SocketAddress remote; SocketAddress remote;
switch (_family) switch (_family)
{ {
case INET: case INET ->
{ {
byte[] addr = new byte[4]; byte[] addr = new byte[4];
byteBuffer.get(addr); byteBuffer.get(addr);
@ -602,9 +604,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
int dstPort = byteBuffer.getChar(); int dstPort = byteBuffer.getChar();
local = new InetSocketAddress(dstAddr, dstPort); local = new InetSocketAddress(dstAddr, dstPort);
remote = new InetSocketAddress(srcAddr, srcPort); remote = new InetSocketAddress(srcAddr, srcPort);
break;
} }
case INET6: case INET6 ->
{ {
byte[] addr = new byte[16]; byte[] addr = new byte[16];
byteBuffer.get(addr); byteBuffer.get(addr);
@ -615,9 +616,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
int dstPort = byteBuffer.getChar(); int dstPort = byteBuffer.getChar();
local = new InetSocketAddress(dstAddr, dstPort); local = new InetSocketAddress(dstAddr, dstPort);
remote = new InetSocketAddress(srcAddr, srcPort); remote = new InetSocketAddress(srcAddr, srcPort);
break;
} }
case UNIX: case UNIX ->
{ {
byte[] addr = new byte[108]; byte[] addr = new byte[108];
byteBuffer.get(addr); byteBuffer.get(addr);
@ -626,12 +626,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
String dst = UnixDomain.toPath(addr); String dst = UnixDomain.toPath(addr);
local = UnixDomain.newSocketAddress(dst); local = UnixDomain.newSocketAddress(dst);
remote = UnixDomain.newSocketAddress(src); remote = UnixDomain.newSocketAddress(src);
break;
}
default:
{
throw new IllegalStateException("Unsupported family " + _family);
} }
default -> throw new IllegalStateException("Unsupported family " + _family);
} }
proxyEndPoint = new ProxyEndPoint(endPoint, local, remote); proxyEndPoint = new ProxyEndPoint(endPoint, local, remote);
@ -714,37 +710,20 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
int transportAndFamily = 0xFF & byteBuffer.get(); int transportAndFamily = 0xFF & byteBuffer.get();
switch (transportAndFamily >> 4) switch (transportAndFamily >> 4)
{ {
case 0: case 0 -> _family = Family.UNSPEC;
_family = Family.UNSPEC; case 1 -> _family = Family.INET;
break; case 2 -> _family = Family.INET6;
case 1: case 3 -> _family = Family.UNIX;
_family = Family.INET; default -> throw new IOException("Proxy v2 bad PROXY family");
break;
case 2:
_family = Family.INET6;
break;
case 3:
_family = Family.UNIX;
break;
default:
throw new IOException("Proxy v2 bad PROXY family");
} }
Transport transport; Transport transport = switch (transportAndFamily & 0xF)
switch (transportAndFamily & 0xF)
{ {
case 0: case 0 -> Transport.UNSPEC;
transport = Transport.UNSPEC; case 1 -> Transport.STREAM;
break; case 2 -> Transport.DGRAM;
case 1: default -> throw new IOException("Proxy v2 bad PROXY family");
transport = Transport.STREAM; };
break;
case 2:
transport = Transport.DGRAM;
break;
default:
throw new IOException("Proxy v2 bad PROXY family");
}
_length = byteBuffer.getChar(); _length = byteBuffer.getChar();
@ -761,6 +740,8 @@ public class ProxyConnectionFactory extends DetectorConnectionFactory
private void releaseAndClose() private void releaseAndClose()
{ {
if (LOG.isDebugEnabled())
LOG.debug("Proxy v2 releasing buffer and closing");
_buffer.release(); _buffer.release();
close(); close();
} }

View File

@ -403,6 +403,12 @@ public interface Request extends Attributes, Content.Source
return -1; return -1;
} }
/**
* Get the logical name the request was sent to, which may be from the authority of the
* request; the configured server authority; the actual network name of the server;
* @param request The request to get the server name of
* @return The logical server name or null if it cannot be determined.
*/
static String getServerName(Request request) static String getServerName(Request request)
{ {
if (request == null) if (request == null)
@ -414,38 +420,42 @@ public interface Request extends Attributes, Content.Source
HostPort authority = request.getConnectionMetaData().getServerAuthority(); HostPort authority = request.getConnectionMetaData().getServerAuthority();
if (authority != null) if (authority != null)
return HostPort.normalizeHost(authority.getHost()); return authority.getHost();
SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress(); return null;
if (local instanceof InetSocketAddress)
return HostPort.normalizeHost(((InetSocketAddress)local).getHostString());
return local == null ? null : local.toString();
} }
/**
* Get the logical port a request was received on, which may be from the authority of the request; the
* configured server authority; the default port for the scheme; or the actual network port.
* @param request The request to get the port of
* @return The port for the request if it can be determined, otherwise -1
*/
static int getServerPort(Request request) static int getServerPort(Request request)
{ {
if (request == null) if (request == null)
return -1; return -1;
// Does the request have an explicit port?
HttpURI uri = request.getHttpURI(); HttpURI uri = request.getHttpURI();
if (uri.hasAuthority() && uri.getPort() > 0) if (uri.hasAuthority() && uri.getPort() > 0)
return uri.getPort(); return uri.getPort();
HostPort authority = request.getConnectionMetaData().getServerAuthority(); // Is there a configured server authority?
HostPort authority = request.getConnectionMetaData().getHttpConfiguration().getServerAuthority();
if (authority != null && authority.getPort() > 0) if (authority != null && authority.getPort() > 0)
return authority.getPort(); return authority.getPort();
if (authority == null) // Is there a scheme with a default port?
{
SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress();
if (local instanceof InetSocketAddress)
return ((InetSocketAddress)local).getPort();
}
HttpScheme scheme = HttpScheme.CACHE.get(request.getHttpURI().getScheme()); HttpScheme scheme = HttpScheme.CACHE.get(request.getHttpURI().getScheme());
if (scheme != null) if (scheme != null && scheme.getDefaultPort() > 0)
return scheme.getDefaultPort(); return scheme.getDefaultPort();
// Is there a local port?
SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress();
if (local instanceof InetSocketAddress inetSocketAddress && inetSocketAddress.getPort() > 0)
return inetSocketAddress.getPort();
return -1; return -1;
} }
@ -770,11 +780,11 @@ public interface Request extends Attributes, Content.Source
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static <T> T as(Request request, Class<T> type) static <T> T as(Request request, Class<T> type)
{ {
while (request instanceof Request.Wrapper wrapper) while (request != null)
{ {
if (type.isInstance(wrapper)) if (type.isInstance(request))
return (T)wrapper; return (T)request;
request = wrapper.getWrapped(); request = request instanceof Request.Wrapper wrapper ? wrapper.getWrapped() : null;
} }
return null; return null;
} }

View File

@ -145,7 +145,7 @@ public class ResourceService
return _gzipEquivalentFileExtensions; return _gzipEquivalentFileExtensions;
} }
public void doGet(Request request, Response response, Callback callback, HttpContent content) throws Exception public void doGet(Request request, Response response, Callback callback, HttpContent content)
{ {
String pathInContext = Request.getPathInContext(request); String pathInContext = Request.getPathInContext(request);
@ -523,7 +523,7 @@ public class ResourceService
// TODO : check conditional headers. // TODO : check conditional headers.
serveWelcome(request, response, callback, welcomeAction.target); serveWelcome(request, response, callback, welcomeAction.target);
case REHANDLE -> rehandleWelcome(request, response, callback, welcomeAction.target); case REHANDLE -> rehandleWelcome(request, response, callback, welcomeAction.target);
}; }
} }
/** /**
@ -683,14 +683,15 @@ public class ResourceService
} }
// There are multiple non-overlapping ranges, send a multipart/byteranges 206 response. // There are multiple non-overlapping ranges, send a multipart/byteranges 206 response.
putHeaders(response, content, NO_CONTENT_LENGTH);
response.setStatus(HttpStatus.PARTIAL_CONTENT_206); response.setStatus(HttpStatus.PARTIAL_CONTENT_206);
String contentType = "multipart/byteranges; boundary="; String contentType = "multipart/byteranges; boundary=";
String boundary = MultiPart.generateBoundary(null, 24); String boundary = MultiPart.generateBoundary(null, 24);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType + boundary);
MultiPartByteRanges.ContentSource byteRanges = new MultiPartByteRanges.ContentSource(boundary); MultiPartByteRanges.ContentSource byteRanges = new MultiPartByteRanges.ContentSource(boundary);
ranges.forEach(range -> byteRanges.addPart(new MultiPartByteRanges.Part(content.getContentTypeValue(), content.getResource().getPath(), range, contentLength))); ranges.forEach(range -> byteRanges.addPart(new MultiPartByteRanges.Part(content.getContentTypeValue(), content.getResource().getPath(), range, contentLength)));
byteRanges.close(); byteRanges.close();
long partsContentLength = byteRanges.getLength();
putHeaders(response, content, partsContentLength);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType + boundary);
Content.copy(byteRanges, response, callback); Content.copy(byteRanges, response, callback);
} }

View File

@ -99,6 +99,13 @@ public interface Response extends Content.Sink
*/ */
boolean isCommitted(); boolean isCommitted();
/**
* <p>Returns whether the last write has been initiated on the response.</p>
*
* @return {@code true} if {@code last==true} has been passed to {@link #write(boolean, ByteBuffer, Callback)}.
*/
boolean hasLastWrite();
/** /**
* <p>Returns whether the response completed successfully.</p> * <p>Returns whether the response completed successfully.</p>
* <p>The response HTTP status code, HTTP headers and content * <p>The response HTTP status code, HTTP headers and content
@ -207,13 +214,13 @@ public interface Response extends Content.Sink
* @see Wrapper * @see Wrapper
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static <T extends Response.Wrapper> T as(Response response, Class<T> type) static <T extends Response> T as(Response response, Class<T> type)
{ {
while (response instanceof Response.Wrapper wrapper) while (response != null)
{ {
if (type.isInstance(wrapper)) if (type.isInstance(response))
return (T)wrapper; return (T)response;
response = wrapper.getWrapped(); response = response instanceof Response.Wrapper wrapper ? wrapper.getWrapped() : null;
} }
return null; return null;
} }
@ -580,6 +587,12 @@ public interface Response extends Content.Sink
return getWrapped().isCommitted(); return getWrapped().isCommitted();
} }
@Override
public boolean hasLastWrite()
{
return getWrapped().hasLastWrite();
}
@Override @Override
public boolean isCompletedSuccessfully() public boolean isCompletedSuccessfully()
{ {

View File

@ -69,8 +69,7 @@ public abstract class ReHandlingErrorHandler extends ErrorHandler
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Unable to process error {}", reRequest, e); LOG.debug("Unable to process error {}", reRequest, e);
if (ExceptionUtil.areNotAssociated(cause, e)) ExceptionUtil.addSuppressedIfNotAssociated(cause, e);
cause.addSuppressed(e);
response.setStatus(code); response.setStatus(code);
} }
} }

View File

@ -136,7 +136,13 @@ public class GzipResponseAndCallback extends Response.Wrapper implements Callbac
case NOT_COMPRESSING -> super.write(last, content, callback); case NOT_COMPRESSING -> super.write(last, content, callback);
case COMMITTING -> callback.failed(new WritePendingException()); case COMMITTING -> callback.failed(new WritePendingException());
case COMPRESSING -> gzip(last, callback, content); case COMPRESSING -> gzip(last, callback, content);
default -> callback.failed(new IllegalStateException("state=" + _state.get())); default ->
{
if (BufferUtil.isEmpty(content))
callback.succeeded();
else
callback.failed(new IllegalStateException("state=" + _state.get()));
}
} }
} }

View File

@ -473,8 +473,7 @@ public class HttpChannelState implements HttpChannel, Components
} }
catch (Throwable throwable) catch (Throwable throwable)
{ {
if (ExceptionUtil.areNotAssociated(x, throwable)) ExceptionUtil.addSuppressedIfNotAssociated(x, throwable);
x.addSuppressed(throwable);
} }
// If the application has not been otherwise informed of the failure // If the application has not been otherwise informed of the failure
@ -1080,8 +1079,7 @@ public class HttpChannelState implements HttpChannel, Components
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(throwable, t)) ExceptionUtil.addSuppressedIfNotAssociated(throwable, t);
throwable.addSuppressed(t);
} }
finally finally
{ {
@ -1354,6 +1352,18 @@ public class HttpChannelState implements HttpChannel, Components
return _httpFields.isCommitted(); return _httpFields.isCommitted();
} }
@Override
public boolean hasLastWrite()
{
try (AutoLock ignored = _request._lock.lock())
{
if (_request._httpChannelState == null)
return true;
return _request._httpChannelState._streamSendState != StreamSendState.SENDING;
}
}
@Override @Override
public boolean isCompletedSuccessfully() public boolean isCompletedSuccessfully()
{ {
@ -1540,8 +1550,7 @@ public class HttpChannelState implements HttpChannel, Components
// Consume any input. // Consume any input.
Throwable unconsumed = stream.consumeAvailable(); Throwable unconsumed = stream.consumeAvailable();
if (ExceptionUtil.areNotAssociated(unconsumed, failure)) ExceptionUtil.addSuppressedIfNotAssociated(failure, unconsumed);
failure.addSuppressed(unconsumed);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", httpChannelState._stream.isCommitted(), httpChannelState._response.isCommitted(), this); LOG.debug("failed stream.isCommitted={}, response.isCommitted={} {}", httpChannelState._stream.isCommitted(), httpChannelState._response.isCommitted(), this);
@ -1689,8 +1698,7 @@ public class HttpChannelState implements HttpChannel, Components
Callback.from(() -> httpChannelState._handlerInvoker.failed(failure), Callback.from(() -> httpChannelState._handlerInvoker.failed(failure),
x -> x ->
{ {
if (ExceptionUtil.areNotAssociated(failure, x)) ExceptionUtil.addSuppressedIfNotAssociated(failure, x);
failure.addSuppressed(x);
httpChannelState._handlerInvoker.failed(failure); httpChannelState._handlerInvoker.failed(failure);
})); }));
} }
@ -1758,8 +1766,7 @@ public class HttpChannelState implements HttpChannel, Components
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(failure, t)) ExceptionUtil.addSuppressedIfNotAssociated(failure, t);
failure.addSuppressed(t);
super.onError(task, failure); super.onError(task, failure);
} }
} }

View File

@ -288,15 +288,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
return getEndPoint().getLocalSocketAddress(); return getEndPoint().getLocalSocketAddress();
} }
@Override
public HostPort getServerAuthority()
{
HostPort authority = ConnectionMetaData.getServerAuthority(getHttpConfiguration(), this);
if (authority == null)
authority = new HostPort(getLocalSocketAddress().toString(), -1);
return authority;
}
@Override @Override
public Object removeAttribute(String name) public Object removeAttribute(String name)
{ {

View File

@ -14,6 +14,8 @@
package org.eclipse.jetty.server; package org.eclipse.jetty.server;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -22,6 +24,9 @@ import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpCompliance; import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.internal.HttpConnection;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -75,7 +80,25 @@ public class ForwardedRequestCustomizerTest
server.addConnector(connector); server.addConnector(connector);
// Alternate behavior Connector // Alternate behavior Connector
HttpConnectionFactory httpAlt = new HttpConnectionFactory(); HttpConnectionFactory httpAlt = new HttpConnectionFactory()
{
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
HttpConnection connection = new HttpConnection(getHttpConfiguration(), connector, endPoint, isRecordHttpComplianceViolations())
{
@Override
public SocketAddress getLocalSocketAddress()
{
return InetSocketAddress.createUnresolved("test", 42);
}
};
connection.setUseInputDirectByteBuffers(isUseInputDirectByteBuffers());
connection.setUseOutputDirectByteBuffers(isUseOutputDirectByteBuffers());
return configure(connection, connector, endPoint);
}
};
httpAlt.getHttpConfiguration().setSecurePort(8443); httpAlt.getHttpConfiguration().setSecurePort(8443);
httpAlt.getHttpConfiguration().setHttpCompliance(mismatchedAuthorityHttpCompliance); httpAlt.getHttpConfiguration().setHttpCompliance(mismatchedAuthorityHttpCompliance);
customizerAlt = new ForwardedRequestCustomizer(); customizerAlt = new ForwardedRequestCustomizer();
@ -128,27 +151,6 @@ public class ForwardedRequestCustomizerTest
server.stop(); server.stop();
} }
public static Stream<Arguments> cases2()
{
return Stream.of(
Arguments.of(new TestRequest("https initial authority, X-Forwarded-Proto on http, Proxy-Ssl-Id exists (setSslIsSecure==false)")
.configureCustomizer((customizer) -> customizer.setSslIsSecure(false))
.headers(
"GET https://alt.example.net/foo HTTP/1.1",
"Host: alt.example.net",
"X-Forwarded-Proto: http",
"Proxy-Ssl-Id: Wibble"
),
new Expectations()
.scheme("http").serverName("alt.example.net").serverPort(80)
.secure(false)
.requestURL("http://alt.example.net/foo")
.remoteAddr("0.0.0.0").remotePort(0)
.sslSession("Wibble")
)
);
}
public static Stream<Arguments> cases() public static Stream<Arguments> cases()
{ {
return Stream.of( return Stream.of(
@ -1032,7 +1034,6 @@ public class ForwardedRequestCustomizerTest
request.configure(customizer); request.configure(customizer);
String rawRequest = request.getRawRequest((header) -> header); String rawRequest = request.getRawRequest((header) -> header);
// System.out.println(rawRequest);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest)); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest));
assertThat("status", response.getStatus(), is(200)); assertThat("status", response.getStatus(), is(200));
@ -1052,7 +1053,6 @@ public class ForwardedRequestCustomizerTest
.replaceFirst("X-Proxied-Https:", "Jetty-Proxied-Https:") .replaceFirst("X-Proxied-Https:", "Jetty-Proxied-Https:")
.replaceFirst("Proxy-Ssl-Id:", "Jetty-Proxy-Ssl-Id:") .replaceFirst("Proxy-Ssl-Id:", "Jetty-Proxy-Ssl-Id:")
.replaceFirst("Proxy-auth-cert:", "Jetty-Proxy-Auth-Cert:")); .replaceFirst("Proxy-auth-cert:", "Jetty-Proxy-Auth-Cert:"));
// System.out.println(rawRequest);
HttpTester.Response response = HttpTester.parseResponse(connectorConfigured.getResponse(rawRequest)); HttpTester.Response response = HttpTester.parseResponse(connectorConfigured.getResponse(rawRequest));
assertThat("status", response.getStatus(), is(200)); assertThat("status", response.getStatus(), is(200));
@ -1063,6 +1063,40 @@ public class ForwardedRequestCustomizerTest
public static Stream<Arguments> nonStandardPortCases() public static Stream<Arguments> nonStandardPortCases()
{ {
return Stream.of( return Stream.of(
// HTTP 1.0
Arguments.of(
new TestRequest("HTTP/1.0 - no Host header")
.headers(
"GET /example HTTP/1.0"
),
new Expectations()
.scheme("http").serverName("test").serverPort(42)
.secure(false)
.requestURL("http://test:42/example")
),
Arguments.of(
new TestRequest("HTTP/1.0 - Empty Host header")
.headers(
"GET scheme:///example HTTP/1.0",
"Host:"
),
new Expectations()
.scheme("scheme").serverName(null).serverPort(42)
.secure(false)
.requestURL("scheme:///example")
),
Arguments.of(
new TestRequest("HTTP/1.0 - Host header")
.headers(
"GET /example HTTP/1.0",
"Host: server:9999"
),
new Expectations()
.scheme("http").serverName("server").serverPort(9999)
.secure(false)
.requestURL("http://server:9999/example")
),
// RFC7239 Tests with https. // RFC7239 Tests with https.
Arguments.of(new TestRequest("RFC7239 with https and h2") Arguments.of(new TestRequest("RFC7239 with https and h2")
.headers( .headers(
@ -1106,7 +1140,7 @@ public class ForwardedRequestCustomizerTest
/** /**
* Tests against a Connector with a HttpConfiguration on non-standard ports. * Tests against a Connector with a HttpConfiguration on non-standard ports.
* HttpConfiguration is set to securePort of 8443 * HttpConfiguration is set to securePort of 8443 and the local port is 42.
*/ */
@ParameterizedTest(name = "{0}") @ParameterizedTest(name = "{0}")
@MethodSource("nonStandardPortCases") @MethodSource("nonStandardPortCases")

View File

@ -19,7 +19,6 @@ import java.nio.channels.SocketChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.http.ByteRange; import org.eclipse.jetty.http.ByteRange;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
@ -31,7 +30,6 @@ import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartByteRanges; import org.eclipse.jetty.http.MultiPartByteRanges;
import org.eclipse.jetty.io.ArrayByteBufferPool; import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
@ -49,13 +47,13 @@ public class MultiPartByteRangesTest
{ {
private Server server; private Server server;
private ServerConnector connector; private ServerConnector connector;
private LeakTrackingBufferPool byteBufferPool; private ArrayByteBufferPool.Tracking byteBufferPool;
private void start(Handler handler) throws Exception private void start(Handler handler) throws Exception
{ {
QueuedThreadPool serverThreads = new QueuedThreadPool(); QueuedThreadPool serverThreads = new QueuedThreadPool();
serverThreads.setName("server"); serverThreads.setName("server");
byteBufferPool = new LeakTrackingBufferPool(); byteBufferPool = new ArrayByteBufferPool.Tracking();
server = new Server(serverThreads, null, byteBufferPool); server = new Server(serverThreads, null, byteBufferPool);
connector = new ServerConnector(server, 1, 1); connector = new ServerConnector(server, 1, 1);
server.addConnector(connector); server.addConnector(connector);
@ -67,7 +65,7 @@ public class MultiPartByteRangesTest
public void dispose() public void dispose()
{ {
LifeCycle.stop(server); LifeCycle.stop(server);
assertEquals(0, byteBufferPool.countLeaks()); assertEquals(0, byteBufferPool.getLeaks().size());
} }
@Test @Test
@ -131,31 +129,4 @@ public class MultiPartByteRangesTest
assertEquals("CDEF", Content.Source.asString(part3.getContentSource())); assertEquals("CDEF", Content.Source.asString(part3.getContentSource()));
} }
} }
private static class LeakTrackingBufferPool extends ArrayByteBufferPool
{
private final AtomicInteger leaks = new AtomicInteger();
public int countLeaks()
{
return leaks.get();
}
@Override
public RetainableByteBuffer acquire(int size, boolean direct)
{
leaks.incrementAndGet();
return new RetainableByteBuffer.Wrapper(super.acquire(size, direct))
{
@Override
public boolean release()
{
boolean released = super.release();
if (released)
leaks.decrementAndGet();
return released;
}
};
}
}
} }

View File

@ -207,7 +207,7 @@ public class BaseBuilder
List<String> startLines = new ArrayList<>(); List<String> startLines = new ArrayList<>();
for (Path path : paths) for (Path path : paths)
{ {
StartLog.info("copy " + baseHome.toShortForm(path) + " into " + baseHome.toShortForm(startini)); StartLog.info("copy %s into %s", baseHome.toShortForm(path), baseHome.toShortForm(startini));
startLines.add(""); startLines.add("");
startLines.add("# Config from " + baseHome.toShortForm(path)); startLines.add("# Config from " + baseHome.toShortForm(path));
startLines.addAll(Files.readAllLines(path)); startLines.addAll(Files.readAllLines(path));
@ -250,7 +250,7 @@ public class BaseBuilder
if (FS.ensureDirectoryExists(startd)) if (FS.ensureDirectoryExists(startd))
{ {
StartLog.info("mkdir " + baseHome.toShortForm(startd)); StartLog.info("mkdir %s", baseHome.toShortForm(startd));
modified.set(true); modified.set(true);
} }

View File

@ -23,12 +23,9 @@ import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.FileTime; import java.nio.file.attribute.FileTime;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.eclipse.jetty.util.FileID; import org.eclipse.jetty.util.FileID;
@ -130,7 +127,7 @@ public class FS
if (!Files.isDirectory(path)) if (!Files.isDirectory(path))
{ {
// not a directory (as expected) // not a directory (as expected)
StartLog.warn("Not a directory: " + path); StartLog.warn("Not a directory: %s", path);
return false; return false;
} }

View File

@ -14,14 +14,10 @@
package org.eclipse.jetty.start; package org.eclipse.jetty.start;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URLConnection;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -167,7 +163,7 @@ public abstract class FileInitializer
{ {
if (FS.ensureDirectoryExists(to)) if (FS.ensureDirectoryExists(to))
{ {
StartLog.info("mkdir " + _basehome.toShortForm(to)); StartLog.info("mkdir %s", _basehome.toShortForm(to));
modified = true; modified = true;
} }

View File

@ -186,8 +186,8 @@ public class Main
StartLog.error("Do not start with ${jetty.base} == ${jetty.home}!"); StartLog.error("Do not start with ${jetty.base} == ${jetty.home}!");
else else
StartLog.error("No enabled jetty modules found!"); StartLog.error("No enabled jetty modules found!");
StartLog.info("${jetty.home} = " + getBaseHome().getHomePath()); StartLog.info("${jetty.home} = %s", getBaseHome().getHomePath());
StartLog.info("${jetty.base} = " + getBaseHome().getBasePath()); StartLog.info("${jetty.base} = %s", getBaseHome().getBasePath());
StartLog.error("Please create and/or configure a ${jetty.base} directory."); StartLog.error("Please create and/or configure a ${jetty.base} directory.");
usageExit(ERR_INVOKE_MAIN); usageExit(ERR_INVOKE_MAIN);
return; return;
@ -201,7 +201,7 @@ public class Main
} }
catch (ClassNotFoundException e) catch (ClassNotFoundException e)
{ {
StartLog.error("Unable to find: " + mainclass); StartLog.error("Unable to find: %s", mainclass);
StartLog.debug(e); StartLog.debug(e);
usageExit(ERR_INVOKE_MAIN); usageExit(ERR_INVOKE_MAIN);
return; return;
@ -489,7 +489,7 @@ public class Main
final Process process = pbuilder.start(); final Process process = pbuilder.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> Runtime.getRuntime().addShutdownHook(new Thread(() ->
{ {
StartLog.debug("Destroying " + process); StartLog.debug("Destroying %s", process);
process.destroy(); process.destroy();
})); }));
@ -685,7 +685,7 @@ public class Main
} }
else else
{ {
StartLog.warn("Unable to find resource: " + resourceName); StartLog.warn("Unable to find resource: %s", resourceName);
} }
} }
catch (IOException e) catch (IOException e)

View File

@ -42,7 +42,7 @@ public class PathFinder extends SimpleFileVisitor<Path>
private void addHit(Path path) private void addHit(Path path)
{ {
String relPath = basePath.relativize(path).toString(); String relPath = basePath.relativize(path).toString();
StartLog.debug("Found [" + relPath + "] " + path); StartLog.debug("Found [%s] %s", relPath, path);
hits.put(relPath, path); hits.put(relPath, path);
} }
@ -76,7 +76,7 @@ public class PathFinder extends SimpleFileVisitor<Path>
{ {
if (dirMatcher.matches(dir)) if (dirMatcher.matches(dir))
{ {
StartLog.trace("Following dir: " + dir); StartLog.trace("Following dir: %s", dir);
if (includeDirsInResults && fileMatcher.matches(dir)) if (includeDirsInResults && fileMatcher.matches(dir))
{ {
addHit(dir); addHit(dir);
@ -85,7 +85,7 @@ public class PathFinder extends SimpleFileVisitor<Path>
} }
else else
{ {
StartLog.trace("Skipping dir: " + dir); StartLog.trace("Skipping dir: %s", dir);
return FileVisitResult.SKIP_SUBTREE; return FileVisitResult.SKIP_SUBTREE;
} }
} }
@ -131,7 +131,7 @@ public class PathFinder extends SimpleFileVisitor<Path>
} }
else else
{ {
StartLog.trace("Ignoring file: " + file); StartLog.trace("Ignoring file: %s", file);
} }
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@ -143,7 +143,7 @@ public class PathFinder extends SimpleFileVisitor<Path>
{ {
if (!NOTIFIED_PATHS.contains(file)) if (!NOTIFIED_PATHS.contains(file))
{ {
StartLog.warn("skipping detected filesystem loop: " + file); StartLog.warn("skipping detected filesystem loop: %s", file);
NOTIFIED_PATHS.add(file); NOTIFIED_PATHS.add(file);
} }
return FileVisitResult.SKIP_SUBTREE; return FileVisitResult.SKIP_SUBTREE;

View File

@ -84,7 +84,7 @@ public class PathMatchers
// use FileSystem default pattern behavior // use FileSystem default pattern behavior
if (pattern.startsWith("glob:") || pattern.startsWith("regex:")) if (pattern.startsWith("glob:") || pattern.startsWith("regex:"))
{ {
StartLog.debug("Using Standard " + fs.getClass().getName() + " pattern: " + pattern); StartLog.debug("Using Standard %s pattern: %s", fs.getClass().getName(), pattern);
return fs.getPathMatcher(pattern); return fs.getPathMatcher(pattern);
} }
@ -93,14 +93,14 @@ public class PathMatchers
if (isAbsolute(pattern)) if (isAbsolute(pattern))
{ {
String pat = "glob:" + pattern; String pat = "glob:" + pattern;
StartLog.debug("Using absolute path pattern: " + pat); StartLog.debug("Using absolute path pattern: %s", pat);
return fs.getPathMatcher(pat); return fs.getPathMatcher(pat);
} }
// Doesn't start with filesystem root, then assume the pattern // Doesn't start with filesystem root, then assume the pattern
// is a relative file path pattern. // is a relative file path pattern.
String pat = "glob:**/" + pattern; String pat = "glob:**/" + pattern;
StartLog.debug("Using relative path pattern: " + pat); StartLog.debug("Using relative path pattern: %s", pat);
return fs.getPathMatcher(pat); return fs.getPathMatcher(pat);
} }

View File

@ -428,9 +428,9 @@ public class StartArgs
for (String rawlibref : module.getLibs()) for (String rawlibref : module.getLibs())
{ {
StartLog.debug("rawlibref = " + rawlibref); StartLog.debug("rawlibref = %s", rawlibref);
String libref = environment.getProperties().expand(rawlibref); String libref = environment.getProperties().expand(rawlibref);
StartLog.debug("expanded = " + libref); StartLog.debug("expanded = %s", libref);
for (Path libpath : baseHome.getPaths(libref)) for (Path libpath : baseHome.getPaths(libref))
{ {
@ -555,6 +555,7 @@ public class StartArgs
if (parts.contains("path")) if (parts.contains("path"))
{ {
Classpath classpath = jettyEnvironment.getClasspath(); Classpath classpath = jettyEnvironment.getClasspath();
StartLog.debug("classpath=%s - isJPMS=%b", classpath, isJPMS());
if (isJPMS()) if (isJPMS())
{ {
Map<Boolean, List<Path>> dirsAndFiles = StreamSupport.stream(classpath.spliterator(), false) Map<Boolean, List<Path>> dirsAndFiles = StreamSupport.stream(classpath.spliterator(), false)
@ -605,6 +606,7 @@ public class StartArgs
} }
generateJpmsArgs(cmd); generateJpmsArgs(cmd);
StartLog.debug("JPMS resulting cmd=%s", cmd.toCommandLine());
} }
else if (!classpath.isEmpty()) else if (!classpath.isEmpty())
{ {
@ -1255,7 +1257,7 @@ public class StartArgs
if (arg.startsWith("--add-to-start=") || arg.startsWith("--add-to-startd=")) if (arg.startsWith("--add-to-start=") || arg.startsWith("--add-to-startd="))
{ {
String value = Props.getValue(arg); String value = Props.getValue(arg);
StartLog.warn("Option " + arg.split("=")[0] + " is deprecated! Instead use: --add-modules=%s", value); StartLog.warn("Option %s is deprecated! Instead use: --add-modules=%s", arg.split("=")[0], value);
} }
startModules.addAll(Props.getValues(arg)); startModules.addAll(Props.getValues(arg));
run = false; run = false;

View File

@ -231,9 +231,9 @@ public class StartEnvironment
StartLog.debug("Expanding Libs"); StartLog.debug("Expanding Libs");
for (String rawlibref : _libRefs) for (String rawlibref : _libRefs)
{ {
StartLog.debug("rawlibref = " + rawlibref); StartLog.debug("rawlibref = %s", rawlibref);
String libref = getProperties().expand(rawlibref); String libref = getProperties().expand(rawlibref);
StartLog.debug("expanded = " + libref); StartLog.debug("expanded = %s", libref);
// perform path escaping (needed by windows) // perform path escaping (needed by windows)
libref = libref.replaceAll("\\\\([^\\\\])", "\\\\\\\\$1"); libref = libref.replaceAll("\\\\([^\\\\])", "\\\\\\\\$1");

View File

@ -42,7 +42,7 @@ public class StartDirBuilder implements BaseBuilder.Config
this.baseHome = baseBuilder.getBaseHome(); this.baseHome = baseBuilder.getBaseHome();
this.startDir = baseHome.getBasePath("start.d"); this.startDir = baseHome.getBasePath("start.d");
if (FS.ensureDirectoryExists(startDir)) if (FS.ensureDirectoryExists(startDir))
StartLog.info("mkdir " + baseHome.toShortForm(startDir)); StartLog.info("mkdir %s", baseHome.toShortForm(startDir));
} }
@Override @Override

View File

@ -64,7 +64,7 @@ public class BaseHomeFileInitializer extends FileInitializer
else if (FS.ensureDirectoryExists(destination)) else if (FS.ensureDirectoryExists(destination))
{ {
modified = true; modified = true;
StartLog.info("mkdir " + _basehome.toShortForm(destination)); StartLog.info("mkdir %s", _basehome.toShortForm(destination));
} }
copyDirectory(source, destination); copyDirectory(source, destination);
@ -74,7 +74,7 @@ public class BaseHomeFileInitializer extends FileInitializer
if (FS.ensureDirectoryExists(destination.getParent())) if (FS.ensureDirectoryExists(destination.getParent()))
{ {
modified = true; modified = true;
StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent()));
} }
if (!FS.exists(destination)) if (!FS.exists(destination))

View File

@ -61,7 +61,7 @@ public abstract class DownloadFileInitializer extends FileInitializer
} }
if (FS.ensureDirectoryExists(destination.getParent())) if (FS.ensureDirectoryExists(destination.getParent()))
StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent()));
StartLog.info("download %s to %s", uri, _basehome.toShortForm(destination)); StartLog.info("download %s to %s", uri, _basehome.toShortForm(destination));

View File

@ -73,7 +73,7 @@ public class LocalFileInitializer extends FileInitializer
// Create directory // Create directory
boolean mkdir = FS.ensureDirectoryExists(destination); boolean mkdir = FS.ensureDirectoryExists(destination);
if (mkdir) if (mkdir)
StartLog.info("mkdir " + _basehome.toShortForm(destination)); StartLog.info("mkdir %s", _basehome.toShortForm(destination));
return mkdir; return mkdir;
} }

View File

@ -165,7 +165,7 @@ public class MavenLocalRepoFileInitializer extends DownloadFileInitializer
if (!FS.canReadFile(localFile)) if (!FS.canReadFile(localFile))
{ {
if (FS.ensureDirectoryExists(localFile.getParent())) if (FS.ensureDirectoryExists(localFile.getParent()))
StartLog.info("mkdir " + _basehome.toShortForm(localFile.getParent())); StartLog.info("mkdir %s", _basehome.toShortForm(localFile.getParent()));
download(coords, localFile); download(coords, localFile);
if (!FS.canReadFile(localFile)) if (!FS.canReadFile(localFile))
{ {
@ -209,7 +209,7 @@ public class MavenLocalRepoFileInitializer extends DownloadFileInitializer
if (localRepoFile != null) if (localRepoFile != null)
{ {
if (FS.ensureDirectoryExists(destination.getParent())) if (FS.ensureDirectoryExists(destination.getParent()))
StartLog.info("mkdir " + _basehome.toShortForm(destination.getParent())); StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent()));
StartLog.info("copy %s to %s", localRepoFile, _basehome.toShortForm(destination)); StartLog.info("copy %s to %s", localRepoFile, _basehome.toShortForm(destination));
Files.copy(localRepoFile, destination); Files.copy(localRepoFile, destination);
return true; return true;

View File

@ -59,11 +59,16 @@ import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
public class AbstractTest public class AbstractTest
{ {
@RegisterExtension
public final BeforeTestExecutionCallback printMethodName = context ->
System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName());
protected final HttpConfiguration httpConfig = new HttpConfiguration(); protected final HttpConfiguration httpConfig = new HttpConfiguration();
protected SslContextFactory.Server sslContextFactoryServer; protected SslContextFactory.Server sslContextFactoryServer;
protected Server server; protected Server server;

View File

@ -22,7 +22,6 @@ import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.Destination; import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpClientTransport; import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.transport.HttpExchange; import org.eclipse.jetty.client.transport.HttpExchange;
import org.eclipse.jetty.client.transport.internal.HttpChannelOverHTTP; import org.eclipse.jetty.client.transport.internal.HttpChannelOverHTTP;
@ -209,9 +208,9 @@ public class HttpChannelAssociationTest extends AbstractTest
return new HttpConnectionOverFCGI(endPoint, destination, promise) return new HttpConnectionOverFCGI(endPoint, destination, promise)
{ {
@Override @Override
protected HttpChannelOverFCGI newHttpChannel(Request request) protected HttpChannelOverFCGI newHttpChannel()
{ {
return new HttpChannelOverFCGI(this, getFlusher(), request.getIdleTimeout()) return new HttpChannelOverFCGI(this)
{ {
@Override @Override
public boolean associate(HttpExchange exchange) public boolean associate(HttpExchange exchange)

View File

@ -478,8 +478,7 @@ public interface Callback extends Invocable
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(x, t)) ExceptionUtil.addSuppressedIfNotAssociated(x, t);
x.addSuppressed(t);
} }
finally finally
{ {

View File

@ -0,0 +1,76 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.util;
import java.util.concurrent.CompletableFuture;
/**
* <p>A {@link CompletableFuture} that implements {@link Runnable} to perform
* a one-shot task that eventually completes this {@link CompletableFuture}.</p>
* <p>Subclasses override {@link #run()} to implement the task.</p>
* <p>Users of this class start the task execution via {@link #start()}.</p>
* <p>Typical usage:</p>
* <pre>{@code
* CompletableTask<T> task = new CompletableTask<>()
* {
* @Override
* public void run()
* {
* try
* {
* // Perform some task.
* T result = performTask();
*
* // Eventually complete this CompletableFuture.
* complete(result);
* }
* catch (Throwable x)
* {
* completeExceptionally(x);
* }
* }
* }
*
* // Start the task and then process the
* // result of the task when it is complete.
* task.start()
* .whenComplete((result, failure) ->
* {
* if (failure == null)
* {
* // The task completed successfully.
* }
* else
* {
* // The task failed.
* }
* });
* }</pre>
*
* @param <T> the type of the result of the task
*/
public abstract class CompletableTask<T> extends CompletableFuture<T> implements Runnable
{
/**
* <p>Starts the task by calling {@link #run()}
* and returns this {@link CompletableTask}.</p>
*
* @return this {@link CompletableTask}
*/
public CompletableTask<T> start()
{
run();
return this;
}
}

View File

@ -26,6 +26,7 @@ import org.eclipse.jetty.util.thread.Invocable;
*/ */
public class ExceptionUtil public class ExceptionUtil
{ {
/** /**
* <p>Convert a {@link Throwable} to a specific type by casting or construction on a new instance.</p> * <p>Convert a {@link Throwable} to a specific type by casting or construction on a new instance.</p>
* *
@ -178,6 +179,18 @@ public class ExceptionUtil
return true; return true;
} }
/**
* Add a suppressed exception if it is not associated.
* @see #areNotAssociated(Throwable, Throwable)
* @param throwable The main Throwable
* @param suppressed The Throwable to suppress if it is not associated.
*/
public static void addSuppressedIfNotAssociated(Throwable throwable, Throwable suppressed)
{
if (areNotAssociated(throwable, suppressed))
throwable.addSuppressed(suppressed);
}
/** /**
* Decorate a Throwable with the suppressed errors and return it. * Decorate a Throwable with the suppressed errors and return it.
* @param t the throwable * @param t the throwable

View File

@ -1836,8 +1836,7 @@ public final class URIUtil
Objects.requireNonNull(resource); Objects.requireNonNull(resource);
// Only try URI for string for known schemes, otherwise assume it is a Path // Only try URI for string for known schemes, otherwise assume it is a Path
ResourceFactory resourceFactory = ResourceFactory.getBestByScheme(resource); return (ResourceFactory.isSupported(resource))
return (resourceFactory != null)
? correctFileURI(URI.create(resource)) ? correctFileURI(URI.create(resource))
: Paths.get(resource).toUri(); : Paths.get(resource).toUri();
} }

View File

@ -27,7 +27,89 @@ import org.eclipse.jetty.util.component.Container;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
/** /**
* ResourceFactory. * <p>ResourceFactory is the source of new {@link Resource} instances.</p>
*
* <p>
* Some {@link Resource} objects have an internal allocation / release model,
* that the {@link ResourceFactory} is responsible for.
* Once a {@link ResourceFactory} is stopped, the {@link Resource}
* objects created from that {@link ResourceFactory} are released.
* </p>
*
* <h2>A {@link ResourceFactory.LifeCycle} tied to a Jetty {@link org.eclipse.jetty.util.component.Container}</h2>
* <pre>
* ResourceFactory.LifeCycle resourceFactory = ResourceFactory.of(container);
* Resource resource = resourceFactory.newResource(ref);
* </pre>
* <p>
* The use of {@link ResourceFactory#of(Container)} results in a {@link ResourceFactory.LifeCycle} that is tied
* to a specific Jetty {@link org.eclipse.jetty.util.component.Container} such as a {@code Server},
* {@code ServletContextHandler}, or {@code WebAppContext}. This will free the {@code Resource}
* instances created by the {@link org.eclipse.jetty.util.resource.ResourceFactory} once
* the {@code container} that manages it is stopped.
* </p>
*
* <h2>A {@link ResourceFactory.Closeable} that exists within a {@code try-with-resources} call</h2>
* <pre>
* try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable()) {
* Resource resource = resourceFactory.newResource(ref);
* }
* </pre>
* <p>
* The use of {@link ResourceFactory#closeable()} results in a {@link ResourceFactory} that only exists for
* the duration of the {@code try-with-resources} code block, once this {@code try-with-resources} is closed,
* all {@link Resource} objects associated with that {@link ResourceFactory} are freed as well.
* </p>
*
* <h2>A {@code ResourceFactory} that lives at the JVM level</h2>
* <pre>
* ResourceFactory resourceFactory = ResourceFactory.root();
* Resource resource = resourceFactory.newResource(ref);
* </pre>
* <p>
* The use of {@link ResourceFactory#root()} results in a {@link ResourceFactory} that exists for
* the life of the JVM, and the resources allocated via this {@link ResourceFactory} will not
* be freed until the JVM exits.
* </p>
*
* <h2>Supported URI Schemes</h2>
* <p>
* By default, the following schemes are supported by Jetty.
* </p>
* <dl>
* <dt>file</dt>
* <dd>The standard Java {@code file:/path/to/dir/} syntax</dd>
*
* <dt>jar</dt>
* <dd>The standard Java {@code jar:file:/path/to/file.jar!/} syntax</dd>
*
* <dt>jrt</dt>
* <dd>The standard Java {@code jrt:module-name} syntax</dd>
* </dl>
* <p>
* Special Note: An effort is made to discover any new schemes that
* might be present at JVM startup (eg: graalvm and {@code resource:} scheme).
* At startup Jetty will access an internal Jetty resource (found in
* the jetty-util jar) and seeing what {@code scheme} it is using to access
* it, and will register it with a call to
* {@link ResourceFactory#registerResourceFactory(String, ResourceFactory)}.
* </p>
*
* <h2>Supporting more Schemes</h2>
* <p>
* You can register a new URI scheme to a {@link ResourceFactory} implementation
* using the {@link ResourceFactory#registerResourceFactory(String, ResourceFactory)}
* method, which will cause all new uses of ResourceFactory to use this newly
* registered scheme.
* </p>
* <pre>
* URLResourceFactory urlResourceFactory = new URLResourceFactory();
* urlResourceFactory.setConnectTimeout(1000);
* ResourceFactory.registerResourceFactory("https", urlResourceFactory);
*
* URI web = URI.create("https://eclipse.dev/jetty/");
* Resource resource = ResourceFactory.root().newResource(web);
* </pre>
*/ */
public interface ResourceFactory public interface ResourceFactory
{ {
@ -39,7 +121,6 @@ public interface ResourceFactory
* or null if none are passed. * or null if none are passed.
* The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()} * The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()}
* @throws IllegalArgumentException if a non-directory resource is passed. * @throws IllegalArgumentException if a non-directory resource is passed.
* @see CombinedResource
*/ */
static Resource combine(List<Resource> resources) static Resource combine(List<Resource> resources)
{ {
@ -54,7 +135,6 @@ public interface ResourceFactory
* or null if none are passed. * or null if none are passed.
* The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()} * The returned {@link Resource} will always return {@code true} from {@link Resource#isDirectory()}
* @throws IllegalArgumentException if a non-directory resource is passed. * @throws IllegalArgumentException if a non-directory resource is passed.
* @see CombinedResource
*/ */
static Resource combine(Resource... resources) static Resource combine(Resource... resources)
{ {
@ -65,17 +145,21 @@ public interface ResourceFactory
* Construct a resource from a uri. * Construct a resource from a uri.
* *
* @param uri A URI. * @param uri A URI.
* @return A Resource object. * @return A Resource object, or null if uri points to a location that does not exist.
*/ */
Resource newResource(URI uri); Resource newResource(URI uri);
/** /**
* Construct a system resource from a string. * <p>Construct a system resource from a string.</p>
* The resource is tried as classloader resource before being *
* treated as a normal resource. * <p>
* The resource is first attempted to be accessed via the {@link Thread#getContextClassLoader()}
* before being treated as a normal resource.
* </p>
* *
* @param resource Resource as string representation * @param resource Resource as string representation
* @return The new Resource * @return The new Resource, or null if string points to a location that does not exist
* @throws IllegalArgumentException if string is blank
*/ */
default Resource newSystemResource(String resource) default Resource newSystemResource(String resource)
{ {
@ -134,12 +218,16 @@ public interface ResourceFactory
} }
/** /**
* Find a classpath resource. * <p>Find a classpath resource.</p>
*
* <p>
* The {@link Class#getResource(String)} method is used to lookup the resource. If it is not * The {@link Class#getResource(String)} method is used to lookup the resource. If it is not
* found, then the {@link Loader#getResource(String)} method is used. * found, then the {@link Loader#getResource(String)} method is used.
* </p>
* *
* @param resource the relative name of the resource * @param resource the relative name of the resource
* @return Resource or null * @return Resource, or null if string points to a location that does not exist
* @throws IllegalArgumentException if string is blank
*/ */
default Resource newClassPathResource(String resource) default Resource newClassPathResource(String resource)
{ {
@ -164,9 +252,20 @@ public interface ResourceFactory
} }
/** /**
* <p>
* Load a URL into a memory resource. * Load a URL into a memory resource.
* </p>
*
* <p>
* A Memory Resource is created from a the contents of
* {@link URL#openStream()} and kept in memory from
* that point forward. Never accessing the URL
* again to refresh it's contents.
* </p>
*
* @param url the URL to load into memory * @param url the URL to load into memory
* @return Resource or null * @return Resource, or null if url points to a location that does not exist
* @throws IllegalArgumentException if URL is null
* @see #newClassPathResource(String) * @see #newClassPathResource(String)
*/ */
default Resource newMemoryResource(URL url) default Resource newMemoryResource(URL url)
@ -181,7 +280,9 @@ public interface ResourceFactory
* Construct a resource from a string. * Construct a resource from a string.
* *
* @param resource A URL or filename. * @param resource A URL or filename.
* @return A Resource object. * @return A Resource object, or null if the string points to a location
* that does not exist
* @throws IllegalArgumentException if resource is invalid
*/ */
default Resource newResource(String resource) default Resource newResource(String resource)
{ {
@ -192,10 +293,12 @@ public interface ResourceFactory
} }
/** /**
* Construct a Resource from provided path * Construct a Resource from provided path.
* *
* @param path the path * @param path the path
* @return the Resource for the provided path * @return the Resource for the provided path, or null if the path
* does not exist
* @throws IllegalArgumentException if path is null
*/ */
default Resource newResource(Path path) default Resource newResource(Path path)
{ {
@ -206,10 +309,12 @@ public interface ResourceFactory
} }
/** /**
* Construct a possible {@link CombinedResource} from a list of URIs * Construct a possible combined {@code Resource} from a list of URIs.
* *
* @param uris the URIs * @param uris the URIs
* @return the Resource for the provided path * @return the Resource for the provided URIs, or null if all
* of the provided URIs do not exist
* @throws IllegalArgumentException if list of URIs is empty or null
*/ */
default Resource newResource(List<URI> uris) default Resource newResource(List<URI> uris)
{ {
@ -220,10 +325,12 @@ public interface ResourceFactory
} }
/** /**
* Construct a {@link Resource} from a provided URL * Construct a {@link Resource} from a provided URL.
* *
* @param url the URL * @param url the URL
* @return the Resource for the provided URL * @return the Resource for the provided URL, or null if the
* url points to a location that does not exist
* @throws IllegalArgumentException if url is null
*/ */
default Resource newResource(URL url) default Resource newResource(URL url)
{ {
@ -241,10 +348,12 @@ public interface ResourceFactory
} }
/** /**
* Construct a {@link Resource} from a {@code file:} based URI that is mountable (eg: a jar file) * Construct a {@link Resource} from a {@code file:} based URI that is mountable (eg: a jar file).
* *
* @param uri the URI * @param uri the URI
* @return the Resource, mounted as a {@link java.nio.file.FileSystem} * @return the Resource, mounted as a {@link java.nio.file.FileSystem}, or null if
* the uri points to a location that does not exist.
* @throws IllegalArgumentException if provided URI is not "file" scheme.
*/ */
default Resource newJarFileResource(URI uri) default Resource newJarFileResource(URI uri)
{ {
@ -253,6 +362,28 @@ public interface ResourceFactory
return newResource(URIUtil.toJarFileUri(uri)); return newResource(URIUtil.toJarFileUri(uri));
} }
/**
* Test to see if provided string is supported.
*
* @param str the string to test
* @return true if it is supported
*/
static boolean isSupported(String str)
{
return ResourceFactoryInternals.isSupported(str);
}
/**
* Test to see if provided uri is supported.
*
* @param uri the uri to test
* @return true if it is supported
*/
static boolean isSupported(URI uri)
{
return ResourceFactoryInternals.isSupported(uri);
}
/** /**
* Register a new ResourceFactory that can handle the specific scheme for the Resource API. * Register a new ResourceFactory that can handle the specific scheme for the Resource API.
* *
@ -262,12 +393,14 @@ public interface ResourceFactory
* *
* @param scheme the scheme to support (eg: `ftp`, `http`, `resource`, etc) * @param scheme the scheme to support (eg: `ftp`, `http`, `resource`, etc)
* @param resourceFactory the ResourceFactory to be responsible for the registered scheme. * @param resourceFactory the ResourceFactory to be responsible for the registered scheme.
* @throws IllegalArgumentException if scheme is blank
* @see #unregisterResourceFactory(String) * @see #unregisterResourceFactory(String)
* @see #byScheme(String)
* @see #getBestByScheme(String)
*/ */
static void registerResourceFactory(String scheme, ResourceFactory resourceFactory) static void registerResourceFactory(String scheme, ResourceFactory resourceFactory)
{ {
if (StringUtil.isBlank(scheme))
throw new IllegalArgumentException("Scheme is blank");
ResourceFactoryInternals.RESOURCE_FACTORIES.put(scheme, resourceFactory); ResourceFactoryInternals.RESOURCE_FACTORIES.put(scheme, resourceFactory);
} }
@ -276,53 +409,16 @@ public interface ResourceFactory
* *
* @param scheme the scheme to unregister * @param scheme the scheme to unregister
* @return the existing {@link ResourceFactory} that was registered to that scheme. * @return the existing {@link ResourceFactory} that was registered to that scheme.
* @throws IllegalArgumentException if scheme is blank
* @see #registerResourceFactory(String, ResourceFactory) * @see #registerResourceFactory(String, ResourceFactory)
* @see #byScheme(String)
* @see #getBestByScheme(String)
*/ */
static ResourceFactory unregisterResourceFactory(String scheme) static ResourceFactory unregisterResourceFactory(String scheme)
{ {
if (StringUtil.isBlank(scheme))
throw new IllegalArgumentException("Scheme is blank");
return ResourceFactoryInternals.RESOURCE_FACTORIES.remove(scheme); return ResourceFactoryInternals.RESOURCE_FACTORIES.remove(scheme);
} }
/**
* Get the {@link ResourceFactory} that is registered for the specific scheme.
*
* <pre>{@code
* .byScheme("jar") == ResourceFactory supporting jar
* .byScheme("jar:file://foo.jar!/") == null // full url strings not supported)
* }</pre>
*
* @param scheme the scheme to look up
* @return the {@link ResourceFactory} responsible for the scheme, null if no {@link ResourceFactory} handles the scheme.
* @see #registerResourceFactory(String, ResourceFactory)
* @see #unregisterResourceFactory(String)
* @see #getBestByScheme(String)
*/
static ResourceFactory byScheme(String scheme)
{
return ResourceFactoryInternals.RESOURCE_FACTORIES.get(scheme);
}
/**
* Get the best ResourceFactory for the provided scheme.
*
* <p>
* Unlike {@link #byScheme(String)}, this supports arbitrary Strings, that might start with a supported scheme.
* </p>
*
* @param scheme the scheme to look up
* @return the ResourceFactory that best fits the provided scheme.
* @see org.eclipse.jetty.util.Index#getBest(String)
* @see #registerResourceFactory(String, ResourceFactory)
* @see #unregisterResourceFactory(String)
* @see #byScheme(String)
*/
static ResourceFactory getBestByScheme(String scheme)
{
return ResourceFactoryInternals.RESOURCE_FACTORIES.getBest(scheme);
}
/** /**
* The JVM wide (root) ResourceFactory. * The JVM wide (root) ResourceFactory.
* *

View File

@ -24,6 +24,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable; import org.eclipse.jetty.util.component.Dumpable;
@ -93,6 +94,39 @@ class ResourceFactoryInternals
} }
}; };
/**
* Test uri to know if a {@link ResourceFactory} is registered for it.
*
* @param uri the uri to test
* @return true if a ResourceFactory is registered to support the uri
* @see ResourceFactory#registerResourceFactory(String, ResourceFactory)
* @see ResourceFactory#unregisterResourceFactory(String)
* @see #isSupported(String)
*/
static boolean isSupported(URI uri) // TODO: boolean isSupported
{
if (uri == null || uri.getScheme() == null)
return false;
return RESOURCE_FACTORIES.get(uri.getScheme()) != null;
}
/**
* Test string to know if a {@link ResourceFactory} is registered for it.
*
* @param str the string representing the resource location
* @return true if a ResourceFactory is registered to support the string representation
* @see org.eclipse.jetty.util.Index#getBest(String)
* @see ResourceFactory#registerResourceFactory(String, ResourceFactory)
* @see ResourceFactory#unregisterResourceFactory(String)
* @see #isSupported(URI)
*/
static boolean isSupported(String str)
{
if (StringUtil.isBlank(str))
return false;
return RESOURCE_FACTORIES.getBest(str) != null;
}
static class Closeable implements ResourceFactory.Closeable static class Closeable implements ResourceFactory.Closeable
{ {
private final CompositeResourceFactory _compositeResourceFactory = new CompositeResourceFactory(); private final CompositeResourceFactory _compositeResourceFactory = new CompositeResourceFactory();
@ -168,7 +202,7 @@ class ResourceFactoryInternals
uri = URIUtil.correctFileURI(uri); uri = URIUtil.correctFileURI(uri);
} }
ResourceFactory resourceFactory = ResourceFactory.byScheme(uri.getScheme()); ResourceFactory resourceFactory = RESOURCE_FACTORIES.get(uri.getScheme());
if (resourceFactory == null) if (resourceFactory == null)
throw new IllegalArgumentException("URI scheme not supported: " + uri); throw new IllegalArgumentException("URI scheme not supported: " + uri);
if (resourceFactory instanceof MountedPathResourceFactory) if (resourceFactory instanceof MountedPathResourceFactory)

View File

@ -12,7 +12,17 @@
// //
/** /**
* Jetty Util : Common Resource Utilities * <p>Jetty Util : Resource Utilities</p>
*
* <p>
* A {@link org.eclipse.jetty.util.resource.Resource} in Jetty is an abstraction that
* allows for a common API to access various forms of resource sources across the Jetty
* ecosystem.
* </p>
* <p>
* A {@link org.eclipse.jetty.util.resource.Resource} is created via one of the
* {@link org.eclipse.jetty.util.resource.ResourceFactory}{@code .newResource(...)} APIs.
* </p>
*/ */
package org.eclipse.jetty.util.resource; package org.eclipse.jetty.util.resource;

View File

@ -149,11 +149,6 @@ public class AsyncContextState implements AsyncContext
_state = null; _state = null;
} }
public ServletChannelState getServletChannelState()
{
return state();
}
public static class WrappedAsyncListener implements AsyncListener public static class WrappedAsyncListener implements AsyncListener
{ {
private final AsyncListener _listener; private final AsyncListener _listener;

View File

@ -15,26 +15,17 @@ package org.eclipse.jetty.ee10.servlet;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType; import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher; import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
@ -43,16 +34,12 @@ import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import jakarta.servlet.http.MappingMatch; import jakarta.servlet.http.MappingMatch;
import org.eclipse.jetty.http.CompressedContentFormat; import org.eclipse.jetty.http.CompressedContentFormat;
import org.eclipse.jetty.http.HttpException; import org.eclipse.jetty.http.HttpException;
import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.content.FileMappingHttpContentFactory; import org.eclipse.jetty.http.content.FileMappingHttpContentFactory;
import org.eclipse.jetty.http.content.HttpContent; import org.eclipse.jetty.http.content.HttpContent;
@ -60,7 +47,6 @@ import org.eclipse.jetty.http.content.PreCompressedHttpContentFactory;
import org.eclipse.jetty.http.content.ResourceHttpContentFactory; import org.eclipse.jetty.http.content.ResourceHttpContentFactory;
import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory; import org.eclipse.jetty.http.content.ValidatingCachingHttpContentFactory;
import org.eclipse.jetty.http.content.VirtualHttpContentFactory; import org.eclipse.jetty.http.content.VirtualHttpContentFactory;
import org.eclipse.jetty.io.ByteBufferInputStream;
import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.Context; import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
@ -69,9 +55,8 @@ import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory; import org.eclipse.jetty.util.resource.ResourceFactory;
@ -79,6 +64,8 @@ import org.eclipse.jetty.util.resource.Resources;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.util.URIUtil.encodePath;
/** /**
* <p>The default Servlet, normally mapped to {@code /}, that handles static resources.</p> * <p>The default Servlet, normally mapped to {@code /}, that handles static resources.</p>
* <p>The following init parameters are supported:</p> * <p>The following init parameters are supported:</p>
@ -185,7 +172,6 @@ public class DefaultServlet extends HttpServlet
private ServletContextHandler _contextHandler; private ServletContextHandler _contextHandler;
private ServletResourceService _resourceService; private ServletResourceService _resourceService;
private WelcomeServletMode _welcomeServletMode; private WelcomeServletMode _welcomeServletMode;
private Resource _baseResource;
public ResourceService getResourceService() public ResourceService getResourceService()
{ {
@ -198,14 +184,14 @@ public class DefaultServlet extends HttpServlet
_contextHandler = initContextHandler(getServletContext()); _contextHandler = initContextHandler(getServletContext());
_resourceService = new ServletResourceService(_contextHandler); _resourceService = new ServletResourceService(_contextHandler);
_resourceService.setWelcomeFactory(_resourceService); _resourceService.setWelcomeFactory(_resourceService);
_baseResource = _contextHandler.getBaseResource(); Resource baseResource = _contextHandler.getBaseResource();
String rb = getInitParameter("baseResource", "resourceBase"); String rb = getInitParameter("baseResource", "resourceBase");
if (rb != null) if (rb != null)
{ {
try try
{ {
_baseResource = Objects.requireNonNull(_contextHandler.newResource(rb)); baseResource = Objects.requireNonNull(_contextHandler.newResource(rb));
} }
catch (Exception e) catch (Exception e)
{ {
@ -222,7 +208,7 @@ public class DefaultServlet extends HttpServlet
if (contentFactory == null) if (contentFactory == null)
{ {
MimeTypes mimeTypes = _contextHandler.getMimeTypes(); MimeTypes mimeTypes = _contextHandler.getMimeTypes();
ResourceFactory resourceFactory = _baseResource != null ? ResourceFactory.of(_baseResource) : this::getResource; ResourceFactory resourceFactory = baseResource != null ? ResourceFactory.of(baseResource) : this::getResource;
contentFactory = new ResourceHttpContentFactory(resourceFactory, mimeTypes); contentFactory = new ResourceHttpContentFactory(resourceFactory, mimeTypes);
// Use the servers default stylesheet unless there is one explicitly set by an init param. // Use the servers default stylesheet unless there is one explicitly set by an init param.
@ -326,7 +312,7 @@ public class DefaultServlet extends HttpServlet
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
{ {
LOG.debug(" .baseResource = {}", _baseResource); LOG.debug(" .baseResource = {}", baseResource);
LOG.debug(" .resourceService = {}", _resourceService); LOG.debug(" .resourceService = {}", _resourceService);
LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode); LOG.debug(" .welcomeServletMode = {}", _welcomeServletMode);
} }
@ -458,18 +444,18 @@ public class DefaultServlet extends HttpServlet
} }
@Override @Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException
{ {
String includedServletPath = (String)req.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); String includedServletPath = (String)httpServletRequest.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
String encodedPathInContext = getEncodedPathInContext(req, includedServletPath); String encodedPathInContext = getEncodedPathInContext(httpServletRequest, includedServletPath);
boolean included = includedServletPath != null; boolean included = includedServletPath != null;
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("doGet(req={}, resp={}) pathInContext={}, included={}", req, resp, encodedPathInContext, included); LOG.debug("doGet(hsReq={}, hsResp={}) pathInContext={}, included={}", httpServletRequest, httpServletResponse, encodedPathInContext, included);
try try
{ {
HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(req)); HttpContent content = _resourceService.getContent(encodedPathInContext, ServletContextRequest.getServletContextRequest(httpServletRequest));
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("content = {}", content); LOG.debug("content = {}", content);
@ -487,13 +473,31 @@ public class DefaultServlet extends HttpServlet
} }
// no content // no content
resp.sendError(404); httpServletResponse.sendError(404);
} }
else else
{ {
ServletCoreRequest coreRequest = new ServletCoreRequest(req); // lookup the core request and response as wrapped by the ServletContextHandler
ServletCoreResponse coreResponse = new ServletCoreResponse(coreRequest, resp, included); ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(httpServletRequest);
ServletContextResponse servletContextResponse = servletContextRequest.getServletContextResponse();
ServletChannel servletChannel = servletContextRequest.getServletChannel();
// If the servlet request has not been wrapped,
// we can use the core request directly,
// otherwise wrap the servlet request as a core request
Request coreRequest = httpServletRequest instanceof ServletApiRequest
? servletChannel.getRequest()
: new ServletCoreRequest(httpServletRequest);
// If the servlet response has been wrapped and has been written to,
// then the servlet response must be wrapped as a core response
// otherwise we can use the core response directly.
boolean useServletResponse = !(httpServletResponse instanceof ServletApiResponse) || servletContextResponse.isWritingOrStreaming();
Response coreResponse = useServletResponse
? new ServletCoreResponse(coreRequest, httpServletResponse, included)
: servletChannel.getResponse();
// If the core response is already committed then do nothing more
if (coreResponse.isCommitted()) if (coreResponse.isCommitted())
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
@ -501,22 +505,30 @@ public class DefaultServlet extends HttpServlet
return; return;
} }
// Servlet Filters could be interacting with the Response already. // Get the content length before we may wrap the content
if (coreResponse.isHttpServletResponseWrapped() || long contentLength = content.getContentLengthValue();
coreResponse.isWritingOrStreaming())
{
content = new UnknownLengthHttpContent(content);
}
ServletContextResponse contextResponse = coreResponse.getServletContextResponse(); // Servlet Filters could be interacting with the Response already.
if (contextResponse != null) if (useServletResponse)
{ content = new UnknownLengthHttpContent(content);
String characterEncoding = contextResponse.getRawCharacterEncoding();
// The character encoding may be forced
String characterEncoding = servletContextResponse.getRawCharacterEncoding();
if (characterEncoding != null) if (characterEncoding != null)
content = new ForcedCharacterEncodingHttpContent(content, characterEncoding); content = new ForcedCharacterEncodingHttpContent(content, characterEncoding);
}
// serve content // If async is supported and the unwrapped content is larger than an output buffer
if (httpServletRequest.isAsyncSupported() &&
(contentLength < 0 || contentLength > coreRequest.getConnectionMetaData().getHttpConfiguration().getOutputBufferSize()))
{
// send the content asynchronously
AsyncContext asyncContext = httpServletRequest.startAsync();
Callback callback = new AsyncContextCallback(asyncContext, httpServletResponse);
_resourceService.doGet(coreRequest, coreResponse, callback, content);
}
else
{
// send the content blocking
try (Blocker.Callback callback = Blocker.callback()) try (Blocker.Callback callback = Blocker.callback())
{ {
_resourceService.doGet(coreRequest, coreResponse, callback, content); _resourceService.doGet(coreRequest, coreResponse, callback, content);
@ -528,22 +540,23 @@ public class DefaultServlet extends HttpServlet
} }
} }
} }
}
catch (InvalidPathException e) catch (InvalidPathException e)
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e); LOG.debug("InvalidPathException for pathInContext: {}", encodedPathInContext, e);
if (included) if (included)
throw new FileNotFoundException(encodedPathInContext); throw new FileNotFoundException(encodedPathInContext);
resp.setStatus(404); httpServletResponse.setStatus(404);
} }
} }
protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath) protected String getEncodedPathInContext(HttpServletRequest req, String includedServletPath)
{ {
if (includedServletPath != null) if (includedServletPath != null)
return URIUtil.encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req))); return encodePath(getIncludedPathInContext(req, includedServletPath, !isDefaultMapping(req)));
else if (!isDefaultMapping(req)) else if (!isDefaultMapping(req))
return URIUtil.encodePath(req.getPathInfo()); return encodePath(req.getPathInfo());
else if (req instanceof ServletApiRequest apiRequest) else if (req instanceof ServletApiRequest apiRequest)
return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath()); return Context.getPathInContext(req.getContextPath(), apiRequest.getRequest().getHttpURI().getCanonicalPath());
else else
@ -589,476 +602,6 @@ public class DefaultServlet extends HttpServlet
return result; return result;
} }
private static class ServletCoreRequest extends Request.Wrapper
{
// TODO fully implement this class and move it to the top level
// TODO Some methods are directed to core that probably should be intercepted
private final HttpServletRequest _servletRequest;
private final HttpFields _httpFields;
private final HttpURI _uri;
ServletCoreRequest(HttpServletRequest request)
{
super(ServletContextRequest.getServletContextRequest(request));
_servletRequest = request;
HttpFields.Mutable fields = HttpFields.build();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String headerName = headerNames.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements())
{
String headerValue = headerValues.nextElement();
fields.add(new HttpField(headerName, headerValue));
}
}
_httpFields = fields.asImmutable();
String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
boolean included = includedServletPath != null;
if (request.getDispatcherType() == DispatcherType.REQUEST)
_uri = getWrapped().getHttpURI();
else if (included)
_uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(getIncludedPathInContext(request, includedServletPath, false)));
else
_uri = Request.newHttpURIFrom(getWrapped(), URIUtil.encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo())));
}
@Override
public HttpFields getHeaders()
{
return _httpFields;
}
@Override
public HttpURI getHttpURI()
{
return _uri;
}
@Override
public String getId()
{
return _servletRequest.getRequestId();
}
@Override
public String getMethod()
{
return _servletRequest.getMethod();
}
@Override
public boolean isSecure()
{
return _servletRequest.isSecure();
}
@Override
public Object removeAttribute(String name)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.removeAttribute(name);
return value;
}
@Override
public Object setAttribute(String name, Object attribute)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.setAttribute(name, attribute);
return value;
}
@Override
public Object getAttribute(String name)
{
return _servletRequest.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
Set<String> set = new HashSet<>();
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
set.add(e.nextElement());
return set;
}
@Override
public void clearAttributes()
{
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
_servletRequest.removeAttribute(e.nextElement());
}
}
private static class HttpServletResponseHttpFields implements HttpFields.Mutable
{
private final HttpServletResponse _response;
private HttpServletResponseHttpFields(HttpServletResponse response)
{
_response = response;
}
@Override
public ListIterator<HttpField> listIterator()
{
// The minimum requirement is to implement the listIterator, but it is inefficient.
// Other methods are implemented for efficiency.
final ListIterator<HttpField> list = _response.getHeaderNames().stream()
.map(n -> new HttpField(n, _response.getHeader(n)))
.collect(Collectors.toList())
.listIterator();
return new ListIterator<>()
{
HttpField _last;
@Override
public boolean hasNext()
{
return list.hasNext();
}
@Override
public HttpField next()
{
return _last = list.next();
}
@Override
public boolean hasPrevious()
{
return list.hasPrevious();
}
@Override
public HttpField previous()
{
return _last = list.previous();
}
@Override
public int nextIndex()
{
return list.nextIndex();
}
@Override
public int previousIndex()
{
return list.previousIndex();
}
@Override
public void remove()
{
if (_last != null)
{
// This is not exactly the right semantic for repeated field names
list.remove();
_response.setHeader(_last.getName(), null);
}
}
@Override
public void set(HttpField httpField)
{
list.set(httpField);
_response.setHeader(httpField.getName(), httpField.getValue());
}
@Override
public void add(HttpField httpField)
{
list.add(httpField);
_response.addHeader(httpField.getName(), httpField.getValue());
}
};
}
@Override
public Mutable add(String name, String value)
{
_response.addHeader(name, value);
return this;
}
@Override
public Mutable add(HttpHeader header, HttpHeaderValue value)
{
_response.addHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable add(HttpHeader header, String value)
{
_response.addHeader(header.asString(), value);
return this;
}
@Override
public Mutable add(HttpField field)
{
_response.addHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(HttpField field)
{
_response.setHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(String name, String value)
{
_response.setHeader(name, value);
return this;
}
@Override
public Mutable put(HttpHeader header, HttpHeaderValue value)
{
_response.setHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable put(HttpHeader header, String value)
{
_response.setHeader(header.asString(), value);
return this;
}
@Override
public Mutable put(String name, List<String> list)
{
Objects.requireNonNull(name);
Objects.requireNonNull(list);
boolean first = true;
for (String s : list)
{
if (first)
_response.setHeader(name, s);
else
_response.addHeader(name, s);
first = false;
}
return this;
}
@Override
public Mutable remove(HttpHeader header)
{
_response.setHeader(header.asString(), null);
return this;
}
@Override
public Mutable remove(EnumSet<HttpHeader> fields)
{
for (HttpHeader header : fields)
remove(header);
return this;
}
@Override
public Mutable remove(String name)
{
_response.setHeader(name, null);
return this;
}
}
private static class ServletCoreResponse implements Response
{
// TODO fully implement this class and move it to the top level
private final HttpServletResponse _response;
private final ServletCoreRequest _coreRequest;
private final Response _coreResponse;
private final HttpFields.Mutable _httpFields;
private final boolean _included;
public ServletCoreResponse(ServletCoreRequest coreRequest, HttpServletResponse response, boolean included)
{
_coreRequest = coreRequest;
_response = response;
_coreResponse = ServletContextResponse.getServletContextResponse(response);
HttpFields.Mutable fields = new HttpServletResponseHttpFields(response);
if (included)
{
// If included, accept but ignore mutations.
fields = new HttpFields.Mutable.Wrapper(fields)
{
@Override
public HttpField onAddField(HttpField field)
{
return null;
}
@Override
public boolean onRemoveField(HttpField field)
{
return false;
}
};
}
_httpFields = fields;
_included = included;
}
@Override
public HttpFields.Mutable getHeaders()
{
return _httpFields;
}
public ServletContextResponse getServletContextResponse()
{
return ServletContextResponse.getServletContextResponse(_response);
}
@Override
public boolean isCommitted()
{
return _response.isCommitted();
}
/**
* Test if the HttpServletResponse is wrapped by the webapp.
*
* @return true if wrapped.
*/
public boolean isHttpServletResponseWrapped()
{
return (_response instanceof HttpServletResponseWrapper);
}
/**
* Test if {@link HttpServletResponse#getOutputStream()} or
* {@link HttpServletResponse#getWriter()} has been called already
*
* @return true if {@link HttpServletResponse} has started to write or stream content
*/
public boolean isWritingOrStreaming()
{
ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class);
return servletContextResponse.isWritingOrStreaming();
}
public boolean isWriting()
{
ServletContextResponse servletContextResponse = Response.as(_coreResponse, ServletContextResponse.class);
return servletContextResponse.isWriting();
}
@Override
public void write(boolean last, ByteBuffer byteBuffer, Callback callback)
{
if (_included)
last = false;
try
{
if (BufferUtil.hasContent(byteBuffer))
{
if (isWriting())
{
String characterEncoding = _response.getCharacterEncoding();
try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer);
InputStreamReader reader = new InputStreamReader(bbis, characterEncoding))
{
IO.copy(reader, _response.getWriter());
}
if (last)
_response.getWriter().close();
}
else
{
BufferUtil.writeTo(byteBuffer, _response.getOutputStream());
if (last)
_response.getOutputStream().close();
}
}
callback.succeeded();
}
catch (Throwable t)
{
callback.failed(t);
}
}
@Override
public Request getRequest()
{
return _coreRequest;
}
@Override
public int getStatus()
{
return _response.getStatus();
}
@Override
public void setStatus(int code)
{
if (LOG.isDebugEnabled())
LOG.debug("{}.setStatus({})", this.getClass().getSimpleName(), code);
if (_included)
return;
_response.setStatus(code);
}
@Override
public Supplier<HttpFields> getTrailersSupplier()
{
return null;
}
@Override
public void setTrailersSupplier(Supplier<HttpFields> trailers)
{
}
@Override
public boolean isCompletedSuccessfully()
{
return _coreResponse.isCompletedSuccessfully();
}
@Override
public void reset()
{
_response.reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
return null;
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response);
}
}
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory
{ {
private final ServletContextHandler _servletContextHandler; private final ServletContextHandler _servletContextHandler;
@ -1214,18 +757,32 @@ public class DefaultServlet extends HttpServlet
private HttpServletRequest getServletRequest(Request request) private HttpServletRequest getServletRequest(Request request)
{ {
// TODO, this unwrapping is fragile ServletCoreRequest servletCoreRequest = Request.as(request, ServletCoreRequest.class);
return ((ServletCoreRequest)request)._servletRequest; if (servletCoreRequest != null)
return servletCoreRequest.getServletRequest();
ServletContextRequest servletContextRequest = Request.as(request, ServletContextRequest.class);
if (servletContextRequest != null)
return servletContextRequest.getServletApiRequest();
throw new IllegalStateException("instanceof " + request.getClass());
} }
private HttpServletResponse getServletResponse(Response response) private HttpServletResponse getServletResponse(Response response)
{ {
// TODO, this unwrapping is fragile ServletCoreResponse servletCoreResponse = Response.as(response, ServletCoreResponse.class);
return ((ServletCoreResponse)response)._response; if (servletCoreResponse != null)
return servletCoreResponse.getServletResponse();
ServletContextResponse servletContextResponse = Response.as(response, ServletContextResponse.class);
if (servletContextResponse != null)
return servletContextResponse.getServletApiResponse();
throw new IllegalStateException("instanceof " + response.getClass());
} }
} }
private static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly) static String getIncludedPathInContext(HttpServletRequest request, String includedServletPath, boolean isPathInfoOnly)
{ {
String servletPath = isPathInfoOnly ? "/" : includedServletPath; String servletPath = isPathInfoOnly ? "/" : includedServletPath;
String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); String pathInfo = (String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO);
@ -1330,4 +887,45 @@ public class DefaultServlet extends HttpServlet
*/ */
EXACT EXACT
} }
private static class AsyncContextCallback implements Callback
{
private final AsyncContext _asyncContext;
private final HttpServletResponse _response;
private AsyncContextCallback(AsyncContext asyncContext, HttpServletResponse response)
{
_asyncContext = asyncContext;
_response = response;
}
@Override
public void succeeded()
{
_asyncContext.complete();
}
@Override
public void failed(Throwable x)
{
try
{
if (LOG.isDebugEnabled())
LOG.debug("AsyncContextCallback failed {}", _asyncContext, x);
// It is known that this callback is only failed if the response is already committed,
// thus we can only abort the response here.
_response.sendError(-1);
}
catch (IOException e)
{
ExceptionUtil.addSuppressedIfNotAssociated(x, e);
}
finally
{
_asyncContext.complete();
}
if (LOG.isDebugEnabled())
LOG.debug("Async get failed", x);
}
}
} }

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee10.servlet; package org.eclipse.jetty.ee10.servlet;
import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
@ -41,6 +42,7 @@ import org.eclipse.jetty.ee10.servlet.util.ServletOutputStreamWrapper;
import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.pathmap.MatchedResource; import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.io.WriterOutputStream; import org.eclipse.jetty.io.WriterOutputStream;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiMap; import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded; import org.eclipse.jetty.util.UrlEncoded;
@ -122,16 +124,18 @@ public class Dispatcher implements RequestDispatcher
_mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse); _mappedServlet.handle(_servletHandler, _decodedPathInContext, new ForwardRequest(httpRequest), httpResponse);
// If we are not async and not closed already, then close via the possibly wrapped response. // If we are not async and not closed already, then close via the possibly wrapped response.
if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getHttpOutput().isClosed()) if (!servletContextRequest.getState().isAsync() && !servletContextRequest.getServletContextResponse().hasLastWrite())
{ {
Closeable closeable;
try try
{ {
response.getOutputStream().close(); closeable = response.getOutputStream();
} }
catch (IllegalStateException e) catch (IllegalStateException e)
{ {
response.getWriter().close(); closeable = response.getWriter();
} }
IO.close(closeable);
} }
} }
@ -624,6 +628,24 @@ public class Dispatcher implements RequestDispatcher
{ {
// NOOP for include. // NOOP for include.
} }
@Override
public void sendError(int sc, String msg) throws IOException
{
// NOOP for include.
}
@Override
public void sendError(int sc) throws IOException
{
// NOOP for include.
}
@Override
public void sendRedirect(String location) throws IOException
{
// NOOP for include.
}
} }
private class AsyncRequest extends ParameterRequestWrapper private class AsyncRequest extends ParameterRequestWrapper

View File

@ -36,6 +36,7 @@ import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException; import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.RetainableByteBuffer; import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Blocker; import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.Callback;
@ -155,11 +156,19 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
} }
/**
* @return True if any content has been written via the {@link jakarta.servlet.http.HttpServletResponse} API.
*/
public boolean isWritten() public boolean isWritten()
{ {
return _written > 0; return _written > 0;
} }
/**
* @return The bytes written via the {@link jakarta.servlet.http.HttpServletResponse} API. This
* may differ from the bytes reported by {@link org.eclipse.jetty.server.Response#getContentBytesWritten(Response)}
* due to buffering, compression, other interception or writes that bypass the servlet API.
*/
public long getWritten() public long getWritten()
{ {
return _written; return _written;
@ -445,6 +454,9 @@ public class HttpOutput extends ServletOutputStream implements Runnable
Blocker.Callback blocker = null; Blocker.Callback blocker = null;
try (AutoLock l = _channelState.lock()) try (AutoLock l = _channelState.lock())
{ {
if (_softClose)
return;
if (_onError != null) if (_onError != null)
{ {
if (_onError instanceof IOException) if (_onError instanceof IOException)
@ -1456,8 +1468,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(e, t)) ExceptionUtil.addSuppressedIfNotAssociated(e, t);
e.addSuppressed(t);
} }
finally finally
{ {

View File

@ -28,7 +28,7 @@ public class NoJspServlet extends HttpServlet
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
{ {
if (!_warned) if (!_warned)
getServletContext().log("No JSP support. Check that JSP jars are in lib/jsp and that the JSP option has been specified to start.jar"); getServletContext().log("No JSP support. Check that the ee10-jsp module is enabled, or otherwise ensure the jsp jars are on the server classpath.");
_warned = true; _warned = true;
response.sendError(500, "JSP support not configured"); response.sendError(500, "JSP support not configured");

View File

@ -100,7 +100,6 @@ public class ServletApiRequest implements HttpServletRequest
private static final Logger LOG = LoggerFactory.getLogger(ServletApiRequest.class); private static final Logger LOG = LoggerFactory.getLogger(ServletApiRequest.class);
private final ServletContextRequest _servletContextRequest; private final ServletContextRequest _servletContextRequest;
private final ServletChannel _servletChannel; private final ServletChannel _servletChannel;
//TODO review which fields should be in ServletContextRequest
private AsyncContextState _async; private AsyncContextState _async;
private String _characterEncoding; private String _characterEncoding;
private int _inputState = ServletContextRequest.INPUT_NONE; private int _inputState = ServletContextRequest.INPUT_NONE;
@ -413,7 +412,7 @@ public class ServletApiRequest implements HttpServletRequest
public boolean isRequestedSessionIdValid() public boolean isRequestedSessionIdValid()
{ {
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession(); AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie(); return requestedSession != null && requestedSession.sessionId() != null && requestedSession.session() != null;
} }
@Override @Override

View File

@ -88,7 +88,6 @@ public class ServletChannel
private Response _response; private Response _response;
private Callback _callback; private Callback _callback;
private boolean _expects100Continue; private boolean _expects100Continue;
private long _written;
public ServletChannel(ServletContextHandler servletContextHandler, Request request) public ServletChannel(ServletContextHandler servletContextHandler, Request request)
{ {
@ -218,7 +217,7 @@ public class ServletChannel
public long getBytesWritten() public long getBytesWritten()
{ {
return _written; return Response.getContentBytesWritten(getServletContextResponse());
} }
/** /**
@ -449,15 +448,13 @@ public class ServletChannel
_request = _servletContextRequest = null; _request = _servletContextRequest = null;
_response = null; _response = null;
_callback = null; _callback = null;
_written = 0;
_expects100Continue = false; _expects100Continue = false;
} }
/** /**
* Handle the servlet request. This is called on the initial dispatch and then again on any asynchronous events. * Handle the servlet request. This is called on the initial dispatch and then again on any asynchronous events.
* @return True if the channel is ready to continue handling (ie it is not suspended)
*/ */
public boolean handle() public void handle()
{ {
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("handle {} {} ", _servletContextRequest.getHttpURI(), this); LOG.debug("handle {} {} ", _servletContextRequest.getHttpURI(), this);
@ -553,15 +550,15 @@ public class ServletChannel
// If the callback has already been completed we should continue in handle loop. // If the callback has already been completed we should continue in handle loop.
// Otherwise, the callback will schedule a dispatch to handle(). // Otherwise, the callback will schedule a dispatch to handle().
if (asyncCompletion.compareAndSet(false, true)) if (asyncCompletion.compareAndSet(false, true))
return false; return;
} }
} }
catch (Throwable x) catch (Throwable x)
{ {
if (cause == null) if (cause == null)
cause = x; cause = x;
else if (ExceptionUtil.areNotAssociated(cause, x)) else
cause.addSuppressed(x); ExceptionUtil.addSuppressedIfNotAssociated(cause, x);
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("Could not perform ERROR dispatch, aborting", cause); LOG.debug("Could not perform ERROR dispatch, aborting", cause);
if (_state.isResponseCommitted()) if (_state.isResponseCommitted())
@ -577,8 +574,7 @@ public class ServletChannel
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(cause, t)) ExceptionUtil.addSuppressedIfNotAssociated(cause, t);
cause.addSuppressed(t);
abort(cause); abort(cause);
} }
} }
@ -617,12 +613,11 @@ public class ServletChannel
ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse()); ResponseUtils.ensureConsumeAvailableOrNotPersistent(_servletContextRequest, _servletContextRequest.getServletContextResponse());
} }
// RFC 7230, section 3.3. // RFC 7230, section 3.3. We do this here so that a servlet error page can be sent.
if (!_servletContextRequest.isHead() && if (!_servletContextRequest.isHead() && getServletContextResponse().getStatus() != HttpStatus.NOT_MODIFIED_304)
getServletContextResponse().getStatus() != HttpStatus.NOT_MODIFIED_304 &&
getServletContextResponse().isContentIncomplete(_servletContextRequest.getHttpOutput().getWritten()))
{ {
if (sendErrorOrAbort("Insufficient content written")) long written = getBytesWritten();
if (getServletContextResponse().isContentIncomplete(written) && sendErrorOrAbort("Insufficient content written %d < %d".formatted(written, getServletContextResponse().getContentLength())))
break; break;
} }
@ -649,9 +644,6 @@ public class ServletChannel
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("!handle {} {}", action, this); LOG.debug("!handle {} {}", action, this);
boolean suspended = action == Action.WAIT;
return !suspended;
} }
private void reopen() private void reopen()
@ -664,7 +656,7 @@ public class ServletChannel
* @param message the error message. * @param message the error message.
* @return true if we have sent an error, false if we have aborted. * @return true if we have sent an error, false if we have aborted.
*/ */
public boolean sendErrorOrAbort(String message) private boolean sendErrorOrAbort(String message)
{ {
try try
{ {

View File

@ -41,7 +41,7 @@ import static jakarta.servlet.RequestDispatcher.ERROR_SERVLET_NAME;
import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE; import static jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE;
/** /**
* Implementation of AsyncContext interface that holds the state of request-response cycle. * holder of the state of request-response cycle.
*/ */
public class ServletChannelState public class ServletChannelState
{ {

View File

@ -0,0 +1,261 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Components;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Context;
import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.TunnelSupport;
import org.eclipse.jetty.util.URIUtil;
import static org.eclipse.jetty.util.URIUtil.addEncodedPaths;
import static org.eclipse.jetty.util.URIUtil.encodePath;
/**
* Wrap a {@link jakarta.servlet.ServletRequest} as a core {@link Request}.
* <p>
* Whilst similar to a {@link Request.Wrapper}, this class is not a {@code Wrapper}
* as callers should not be able to access {@link Wrapper#getWrapped()} and bypass
* the {@link jakarta.servlet.ServletRequest}.
* </p>
* <p>
* The current implementation does not support any read operations.
* </p>
*/
class ServletCoreRequest implements Request
{
private final HttpServletRequest _servletRequest;
private final ServletContextRequest _servletContextRequest;
private final HttpFields _httpFields;
private final HttpURI _uri;
ServletCoreRequest(HttpServletRequest request)
{
_servletRequest = request;
_servletContextRequest = ServletContextRequest.getServletContextRequest(_servletRequest);
HttpFields.Mutable fields = HttpFields.build();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String headerName = headerNames.nextElement();
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements())
{
String headerValue = headerValues.nextElement();
fields.add(new HttpField(headerName, headerValue));
}
}
_httpFields = fields.asImmutable();
String includedServletPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH);
boolean included = includedServletPath != null;
HttpURI.Mutable builder = HttpURI.build(request.getRequestURI());
if (included)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(DefaultServlet.getIncludedPathInContext(request, includedServletPath, false))));
else if (request.getDispatcherType() != DispatcherType.REQUEST)
builder.path(addEncodedPaths(request.getContextPath(), encodePath(URIUtil.addPaths(_servletRequest.getServletPath(), _servletRequest.getPathInfo()))));
builder.query(request.getQueryString());
_uri = builder.asImmutable();
}
@Override
public HttpFields getHeaders()
{
return _httpFields;
}
@Override
public HttpURI getHttpURI()
{
return _uri;
}
@Override
public String getId()
{
return _servletRequest.getRequestId();
}
@Override
public String getMethod()
{
return _servletRequest.getMethod();
}
public HttpServletRequest getServletRequest()
{
return _servletRequest;
}
@Override
public boolean isSecure()
{
return _servletRequest.isSecure();
}
@Override
public Object removeAttribute(String name)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.removeAttribute(name);
return value;
}
@Override
public Object setAttribute(String name, Object attribute)
{
Object value = _servletRequest.getAttribute(name);
_servletRequest.setAttribute(name, attribute);
return value;
}
@Override
public Object getAttribute(String name)
{
return _servletRequest.getAttribute(name);
}
@Override
public Set<String> getAttributeNameSet()
{
Set<String> set = new HashSet<>();
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
{
set.add(e.nextElement());
}
return set;
}
@Override
public void clearAttributes()
{
Enumeration<String> e = _servletRequest.getAttributeNames();
while (e.hasMoreElements())
{
_servletRequest.removeAttribute(e.nextElement());
}
}
@Override
public void fail(Throwable failure)
{
throw new UnsupportedOperationException();
}
@Override
public Components getComponents()
{
return _servletContextRequest.getComponents();
}
@Override
public ConnectionMetaData getConnectionMetaData()
{
return _servletContextRequest.getConnectionMetaData();
}
@Override
public Context getContext()
{
return _servletContextRequest.getContext();
}
@Override
public void demand(Runnable demandCallback)
{
throw new UnsupportedOperationException();
}
@Override
public HttpFields getTrailers()
{
return _servletContextRequest.getTrailers();
}
@Override
public long getBeginNanoTime()
{
return _servletContextRequest.getBeginNanoTime();
}
@Override
public long getHeadersNanoTime()
{
return _servletContextRequest.getHeadersNanoTime();
}
@Override
public Content.Chunk read()
{
throw new UnsupportedOperationException();
}
@Override
public boolean consumeAvailable()
{
throw new UnsupportedOperationException();
}
@Override
public void addIdleTimeoutListener(Predicate<TimeoutException> onIdleTimeout)
{
_servletContextRequest.addIdleTimeoutListener(onIdleTimeout);
}
@Override
public void addFailureListener(Consumer<Throwable> onFailure)
{
_servletContextRequest.addFailureListener(onFailure);
}
@Override
public TunnelSupport getTunnelSupport()
{
return null;
}
@Override
public void addHttpStreamWrapper(Function<HttpStream, HttpStream> wrapper)
{
_servletContextRequest.addHttpStreamWrapper(wrapper);
}
@Override
public Session getSession(boolean create)
{
return Session.getSession(_servletRequest.getSession(create));
}
}

View File

@ -0,0 +1,379 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.util.EnumSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.io.ByteBufferInputStream;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
/**
* A {@link HttpServletResponse} wrapped as a core {@link Response}.
* All write operations are internally converted to blocking writes on the servlet API.
*/
class ServletCoreResponse implements Response
{
private final HttpServletResponse _response;
private final Request _coreRequest;
private final HttpFields.Mutable _httpFields;
private final boolean _included;
private final ServletContextResponse _servletContextResponse;
public ServletCoreResponse(Request coreRequest, HttpServletResponse response, boolean included)
{
_coreRequest = coreRequest;
_response = response;
_servletContextResponse = ServletContextResponse.getServletContextResponse(response);
HttpFields.Mutable fields = new HttpServletResponseHttpFields(response);
if (included)
{
// If included, accept but ignore mutations.
fields = new HttpFields.Mutable.Wrapper(fields)
{
@Override
public HttpField onAddField(HttpField field)
{
return null;
}
@Override
public boolean onRemoveField(HttpField field)
{
return false;
}
};
}
_httpFields = fields;
_included = included;
}
@Override
public HttpFields.Mutable getHeaders()
{
return _httpFields;
}
public HttpServletResponse getServletResponse()
{
return _response;
}
@Override
public boolean hasLastWrite()
{
return _servletContextResponse.hasLastWrite();
}
@Override
public boolean isCompletedSuccessfully()
{
return _servletContextResponse.isCompletedSuccessfully();
}
@Override
public boolean isCommitted()
{
return _response.isCommitted();
}
private boolean isWriting()
{
return _servletContextResponse.isWriting();
}
@Override
public void write(boolean last, ByteBuffer byteBuffer, Callback callback)
{
if (_included)
last = false;
try
{
if (BufferUtil.hasContent(byteBuffer))
{
if (isWriting())
{
String characterEncoding = _response.getCharacterEncoding();
try (ByteBufferInputStream bbis = new ByteBufferInputStream(byteBuffer);
InputStreamReader reader = new InputStreamReader(bbis, characterEncoding))
{
IO.copy(reader, _response.getWriter());
}
if (last)
_response.getWriter().close();
}
else
{
BufferUtil.writeTo(byteBuffer, _response.getOutputStream());
if (last)
_response.getOutputStream().close();
}
}
callback.succeeded();
}
catch (Throwable t)
{
callback.failed(t);
}
}
@Override
public Request getRequest()
{
return _coreRequest;
}
@Override
public int getStatus()
{
return _response.getStatus();
}
@Override
public void setStatus(int code)
{
if (_included)
return;
_response.setStatus(code);
}
@Override
public Supplier<HttpFields> getTrailersSupplier()
{
return null;
}
@Override
public void setTrailersSupplier(Supplier<HttpFields> trailers)
{
}
@Override
public void reset()
{
_response.reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
throw new UnsupportedOperationException();
}
@Override
public String toString()
{
return "%s@%x{%s,%s}".formatted(this.getClass().getSimpleName(), hashCode(), this._coreRequest, _response);
}
private static class HttpServletResponseHttpFields implements HttpFields.Mutable
{
private final HttpServletResponse _response;
private HttpServletResponseHttpFields(HttpServletResponse response)
{
_response = response;
}
@Override
public ListIterator<HttpField> listIterator()
{
// The minimum requirement is to implement the listIterator, but it is inefficient.
// Other methods are implemented for efficiency.
final ListIterator<HttpField> list = _response.getHeaderNames().stream()
.map(n -> new HttpField(n, _response.getHeader(n)))
.collect(Collectors.toList())
.listIterator();
return new ListIterator<>()
{
HttpField _last;
@Override
public boolean hasNext()
{
return list.hasNext();
}
@Override
public HttpField next()
{
return _last = list.next();
}
@Override
public boolean hasPrevious()
{
return list.hasPrevious();
}
@Override
public HttpField previous()
{
return _last = list.previous();
}
@Override
public int nextIndex()
{
return list.nextIndex();
}
@Override
public int previousIndex()
{
return list.previousIndex();
}
@Override
public void remove()
{
if (_last != null)
{
// This is not exactly the right semantic for repeated field names
list.remove();
_response.setHeader(_last.getName(), null);
}
}
@Override
public void set(HttpField httpField)
{
list.set(httpField);
_response.setHeader(httpField.getName(), httpField.getValue());
}
@Override
public void add(HttpField httpField)
{
list.add(httpField);
_response.addHeader(httpField.getName(), httpField.getValue());
}
};
}
@Override
public Mutable add(String name, String value)
{
_response.addHeader(name, value);
return this;
}
@Override
public Mutable add(HttpHeader header, HttpHeaderValue value)
{
_response.addHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable add(HttpHeader header, String value)
{
_response.addHeader(header.asString(), value);
return this;
}
@Override
public Mutable add(HttpField field)
{
_response.addHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(HttpField field)
{
_response.setHeader(field.getName(), field.getValue());
return this;
}
@Override
public Mutable put(String name, String value)
{
_response.setHeader(name, value);
return this;
}
@Override
public Mutable put(HttpHeader header, HttpHeaderValue value)
{
_response.setHeader(header.asString(), value.asString());
return this;
}
@Override
public Mutable put(HttpHeader header, String value)
{
_response.setHeader(header.asString(), value);
return this;
}
@Override
public Mutable put(String name, List<String> list)
{
Objects.requireNonNull(name);
Objects.requireNonNull(list);
boolean first = true;
for (String s : list)
{
if (first)
_response.setHeader(name, s);
else
_response.addHeader(name, s);
first = false;
}
return this;
}
@Override
public Mutable remove(HttpHeader header)
{
_response.setHeader(header.asString(), null);
return this;
}
@Override
public Mutable remove(EnumSet<HttpHeader> fields)
{
for (HttpHeader header : fields)
remove(header);
return this;
}
@Override
public Mutable remove(String name)
{
_response.setHeader(name, null);
return this;
}
}
}

View File

@ -2035,7 +2035,7 @@ public class DefaultServletTest
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges"));
assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length())));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE);
String boundary = getContentTypeBoundary(contentType); String boundary = getContentTypeBoundary(contentType);
@ -2063,7 +2063,7 @@ public class DefaultServletTest
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges"));
assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length())));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE);
String boundary = getContentTypeBoundary(contentType); String boundary = getContentTypeBoundary(contentType);
@ -2093,7 +2093,7 @@ public class DefaultServletTest
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges"));
assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length())));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE);
String boundary = getContentTypeBoundary(contentType); String boundary = getContentTypeBoundary(contentType);
@ -2154,7 +2154,7 @@ public class DefaultServletTest
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges"));
assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length())));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE);
String boundary = getContentTypeBoundary(contentType); String boundary = getContentTypeBoundary(contentType);
@ -2183,7 +2183,7 @@ public class DefaultServletTest
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges")); assertThat(response, containsHeaderValue("Content-Type", "multipart/byteranges"));
assertThat(response, containsHeaderValue("Content-Length", "" + body.length())); // TODO #10307 assertThat(response, containsHeaderValue("Content-Length", String.valueOf(body.length())));
HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE); HttpField contentType = response.getField(HttpHeader.CONTENT_TYPE);
String boundary = getContentTypeBoundary(contentType); String boundary = getContentTypeBoundary(contentType);
@ -2295,7 +2295,7 @@ public class DefaultServletTest
HttpTester.Response response = HttpTester.parseResponse(rawResponse); HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
String body = response.getContent(); String body = response.getContent();
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "" + body.length())); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, String.valueOf(body.length())));
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "text/plain;charset=UTF-8")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "text/plain;charset=UTF-8"));
assertThat(body, containsString("Extra Info")); assertThat(body, containsString("Extra Info"));
@ -2308,7 +2308,7 @@ public class DefaultServletTest
response = HttpTester.parseResponse(rawResponse); response = HttpTester.parseResponse(rawResponse);
assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200)); assertThat(response.toString(), response.getStatus(), is(HttpStatus.OK_200));
body = response.getContent(); body = response.getContent();
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, "" + body.length())); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_LENGTH, String.valueOf(body.length())));
assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "image/jpeg;charset=utf-8")); assertThat(response, containsHeaderValue(HttpHeader.CONTENT_TYPE, "image/jpeg;charset=utf-8"));
assertThat(body, containsString("Extra Info")); assertThat(body, containsString("Extra Info"));
} }

View File

@ -16,6 +16,8 @@ package org.eclipse.jetty.ee10.servlet;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader; import java.net.URLClassLoader;
import java.nio.file.Files; import java.nio.file.Files;
@ -23,6 +25,7 @@ import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
@ -57,6 +60,7 @@ import org.eclipse.jetty.session.SessionDataStoreFactory;
import org.eclipse.jetty.toolchain.test.IO; import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.URIUtil;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -359,6 +363,162 @@ public class SessionHandlerTest
} }
} }
@Test
public void testRequestedSessionIdFromCookie() throws Exception
{
String contextPath = "/";
String servletMapping = "/server";
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
DefaultSessionIdManager sessionIdManager = new DefaultSessionIdManager(server);
server.addBean(sessionIdManager, true);
DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT);
server.addBean(cacheFactory);
SessionDataStoreFactory storeFactory = new NullSessionDataStoreFactory();
server.addBean(storeFactory);
HouseKeeper housekeeper = new HouseKeeper();
housekeeper.setIntervalSec(-1); //turn off scavenging
sessionIdManager.setSessionHouseKeeper(housekeeper);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath(contextPath);
server.setHandler(context);
SessionHandler sessionHandler = new SessionHandler();
sessionHandler.setSessionIdManager(sessionIdManager);
sessionHandler.setMaxInactiveInterval(-1); //immortal session
context.setSessionHandler(sessionHandler);
TestRequestedSessionIdServlet servlet = new TestRequestedSessionIdServlet();
ServletHolder holder = new ServletHolder(servlet);
context.addServlet(holder, servletMapping);
server.start();
int port = connector.getLocalPort();
try (StacklessLogging stackless = new StacklessLogging(SessionHandlerTest.class.getPackage()))
{
HttpClient client = new HttpClient();
client.start();
//test with no session cookie
String path = contextPath + (contextPath.endsWith("/") && servletMapping.startsWith("/") ? servletMapping.substring(1) : servletMapping);
String url = "http://localhost:" + port + path;
ContentResponse response = client.GET(url);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString(), containsString("valid=false"));
//test with a cookie for non-existant session
URI uri = URIUtil.toURI(URIUtil.newURI("http", "localhost", port, path, ""));
HttpCookie cookie = HttpCookie.build(SessionHandler.__DefaultSessionCookie, "123456789").path("/").domain("localhost").build();
client.getHttpCookieStore().add(uri, cookie);
response = client.GET(url);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
String content = response.getContentAsString();
assertThat(content, containsString("requestedId=123456789"));
assertThat(content, containsString("valid=false"));
//Get rid of fake cookie
client.getHttpCookieStore().clear();
//Make a real session
response = client.GET(url + "?action=create");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertNotNull(response.getHeaders().get("Set-Cookie"));
//Check the requestedSessionId is valid
response = client.GET(url);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
content = response.getContentAsString();
assertThat(content, containsString("valid=true"));
}
finally
{
server.stop();
}
}
@Test
public void testRequestedSessionIdFromURL() throws Exception
{
String contextPath = "/";
String servletMapping = "/server";
Server server = new Server();
ServerConnector connector = new ServerConnector(server);
server.addConnector(connector);
DefaultSessionIdManager sessionIdManager = new DefaultSessionIdManager(server);
server.addBean(sessionIdManager, true);
DefaultSessionCacheFactory cacheFactory = new DefaultSessionCacheFactory();
cacheFactory.setEvictionPolicy(SessionCache.NEVER_EVICT);
server.addBean(cacheFactory);
SessionDataStoreFactory storeFactory = new NullSessionDataStoreFactory();
server.addBean(storeFactory);
HouseKeeper housekeeper = new HouseKeeper();
housekeeper.setIntervalSec(-1); //turn off scavenging
sessionIdManager.setSessionHouseKeeper(housekeeper);
ServletContextHandler context = new ServletContextHandler();
context.setContextPath(contextPath);
server.setHandler(context);
SessionHandler sessionHandler = new SessionHandler();
sessionHandler.setUsingCookies(false);
sessionHandler.setSessionIdManager(sessionIdManager);
sessionHandler.setMaxInactiveInterval(-1); //immortal session
context.setSessionHandler(sessionHandler);
TestRequestedSessionIdServlet servlet = new TestRequestedSessionIdServlet();
ServletHolder holder = new ServletHolder(servlet);
context.addServlet(holder, servletMapping);
server.start();
int port = connector.getLocalPort();
try (StacklessLogging stackless = new StacklessLogging(SessionHandlerTest.class.getPackage()))
{
HttpClient client = new HttpClient();
client.start();
//test with no session cookie
String path = contextPath + (contextPath.endsWith("/") && servletMapping.startsWith("/") ? servletMapping.substring(1) : servletMapping);
String url = "http://localhost:" + port + path;
ContentResponse response = client.GET(url);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertThat(response.getContentAsString(), containsString("valid=false"));
//test with id for non-existent session
response = client.GET(url + ";" + SessionHandler.__DefaultSessionIdPathParameterName + "=" + "123456789");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
String content = response.getContentAsString();
assertThat(content, containsString("requestedId=123456789"));
assertThat(content, containsString("valid=false"));
//Make a real session
response = client.GET(url + "?action=create");
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
content = response.getContentAsString();
assertThat(content, containsString("createdId="));
String sessionId = content.substring(content.indexOf("createdId=") + 10);
sessionId = sessionId.trim();
//Check the requestedSessionId is valid
response = client.GET(url + ";" + SessionHandler.__DefaultSessionIdPathParameterName + "=" + sessionId);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
content = response.getContentAsString();
assertThat(content, containsString("valid=true"));
}
finally
{
server.stop();
}
}
public static class TestServlet extends HttpServlet public static class TestServlet extends HttpServlet
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -386,6 +546,26 @@ public class SessionHandlerTest
} }
} }
public static class TestRequestedSessionIdServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
public String _id = null;
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
PrintWriter writer = response.getWriter();
writer.println("requestedId=" + request.getRequestedSessionId());
writer.println("valid=" + request.isRequestedSessionIdValid());
if ("create".equals(request.getParameter("action")))
{
HttpSession session = request.getSession(true);
writer.println("createdId=" + session.getId());
}
}
}
public class MockSessionCache extends AbstractSessionCache public class MockSessionCache extends AbstractSessionCache
{ {
@ -395,8 +575,9 @@ public class SessionHandlerTest
} }
@Override @Override
public void shutdown() public ManagedSession doDelete(String key)
{ {
return null;
} }
@Override @Override
@ -411,12 +592,6 @@ public class SessionHandlerTest
return null; return null;
} }
@Override
public ManagedSession doDelete(String key)
{
return null;
}
@Override @Override
public boolean doReplace(String id, ManagedSession oldValue, ManagedSession newValue) public boolean doReplace(String id, ManagedSession oldValue, ManagedSession newValue)
{ {
@ -429,6 +604,11 @@ public class SessionHandlerTest
return null; return null;
} }
@Override
public void shutdown()
{
}
@Override @Override
protected ManagedSession doComputeIfAbsent(String id, Function<String, ManagedSession> mappingFunction) protected ManagedSession doComputeIfAbsent(String id, Function<String, ManagedSession> mappingFunction)
{ {

View File

@ -1117,12 +1117,12 @@ public abstract class RFC2616BaseTest
HttpTester.Response response = responses.get(0); HttpTester.Response response = responses.get(0);
String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)"; String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId);
response = responses.get(1); response = responses.get(1);
specId = "10.3 Redirection HTTP/1.1 - basic (response 2)"; specId = "10.3 Redirection HTTP/1.1 - basic (response 2)";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId);
assertEquals("close", response.get("Connection"), specId); assertEquals("close", response.get("Connection"), specId);
} }
@ -1146,7 +1146,7 @@ public abstract class RFC2616BaseTest
String specId = "10.3 Redirection HTTP/1.0 w/content"; String specId = "10.3 Redirection HTTP/1.0 w/content";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R1.txt", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/R1.txt", response.get("Location"), specId);
} }
/** /**
@ -1169,7 +1169,7 @@ public abstract class RFC2616BaseTest
String specId = "10.3 Redirection HTTP/1.1 w/content"; String specId = "10.3 Redirection HTTP/1.1 w/content";
assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302));
assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R2.txt")); assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost/tests/R2.txt"));
assertThat(specId + " [connection]", response.get("Connection"), is("close")); assertThat(specId + " [connection]", response.get("Connection"), is("close"));
} }

View File

@ -447,7 +447,7 @@ public class MetaInfConfiguration extends AbstractConfiguration
* Scan for META-INF/web-fragment.xml file in the given jar. * Scan for META-INF/web-fragment.xml file in the given jar.
* *
* @param context the context for the scan * @param context the context for the scan
* @param jar the jar resource to scan for fragements in * @param jar the jar resource to scan for fragments in
* @param cache the resource cache * @param cache the resource cache
*/ */
public void scanForFragment(WebAppContext context, Resource jar, ConcurrentHashMap<Resource, Resource> cache) public void scanForFragment(WebAppContext context, Resource jar, ConcurrentHashMap<Resource, Resource> cache)

View File

@ -13,153 +13,578 @@
package org.eclipse.jetty.ee10.webapp; package org.eclipse.jetty.ee10.webapp;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.HashMap;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.FileSystemPool; import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollators;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.Matchers.hasItems;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(WorkDirExtension.class)
public class MetaInfConfigurationTest public class MetaInfConfigurationTest
{ {
public class TestableMetaInfConfiguration extends MetaInfConfiguration @BeforeEach
public void beforeEach()
{ {
List<String> _expectedContainerScanTypes; assertThat(FileSystemPool.INSTANCE.mounts(), empty());
List<String> _expectedWebAppScanTypes;
int _invocationCount = 0;
public TestableMetaInfConfiguration(List<String> expectedContainerScanTypes, List<String> expectedWebAppScanTypes)
{
_expectedContainerScanTypes = expectedContainerScanTypes;
_expectedWebAppScanTypes = expectedWebAppScanTypes;
} }
@Override @AfterEach
public void scanJars(WebAppContext context, Collection<Resource> jars, boolean useCaches, List<String> scanTypes) throws Exception public void tearDown()
{ {
assertNotNull(scanTypes); assertThat(FileSystemPool.INSTANCE.mounts(), empty());
List<String> expectedScanTypes = null;
switch (_invocationCount)
{
case 0:
{
expectedScanTypes = _expectedContainerScanTypes;
break;
}
case 1:
{
expectedScanTypes = _expectedWebAppScanTypes;
break;
}
default:
{
fail("Too many invocations");
}
}
++_invocationCount;
assertNotNull(expectedScanTypes);
assertTrue(expectedScanTypes.containsAll(scanTypes));
assertEquals(expectedScanTypes.size(), scanTypes.size());
}
}
@Test
public void testScanTypes() throws Exception
{
Path web25 = MavenTestingUtils.getTestResourcePathFile("web25.xml");
Path web31 = MavenTestingUtils.getTestResourcePathFile("web31.xml");
Path web31false = MavenTestingUtils.getTestResourcePathFile("web31false.xml");
//test a 2.5 webapp will not look for fragments as manually configured
MetaInfConfiguration meta25 = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes,
Arrays.asList(MetaInfConfiguration.METAINF_TLDS, MetaInfConfiguration.METAINF_RESOURCES));
WebAppContext context25 = new WebAppContext();
context25.setConfigurationDiscovered(false);
context25.getMetaData().setWebDescriptor(new WebDescriptor(context25.getResourceFactory().newResource(web25)));
context25.getContext().getServletContext().setEffectiveMajorVersion(2);
context25.getContext().getServletContext().setEffectiveMinorVersion(5);
meta25.preConfigure(context25);
//test a 2.5 webapp will look for fragments as configurationDiscovered default true
MetaInfConfiguration meta25b = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes,
MetaInfConfiguration.__allScanTypes);
WebAppContext context25b = new WebAppContext();
context25b.getMetaData().setWebDescriptor(new WebDescriptor(context25b.getResourceFactory().newResource(web25)));
context25b.getContext().getServletContext().setEffectiveMajorVersion(2);
context25b.getContext().getServletContext().setEffectiveMinorVersion(5);
meta25b.preConfigure(context25b);
//test a 3.x metadata-complete webapp will not look for fragments
MetaInfConfiguration meta31 = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes,
Arrays.asList(MetaInfConfiguration.METAINF_TLDS, MetaInfConfiguration.METAINF_RESOURCES));
WebAppContext context31 = new WebAppContext();
context31.getMetaData().setWebDescriptor(new WebDescriptor(context31.getResourceFactory().newResource(web31)));
context31.getContext().getServletContext().setEffectiveMajorVersion(3);
context31.getContext().getServletContext().setEffectiveMinorVersion(1);
meta31.preConfigure(context31);
//test a 3.x non metadata-complete webapp will look for fragments
MetaInfConfiguration meta31false = new TestableMetaInfConfiguration(MetaInfConfiguration.__allScanTypes,
MetaInfConfiguration.__allScanTypes);
WebAppContext context31false = new WebAppContext();
context31false.setConfigurationDiscovered(true);
context31false.getMetaData().setWebDescriptor(new WebDescriptor(context31false.getResourceFactory().newResource(web31false)));
context31false.getContext().getServletContext().setEffectiveMajorVersion(3);
context31false.getContext().getServletContext().setEffectiveMinorVersion(1);
meta31false.preConfigure(context31false);
} }
/** /**
* This test examines both the classpath and the module path to find * Test of a MetaInf scan of a Servlet 2.5 webapp, where
* container resources. * {@link WebAppContext#setConfigurationDiscovered(boolean)} set to {@code false},
* NOTE: the behaviour of the surefire plugin 3.0.0.M2 is different in * thus not performing any Servlet 3.0+ discovery steps for {@code META-INF/web-fragment.xml}.
* jetty-9.4.x to jetty-10.0.x (where we use module-info): in jetty-9.4.x, * Scanning for {@code META-INF/resources} is unaffected by configuration.
* we can use the --add-module argument to put the foo-bar-janb.jar onto the
* module path, but this doesn't seem to work in jetty-10.0.x. So this test
* will find foo-bar.janb.jar on the classpath, and jetty-util from the module path.
*
* @throws Exception if the test fails
*/ */
@Test @Test
public void testFindAndFilterContainerPathsJDK9() throws Exception public void testScanServlet25ConfigurationDiscoveredOff(WorkDir workDir) throws Exception
{
Path webappDir = workDir.getEmptyPathDir();
Path webinf = webappDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
Path webxml = webinf.resolve("web.xml");
String web25 = """
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Test 2.5 WebApp</display-name>
</web-app>
""";
Files.writeString(webxml, web25, StandardCharsets.UTF_8);
Path libDir = webinf.resolve("lib");
FS.ensureDirExists(libDir);
Path fooFragmentJar = libDir.resolve("foo-fragment.jar");
try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar))
{
Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml");
FS.ensureDirExists(webfragment.getParent());
Files.writeString(webfragment, "<web-fragment />", StandardCharsets.UTF_8);
}
Path barResourceJar = libDir.resolve("bar-resources.jar");
try (FileSystem jarfs = createNewJarFileSystem(barResourceJar))
{
Path resourcesDir = jarfs.getPath("/META-INF/resources");
Files.createDirectories(resourcesDir);
Path testTxt = resourcesDir.resolve("test.txt");
Files.writeString(testTxt, "Test", StandardCharsets.UTF_8);
}
Path zedTldJar = libDir.resolve("zed-tlds.jar");
try (FileSystem jarfs = createNewJarFileSystem(zedTldJar))
{
Path tldFile = jarfs.getPath("/META-INF/zed.tld");
Files.createDirectory(tldFile.getParent());
Files.writeString(tldFile, "<taglib />", StandardCharsets.UTF_8);
}
WebAppContext context = new WebAppContext();
context.setServer(new Server());
try
{
context.setBaseResource(context.getResourceFactory().newResource(webappDir));
context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml)));
context.setConfigurationDiscovered(false); // don't allow discovery of servlet 3.0+ features
context.getContext().getServletContext().setEffectiveMajorVersion(2);
context.getContext().getServletContext().setEffectiveMinorVersion(5);
MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration();
metaInfConfiguration.preConfigure(context);
List<String> discoveredWebInfResources = context.getMetaData().getWebInfResources(false)
.stream()
.sorted(ResourceCollators.byName(true))
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedWebInfResources = {
fooFragmentJar.toUri().toASCIIString(),
barResourceJar.toUri().toASCIIString(),
zedTldJar.toUri().toASCIIString()
};
assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources));
// Since this is Servlet 2.5, and we have configuration-discovered turned off, we shouldn't see any web fragments
Map<Resource, Resource> fragmentMap = getDiscoveredMetaInfFragments(context);
assertThat("META-INF/web-fragment.xml discovered (servlet 2.5 and configuration-discovered turned off)", fragmentMap.size(), is(0));
// Even on Servlet 2.5, when we have configuration-discovered turned off, we should still see the META-INF/resources/
Set<Resource> resourceSet = getDiscoveredMetaInfResource(context);
assertThat(resourceSet.size(), is(1));
List<String> discoveredResources = resourceSet
.stream()
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedResources = {
URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/"
};
assertThat("META-INF/resources discovered (servlet 2.5 and configuration-discovered turned off)", discoveredResources, hasItems(expectedResources));
// TLDs discovered
Set<URL> tldSet = getDiscoveredMetaInfTlds(context);
assertThat(tldSet.size(), is(1));
List<String> discoveredTlds = tldSet
.stream()
.map(URL::toExternalForm)
.toList();
String[] expectedTlds = {
URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld"
};
assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds));
}
finally
{
LifeCycle.stop(context.getResourceFactory());
}
}
/**
* Test of a MetaInf scan of a Servlet 2.5 webapp, where
* {@link WebAppContext#setConfigurationDiscovered(boolean)} is left at default (@{code true})
* allowing the performing of Servlet 3.0+ discovery steps for {@code META-INF/web-fragment.xml} and {@code META-INF/resources}
*/
@Test
public void testScanServlet25ConfigurationDiscoveredDefault(WorkDir workDir) throws Exception
{
Path webappDir = workDir.getEmptyPathDir();
Path webinf = webappDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
Path webxml = webinf.resolve("web.xml");
String web25 = """
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Test 2.5 WebApp</display-name>
</web-app>
""";
Files.writeString(webxml, web25, StandardCharsets.UTF_8);
Path libDir = webinf.resolve("lib");
FS.ensureDirExists(libDir);
Path fooFragmentJar = libDir.resolve("foo-fragment.jar");
try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar))
{
Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml");
FS.ensureDirExists(webfragment.getParent());
Files.writeString(webfragment, "<web-fragment />", StandardCharsets.UTF_8);
}
Path barResourceJar = libDir.resolve("bar-resources.jar");
try (FileSystem jarfs = createNewJarFileSystem(barResourceJar))
{
Path resourcesDir = jarfs.getPath("/META-INF/resources");
Files.createDirectories(resourcesDir);
Path testTxt = resourcesDir.resolve("test.txt");
Files.writeString(testTxt, "Test", StandardCharsets.UTF_8);
}
Path zedTldJar = libDir.resolve("zed-tlds.jar");
try (FileSystem jarfs = createNewJarFileSystem(zedTldJar))
{
Path tldFile = jarfs.getPath("/META-INF/zed.tld");
Files.createDirectory(tldFile.getParent());
Files.writeString(tldFile, "<taglib />", StandardCharsets.UTF_8);
}
WebAppContext context = new WebAppContext();
context.setServer(new Server());
try
{
context.setBaseResource(context.getResourceFactory().newResource(webappDir));
context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml)));
// context25.setConfigurationDiscovered(true); // The default value
context.getContext().getServletContext().setEffectiveMajorVersion(2);
context.getContext().getServletContext().setEffectiveMinorVersion(5);
MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration();
metaInfConfiguration.preConfigure(context);
List<String> discoveredWebInfResources = context.getMetaData().getWebInfResources(false)
.stream()
.sorted(ResourceCollators.byName(true))
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedWebInfResources = {
fooFragmentJar.toUri().toASCIIString(),
barResourceJar.toUri().toASCIIString(),
zedTldJar.toUri().toASCIIString()
};
assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources));
// Since this is Servlet 2.5, and we have configuration-discovered turned on, we should see the META-INF/web-fragment.xml entries
Map<Resource, Resource> fragmentMap = getDiscoveredMetaInfFragments(context);
assertThat(fragmentMap.size(), is(1));
List<String> discoveredFragments = fragmentMap.entrySet()
.stream()
.map(e -> e.getValue().getURI().toASCIIString())
.toList();
String[] expectedFragments = {
URIUtil.toJarFileUri(fooFragmentJar.toUri()).toASCIIString() + "META-INF/web-fragment.xml"
};
assertThat("META-INF/web-fragment.xml discovered (servlet 2.5 and configuration-discovered=true)", discoveredFragments, hasItems(expectedFragments));
// Since this is Servlet 2.5, and we have configuration-discovered turned on, we should see the META-INF/resources/
Set<Resource> resourceSet = getDiscoveredMetaInfResource(context);
assertThat(resourceSet.size(), is(1));
List<String> discoveredResources = resourceSet
.stream()
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedResources = {
URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/"
};
assertThat("META-INF/resources discovered (servlet 2.5 and configuration-discovered=true)", discoveredResources, hasItems(expectedResources));
// TLDs discovered
Set<URL> tldSet = getDiscoveredMetaInfTlds(context);
assertThat(tldSet.size(), is(1));
List<String> discoveredTlds = tldSet
.stream()
.map(URL::toExternalForm)
.toList();
String[] expectedTlds = {
URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld"
};
assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds));
}
finally
{
LifeCycle.stop(context.getResourceFactory());
}
}
/**
* Test of a MetaInf scan of a Servlet 3.0 webapp, metadata-complete is set to {@code false}
*/
@Test
public void testScanServlet30MetadataCompleteFalse(WorkDir workDir) throws Exception
{
Path webappDir = workDir.getEmptyPathDir();
Path webinf = webappDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
Path webxml = webinf.resolve("web.xml");
String web30 = """
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
metadata-complete="false"
version="3.0">
<display-name>Test 3.0 WebApp</display-name>
</web-app>
""";
Files.writeString(webxml, web30, StandardCharsets.UTF_8);
Path libDir = webinf.resolve("lib");
FS.ensureDirExists(libDir);
Path fooFragmentJar = libDir.resolve("foo-fragment.jar");
try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar))
{
Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml");
FS.ensureDirExists(webfragment.getParent());
Files.writeString(webfragment, "<web-fragment />", StandardCharsets.UTF_8);
}
Path barResourceJar = libDir.resolve("bar-resources.jar");
try (FileSystem jarfs = createNewJarFileSystem(barResourceJar))
{
Path resourcesDir = jarfs.getPath("/META-INF/resources");
Files.createDirectories(resourcesDir);
Path testTxt = resourcesDir.resolve("test.txt");
Files.writeString(testTxt, "Test", StandardCharsets.UTF_8);
}
Path zedTldJar = libDir.resolve("zed-tlds.jar");
try (FileSystem jarfs = createNewJarFileSystem(zedTldJar))
{
Path tldFile = jarfs.getPath("/META-INF/zed.tld");
Files.createDirectory(tldFile.getParent());
Files.writeString(tldFile, "<taglib />", StandardCharsets.UTF_8);
}
WebAppContext context = new WebAppContext();
context.setServer(new Server());
try
{
context.setBaseResource(context.getResourceFactory().newResource(webappDir));
context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml)));
// context25.setConfigurationDiscovered(true); // The default value
context.getContext().getServletContext().setEffectiveMajorVersion(3);
context.getContext().getServletContext().setEffectiveMinorVersion(0);
MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration();
metaInfConfiguration.preConfigure(context);
List<String> discoveredWebInfResources = context.getMetaData().getWebInfResources(false)
.stream()
.sorted(ResourceCollators.byName(true))
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedWebInfResources = {
fooFragmentJar.toUri().toASCIIString(),
barResourceJar.toUri().toASCIIString(),
zedTldJar.toUri().toASCIIString()
};
assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources));
// Since this is Servlet 3.0, and we have configuration-discovered turned on, we should see the META-INF/web-fragment.xml entries
Map<Resource, Resource> fragmentMap = getDiscoveredMetaInfFragments(context);
assertThat(fragmentMap.size(), is(1));
List<String> discoveredFragments = fragmentMap.entrySet()
.stream()
.map(e -> e.getValue().getURI().toASCIIString())
.toList();
String[] expectedFragments = {
URIUtil.toJarFileUri(fooFragmentJar.toUri()).toASCIIString() + "META-INF/web-fragment.xml"
};
assertThat("META-INF/web-fragment.xml discovered (servlet 3.0, and metadata-complete=false, and configuration-discovered=true)", discoveredFragments, hasItems(expectedFragments));
// Since this is Servlet 3.0, and we have configuration-discovered turned on, we should see the META-INF/resources/
Set<Resource> resourceSet = getDiscoveredMetaInfResource(context);
assertThat(resourceSet.size(), is(1));
List<String> discoveredResources = resourceSet
.stream()
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedResources = {
URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/"
};
assertThat("META-INF/resources discovered (servlet 3.0, and metadata-complete=false, and configuration-discovered=true)", discoveredResources, hasItems(expectedResources));
// TLDs discovered
Set<URL> tldSet = getDiscoveredMetaInfTlds(context);
assertThat(tldSet.size(), is(1));
List<String> discoveredTlds = tldSet
.stream()
.map(URL::toExternalForm)
.toList();
String[] expectedTlds = {
URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld"
};
assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds));
}
finally
{
LifeCycle.stop(context.getResourceFactory());
}
}
/**
* Test of a MetaInf scan of a Servlet 3.1 webapp, metadata-complete is set to {@code true}
*/
@Test
public void testScanServlet31MetadataCompleteTrue(WorkDir workDir) throws Exception
{
Path webappDir = workDir.getEmptyPathDir();
Path webinf = webappDir.resolve("WEB-INF");
FS.ensureDirExists(webinf);
Path webxml = webinf.resolve("web.xml");
String web31 = """
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
metadata-complete="true"
version="3.1">
<display-name>Test 3.1 WebApp</display-name>
</web-app>
""";
Files.writeString(webxml, web31, StandardCharsets.UTF_8);
Path libDir = webinf.resolve("lib");
FS.ensureDirExists(libDir);
Path fooFragmentJar = libDir.resolve("foo-fragment.jar");
try (FileSystem jarfs = createNewJarFileSystem(fooFragmentJar))
{
Path webfragment = jarfs.getPath("/META-INF/web-fragment.xml");
FS.ensureDirExists(webfragment.getParent());
Files.writeString(webfragment, "<web-fragment />", StandardCharsets.UTF_8);
}
Path barResourceJar = libDir.resolve("bar-resources.jar");
try (FileSystem jarfs = createNewJarFileSystem(barResourceJar))
{
Path resourcesDir = jarfs.getPath("/META-INF/resources");
Files.createDirectories(resourcesDir);
Path testTxt = resourcesDir.resolve("test.txt");
Files.writeString(testTxt, "Test", StandardCharsets.UTF_8);
}
Path zedTldJar = libDir.resolve("zed-tlds.jar");
try (FileSystem jarfs = createNewJarFileSystem(zedTldJar))
{
Path tldFile = jarfs.getPath("/META-INF/zed.tld");
Files.createDirectory(tldFile.getParent());
Files.writeString(tldFile, "<taglib />", StandardCharsets.UTF_8);
}
WebAppContext context = new WebAppContext();
context.setServer(new Server());
try
{
context.setBaseResource(context.getResourceFactory().newResource(webappDir));
context.getMetaData().setWebDescriptor(new WebDescriptor(context.getResourceFactory().newResource(webxml)));
context.getContext().getServletContext().setEffectiveMajorVersion(3);
context.getContext().getServletContext().setEffectiveMinorVersion(1);
MetaInfConfiguration metaInfConfiguration = new MetaInfConfiguration();
metaInfConfiguration.preConfigure(context);
List<String> discoveredWebInfResources = context.getMetaData().getWebInfResources(false)
.stream()
.sorted(ResourceCollators.byName(true))
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedWebInfResources = {
fooFragmentJar.toUri().toASCIIString(),
barResourceJar.toUri().toASCIIString(),
zedTldJar.toUri().toASCIIString()
};
assertThat("Discovered WEB-INF resources", discoveredWebInfResources, hasItems(expectedWebInfResources));
// Since this is Servlet 3.1, and we have metadata-complete=true, we should see no fragments
Map<Resource, Resource> fragmentMap = getDiscoveredMetaInfFragments(context);
assertThat("META-INF/web-fragment.xml discovered (servlet 3.1, and metadata-complete=true)", fragmentMap.size(), is(0));
// Even on Servlet 3.1, with metadata-complete=true, we should still see the META-INF/resources/
Set<Resource> resourceSet = getDiscoveredMetaInfResource(context);
assertThat(resourceSet.size(), is(1));
List<String> discoveredResources = resourceSet
.stream()
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
String[] expectedResources = {
URIUtil.toJarFileUri(barResourceJar.toUri()).toASCIIString() + "META-INF/resources/"
};
assertThat("META-INF/resources discovered (servlet 3.1 and metadata-complete=true)", discoveredResources, hasItems(expectedResources));
// TLDs discovered
Set<URL> tldSet = getDiscoveredMetaInfTlds(context);
assertThat(tldSet.size(), is(1));
List<String> discoveredTlds = tldSet
.stream()
.map(URL::toExternalForm)
.toList();
String[] expectedTlds = {
URIUtil.toJarFileUri(zedTldJar.toUri()).toASCIIString() + "META-INF/zed.tld"
};
assertThat("Discovered TLDs", discoveredTlds, hasItems(expectedTlds));
}
finally
{
LifeCycle.stop(context.getResourceFactory());
}
}
private FileSystem createNewJarFileSystem(Path jarFile) throws IOException
{
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI jarUri = URIUtil.uriJarPrefix(jarFile.toUri(), "!/");
return FileSystems.newFileSystem(jarUri, env);
}
/**
* This test examines both the classpath and the module path to find container resources.
* This test looks {@code foo-bar.janb.jar} on the classpath (it was added there by the surefire configuration
* present in the {@code pom.xml}), and the {@code servlet-api} from the module path.
*/
@Test
public void testGetContainerPathsWithModuleSystem() throws Exception
{ {
MetaInfConfiguration config = new MetaInfConfiguration(); MetaInfConfiguration config = new MetaInfConfiguration();
WebAppContext context = new WebAppContext(); WebAppContext context = new WebAppContext();
context.setServer(new Server()); context.setServer(new Server());
config.preConfigure(context);
try try
{ {
context.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, ".*servlet-api-[^/]*\\.jar$|.*/foo-bar-janb.jar"); context.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, ".*servlet-api-[^/]*\\.jar$|.*/foo-bar-janb.jar");
WebAppClassLoader loader = new WebAppClassLoader(context); WebAppClassLoader loader = new WebAppClassLoader(context);
context.setClassLoader(loader); context.setClassLoader(loader);
config.findAndFilterContainerPaths(context); config.preConfigure(context);
List<Resource> containerResources = context.getMetaData().getContainerResources();
assertEquals(2, containerResources.size()); Class janbClazz = Class.forName("foo.bar.janb.What", false, loader);
for (Resource r : containerResources) URI janbUri = TypeUtil.getLocationOfClass(janbClazz);
{ Class servletClazz = Class.forName("jakarta.servlet.Servlet", false, loader);
String s = r.toString(); URI servletUri = TypeUtil.getLocationOfClass(servletClazz);
assertTrue(s.endsWith("foo-bar-janb.jar") || s.contains("servlet-api"));
} List<String> discoveredContainerResources = context.getMetaData().getContainerResources()
.stream()
.sorted(ResourceCollators.byName(true))
.map(Resource::getURI)
.map(URI::toASCIIString)
.toList();
// we "correct" the bad file URLs that come from the ClassLoader
// to be the same as what comes from every non-classloader URL/URI.
String[] expectedContainerResources = {
URIUtil.correctFileURI(janbUri).toASCIIString(),
URIUtil.correctFileURI(servletUri).toASCIIString()
};
assertThat("Discovered Container resources", discoveredContainerResources, hasItems(expectedContainerResources));
} }
finally finally
{ {
config.postConfigure(context); config.postConfigure(context);
// manually stop ResourceFactory.
// normally this would be done via WebAppContext.stop(), but we didn't start the context.
LifeCycle.stop(context.getResourceFactory());
} }
} }
private Map<Resource, Resource> getDiscoveredMetaInfFragments(WebAppContext context)
{
return (Map<Resource, Resource>)context.getAttribute(MetaInfConfiguration.METAINF_FRAGMENTS);
}
private Set<Resource> getDiscoveredMetaInfResource(WebAppContext context)
{
return (Set<Resource>)context.getAttribute(MetaInfConfiguration.METAINF_RESOURCES);
}
private Set<URL> getDiscoveredMetaInfTlds(WebAppContext context)
{
return (Set<URL>)context.getAttribute(MetaInfConfiguration.METAINF_TLDS);
}
} }

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<display-name>Test 2.5 WebApp</display-name>
</web-app>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
metadata-complete="true"
version="6.0">
<display-name>Test 31 WebApp</display-name>
</web-app>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
metadata-complete="false"
version="6.0">
<display-name>Test 31 WebApp</display-name>
</web-app>

View File

@ -1585,8 +1585,7 @@ public class HttpOutput extends ServletOutputStream implements Runnable
} }
catch (Throwable t) catch (Throwable t)
{ {
if (ExceptionUtil.areNotAssociated(e, t)) ExceptionUtil.addSuppressedIfNotAssociated(e, t);
e.addSuppressed(t);
} }
finally finally
{ {

View File

@ -2482,6 +2482,12 @@ public class ResponseTest
return _committed; return _committed;
} }
@Override
public boolean hasLastWrite()
{
return _last;
}
@Override @Override
public boolean isCompletedSuccessfully() public boolean isCompletedSuccessfully()
{ {

View File

@ -50,7 +50,6 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -87,6 +86,7 @@ public class SessionHandlerTest
String pathInContext = request.getPathInfo(); String pathInContext = request.getPathInfo();
String[] split = pathInContext.substring(1).split("/"); String[] split = pathInContext.substring(1).split("/");
String requestedSessionId = request.getRequestedSessionId();
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
if (split.length > 0) if (split.length > 0)
@ -135,6 +135,10 @@ public class SessionHandlerTest
} }
StringBuilder out = new StringBuilder(); StringBuilder out = new StringBuilder();
out.append("requestedSessionId=" + requestedSessionId).append('\n');
out.append("requestedSessionIdValid=" + request.isRequestedSessionIdValid()).append('\n');
if (session == null) if (session == null)
out.append("No Session\n"); out.append("No Session\n");
else else
@ -309,8 +313,9 @@ public class SessionHandlerTest
String setCookie = response.get("SET-COOKIE"); String setCookie = response.get("SET-COOKIE");
assertThat(setCookie, containsString("Path=/")); assertThat(setCookie, containsString("Path=/"));
String content = response.getContent(); String content = response.getContent();
assertThat(content, startsWith("Session=")); assertThat(content, containsString("Session="));
String id = content.substring(content.indexOf('=') + 1, content.indexOf('\n')); String id = content.substring(content.indexOf("Session=") + 8);
id = id.trim();
assertThat(id, not(equalTo("oldCookieId"))); assertThat(id, not(equalTo("oldCookieId")));
endPoint.addInput(""" endPoint.addInput("""
@ -326,6 +331,64 @@ public class SessionHandlerTest
assertThat(content, containsString("Session=" + id)); assertThat(content, containsString("Session=" + id));
} }
@Test
public void testRequestedSessionIdFromCookie() throws Exception
{
_server.stop();
_sessionHandler.setSessionPath(null);
_contextHandler.setContextPath("/");
_server.start();
//test with no session cookie
LocalConnector.LocalEndPoint endPoint = _connector.connect();
endPoint.addInput("""
GET / HTTP/1.1
Host: localhost
""");
HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getStatus(), equalTo(200));
assertThat(response.getContent(), containsString("No Session"));
assertThat(response.getContent(), containsString("requestedSessionIdValid=false"));
//test with a cookie for non-existant session
endPoint.addInput("""
GET / HTTP/1.1
Host: localhost
Cookie: JSESSIONID=%s
""".formatted("123456789"));
response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getStatus(), equalTo(200));
assertThat(response.getContent(), containsString("No Session"));
assertThat(response.getContent(), containsString("requestedSessionIdValid=false"));
//Make a real session
endPoint.addInput("""
GET /create HTTP/1.1
Host: localhost
""");
response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getStatus(), equalTo(200));
String content = response.getContent();
assertThat(content, containsString("Session="));
String id = content.substring(content.indexOf("Session=") + 8);
id = id.trim();
//Check the requestedSessionId is valid
endPoint.addInput("""
GET / HTTP/1.1
Host: localhost
Cookie: JSESSIONID=%s
""".formatted(id));
response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getContent(), containsString("requestedSessionIdValid=true"));
}
@Test @Test
public void testSetAttribute() throws Exception public void testSetAttribute() throws Exception
{ {
@ -339,8 +402,9 @@ public class SessionHandlerTest
HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getStatus(), equalTo(200)); assertThat(response.getStatus(), equalTo(200));
String content = response.getContent(); String content = response.getContent();
assertThat(content, startsWith("Session=")); assertThat(content, containsString("Session="));
String id = content.substring(content.indexOf('=') + 1, content.indexOf('\n')); String id = content.substring(content.indexOf("Session=") + 8);
id = id.trim();
endPoint.addInput(""" endPoint.addInput("""
GET /set/attribute/value HTTP/1.1 GET /set/attribute/value HTTP/1.1
@ -380,7 +444,7 @@ public class SessionHandlerTest
HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse()); HttpTester.Response response = HttpTester.parseResponse(endPoint.getResponse());
assertThat(response.getStatus(), equalTo(200)); assertThat(response.getStatus(), equalTo(200));
String content = response.getContent(); String content = response.getContent();
assertThat(content, startsWith("Session=")); assertThat(content, containsString("Session="));
String setCookie = response.get(HttpHeader.SET_COOKIE); String setCookie = response.get(HttpHeader.SET_COOKIE);
String id = setCookie.substring(setCookie.indexOf("JSESSIONID=") + 11, setCookie.indexOf("; Path=/")); String id = setCookie.substring(setCookie.indexOf("JSESSIONID=") + 11, setCookie.indexOf("; Path=/"));

View File

@ -28,7 +28,7 @@ public class NoJspServlet extends HttpServlet
protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException protected void doGet(HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException
{ {
if (!_warned) if (!_warned)
getServletContext().log("No JSP support. Check that JSP jars are in lib/jsp and that the JSP option has been specified to start.jar"); getServletContext().log("No JSP support. Check that the ee9-jsp module is enabled, or otherwise ensure the jsp jars are on the server classpath.");
_warned = true; _warned = true;
response.sendError(500, "JSP support not configured"); response.sendError(500, "JSP support not configured");

View File

@ -1114,12 +1114,12 @@ public abstract class RFC2616BaseTest
HttpTester.Response response = responses.get(0); HttpTester.Response response = responses.get(0);
String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)"; String specId = "10.3 Redirection HTTP/1.1 - basic (response 1)";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId);
response = responses.get(1); response = responses.get(1);
specId = "10.3 Redirection HTTP/1.1 - basic (response 2)"; specId = "10.3 Redirection HTTP/1.1 - basic (response 2)";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/", response.get("Location"), specId);
assertEquals("close", response.get("Connection"), specId); assertEquals("close", response.get("Connection"), specId);
} }
@ -1143,7 +1143,7 @@ public abstract class RFC2616BaseTest
String specId = "10.3 Redirection HTTP/1.0 w/content"; String specId = "10.3 Redirection HTTP/1.0 w/content";
assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId, response.getStatus(), is(HttpStatus.FOUND_302));
assertEquals(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R1.txt", response.get("Location"), specId); assertEquals(getServer().getScheme() + "://localhost/tests/R1.txt", response.get("Location"), specId);
} }
/** /**
@ -1166,7 +1166,7 @@ public abstract class RFC2616BaseTest
String specId = "10.3 Redirection HTTP/1.1 w/content"; String specId = "10.3 Redirection HTTP/1.1 w/content";
assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302)); assertThat(specId + " [status]", response.getStatus(), is(HttpStatus.FOUND_302));
assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost:" + getServer().getServerPort() + "/tests/R2.txt")); assertThat(specId + " [location]", response.get("Location"), is(getServer().getScheme() + "://localhost/tests/R2.txt"));
assertThat(specId + " [connection]", response.get("Connection"), is("close")); assertThat(specId + " [connection]", response.get("Connection"), is("close"));
} }

View File

@ -37,7 +37,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty.ee9</groupId> <groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-annotations</artifactId> <artifactId>jetty-ee9-webapp</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.eclipse.jetty</groupId> <groupId>org.eclipse.jetty</groupId>

View File

@ -72,6 +72,11 @@
<artifactId>jetty-jmx</artifactId> <artifactId>jetty-jmx</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.jetty.ee9</groupId>
<artifactId>jetty-ee9-annotations</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

14
pom.xml
View File

@ -56,7 +56,7 @@
<google.errorprone.version>2.20.0</google.errorprone.version> <google.errorprone.version>2.20.0</google.errorprone.version>
<grpc.version>1.56.1</grpc.version> <grpc.version>1.56.1</grpc.version>
<gson.version>2.10.1</gson.version> <gson.version>2.10.1</gson.version>
<guava.version>32.1.1-jre</guava.version> <guava.version>32.1.2-jre</guava.version>
<guice.version>7.0.0</guice.version> <guice.version>7.0.0</guice.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
<hazelcast.version>5.3.1</hazelcast.version> <hazelcast.version>5.3.1</hazelcast.version>
@ -87,7 +87,7 @@
<jboss.logging.annotations.version>2.2.1.Final</jboss.logging.annotations.version> <jboss.logging.annotations.version>2.2.1.Final</jboss.logging.annotations.version>
<jboss.logging.processor.version>2.2.1.Final</jboss.logging.processor.version> <jboss.logging.processor.version>2.2.1.Final</jboss.logging.processor.version>
<jboss.logging.version>3.5.3.Final</jboss.logging.version> <jboss.logging.version>3.5.3.Final</jboss.logging.version>
<jboss-logmanager.version>2.3.0.Alpha1</jboss-logmanager.version> <jboss-logmanager.version>3.0.1.Final</jboss-logmanager.version>
<jboss-threads.version>3.5.0.Final</jboss-threads.version> <jboss-threads.version>3.5.0.Final</jboss-threads.version>
<jetty-assembly-descriptors.version>1.1</jetty-assembly-descriptors.version> <jetty-assembly-descriptors.version>1.1</jetty-assembly-descriptors.version>
<jetty.perf-helper.version>1.0.7</jetty.perf-helper.version> <jetty.perf-helper.version>1.0.7</jetty.perf-helper.version>
@ -95,7 +95,7 @@
<jetty-test-policy.version>1.2</jetty-test-policy.version> <jetty-test-policy.version>1.2</jetty-test-policy.version>
<jetty.test.version>6.1</jetty.test.version> <jetty.test.version>6.1</jetty.test.version>
<jetty.xhtml.schemas-version>1.1</jetty.xhtml.schemas-version> <jetty.xhtml.schemas-version>1.1</jetty.xhtml.schemas-version>
<jmh.version>1.36</jmh.version> <jmh.version>1.37</jmh.version>
<jna.version>5.13.0</jna.version> <jna.version>5.13.0</jna.version>
<json-simple.version>1.1.1</json-simple.version> <json-simple.version>1.1.1</json-simple.version>
<json-smart.version>2.5.0</json-smart.version> <json-smart.version>2.5.0</json-smart.version>
@ -103,12 +103,12 @@
<jsp.impl.version>10.0.14</jsp.impl.version> <jsp.impl.version>10.0.14</jsp.impl.version>
<kerb-simplekdc.version>2.0.3</kerb-simplekdc.version> <kerb-simplekdc.version>2.0.3</kerb-simplekdc.version>
<log4j2.version>2.20.0</log4j2.version> <log4j2.version>2.20.0</log4j2.version>
<logback.version>1.4.8</logback.version> <logback.version>1.4.9</logback.version>
<mariadb.version>3.1.4</mariadb.version> <mariadb.version>3.1.4</mariadb.version>
<mariadb.docker.version>10.3.6</mariadb.docker.version> <mariadb.docker.version>10.3.6</mariadb.docker.version>
<maven.deps.version>3.9.0</maven.deps.version> <maven.deps.version>3.9.0</maven.deps.version>
<maven-artifact-transfer.version>0.13.1</maven-artifact-transfer.version> <maven-artifact-transfer.version>0.13.1</maven-artifact-transfer.version>
<maven.resolver.version>1.9.14</maven.resolver.version> <maven.resolver.version>1.9.15</maven.resolver.version>
<maven.version>3.9.0</maven.version> <maven.version>3.9.0</maven.version>
<mongodb.version>3.12.11</mongodb.version> <mongodb.version>3.12.11</mongodb.version>
<openpojo.version>0.9.1</openpojo.version> <openpojo.version>0.9.1</openpojo.version>
@ -151,7 +151,7 @@
<!-- some maven plugins versions --> <!-- some maven plugins versions -->
<asciidoctor.maven.plugin.version>2.2.4</asciidoctor.maven.plugin.version> <asciidoctor.maven.plugin.version>2.2.4</asciidoctor.maven.plugin.version>
<build-helper.maven.plugin.version>3.3.0</build-helper.maven.plugin.version> <build-helper.maven.plugin.version>3.3.0</build-helper.maven.plugin.version>
<buildnumber.maven.plugin.version>3.0.0</buildnumber.maven.plugin.version> <buildnumber.maven.plugin.version>3.2.0</buildnumber.maven.plugin.version>
<depends.maven.plugin.version>1.5.0</depends.maven.plugin.version> <depends.maven.plugin.version>1.5.0</depends.maven.plugin.version>
<flatten.maven.plugin.version>1.3.0</flatten.maven.plugin.version> <flatten.maven.plugin.version>1.3.0</flatten.maven.plugin.version>
<groovy.version>4.0.6</groovy.version> <groovy.version>4.0.6</groovy.version>
@ -2222,7 +2222,7 @@
</property> </property>
</activation> </activation>
<properties> <properties>
<cbi-plugins.version>1.1.7</cbi-plugins.version> <cbi-plugins.version>1.4.2</cbi-plugins.version>
</properties> </properties>
<build> <build>
<plugins> <plugins>

View File

@ -50,6 +50,7 @@ import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ByteBufferContentSource; import org.eclipse.jetty.io.content.ByteBufferContentSource;
import org.eclipse.jetty.tests.hometester.JettyHomeTester; import org.eclipse.jetty.tests.hometester.JettyHomeTester;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.PathMatchers; import org.eclipse.jetty.toolchain.test.PathMatchers;
import org.eclipse.jetty.util.BlockingArrayQueue; import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
@ -1529,4 +1530,85 @@ public class DistributionTests extends AbstractJettyHomeTest
} }
} }
} }
@ParameterizedTest
@ValueSource(strings = {"ee8", "ee9", "ee10"})
public void testXmlDeployWarNotInWebapps(String env) throws Exception
{
Path jettyBase = newTestJettyBaseDirectory();
String jettyVersion = System.getProperty("jettyVersion");
JettyHomeTester distribution = JettyHomeTester.Builder.newInstance()
.jettyVersion(jettyVersion)
.jettyBase(jettyBase)
.mavenLocalRepository(System.getProperty("mavenRepoPath"))
.build();
int httpPort = distribution.freePort();
String[] argsConfig = {
"--add-modules=http," + toEnvironment("deploy", env) + "," + toEnvironment("webapp", env)
};
try (JettyHomeTester.Run runConfig = distribution.start(argsConfig))
{
assertTrue(runConfig.awaitFor(START_TIMEOUT, TimeUnit.SECONDS));
assertEquals(0, runConfig.getExitValue());
String[] argsStart = {
"jetty.http.port=" + httpPort,
"jetty.httpConfig.port=" + httpPort
};
// Put war into ${jetty.base}/wars/ directory
File srcWar = distribution.resolveArtifact("org.eclipse.jetty." + env + ".demos:jetty-" + env + "-demo-simple-webapp:war:" + jettyVersion);
Path warsDir = jettyBase.resolve("wars");
FS.ensureDirExists(warsDir);
Path destWar = warsDir.resolve("demo.war");
Files.copy(srcWar.toPath(), destWar);
// Create XML for deployable
String xml = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://eclipse.dev/jetty/configure.dtd">
<Configure class="org.eclipse.jetty.%s.webapp.WebAppContext">
<Set name="contextPath">/demo</Set>
<Set name="war">%s</Set>
</Configure>
""".formatted(env, destWar.toString());
Files.writeString(jettyBase.resolve("webapps/demo.xml"), xml, StandardCharsets.UTF_8);
// Specify Environment Properties for this raw XML based deployable
String props = """
environment=%s
""".formatted(env);
Files.writeString(jettyBase.resolve("webapps/demo.properties"), props, StandardCharsets.UTF_8);
/* The jetty.base tree should now look like this
*
* ${jetty.base}
* resources/
* jetty-logging.properties
* start.d/
* ${env}-deploy.ini
* ${env}-webapp.ini
* http.ini
* wars/
* demo.war
* webapps/
* demo.properties
* demo.xml
* work/
*/
try (JettyHomeTester.Run runStart = distribution.start(argsStart))
{
assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS));
startHttpClient();
ContentResponse response = client.GET("http://localhost:" + httpPort + "/demo/index.html");
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
}
} }