Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.1.x
This commit is contained in:
commit
c7ad22e861
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -217,4 +217,10 @@ public class MavenResource extends Resource
|
|||
return null;
|
||||
return _resource.resolve(subUriPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "(Maven) " + _resource.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -277,4 +277,10 @@ public class SelectiveJarResource extends Resource
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "(Selective Jar/Maven) " + _delegate.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -155,6 +155,6 @@ public class MemoryResource extends Resource
|
|||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getName();
|
||||
return "(Memory) " + _uri.toASCIIString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
30
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue