Merge remote-tracking branch 'origin/jetty-12.1.x' into jetty-12.1.x-websocketMethodHolder

This commit is contained in:
Lachlan Roberts 2024-08-21 14:20:43 +10:00
commit e486b7161f
No known key found for this signature in database
GPG Key ID: 5663FB7A8FF7E348
80 changed files with 1674 additions and 685 deletions

View File

@ -17,11 +17,9 @@ nav:
- modules/programming-guide/nav.adoc
ext:
collector:
- run:
command: mvn install -ntp -B -Dcollector -Pfast -am -pl documentation/jetty
scan:
dir: documentation/jetty/target/collector
- run: mvn install -ntp -B -Dcollector -Pfast -am -pl documentation/jetty
scan: ./target/collector
- scan:
dir: jetty-core/jetty-server/src/main/java
files: org/eclipse/jetty/server/CustomRequestLog.java
base: modules/code/partials
into: modules/code/partials

View File

@ -1049,6 +1049,31 @@ public class HTTPClientDocs
// end::setConnectionPool[]
}
public void preCreateConnections() throws Exception
{
// tag::preCreateConnections[]
HttpClient httpClient = new HttpClient();
httpClient.start();
// For HTTP/1.1, you need to explicitly configure to initialize connections.
if (httpClient.getTransport() instanceof HttpClientTransportOverHTTP http1)
http1.setInitializeConnections(true);
// Create a dummy request to the server you want to pre-create connections to.
Request request = httpClient.newRequest("https://host/");
// Resolve the destination for that request.
Destination destination = httpClient.resolveDestination(request);
// Pre-create, for example, half of the connections.
int preCreate = httpClient.getMaxConnectionsPerDestination() / 2;
CompletableFuture<Void> completable = destination.getConnectionPool().preCreateConnections(preCreate);
// Wait for the connections to be created.
completable.get(5, TimeUnit.SECONDS);
// end::preCreateConnections[]
}
public void unixDomain() throws Exception
{
// tag::unixDomain[]

View File

@ -95,7 +95,7 @@ public class SessionDocs
org.eclipse.jetty.session.SessionHandler sessionHandler = new org.eclipse.jetty.session.SessionHandler();
sessionHandler.setSessionCookie("SIMPLE");
sessionHandler.setUsingCookies(true);
sessionHandler.setUsingURLs(false);
sessionHandler.setUsingUriParameters(false);
sessionHandler.setSessionPath("/");
server.setHandler(sessionHandler);
sessionHandler.setHandler(new Handler.Abstract()

View File

@ -46,3 +46,4 @@
* Migration Guides
** xref:migration/94-to-10.adoc[]
** xref:migration/11-to-12.adoc[]
** xref:migration/12.0-to-12.1.adoc[]

View File

@ -158,7 +158,7 @@ Jetty's client library provides the following `ConnectionPool` implementations:
* `DuplexConnectionPool`, historically the first implementation, only used by the HTTP/1.1 transport.
* `MultiplexConnectionPool`, the generic implementation valid for any transport where connections are reused with a most recently used algorithm (that is, the connections most recently returned to the connection pool are the more likely to be used again).
* `RoundRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with a round-robin algorithm.
* `RandomRobinConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.
* `RandomConnectionPool`, similar to `MultiplexConnectionPool` but where connections are reused with an algorithm that chooses them randomly.
The `ConnectionPool` implementation can be customized for each destination in by setting a `ConnectionPool.Factory` on the `HttpClientTransport`:
@ -167,6 +167,34 @@ The `ConnectionPool` implementation can be customized for each destination in by
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=setConnectionPool]
----
[[connection-pool-precreate-connections]]
=== Pre-Creating Connections
`ConnectionPool` offers the ability to pre-create connections by calling `ConnectionPool.preCreateConnections(int)`.
Pre-creating the connections saves the time and processing spent to establish the TCP connection, performing the TLS handshake (if necessary) and, for HTTP/2 and HTTP/3, perform the initial protocol setup.
This is particularly important for HTTP/2 because in the initial protocol setup the server informs the client of the maximum number of concurrent requests per connection (otherwise assumed to be just `1` by the client).
The scenarios where pre-creating connections is useful are, for example:
* Load testing, where you want to prepare the system with connections already created to avoid paying of cost of connection setup.
* Proxying scenarios, often in conjunction with the use of `RoundRobinConnectionPool` or `RandomConnectionPool`, where the proxy creates early the connections to the backend servers.
This is an example of how to pre-create connections:
[,java,indent=0]
----
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/client/http/HTTPClientDocs.java[tags=preCreateConnections]
----
[NOTE]
====
Pre-creating connections for secure HTTP/1.1 requires you to call `HttpClientTransportOverHTTP.setInitializeConnections(true)`, otherwise only the TCP connection is established, but the TLS handshake is not initiated.
To initialize connections for secure HTTP/1.1, the client sends an initial `OPTIONS * HTTP/1.1` request to the server.
The server must be able to handle this request without closing the connection (in particular it must not add the `Connection: close` header in the response).
====
[[request-processing]]
== HttpClient Request Processing

View File

@ -99,6 +99,15 @@
| `org.eclipse.jetty.websocket.api.**WebSocketPolicy**` | `org.eclipse.jetty.websocket.api.**Configurable**`
|===
== Server-Side Web Application APIs Changes
Jetty 12 introduced redesigned server-side APIs for web applications.
In Jetty 11, these APIs were based on a mix of Jakarta Servlet APIs and Jetty Handler APIs, while in Jetty 12 they are solely based on Jetty Handler APIs.
In Jetty 12 you can now write web applications independently of the Servlet APIs, so you can migrate Jakarta Servlets to Jetty Handlers as explained in xref:servlet-to-handler[this section].
If you were already using the Jetty 11 Handler APIs, you can migrate them to the Jetty 12 Handler APIs as explained in xref:api-changes-handler[this section].
[[servlet-to-handler]]
== Migrate Servlets to Jetty Handlers
@ -126,6 +135,8 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=requestContent-source]
----
Refer also to the `Content.Source` APIs detailed in xref:arch/io.adoc#content-source[this section].
=== Handler Response APIs
[,java,indent=0]
----
@ -150,9 +161,48 @@ include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration
include::code:example$src/main/java/org/eclipse/jetty/docs/programming/migration/ServletToHandlerDocs.java[tags=responseContent-trailers]
----
Refer also to the `Content.Sink` APIs detailed in xref:arch/io.adoc#content-sink[this section].
[[api-changes]]
== APIs Changes
[[api-changes-handler]]
=== `Handler`
The server-side `Handler` class, and the APIs to use for request/response processing, have been redesigned in Jetty 12.
The Jetty 11 `Handler` method:
`Handler.handle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response)`
has been changed in Jetty 12 to:
`Handler.handle(Request request, Response response, Callback callback)`
The Jetty 11 `target` parameter has been removed, and in Jetty 12 it has been replaced by the information present in `Request.getHttpURI()`.
In Jetty 11, ``Handler``s would mark the fact that they handled the request, and therefore are producing a response, by calling `Request.setHandled(true)`.
In Jetty 12, this is performed by returning `true` from the `Handler.handle(\...)` method, which also requires that the `Callback` parameter must be completed, either by succeeding it or failing it.
In Jetty 11, the `Handler.handle(\...)` method has a blocking semantic, while in Jetty 12 the `Handler.handle(\...)` method has an asynchronous semantic, thanks to the `Callback` parameter.
This means that you can return from the `Handler.handle(\...)` method _before_ the response has been sent, similarly to what you can do with the Servlet APIs when you call `HttpServletRequest.startAsync()`.
Similarly, in Jetty 11 after a call to `startAsync()` you must call `AsyncContext.complete()`, while in Jetty 12 you must complete the `Callback` parameter, either by succeeding it or failing it.
In Jetty 11, `AbstractHandler` provides a utility class to implement `Handler`.
In Jetty 12, use `Handler.Abstract`.
In Jetty 11, the APIs to deal with request or response HTTP headers are based on either Jetty's `HttpFields`, or the Servlet APIs.
In Jetty 12, the HTTP headers API are only based on `HttpFields`.
Please refer to the `HttpFields` link:{javadoc-url}/org/eclipse/jetty/http/HttpFields.html[javadocs] for details.
In Jetty 11, the request content is accessed via `Request.getInputStream()` or `HttpServletRequest.getInputStream()`.
In Jetty 12, the `Request` object itself _is-a_ `Content.Source` that can be read as explained in xref:arch/io.adoc#content-source[this section].
In Jetty 12, you can use `Content.Source.asInputStream(request)` to obtain an `InputStream` and minimize the changes to your code, but remember that `InputStream` only provides blocking APIs, while `Content.Source` provides non-blocking APIs.
In Jetty 11, the response content is accessed via `Response.getOutputStream()` or `HttpServletResponse.getOutputStream()`.
In Jetty 12, the `Response` object itself _is-a_ `Content.Sink` that can be written as explained in xref:arch/io.adoc#content-sink[this section].
In Jetty 12, you can use `Content.Sink.asOutputStream(response)` to obtain an `OutputStream` and minimize the changes to your code, but remember that `OutputStream` only provides blocking APIs, while `Content.Sink` provides non-blocking APIs.
=== `HttpClient`
The Jetty 11 `Request.onResponseContentDemanded(Response.DemandedContentListener)` API has been replaced by `Request.onResponseContentSource(Response.ContentSourceListener)` in Jetty 12.
@ -165,6 +215,8 @@ The Jetty 12 model is a "demand+pull" model: when the content is available, the
For more information about the new model, see xref:arch/io.adoc#content-source[this section].
Jetty 12 introduced the concept of low-level transport for high-level protocols, described in xref:client/io-arch.adoc#transport[this section].
=== WebSocket
The Jetty WebSocket APIs have been vastly simplified, and brought in line with the style of other APIs.

View File

@ -0,0 +1,27 @@
//
// ========================================================================
// 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
// ========================================================================
//
= Migrating from Jetty 12.0.x to Jetty 12.1.x
[[api-changes]]
== APIs Changes
=== `IteratingCallback`
Class `IteratingCallback` underwent refinements that changed the behavior of the `onCompleteFailure(Throwable)` method.
In Jetty 12.0.x, `IteratingCallback.onCompleteFailure(Throwable)` was called as soon as a failure was reported, without waiting the completion of the asynchronous operation (despite its name containing the word "complete").
For example, if a write operation performed with `IteratingCallback` was pending due to TCP congestion, and a timeout happened, `onCompleteFailure(Throwable)` was called as soon as the timeout happened, without waiting for the TCP congestion to resolve.
In Jetty 12.1.x, the same behavior is achieved by `IteratingCallback.onFailure(Throwable)`, so applications should review their usage of `IteratingCallback` and change the overrides of `onCompleteFailure(Throwable)` to override `onFailure(Throwable)` instead.

View File

@ -57,8 +57,7 @@ public class ConscryptHTTP2ClientTest
sslContextFactory.setProvider("Conscrypt");
Conscrypt.setDefaultHostnameVerifier((certs, hostname, session) -> true);
HTTP2Client client = new HTTP2Client();
try
try (HTTP2Client client = new HTTP2Client())
{
client.addBean(sslContextFactory);
client.start();
@ -97,10 +96,6 @@ public class ConscryptHTTP2ClientTest
assertTrue(latch.await(15, TimeUnit.SECONDS));
}
finally
{
client.stop();
}
}
private boolean canConnectTo(String host, int port)

View File

@ -132,17 +132,12 @@ public class ConscryptHTTP2ServerTest
ClientConnector clientConnector = new ClientConnector();
clientConnector.setSslContextFactory(newClientSslContextFactory());
HTTP2Client h2Client = new HTTP2Client(clientConnector);
HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client));
client.start();
try
try (HttpClient client = new HttpClient(new HttpClientTransportOverHTTP2(h2Client)))
{
client.start();
int port = ((ServerConnector)server.getConnectors()[0]).getLocalPort();
ContentResponse contentResponse = client.GET("https://localhost:" + port);
assertEquals(200, contentResponse.getStatus());
}
finally
{
client.stop();
}
}
}

View File

@ -45,8 +45,7 @@ public class JDK9HTTP2ClientTest
Assumptions.assumeTrue(canConnectTo(host, port));
HTTP2Client client = new HTTP2Client();
try
try (HTTP2Client client = new HTTP2Client())
{
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
client.addBean(sslContextFactory);
@ -87,10 +86,6 @@ public class JDK9HTTP2ClientTest
latch.await(15, TimeUnit.SECONDS);
}
finally
{
client.stop();
}
}
private boolean canConnectTo(String host, int port)

View File

@ -39,10 +39,14 @@ public class ContinueProtocolHandler implements ProtocolHandler
@Override
public boolean accept(Request request, Response response)
{
boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100;
boolean expect100 = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
boolean handled100 = request.getAttributes().containsKey(ATTRIBUTE);
return (is100 || expect100) && !handled100;
if (handled100)
return false;
boolean is100 = response.getStatus() == HttpStatus.CONTINUE_100;
if (is100)
return true;
// Also handle non-100 responses, because we need to complete the request to complete the whole exchange.
return request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
}
@Override

View File

@ -104,7 +104,7 @@ import org.slf4j.LoggerFactory;
* }</pre>
*/
@ManagedObject("The HTTP client")
public class HttpClient extends ContainerLifeCycle
public class HttpClient extends ContainerLifeCycle implements AutoCloseable
{
public static final String USER_AGENT = "Jetty/" + Jetty.VERSION;
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -1141,4 +1141,10 @@ public class HttpClient extends ContainerLifeCycle
sslContextFactory = getSslContextFactory();
return new SslClientConnectionFactory(sslContextFactory, getByteBufferPool(), getExecutor(), connectionFactory);
}
@Override
public void close() throws Exception
{
stop();
}
}

View File

