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

This commit is contained in:
Joakim Erdfelt 2024-06-27 06:45:25 -05:00
commit c7ad22e861
No known key found for this signature in database
GPG Key ID: 2D0E1FB8FE4B68B4
29 changed files with 1178 additions and 139 deletions

View File

@ -414,8 +414,6 @@ jetty-10.0.18 - 26 October 2023
+ 10537 HTTP/3: Incomplete Data Transfer When Used with Spring Boot WebFlux
+ 10696 jetty.sh doesn't work with JETTY_USER in Jetty 10.0.17 thru Jetty
12.0.2
+ 10669 Provide ability to defer initial deployment of webapps until after
Server has started
+ 10705 Creating a `HTTP3ServerConnector` with a `SslContextFactory` that has
a non-null `SSLContext` makes the server fail to start with an unclear error
message

View File

@ -188,7 +188,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
protected void doStop() throws Exception
{
requestTimeouts.destroy();
abort(new AsynchronousCloseException());
abortExchanges(new AsynchronousCloseException());
Sweeper connectionPoolSweeper = client.getBean(Sweeper.class);
if (connectionPoolSweeper != null && connectionPool instanceof Sweeper.Sweepable)
connectionPoolSweeper.remove((Sweeper.Sweepable)connectionPool);
@ -294,7 +294,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
@Override
public void failed(Throwable x)
{
abort(x);
abortExchanges(x);
}
@Override
@ -513,7 +513,7 @@ public class HttpDestination extends ContainerLifeCycle implements Destination,
*
* @param cause the abort cause
*/
public void abort(Throwable cause)
private void abortExchanges(Throwable cause)
{
// Copy the queue of exchanges and fail only those that are queued at this moment.
// The application may queue another request from the failure/complete listener

View File

@ -32,10 +32,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

View File

@ -66,13 +66,9 @@ public final class EtagUtils
*/
public static HttpField createWeakEtagField(Resource resource, String etagSuffix)
{
Path path = resource.getPath();
if (path == null)
return null;
String etagValue = EtagUtils.computeWeakEtag(path, etagSuffix);
if (etagValue != null)
return new PreEncodedHttpField(HttpHeader.ETAG, etagValue);
String etag = EtagUtils.computeWeakEtag(resource, etagSuffix);
if (etag != null)
return new PreEncodedHttpField(HttpHeader.ETAG, etag);
return null;
}
@ -213,6 +209,9 @@ public final class EtagUtils
*/
public static String rewriteWithSuffix(String etag, String newSuffix)
{
if (etag == null)
return null;
StringBuilder ret = new StringBuilder();
boolean weak = etag.startsWith("W/");
int start = 0;

View File

@ -0,0 +1,220 @@
//
// ========================================================================
// 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.http;
import java.nio.file.Path;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.util.Attributes;
import static org.eclipse.jetty.http.ComplianceViolation.Listener.NOOP;
/**
* The Configuration needed to parse multipart/form-data.
* @see MultiPartFormData#from(Content.Source, Attributes, String, MultiPartConfig)
*/
public class MultiPartConfig
{
private static final int DEFAULT_MAX_PARTS = 100;
private static final int DEFAULT_MAX_SIZE = 50 * 1024 * 1024;
private static final int DEFAULT_MAX_PART_SIZE = 10 * 1024 * 1024;
private static final int DEFAULT_MAX_MEMORY_PART_SIZE = 1024;
private static final int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024;
private static final boolean DEFAULT_USE_FILES_FOR_PARTS_WITHOUT_FILE_NAME = true;
public static class Builder
{
private Path _location;
private Integer _maxParts;
private Long _maxSize;
private Long _maxPartSize;
private Long _maxMemoryPartSize;
private Integer _maxHeadersSize;
private Boolean _useFilesForPartsWithoutFileName;
private MultiPartCompliance _complianceMode;
private ComplianceViolation.Listener _violationListener;
public Builder()
{
}
/**
* @param location the directory where parts will be saved as files.
*/
public Builder location(Path location)
{
_location = location;
return this;
}
/**
* @param maxParts the maximum number of parts that can be parsed from the multipart content, or -1 for unlimited.
*/
public Builder maxParts(int maxParts)
{
_maxParts = maxParts;
return this;
}
/**
* @return the maximum size in bytes of the whole multipart content, or -1 for unlimited.
*/
public Builder maxSize(long maxSize)
{
_maxSize = maxSize;
return this;
}
/**
* @return the maximum part size in bytes, or -1 for unlimited.
*/
public Builder maxPartSize(long maxPartSize)
{
_maxPartSize = maxPartSize;
return this;
}
/**
* <p>Sets the maximum size of a part in memory, after which it will be written as a file.</p>
* <p>Use value {@code 0} to always write the part to disk.</p>
* <p>Use value {@code -1} to never write the part to disk.</p>
*
* @param maxMemoryPartSize the maximum part size which can be held in memory.
*/
public Builder maxMemoryPartSize(long maxMemoryPartSize)
{
_maxMemoryPartSize = maxMemoryPartSize;
return this;
}
/**
* @param maxHeadersSize the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length.
*/
public Builder maxHeadersSize(int maxHeadersSize)
{
_maxHeadersSize = maxHeadersSize;
return this;
}
/**
* @param useFilesForPartsWithoutFileName whether parts without a fileName may be stored as files.
*/
public Builder useFilesForPartsWithoutFileName(Boolean useFilesForPartsWithoutFileName)
{
_useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
return this;
}
/**
* @param complianceMode the compliance mode.
*/
public Builder complianceMode(MultiPartCompliance complianceMode)
{
_complianceMode = complianceMode;
return this;
}
/**
* @param violationListener the compliance violation listener.
*/
public Builder violationListener(ComplianceViolation.Listener violationListener)
{
_violationListener = violationListener;
return this;
}
public MultiPartConfig build()
{
return new MultiPartConfig(_location,
_maxParts == null ? DEFAULT_MAX_PARTS : _maxParts,
_maxSize == null ? DEFAULT_MAX_SIZE : _maxSize,
_maxPartSize == null ? DEFAULT_MAX_PART_SIZE : _maxPartSize,
_maxMemoryPartSize == null ? DEFAULT_MAX_MEMORY_PART_SIZE : _maxMemoryPartSize,
_maxHeadersSize == null ? DEFAULT_MAX_HEADERS_SIZE : _maxHeadersSize,
_useFilesForPartsWithoutFileName == null ? DEFAULT_USE_FILES_FOR_PARTS_WITHOUT_FILE_NAME : _useFilesForPartsWithoutFileName,
_complianceMode == null ? MultiPartCompliance.RFC7578 : _complianceMode,
_violationListener == null ? NOOP : _violationListener);
}
}
private final Path _location;
private final long _maxMemoryPartSize;
private final long _maxPartSize;
private final long _maxSize;
private final int _maxParts;
private final int _maxHeadersSize;
private final boolean _useFilesForPartsWithoutFileName;
private final MultiPartCompliance _compliance;
private final ComplianceViolation.Listener _listener;
private MultiPartConfig(Path location, int maxParts, long maxSize, long maxPartSize, long maxMemoryPartSize,
int maxHeadersSize, boolean useFilesForPartsWithoutFileName,
MultiPartCompliance compliance, ComplianceViolation.Listener listener)
{
this._location = location;
this._maxParts = maxParts;
this._maxSize = maxSize;
this._maxPartSize = maxPartSize;
this._maxMemoryPartSize = maxMemoryPartSize;
this._maxHeadersSize = maxHeadersSize;
this._useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
this._compliance = compliance;
this._listener = listener;
}
public Path getLocation()
{
return _location;
}
public int getMaxParts()
{
return _maxParts;
}
public long getMaxSize()
{
return _maxSize;
}
public long getMaxPartSize()
{
return _maxPartSize;
}
public long getMaxMemoryPartSize()
{
return _maxMemoryPartSize;
}
public int getMaxHeadersSize()
{
return _maxHeadersSize;
}
public boolean isUseFilesForPartsWithoutFileName()
{
return _useFilesForPartsWithoutFileName;
}
public MultiPartCompliance getMultiPartCompliance()
{
return _compliance;
}
public ComplianceViolation.Listener getViolationListener()
{
return _listener;
}
}

View File

@ -79,8 +79,45 @@ public class MultiPartFormData
}
/**
* Returns {@code multipart/form-data} parts using {@link MultiPartCompliance#RFC7578}.
* Returns {@code multipart/form-data} parts using the given {@link Content.Source} and {@link MultiPartConfig}.
*
* @param content the source of the multipart content.
* @param attributes the attributes where the futureParts are tracked.
* @param contentType the value of the {@link HttpHeader#CONTENT_TYPE} header.
* @param config the multipart configuration.
* @return the future parts
*/
public static CompletableFuture<MultiPartFormData.Parts> from(Content.Source content, Attributes attributes, String contentType, MultiPartConfig config)
{
// Look for an existing future (we use the future here rather than the parts as it can remember any failure).
CompletableFuture<MultiPartFormData.Parts> futureParts = MultiPartFormData.get(attributes);
if (futureParts == null)
{
// No existing parts, so we need to try to read them ourselves
// Are we the right content type to produce our own parts?
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.getValueParameters(contentType, null)))
return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
// Do we have a boundary?
String boundary = MultiPart.extractBoundary(contentType);
if (boundary == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
Parser parser = new Parser(boundary);
parser.configure(config);
futureParts = parser.parse(content);
attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
return futureParts;
}
return futureParts;
}
/**
* Returns {@code multipart/form-data} parts using {@link MultiPartCompliance#RFC7578}.
* @deprecated use {@link #from(Content.Source, Attributes, String, MultiPartConfig)}.
*/
@Deprecated
public static CompletableFuture<Parts> from(Attributes attributes, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
{
return from(attributes, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP, boundary, parse);
@ -95,11 +132,12 @@ public class MultiPartFormData
* @param boundary the boundary for the {@code multipart/form-data} parts
* @param parse the parser completable future
* @return the future parts
* @deprecated use {@link #from(Content.Source, Attributes, String, MultiPartConfig)}.
*/
@Deprecated
public static CompletableFuture<Parts> from(Attributes attributes, MultiPartCompliance compliance, ComplianceViolation.Listener listener, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
{
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
CompletableFuture<Parts> futureParts = get(attributes);
if (futureParts == null)
{
futureParts = parse.apply(new Parser(boundary, compliance, listener));
@ -108,6 +146,18 @@ public class MultiPartFormData
return futureParts;
}
/**
* Returns {@code multipart/form-data} parts if they have already been created.
*
* @param attributes the attributes where the futureParts are tracked
* @return the future parts
*/
@SuppressWarnings("unchecked")
public static CompletableFuture<Parts> get(Attributes attributes)
{
return (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
}
/**
* <p>An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.</p>
@ -219,9 +269,9 @@ public class MultiPartFormData
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private final MultiPartCompliance compliance;
private final ComplianceViolation.Listener complianceListener;
private boolean useFilesForPartsWithoutFileName;
private MultiPartCompliance compliance;
private ComplianceViolation.Listener complianceListener;
private boolean useFilesForPartsWithoutFileName = true;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
@ -234,6 +284,10 @@ public class MultiPartFormData
this(boundary, MultiPartCompliance.RFC7578, ComplianceViolation.Listener.NOOP);
}
/**
* @deprecated use {@link Parser#Parser(String)} with {@link #configure(MultiPartConfig)}.
*/
@Deprecated
public Parser(String boundary, MultiPartCompliance multiPartCompliance, ComplianceViolation.Listener complianceViolationListener)
{
compliance = Objects.requireNonNull(multiPartCompliance);
@ -429,6 +483,23 @@ public class MultiPartFormData
parser.setMaxParts(maxParts);
}
/**
* Configure the Parser given a {@link MultiPartConfig} instance.
* @param config the configuration.
*/
public void configure(MultiPartConfig config)
{
parser.setMaxParts(config.getMaxParts());
maxMemoryFileSize = config.getMaxMemoryPartSize();
maxFileSize = config.getMaxPartSize();
maxLength = config.getMaxSize();
parser.setPartHeadersMaxLength(config.getMaxHeadersSize());
useFilesForPartsWithoutFileName = config.isUseFilesForPartsWithoutFileName();
filesDirectory = config.getLocation();
complianceListener = config.getViolationListener();
compliance = config.getMultiPartCompliance();
}
// Only used for testing.
int getPartsSize()
{
@ -440,8 +511,7 @@ public class MultiPartFormData
private final AutoLock lock = new AutoLock();
private final List<MultiPart.Part> parts = new ArrayList<>();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private long fileSize;
private long memoryFileSize;
private long size;
private Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
@ -450,22 +520,21 @@ public class MultiPartFormData
public void onPartContent(Content.Chunk chunk)
{
ByteBuffer buffer = chunk.getByteBuffer();
long maxPartSize = getMaxFileSize();
size += buffer.remaining();
if (maxPartSize >= 0 && size > maxPartSize)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxPartSize)));
return;
}
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
long maxMemoryPartSize = getMaxMemoryFileSize();
if (maxMemoryPartSize >= 0)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
{
memoryFileSize += buffer.remaining();
if (memoryFileSize > maxMemoryFileSize)
if (size > maxMemoryPartSize)
{
try
{
@ -482,6 +551,11 @@ public class MultiPartFormData
{
write(c.getByteBuffer());
}
try (AutoLock ignored = lock.lock())
{
this.partChunks.forEach(Content.Chunk::release);
this.partChunks.clear();
}
}
write(buffer);
if (chunk.isLast())
@ -491,16 +565,23 @@ public class MultiPartFormData
{
onFailure(x);
}
try (AutoLock ignored = lock.lock())
{
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return;
}
}
}
else
{
long maxMemoryPartSize = getMaxMemoryFileSize();
if (maxMemoryPartSize >= 0)
{
if (size > maxMemoryPartSize)
{
onFailure(new IllegalStateException("max memory file size exceeded: %d".formatted(maxMemoryPartSize)));
return;
}
}
}
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
@ -541,8 +622,7 @@ public class MultiPartFormData
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
fileSize = 0;
memoryFileSize = 0;
size = 0;
try (AutoLock ignored = lock.lock())
{
// Content-Transfer-Encoding is not a multi-valued field.

View File

@ -58,7 +58,7 @@ public class MultiPartCaptureTest
FS.ensureDirExists(tempDir);
MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
parser.setUseFilesForPartsWithoutFileName(false);
parser.setUseFilesForPartsWithoutFileName(true);
parser.setFilesDirectory(tempDir);
ByteBufferContentSource contentSource = new ByteBufferContentSource(formRequest.asByteBuffer());
MultiPartFormData.Parts parts = parser.parse(contentSource).get();

View File

@ -113,7 +113,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
{
entries.offer(entry);
if (LOG.isDebugEnabled())
LOG.debug("Appended {}, entries={}", entry, entries.size());
LOG.debug("Appended {}, entries={}, {}", entry, entries.size(), this);
}
}
if (closed == null)
@ -132,7 +132,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
{
list.forEach(entries::offer);
if (LOG.isDebugEnabled())
LOG.debug("Appended {}, entries={}", list, entries.size());
LOG.debug("Appended {}, entries={} {}", list, entries.size(), this);
}
}
if (closed == null)
@ -161,7 +161,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
protected Action process() throws Throwable
{
if (LOG.isDebugEnabled())
LOG.debug("Flushing {}", session);
LOG.debug("process {} {}", session, this);
try (AutoLock ignored = lock.lock())
{
@ -184,7 +184,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
if (pendingEntries.isEmpty())
{
if (LOG.isDebugEnabled())
LOG.debug("Flushed {}", session);
LOG.debug("Flushed {} {}", session, this);
return Action.IDLE;
}
@ -257,7 +257,7 @@ public class HTTP2Flusher extends IteratingCallback implements Dumpable
if (LOG.isDebugEnabled())
LOG.debug("Failure generating {}", entry, failure);
failed(failure);
return Action.SUCCEEDED;
return Action.SCHEDULED;
}
}

View File

@ -217,4 +217,10 @@ public class MavenResource extends Resource
return null;
return _resource.resolve(subUriPath);
}
@Override
public String toString()
{
return "(Maven) " + _resource.toString();
}
}

View File

@ -277,4 +277,10 @@ public class SelectiveJarResource extends Resource
}
}
}
@Override
public String toString()
{
return "(Selective Jar/Maven) " + _delegate.toString();
}
}