@ -26,20 +26,49 @@ public class HttpClientConnectionFactory implements ClientConnectionFactory
/**
* <p>Representation of the {@code HTTP/1.1} application protocol used by {@link HttpClientTransportDynamic}.</p>
*/
public static final Info HTTP11 = new HTTP11(new HttpClientConnectionFactory());
public static final Info HTTP11 = new HTTP11();
private boolean initializeConnections;
/**
* @return whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public boolean isInitializeConnections()
{
return initializeConnections;
}
/**
* @param initialize whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public void setInitializeConnections(boolean initialize)
{
this.initializeConnections = initialize;
}
@Override
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context)
{
HttpConnectionOverHTTP connection = new HttpConnectionOverHTTP(endPoint, context);
connection.setInitialize(isInitializeConnections());
return customize(connection, context);
}
private static class HTTP11 extends Info
/**
* <p>Representation of the {@code HTTP/1.1} application protocol used by {@link HttpClientTransportDynamic}.</p>
* <p>Applications should prefer using the constant {@link HttpClientConnectionFactory#HTTP11}, unless they
* need to customize the associated {@link HttpClientConnectionFactory}.</p>
*/
public static class HTTP11 extends Info
{
private static final List<String> protocols = List.of("http/1.1");
private HTTP11(ClientConnectionFactory factory)
public HTTP11()
{
this(new HttpClientConnectionFactory());
}
public HTTP11(ClientConnectionFactory factory)
{
super(factory);
}

View File

@ -22,7 +22,6 @@ import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.DuplexConnectionPool;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.io.ClientConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ProcessorUtils;
@ -37,7 +36,7 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
public static final Origin.Protocol HTTP11 = new Origin.Protocol(List.of("http/1.1"), false);
private static final Logger LOG = LoggerFactory.getLogger(HttpClientTransportOverHTTP.class);
private final ClientConnectionFactory factory = new HttpClientConnectionFactory();
private final HttpClientConnectionFactory factory = new HttpClientConnectionFactory();
private int headerCacheSize = 1024;
private boolean headerCacheCaseSensitive;
@ -79,25 +78,54 @@ public class HttpClientTransportOverHTTP extends AbstractConnectorHttpClientTran
return connection;
}
@ManagedAttribute("The maximum allowed size in bytes for an HTTP header field cache")
/**
* @return the max size in bytes for the HTTP header field cache
*/
@ManagedAttribute("The maximum allowed size in bytes for the HTTP header field cache")
public int getHeaderCacheSize()
{
return headerCacheSize;
}
/**
* @param headerCacheSize the max size in bytes for the HTTP header field cache
*/
public void setHeaderCacheSize(int headerCacheSize)
{
this.headerCacheSize = headerCacheSize;
}
@ManagedAttribute("Whether the header field cache is case sensitive")
/**
* @return whether the HTTP header field cache is case-sensitive
*/
@ManagedAttribute("Whether the HTTP header field cache is case-sensitive")
public boolean isHeaderCacheCaseSensitive()
{
return headerCacheCaseSensitive;
}
/**
* @param headerCacheCaseSensitive whether the HTTP header field cache is case-sensitive
*/
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
this.headerCacheCaseSensitive = headerCacheCaseSensitive;
}
/**
* @return whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
@ManagedAttribute("Whether newly created connections should be initialized with an OPTIONS * HTTP/1.1 request")
public boolean isInitializeConnections()
{
return factory.isInitializeConnections();
}
/**
* @param initialize whether newly created connections should be initialized with an {@code OPTIONS * HTTP/1.1} request
*/
public void setInitializeConnections(boolean initialize)
{
factory.setInitializeConnections(initialize);
}
}

View File

@ -20,7 +20,6 @@ import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.Authentication;
import org.eclipse.jetty.client.AuthenticationStore;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpProxy;
import org.eclipse.jetty.client.HttpRequestException;
@ -35,6 +34,7 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.io.CyclicTimeouts;
import org.eclipse.jetty.util.Attachable;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
@ -146,7 +146,7 @@ public abstract class HttpConnection implements IConnection, Attachable
// Make sure the path is there
String path = request.getPath();
if (path.trim().length() == 0)
if (StringUtil.isBlank(path))
{
path = "/";
request.path(path);
@ -191,11 +191,7 @@ public abstract class HttpConnection implements IConnection, Attachable
// Add content headers.
Request.Content content = request.getBody();
if (content == null)
{
request.body(new BytesRequestContent());
}
else
if (content != null)
{
if (!headers.contains(HttpHeader.CONTENT_TYPE))
{
@ -203,10 +199,7 @@ public abstract class HttpConnection implements IConnection, Attachable
if (contentType == null)
contentType = getHttpClient().getDefaultRequestContentType();
if (contentType != null)
{
HttpField field = new HttpField(HttpHeader.CONTENT_TYPE, contentType);
request.addHeader(field);
}
request.addHeader(new HttpField(HttpHeader.CONTENT_TYPE, contentType));
}
long contentLength = content.getLength();
if (contentLength >= 0)
@ -215,6 +208,9 @@ public abstract class HttpConnection implements IConnection, Attachable
request.addHeader(new HttpField.LongValueHttpField(HttpHeader.CONTENT_LENGTH, contentLength));
}
}
// RFC 9110, section 10.1.1.
if (content == null || content.getLength() == 0)
request.headers(h -> h.remove(HttpHeader.EXPECT));
// Cookies.
StringBuilder cookies = convertCookies(request.getCookies(), null);
@ -243,7 +239,7 @@ public abstract class HttpConnection implements IConnection, Attachable
{
if (builder == null)
builder = new StringBuilder();
if (builder.length() > 0)
if (!builder.isEmpty())
builder.append("; ");
builder.append(cookie.getName()).append("=").append(cookie.getValue());
}

View File

@ -315,13 +315,35 @@ public abstract class HttpReceiver
* Method to be invoked when response content is available to be read.
* <p>
* This method takes care of ensuring the {@link Content.Source} passed to
* {@link Response.ContentSourceListener#onContentSource(Response, Content.Source)} calls the
* demand callback.
* {@link Response.ContentSourceListener#onContentSource(Response, Content.Source)}
* calls the demand callback.
* The call to the demand callback is serialized with other events.
*/
protected void responseContentAvailable(HttpExchange exchange)
{
if (LOG.isDebugEnabled())
LOG.debug("Invoking responseContentAvailable on {}", this);
invoker.run(() ->
{
if (LOG.isDebugEnabled())
LOG.debug("Executing responseContentAvailable on {}", this);
if (exchange.isResponseCompleteOrTerminated())
return;
responseContentAvailable();
});
}
/**
* Method to be invoked when response content is available to be read.
* <p>
* This method directly invokes the demand callback, assuming the caller
* is already serialized with other events.
*/
protected void responseContentAvailable()
{
if (LOG.isDebugEnabled())
LOG.debug("Response content available on {}", this);
contentSource.onDataAvailable();
}
@ -344,7 +366,7 @@ public abstract class HttpReceiver
if (!exchange.responseComplete(null))
return;
invoker.run(() ->
Runnable successTask = () ->
{
if (LOG.isDebugEnabled())
LOG.debug("Executing responseSuccess on {}", this);
@ -365,7 +387,12 @@ public abstract class HttpReceiver
// Mark atomically the response as terminated, with
// respect to concurrency between request and response.
terminateResponse(exchange);
}, afterSuccessTask);
};
if (afterSuccessTask == null)
invoker.run(successTask);
else
invoker.run(successTask, afterSuccessTask);
}
/**
@ -712,9 +739,9 @@ public abstract class HttpReceiver
{
if (LOG.isDebugEnabled())
LOG.debug("onDataAvailable on {}", this);
// The demandCallback will call read() that will itself call
// HttpReceiver.read(boolean) so it must be called by the invoker.
invokeDemandCallback(true);
// The onDataAvailable() method is only ever called
// by the invoker so avoid using the invoker again.
invokeDemandCallback(false);
}
@Override
@ -760,8 +787,8 @@ public abstract class HttpReceiver
}
}
// The processDemand method is only ever called by the
// invoker so there is no need to use the latter here.
// The processDemand() method is only ever called
// by the invoker so avoid using the invoker again.
invokeDemandCallback(false);
}
@ -769,20 +796,19 @@ public abstract class HttpReceiver
{
Runnable demandCallback = demandCallbackRef.getAndSet(null);
if (LOG.isDebugEnabled())
LOG.debug("Invoking demand callback on {}", this);
if (demandCallback != null)
LOG.debug("Invoking demand callback {} on {}", demandCallback, this);
if (demandCallback == null)
return;
try
{
try
{
if (invoke)
invoker.run(demandCallback);
else
demandCallback.run();
}
catch (Throwable x)
{
fail(x);
}
if (invoke)
invoker.run(demandCallback);
else
demandCallback.run();
}
catch (Throwable x)
{
fail(x);
}
}

View File

@ -528,7 +528,7 @@ public abstract class HttpSender
action.run();
// Read the request content.
chunk = content.read();
chunk = content != null ? content.read() : Content.Chunk.EOF;
}
if (LOG.isDebugEnabled())
LOG.debug("Content {} for {}", chunk, request);
@ -539,6 +539,7 @@ public abstract class HttpSender
{
// No content after the headers, demand.
demanded = true;
assert content != null;
content.demand(this::succeeded);
return Action.SCHEDULED;
}

View File

@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.HttpClientTransport;
import org.eclipse.jetty.client.HttpUpgrader;
import org.eclipse.jetty.client.Request;
@ -40,6 +41,7 @@ import org.eclipse.jetty.client.transport.HttpRequest;
import org.eclipse.jetty.client.transport.IConnection;
import org.eclipse.jetty.client.transport.SendFailure;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.EndPoint;
@ -61,6 +63,7 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
private final LongAdder bytesIn = new LongAdder();
private final LongAdder bytesOut = new LongAdder();
private long idleTimeout;
private boolean initialize;
public HttpConnectionOverHTTP(EndPoint endPoint, Map<String, Object> context)
{
@ -159,12 +162,46 @@ public class HttpConnectionOverHTTP extends AbstractConnection implements IConne
return delegate.send(exchange);
}
/**
* @return whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public boolean isInitialize()
{
return initialize;
}
/**
* @param initialize whether to initialize the connection with an {@code OPTIONS * HTTP/1.1} request.
*/
public void setInitialize(boolean initialize)
{
this.initialize = initialize;
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
promise.succeeded(this);
boolean initialize = isInitialize();
if (initialize)
{
Destination destination = getHttpDestination();
Request request = destination.getHttpClient().newRequest(destination.getOrigin().asString())
.method(HttpMethod.OPTIONS)
.path("*");
send(request, result ->
{
if (result.isSucceeded())
promise.succeeded(this);
else
promise.failed(result.getFailure());
});
}
else
{
promise.succeeded(this);
}
}
@Override

View File

@ -79,7 +79,9 @@ public class HttpReceiverOverHTTP extends HttpReceiver implements HttpParser.Res
}
else
{
responseContentAvailable();
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseContentAvailable(exchange);
}
}

View File

@ -703,7 +703,7 @@ public class ConnectionPoolTest
assertThat(connectionPool.toString(), not(nullValue()));
}
private static class ConnectionPoolFactory
public static class ConnectionPoolFactory
{
private final String name;
private final ConnectionPool.Factory factory;

View File

@ -31,12 +31,11 @@ public class HttpClientJMXTest
@Test
public void testHttpClientName() throws Exception
{
String name = "foo";
HttpClient httpClient = new HttpClient();
httpClient.setName(name);
try
try (HttpClient httpClient = new HttpClient())
{
String name = "foo";
httpClient.setName(name);
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
MBeanContainer mbeanContainer = new MBeanContainer(mbeanServer);
// Adding MBeanContainer as a bean will trigger the registration of MBeans.
@ -59,9 +58,5 @@ public class HttpClientJMXTest
assertEquals(name, oName.getKeyProperty("context"));
}
}
finally
{
httpClient.stop();
}
}
}

View File

@ -40,7 +40,9 @@ public class HttpReceiverOverFCGI extends HttpReceiver
}
else
{
responseContentAvailable();
HttpExchange exchange = getHttpExchange();
if (exchange != null)
responseContentAvailable(exchange);
}
}
@ -107,6 +109,9 @@ public class HttpReceiverOverFCGI extends HttpReceiver
void content(Content.Chunk chunk)
{
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return;
if (this.chunk != null)
throw new IllegalStateException();
// Retain the chunk because it is stored for later reads.

View File

@ -50,8 +50,6 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
{
private static final Logger LOG = LoggerFactory.getLogger(HttpReceiverOverHTTP2.class);
private final Runnable onDataAvailableTask = new Invocable.ReadyTask(Invocable.InvocationType.NON_BLOCKING, this::responseContentAvailable);
public HttpReceiverOverHTTP2(HttpChannel channel)
{
super(channel);
@ -213,7 +211,7 @@ public class HttpReceiverOverHTTP2 extends HttpReceiver implements HTTP2Channel.
HttpExchange exchange = getHttpExchange();
if (exchange == null)
return null;
return onDataAvailableTask;
return new Invocable.ReadyTask(Invocable.InvocationType.NON_BLOCKING, () -> responseContentAvailable(exchange));
}
@Override

View File

@ -100,7 +100,7 @@ import org.eclipse.jetty.util.thread.Scheduler;
*} </pre>
*/
@ManagedObject
public class HTTP2Client extends ContainerLifeCycle
public class HTTP2Client extends ContainerLifeCycle implements AutoCloseable
{
private final ClientConnector connector;
private int inputBufferSize = IO.DEFAULT_BUFFER_SIZE;
@ -493,4 +493,10 @@ public class HTTP2Client extends ContainerLifeCycle
}
return factory;
}
@Override
public void close() throws Exception
{
stop();
}
}

View File

@ -81,7 +81,6 @@
<skip>${h2spec.skip}</skip>
<junitPackage>org.eclipse.jetty.h2spec</junitPackage>
<skipNoDockerAvailable>true</skipNoDockerAvailable>
<reportsDirectory>${project.build.directory}/h2spec-reports</reportsDirectory>
<excludeSpecs>
<excludeSpec>3.5 - Sends invalid connection preface</excludeSpec>
</excludeSpecs>

View File

@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
@ -33,6 +34,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;
import org.eclipse.jetty.client.AsyncRequestContent;
import org.eclipse.jetty.client.CompletableResponseListener;
import org.eclipse.jetty.client.Connection;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.Destination;
@ -103,27 +106,26 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
public void testPropertiesAreForwarded() throws Exception
{
HTTP2Client http2Client = new HTTP2Client();
HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client));
Executor executor = new QueuedThreadPool();
httpClient.setExecutor(executor);
httpClient.setConnectTimeout(13);
httpClient.setIdleTimeout(17);
httpClient.setUseInputDirectByteBuffers(false);
httpClient.setUseOutputDirectByteBuffers(false);
try (HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)))
{
Executor executor = new QueuedThreadPool();
httpClient.setExecutor(executor);
httpClient.setConnectTimeout(13);
httpClient.setIdleTimeout(17);
httpClient.setUseInputDirectByteBuffers(false);
httpClient.setUseOutputDirectByteBuffers(false);
httpClient.start();
assertTrue(http2Client.isStarted());
assertSame(httpClient.getExecutor(), http2Client.getExecutor());
assertSame(httpClient.getScheduler(), http2Client.getScheduler());
assertSame(httpClient.getByteBufferPool(), http2Client.getByteBufferPool());
assertEquals(httpClient.getConnectTimeout(), http2Client.getConnectTimeout());
assertEquals(httpClient.getIdleTimeout(), http2Client.getIdleTimeout());
assertEquals(httpClient.isUseInputDirectByteBuffers(), http2Client.isUseInputDirectByteBuffers());
assertEquals(httpClient.isUseOutputDirectByteBuffers(), http2Client.isUseOutputDirectByteBuffers());
httpClient.stop();
httpClient.start();
assertTrue(http2Client.isStarted());
assertSame(httpClient.getExecutor(), http2Client.getExecutor());
assertSame(httpClient.getScheduler(), http2Client.getScheduler());
assertSame(httpClient.getByteBufferPool(), http2Client.getByteBufferPool());
assertEquals(httpClient.getConnectTimeout(), http2Client.getConnectTimeout());
assertEquals(httpClient.getIdleTimeout(), http2Client.getIdleTimeout());
assertEquals(httpClient.isUseInputDirectByteBuffers(), http2Client.isUseInputDirectByteBuffers());
assertEquals(httpClient.isUseOutputDirectByteBuffers(), http2Client.isUseOutputDirectByteBuffers());
}
assertTrue(http2Client.isStopped());
}
@ -790,22 +792,53 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
assertThat(onContentSourceErrorRef.get(), is(nullValue()));
}
@Test
public void testRequestContentResponseContent() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
Content.copy(request, response, callback);
return true;
}
});
AsyncRequestContent content = new AsyncRequestContent();
var request = httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.body(content);
CompletableFuture<ContentResponse> completable = new CompletableResponseListener(request).send();
for (int i = 0; i < 16; ++i)
{
content.write(false, ByteBuffer.allocate(512), Callback.NOOP);
Thread.sleep(10);
}
content.close();
ContentResponse response = completable.get(15, TimeUnit.SECONDS);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
@Tag("external")
public void testExternalServer() throws Exception
{
ClientConnector clientConnector = new ClientConnector();
HTTP2Client http2Client = new HTTP2Client(clientConnector);
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
clientConnector.setSslContextFactory(sslContextFactory);
HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client));
Executor executor = new QueuedThreadPool();
clientConnector.setExecutor(executor);
httpClient.start();
try (HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP2(http2Client)))
{
Executor executor = new QueuedThreadPool();
clientConnector.setExecutor(executor);
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
clientConnector.setSslContextFactory(sslContextFactory);
httpClient.start();
ContentResponse response = httpClient.GET("https://webtide.com/");
assertEquals(HttpStatus.OK_200, response.getStatus());
httpClient.stop();
ContentResponse response = httpClient.GET("https://webtide.com/");
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
}

View File

@ -73,10 +73,9 @@ public class ResponseTrailerTest extends AbstractTest
}
});
HTTP2Client http2Client = new HTTP2Client();
http2Client.start();
try
try (HTTP2Client http2Client = new HTTP2Client())
{
http2Client.start();
String host = "localhost";
int port = connector.getLocalPort();
InetSocketAddress address = new InetSocketAddress(host, port);
@ -116,9 +115,5 @@ public class ResponseTrailerTest extends AbstractTest
assertNotNull(frame);
assertTrue(frame.getMetaData().isResponse());
}
finally
{
http2Client.stop();
}
}
}

View File

@ -127,7 +127,7 @@ public class HttpReceiverOverHTTP3 extends HttpReceiver implements Stream.Client
if (exchange == null)
return;
responseContentAvailable();
responseContentAvailable(exchange);
}
@Override

View File

@ -125,7 +125,7 @@ import org.slf4j.LoggerFactory;
* <p>HTTP/3+QUIC support is experimental and not suited for production use.
* APIs may change incompatibly between releases.</p>
*/
public class HTTP3Client extends ContainerLifeCycle
public class HTTP3Client extends ContainerLifeCycle implements AutoCloseable
{
public static final String CLIENT_CONTEXT_KEY = HTTP3Client.class.getName();
public static final String SESSION_LISTENER_CONTEXT_KEY = CLIENT_CONTEXT_KEY + ".listener";
@ -217,4 +217,10 @@ public class HTTP3Client extends ContainerLifeCycle
{
return container.shutdown();
}
@Override
public void close() throws Exception
{
stop();
}
}

View File

@ -54,19 +54,13 @@ public class ExternalServerTest
SslContextFactory.Client sslClient = new SslContextFactory.Client();
ClientQuicConfiguration quicConfig = new ClientQuicConfiguration(sslClient, null);
HTTP3Client client = new HTTP3Client(quicConfig);
HttpClientTransportOverHTTP3 transport = new HttpClientTransportOverHTTP3(client);
HttpClient httpClient = new HttpClient(transport);
httpClient.start();
try
try (HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP3(client)))
{
httpClient.start();
URI uri = URI.create("https://maven-central-eu.storage-download.googleapis.com/maven2/org/apache/maven/maven-parent/38/maven-parent-38.pom");
ContentResponse response = httpClient.newRequest(uri).send();
assertThat(response.getContentAsString(), containsString("<artifactId>maven-parent</artifactId>"));
}
finally
{
httpClient.stop();
}
}
@Test
@ -75,10 +69,9 @@ public class ExternalServerTest
{
SslContextFactory.Client sslClient = new SslContextFactory.Client();
ClientQuicConfiguration quicConfig = new ClientQuicConfiguration(sslClient, null);
HTTP3Client client = new HTTP3Client(quicConfig);
client.start();
try
try (HTTP3Client client = new HTTP3Client(quicConfig))
{
client.start();
// Well-known HTTP/3 servers to try.
// HostPort hostPort = new HostPort("maven-central-eu.storage-download.googleapis.com:443");
HostPort hostPort = new HostPort("google.com:443");
@ -137,9 +130,5 @@ public class ExternalServerTest
assertTrue(requestLatch.await(5, TimeUnit.SECONDS));
}
finally
{
client.stop();
}
}
}

View File

@ -444,14 +444,12 @@ public class OpenIdAuthenticator extends LoginAuthenticator
return AuthenticationState.SEND_FAILURE;
}
// TODO: No session API to work this out?
/*
if (request.isRequestedSessionIdFromURL())
String sessionIdFrom = (String)request.getAttribute("org.eclipse.jetty.session.RequestedSession.sessionIdFrom");
if (sessionIdFrom != null && !sessionIdFrom.startsWith("cookie"))
{
sendError(req, res, cb, "Session ID must be a cookie to support OpenID authentication");
return Authentication.SEND_FAILURE;
sendError(request, response, cb, "Session ID must be a cookie to support OpenID authentication");
return AuthenticationState.SEND_FAILURE;
}
*/
// Handle a request for authentication.
if (isJSecurityCheck(uri))

View File

@ -393,11 +393,12 @@ public abstract class ProxyHandler extends Handler.Abstract
private boolean hasContent(Request clientToProxyRequest)
{
if (clientToProxyRequest.getLength() > 0)
long contentLength = clientToProxyRequest.getLength();
if (contentLength == 0)
return false;
if (contentLength > 0)
return true;
HttpFields headers = clientToProxyRequest.getHeaders();
return headers.get(HttpHeader.CONTENT_TYPE) != null ||
headers.get(HttpHeader.TRANSFER_ENCODING) != null;
return clientToProxyRequest.getHeaders().get(HttpHeader.TRANSFER_ENCODING) != null;
}
private boolean expects100Continue(Request clientToProxyRequest)

View File

@ -396,5 +396,4 @@ public class JAASLdapLoginServiceTest extends AbstractLdapTestUnit
return null;
}
}
}

View File