View File

@ -21,6 +21,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Path;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
@ -34,6 +35,7 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -41,6 +43,8 @@ import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPartCompliance;
import org.eclipse.jetty.http.MultiPartConfig;
import org.eclipse.jetty.http.Trailers;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.internal.CompletionStreamWrapper;
@ -581,6 +585,36 @@ public interface Request extends Attributes, Content.Source
return CookieCache.getCookies(request);
}
/**
* <p>Get a {@link MultiPartConfig.Builder} given a {@link Request} and a location.</p>
*
* <p>If the location is null this will extract the {@link Context} temp directory from the request.
* The {@code maxHeaderSize}, {@link MultiPartCompliance}, {@link ComplianceViolation.Listener}
* are also extracted from the request. Additional settings can be configured through the
* {@link MultiPartConfig.Builder} which is returned.</p>
*
* @param request the request.
* @param location the temp directory location, or null to use the context default.
* @return a {@link MultiPartConfig} with settings extracted from the request.
*/
static MultiPartConfig.Builder getMultiPartConfig(Request request, Path location)
{
HttpChannel httpChannel = HttpChannel.from(request);
HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration();
MultiPartCompliance multiPartCompliance = httpConfiguration.getMultiPartCompliance();
ComplianceViolation.Listener complianceViolationListener = httpChannel.getComplianceViolationListener();
int maxHeaderSize = httpConfiguration.getRequestHeaderSize();
if (location == null)
location = request.getContext().getTempDirectory().toPath();
return new MultiPartConfig.Builder()
.location(location)
.maxHeadersSize(maxHeaderSize)
.complianceMode(multiPartCompliance)
.violationListener(complianceViolationListener);
}
/**
* Generate a proper "Location" header for redirects.
*

View File

@ -715,7 +715,14 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Alias
if (!Resources.isReadable(baseResource))
throw new IllegalArgumentException("Base Resource is not valid: " + baseResource);
if (baseResource.isAlias())
LOG.warn("Base Resource should not be an alias");
{
URI realUri = baseResource.getRealURI();
if (realUri == null)
LOG.warn("Base Resource should not be an alias (100% of requests to context are subject to Security/Alias Checks): {}", baseResource);
else
LOG.warn("Base Resource should not be an alias (100% of requests to context are subject to Security/Alias Checks): {} points to {}",
baseResource, realUri.toASCIIString());
}
}
_availability.set(Availability.STARTING);

View File

@ -202,7 +202,7 @@ public class ReadWriteFailuresTest
POST / HTTP/1.1
Host: localhost
Content-Length: 1
""";
try (LocalConnector.LocalEndPoint endPoint = connector.executeRequest(request))
{
@ -234,7 +234,7 @@ public class ReadWriteFailuresTest
POST / HTTP/1.1
Host: localhost
Content-Length: %d
%s
""".formatted(content.length(), content);
HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request, 5, TimeUnit.SECONDS));

View File

@ -17,6 +17,7 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@ -39,6 +40,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class EventsHandlerTest
{
@ -117,6 +119,7 @@ public class EventsHandlerTest
{
AtomicReference<Long> beginNanoTime = new AtomicReference<>();
AtomicReference<Long> readyNanoTime = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
EventsHandler eventsHandler = new EventsHandler(new EchoHandler())
{
@Override
@ -124,6 +127,7 @@ public class EventsHandlerTest
{
beginNanoTime.set(request.getBeginNanoTime());
readyNanoTime.set(request.getHeadersNanoTime());
latch.countDown();
}
};
startServer(eventsHandler);
@ -148,6 +152,7 @@ public class EventsHandlerTest
String response = endPoint.getResponse();
assertThat(response, containsString("HTTP/1.1 200 OK"));
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertThat(NanoTime.millisSince(beginNanoTime.get()), greaterThan(900L));
assertThat(NanoTime.millisSince(readyNanoTime.get()), greaterThan(450L));
}

View File

@ -78,8 +78,9 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData.Parser(boundary)
.parse(request)
MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
parser.setMaxMemoryFileSize(-1);
parser.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
@ -128,8 +129,9 @@ public class MultiPartFormDataHandlerTest
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData.Parser(boundary)
.parse(request)
MultiPartFormData.Parser parser = new MultiPartFormData.Parser(boundary);
parser.setMaxMemoryFileSize(-1);
parser.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)

View File

@ -303,58 +303,28 @@ public class ExceptionUtil
return t1;
}
public static void callAndThen(Throwable cause, Consumer<Throwable> first, Consumer<Throwable> second)
/** Call a method that handles a {@link Throwable}, catching and associating any exception that it throws.
* @param cause The {@link Throwable} to pass to the consumer
* @param consumer The handler of a {@link Throwable}
*/
public static void call(Throwable cause, Consumer<Throwable> consumer)
{
try
{
first.accept(cause);
consumer.accept(cause);
}
catch (Throwable t)
{
addSuppressedIfNotAssociated(cause, t);
}
finally
{
second.accept(cause);
}
}
public static void callAndThen(Throwable cause, Consumer<Throwable> first, Runnable second)
{
try
{
first.accept(cause);
}
catch (Throwable t)
{
addSuppressedIfNotAssociated(cause, t);
}
finally
{
second.run();
}
}
public static void callAndThen(Runnable first, Runnable second)
{
try
{
first.run();
}
catch (Throwable t)
{
// ignored
}
finally
{
second.run();
ExceptionUtil.addSuppressedIfNotAssociated(t, cause);
throw t;
}
}
/**
* Call a {@link Invocable.Callable} and handle failures
* @param callable The runnable to call
* @param failure The handling of failures
* Call a {@link Invocable.Callable} and handle any resulting failures
* @param callable The {@link org.eclipse.jetty.util.thread.Invocable.Callable} to call
* @param failure A handler of failures from the call
* @see #run(Runnable, Consumer)
*/
public static void call(Invocable.Callable callable, Consumer<Throwable> failure)
{
@ -380,6 +350,7 @@ public class ExceptionUtil
* Call a {@link Runnable} and handle failures
* @param runnable The runnable to call
* @param failure The handling of failures
* @see #call(Throwable, Consumer)
*/
public static void run(Runnable runnable, Consumer<Throwable> failure)
{
@ -401,6 +372,71 @@ public class ExceptionUtil
}
}
/**
* Call a handler of {@link Throwable} and then always call another, suppressing any exceptions thrown.
* @param cause The {@link Throwable} to be passed to both consumers.
* @param call The first {@link Consumer} of {@link Throwable} to call.
* @param then The second {@link Consumer} of {@link Throwable} to call.
*/
public static void callAndThen(Throwable cause, Consumer<Throwable> call, Consumer<Throwable> then)
{
try
{
call.accept(cause);
}
catch (Throwable t)
{
addSuppressedIfNotAssociated(cause, t);
}
finally
{
then.accept(cause);
}
}
/**
* Call a handler of {@link Throwable} and then always call a {@link Runnable}, suppressing any exceptions thrown.
* @param cause The {@link Throwable} to be passed to both consumers.
* @param call The {@link Consumer} of {@link Throwable} to call.
* @param then The {@link Runnable} to call.
*/
public static void callAndThen(Throwable cause, Consumer<Throwable> call, Runnable then)
{
try
{
call.accept(cause);
}
catch (Throwable t)
{
addSuppressedIfNotAssociated(cause, t);
}
finally
{
then.run();
}
}
/**
* Call a {@link Runnable} and then always call another, ignoring any exceptions thrown.
* @param call The first {@link Runnable} to call.
* @param then The second {@link Runnable} to call.
*/
public static void callAndThen(Runnable call, Runnable then)
{
try
{
call.run();
}
catch (Throwable t)
{
// ignored
}
finally
{
then.run();
}
}
/**
* <p>Get from a {@link CompletableFuture} and convert any uncheck exceptions to {@link RuntimeException}.</p>
* @param completableFuture The future to get from.

View File

@ -155,6 +155,6 @@ public class MemoryResource extends Resource
@Override
public String toString()
{
return getName();
return "(Memory) " + _uri.toASCIIString();
}
}

View File

@ -460,4 +460,13 @@ public abstract class Resource implements Iterable<Resource>
}
return false;
}
public String toString()
{
String str = getName();
URI uri = getURI();
if (uri != null)
str = getURI().toASCIIString();
return "(" + this.getClass().getSimpleName() + ") " + str;
}
}

View File

@ -308,7 +308,7 @@ public class URLResourceFactory implements ResourceFactory
@Override
public String toString()
{
return String.format("URLResource@%X(%s)", this.uri.hashCode(), this.uri.toASCIIString());
return "(URL) " + this.uri.toASCIIString();
}
private static class InputStreamReference extends AtomicReference<InputStream> implements Runnable

View File

@ -26,12 +26,11 @@ import java.util.concurrent.CompletableFuture;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Part;
import org.eclipse.jetty.http.ComplianceViolation;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartCompliance;
import org.eclipse.jetty.http.MultiPartConfig;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
@ -39,7 +38,7 @@ import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.StringUtil;
/**
@ -99,16 +98,13 @@ public class ServletMultiPartFormData
return CompletableFuture.failedFuture(new IllegalStateException("No core request"));
// Get a temporary directory for larger parts.
File filesDirectory = StringUtil.isBlank(config.getLocation())
? servletContextRequest.getContext().getTempDirectory()
: new File(config.getLocation());
HttpChannel httpChannel = HttpChannel.from(servletContextRequest);
ComplianceViolation.Listener complianceViolationListener = httpChannel.getComplianceViolationListener();
MultiPartCompliance compliance = servletContextRequest.getConnectionMetaData().getHttpConfiguration().getMultiPartCompliance();
Path filesDirectory = StringUtil.isBlank(config.getLocation())
? servletContextRequest.getContext().getTempDirectory().toPath()
: new File(config.getLocation()).toPath();
// Look for an existing future MultiPartFormData.Parts
CompletableFuture<MultiPartFormData.Parts> futureFormData = MultiPartFormData.from(servletContextRequest, compliance, complianceViolationListener, boundary, parser ->
CompletableFuture<MultiPartFormData.Parts> futureFormData = MultiPartFormData.get(servletContextRequest);
if (futureFormData == null)
{
try
{
@ -131,24 +127,24 @@ public class ServletMultiPartFormData
source = iscs;
}
parser.setMaxParts(contextHandler.getMaxFormKeys());
parser.setFilesDirectory(filesDirectory.toPath());
parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
parser.setMaxFileSize(config.getMaxFileSize());
parser.setMaxLength(config.getMaxRequestSize());
parser.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
MultiPartConfig multiPartConfig = Request.getMultiPartConfig(servletContextRequest, filesDirectory)
.location(filesDirectory)
.maxParts(contextHandler.getMaxFormKeys())
.maxMemoryPartSize(config.getFileSizeThreshold())
.maxPartSize(config.getMaxFileSize())
.maxSize(config.getMaxRequestSize())
.build();
// parse the core parts.
return parser.parse(source);
futureFormData = MultiPartFormData.from(source, servletContextRequest, contentType, multiPartConfig);
}
catch (Throwable failure)
{
return CompletableFuture.failedFuture(failure);
}
});
}
// When available, convert the core parts to servlet parts
futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory.toPath(), formDataParts));
futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory, formDataParts));
// cache the result in attributes.
servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts);

View File

@ -472,6 +472,7 @@ public class MultiPartServletTest
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setMaxParts(1);
formData.setMaxMemoryFileSize(-1);
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
assertThat(parts.size(), is(1));

View File

@ -74,6 +74,8 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Reuse Forks causes test failures -->
<reuseForks>false</reuseForks>
<argLine>@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax
--add-reads org.eclipse.jetty.ee8.servlet=org.eclipse.jetty.logging</argLine>

View File

@ -47,7 +47,6 @@ import org.eclipse.jetty.server.HttpStream;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Session;
import org.eclipse.jetty.session.AbstractSessionManager;
import org.eclipse.jetty.session.DefaultSessionIdManager;
import org.eclipse.jetty.session.ManagedSession;
import org.eclipse.jetty.session.SessionCache;
import org.eclipse.jetty.session.SessionConfig;

View File

@ -72,6 +72,8 @@
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Reuse Forks causes test failures -->
<reuseForks>false</reuseForks>
<argLine>@{argLine} ${jetty.surefire.argLine}
--add-modules org.eclipse.jetty.util.ajax
--add-reads org.eclipse.jetty.ee9.servlet=org.eclipse.jetty.logging</argLine>

View File

@ -18,7 +18,6 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -70,10 +69,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

View File

@ -13,6 +13,8 @@
package org.eclipse.jetty.ee9.servlet;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@ -28,6 +30,8 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.Filter;
@ -59,10 +63,12 @@ import org.eclipse.jetty.toolchain.test.MavenPaths;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.resource.URLResourceFactory;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -2560,6 +2566,49 @@ public class DefaultServletTest
}
}
@Test
public void testGetPrecompressedSuffixMapping() throws Exception
{
Path docRoot = workDir.getEmptyPathDir().resolve("docroot");
FS.ensureDirExists(docRoot);
startServer((context) ->
{
ResourceFactory.registerResourceFactory("file", new URLResourceFactory());
Resource resource = ResourceFactory.of(context).newResource(docRoot);
assertThat("Expecting URLResource", resource.getClass().getName(), endsWith("URLResource"));
context.setBaseResource(resource);
ServletHolder defholder = context.addServlet(DefaultServlet.class, "*.js");
defholder.setInitParameter("cacheControl", "no-store");
defholder.setInitParameter("dirAllowed", "false");
defholder.setInitParameter("gzip", "false");
defholder.setInitParameter("precompressed", "gzip=.gz");
});
FS.ensureDirExists(docRoot.resolve("scripts"));
String scriptText = "This is a script";
Files.writeString(docRoot.resolve("scripts/script.js"), scriptText, UTF_8);
byte[] compressedBytes = compressGzip(scriptText);
Files.write(docRoot.resolve("scripts/script.js.gz"), compressedBytes);
String rawResponse = connector.getResponse("""
GET /context/scripts/script.js HTTP/1.1
Host: test
Accept-Encoding: gzip
Connection: close
""");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat(response.getStatus(), is(HttpStatus.OK_200));
assertThat("Suffix url-pattern mapping not used", response.get(HttpHeader.CACHE_CONTROL), is("no-store"));
String responseDecompressed = decompressGzip(response.getContentBytes());
assertThat(responseDecompressed, is("This is a script"));
}
@Test
public void testHead() throws Exception
{
@ -2950,6 +2999,31 @@ public class DefaultServletTest
return ret;
}
private static byte[] compressGzip(String textToCompress) throws IOException
{
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
ByteArrayInputStream input = new ByteArrayInputStream(textToCompress.getBytes(UTF_8)))
{
IO.copy(input, gzipOut);
gzipOut.flush();
gzipOut.finish();
return baos.toByteArray();
}
}
private static String decompressGzip(byte[] compressedContent) throws IOException
{
try (ByteArrayInputStream input = new ByteArrayInputStream(compressedContent);
GZIPInputStream gzipInput = new GZIPInputStream(input);
ByteArrayOutputStream output = new ByteArrayOutputStream())
{
IO.copy(gzipInput, output);
output.flush();
return output.toString(UTF_8);
}
}
public static class Scenarios extends ArrayList<Arguments>
{
public void addScenario(String description, String rawRequest, int expectedStatus)

View File

@ -66,8 +66,10 @@ import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.hamcrest.Matcher;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@ -75,6 +77,7 @@ import org.slf4j.LoggerFactory;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -124,6 +127,12 @@ public class DispatcherTest
return _contextCollection;
}
@BeforeEach
public void ensureFileSystemPoolIsSane()
{
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
@AfterEach
public void destroy() throws Exception
{

30
pom.xml
View File

@ -201,7 +201,7 @@
<ee10.jakarta.xml.jaxws.impl.version>4.0.2</ee10.jakarta.xml.jaxws.impl.version>
<ee10.jakarta.xml.ws.api.version>4.0.1</ee10.jakarta.xml.ws.api.version>
<ee10.jersey.version>3.1.5</ee10.jersey.version>
<ee10.jsp.impl.version>10.1.16</ee10.jsp.impl.version>
<ee10.jsp.impl.version>10.1.25</ee10.jsp.impl.version>
<ee10.mail.impl.version>2.0.1</ee10.mail.impl.version>
<ee10.weld.version>5.1.2.Final</ee10.weld.version>
@ -226,7 +226,7 @@
<ee11.jakarta.xml.jaxws.impl.version>4.0.2</ee11.jakarta.xml.jaxws.impl.version>
<ee11.jakarta.xml.ws.api.version>4.0.1</ee11.jakarta.xml.ws.api.version>
<ee11.jersey.version>4.0.0-M1</ee11.jersey.version>
<ee11.jsp.impl.version>11.0.0-M20</ee11.jsp.impl.version>
<ee11.jsp.impl.version>11.0.0-M21</ee11.jsp.impl.version>
<ee11.mail.impl.version>2.0.1</ee11.mail.impl.version>
<ee11.weld.version>6.0.0.Beta1</ee11.weld.version>
@ -240,7 +240,7 @@
<ee8.javax.security.auth.message>1.0.0.v201108011116</ee8.javax.security.auth.message>
<ee8.javax.servlet.jsp.jstl.impl.version>1.2.5</ee8.javax.servlet.jsp.jstl.impl.version>
<ee8.jetty.servlet.api.version>4.0.6</ee8.jetty.servlet.api.version>
<ee8.jsp.impl.version>9.0.83.1</ee8.jsp.impl.version>
<ee8.jsp.impl.version>9.0.90</ee8.jsp.impl.version>
<ee8.weld.version>3.1.9.Final</ee8.weld.version>
<ee9.jakarta.activation.api.version>2.0.1</ee9.jakarta.activation.api.version>
@ -285,9 +285,9 @@
<hazelcast.version>5.4.0</hazelcast.version>
<hibernate.search.version>7.1.1.Final</hibernate.search.version>
<infinispan.docker.image.name>infinispan/server</infinispan.docker.image.name>
<infinispan.docker.image.version>15.0.1.Final-1</infinispan.docker.image.version>
<infinispan.protostream.version>5.0.2.Final</infinispan.protostream.version>
<infinispan.version>15.0.1.Final</infinispan.version>
<infinispan.docker.image.version>15.0.5.Final</infinispan.docker.image.version>
<infinispan.protostream.version>5.0.4.Final</infinispan.protostream.version>
<infinispan.version>15.0.5.Final</infinispan.version>
<injection.bundle.version>1.2</injection.bundle.version>
<invoker.mergeUserSettings>false</invoker.mergeUserSettings>
<it.debug>false</it.debug>
@ -367,7 +367,7 @@
<mina.core.version>2.2.3</mina.core.version>
<mongo.docker.version>3.2.20</mongo.docker.version>
<mongodb.version>3.12.14</mongodb.version>
<netty.version>4.1.107.Final</netty.version>
<netty.version>4.1.109.Final</netty.version>
<openpojo.version>0.9.1</openpojo.version>
<org.osgi.annotation.version>8.1.0</org.osgi.annotation.version>
<org.osgi.core.version>8.0.0</org.osgi.core.version>
@ -407,7 +407,7 @@
<tinybundles.version>3.0.0</tinybundles.version>
<versions.maven.plugin.version>2.16.2</versions.maven.plugin.version>
<wildfly.common.version>1.7.0.Final</wildfly.common.version>
<wildfly.elytron.version>2.3.1.Final</wildfly.elytron.version>
<wildfly.elytron.version>2.4.2.Final</wildfly.elytron.version>
<xmemcached.version>2.4.8</xmemcached.version>
</properties>
@ -1305,37 +1305,37 @@
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-digest</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-external</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-gs2</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-gssapi</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-oauth2</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-plain</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
<dependency>
<groupId>org.wildfly.security</groupId>
<artifactId>wildfly-elytron-sasl-scram</artifactId>
<version>2.3.1.Final</version>
<version>${wildfly.elytron.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -0,0 +1,559 @@
//
// ========================================================================
// 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;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.zip.GZIPInputStream;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.InputStreamResponseListener;
import org.eclipse.jetty.client.MultiPartRequestContent;
import org.eclipse.jetty.client.OutputStreamRequestContent;
import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartConfig;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class CoreMultiPartTest
{
private static final int MAX_FILE_SIZE = 512 * 1024;
private Server server;
private ServerConnector connector;
private HttpClient client;
private Path tmpDir;
private MultiPartConfig config;
@BeforeEach
public void before() throws Exception
{
tmpDir = Files.createTempDirectory(CoreMultiPartTest.class.getSimpleName());
}
private MultiPartConfig multiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
{
return new MultiPartConfig.Builder()
.location(location)
.maxParts(maxFormKeys)
.maxSize(maxRequestSize)
.maxPartSize(maxFileSize)
.maxMemoryPartSize(fileSizeThreshold)
.useFilesForPartsWithoutFileName(true)
.build();
}
private void start(Handler handler, MultiPartConfig config) throws Exception
{
this.config = config == null ? multiPartConfig(tmpDir, -1, -1, MAX_FILE_SIZE, 0) : config;
server = new Server(null, null, null);
connector = new ServerConnector(server);
server.addConnector(connector);
server.setHandler(handler);
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes("multipart/form-data");
gzipHandler.setMinGzipSize(32);
server.insertHandler(gzipHandler);
server.start();
client = new HttpClient();
client.start();
}
@AfterEach
public void stop() throws Exception
{
LifeCycle.stop(client);
LifeCycle.stop(server);
IO.delete(tmpDir.toFile());
}
private MultiPartFormData.Parts getParts(Request request, MultiPartConfig config)
{
try
{
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
return MultiPartFormData.from(request, request, contentType, config).get();
}
catch (Throwable t)
{
throw new BadMessageException("bad multipart", t.getCause());
}
}
@Test
public void testLargePart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, multiPartConfig(null, -1, -1, 1024 * 1024, -1));
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
// The write side will eventually throw because connection is closed.
assertThrows(Throwable.class, () ->
{
// Write large amount of content to the part.
byte[] byteArray = new byte[1024 * 1024];
Arrays.fill(byteArray, (byte)1);
for (int i = 0; i < 1024 * 2; i++)
{
content.getOutputStream().write(byteArray);
}
content.close();
});
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
}
@Test
public void testManyParts() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, multiPartConfig(null, 1024, -1, -1, -1));
byte[] byteArray = new byte[1024];
Arrays.fill(byteArray, (byte)1);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
for (int i = 0; i < 1024 * 1024; i++)
{
BytesRequestContent content = new BytesRequestContent(byteArray);
multiPart.addPart(new MultiPart.ContentSourcePart("part" + i, null, null, content));
}
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
}
@Test
public void testMaxRequestSize() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, multiPartConfig(tmpDir, -1, 1024, -1, 1024 * 1024 * 8));
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/requestSizeLimit")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
Throwable writeError = null;
try
{
// Write large amount of content to the part.
byte[] byteArray = new byte[1024 * 1024];
Arrays.fill(byteArray, (byte)1);
for (int i = 0; i < 1024 * 1024; i++)
{
content.getOutputStream().write(byteArray);
}
fail("We should never be able to write all the content.");
}
catch (Exception e)
{
writeError = e;
}
assertThat(writeError, instanceOf(EofException.class));
assert400orEof(listener, null);
}
private static void assert400orEof(InputStreamResponseListener listener, Consumer<String> checkbody) throws InterruptedException, TimeoutException
{
// There is a race here, either we fail trying to write some more content OR
// we get 400 response, for some reason reading the content throws EofException.
String responseContent = null;
try
{
org.eclipse.jetty.client.Response response = listener.get(60, TimeUnit.SECONDS);
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
responseContent = IO.toString(listener.getInputStream());
}
catch (ExecutionException | IOException e)
{
Throwable cause = e.getCause();
assertThat(cause, instanceOf(EofException.class));
return;
}
if (checkbody != null)
checkbody.accept(responseContent);
}
@Test
public void testSimpleMultiPart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
MultiPartFormData.Parts parts = getParts(request, config);
assertNotNull(parts);
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
assertEquals("part1", part.getName());
HttpFields fields = part.getHeaders();
assertNotNull(fields);
assertEquals(2, fields.size());
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
String content1 = IO.toString(inputStream, UTF_8);
assertEquals("content1", content1);
callback.succeeded();
return true;
}
}, null);
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String content = """
--A1B2C3
Content-Disposition: form-data; name="part1"
Content-Type: text/plain; charset="UTF-8"
content1
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary="A1B2C3"
Content-Length: $L
""".replace("$L", String.valueOf(content.length()));
output.write(header.getBytes(UTF_8));
output.write(content.getBytes(UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testTempFilesDeletedOnError() throws Exception
{
byte[] bytes = new byte[2 * MAX_FILE_SIZE];
Arrays.fill(bytes, (byte)1);
// Should throw as the max file size is exceeded.
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
getParts(request, config);
callback.succeeded();
return true;
}
}, null);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("largePart", "largeFile.bin", HttpFields.EMPTY, new BytesRequestContent(bytes)));
multiPart.close();
try (StacklessLogging ignored = new StacklessLogging(HttpChannel.class))
{
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send();
assertEquals(400, response.getStatus());
}
String[] fileList = tmpDir.toFile().list();
assertNotNull(fileList);
assertThat(fileList.length, is(0));
}
@Test
public void testDefaultTempDirectory() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
MultiPartConfig conf = Request.getMultiPartConfig(request, config.getLocation())
.maxParts(config.getMaxParts())
.maxSize(config.getMaxSize())
.maxPartSize(config.getMaxPartSize())
.maxMemoryPartSize(config.getMaxMemoryPartSize())
.useFilesForPartsWithoutFileName(config.isUseFilesForPartsWithoutFileName())
.build();
MultiPartFormData.Parts parts = getParts(request, conf);
assertNotNull(parts);
assertEquals(1, parts.size());
MultiPart.Part part = parts.iterator().next();
assertEquals("part1", part.getName());
HttpFields headers = part.getHeaders();
assertNotNull(headers);
assertEquals(2, headers.size());
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
String content1 = IO.toString(inputStream, UTF_8);
assertEquals("content1", content1);
callback.succeeded();
return true;
}
}, multiPartConfig(null, -1, MAX_FILE_SIZE, -1, 0));
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
OutputStream output = socket.getOutputStream();
String content = """
--A1B2C3
Content-Disposition: form-data; name="part1"
Content-Type: text/plain; charset="UTF-8"
content1
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary="A1B2C3"
Content-Length: $L
""".replace("$L", String.valueOf(content.length()));
output.write(header.getBytes(UTF_8));
output.write(content.getBytes(UTF_8));
output.flush();
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
}
}
@Test
public void testMultiPartGzip() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType);
MultiPartRequestContent echoParts = new MultiPartRequestContent(MultiPart.extractBoundary(contentType));
MultiPartFormData.Parts servletParts = getParts(request, config);
for (MultiPart.Part part : servletParts)
{
HttpFields.Mutable partHeaders = HttpFields.build();
for (HttpField field : part.getHeaders())
partHeaders.add(field);
echoParts.addPart(new MultiPart.ContentSourcePart(part.getName(), part.getFileName(), partHeaders, part.getContentSource()));
}
echoParts.close();
IO.copy(Content.Source.asInputStream(echoParts), Content.Sink.asOutputStream(response));
callback.succeeded();
return true;
}
}, null);
// Do not automatically handle gzip.
client.getContentDecoderFactories().clear();
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
StringRequestContent content = new StringRequestContent(contentString);
MultiPartRequestContent multiPartContent = new MultiPartRequestContent();
multiPartContent.addPart(new MultiPart.ContentSourcePart("stringPart", null, HttpFields.EMPTY, content));
multiPartContent.close();
InputStreamResponseListener responseStream = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/echo")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.headers(h -> h.add(HttpHeader.ACCEPT_ENCODING, "gzip"))
.body(multiPartContent)
.send(responseStream);
org.eclipse.jetty.client.Response response = responseStream.get(5, TimeUnit.SECONDS);
HttpFields headers = response.getHeaders();
assertThat(headers.get(HttpHeader.CONTENT_TYPE), startsWith("multipart/form-data"));
assertThat(headers.get(HttpHeader.CONTENT_ENCODING), is("gzip"));
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
String boundary = MultiPart.extractBoundary(contentType);
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setMaxParts(1);
formData.setMaxMemoryFileSize(-1);
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
assertThat(parts.size(), is(1));
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));
}
@Test
public void testDoubleReadFromPart() throws Exception
{
start(new Handler.Abstract()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
PrintWriter writer = new PrintWriter(Content.Sink.asOutputStream(response));
for (MultiPart.Part part : getParts(request, config))
{
String partContent = IO.toString(Content.Source.asInputStream(part.getContentSource()));
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
// We can only consume the getContentSource() once so we must use newContentSource().
partContent = IO.toString(Content.Source.asInputStream(part.newContentSource()));
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
}
writer.close();
callback.succeeded();
return true;
}
}, null);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
StringRequestContent content = new StringRequestContent(contentString);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("myPart", null, HttpFields.EMPTY, content));
multiPart.close();
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send();
assertEquals(200, response.getStatus());
assertThat(response.getContentAsString(), containsString("Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog\n" +
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
}
}