@ -165,6 +165,7 @@ public class HttpConfiguration implements Dumpable
_redirectUriCompliance = config._redirectUriCompliance;
_serverAuthority = config._serverAuthority;
_localAddress = config._localAddress;
_maxUnconsumedRequestContentReads = config._maxUnconsumedRequestContentReads;
}
/**
@ -501,8 +502,9 @@ public class HttpConfiguration implements Dumpable
/**
* @return the set of HTTP methods of requests that can be decoded as
* {@code x-www-form-urlencoded} content.
* {@code application/x-www-form-urlencoded} content.
*/
@ManagedAttribute("The methods that support application/x-www-form-urlencoded content")
public Set<String> getFormEncodedMethods()
{
return _formEncodedMethods.keySet();
@ -587,6 +589,7 @@ public class HttpConfiguration implements Dumpable
_minResponseDataRate = bytesPerSecond;
}
@ManagedAttribute("The HTTP compliance mode")
public HttpCompliance getHttpCompliance()
{
return _httpCompliance;
@ -597,6 +600,7 @@ public class HttpConfiguration implements Dumpable
_httpCompliance = httpCompliance;
}
@ManagedAttribute("The URI compliance mode")
public UriCompliance getUriCompliance()
{
return _uriCompliance;
@ -624,6 +628,7 @@ public class HttpConfiguration implements Dumpable
* @return The CookieCompliance used for parsing request {@code Cookie} headers.
* @see #getResponseCookieCompliance()
*/
@ManagedAttribute("The HTTP request cookie compliance mode")
public CookieCompliance getRequestCookieCompliance()
{
return _requestCookieCompliance;
@ -641,6 +646,7 @@ public class HttpConfiguration implements Dumpable
* @return The CookieCompliance used for generating response {@code Set-Cookie} headers
* @see #getRequestCookieCompliance()
*/
@ManagedAttribute("The HTTP response cookie compliance mode")
public CookieCompliance getResponseCookieCompliance()
{
return _responseCookieCompliance;
@ -657,6 +663,7 @@ public class HttpConfiguration implements Dumpable
/**
* @return the {@link MultiPartCompliance} used for validating multipart form syntax.
*/
@ManagedAttribute("The multipart/form-data compliance mode")
public MultiPartCompliance getMultiPartCompliance()
{
return _multiPartCompliance;
@ -784,7 +791,7 @@ public class HttpConfiguration implements Dumpable
*
* @return Returns the connection server authority (name/port) or null
*/
@ManagedAttribute("The server authority if none provided by requests")
@ManagedAttribute("The server authority override")
public HostPort getServerAuthority()
{
return _serverAuthority;
@ -852,6 +859,7 @@ public class HttpConfiguration implements Dumpable
"requestHeaderSize=" + _requestHeaderSize,
"responseHeaderSize=" + _responseHeaderSize,
"headerCacheSize=" + _headerCacheSize,
"headerCacheCaseSensitive=" + _headerCacheCaseSensitive,
"secureScheme=" + _secureScheme,
"securePort=" + _securePort,
"idleTimeout=" + _idleTimeout,
@ -861,12 +869,21 @@ public class HttpConfiguration implements Dumpable
"delayDispatchUntilContent=" + _delayDispatchUntilContent,
"persistentConnectionsEnabled=" + _persistentConnectionsEnabled,
"maxErrorDispatches=" + _maxErrorDispatches,
"useInputDirectByteBuffers=" + _useInputDirectByteBuffers,
"useOutputDirectByteBuffers=" + _useOutputDirectByteBuffers,
"minRequestDataRate=" + _minRequestDataRate,
"minResponseDataRate=" + _minResponseDataRate,
"httpCompliance=" + _httpCompliance,
"uriCompliance=" + _uriCompliance,
"redirectUriCompliance=" + _redirectUriCompliance,
"requestCookieCompliance=" + _requestCookieCompliance,
"responseCookieCompliance=" + _responseCookieCompliance,
"multiPartCompliance=" + _multiPartCompliance,
"notifyRemoteAsyncErrors=" + _notifyRemoteAsyncErrors,
"relativeRedirectAllowed=" + _relativeRedirectAllowed
"relativeRedirectAllowed=" + _relativeRedirectAllowed,
"serverAuthority=" + _serverAuthority,
"localAddress=" + _localAddress,
"maxUnconsumedRequestContentReads=" + _maxUnconsumedRequestContentReads
);
}

View File

@ -1117,8 +1117,6 @@ public class HttpChannelState implements HttpChannel, Components
*/
public static class ChannelResponse implements Response, Callback
{
private static final CompletableFuture<Void> UNEXPECTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("100 not expected"));
private static final CompletableFuture<Void> COMMITTED_100_CONTINUE = CompletableFuture.failedFuture(new IllegalStateException("Committed"));
private final ChannelRequest _request;
private final ResponseHttpFields _httpFields;
protected int _status;
@ -1408,12 +1406,14 @@ public class HttpChannelState implements HttpChannel, Components
if (status == HttpStatus.CONTINUE_100)
{
if (!httpChannelState._expects100Continue)
return UNEXPECTED_100_CONTINUE;
return CompletableFuture.failedFuture(new IllegalStateException("100 not expected"));
if (_request.getLength() == 0)
return CompletableFuture.completedFuture(null);
httpChannelState._expects100Continue = false;
}
if (_httpFields.isCommitted())
return status == HttpStatus.CONTINUE_100 ? COMMITTED_100_CONTINUE : CompletableFuture.failedFuture(new IllegalStateException("Committed"));
return CompletableFuture.failedFuture(new IllegalStateException("Committed"));
if (_writeCallback != null)
return CompletableFuture.failedFuture(new WritePendingException());

View File

@ -40,6 +40,7 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
@ -981,24 +982,6 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
_usingUriParameters = usingUriParameters;
}
/**
* @deprecated use {@link #isUsingUriParameters()} instead, will be removed in Jetty 12.1.0
*/
@Deprecated(since = "12.0.1", forRemoval = true)
public boolean isUsingURLs()
{
return isUsingUriParameters();
}
/**
* @deprecated use {@link #setUsingUriParameters(boolean)} instead, will be removed in Jetty 12.1.0
*/
@Deprecated(since = "12.0.1", forRemoval = true)
public void setUsingURLs(boolean usingURLs)
{
setUsingUriParameters(usingURLs);
}
/**
* Create a new Session, using the requested session id if possible.
* @param request the inbound request
@ -1229,7 +1212,7 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
{
//Cookie[] cookies = request.getCookies();
List<HttpCookie> cookies = Request.getCookies(request);
if (cookies != null && cookies.size() > 0)
if (!cookies.isEmpty())
{
final String sessionCookie = getSessionCookie();
for (HttpCookie cookie : cookies)
@ -1279,7 +1262,7 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
}
if (ids == null)
return NO_REQUESTED_SESSION;
return RequestedSession.NO_REQUESTED_SESSION;
if (LOG.isDebugEnabled())
LOG.debug("Got Session IDs {} from cookies {}", ids, cookieIds);
@ -1319,8 +1302,7 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
{
//we already have a valid session and now have a duplicate ID for it
if (LOG.isDebugEnabled())
LOG.debug(duplicateSession(
requestedSessionId, true, requestedSessionIdFromCookie,
LOG.debug(duplicateSession(requestedSessionId, requestedSessionIdFromCookie,
id, false, i < cookieIds));
}
else
@ -1350,26 +1332,27 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
}
throw new BadMessageException(duplicateSession(
requestedSessionId, true, requestedSessionIdFromCookie,
requestedSessionId, requestedSessionIdFromCookie,
id, true, i < cookieIds));
}
else if (LOG.isDebugEnabled())
{
LOG.debug(duplicateSession(
requestedSessionId, true, requestedSessionIdFromCookie,
requestedSessionId, requestedSessionIdFromCookie,
id, false, i < cookieIds));
}
}
}
return new RequestedSession((session != null && session.isValid()) ? session : null, requestedSessionId, requestedSessionIdFromCookie);
return new RequestedSession((session != null && session.isValid()) ? session : null, requestedSessionId,
requestedSessionIdFromCookie ? RequestedSession.ID_FROM_COOKIE : RequestedSession.ID_FROM_URI_PARAMETER);
}
private static String duplicateSession(String id0, boolean valid0, boolean cookie0, String id1, boolean valid1, boolean cookie1)
private static String duplicateSession(String id0, boolean fromCookie0, String id1, boolean valid1, boolean fromCookie1)
{
return "Duplicate sessions: %s[%s,%s] & %s[%s,%s]".formatted(
id0, valid0 ? "valid" : "unknown", cookie0 ? "cookie" : "param",
id1, valid1 ? "valid" : "unknown", cookie1 ? "cookie" : "param");
id0, "valid", fromCookie0 ? RequestedSession.ID_FROM_COOKIE : RequestedSession.ID_FROM_URI_PARAMETER,
id1, valid1 ? "valid" : "unknown", fromCookie1 ? RequestedSession.ID_FROM_COOKIE : RequestedSession.ID_FROM_URI_PARAMETER);
}
/**
@ -1379,12 +1362,89 @@ public abstract class AbstractSessionManager extends ContainerLifeCycle implemen
{
_sessionCache.shutdown();
}
public record RequestedSession(ManagedSession session, String sessionId, boolean sessionIdFromCookie)
{
}
private static final RequestedSession NO_REQUESTED_SESSION = new RequestedSession(null, null, false);
/**
* Details of the requested session.
* Session implementations should make an instance of this record available as a hidden (not in name set) request
* attribute for the name "org.eclipse.jetty.session.AbstractSessionManager$RequestedSession"
* @param session The {@link Session} associated with the ID, which may have been invalidated or changed ID since the
* request was received; or {@code null} if no session existed matching the requested ID.
* @param sessionId The requested session ID.
* @param sessionIdFrom A {@link String} representing the source of the session ID. Common values include:
* {@link #ID_FROM_COOKIE} or {@link #ID_FROM_URI_PARAMETER} if there is no ID.
*/
public record RequestedSession(ManagedSession session, String sessionId, String sessionIdFrom)
{
public static final RequestedSession NO_REQUESTED_SESSION = new RequestedSession(null, null, null);
public static final String ATTRIBUTE = "org.eclipse.jetty.session.RequestedSession";
public static final String ID_FROM_COOKIE = "cookie";
public static final String ID_FROM_URI_PARAMETER = "uri";
/**
* Get the {@code RequestedSession} by attribute
* @param request The attributes to query
* @return The found {@code RequestedSession} or {@link #NO_REQUESTED_SESSION} if none found. Never {@code null}.
*/
public static RequestedSession byAttribute(Attributes request)
{
RequestedSession requestedSession = (RequestedSession)request.getAttribute(ATTRIBUTE);
return requestedSession == null ? NO_REQUESTED_SESSION : requestedSession;
}
/**
* @param name An attribute name
* @return {@code true} if the attribute name is applicable to a requested session.
* @see #getAttribute(String)
*/
public static boolean isApplicableAttribute(String name)
{
return name != null && name.startsWith(ATTRIBUTE);
}
/**
* Get attributes asssociated with this requested session:
* <ul>
* <li>`org.eclipse.jetty.session.RequestedSession` this instance.</li>
* <li>`org.eclipse.jetty.session.RequestedSession.session` the {@link #session()}.</li>
* <li>`org.eclipse.jetty.session.RequestedSession.sessionId` the {@link #sessionId()}.</li>
* <li>`org.eclipse.jetty.session.RequestedSession.sessionIdFrom` the {@link #sessionIdFrom()}.</li>
* </ul>
* @param name An attributed name
* @return the attribute value or {@code null}
*/
public Object getAttribute(String name)
{
if (name == null || name.length() < ATTRIBUTE.length())
return null;
if (ATTRIBUTE.equals(name))
return this;
if (name.startsWith(ATTRIBUTE) && name.charAt(ATTRIBUTE.length()) == '.')
{
return switch (name.substring(ATTRIBUTE.length() + 1))
{
case "session" -> session();
case "sessionId" -> sessionId();
case "sessionIdFrom" -> sessionIdFrom();
default -> null;
};
}
return null;
}
/**
* Test if this {@code RequestedSession} ID is from a particular session source
* @param source A {@link String} representing the source of the session ID. Common values include:
* {@link #ID_FROM_COOKIE} or {@link #ID_FROM_URI_PARAMETER} if there is no ID.
* @return {@code True} iff this {@code RequestedSession} ID is from the source.
*/
public boolean isSessionIdFrom(String source)
{
return source != null && source.equals(sessionIdFrom);
}
}
/**
* A session cookie is marked as secure IFF any of the following conditions are true:

View File

@ -82,10 +82,10 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
return null;
}
private class SessionRequest extends Request.Wrapper
public class SessionRequest extends Request.Wrapper
{
private final AtomicReference<ManagedSession> _session = new AtomicReference<>();
private String _requestedSessionId;
RequestedSession _requestedSession;
private Response _response;
public SessionRequest(Request request)
@ -103,6 +103,14 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
return _session.get();
}
@Override
public Object getAttribute(String name)
{
if (RequestedSession.isApplicableAttribute(name))
return _requestedSession.getAttribute(name);
return super.getAttribute(name);
}
@Override
public Session getSession(boolean create)
{
@ -113,7 +121,7 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
if (session == null && create)
{
newSession(this, _requestedSessionId, this::setManagedSession);
newSession(this, _requestedSession.sessionId(), this::setManagedSession);
session = _session.get();
HttpCookie cookie = getSessionCookie(session, getConnectionMetaData().isSecure());
if (cookie != null)
@ -126,10 +134,8 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
public boolean process(Handler handler, Response response, Callback callback) throws Exception
{
_response = response;
RequestedSession requestedSession = resolveRequestedSessionId(this);
_requestedSessionId = requestedSession.sessionId();
ManagedSession session = requestedSession.session();
_requestedSession = resolveRequestedSessionId(this);
ManagedSession session = _requestedSession.session();
if (session != null)
{

View File

@ -25,6 +25,7 @@ import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager.RequestedSession;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -148,8 +149,21 @@ public class SessionHandlerTest
{
if (session.isNew())
out.append("New\n");
RequestedSession requestedSession = RequestedSession.byAttribute(request);
out.append("RequestedSessionIdFromCookie: ")
.append(requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_COOKIE))
.append('\n');
out.append("RequestedSessionIdFromURL: ")
.append(requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_URI_PARAMETER))
.append('\n');
for (String name : session.getAttributeNameSet())
out.append("Attribute ").append(name).append(" = ").append(session.getAttribute(name)).append('\n');
out.append("Attribute ")
.append(name)
.append(" = ")
.append(session.getAttribute(name))
.append('\n');
out.append("URI [")
.append(session.encodeURI(request, "/some/path", request.getHeaders().contains(HttpHeader.COOKIE)))
.append("]");
@ -499,6 +513,8 @@ public class SessionHandlerTest
assertThat(response.getStatus(), equalTo(200));
content = response.getContent();
assertThat(content, containsString("Session=" + id.substring(0, id.indexOf(".node0"))));
assertThat(content, containsString("RequestedSessionIdFromCookie: true"));
assertThat(content, containsString("RequestedSessionIdFromURL: false"));
assertThat(content, containsString("URI [/some/path]")); // Cookies known to be in use
// Get with parameter
@ -513,6 +529,8 @@ public class SessionHandlerTest
assertThat(response.getStatus(), equalTo(200));
content = response.getContent();
assertThat(content, containsString("Session=" + id.substring(0, id.indexOf(".node0"))));
assertThat(content, containsString("RequestedSessionIdFromCookie: false"));
assertThat(content, containsString("RequestedSessionIdFromURL: true"));
assertThat(content, containsString("URI [/some/path;session_id=%s]".formatted(id))); // Cookies not in use
// Get with both, but param wrong

View File

@ -687,8 +687,9 @@ public class StartArgs
// TODO module path
for (Prop property : environment.getProperties())
cmd.addArg(property.key, property.value);
Props props = environment.getProperties();
for (Prop property : props)
cmd.addArg(property.key, props.expand(property.value));
for (Path xmlFile : environment.getXmlFiles())
cmd.addArg(xmlFile.toAbsolutePath().toString());

View File

@ -14,7 +14,6 @@
package org.eclipse.jetty.start;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@ -24,7 +23,6 @@ import java.util.Iterator;
import java.util.List;
import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -67,7 +65,7 @@ public class MainTest
public void testListConfig() throws Exception
{
List<String> cmdLineArgs = new ArrayList<>();
File testJettyHome = MavenTestingUtils.getTestResourceDir("dist-home");
Path testJettyHome = MavenPaths.findTestResourceDir("dist-home");
cmdLineArgs.add("user.dir=" + testJettyHome);
cmdLineArgs.add("-Duser.dir=foo"); // used to test "source" display on "Java Environment"
cmdLineArgs.add("jetty.home=" + testJettyHome);
@ -99,8 +97,8 @@ public class MainTest
public void testUnknownDistroCommand() throws Exception
{
List<String> cmdLineArgs = new ArrayList<>();
File testJettyHome = MavenTestingUtils.getTestResourceDir("dist-home");
Path testJettyBase = MavenTestingUtils.getTargetTestingPath("base-example-unknown");
Path testJettyHome = MavenPaths.findTestResourceDir("dist-home");
Path testJettyBase = MavenPaths.targetTestDir("base-example-unknown");
FS.ensureDirectoryExists(testJettyBase);
Path zedIni = testJettyBase.resolve("start.d/zed.ini");
FS.ensureDirectoryExists(zedIni.getParent());
@ -159,7 +157,7 @@ public class MainTest
{
List<String> cmdLineArgs = new ArrayList<>();
Path homePath = MavenTestingUtils.getTestResourcePathDir("dist-home").toRealPath();
Path homePath = MavenPaths.findTestResourceDir("dist-home");
cmdLineArgs.add("jetty.home=" + homePath);
cmdLineArgs.add("user.dir=" + homePath);

View File

@ -16,30 +16,35 @@ package org.eclipse.jetty.start;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Collections;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Predicate;
public class PropertyDump
{
public static void main(String[] args)
{
System.out.printf("PropertyDump%n");
Predicate<String> nameSelectionPredicate =
(name) ->
name.startsWith("test.") ||
name.startsWith("jetty.");
// As System Properties
Properties props = System.getProperties();
Enumeration<?> names = props.propertyNames();
while (names.hasMoreElements())
{
String name = (String)names.nextElement();
// only interested in "test." prefixed properties
if (name.startsWith("test."))
{
System.out.printf("System %s=%s%n", name, props.getProperty(name));
}
}
props.stringPropertyNames()
.stream()
.filter(nameSelectionPredicate)
.sorted()
.forEach((name) ->
System.out.printf("System %s=%s%n", name, props.getProperty(name)));
// As File Argument
for (String arg : args)
{
System.out.printf("Arg [%s]%n", arg);
if (arg.endsWith(".properties"))
{
Properties aprops = new Properties();
@ -47,15 +52,13 @@ public class PropertyDump
try (FileReader reader = new FileReader(propFile))
{
aprops.load(reader);
Enumeration<?> anames = aprops.propertyNames();
while (anames.hasMoreElements())
{
String name = (String)anames.nextElement();
if (name.startsWith("test."))
{
System.out.printf("%s %s=%s%n", propFile.getName(), name, aprops.getProperty(name));
}
}
Collections.list(aprops.propertyNames())
.stream()
.map(Objects::toString)
.filter(nameSelectionPredicate)
.sorted()
.forEach((name) ->
System.out.printf("%s %s=%s%n", propFile.getName(), name, aprops.getProperty(name)));
}
catch (IOException e)
{

View File

@ -20,19 +20,26 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
@ExtendWith(WorkDirExtension.class)
public class PropertyPassingTest
{
private static class ConsoleCapture implements Runnable
@ -86,10 +93,12 @@ public class PropertyPassingTest
}
}
public WorkDir workDir;
@Test
public void testAsJvmArg() throws IOException, InterruptedException
{
File bogusXml = MavenTestingUtils.getTestResourceFile("bogus.xml");
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
// Setup command line
List<String> commands = new ArrayList<>();
@ -100,7 +109,7 @@ public class PropertyPassingTest
// addDebug(commands);
commands.add("-Dtest.foo=bar"); // TESTING THIS
commands.add(getStartJarBin());
commands.add(bogusXml.getAbsolutePath());
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
@ -112,7 +121,7 @@ public class PropertyPassingTest
@Test
public void testAsCommandLineArg() throws IOException, InterruptedException
{
File bogusXml = MavenTestingUtils.getTestResourceFile("bogus.xml");
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
// Setup command line
List<String> commands = new ArrayList<>();
@ -123,7 +132,7 @@ public class PropertyPassingTest
// addDebug(commands);
commands.add(getStartJarBin());
commands.add("test.foo=bar"); // TESTING THIS
commands.add(bogusXml.getAbsolutePath());
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
@ -135,7 +144,7 @@ public class PropertyPassingTest
@Test
public void testAsDashDCommandLineArg() throws IOException, InterruptedException
{
File bogusXml = MavenTestingUtils.getTestResourceFile("bogus.xml");
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
// Setup command line
List<String> commands = new ArrayList<>();
@ -146,7 +155,7 @@ public class PropertyPassingTest
// addDebug(commands);
commands.add(getStartJarBin());
commands.add("-Dtest.foo=bar"); // TESTING THIS
commands.add(bogusXml.getAbsolutePath());
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
@ -155,16 +164,143 @@ public class PropertyPassingTest
assertThat(output, containsString("test.foo=bar"));
}
@Test
public void testExpandPropertyArg() throws IOException, InterruptedException
{
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
// Setup command line
List<String> commands = new ArrayList<>();
commands.add(getJavaBin());
commands.add("-Dmain.class=" + PropertyDump.class.getName());
commands.add("-Dtest.dir=/opt/dists/jetty");
commands.add("-cp");
commands.add(getClassPath());
// addDebug(commands);
commands.add(getStartJarBin());
commands.add("test.config=${test.dir}/etc/config.ini"); // TESTING THIS
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
// Test for values
assertThat(output, containsString("test.config=/opt/dists/jetty/etc/config.ini"));
}
@Test
public void testExpandPropertyDArg() throws IOException, InterruptedException
{
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
// Setup command line
List<String> commands = new ArrayList<>();
commands.add(getJavaBin());
commands.add("-Dmain.class=" + PropertyDump.class.getName());
commands.add("-Dtest.dir=/opt/dists/jetty");
commands.add("-cp");
commands.add(getClassPath());
// addDebug(commands);
commands.add(getStartJarBin());
commands.add("-Dtest.config=${test.dir}/etc/config.ini"); // TESTING THIS
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
// Test for values
assertThat(output, containsString("test.config=/opt/dists/jetty/etc/config.ini"));
}
@Test
public void testExpandPropertyStartIni() throws IOException, InterruptedException
{
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
Path base = workDir.getEmptyPathDir();
Path ini = base.resolve("start.d/config.ini");
FS.ensureDirectoryExists(ini.getParent());
String iniBody = """
# Enabling a single module (that does nothing) to let start.jar run
--module=empty
# TESTING THIS (it should expand the ${jetty.base} portion
test.config=${jetty.base}/etc/config.ini
""";
Files.writeString(ini, iniBody, StandardCharsets.UTF_8);
// Setup command line
List<String> commands = new ArrayList<>();
commands.add(getJavaBin());
commands.add("-Dmain.class=" + PropertyDump.class.getName());
commands.add("-Djetty.base=" + base);
commands.add("-cp");
commands.add(getClassPath());
// addDebug(commands);
commands.add(getStartJarBin());
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
// Test for values
Path expectedPath = base.resolve("etc/config.ini");
assertThat(output, containsString("test.config=" + expectedPath));
}
@Test
public void testExpandEnvProperty() throws IOException, InterruptedException
{
Path bogusXml = MavenPaths.findTestResourceFile("bogus.xml");
Path base = workDir.getEmptyPathDir();
Path module = base.resolve("modules/env-config.mod");
FS.ensureDirectoryExists(module.getParent());
String moduleBody = """
[environment]
eex
[ini-template]
# configuration option
# test.config=${jetty.home}/etc/eex-config.ini
""";
Files.writeString(module, moduleBody, StandardCharsets.UTF_8);
Path ini = base.resolve("start.d/config.ini");
FS.ensureDirectoryExists(ini.getParent());
String iniBody = """
# Enabling a single module (that does nothing) to let start.jar run
--module=env-config
# TESTING THIS (it should expand the ${jetty.base} portion
test.config=${jetty.base}/etc/config.ini
""";
Files.writeString(ini, iniBody, StandardCharsets.UTF_8);
// Setup command line
List<String> commands = new ArrayList<>();
commands.add(getJavaBin());
commands.add("-Dmain.class=" + PropertyDump.class.getName());
commands.add("-Djetty.base=" + base);
commands.add("-cp");
commands.add(getClassPath());
// addDebug(commands);
commands.add(getStartJarBin());
commands.add(bogusXml.toAbsolutePath().toString());
// Run command, collect output
String output = collectRunOutput(commands);
// Test for values
Path expectedPath = base.resolve("etc/config.ini");
assertThat(output, containsString("test.config=" + expectedPath));
}
private String getClassPath()
{
StringBuilder cp = new StringBuilder();
String pathSep = System.getProperty("path.separator");
cp.append(MavenTestingUtils.getProjectDir("target/classes"));
cp.append(pathSep);
cp.append(MavenTestingUtils.getProjectDir("target/test-classes"));
cp.append(pathSep);
cp.append(MavenTestingUtils.getProjectDir("target/jetty-util"));
return cp.toString();
return String.join(
File.pathSeparator,
List.of(
MavenPaths.projectBase().resolve("target/classes").toString(),
MavenPaths.projectBase().resolve("target/test-classes").toString(),
MavenPaths.projectBase().resolve("target/jetty-util").toString()
)
);
}
protected void addDebug(List<String> commands)
@ -180,11 +316,10 @@ public class PropertyPassingTest
{
cline.append(command).append(" ");
}
System.out.println("Command line: " + cline);
ProcessBuilder builder = new ProcessBuilder(commands);
// Set PWD
builder.directory(MavenTestingUtils.getTestResourceDir("empty.home"));
builder.directory(MavenPaths.findTestResourceDir("empty.home").toFile());
Process pid = builder.start();
ConsoleCapture stdOutPump = new ConsoleCapture("STDOUT", pid.getInputStream()).start();
@ -193,6 +328,7 @@ public class PropertyPassingTest
int exitCode = pid.waitFor();
if (exitCode != 0)
{
System.out.println("Command line: " + cline);
System.out.printf("STDERR: [" + stdErrPump.getConsoleOutput() + "]%n");
System.out.printf("STDOUT: [" + stdOutPump.getConsoleOutput() + "]%n");
assertThat("Exit code", exitCode, is(0));

View File

@ -290,6 +290,12 @@ public class AbstractTest
}
protected void startClient(Transport transport) throws Exception
{
prepareClient(transport);
client.start();
}
protected void prepareClient(Transport transport) throws Exception
{
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
@ -298,7 +304,6 @@ public class AbstractTest
client.setByteBufferPool(clientBufferPool);
client.setExecutor(clientThreads);
client.setSocketAddressResolver(new SocketAddressResolver.Sync());
client.start();
}
public AbstractConnector newConnector(Transport transport, Server server)

View File

@ -0,0 +1,104 @@
//
// ========================================================================
// 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.test.client.transport;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.Destination;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.fcgi.server.internal.ServerFCGIConnection;
import org.eclipse.jetty.http2.server.internal.HTTP2ServerConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.quic.server.ServerQuicConnection;
import org.eclipse.jetty.server.internal.HttpConnection;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.not;
public class ConnectionPoolTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transports")
public void testPreCreateConnections(Transport transport) throws Exception
{
prepareServer(transport, new EmptyServerHandler());
ConnectionListener serverConnections = new ConnectionListener();
connector.addBean(serverConnections);
server.start();
startClient(transport);
client.setMaxConnectionsPerDestination(8);
if (transport == Transport.HTTPS)
((HttpClientTransportOverHTTP)client.getTransport()).setInitializeConnections(true);
var request = client.newRequest(newURI(transport));
Destination destination = client.resolveDestination(request);
destination.getConnectionPool().preCreateConnections(client.getMaxConnectionsPerDestination())
.get(5, TimeUnit.SECONDS);
// Verify that connections have been created.
List<Connection> connections = switch (transport)
{
case HTTP, HTTPS -> serverConnections.filter(HttpConnection.class);
case H2C, H2 -> serverConnections.filter(HTTP2ServerConnection.class);
case H3 -> serverConnections.filter(ServerQuicConnection.class);
case FCGI -> serverConnections.filter(ServerFCGIConnection.class);
};
assertThat(connections, not(empty()));
// Verify that TLS was performed.
List<Connection> sslConnections = switch (transport)
{
case HTTP, H2C, FCGI, H3 -> null;
case HTTPS, H2 -> serverConnections.filter(SslConnection.class);
};
if (sslConnections != null)
{
assertThat(sslConnections.size(), greaterThan(0));
sslConnections.forEach(c -> assertThat(c.getBytesIn(), greaterThan(0L)));
sslConnections.forEach(c -> assertThat(c.getBytesOut(), greaterThan(0L)));
}
}
private static class ConnectionListener implements Connection.Listener
{
private final List<Connection> connections = new ArrayList<>();
@Override
public void onOpened(Connection connection)
{
connections.add(connection);
}
@Override
public void onClosed(Connection connection)
{
connections.remove(connection);
}
private List<Connection> filter(Class<? extends Connection> klass)
{
return connections.stream()
.filter(klass::isInstance)
.toList();
}
}
}

View File

@ -118,20 +118,15 @@ public class UnixDomainTest
// Use the deprecated APIs for backwards compatibility testing.
ClientConnector clientConnector = ClientConnector.forUnixDomain(unixDomainPath);
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector));
httpClient.start();
try
try (HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic(clientConnector)))
{
httpClient.start();
ContentResponse response = httpClient.newRequest(uri)
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
}
finally
{
httpClient.stop();
}
}
@Test
@ -153,18 +148,17 @@ public class UnixDomainTest
}
});
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic());
Origin proxyOrigin = new Origin(
"http",
new Origin.Address("localhost", fakeProxyPort),
null,
new Origin.Protocol(List.of("http/1.1"), false),
new Transport.TCPUnix(unixDomainPath)
);
httpClient.getProxyConfiguration().addProxy(new HttpProxy(proxyOrigin, null));
httpClient.start();
try
try (HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()))
{
Origin proxyOrigin = new Origin(
"http",
new Origin.Address("localhost", fakeProxyPort),
null,
new Origin.Protocol(List.of("http/1.1"), false),
new Transport.TCPUnix(unixDomainPath)
);
httpClient.getProxyConfiguration().addProxy(new HttpProxy(proxyOrigin, null));
httpClient.start();
ContentResponse response = httpClient.newRequest("localhost", fakeServerPort)
.transport(new Transport.TCPUnix(unixDomainPath))
.timeout(5, TimeUnit.SECONDS)
@ -172,10 +166,6 @@ public class UnixDomainTest
assertEquals(HttpStatus.OK_200, response.getStatus());
}
finally
{
httpClient.stop();
}
}
@Test
@ -215,10 +205,9 @@ public class UnixDomainTest
}
});
HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic());
httpClient.start();
try
try (HttpClient httpClient = new HttpClient(new HttpClientTransportDynamic()))
{
httpClient.start();
// Try PROXYv1 with the PROXY information retrieved from the EndPoint.
// PROXYv1 does not support the UNIX family.
ContentResponse response1 = httpClient.newRequest("localhost", 0)
@ -241,10 +230,6 @@ public class UnixDomainTest
assertEquals(HttpStatus.OK_200, response2.getStatus());
}
finally
{
httpClient.stop();
}
}
@Test

View File

@ -32,6 +32,10 @@ import org.eclipse.jetty.util.component.Dumpable;
/**
* Attributes.
* Interface commonly used for storing attributes.
* <p>
* Some attributes may be "hidden" attributes, in that they are only found by an explicit call to
* {@link #getAttribute(String)} and they do not otherwise appear in {@link #getAttributeNameSet()}
* or {@link #asAttributeMap()}.
*/
public interface Attributes
{
@ -51,7 +55,10 @@ public interface Attributes
Object setAttribute(String name, Object attribute);
/**
* Get an attribute
* Get an attribute by name.
* Some attributes may be "hidden" attributes, in that they are only found by an explicit call to
* {@code getAttribute(String)} and they do not otherwise appear in {@link #getAttributeNameSet()}
* or {@link #asAttributeMap()}.
* @param name the attribute to get
* @return the value of the attribute, or {@code null} if no such attribute exists
*/

View File

@ -657,21 +657,7 @@ public class StringUtil
*/
public static boolean isBlank(String str)
{
if (str == null)
{
return true;
}
int len = str.length();
for (int i = 0; i < len; i++)
{
if (!Character.isWhitespace(str.codePointAt(i)))
{
// found a non-whitespace, we can stop searching now
return false;
}
}
// only whitespace
return true;
return str == null || str.isBlank();
}
/**
@ -727,21 +713,7 @@ public class StringUtil
*/
public static boolean isNotBlank(String str)
{
if (str == null)
{
return false;
}
int len = str.length();
for (int i = 0; i < len; i++)
{
if (!Character.isWhitespace(str.codePointAt(i)))
{
// found a non-whitespace, we can stop searching now
return true;
}
}
// only whitespace
return false;
return !isBlank(str);
}
public static boolean isHex(String str, int offset, int length)

View File

@ -50,7 +50,7 @@ import org.eclipse.jetty.websocket.core.client.WebSocketCoreClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebSocketClient extends ContainerLifeCycle implements Configurable, WebSocketContainer
public class WebSocketClient extends ContainerLifeCycle implements Configurable, WebSocketContainer, AutoCloseable
{
private static final Logger LOG = LoggerFactory.getLogger(WebSocketClient.class);
private final WebSocketCoreClient coreClient;
@ -403,6 +403,12 @@ public class WebSocketClient extends ContainerLifeCycle implements Configurable,
super.doStop();
}
@Override
public void close() throws Exception
{
stop();
}
@Override
public String toString()
{

View File

@ -30,8 +30,7 @@ public class HttpClientInitTest
@Test
public void testDefaultInit() throws Exception
{
WebSocketClient client = new WebSocketClient();
try
try (WebSocketClient client = new WebSocketClient())
{
client.start();
HttpClient httpClient = client.getHttpClient();
@ -43,27 +42,20 @@ public class HttpClientInitTest
QueuedThreadPool threadPool = (QueuedThreadPool)executor;
assertThat("QueuedThreadPool.name", threadPool.getName(), startsWith("WebSocket@"));
}
finally
{
client.stop();
}
}
@Test
public void testManualInit() throws Exception
{
HttpClient http = new HttpClient();
{
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setName("ManualWSClient@" + http.hashCode());
http.setExecutor(threadPool);
http.setConnectTimeout(7777);
}
QueuedThreadPool httpThreadPool = new QueuedThreadPool();
httpThreadPool.setName("ManualWSClient@" + http.hashCode());
http.setExecutor(httpThreadPool);
http.setConnectTimeout(7777);
WebSocketClient client = new WebSocketClient(http);
client.addBean(http);
try
try (WebSocketClient client = new WebSocketClient(http))
{
client.addBean(http);
client.start();
HttpClient httpClient = client.getHttpClient();
assertThat("HttpClient exists", httpClient, notNullValue());
@ -75,9 +67,5 @@ public class HttpClientInitTest
QueuedThreadPool threadPool = (QueuedThreadPool)executor;
assertThat("QueuedThreadPool.name", threadPool.getName(), startsWith("ManualWSClient@"));
}
finally
{
client.stop();
}
}
}

View File

@ -83,22 +83,18 @@ public class ClientSessionsTest
@Test
public void testBasicEchoFromClient() throws Exception
{
WebSocketClient client = new WebSocketClient();
CountDownLatch onSessionCloseLatch = new CountDownLatch(1);
client.addSessionListener(new WebSocketSessionListener()
try (WebSocketClient client = new WebSocketClient())
{
@Override
public void onWebSocketSessionClosed(Session session)
CountDownLatch onSessionCloseLatch = new CountDownLatch(1);
client.addSessionListener(new WebSocketSessionListener()
{
onSessionCloseLatch.countDown();
}
});
client.start();
try
{
@Override
public void onWebSocketSessionClosed(Session session)
{
onSessionCloseLatch.countDown();
}
});
client.start();
CloseTrackingEndpoint cliSock = new CloseTrackingEndpoint();
client.setIdleTimeout(Duration.ofSeconds(10));
@ -136,9 +132,5 @@ public class ClientSessionsTest
Collection<Session> open = client.getOpenSessions();
assertThat("(After Close) Open Sessions.size", open.size(), is(0));
}
finally
{
client.stop();
}
}
}

View File

@ -125,28 +125,19 @@ public class WebSocketClientTest
@ValueSource(booleans = {false, true})
public void testCustomizeExecutorDirectly(boolean startHttpClient) throws Exception
{
Executor executor = Executors.newFixedThreadPool(50);
HttpClient httpClient = new HttpClient();
httpClient.setExecutor(executor);
try
try (HttpClient httpClient = new HttpClient())
{
Executor executor = Executors.newFixedThreadPool(50);
httpClient.setExecutor(executor);
if (startHttpClient)
httpClient.start();
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
try
try (WebSocketClient webSocketClient = new WebSocketClient(httpClient))
{
webSocketClient.start();
Executor wsExecutor = webSocketClient.getExecutor();
assertSame(executor, wsExecutor);
}
finally
{
webSocketClient.stop();
}
}
finally
{
httpClient.stop();
}
}
@ -158,25 +149,15 @@ public class WebSocketClientTest
Executor executor = Executors.newFixedThreadPool(50);
clientConnector.setExecutor(executor);
HttpClientTransport transport = new HttpClientTransportOverHTTP(clientConnector);
HttpClient httpClient = new HttpClient(transport);
try
try (HttpClient httpClient = new HttpClient(transport))
{
httpClient.start();
WebSocketClient webSocketClient = new WebSocketClient(httpClient);
try
try (WebSocketClient webSocketClient = new WebSocketClient(httpClient))
{
webSocketClient.start();
Executor inuseExecutor = webSocketClient.getExecutor();
assertSame(executor, inuseExecutor);
}
finally
{
webSocketClient.stop();
}
}
finally
{
httpClient.stop();
}
}

View File

@ -242,6 +242,10 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
if ("war".equalsIgnoreCase(artifact.getType()))
return false;
//The dependency cannot be a pom
if ("pom".equalsIgnoreCase(artifact.getType()))
return false;
//The dependency cannot be scope provided (those should be added to the plugin classpath)
if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
return false;

View File

@ -456,9 +456,12 @@ public abstract class AbstractProxyServlet extends HttpServlet
protected boolean hasContent(HttpServletRequest clientRequest)
{
return clientRequest.getContentLength() > 0 ||
clientRequest.getContentType() != null ||
clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
long contentLength = clientRequest.getContentLengthLong();
if (contentLength == 0)
return false;
if (contentLength > 0)
return true;
return clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
}
protected boolean expects100Continue(HttpServletRequest request)

View File

@ -20,7 +20,9 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -80,6 +82,7 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
@ -143,7 +146,7 @@ public class ProxyServletTest
server.addConnector(serverConnector);
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
String keyStorePath = MavenTestingUtils.getTestResourceFile("server_keystore.p12").getAbsolutePath();
String keyStorePath = MavenTestingUtils.getTestResourcePathFile("server_keystore.p12").toString();
sslContextFactory.setKeyStorePath(keyStorePath);
sslContextFactory.setKeyStorePassword("storepwd");
tlsServerConnector = new ServerConnector(server, new SslConnectionFactory(
@ -427,7 +430,7 @@ public class ProxyServletTest
{
// Create a 6 MiB file
final int length = 6 * 1024;
Path targetTestsDir = MavenTestingUtils.getTargetTestingDir().toPath();
Path targetTestsDir = MavenTestingUtils.getTargetTestingPath();
Files.createDirectories(targetTestsDir);
final Path temp = Files.createTempFile(targetTestsDir, "test_", null);
byte[] kb = new byte[1024];
@ -1703,4 +1706,80 @@ public class ProxyServletTest
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueContentLengthZero(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, false);
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueEmptyChunkedContent(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, true);
}
private void testExpect100ContinueNoRequestContent(Class<? extends ProxyServlet> proxyServletClass, boolean chunked) throws Exception
{
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// Send the 100 Continue.
ServletInputStream input = request.getInputStream();
// Echo the content.
IO.copy(input, response.getOutputStream());
}
});
startProxy(proxyServletClass);
String authority = "localhost:" + serverConnector.getLocalPort();
for (int i = 0; i < 50; i++)
{
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", proxyConnector.getLocalPort())))
{
String request;
if (chunked)
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Transfer-Encoding: chunked
0
""";
}
else
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Content-Length: 0
""";
}
request = request.replace("$A", authority);
client.write(StandardCharsets.UTF_8.encode(request));
HttpTester.Input input = HttpTester.from(client);
HttpTester.Response response1 = HttpTester.parseResponse(input);
if (chunked)
{
assertEquals(HttpStatus.CONTINUE_100, response1.getStatus());
HttpTester.Response response2 = HttpTester.parseResponse(input);
assertEquals(HttpStatus.OK_200, response2.getStatus());
}
else
{
assertEquals(HttpStatus.OK_200, response1.getStatus());
}
}
}
}
}

View File

@ -83,7 +83,7 @@ import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.AbstractSessionManager.RequestedSession;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Callback;
@ -492,7 +492,7 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public String getRequestedSessionId()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession == null ? null : requestedSession.sessionId();
}
@ -551,7 +551,7 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public boolean isRequestedSessionIdValid()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
HttpSession session = getSession(false);
SessionManager manager = getServletRequestInfo().getSessionManager();
return requestedSession != null &&
@ -565,15 +565,15 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public boolean isRequestedSessionIdFromCookie()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.sessionIdFromCookie();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_COOKIE);
}
@Override
public boolean isRequestedSessionIdFromURL()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_URI_PARAMETER);
}
@Override

View File

@ -330,6 +330,8 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
@Override
public Object getAttribute(String name)
{
if (AbstractSessionManager.RequestedSession.isApplicableAttribute(name))
return _requestedSession.getAttribute(name);
return _attributes.getAttribute(name);
}

View File

@ -708,27 +708,33 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
private class NonServletSessionRequest extends Request.Wrapper
{
private final Response _response;
private RequestedSession _session;
private RequestedSession _requestedSession;
public NonServletSessionRequest(Request request, Response response, RequestedSession requestedSession)
{
super(request);
_response = response;
_session = requestedSession;
_requestedSession = requestedSession;
}
@Override
public Object getAttribute(String name)
{
if (AbstractSessionManager.RequestedSession.isApplicableAttribute(name))
return _requestedSession.getAttribute(name);
return super.getAttribute(name);
}
@Override
public Session getSession(boolean create)
{
ManagedSession session = _session.session();
ManagedSession session = _requestedSession.session();
if (session != null || !create)
return session;
newSession(getWrapped(), _session.sessionId(), ms ->
_session = new RequestedSession(ms, _session.sessionId(), true));
session = _session.session();
newSession(getWrapped(), _requestedSession.sessionId(), ms -> _requestedSession = new RequestedSession(ms, _requestedSession.sessionId(), _requestedSession.sessionIdFrom()));
session = _requestedSession.session();
if (session == null)
throw new IllegalStateException("Create session failed");
@ -740,7 +746,7 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
ManagedSession getManagedSession()
{
return _session.session();
return _requestedSession.session();
}
}
}

View File

@ -22,6 +22,7 @@ import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.Random;
@ -45,11 +46,13 @@ import org.eclipse.jetty.client.ContinueProtocolHandler;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.Tag;
@ -165,7 +168,6 @@ public class HttpClientContinueTest extends AbstractTest
.body(content)
.timeout(5, TimeUnit.SECONDS)
.send();
}
assertNotNull(response);
@ -235,14 +237,14 @@ public class HttpClientContinueTest extends AbstractTest
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentRespond417ExpectationFailed(Transport transport) throws Exception
{
testExpect100ContinueWithContentRespondError(transport, 417);
testExpect100ContinueWithContentRespondError(transport, HttpStatus.EXPECTATION_FAILED_417);
}
@ParameterizedTest
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentRespond413RequestEntityTooLarge(Transport transport) throws Exception
{
testExpect100ContinueWithContentRespondError(transport, 413);
testExpect100ContinueWithContentRespondError(transport, HttpStatus.PAYLOAD_TOO_LARGE_413);
}
private void testExpect100ContinueWithContentRespondError(Transport transport, int error) throws Exception
@ -798,6 +800,64 @@ public class HttpClientContinueTest extends AbstractTest
assertTrue(clientLatch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentLengthZeroExpectIsRemoved(Transport transport) throws Exception
{
start(transport, new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
{
assertEquals(0, request.getContentLengthLong());
// The Expect header must have been removed by the client.
assertNull(request.getHeader(HttpHeader.EXPECT.asString()));
}
});
ContentResponse response = client.newRequest(newURI(transport))
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()))
.body(new StringRequestContent(""))
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
public void testExpect100ContinueWithContentLengthZero() throws Exception
{
startServer(Transport.HTTP, new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
assertEquals(0, request.getContentLengthLong());
assertNotNull(request.getHeader(HttpHeader.EXPECT.asString()));
// Trigger the 100-Continue logic.
// The 100 continue will not be sent, since there is no request content.
ServletInputStream input = request.getInputStream();
assertEquals(-1, input.read());
}
});
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", ((NetworkConnector)connector).getLocalPort())))
{
String request = """
GET / HTTP/1.1
Host: localhost
Expect: 100-Continue
Content-Length: 0
""";
client.write(StandardCharsets.UTF_8.encode(request));
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client));
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testExpect100ContinueWithTwoResponsesInOneRead() throws Exception
{

View File

@ -14,13 +14,7 @@
package org.eclipse.jetty.ee10.webapp;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
@ -29,7 +23,6 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.PathMatchers;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -39,6 +32,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.io.FileMatchers.anExistingDirectory;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ -91,7 +85,7 @@ public class TempDirTest
WebAppContext webAppContext = new WebAppContext();
server.setHandler(webAppContext);
Path tmpDir = path.resolve("foo_did_not_exist");
assertThat(Files.exists(tmpDir), is(false));
assertThat(tmpDir.toFile(), not(anExistingDirectory()));
switch (type)
{
@ -125,7 +119,7 @@ public class TempDirTest
WebInfConfiguration webInfConfiguration = new WebInfConfiguration();
webInfConfiguration.resolveTempDirectory(webAppContext);
File tempDirectory = webAppContext.getTempDirectory();
assertThat(tempDirectory.exists(), is(true));
assertThat(tempDirectory, anExistingDirectory());
assertThat(tempDirectory.getParentFile().toPath(), PathMatchers.isSame(tmpDir));
}
@ -152,25 +146,13 @@ public class TempDirTest
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
//Test that if jetty is creating a tmp dir for the webapp, it is different on
//restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
@ -185,26 +167,14 @@ public class TempDirTest
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
//Test that if we explicitly configure the temp dir, it is the same after restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
Path configuredTmpDir = workDir.getPath().resolve("tmp");
Path configuredTmpDir = workDir.getEmptyPathDir().resolve("tmp");
webAppContext.setTempDirectory(configuredTmpDir.toFile());
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
@ -213,6 +183,7 @@ public class TempDirTest
assertNotNull(webAppContext.getTempDirectory());
webAppContext.start();
assertThat(tempDirectory.toPath(), PathMatchers.isSame(webAppContext.getTempDirectory().toPath()));
_server.stop();
}
@Test
@ -220,28 +191,36 @@ public class TempDirTest
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
_server.stop();
assertThat("Temp dir exists", !Files.exists(tempDirectory.toPath()));
assertThat(tempDirectory, not(anExistingDirectory()));
assertNull(webAppContext.getTempDirectory());
}
}
@Test
public void testExplicitTempDir(WorkDir workDir) throws Exception
{
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path myTempDir = workDir.getEmptyPathDir().resolve("my-temp-dir");
FS.ensureDirExists(myTempDir);
//Tell jetty what the temp dir is for the webapp
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
webAppContext.setTempDirectory(myTempDir.toFile());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(tempDirectory));
assertThat(tempDirectory.toPath(), is(myTempDir));
}
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee10.webapp;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
@ -28,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenPaths;
@ -46,7 +48,9 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -247,6 +251,30 @@ public class WebInfConfigurationTest
assertTrue(Files.exists(unpackedWebInfDir.resolve("WEB-INF").resolve("lib").resolve("alpha.jar")));
}
@Test
public void testResolveTempDirectory(WorkDir workDir) throws Exception
{
Path testPath = MavenPaths.targetTestDir("testSimple");
FS.ensureDirExists(testPath);
FS.ensureEmpty(testPath);
_server = new Server();
WebAppContext context = new WebAppContext();
context.setContextPath("/");
Path warPath = createWar(testPath, "test.war");
context.setExtractWAR(true);
context.setWar(warPath.toUri().toURL().toString());
_server.setHandler(context);
_server.start();
File tmpDir = context.getTempDirectory();
assertNotNull(tmpDir);
Path tmpPath = tmpDir.toPath();
Path lastName = tmpPath.getName(tmpPath.getNameCount() - 1);
assertThat(lastName.toString(), startsWith("jetty-test_war-_-any-"));
assertThat(context.getAttribute(ServletContext.TEMPDIR), is(tmpDir));
}
/**
* Assert that for each of the expected jar names (stripped of any path info),
* there is only 1 actual jar url in the context classloader

View File

@ -242,6 +242,10 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
if ("war".equalsIgnoreCase(artifact.getType()))
return false;
//The dependency cannot be a pom
if ("pom".equalsIgnoreCase(artifact.getType()))
return false;
//The dependency cannot be scope provided (those should be added to the plugin classpath)
if (Artifact.SCOPE_PROVIDED.equals(artifact.getScope()))
return false;

View File

@ -456,9 +456,12 @@ public abstract class AbstractProxyServlet extends HttpServlet
protected boolean hasContent(HttpServletRequest clientRequest)
{
return clientRequest.getContentLength() > 0 ||
clientRequest.getContentType() != null ||
clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
long contentLength = clientRequest.getContentLengthLong();
if (contentLength == 0)
return false;
if (contentLength > 0)
return true;
return clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
}
protected boolean expects100Continue(HttpServletRequest request)

View File

@ -20,7 +20,9 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -80,13 +82,14 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
@ -143,7 +146,7 @@ public class ProxyServletTest
server.addConnector(serverConnector);
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
String keyStorePath = MavenTestingUtils.getTestResourceFile("server_keystore.p12").getAbsolutePath();
String keyStorePath = MavenPaths.findTestResourceFile("server_keystore.p12").toString();
sslContextFactory.setKeyStorePath(keyStorePath);
sslContextFactory.setKeyStorePassword("storepwd");
tlsServerConnector = new ServerConnector(server, new SslConnectionFactory(
@ -427,7 +430,7 @@ public class ProxyServletTest
{
// Create a 6 MiB file
final int length = 6 * 1024;
Path targetTestsDir = MavenTestingUtils.getTargetTestingDir().toPath();
Path targetTestsDir = MavenPaths.targetTests();
Files.createDirectories(targetTestsDir);
final Path temp = Files.createTempFile(targetTestsDir, "test_", null);
byte[] kb = new byte[1024];
@ -1703,4 +1706,80 @@ public class ProxyServletTest
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueContentLengthZero(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, false);
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueEmptyChunkedContent(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, true);
}
private void testExpect100ContinueNoRequestContent(Class<? extends ProxyServlet> proxyServletClass, boolean chunked) throws Exception
{
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// Send the 100 Continue.
ServletInputStream input = request.getInputStream();
// Echo the content.
IO.copy(input, response.getOutputStream());
}
});
startProxy(proxyServletClass);
String authority = "localhost:" + serverConnector.getLocalPort();
for (int i = 0; i < 50; i++)
{
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", proxyConnector.getLocalPort())))
{
String request;
if (chunked)
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Transfer-Encoding: chunked
0
""";
}
else
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Content-Length: 0
""";
}
request = request.replace("$A", authority);
client.write(StandardCharsets.UTF_8.encode(request));
HttpTester.Input input = HttpTester.from(client);
HttpTester.Response response1 = HttpTester.parseResponse(input);
if (chunked)
{
assertEquals(HttpStatus.CONTINUE_100, response1.getStatus());
HttpTester.Response response2 = HttpTester.parseResponse(input);
assertEquals(HttpStatus.OK_200, response2.getStatus());
}
else
{
assertEquals(HttpStatus.OK_200, response1.getStatus());
}
}
}
}
}

View File

@ -83,7 +83,7 @@ import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.AbstractSessionManager.RequestedSession;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Callback;
@ -492,7 +492,7 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public String getRequestedSessionId()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession == null ? null : requestedSession.sessionId();
}
@ -551,7 +551,7 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public boolean isRequestedSessionIdValid()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
HttpSession session = getSession(false);
SessionManager manager = getServletRequestInfo().getSessionManager();
return requestedSession != null &&
@ -565,15 +565,15 @@ public class ServletApiRequest implements HttpServletRequest
@Override
public boolean isRequestedSessionIdFromCookie()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.sessionIdFromCookie();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_COOKIE);
}
@Override
public boolean isRequestedSessionIdFromURL()
{
AbstractSessionManager.RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie();
RequestedSession requestedSession = getServletRequestInfo().getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.isSessionIdFrom(RequestedSession.ID_FROM_URI_PARAMETER);
}
@Override

View File

@ -330,6 +330,8 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
@Override
public Object getAttribute(String name)
{
if (AbstractSessionManager.RequestedSession.class.getName().equals(name))
return _requestedSession;
return _attributes.getAttribute(name);
}

View File

@ -752,27 +752,33 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
private class NonServletSessionRequest extends Request.Wrapper
{
private final Response _response;
private RequestedSession _session;
private RequestedSession _requestedSession;
public NonServletSessionRequest(Request request, Response response, RequestedSession requestedSession)
{
super(request);
_response = response;
_session = requestedSession;
_requestedSession = requestedSession;
}
@Override
public Object getAttribute(String name)
{
if (AbstractSessionManager.RequestedSession.isApplicableAttribute(name))
return _requestedSession.getAttribute(name);
return super.getAttribute(name);
}
@Override
public Session getSession(boolean create)
{
ManagedSession session = _session.session();
ManagedSession session = _requestedSession.session();
if (session != null || !create)
return session;
newSession(getWrapped(), _session.sessionId(), ms ->
_session = new RequestedSession(ms, _session.sessionId(), true));
session = _session.session();
newSession(getWrapped(), _requestedSession.sessionId(), ms -> _requestedSession = new RequestedSession(ms, _requestedSession.sessionId(), _requestedSession.sessionIdFrom()));
session = _requestedSession.session();
if (session == null)
throw new IllegalStateException("Create session failed");
@ -784,7 +790,7 @@ public class SessionHandler extends AbstractSessionManager implements Handler.Si
ManagedSession getManagedSession()
{
return _session.session();
return _requestedSession.session();
}
}
}

View File

@ -22,6 +22,7 @@ import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.Random;
@ -45,11 +46,13 @@ import org.eclipse.jetty.client.ContinueProtocolHandler;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.Tag;
@ -856,6 +859,64 @@ public class HttpClientContinueTest extends AbstractTest
}
}
@ParameterizedTest
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentLengthZeroExpectIsRemoved(Transport transport) throws Exception
{
start(transport, new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
{
assertEquals(0, request.getContentLengthLong());
// The Expect header must have been removed by the client.
assertNull(request.getHeader(HttpHeader.EXPECT.asString()));
}
});
ContentResponse response = client.newRequest(newURI(transport))
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString()))
.body(new StringRequestContent(""))
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@Test
public void testExpect100ContinueWithContentLengthZero() throws Exception
{
startServer(Transport.HTTP, new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
assertEquals(0, request.getContentLengthLong());
assertNotNull(request.getHeader(HttpHeader.EXPECT.asString()));
// Trigger the 100-Continue logic.
// The 100 continue will not be sent, since there is no request content.
ServletInputStream input = request.getInputStream();
assertEquals(-1, input.read());
}
});
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", ((NetworkConnector)connector).getLocalPort())))
{
String request = """
GET / HTTP/1.1
Host: localhost
Expect: 100-Continue
Content-Length: 0
""";
client.write(StandardCharsets.UTF_8.encode(request));
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client));
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testNoExpectRespond100Continue() throws Exception
{

View File

@ -18,11 +18,8 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.FileID;
import org.eclipse.jetty.util.IO;
@ -39,7 +36,6 @@ public class WebInfConfiguration extends AbstractConfiguration
{
private static final Logger LOG = LoggerFactory.getLogger(WebInfConfiguration.class);
public static final String TEMPDIR_CONFIGURED = "org.eclipse.jetty.tmpdirConfigured";
public static final String TEMPORARY_RESOURCE_BASE = "org.eclipse.jetty.webapp.tmpResourceBase";
public static final String ORIGINAL_RESOURCE_BASE = "org.eclipse.jetty.webapp.originalResourceBase";
@ -89,10 +85,6 @@ public class WebInfConfiguration extends AbstractConfiguration
@Override
public void deconfigure(WebAppContext context) throws Exception
{
//if it wasn't explicitly configured by the user, then unset it
if (!(context.getAttribute(TEMPDIR_CONFIGURED) instanceof Boolean tmpdirConfigured && tmpdirConfigured))
context.setTempDirectory(null);
//reset the base resource back to what it was before we did any unpacking of resources
Resource originalBaseResource = (Resource)context.removeAttribute(ORIGINAL_RESOURCE_BASE);
context.setBaseResource(originalBaseResource);
@ -133,7 +125,6 @@ public class WebInfConfiguration extends AbstractConfiguration
File tempDirectory = context.getTempDirectory();
if (tempDirectory != null)
{
context.setAttribute(TEMPDIR_CONFIGURED, Boolean.TRUE); //the tmp dir was set explicitly
return;
}
@ -148,37 +139,14 @@ public class WebInfConfiguration extends AbstractConfiguration
return;
}
makeTempDirectory(context.getServer().getContext().getTempDirectory(), context);
context.makeTempDirectory();
}
@Deprecated (forRemoval = true, since = "12.0.12")
public void makeTempDirectory(File parent, WebAppContext context)
throws Exception
{
if (parent == null || !parent.exists() || !parent.canWrite() || !parent.isDirectory())
throw new IllegalStateException("Parent for temp dir not configured correctly: " + (parent == null ? "null" : "writeable=" + parent.canWrite()));
boolean persistent = context.isTempDirectoryPersistent() || "work".equals(parent.toPath().getFileName().toString());
//Create a name for the webapp
String temp = getCanonicalNameForWebAppTmpDir(context);
File tmpDir;
if (persistent)
{
//if it is to be persisted, make sure it will be the same name
//by not using File.createTempFile, which appends random digits
tmpDir = new File(parent, temp);
}
else
{
// ensure dir will always be unique by having classlib generate random path name
tmpDir = Files.createTempDirectory(parent.toPath(), temp).toFile();
tmpDir.deleteOnExit();
}
if (LOG.isDebugEnabled())
LOG.debug("Set temp dir {}", tmpDir);
context.setTempDirectory(tmpDir);
context.setTempDirectoryPersistent(persistent);
context.makeTempDirectory();
}
public void unpack(WebAppContext context) throws IOException
@ -395,91 +363,20 @@ public class WebInfConfiguration extends AbstractConfiguration
*
* @param context the context to get the canonical name from
* @return the canonical name for the webapp temp directory
* @deprecated this method is no longer used
*/
@Deprecated(forRemoval = true, since = "12.0.12")
public static String getCanonicalNameForWebAppTmpDir(WebAppContext context)
{
StringBuilder canonicalName = new StringBuilder();
canonicalName.append("jetty-");
//get the host and the port from the first connector
Server server = context.getServer();
if (server != null)
{
Connector[] connectors = server.getConnectors();
if (connectors.length > 0)
{
//Get the host
String host = null;
int port = 0;
if (connectors[0] instanceof NetworkConnector connector)
{
host = connector.getHost();
port = connector.getLocalPort();
if (port < 0)
port = connector.getPort();
}
if (host == null)
host = "0.0.0.0";
canonicalName.append(host);
canonicalName.append("-");
canonicalName.append(port);
canonicalName.append("-");
}
}
// Resource base
try
{
Resource resource = context.getBaseResource();
if (resource == null)
{
if (StringUtil.isBlank(context.getWar()))
throw new IllegalStateException("No resourceBase or war set for context");
// Set dir or WAR to resource
resource = context.newResource(context.getWar());
}
String resourceBaseName = getResourceBaseName(resource);
canonicalName.append(resourceBaseName);
canonicalName.append("-");
}
catch (Exception e)
{
if (LOG.isDebugEnabled())
LOG.debug("Can't get resource base name", e);
canonicalName.append("-"); // empty resourceBaseName segment
}
//Context name
String contextPath = context.getContextPath();
contextPath = contextPath.replace('/', '_');
contextPath = contextPath.replace('\\', '_');
canonicalName.append(contextPath);
//Virtual host (if there is one)
canonicalName.append("-");
List<String> vhosts = context.getVirtualHosts();
if (vhosts == null || vhosts.size() <= 0)
canonicalName.append("any");
else
canonicalName.append(vhosts.get(0));
// sanitize
for (int i = 0; i < canonicalName.length(); i++)
{
char c = canonicalName.charAt(i);
if (!Character.isJavaIdentifierPart(c) && "-.".indexOf(c) < 0)
canonicalName.setCharAt(i, '.');
}
canonicalName.append("-");
return StringUtil.sanitizeFileSystemName(canonicalName.toString());
return context.getCanonicalNameForTmpDir();
}
/**
* @param resource the Resource for which to extract a short name
* @return extract a short name for the resource
* @deprecated this method is no longer needed
*/
@Deprecated(forRemoval = true, since = "12.0.12")
protected static String getResourceBaseName(Resource resource)
{
// Use File System and File interface if present

View File

@ -14,13 +14,7 @@
package org.eclipse.jetty.ee11.webapp;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
@ -29,9 +23,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.PathMatchers;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
@ -40,6 +32,9 @@ import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.io.FileMatchers.anExistingDirectory;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@ExtendWith(WorkDirExtension.class)
public class TempDirTest
@ -90,7 +85,7 @@ public class TempDirTest
WebAppContext webAppContext = new WebAppContext();
server.setHandler(webAppContext);
Path tmpDir = path.resolve("foo_did_not_exist");
assertThat(Files.exists(tmpDir), is(false));
assertThat(tmpDir.toFile(), not(anExistingDirectory()));
switch (type)
{
@ -124,7 +119,7 @@ public class TempDirTest
WebInfConfiguration webInfConfiguration = new WebInfConfiguration();
webInfConfiguration.resolveTempDirectory(webAppContext);
File tempDirectory = webAppContext.getTempDirectory();
assertThat(tempDirectory.exists(), is(true));
assertThat(tempDirectory, is(anExistingDirectory()));
assertThat(tempDirectory.getParentFile().toPath(), PathMatchers.isSame(tmpDir));
}
@ -151,65 +146,81 @@ public class TempDirTest
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
//Test that if jetty is creating a tmp dir for the webapp, it is different on
//restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
webAppContext.stop();
assertNull(webAppContext.getTempDirectory());
webAppContext.start();
assertThat(tempDirectory.toPath(), not(PathMatchers.isSame(webAppContext.getTempDirectory().toPath())));
}
@Disabled ("Enable after issue 11548 fixed")
@Test
public void testSameTempDir(WorkDir workDir) throws Exception
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
//Test that if we explicitly configure the temp dir, it is the same after restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
Path configuredTmpDir = workDir.getPath().resolve("tmp");
Path configuredTmpDir = workDir.getEmptyPathDir().resolve("tmp");
webAppContext.setTempDirectory(configuredTmpDir.toFile());
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
assertThat(tempDirectory.toPath(), PathMatchers.isSame(configuredTmpDir));
webAppContext.stop();
assertNotNull(webAppContext.getTempDirectory());
webAppContext.start();
assertThat(tempDirectory.toPath(), PathMatchers.isSame(webAppContext.getTempDirectory().toPath()));
_server.stop();
}
@Test
public void testTempDirDeleted(WorkDir workDir) throws Exception
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
_server.stop();
assertThat(tempDirectory, not(anExistingDirectory()));
assertNull(webAppContext.getTempDirectory());
}
@Test
public void testExplicitTempDir(WorkDir workDir) throws Exception
{
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path myTempDir = workDir.getEmptyPathDir().resolve("my-temp-dir");
FS.ensureDirExists(myTempDir);
//Tell jetty what the temp dir is for the webapp
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
webAppContext.setTempDirectory(myTempDir.toFile());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(tempDirectory));
assertThat(tempDirectory.toPath(), is(myTempDir));
}
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee11.webapp;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
@ -28,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenPaths;
@ -46,7 +48,9 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -247,6 +251,30 @@ public class WebInfConfigurationTest
assertTrue(Files.exists(unpackedWebInfDir.resolve("WEB-INF").resolve("lib").resolve("alpha.jar")));
}
@Test
public void testResolveTempDirectory(WorkDir workDir) throws Exception
{
Path testPath = MavenPaths.targetTestDir("testSimple");
FS.ensureDirExists(testPath);
FS.ensureEmpty(testPath);
_server = new Server();
WebAppContext context = new WebAppContext();
context.setContextPath("/");
Path warPath = createWar(testPath, "test.war");
context.setExtractWAR(true);
context.setWar(warPath.toUri().toURL().toString());
_server.setHandler(context);
_server.start();
File tmpDir = context.getTempDirectory();
assertNotNull(tmpDir);
Path tmpPath = tmpDir.toPath();
Path lastName = tmpPath.getName(tmpPath.getNameCount() - 1);
assertThat(lastName.toString(), startsWith("jetty-test_war-_-any-"));
assertThat(context.getAttribute(ServletContext.TEMPDIR), is(tmpDir));
}
/**
* Assert that for each of the expected jar names (stripped of any path info),
* there is only 1 actual jar url in the context classloader

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee9.nested;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@ -2465,7 +2466,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
private ManagedSession _managedSession;
private List<ManagedSession> _managedSessions;
AbstractSessionManager.RequestedSession _requestedSession;
AbstractSessionManager.RequestedSession _requestedSession = AbstractSessionManager.RequestedSession.NO_REQUESTED_SESSION;
protected CoreContextRequest(org.eclipse.jetty.server.Request wrapped,
ScopedContext context,
@ -2565,7 +2566,15 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
*/
public void setRequestedSession(AbstractSessionManager.RequestedSession requestedSession)
{
_requestedSession = requestedSession;
_requestedSession = requestedSession == null ? AbstractSessionManager.RequestedSession.NO_REQUESTED_SESSION : requestedSession;
}
@Override
public Object getAttribute(String name)
{
if (AbstractSessionManager.RequestedSession.class.getName().equals(name))
return _requestedSession;
return super.getAttribute(name);
}
/**
@ -2652,7 +2661,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
if (_sessionManager == null)
throw new IllegalStateException("No SessionManager");
_sessionManager.newSession(this, _requestedSession == null ? null : _requestedSession.sessionId(), this::setManagedSession);
_sessionManager.newSession(this, _requestedSession.sessionId(), this::setManagedSession);
if (_managedSession == null)
throw new IllegalStateException("Create session failed");
@ -2687,10 +2696,21 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
@Override
public Resource getResourceForTempDirName()
{
return ContextHandler.this.getNestedResourceForTempDirName();
}
private Resource getSuperResourceForTempDirName()
{
return super.getResourceForTempDirName();
}
public void setTempDirectory(File dir)
{
super.setTempDirectory(dir);
setAttribute(ServletContext.TEMPDIR, super.getTempDirectory());
}
@Override
public void setContextPath(String contextPath)
{
@ -2852,4 +2872,9 @@ public class ContextHandler extends ScopedHandler implements Attributes, Supplie
}
}
}
public Resource getNestedResourceForTempDirName()
{
return getCoreContextHandler().getSuperResourceForTempDirName();
}
}

View File

@ -85,7 +85,7 @@ import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.HttpCookieUtils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.AbstractSessionManager.RequestedSession;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionManager;
import org.eclipse.jetty.util.Attributes;
@ -1245,7 +1245,7 @@ public class Request implements HttpServletRequest
@Override
public String getRequestedSessionId()
{
AbstractSessionManager.RequestedSession requestedSession = _coreRequest.getRequestedSession();
RequestedSession requestedSession = _coreRequest.getRequestedSession();
return requestedSession == null ? null : requestedSession.sessionId();
}
@ -1522,8 +1522,7 @@ public class Request implements HttpServletRequest
@Override
public boolean isRequestedSessionIdFromCookie()
{
AbstractSessionManager.RequestedSession requestedSession = _coreRequest.getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && requestedSession.sessionIdFromCookie();
return _coreRequest.getRequestedSession().isSessionIdFrom(RequestedSession.ID_FROM_COOKIE);
}
@Override
@ -1536,14 +1535,13 @@ public class Request implements HttpServletRequest
@Override
public boolean isRequestedSessionIdFromURL()
{
AbstractSessionManager.RequestedSession requestedSession = _coreRequest.getRequestedSession();
return requestedSession != null && requestedSession.sessionId() != null && !requestedSession.sessionIdFromCookie();
return _coreRequest.getRequestedSession().isSessionIdFrom(RequestedSession.ID_FROM_URI_PARAMETER);
}
@Override
public boolean isRequestedSessionIdValid()
{
AbstractSessionManager.RequestedSession requestedSession = _coreRequest.getRequestedSession();
RequestedSession requestedSession = _coreRequest.getRequestedSession();
SessionManager sessionManager = _coreRequest.getSessionManager();
ManagedSession managedSession = _coreRequest.getManagedSession();
return requestedSession != null &&

View File

@ -556,7 +556,7 @@ public class SessionHandler extends ScopedHandler implements SessionConfig.Mutab
currentSession = currentRequestedSession.session();
}
else
currentRequestedSession = new AbstractSessionManager.RequestedSession(currentSession, currentSession.getId(), false /*TODO!!!*/);
currentRequestedSession = new AbstractSessionManager.RequestedSession(currentSession, currentSession.getId(), null /*TODO!!!*/);
coreRequest.setManagedSession(currentSession);
coreRequest.setRequestedSession(currentRequestedSession);

View File

@ -67,7 +67,7 @@ import org.eclipse.jetty.server.NetworkConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.server.TunnelSupport;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.AbstractSessionManager.RequestedSession;
import org.eclipse.jetty.session.DefaultSessionCache;
import org.eclipse.jetty.session.DefaultSessionIdManager;
import org.eclipse.jetty.session.ManagedSession;
@ -1613,7 +1613,7 @@ public class ResponseTest
ContextHandler.CoreContextRequest coreRequest = response.getHttpChannel().getCoreRequest();
coreRequest.setSessionManager(sessionHandler.getSessionManager());
coreRequest.setRequestedSession(new AbstractSessionManager.RequestedSession(null, "12345", false));
coreRequest.setRequestedSession(new RequestedSession(null, "12345", RequestedSession.ID_FROM_URI_PARAMETER));
assertNotNull(request.getSession(true));
assertThat(request.getSession(false).getId(), is("12345"));
@ -1724,7 +1724,7 @@ public class ResponseTest
ContextHandler.CoreContextRequest coreRequest = response.getHttpChannel().getCoreRequest();
coreRequest.setSessionManager(sessionHandler.getSessionManager());
ManagedSession session = sessionHandler.getSessionManager().getManagedSession("12345");
coreRequest.setRequestedSession(new AbstractSessionManager.RequestedSession(session, "12345", cookie));
coreRequest.setRequestedSession(new RequestedSession(session, "12345", cookie ? RequestedSession.ID_FROM_COOKIE : RequestedSession.ID_FROM_URI_PARAMETER));
if (session == null)
request.getSession(true);
@ -1793,7 +1793,7 @@ public class ResponseTest
request.setContext(_context._apiContext, "/info");
ContextHandler.CoreContextRequest coreRequest = response.getHttpChannel().getCoreRequest();
coreRequest.setRequestedSession(new AbstractSessionManager.RequestedSession(null, "12345", i > 2));
coreRequest.setRequestedSession(new RequestedSession(null, "12345", i > 2 ? RequestedSession.ID_FROM_COOKIE : RequestedSession.ID_FROM_URI_PARAMETER));
SessionHandler handler = new SessionHandler();
NullSessionDataStore dataStore = new NullSessionDataStore();

View File

@ -459,9 +459,12 @@ public abstract class AbstractProxyServlet extends HttpServlet
protected boolean hasContent(HttpServletRequest clientRequest)
{
return clientRequest.getContentLength() > 0 ||
clientRequest.getContentType() != null ||
clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
long contentLength = clientRequest.getContentLengthLong();
if (contentLength == 0)
return false;
if (contentLength > 0)
return true;
return clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
}
protected boolean expects100Continue(HttpServletRequest request)

View File

@ -20,7 +20,9 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@ -79,6 +81,7 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
@ -1701,4 +1704,80 @@ public class ProxyServletTest
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueContentLengthZero(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, false);
}
@ParameterizedTest
@MethodSource("impls")
public void testExpect100ContinueEmptyChunkedContent(Class<? extends ProxyServlet> proxyServletClass) throws Exception
{
testExpect100ContinueNoRequestContent(proxyServletClass, true);
}
private void testExpect100ContinueNoRequestContent(Class<? extends ProxyServlet> proxyServletClass, boolean chunked) throws Exception
{
startServer(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// Send the 100 Continue.
ServletInputStream input = request.getInputStream();
// Echo the content.
IO.copy(input, response.getOutputStream());
}
});
startProxy(proxyServletClass);
String authority = "localhost:" + serverConnector.getLocalPort();
for (int i = 0; i < 50; i++)
{
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", proxyConnector.getLocalPort())))
{
String request;
if (chunked)
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Transfer-Encoding: chunked
0
""";
}
else
{
request = """
POST http://$A/ HTTP/1.1
Host: $A
Expect: 100-Continue
Content-Length: 0
""";
}
request = request.replace("$A", authority);
client.write(StandardCharsets.UTF_8.encode(request));
HttpTester.Input input = HttpTester.from(client);
HttpTester.Response response1 = HttpTester.parseResponse(input);
if (chunked)
{
assertEquals(HttpStatus.CONTINUE_100, response1.getStatus());
HttpTester.Response response2 = HttpTester.parseResponse(input);
assertEquals(HttpStatus.OK_200, response2.getStatus());
}
else
{
assertEquals(HttpStatus.OK_200, response1.getStatus());
}
}
}
}
}

View File

@ -1162,7 +1162,6 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
public void setTempDirectory(File dir)
{
getCoreContextHandler().setTempDirectory(dir);
setAttribute(ServletContext.TEMPDIR, getCoreContextHandler().getTempDirectory());
}
@ManagedAttribute(value = "temporary directory location", readonly = true)
@ -1181,9 +1180,10 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
return getCoreContextHandler().getCanonicalNameForTmpDir();
}
protected Resource getResourceForTempDirName()
@Override
public Resource getNestedResourceForTempDirName()
{
Resource resource = getCoreContextHandler().getResourceForTempDirName();
Resource resource = super.getNestedResourceForTempDirName();
if (resource == null)
{

View File

@ -14,13 +14,7 @@
package org.eclipse.jetty.ee9.webapp;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
@ -29,27 +23,32 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.PathMatchers;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.io.FileMatchers.anExistingDirectory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(WorkDirExtension.class)
public class TempDirTest
{
public static void tearDown()
private Server _server;
@AfterEach
public void afterEach() throws Exception
{
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
if (_server != null)
_server.stop();
}
/**
@ -86,11 +85,11 @@ public class TempDirTest
public void attributeWithNonExistentDirectory(String type, WorkDir workDir) throws Exception
{
Path jettyBase = workDir.getEmptyPathDir();
Server server = new Server();
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
server.setHandler(webAppContext);
_server.setHandler(webAppContext);
Path tmpDir = jettyBase.resolve("foo_did_not_exist");
assertFalse(Files.exists(tmpDir));
assertThat(tmpDir.toFile(), not(anExistingDirectory()));
switch (type)
{
@ -115,23 +114,23 @@ public class TempDirTest
{
Path jettyBase = workDir.getEmptyPathDir();
WebAppContext webAppContext = new WebAppContext();
Server server = new Server();
webAppContext.setServer(server);
_server = new Server();
webAppContext.setServer(_server);
Path tmpDir = jettyBase.resolve("temp_test");
FS.ensureDirExists(tmpDir);
server.setTempDirectory(tmpDir.toFile());
_server.setTempDirectory(tmpDir.toFile());
// Test we have correct value as the webapp temp directory.
WebInfConfiguration webInfConfiguration = new WebInfConfiguration();
webInfConfiguration.resolveTempDirectory(webAppContext);
File tempDirectory = webAppContext.getTempDirectory();
assertTrue(tempDirectory.exists());
assertThat(tempDirectory, anExistingDirectory());
assertThat(tempDirectory.getParentFile().toPath(), is(tmpDir));
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(webAppContext.getTempDirectory()));
}
/**
* <code>${jetty.base}</code> directory exists and has a subdirectory called work
* so webappContent#tempDirectory is created under <code>java.io.tmpdir</code>
*/
@Test
public void jettyBaseWorkExists(WorkDir workDirExt) throws Exception
@ -140,12 +139,13 @@ public class TempDirTest
Path workDir = jettyBase.resolve("work");
FS.ensureDirExists(workDir);
WebInfConfiguration webInfConfiguration = new WebInfConfiguration();
Server server = new Server();
server.setTempDirectory(workDir.toFile());
_server = new Server();
_server.setTempDirectory(workDir.toFile());
WebAppContext webAppContext = new WebAppContext();
server.setHandler(webAppContext);
_server.setHandler(webAppContext);
webInfConfiguration.resolveTempDirectory(webAppContext);
assertThat(webAppContext.getTempDirectory().getParentFile().toPath(), PathMatchers.isSame(workDir));
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(webAppContext.getTempDirectory()));
}
@Test
@ -153,29 +153,84 @@ public class TempDirTest
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
Path warFile = workDir.getEmptyPathDir().resolve("test.war");
Map<String, String> env = new HashMap<>();
env.put("create", "true");
URI uri = URI.create("jar:" + warFile.toUri().toASCIIString());
// Use ZipFS so that we can create paths that are just "/"
try (FileSystem zipfs = FileSystems.newFileSystem(uri, env))
{
Path root = zipfs.getPath("/");
IO.copyDir(testWebappDir, root);
}
//Let jetty create the tmp dir on the fly
Server server = new Server();
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWarResource(webAppContext.getResourceFactory().newResource(warFile));
server.setHandler(webAppContext);
server.start();
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
server.stop();
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(webAppContext.getTempDirectory()));
_server.stop();
assertNull(webAppContext.getTempDirectory());
assertThat("Temp dir exists", !Files.exists(tempDirectory.toPath()));
assertThat(tempDirectory, not(anExistingDirectory()));
}
@Test
public void testExplicitTempDir(WorkDir workDir) throws Exception
{
Path myTempDir = workDir.getEmptyPathDir().resolve("my-temp-dir");
FS.ensureDirExists(myTempDir);
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
//Tell jetty what the temp dir is for the webapp
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
webAppContext.setTempDirectory(myTempDir.toFile());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
assertThat(webAppContext.getAttribute(ServletContext.TEMPDIR), is(tempDirectory));
assertThat(tempDirectory.toPath(), is(myTempDir));
}
@Test
public void testFreshTempDir(WorkDir workDir) throws Exception
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
//Test that if jetty is creating a tmp dir for the webapp, it is different on
//restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
webAppContext.stop();
assertNull(webAppContext.getTempDirectory());
webAppContext.start();
assertThat(tempDirectory.toPath(), not(PathMatchers.isSame(webAppContext.getTempDirectory().toPath())));
}
@Test
public void testSameTempDir(WorkDir workDir) throws Exception
{
// Create war on the fly
Path testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
//Test that if we explicitly configure the temp dir, it is the same after restart
_server = new Server();
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
Path configuredTmpDir = workDir.getPath().resolve("tmp");
webAppContext.setTempDirectory(configuredTmpDir.toFile());
webAppContext.setWar(testWebappDir.toFile().getAbsolutePath());
_server.setHandler(webAppContext);
_server.start();
File tempDirectory = webAppContext.getTempDirectory();
assertThat(tempDirectory.toPath(), PathMatchers.isSame(configuredTmpDir));
webAppContext.stop();
assertNotNull(webAppContext.getTempDirectory());
webAppContext.start();
assertThat(tempDirectory.toPath(), PathMatchers.isSame(webAppContext.getTempDirectory().toPath()));
}
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.ee9.webapp;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
@ -28,6 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import jakarta.servlet.ServletContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenPaths;
@ -45,9 +47,12 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -243,6 +248,30 @@ public class WebInfConfigurationTest
assertTrue(Files.exists(unpackedWebInfDir.resolve("WEB-INF").resolve("lib").resolve("alpha.jar")));
}
@Test
public void testResolveTempDirectory(WorkDir workDir) throws Exception
{
Path testPath = MavenPaths.targetTestDir("testSimple");
FS.ensureDirExists(testPath);
FS.ensureEmpty(testPath);
_server = new Server();
WebAppContext context = new WebAppContext();
context.setContextPath("/");
Path warPath = createWar(testPath, "test.war");
context.setExtractWAR(true);
context.setWar(warPath.toUri().toURL().toString());
_server.setHandler(context);
_server.start();
File tmpDir = context.getTempDirectory();
assertNotNull(tmpDir);
Path tmpPath = tmpDir.toPath();
Path lastName = tmpPath.getName(tmpPath.getNameCount() - 1);
assertThat(lastName.toString(), startsWith("jetty-test_war-_-any-"));
assertThat(context.getAttribute(ServletContext.TEMPDIR), is(tmpDir));
}
/**
* Assert that for each of the expected jar names (stripped of any path info),
* there is only 1 actual jar url in the context classloader

View File

@ -287,7 +287,7 @@
<gson.version>2.11.0</gson.version>
<guava.version>33.2.1-jre</guava.version>
<guice.version>7.0.0</guice.version>
<h2spec.maven.plugin.version>1.0.12</h2spec.maven.plugin.version>
<h2spec.maven.plugin.version>1.0.13</h2spec.maven.plugin.version>
<hamcrest.version>2.2</hamcrest.version>
<hazelcast.version>5.4.0</hazelcast.version>
<hibernate.search.version>7.1.1.Final</hibernate.search.version>