Fully async Multipart Form handling (#9975)

A fully async ContentSourceCompletableFuture for use by MultiPartFormData and MultiPartByteRanges
Restructure MultiPartFormData to have a Parser class
---------

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
Co-authored-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Greg Wilkins 2023-06-30 17:01:16 +02:00 committed by GitHub
parent dd44b30c3e
commit ec2dbe73a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1515 additions and 1227 deletions

View File

@ -13,11 +13,18 @@
package org.eclipse.jetty.docs.programming;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CharsetStringBuilder;
import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -146,8 +153,92 @@ public class ContentDocs
throw new IllegalStateException("EOF expected");
}
public static class FutureString extends CompletableFuture<String>
{
private final CharsetStringBuilder text;
private final Content.Source source;
public FutureString(Content.Source source, Charset charset)
{
this.source = source;
this.text = CharsetStringBuilder.forCharset(charset);
source.demand(this::onContentAvailable);
}
private void onContentAvailable()
{
while (true)
{
Content.Chunk chunk = source.read();
if (chunk == null)
{
source.demand(this::onContentAvailable);
return;
}
try
{
if (Content.Chunk.isFailure(chunk))
throw chunk.getFailure();
if (chunk.hasRemaining())
text.append(chunk.getByteBuffer());
if (chunk.isLast() && complete(text.build()))
return;
}
catch (Throwable e)
{
completeExceptionally(e);
}
finally
{
chunk.release();
}
}
}
}
public static void testFutureString() throws Exception
{
AsyncContent source = new AsyncContent();
FutureString future = new FutureString(source, StandardCharsets.UTF_8);
if (future.isDone())
throw new IllegalStateException();
Callback.Completable writeCallback = new Callback.Completable();
Content.Sink.write(source, false, "One", writeCallback);
if (!writeCallback.isDone() || future.isDone())
throw new IllegalStateException("Should be consumed");
Content.Sink.write(source, false, "Two", writeCallback);
if (!writeCallback.isDone() || future.isDone())
throw new IllegalStateException("Should be consumed");
Content.Sink.write(source, true, "Three", writeCallback);
if (!writeCallback.isDone() || !future.isDone())
throw new IllegalStateException("Should be consumed");
}
public static class FutureUtf8String extends ContentSourceCompletableFuture<String>
{
private final Utf8StringBuilder builder = new Utf8StringBuilder();
public FutureUtf8String(Content.Source content)
{
super(content);
}
@Override
protected String parse(Content.Chunk chunk) throws Throwable
{
if (chunk.hasRemaining())
builder.append(chunk.getByteBuffer());
return chunk.isLast() ? builder.takeCompleteString(IllegalStateException::new) : null;
}
}
public static void main(String... args) throws Exception
{
testEcho();
testFutureString();
}
}

View File

@ -439,12 +439,12 @@ public class MultiPartRequestContentTest extends AbstractHttpClientServerTest
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
assertEquals("multipart/form-data", HttpField.valueParameters(contentType, null));
String boundary = MultiPart.extractBoundary(contentType);
MultiPartFormData formData = new MultiPartFormData(boundary);
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(tmpDir);
formData.parse(request);
try
{
process(formData.join()); // May block waiting for multipart form data.
process(formData.parse(request).join()); // May block waiting for multipart form data.
response.write(true, BufferUtil.EMPTY_BUFFER, callback);
}
catch (Exception x)

View File

@ -23,12 +23,12 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.thread.AutoLock;
/**
* <p>A {@link CompletableFuture} that is completed when a multipart/byteranges
* content has been parsed asynchronously from a {@link Content.Source} via
* {@link #parse(Content.Source)}.</p>
* has been parsed asynchronously from a {@link Content.Source}.</p>
* <p>Once the parsing of the multipart/byteranges content completes successfully,
* objects of this class are completed with a {@link MultiPartByteRanges.Parts}
* object.</p>
@ -52,75 +52,10 @@ import org.eclipse.jetty.util.thread.AutoLock;
*
* @see Parts
*/
public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.Parts>
public class MultiPartByteRanges
{
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
public MultiPartByteRanges(String boundary)
private MultiPartByteRanges()
{
this.parser = new MultiPart.Parser(boundary, listener);
}
/**
* @return the boundary string
*/
public String getBoundary()
{
return parser.getBoundary();
}
@Override
public boolean completeExceptionally(Throwable failure)
{
listener.fail(failure);
return super.completeExceptionally(failure);
}
/**
* <p>Parses the given multipart/byteranges content.</p>
* <p>Returns this {@code MultiPartByteRanges} object,
* so that it can be used in the typical "fluent" style
* of {@link CompletableFuture}.</p>
*
* @param content the multipart/byteranges content to parse
* @return this {@code MultiPartByteRanges} object
*/
public MultiPartByteRanges parse(Content.Source content)
{
new Runnable()
{
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = content.read();
if (chunk == null)
{
content.demand(this);
return;
}
if (Content.Chunk.isFailure(chunk))
{
listener.onFailure(chunk.getFailure());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast() || isDone())
return;
}
}
}.run();
return this;
}
private void parse(Content.Chunk chunk)
{
if (listener.isFailed())
return;
parser.parse(chunk);
}
/**
@ -267,76 +202,123 @@ public class MultiPartByteRanges extends CompletableFuture<MultiPartByteRanges.P
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
public static class Parser
{
private final AutoLock lock = new AutoLock();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private final List<MultiPart.Part> parts = new ArrayList<>();
private Throwable failure;
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private Parts parts;
private boolean isFailed()
public Parser(String boundary)
{
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
parser = new MultiPart.Parser(boundary, listener);
}
@Override
public void onPartContent(Content.Chunk chunk)
public CompletableFuture<MultiPartByteRanges.Parts> parse(Content.Source content)
{
try (AutoLock ignored = lock.lock())
ContentSourceCompletableFuture<MultiPartByteRanges.Parts> futureParts = new ContentSourceCompletableFuture<>(content)
{
// Retain the chunk because it is stored for later use.
chunk.retain();
partChunks.add(chunk);
}
@Override
protected MultiPartByteRanges.Parts parse(Content.Chunk chunk) throws Throwable
{
if (listener.isFailed())
throw listener.failure;
parser.parse(chunk);
if (listener.isFailed())
throw listener.failure;
return parts;
}
@Override
public boolean completeExceptionally(Throwable failure)
{
boolean failed = super.completeExceptionally(failure);
if (failed)
listener.fail(failure);
return failed;
}
};
futureParts.parse();
return futureParts;
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
/**
* @return the boundary string
*/
public String getBoundary()
{
try (AutoLock ignored = lock.lock())
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return parser.getBoundary();
}
@Override
public void onComplete()
private class PartsListener extends MultiPart.AbstractPartsListener
{
super.onComplete();
List<MultiPart.Part> copy;
try (AutoLock ignored = lock.lock())
{
copy = List.copyOf(parts);
}
complete(new Parts(getBoundary(), copy));
}
private final AutoLock lock = new AutoLock();
private final List<Content.Chunk> partChunks = new ArrayList<>();
private final List<MultiPart.Part> parts = new ArrayList<>();
private Throwable failure;
@Override
public void onFailure(Throwable failure)
{
super.onFailure(failure);
completeExceptionally(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
private boolean isFailed()
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
}
@Override
public void onPartContent(Content.Chunk chunk)
{
try (AutoLock ignored = lock.lock())
{
// Retain the chunk because it is stored for later use.
chunk.retain();
partChunks.add(chunk);
}
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
try (AutoLock ignored = lock.lock())
{
parts.add(new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks)));
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
}
@Override
public void onComplete()
{
super.onComplete();
List<MultiPart.Part> copy;
try (AutoLock ignored = lock.lock())
{
copy = List.copyOf(parts);
Parser.this.parts = new Parts(getBoundary(), copy);
}
}
@Override
public void onFailure(Throwable failure)
{
fail(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
}
partsToFail.forEach(p -> p.fail(cause));
}
}
}

View File

@ -26,8 +26,11 @@ import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
@ -37,8 +40,7 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
/**
* <p>A {@link CompletableFuture} that is completed when a multipart/form-data content
* has been parsed asynchronously from a {@link Content.Source} via {@link #parse(Content.Source)}
* or from one or more {@link Content.Chunk}s via {@link #parse(Content.Chunk)}.</p>
* has been parsed asynchronously from a {@link Content.Source}.</p>
* <p>Once the parsing of the multipart/form-data content completes successfully,
* objects of this class are completed with a {@link Parts} object.</p>
* <p>Objects of this class may be configured to save multipart files in a configurable
@ -67,241 +69,31 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
*
* @see Parts
*/
public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts>
public class MultiPartFormData
{
private static final Logger LOG = LoggerFactory.getLogger(MultiPartFormData.class);
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private boolean useFilesForPartsWithoutFileName;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
private long maxLength = -1;
private long length;
public MultiPartFormData(String boundary)
private MultiPartFormData()
{
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
}
/**
* @return the boundary string
*/
public String getBoundary()
public static CompletableFuture<Parts> from(Attributes attributes, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
{
return parser.getBoundary();
}
/**
* <p>Parses the given multipart/form-data content.</p>
* <p>Returns this {@code MultiPartFormData} object,
* so that it can be used in the typical "fluent"
* style of {@link CompletableFuture}.</p>
*
* @param content the multipart/form-data content to parse
* @return this {@code MultiPartFormData} object
*/
public MultiPartFormData parse(Content.Source content)
{
new Runnable()
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
if (futureParts == null)
{
@Override
public void run()
{
while (true)
{
Content.Chunk chunk = content.read();
if (chunk == null)
{
content.demand(this);
return;
}
if (Content.Chunk.isFailure(chunk))
{
listener.onFailure(chunk.getFailure());
return;
}
parse(chunk);
chunk.release();
if (chunk.isLast() || isDone())
return;
}
}
}.run();
return this;
}
/**
* <p>Parses the given chunk containing multipart/form-data bytes.</p>
* <p>One or more chunks may be passed to this method, until the parsing
* of the multipart/form-data content completes.</p>
*
* @param chunk the {@link Content.Chunk} to parse.
*/
public void parse(Content.Chunk chunk)
{
if (listener.isFailed())
return;
length += chunk.getByteBuffer().remaining();
long max = getMaxLength();
if (max > 0 && length > max)
listener.onFailure(new IllegalStateException("max length exceeded: %d".formatted(max)));
else
parser.parse(chunk);
}
/**
* <p>Returns the default charset as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
* that is the charset specified by the part named {@code _charset_}.</p>
* <p>If that part is not present, returns {@code null}.</p>
*
* @return the default charset specified by the {@code _charset_} part,
* or null if that part is not present
*/
public Charset getDefaultCharset()
{
return listener.getDefaultCharset();
}
/**
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public int getPartHeadersMaxLength()
{
return parser.getPartHeadersMaxLength();
}
/**
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public void setPartHeadersMaxLength(int partHeadersMaxLength)
{
parser.setPartHeadersMaxLength(partHeadersMaxLength);
}
/**
* @return whether parts without fileName may be stored as files
*/
public boolean isUseFilesForPartsWithoutFileName()
{
return useFilesForPartsWithoutFileName;
}
/**
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
*/
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
{
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
}
/**
* @return the directory where files are saved
*/
public Path getFilesDirectory()
{
return filesDirectory;
}
/**
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
*
* @param filesDirectory the directory where files are saved
*/
public void setFilesDirectory(Path filesDirectory)
{
this.filesDirectory = filesDirectory;
}
/**
* @return the maximum file size in bytes, or -1 for unlimited file size
*/
public long getMaxFileSize()
{
return maxFileSize;
}
/**
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
*/
public void setMaxFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public long getMaxMemoryFileSize()
{
return maxMemoryFileSize;
}
/**
* <p>Sets the maximum memory file size in bytes, after which files will be saved
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
* <p>Use value {@code 0} to always save the files in the directory.</p>
* <p>Use value {@code -1} to never save the files in the directory.</p>
*
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public void setMaxMemoryFileSize(long maxMemoryFileSize)
{
this.maxMemoryFileSize = maxMemoryFileSize;
}
/**
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public long getMaxLength()
{
return maxLength;
}
/**
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public void setMaxLength(long maxLength)
{
this.maxLength = maxLength;
}
/**
* @return the maximum number of parts that can be parsed from the multipart content.
*/
public long getMaxParts()
{
return parser.getMaxParts();
}
/**
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
*/
public void setMaxParts(long maxParts)
{
parser.setMaxParts(maxParts);
}
@Override
public boolean completeExceptionally(Throwable failure)
{
listener.fail(failure);
return super.completeExceptionally(failure);
}
// Only used for testing.
int getPartsSize()
{
return listener.getPartsSize();
futureParts = parse.apply(new Parser(boundary));
attributes.setAttribute(MultiPartFormData.class.getName(), futureParts);
}
return futureParts;
}
/**
* <p>An ordered list of {@link MultiPart.Part}s that can
* be accessed by index or by name, or iterated over.</p>
*/
public class Parts implements Iterable<MultiPart.Part>, Closeable
public static class Parts implements Iterable<MultiPart.Part>, Closeable
{
private final List<MultiPart.Part> parts;
@ -310,11 +102,6 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
this.parts = parts;
}
public MultiPartFormData getMultiPartFormData()
{
return MultiPartFormData.this;
}
/**
* <p>Returns the {@link MultiPart.Part} at the given index, a number
* between {@code 0} included and the value returned by {@link #size()}
@ -409,251 +196,447 @@ public class MultiPartFormData extends CompletableFuture<MultiPartFormData.Parts
}
}
private class PartsListener extends MultiPart.AbstractPartsListener
public static class Parser
{
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 Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
private final PartsListener listener = new PartsListener();
private final MultiPart.Parser parser;
private boolean useFilesForPartsWithoutFileName;
private Path filesDirectory;
private long maxFileSize = -1;
private long maxMemoryFileSize;
private long maxLength = -1;
private long length;
private Parts parts;
@Override
public void onPartContent(Content.Chunk chunk)
public Parser(String boundary)
{
ByteBuffer buffer = chunk.getByteBuffer();
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
parser = new MultiPart.Parser(Objects.requireNonNull(boundary), listener);
}
public CompletableFuture<Parts> parse(Content.Source content)
{
ContentSourceCompletableFuture<Parts> futureParts = new ContentSourceCompletableFuture<>(content)
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
@Override
protected Parts parse(Content.Chunk chunk) throws Throwable
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
if (listener.isFailed())
throw listener.failure;
length += chunk.getByteBuffer().remaining();
long max = getMaxLength();
if (max >= 0 && length > max)
throw new IllegalStateException("max length exceeded: %d".formatted(max));
parser.parse(chunk);
if (listener.isFailed())
throw listener.failure;
return parts;
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
@Override
public boolean completeExceptionally(Throwable failure)
{
memoryFileSize += buffer.remaining();
if (memoryFileSize > maxMemoryFileSize)
{
try
{
// Must save to disk.
if (ensureFileChannel())
{
// Write existing memory chunks.
List<Content.Chunk> partChunks;
try (AutoLock ignored = lock.lock())
{
partChunks = List.copyOf(this.partChunks);
}
for (Content.Chunk c : partChunks)
{
write(c.getByteBuffer());
}
}
write(buffer);
if (chunk.isLast())
close();
}
catch (Throwable x)
{
onFailure(x);
}
try (AutoLock ignored = lock.lock())
{
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return;
}
boolean failed = super.completeExceptionally(failure);
if (failed)
listener.fail(failure);
return failed;
}
}
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
{
partChunks.add(chunk);
}
};
futureParts.parse();
return futureParts;
}
private void write(ByteBuffer buffer) throws Exception
/**
* @return the boundary string
*/
public String getBoundary()
{
int remaining = buffer.remaining();
while (remaining > 0)
{
SeekableByteChannel channel = fileChannel();
if (channel == null)
throw new IllegalStateException();
int written = channel.write(buffer);
if (written == 0)
throw new NonWritableChannelException();
remaining -= written;
}
return parser.getBoundary();
}
private void close()
/**
* <p>Returns the default charset as specified by
* <a href="https://datatracker.ietf.org/doc/html/rfc7578#section-4.6">RFC 7578, section 4.6</a>,
* that is the charset specified by the part named {@code _charset_}.</p>
* <p>If that part is not present, returns {@code null}.</p>
*
* @return the default charset specified by the {@code _charset_} part,
* or null if that part is not present
*/
public Charset getDefaultCharset()
{
try
{
Closeable closeable = fileChannel();
if (closeable != null)
closeable.close();
}
catch (Throwable x)
{
onFailure(x);
}
return listener.getDefaultCharset();
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
/**
* @return the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public int getPartHeadersMaxLength()
{
fileSize = 0;
memoryFileSize = 0;
try (AutoLock ignored = lock.lock())
{
MultiPart.Part part;
if (fileChannel != null)
part = new MultiPart.PathPart(name, fileName, headers, filePath);
else
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
// Reset part-related state.
filePath = null;
fileChannel = null;
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
// Store the new part.
parts.add(part);
}
return parser.getPartHeadersMaxLength();
}
@Override
public void onComplete()
/**
* @param partHeadersMaxLength the max length of a {@link MultiPart.Part} headers, in bytes, or -1 for unlimited length
*/
public void setPartHeadersMaxLength(int partHeadersMaxLength)
{
super.onComplete();
List<MultiPart.Part> result;
try (AutoLock ignored = lock.lock())
{
result = List.copyOf(parts);
}
complete(new Parts(result));
parser.setPartHeadersMaxLength(partHeadersMaxLength);
}
Charset getDefaultCharset()
/**
* @return whether parts without fileName may be stored as files
*/
public boolean isUseFilesForPartsWithoutFileName()
{
try (AutoLock ignored = lock.lock())
{
return parts.stream()
.filter(part -> "_charset_".equals(part.getName()))
.map(part -> part.getContentAsString(US_ASCII))
.map(Charset::forName)
.findFirst()
.orElse(null);
}
return useFilesForPartsWithoutFileName;
}
/**
* @param useFilesForPartsWithoutFileName whether parts without fileName may be stored as files
*/
public void setUseFilesForPartsWithoutFileName(boolean useFilesForPartsWithoutFileName)
{
this.useFilesForPartsWithoutFileName = useFilesForPartsWithoutFileName;
}
/**
* @return the directory where files are saved
*/
public Path getFilesDirectory()
{
return filesDirectory;
}
/**
* <p>Sets the directory where the files uploaded in the parts will be saved.</p>
*
* @param filesDirectory the directory where files are saved
*/
public void setFilesDirectory(Path filesDirectory)
{
this.filesDirectory = filesDirectory;
}
/**
* @return the maximum file size in bytes, or -1 for unlimited file size
*/
public long getMaxFileSize()
{
return maxFileSize;
}
/**
* @param maxFileSize the maximum file size in bytes, or -1 for unlimited file size
*/
public void setMaxFileSize(long maxFileSize)
{
this.maxFileSize = maxFileSize;
}
/**
* @return the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public long getMaxMemoryFileSize()
{
return maxMemoryFileSize;
}
/**
* <p>Sets the maximum memory file size in bytes, after which files will be saved
* in the directory specified by {@link #setFilesDirectory(Path)}.</p>
* <p>Use value {@code 0} to always save the files in the directory.</p>
* <p>Use value {@code -1} to never save the files in the directory.</p>
*
* @param maxMemoryFileSize the maximum memory file size in bytes, or -1 for unlimited memory file size
*/
public void setMaxMemoryFileSize(long maxMemoryFileSize)
{
this.maxMemoryFileSize = maxMemoryFileSize;
}
/**
* @return the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public long getMaxLength()
{
return maxLength;
}
/**
* @param maxLength the maximum length in bytes of the whole multipart content, or -1 for unlimited length
*/
public void setMaxLength(long maxLength)
{
this.maxLength = maxLength;
}
/**
* @return the maximum number of parts that can be parsed from the multipart content.
*/
public long getMaxParts()
{
return parser.getMaxParts();
}
/**
* @param maxParts the maximum number of parts that can be parsed from the multipart content.
*/
public void setMaxParts(long maxParts)
{
parser.setMaxParts(maxParts);
}
// Only used for testing.
int getPartsSize()
{
try (AutoLock ignored = lock.lock())
{
return parts.size();
}
return listener.getPartsSize();
}
@Override
public void onFailure(Throwable failure)
private class PartsListener extends MultiPart.AbstractPartsListener
{
super.onFailure(failure);
completeExceptionally(failure);
}
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 Path filePath;
private SeekableByteChannel fileChannel;
private Throwable failure;
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
@Override
public void onPartContent(Content.Chunk chunk)
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
close();
delete();
}
ByteBuffer buffer = chunk.getByteBuffer();
String fileName = getFileName();
if (fileName != null || isUseFilesForPartsWithoutFileName())
{
long maxFileSize = getMaxFileSize();
fileSize += buffer.remaining();
if (maxFileSize >= 0 && fileSize > maxFileSize)
{
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
return;
}
private SeekableByteChannel fileChannel()
{
try (AutoLock ignored = lock.lock())
{
return fileChannel;
}
}
long maxMemoryFileSize = getMaxMemoryFileSize();
if (maxMemoryFileSize >= 0)
{
memoryFileSize += buffer.remaining();
if (memoryFileSize > maxMemoryFileSize)
{
try
{
// Must save to disk.
if (ensureFileChannel())
{
// Write existing memory chunks.
List<Content.Chunk> partChunks;
try (AutoLock ignored = lock.lock())
{
partChunks = List.copyOf(this.partChunks);
}
for (Content.Chunk c : partChunks)
{
write(c.getByteBuffer());
}
}
write(buffer);
if (chunk.isLast())
close();
}
catch (Throwable x)
{
onFailure(x);
}
private void delete()
{
try
{
Path path = null;
try (AutoLock ignored = lock.lock())
{
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
return;
}
}
}
// Retain the chunk because it is stored for later use.
chunk.retain();
try (AutoLock ignored = lock.lock())
{
if (filePath != null)
path = filePath;
partChunks.add(chunk);
}
}
private void write(ByteBuffer buffer) throws Exception
{
int remaining = buffer.remaining();
while (remaining > 0)
{
SeekableByteChannel channel = fileChannel();
if (channel == null)
throw new IllegalStateException();
int written = channel.write(buffer);
if (written == 0)
throw new NonWritableChannelException();
remaining -= written;
}
}
private void close()
{
try
{
Closeable closeable = fileChannel();
if (closeable != null)
closeable.close();
}
catch (Throwable x)
{
onFailure(x);
}
}
@Override
public void onPart(String name, String fileName, HttpFields headers)
{
fileSize = 0;
memoryFileSize = 0;
try (AutoLock ignored = lock.lock())
{
MultiPart.Part part;
if (fileChannel != null)
part = new MultiPart.PathPart(name, fileName, headers, filePath);
else
part = new MultiPart.ChunksPart(name, fileName, headers, List.copyOf(partChunks));
// Reset part-related state.
filePath = null;
fileChannel = null;
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
// Store the new part.
parts.add(part);
}
if (path != null)
Files.delete(path);
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("IGNORED", x);
}
}
private boolean isFailed()
{
try (AutoLock ignored = lock.lock())
@Override
public void onComplete()
{
return failure != null;
super.onComplete();
List<MultiPart.Part> result;
try (AutoLock ignored = lock.lock())
{
result = List.copyOf(parts);
Parser.this.parts = new Parts(result);
}
}
}
private boolean ensureFileChannel()
{
try (AutoLock ignored = lock.lock())
Charset getDefaultCharset()
{
if (fileChannel != null)
return false;
createFileChannel();
return true;
try (AutoLock ignored = lock.lock())
{
return parts.stream()
.filter(part -> "_charset_".equals(part.getName()))
.map(part -> part.getContentAsString(US_ASCII))
.map(Charset::forName)
.findFirst()
.orElse(null);
}
}
}
private void createFileChannel()
{
try (AutoLock ignored = lock.lock())
int getPartsSize()
{
Path directory = getFilesDirectory();
Files.createDirectories(directory);
String fileName = "MultiPart";
filePath = Files.createTempFile(directory, fileName, "");
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
try (AutoLock ignored = lock.lock())
{
return parts.size();
}
}
catch (Throwable x)
@Override
public void onFailure(Throwable failure)
{
onFailure(x);
fail(failure);
}
private void fail(Throwable cause)
{
List<MultiPart.Part> partsToFail;
try (AutoLock ignored = lock.lock())
{
if (failure != null)
return;
failure = cause;
partsToFail = List.copyOf(parts);
parts.clear();
partChunks.forEach(Content.Chunk::release);
partChunks.clear();
}
partsToFail.forEach(p -> p.fail(cause));
close();
delete();
}
private SeekableByteChannel fileChannel()
{
try (AutoLock ignored = lock.lock())
{
return fileChannel;
}
}
private void delete()
{
try
{
Path path = null;
try (AutoLock ignored = lock.lock())
{
if (filePath != null)
path = filePath;
filePath = null;
fileChannel = null;
}
if (path != null)
Files.delete(path);
}
catch (Throwable x)
{
if (LOG.isTraceEnabled())
LOG.trace("IGNORED", x);
}
}
private boolean isFailed()
{
try (AutoLock ignored = lock.lock())
{
return failure != null;
}
}
private boolean ensureFileChannel()
{
try (AutoLock ignored = lock.lock())
{
if (fileChannel != null)
return false;
createFileChannel();
return true;
}
}
private void createFileChannel()
{
try (AutoLock ignored = lock.lock())
{
Path directory = getFilesDirectory();
Files.createDirectories(directory);
String fileName = "MultiPart";
filePath = Files.createTempFile(directory, fileName, "");
fileChannel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
}
catch (Throwable x)
{
onFailure(x);
}
}
}
}

View File

@ -16,19 +16,20 @@ package org.eclipse.jetty.http;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@ -71,32 +72,19 @@ public class MultiPartFormDataTest
int leaks = 0;
for (Content.Chunk chunk : _allocatedChunks)
{
// Any release that does not return true is a leak.
if (!chunk.release())
leaks++;
// Any release that does not throw or return true is a leak.
try
{
if (!chunk.release())
leaks++;
}
catch (IllegalStateException ignored)
{
}
}
assertThat("Leaked " + leaks + "/" + _allocatedChunks.size() + " chunk(s)", leaks, is(0));
}
Content.Chunk asChunk(String data, boolean last)
{
byte[] b = data.getBytes(StandardCharsets.UTF_8);
ByteBuffer buffer = BufferUtil.allocate(b.length);
BufferUtil.append(buffer, b);
Content.Chunk chunk = Content.Chunk.from(buffer, last);
_allocatedChunks.add(chunk);
return chunk;
}
Content.Chunk asChunk(ByteBuffer data, boolean last)
{
ByteBuffer buffer = BufferUtil.allocate(data.remaining());
BufferUtil.append(buffer, data);
Content.Chunk chunk = Content.Chunk.from(buffer, last);
_allocatedChunks.add(chunk);
return chunk;
}
@Test
public void testBadMultiPart() throws Exception
{
@ -109,14 +97,14 @@ public class MultiPartFormDataTest
"Content-Disposition: form-data; name=\"fileup\"; filename=\"test.upload\"\r\n" +
"\r\n";
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertInstanceOf(BadMessageException.class, failure);
@ -139,14 +127,14 @@ public class MultiPartFormDataTest
eol +
"--" + boundary + "--" + eol;
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.whenComplete((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -165,14 +153,14 @@ public class MultiPartFormDataTest
String str = eol +
"--" + boundary + "--" + eol;
MultiPartFormData formData = new MultiPartFormData(boundary);
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
formData.whenComplete((parts, failure) ->
Content.Sink.write(source, true, str, Callback.NOOP);
formData.parse(source).whenComplete((parts, failure) ->
{
// No errors and no parts.
assertNull(failure);
@ -213,14 +201,14 @@ public class MultiPartFormDataTest
----\r
""";
MultiPartFormData formData = new MultiPartFormData("");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(str, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, str, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(4));
@ -253,10 +241,10 @@ public class MultiPartFormDataTest
@Test
public void testNoBody() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("boundary");
formData.parse(Content.Chunk.EOF);
formData.handle((parts, failure) ->
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
source.close();
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -268,11 +256,11 @@ public class MultiPartFormDataTest
@Test
public void testBodyWithOnlyCRLF() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("boundary");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("boundary");
String body = " \n\n\n\r\n\r\n\r\n\r\n";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -285,7 +273,7 @@ public class MultiPartFormDataTest
public void testLeadingWhitespaceBodyWithCRLF() throws Exception
{
String body = """
\r
\r
@ -303,14 +291,14 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(2));
MultiPart.Part part1 = parts.getFirst("field1");
@ -340,14 +328,14 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(1024);
formData.setMaxLength(3072);
formData.setMaxMemoryFileSize(50);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
// The first boundary must be on a new line, so the first "part" is not recognized as such.
assertThat(parts.size(), is(1));
@ -361,7 +349,8 @@ public class MultiPartFormDataTest
@Test
public void testDefaultLimits() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
--AaB03x\r
@ -371,9 +360,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -390,7 +378,8 @@ public class MultiPartFormDataTest
@Test
public void testRequestContentTooBig() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxLength(16);
@ -402,9 +391,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -416,7 +404,8 @@ public class MultiPartFormDataTest
@Test
public void testFileTooBig() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxFileSize(16);
@ -428,9 +417,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -442,7 +430,8 @@ public class MultiPartFormDataTest
@Test
public void testTwoFilesOneInMemoryOneOnDisk() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
formData.setMaxMemoryFileSize(chunk.length() + 1);
@ -460,9 +449,8 @@ public class MultiPartFormDataTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(2, parts.size());
@ -482,7 +470,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWrite() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String chunk = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
formData.setMaxMemoryFileSize(chunk.length() + 1);
@ -500,9 +489,8 @@ public class MultiPartFormDataTest
$C$C$C$C\r
--AaB03x--\r
""".replace("$C", chunk);
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(2, parts.size());
@ -528,7 +516,8 @@ public class MultiPartFormDataTest
@Test
public void testPathPartDelete() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String body = """
@ -539,9 +528,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertNotNull(parts);
assertEquals(1, parts.size());
@ -559,7 +547,8 @@ public class MultiPartFormDataTest
@Test
public void testAbort()
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(32);
@ -575,24 +564,27 @@ public class MultiPartFormDataTest
--AaB03x--\r
""";
// Parse only part of the content.
formData.parse(asChunk(body, false));
Content.Sink.write(source, false, body, Callback.NOOP);
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
assertEquals(1, formData.getPartsSize());
// Abort MultiPartFormData.
formData.completeExceptionally(new IOException());
futureParts.completeExceptionally(new IOException());
// Parse the rest of the content.
formData.parse(asChunk(terminator, true));
Content.Sink.write(source, true, terminator, Callback.NOOP);
// Try to get the parts, it should fail.
assertThrows(ExecutionException.class, () -> formData.get(5, TimeUnit.SECONDS));
assertThrows(ExecutionException.class, () -> futureParts.get(5, TimeUnit.SECONDS));
assertEquals(0, formData.getPartsSize());
}
@Test
public void testMaxHeaderLength() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setPartHeadersMaxLength(32);
@ -604,9 +596,8 @@ public class MultiPartFormDataTest
ABCDEFGHIJKLMNOPQRSTUVWXYZ\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
formData.handle((parts, failure) ->
Content.Sink.write(source, true, body, Callback.NOOP);
formData.parse(source).handle((parts, failure) ->
{
assertNull(parts);
assertNotNull(failure);
@ -618,7 +609,8 @@ public class MultiPartFormDataTest
@Test
public void testDefaultCharset() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -645,13 +637,14 @@ public class MultiPartFormDataTest
\r
--AaB03x--\r
""";
formData.parse(asChunk(body1, false));
formData.parse(asChunk(isoCedilla, false));
formData.parse(asChunk(body2, false));
formData.parse(asChunk(utfCedilla, false));
formData.parse(asChunk(terminator, true));
CompletableFuture<MultiPartFormData.Parts> futureParts = formData.parse(source);
Content.Sink.write(source, false, body1, Callback.NOOP);
source.write(false, isoCedilla, Callback.NOOP);
Content.Sink.write(source, false, body2, Callback.NOOP);
source.write(false, utfCedilla, Callback.NOOP);
Content.Sink.write(source, true, terminator, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
try (MultiPartFormData.Parts parts = futureParts.get(5, TimeUnit.SECONDS))
{
Charset defaultCharset = formData.getDefaultCharset();
assertEquals(ISO_8859_1, defaultCharset);
@ -669,7 +662,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWithBackSlashInFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -681,9 +675,9 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -694,7 +688,8 @@ public class MultiPartFormDataTest
@Test
public void testPartWithWindowsFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -706,9 +701,8 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -722,7 +716,8 @@ public class MultiPartFormDataTest
@Disabled
public void testCorrectlyEncodedMSFilename() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setMaxMemoryFileSize(-1);
@ -734,9 +729,8 @@ public class MultiPartFormDataTest
stuffaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(contents, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, contents, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -747,7 +741,8 @@ public class MultiPartFormDataTest
@Test
public void testWriteFilesForPartWithoutFileName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
formData.setUseFilesForPartsWithoutFileName(true);
@ -759,9 +754,8 @@ public class MultiPartFormDataTest
sssaaa\r
--AaB03x--\r
""";
formData.parse(asChunk(body, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, body, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(1));
MultiPart.Part part = parts.get(0);
@ -775,7 +769,8 @@ public class MultiPartFormDataTest
@Test
public void testPartsWithSameName() throws Exception
{
MultiPartFormData formData = new MultiPartFormData("AaB03x");
AsyncContent source = new TestContent();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
String sameNames = """
@ -791,9 +786,8 @@ public class MultiPartFormDataTest
AAAAA\r
--AaB03x--\r
""";
formData.parse(asChunk(sameNames, true));
try (MultiPartFormData.Parts parts = formData.get(5, TimeUnit.SECONDS))
Content.Sink.write(source, true, sameNames, Callback.NOOP);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertEquals(2, parts.size());
@ -810,4 +804,16 @@ public class MultiPartFormDataTest
assertEquals("AAAAA", part2.getContentAsString(formData.getDefaultCharset()));
}
}
private class TestContent extends AsyncContent
{
@Override
public Content.Chunk read()
{
Content.Chunk chunk = super.read();
if (chunk != null && chunk.canRetain())
_allocatedChunks.add(chunk);
return chunk;
}
}
}

View File

@ -0,0 +1,144 @@
//
// ========================================================================
// 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.io.content;
import java.io.EOFException;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.io.Content;
/**
* <p>A utility class to convert content from a {@link Content.Source} to an instance
* available via a {@link CompletableFuture}.</p>
* <p>An example usage to asynchronously read UTF-8 content is:</p>
* <pre>{@code
* public static class CompletableUTF8String extends ContentSourceCompletableFuture<String>;
* {
* private final Utf8StringBuilder builder = new Utf8StringBuilder();
*
* public CompletableUTF8String(Content.Source content)
* {
* super(content);
* }
*
* @Override
* protected String parse(Content.Chunk chunk) throws Throwable
* {
* // Accumulate the chunk bytes.
* if (chunk.hasRemaining())
* builder.append(chunk.getByteBuffer());
*
* // Not the last chunk, the result is not ready yet.
* if (!chunk.isLast())
* return null;
*
* // The result is ready.
* return builder.takeCompleteString(IllegalStateException::new);
* }
* }
*
* new CompletableUTF8String(source).thenAccept(System.err::println);
* }</pre>
*/
public abstract class ContentSourceCompletableFuture<X> extends CompletableFuture<X>
{
private final Content.Source _content;
public ContentSourceCompletableFuture(Content.Source content)
{
_content = content;
}
/**
* <p>Initiates the parsing of the {@link Content.Source}.</p>
* <p>For every valid chunk that is read, {@link #parse(Content.Chunk)}
* is called, until a result is produced that is used to
* complete this {@link CompletableFuture}.</p>
* <p>Internally, this method is called multiple times to progress
* the parsing in response to {@link Content.Source#demand(Runnable)}
* calls.</p>
* <p>Exceptions thrown during parsing result in this
* {@link CompletableFuture} to be completed exceptionally.</p>
*/
public void parse()
{
while (true)
{
Content.Chunk chunk = _content.read();
if (chunk == null)
{
_content.demand(this::parse);
return;
}
if (Content.Chunk.isFailure(chunk))
{
if (!chunk.isLast() && onTransientFailure(chunk.getFailure()))
continue;
completeExceptionally(chunk.getFailure());
return;
}
try
{
X x = parse(chunk);
if (x != null)
{
complete(x);
return;
}
}
catch (Throwable failure)
{
completeExceptionally(failure);
return;
}
finally
{
chunk.release();
}
if (chunk.isLast())
{
completeExceptionally(new EOFException());
return;
}
}
}
/**
* <p>Called by {@link #parse()} to parse a {@link org.eclipse.jetty.io.Content.Chunk}.</p>
*
* @param chunk The chunk containing content to parse. The chunk will never be {@code null} nor a
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}.
* If the chunk is stored away to be used later beyond the scope of this call,
* then implementations must call {@link Content.Chunk#retain()} and
* {@link Content.Chunk#release()} as appropriate.
* @return The parsed {@code X} result instance or {@code null} if parsing is not yet complete
* @throws Throwable If there is an error parsing
*/
protected abstract X parse(Content.Chunk chunk) throws Throwable;
/**
* <p>Callback method that informs the parsing about how to handle transient failures.</p>
*
* @param cause A transient failure obtained by reading a {@link Content.Chunk#isLast() non-last}
* {@link org.eclipse.jetty.io.Content.Chunk#isFailure(Content.Chunk) failure chunk}
* @return {@code true} if the transient failure can be ignored, {@code false} otherwise
*/
protected boolean onTransientFailure(Throwable cause)
{
return false;
}
}

View File

@ -22,6 +22,8 @@ import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.ContentSourceCompletableFuture;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.CharsetStringBuilder;
import org.eclipse.jetty.util.Fields;
@ -33,7 +35,7 @@ import static org.eclipse.jetty.util.UrlEncoded.decodeHexByte;
* A {@link CompletableFuture} that is completed once a {@code application/x-www-form-urlencoded}
* content has been parsed asynchronously from the {@link Content.Source}.
*/
public class FormFields extends CompletableFuture<Fields> implements Runnable
public class FormFields extends ContentSourceCompletableFuture<Fields>
{
public static final String MAX_FIELDS_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormKeys";
public static final String MAX_LENGTH_ATTRIBUTE = "org.eclipse.jetty.server.Request.maxFormContentSize";
@ -57,29 +59,22 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
return StringUtil.isEmpty(cs) ? StandardCharsets.UTF_8 : Charset.forName(cs);
}
public static CompletableFuture<Fields> from(Request request)
{
// TODO make this attributes provided by the ContextRequest wrapper
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, maxFields, maxLength);
}
public static CompletableFuture<Fields> from(Request request, Charset charset)
{
// TODO make this attributes provided by the ContextRequest wrapper
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, charset, maxFields, maxLength);
}
/**
* Set a {@link Fields} or related failure for the request
* @param request The request to which to associate the fields with
* @param fields A {@link CompletableFuture} that will provide either the fields or a failure.
*/
public static void set(Request request, CompletableFuture<Fields> fields)
{
request.setAttribute(FormFields.class.getName(), fields);
}
/**
* @param request The request to enquire from
* @return A {@link CompletableFuture} that will provide either the fields or a failure, or null if none set.
* @see #from(Request)
*
*/
public static CompletableFuture<Fields> get(Request request)
{
Object attr = request.getAttribute(FormFields.class.getName());
@ -88,26 +83,93 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
return EMPTY;
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request)
{
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param charset the {@link Charset} to use for byte to string conversion.
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, Charset charset)
{
int maxFields = getRequestAttribute(request, FormFields.MAX_FIELDS_ATTRIBUTE);
int maxLength = getRequestAttribute(request, FormFields.MAX_LENGTH_ATTRIBUTE);
return from(request, charset, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, int maxFields, int maxLength)
{
Object attr = request.getAttribute(FormFields.class.getName());
return from(request, getFormEncodedCharset(request), maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param request The {@link Request} in which to look for an existing {@link FormFields} attribute,
* using the classname as the attribute name, else the request is used
* as a {@link Content.Source} from which to read the fields and set the attribute.
* @param charset the {@link Charset} to use for byte to string conversion.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
* @see #from(Content.Source, Attributes, Charset, int, int)
*/
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
{
return from(request, request, charset, maxFields, maxLength);
}
/**
* Find or create a {@link FormFields} from a {@link Content.Source}.
* @param source The {@link Content.Source} from which to read the fields.
* @param attributes The {@link Attributes} in which to look for an existing {@link CompletableFuture} of
* {@link FormFields}, using the classname as the attribute name. If not found the attribute
* is set with the created {@link CompletableFuture} of {@link FormFields}.
* @param charset the {@link Charset} to use for byte to string conversion.
* @param maxFields The maximum number of fields to be parsed
* @param maxLength The maximum total size of the fields
* @return A {@link CompletableFuture} that will provide the {@link Fields} or a failure.
*/
static CompletableFuture<Fields> from(Content.Source source, Attributes attributes, Charset charset, int maxFields, int maxLength)
{
Object attr = attributes.getAttribute(FormFields.class.getName());
if (attr instanceof FormFields futureFormFields)
return futureFormFields;
else if (attr instanceof Fields fields)
return CompletableFuture.completedFuture(fields);
Charset charset = getFormEncodedCharset(request);
if (charset == null)
return EMPTY;
return from(request, charset, maxFields, maxLength);
}
public static CompletableFuture<Fields> from(Request request, Charset charset, int maxFields, int maxLength)
{
FormFields futureFormFields = new FormFields(request, charset, maxFields, maxLength);
request.setAttribute(FormFields.class.getName(), futureFormFields);
futureFormFields.run();
FormFields futureFormFields = new FormFields(source, charset, maxFields, maxLength);
attributes.setAttribute(FormFields.class.getName(), futureFormFields);
futureFormFields.parse();
return futureFormFields;
}
@ -126,7 +188,6 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
}
}
private final Content.Source _source;
private final Fields _fields;
private final CharsetStringBuilder _builder;
private final int _maxFields;
@ -136,9 +197,9 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
private int _percent = 0;
private byte _percentCode;
public FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
private FormFields(Content.Source source, Charset charset, int maxFields, int maxSize)
{
_source = source;
super(source);
_maxFields = maxFields;
_maxLength = maxSize;
_builder = CharsetStringBuilder.forCharset(charset);
@ -146,137 +207,91 @@ public class FormFields extends CompletableFuture<Fields> implements Runnable
}
@Override
public void run()
{
Content.Chunk chunk = null;
try
{
while (true)
{
chunk = _source.read();
if (chunk == null)
{
_source.demand(this);
return;
}
if (Content.Chunk.isFailure(chunk))
{
completeExceptionally(chunk.getFailure());
return;
}
while (true)
{
Fields.Field field = parse(chunk);
if (field == null)
break;
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
{
chunk.release();
// Do not double release if completeExceptionally() throws.
chunk = null;
completeExceptionally(new IllegalStateException("form with too many fields"));
return;
}
_fields.add(field);
}
chunk.release();
if (chunk.isLast())
{
// Do not double release if complete() throws.
chunk = null;
complete(_fields);
return;
}
}
}
catch (Throwable x)
{
if (chunk != null)
chunk.release();
completeExceptionally(x);
}
}
protected Fields.Field parse(Content.Chunk chunk) throws CharacterCodingException
protected Fields parse(Content.Chunk chunk) throws CharacterCodingException
{
String value = null;
ByteBuffer buffer = chunk.getByteBuffer();
loop:
while (BufferUtil.hasContent(buffer))
do
{
byte b = buffer.get();
switch (_percent)
loop:
while (BufferUtil.hasContent(buffer))
{
case 1 ->
byte b = buffer.get();
switch (_percent)
{
_percentCode = b;
_percent++;
continue;
case 1 ->
{
_percentCode = b;
_percent++;
continue;
}
case 2 ->
{
_builder.append(decodeHexByte((char)_percentCode, (char)b));
_percent = 0;
continue;
}
}
case 2 ->
if (_name == null)
{
_builder.append(decodeHexByte((char)_percentCode, (char)b));
_percent = 0;
continue;
switch (b)
{
case '=' ->
{
_name = _builder.build();
checkLength(_name);
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
else
{
switch (b)
{
case '&' ->
{
value = _builder.build();
checkLength(value);
break loop;
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
}
}
}
if (_name == null)
if (_name != null)
{
switch (b)
if (value == null && chunk.isLast())
{
case '=' ->
if (_percent > 0)
{
_name = _builder.build();
checkLength(_name);
_builder.append((byte)'%');
_builder.append(_percentCode);
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
value = _builder.build();
checkLength(value);
}
}
else
{
switch (b)
if (value != null)
{
case '&' ->
{
value = _builder.build();
checkLength(value);
break loop;
}
case '+' -> _builder.append((byte)' ');
case '%' -> _percent++;
default -> _builder.append(b);
Fields.Field field = new Fields.Field(_name, value);
_name = null;
value = null;
if (_maxFields >= 0 && _fields.getSize() >= _maxFields)
throw new IllegalStateException("form with too many fields > " + _maxFields);
_fields.add(field);
}
}
}
while (BufferUtil.hasContent(buffer));
if (_name != null)
{
if (value == null && chunk.isLast())
{
if (_percent > 0)
{
_builder.append((byte)'%');
_builder.append(_percentCode);
}
value = _builder.build();
checkLength(value);
}
if (value != null)
{
Fields.Field field = new Fields.Field(_name, value);
_name = null;
return field;
}
}
return null;
return chunk.isLast() ? _fields : null;
}
private void checkLength(String nameOrValue)

View File

@ -13,7 +13,6 @@
package org.eclipse.jetty.server.handler;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
@ -25,8 +24,6 @@ import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Handler;
@ -115,7 +112,6 @@ public class DelayedHandler extends Handler.Wrapper
return switch (mimeType)
{
case FORM_ENCODED -> new UntilFormDelayedProcess(handler, request, response, callback, contentType);
case MULTIPART_FORM_DATA -> new UntilMultiPartDelayedProcess(handler, request, response, callback, contentType);
default -> new UntilContentDelayedProcess(handler, request, response, callback);
};
}
@ -270,102 +266,4 @@ public class DelayedHandler extends Handler.Wrapper
Response.writeError(getRequest(), getResponse(), getCallback(), x);
}
}
protected static class UntilMultiPartDelayedProcess extends DelayedProcess
{
private final MultiPartFormData _formData;
public UntilMultiPartDelayedProcess(Handler handler, Request wrapped, Response response, Callback callback, String contentType)
{
super(handler, wrapped, response, callback);
String boundary = MultiPart.extractBoundary(contentType);
_formData = boundary == null ? null : new MultiPartFormData(boundary);
}
private void process(MultiPartFormData.Parts parts, Throwable x)
{
if (x == null)
{
getRequest().setAttribute(MultiPartFormData.Parts.class.getName(), parts);
super.process();
}
else
{
Response.writeError(getRequest(), getResponse(), getCallback(), x);
}
}
private void executeProcess(MultiPartFormData.Parts parts, Throwable x)
{
if (x == null)
{
// We must execute here as even though we have consumed all the input, we are probably
// invoked in a demand runnable that is serialized with any write callbacks that might be done in process
getRequest().getContext().execute(() -> process(parts, x));
}
else
{
Response.writeError(getRequest(), getResponse(), getCallback(), x);
}
}
@Override
public void delay()
{
if (_formData == null)
{
this.process();
}
else
{
_formData.setFilesDirectory(getRequest().getContext().getTempDirectory().toPath());
readAndParse();
// if we are done already, then we are still in the scope of the original process call and can
// process directly, otherwise we must execute a call to process as we are within a serialized
// demand callback.
if (_formData.isDone())
{
try
{
MultiPartFormData.Parts parts = _formData.join();
process(parts, null);
}
catch (Throwable t)
{
process(null, t);
}
}
else
{
_formData.whenComplete(this::executeProcess);
}
}
}
private void readAndParse()
{
while (!_formData.isDone())
{
Content.Chunk chunk = getRequest().read();
if (chunk == null)
{
getRequest().demand(this::readAndParse);
return;
}
if (Content.Chunk.isFailure(chunk))
{
_formData.completeExceptionally(chunk.getFailure());
return;
}
_formData.parse(chunk);
chunk.release();
if (chunk.isLast())
{
if (!_formData.isDone())
process(null, new IOException("Incomplete multipart"));
return;
}
}
}
}
}

View File

@ -0,0 +1,92 @@
//
// ========================================================================
// 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.server;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.FutureCallback;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FormFieldsTest
{
public static Stream<Arguments> tests()
{
return Stream.of(
Arguments.of(List.of("name=value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("name", "=value", ""), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n", "ame", "=", "value"), UTF_8, -1, -1, Map.of("name", "value")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 2, 4, Map.of("n", "v", "X", "Y")),
Arguments.of(List.of("name=f¤¤&X=Y"), UTF_8, -1, -1, Map.of("name", "f¤¤", "X", "Y")),
Arguments.of(List.of("n=v&X=Y"), UTF_8, 1, -1, null),
Arguments.of(List.of("n=v&X=Y"), UTF_8, -1, 3, null)
);
}
@ParameterizedTest
@MethodSource("tests")
public void testFormFields(List<String> chunks, Charset charset, int maxFields, int maxLength, Map<String, String> expected)
throws Exception
{
AsyncContent source = new AsyncContent();
Attributes attributes = new Attributes.Mapped();
CompletableFuture<Fields> futureFields = FormFields.from(source, attributes, charset, maxFields, maxLength);
assertFalse(futureFields.isDone());
int last = chunks.size() - 1;
FutureCallback eof = new FutureCallback();
for (int i = 0; i <= last; i++)
source.write(i == last, BufferUtil.toBuffer(chunks.get(i), charset), i == last ? eof : Callback.NOOP);
try
{
eof.get(10, TimeUnit.SECONDS);
assertTrue(futureFields.isDone());
Map<String, String> result = new HashMap<>();
for (Fields.Field f : futureFields.get())
result.put(f.getName(), f.getValue());
assertEquals(expected, result);
}
catch (AssertionError e)
{
throw e;
}
catch (Throwable e)
{
assertNull(expected);
}
}
}

View File

@ -119,9 +119,8 @@ public class MultiPartByteRangesTest
assertNotNull(contentType);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges byteRanges = new MultiPartByteRanges(boundary);
byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartByteRanges.Parts parts = byteRanges.join();
MultiPartByteRanges.Parser byteRanges = new MultiPartByteRanges.Parser(boundary);
MultiPartByteRanges.Parts parts = byteRanges.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()))).join();
assertEquals(3, parts.size());
MultiPart.Part part1 = parts.get(0);

View File

@ -17,8 +17,6 @@ import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.file.Path;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
@ -44,7 +42,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ -79,7 +76,8 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
new MultiPartFormData.Parser(boundary)
.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
@ -118,72 +116,6 @@ public class MultiPartFormDataHandlerTest
}
}
@Test
public void testDelayedUntilFormData() throws Exception
{
DelayedHandler delayedHandler = new DelayedHandler();
CountDownLatch processLatch = new CountDownLatch(1);
delayedHandler.setHandler(new Handler.Abstract.NonBlocking()
{
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
processLatch.countDown();
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
assertNotNull(parts);
MultiPart.Part part = parts.get(0);
Content.copy(part.getContentSource(), response, callback);
return true;
}
});
start(delayedHandler);
try (SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", connector.getLocalPort())))
{
String contentBegin = """
--A1B2C3
Content-Disposition: form-data; name="part"
""";
String contentMiddle = """
0123456789\
""";
String contentEnd = """
ABCDEF
--A1B2C3--
""";
String header = """
POST / HTTP/1.1
Host: localhost
Content-Type: multipart/form-data; boundary=A1B2C3
Content-Length: $L
""".replace("$L", String.valueOf(contentBegin.length() + contentMiddle.length() + contentEnd.length()));
client.write(UTF_8.encode(header));
client.write(UTF_8.encode(contentBegin));
// Verify that the handler has not been called yet.
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
client.write(UTF_8.encode(contentMiddle));
// Verify that the handler has not been called yet.
assertFalse(processLatch.await(1, TimeUnit.SECONDS));
// Finish to send the content.
client.write(UTF_8.encode(contentEnd));
// Verify that the handler has been called.
assertTrue(processLatch.await(5, TimeUnit.SECONDS));
HttpTester.Response response = HttpTester.parseResponse(HttpTester.from(client));
assertNotNull(response);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("0123456789ABCDEF", response.getContent());
}
}
@Test
public void testEchoMultiPart() throws Exception
{
@ -193,13 +125,15 @@ public class MultiPartFormDataHandlerTest
public boolean handle(Request request, Response response, Callback callback)
{
String boundary = MultiPart.extractBoundary(request.getHeaders().get(HttpHeader.CONTENT_TYPE));
new MultiPartFormData(boundary).parse(request)
new MultiPartFormData.Parser(boundary)
.parse(request)
.whenComplete((parts, failure) ->
{
if (parts != null)
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(parts.getMultiPartFormData().getBoundary()));
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(parts.getMultiPartFormData().getBoundary());
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "multipart/form-data; boundary=\"%s\"".formatted(boundary));
MultiPartFormData.ContentSource source = new MultiPartFormData.ContentSource(boundary);
source.setPartHeadersMaxLength(1024);
parts.forEach(source::addPart);
source.close();
@ -310,22 +244,22 @@ public class MultiPartFormDataHandlerTest
String boundary = MultiPart.extractBoundary(value);
assertNotNull(boundary);
MultiPartFormData formData = new MultiPartFormData(boundary);
ByteBufferContentSource byteBufferContentSource = new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes()));
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setFilesDirectory(tempDir);
formData.parse(new ByteBufferContentSource(ByteBuffer.wrap(response.getContentBytes())));
MultiPartFormData.Parts parts = formData.join();
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("part1", part1.getName());
assertEquals("hello", part1.getContentAsString(UTF_8));
MultiPart.Part part2 = parts.get(1);
assertEquals("part2", part2.getName());
assertEquals("file2.bin", part2.getFileName());
HttpFields headers2 = part2.getHeaders();
assertEquals(2, headers2.size());
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
assertEquals(32, part2.getContentSource().getLength());
try (MultiPartFormData.Parts parts = formData.parse(byteBufferContentSource).join())
{
assertEquals(2, parts.size());
MultiPart.Part part1 = parts.get(0);
assertEquals("part1", part1.getName());
assertEquals("hello", part1.getContentAsString(UTF_8));
MultiPart.Part part2 = parts.get(1);
assertEquals("part2", part2.getName());
assertEquals("file2.bin", part2.getFileName());
HttpFields headers2 = part2.getHeaders();
assertEquals(2, headers2.size());
assertEquals("application/octet-stream", headers2.get(HttpHeader.CONTENT_TYPE));
}
}
}
}

View File

@ -175,7 +175,7 @@ public class ResourceHandlerByteRangesTest
String contentType = response.get(HttpHeader.CONTENT_TYPE);
assertThat(contentType, startsWith(responseContentType));
String boundary = MultiPart.extractBoundary(contentType);
MultiPartByteRanges.Parts parts = new MultiPartByteRanges(boundary)
MultiPartByteRanges.Parts parts = new MultiPartByteRanges.Parser(boundary)
.parse(new ByteBufferContentSource(response.getContentByteBuffer()))
.join();
assertEquals(2, parts.size());

View File

@ -1984,11 +1984,8 @@ public class GzipHandlerTest
public boolean handle(Request request, Response response, Callback callback) throws Exception
{
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
Fields queryParameters = Request.extractQueryParameters(request);
FormFields futureFormFields = new FormFields(request, StandardCharsets.UTF_8, -1, -1);
futureFormFields.run();
Fields formParameters = futureFormFields.get();
Fields formParameters = FormFields.from(request, UTF_8, -1, -1).get();
Fields parameters = Fields.combine(queryParameters, formParameters);
String dump = parameters.stream().map(f -> "%s: %s\n".formatted(f.getName(), f.getValue())).collect(Collectors.joining());

View File

@ -764,7 +764,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
}
//multipart-config
MultipartConfigElement multipartConfig = ((ServletHolder.Registration)holder.getRegistration()).getMultipartConfig();
MultipartConfigElement multipartConfig = holder.getRegistration().getMultipartConfigElement();
if (multipartConfig != null)
{
out.openTag("multipart-config", origin(md, holder.getName() + ".servlet.multipart-config"));

View File

@ -309,6 +309,15 @@ public class Dispatcher implements RequestDispatcher
{
return null;
}
case ServletContextRequest.MULTIPART_CONFIG_ELEMENT ->
{
// If we already have future parts, return the configuration of the wrapped request.
if (super.getAttribute(ServletMultiPartFormData.class.getName()) != null)
return super.getAttribute(name);
// otherwise, return the configuration of this mapping
return _mappedServlet.getServletHolder().getMultipartConfigElement();
}
default ->
{
return super.getAttribute(name);

View File

@ -0,0 +1,83 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.ee10.servlet;
import java.util.concurrent.CompletableFuture;
import jakarta.servlet.ServletRequest;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.Callback;
/**
* Handler to eagerly and asynchronously read and parse {@link MimeTypes.Type#FORM_ENCODED} and
* {@link MimeTypes.Type#MULTIPART_FORM_DATA} content prior to invoking the {@link ServletHandler},
* which can then consume them with blocking APIs but without blocking.
* @see FormFields#from(Request)
* @see ServletMultiPartFormData#from(ServletRequest)
*/
public class EagerFormHandler extends Handler.Wrapper
{
public EagerFormHandler()
{
this(null);
}
public EagerFormHandler(Handler handler)
{
super(handler);
}
@Override
public boolean handle(Request request, org.eclipse.jetty.server.Response response, Callback callback) throws Exception
{
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
if (contentType == null)
return super.handle(request, response, callback);
MimeTypes.Type mimeType = MimeTypes.getBaseType(contentType);
if (mimeType == null)
return super.handle(request, response, callback);
CompletableFuture<?> future = switch (mimeType)
{
case FORM_ENCODED -> FormFields.from(request);
case MULTIPART_FORM_DATA -> ServletMultiPartFormData.from(Request.as(request, ServletContextRequest.class).getServletApiRequest(), contentType);
default -> null;
};
if (future == null)
return super.handle(request, response, callback);
future.whenComplete((result, failure) ->
{
// The result and failure are not handled here. Rather we call the next handler
// to allow the normal processing to handle the result or failure, which will be
// provided via the attribute to ServletApiRequest#getParts()
try
{
if (!super.handle(request, response, callback))
callback.failed(new IllegalStateException("Not Handled"));
}
catch (Throwable x)
{
callback.failed(x);
}
});
return true;
}
}

View File

@ -34,11 +34,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConnection;
import jakarta.servlet.ServletContext;
@ -489,33 +489,26 @@ public class ServletApiRequest implements HttpServletRequest
{
if (_parts == null)
{
String contentType = getContentType();
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
throw new ServletException("Unsupported Content-Type [%s], expected [%s]".formatted(contentType, MimeTypes.Type.MULTIPART_FORM_DATA.asString()));
MultipartConfigElement config = (MultipartConfigElement)getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
if (config == null)
throw new IllegalStateException("No multipart config for servlet");
ServletContextHandler contextHandler = getServletRequestInfo().getServletContext().getServletContextHandler();
int maxFormContentSize = contextHandler.getMaxFormContentSize();
int maxFormKeys = contextHandler.getMaxFormKeys();
_parts = ServletMultiPartFormData.from(this, maxFormKeys);
Collection<Part> parts = _parts.getParts();
String formCharset = null;
Part charsetPart = _parts.getPart("_charset_");
if (charsetPart != null)
try
{
try (InputStream is = charsetPart.getInputStream())
{
formCharset = IO.toString(is, StandardCharsets.UTF_8);
}
}
CompletableFuture<ServletMultiPartFormData.Parts> futureServletMultiPartFormData = ServletMultiPartFormData.from(this);
/*
Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
_parts = futureServletMultiPartFormData.get();
Collection<Part> parts = _parts.getParts();
String formCharset = null;
Part charsetPart = _parts.getPart("_charset_");
if (charsetPart != null)
{
try (InputStream is = charsetPart.getInputStream())
{
formCharset = IO.toString(is, StandardCharsets.UTF_8);
}
}
/*
Select Charset to use for this part. (NOTE: charset behavior is for the part value only and not the part header/field names)
1. Use the part specific charset as provided in that part's Content-Type header; else
2. Use the overall default charset. Determined by:
a. if part name _charset_ exists, use that part's value.
@ -523,38 +516,66 @@ public class ServletApiRequest implements HttpServletRequest
(note, this can be either from the charset field on the request Content-Type
header, or from a manual call to request.setCharacterEncoding())
c. use utf-8.
*/
Charset defaultCharset;
if (formCharset != null)
defaultCharset = Charset.forName(formCharset);
else if (getCharacterEncoding() != null)
defaultCharset = Charset.forName(getCharacterEncoding());
else
defaultCharset = StandardCharsets.UTF_8;
*/
Charset defaultCharset;
if (formCharset != null)
defaultCharset = Charset.forName(formCharset);
else if (getCharacterEncoding() != null)
defaultCharset = Charset.forName(getCharacterEncoding());
else
defaultCharset = StandardCharsets.UTF_8;
long formContentSize = 0;
for (Part p : parts)
{
if (p.getSubmittedFileName() == null)
// Recheck some constraints here, just in case the preloaded parts were not properly configured.
ServletContextHandler servletContextHandler = getServletRequestInfo().getServletContext().getServletContextHandler();
long maxFormContentSize = servletContextHandler.getMaxFormContentSize();
int maxFormKeys = servletContextHandler.getMaxFormKeys();
long formContentSize = 0;
int count = 0;
for (Part p : parts)
{
formContentSize = Math.addExact(formContentSize, p.getSize());
if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
if (maxFormKeys > 0 && ++count > maxFormKeys)
throw new IllegalStateException("Too many form keys > " + maxFormKeys);
// Servlet Spec 3.0 pg 23, parts without filename must be put into params.
String charset = null;
if (p.getContentType() != null)
charset = MimeTypes.getCharsetFromContentType(p.getContentType());
try (InputStream is = p.getInputStream())
if (p.getSubmittedFileName() == null)
{
String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
if (_contentParameters == null)
_contentParameters = new Fields();
_contentParameters.add(p.getName(), content);
formContentSize = Math.addExact(formContentSize, p.getSize());
if (maxFormContentSize >= 0 && formContentSize > maxFormContentSize)
throw new IllegalStateException("Form is larger than max length " + maxFormContentSize);
// Servlet Spec 3.0 pg 23, parts without filename must be put into params.
String charset = null;
if (p.getContentType() != null)
charset = MimeTypes.getCharsetFromContentType(p.getContentType());
try (InputStream is = p.getInputStream())
{
String content = IO.toString(is, charset == null ? defaultCharset : Charset.forName(charset));
if (_contentParameters == null)
_contentParameters = new Fields();
_contentParameters.add(p.getName(), content);
}
}
}
}
catch (Throwable t)
{
if (LOG.isDebugEnabled())
LOG.debug("getParts", t);
Throwable cause;
if (t instanceof ExecutionException ee)
cause = ee.getCause();
else if (t instanceof ServletException se)
cause = se.getCause();
else
cause = t;
if (cause instanceof IOException ioException)
throw ioException;
throw new ServletException(new BadMessageException("bad multipart", cause));
}
}
return _parts.getParts();
@ -654,6 +675,7 @@ public class ServletApiRequest implements HttpServletRequest
if (_async != null)
{
// This switch works by allowing the attribute to get underneath any dispatch wrapper.
// Note that there are further servlet specific attributes in ServletContextRequest
return switch (name)
{
case AsyncContext.ASYNC_REQUEST_URI -> getRequestURI();
@ -867,13 +889,24 @@ public class ServletApiRequest implements HttpServletRequest
{
getParts();
}
catch (IOException | ServletException e)
catch (IOException e)
{
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
catch (ServletException e)
{
Throwable cause = e.getCause();
if (cause instanceof BadMessageException badMessageException)
throw badMessageException;
String msg = "Unable to extract content parameters";
if (LOG.isDebugEnabled())
LOG.debug(msg, e);
throw new RuntimeIOException(msg, e);
}
}
else
{

View File

@ -32,6 +32,7 @@ import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.http.pathmap.MatchedResource;
import org.eclipse.jetty.server.FormFields;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.SecureRequestCustomizer;
@ -239,6 +240,9 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
case "jakarta.servlet.request.key_size" -> super.getAttribute(SecureRequestCustomizer.KEY_SIZE_ATTRIBUTE);
case "jakarta.servlet.request.ssl_session_id" -> super.getAttribute(SecureRequestCustomizer.SSL_SESSION_ID_ATTRIBUTE);
case "jakarta.servlet.request.X509Certificate" -> super.getAttribute(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE);
case ServletContextRequest.MULTIPART_CONFIG_ELEMENT -> _matchedResource.getResource().getServletHolder().getMultipartConfigElement();
case FormFields.MAX_FIELDS_ATTRIBUTE -> getServletContext().getServletContextHandler().getMaxFormKeys();
case FormFields.MAX_LENGTH_ATTRIBUTE -> getServletContext().getServletContextHandler().getMaxFormContentSize();
default -> super.getAttribute(name);
};
}
@ -255,6 +259,12 @@ public class ServletContextRequest extends ContextRequest implements ServletCont
names.add("jakarta.servlet.request.ssl_session_id");
if (names.contains(SecureRequestCustomizer.PEER_CERTIFICATES_ATTRIBUTE))
names.add("jakarta.servlet.request.X509Certificate");
if (_matchedResource.getResource().getServletHolder().getMultipartConfigElement() != null)
names.add(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
if (getServletContext().getServletContextHandler().getMaxFormKeys() >= 0)
names.add(FormFields.MAX_FIELDS_ATTRIBUTE);
if (getServletContext().getServletContextHandler().getMaxFormContentSize() >= 0L)
names.add(FormFields.MAX_FIELDS_ATTRIBUTE);
return names;
}

View File

@ -74,7 +74,7 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
private Map<String, String> _roleMap;
private String _forcedPath;
private String _runAsRole;
private ServletRegistration.Dynamic _registration;
private ServletHolder.Registration _registration;
private JspContainer _jspContainer;
private volatile Servlet _servlet;
@ -155,6 +155,11 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
setHeldClass(servlet);
}
public MultipartConfigElement getMultipartConfigElement()
{
return _registration == null ? null : _registration.getMultipartConfigElement();
}
/**
* @return The unavailable exception or null if not unavailable
*/
@ -710,14 +715,6 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
{
// Ensure the servlet is initialized prior to any filters being invoked
getServlet();
// Check for multipart config
if (_registration != null)
{
MultipartConfigElement mpce = ((Registration)_registration).getMultipartConfig();
if (mpce != null)
request.setAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT, mpce);
}
}
/**
@ -919,7 +916,7 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
public class Registration extends HolderRegistration implements ServletRegistration.Dynamic
{
protected MultipartConfigElement _multipartConfig;
protected MultipartConfigElement _multipartConfigElement;
@Override
public Set<String> addMapping(String... urlPatterns)
@ -994,12 +991,12 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
@Override
public void setMultipartConfig(MultipartConfigElement element)
{
_multipartConfig = element;
_multipartConfigElement = element;
}
public MultipartConfigElement getMultipartConfig()
public MultipartConfigElement getMultipartConfigElement()
{
return _multipartConfig;
return _multipartConfigElement;
}
@Override
@ -1015,7 +1012,7 @@ public class ServletHolder extends Holder<Servlet> implements Comparable<Servlet
}
}
public ServletRegistration.Dynamic getRegistration()
public ServletHolder.Registration getRegistration()
{
if (_registration == null)
_registration = new Registration();

View File

@ -16,31 +16,32 @@ package org.eclipse.jetty.ee10.servlet;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.Part;
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.MultiPartFormData;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.RetainableByteBuffer;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
/**
* <p>Servlet specific class for multipart content support.</p>
* <p>Use {@link #from(ServletApiRequest)} to
* <p>Use {@link #from(ServletRequest)} to
* parse multipart request content into a {@link Parts} object that can
* be used to access Servlet {@link Part} objects.</p>
*
@ -49,106 +50,103 @@ import org.eclipse.jetty.util.StringUtil;
public class ServletMultiPartFormData
{
/**
* <p>Parses the request content assuming it is a multipart content,
* and returns a {@link Parts} objects that can be used to access
* individual {@link Part}s.</p>
*
* @param request the HTTP request with multipart content
* @return a {@link Parts} object to access the individual {@link Part}s
* @throws IOException if reading the request content fails
* @see org.eclipse.jetty.server.handler.DelayedHandler
* Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
* @param servletRequest A servlet request
* @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
* @see #from(ServletRequest, String)
*/
public static Parts from(ServletApiRequest request) throws IOException
public static CompletableFuture<Parts> from(ServletRequest servletRequest)
{
return from(request, ServletContextHandler.DEFAULT_MAX_FORM_KEYS);
return from(servletRequest, servletRequest.getContentType());
}
/**
* <p>Parses the request content assuming it is a multipart content,
* and returns a {@link Parts} objects that can be used to access
* individual {@link Part}s.</p>
*
* @param request the HTTP request with multipart content
* @return a {@link Parts} object to access the individual {@link Part}s
* @throws IOException if reading the request content fails
* @see org.eclipse.jetty.server.handler.DelayedHandler
* Get future {@link ServletMultiPartFormData.Parts} from a servlet request.
* @param servletRequest A servlet request
* @param contentType The contentType, passed as an optimization as it has likely already been retrieved.
* @return A future {@link ServletMultiPartFormData.Parts}, which may have already been created and/or completed.
*/
public static Parts from(ServletApiRequest request, int maxParts) throws IOException
public static CompletableFuture<Parts> from(ServletRequest servletRequest, String contentType)
{
try
// Look for an existing future (we use the future here rather than the parts as it can remember any failure).
@SuppressWarnings("unchecked")
CompletableFuture<Parts> futureServletParts = (CompletableFuture<Parts>)servletRequest.getAttribute(ServletMultiPartFormData.class.getName());
if (futureServletParts == null)
{
// Look for a previously read and parsed MultiPartFormData from the DelayedHandler.
MultiPartFormData.Parts parts = (MultiPartFormData.Parts)request.getAttribute(MultiPartFormData.Parts.class.getName());
if (parts != null)
return new Parts(parts);
// No existing parts, so we need to try to read them ourselves
// TODO set the files directory
return new ServletMultiPartFormData().parse(request, maxParts);
}
catch (Throwable x)
{
throw IO.rethrow(x);
}
}
// Is this servlet a valid target for Multipart?
MultipartConfigElement config = (MultipartConfigElement)servletRequest.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
if (config == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart configuration element"));
private Parts parse(ServletApiRequest request, int maxParts) throws IOException
{
MultipartConfigElement config = (MultipartConfigElement)request.getAttribute(ServletContextRequest.MULTIPART_CONFIG_ELEMENT);
if (config == null)
throw new IllegalStateException("No multipart configuration element");
// Are we the right content type to produce our own parts?
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
String boundary = MultiPart.extractBoundary(request.getContentType());
if (boundary == null)
throw new IllegalStateException("No multipart boundary parameter in Content-Type");
// Do we have a boundary?
String boundary = MultiPart.extractBoundary(servletRequest.getContentType());
if (boundary == null)
return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
// Store MultiPartFormData as attribute on request so it is released by the HttpChannel.
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setMaxParts(maxParts);
// Can we access the core request, needed for components (eg buffer pools, temp directory, etc.) as well
// as IO optimization
ServletContextRequest servletContextRequest = ServletContextRequest.getServletContextRequest(servletRequest);
if (servletContextRequest == null)
return CompletableFuture.failedFuture(new IllegalStateException("No core request"));
File tmpDirFile = (File)request.getServletContext().getAttribute(ServletContext.TEMPDIR);
if (tmpDirFile == null)
tmpDirFile = new File(System.getProperty("java.io.tmpdir"));
String fileLocation = config.getLocation();
if (!StringUtil.isBlank(fileLocation))
tmpDirFile = new File(fileLocation);
// Get a temporary directory for larger parts.
File filesDirectory = StringUtil.isBlank(config.getLocation())
? servletContextRequest.getContext().getTempDirectory()
: new File(config.getLocation());
formData.setFilesDirectory(tmpDirFile.toPath());
formData.setMaxMemoryFileSize(config.getFileSizeThreshold());
formData.setMaxFileSize(config.getMaxFileSize());
formData.setMaxLength(config.getMaxRequestSize());
ConnectionMetaData connectionMetaData = request.getRequest().getConnectionMetaData();
formData.setPartHeadersMaxLength(connectionMetaData.getHttpConfiguration().getRequestHeaderSize());
ByteBufferPool byteBufferPool = request.getRequest().getComponents().getByteBufferPool();
Connection connection = connectionMetaData.getConnection();
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
InputStream input = request.getInputStream();
while (!formData.isDone())
{
RetainableByteBuffer retainable = byteBufferPool.acquire(bufferSize, false);
boolean readEof = false;
ByteBuffer buffer = retainable.getByteBuffer();
while (BufferUtil.space(buffer) > bufferSize / 2)
// Look for an existing future MultiPartFormData.Parts
CompletableFuture<MultiPartFormData.Parts> futureFormData = MultiPartFormData.from(servletContextRequest, boundary, parser ->
{
int read = BufferUtil.readFrom(input, buffer);
if (read < 0)
try
{
readEof = true;
break;
// No existing core parts, so we need to configure the parser.
ServletContextHandler contextHandler = servletContextRequest.getServletContext().getServletContextHandler();
ByteBufferPool byteBufferPool = servletContextRequest.getComponents().getByteBufferPool();
ConnectionMetaData connectionMetaData = servletContextRequest.getConnectionMetaData();
Connection connection = connectionMetaData.getConnection();
Content.Source source;
if (servletRequest instanceof ServletApiRequest servletApiRequest)
{
source = servletApiRequest.getRequest();
}
else
{
int bufferSize = connection instanceof AbstractConnection c ? c.getInputBufferSize() : 2048;
InputStreamContentSource iscs = new InputStreamContentSource(servletRequest.getInputStream(), byteBufferPool);
iscs.setBufferSize(bufferSize);
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());
// parse the core parts.
return parser.parse(source);
}
}
catch (Throwable failure)
{
return CompletableFuture.failedFuture(failure);
}
});
formData.parse(Content.Chunk.from(buffer, false, retainable::release));
if (readEof)
{
formData.parse(Content.Chunk.EOF);
break;
}
// When available, convert the core parts to servlet parts
futureServletParts = futureFormData.thenApply(formDataParts -> new Parts(filesDirectory.toPath(), formDataParts));
// cache the result in attributes.
servletRequest.setAttribute(ServletMultiPartFormData.class.getName(), futureServletParts);
}
Parts parts = new Parts(formData.join());
request.setAttribute(Parts.class.getName(), parts);
return parts;
return futureServletParts;
}
/**
@ -158,9 +156,9 @@ public class ServletMultiPartFormData
{
private final List<Part> parts = new ArrayList<>();
public Parts(MultiPartFormData.Parts parts)
public Parts(Path directory, MultiPartFormData.Parts parts)
{
parts.forEach(part -> this.parts.add(new ServletPart(parts.getMultiPartFormData(), part)));
parts.forEach(part -> this.parts.add(new ServletPart(directory, part)));
}
public Part getPart(String name)
@ -179,12 +177,12 @@ public class ServletMultiPartFormData
private static class ServletPart implements Part
{
private final MultiPartFormData _formData;
private final Path _directory;
private final MultiPart.Part _part;
private ServletPart(MultiPartFormData formData, MultiPart.Part part)
private ServletPart(Path directory, MultiPart.Part part)
{
_formData = formData;
_directory = directory;
_part = part;
}
@ -222,8 +220,8 @@ public class ServletMultiPartFormData
public void write(String fileName) throws IOException
{
Path filePath = Path.of(fileName);
if (!filePath.isAbsolute())
filePath = _formData.getFilesDirectory().resolve(filePath).normalize();
if (!filePath.isAbsolute() && Files.isDirectory(_directory))
filePath = _directory.resolve(filePath).normalize();
_part.writeTo(filePath);
}

View File

@ -17,7 +17,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
@ -51,9 +50,9 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.MultiPart;
import org.eclipse.jetty.http.MultiPartFormData;
import org.eclipse.jetty.io.ByteBufferPool;
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.Server;
import org.eclipse.jetty.server.ServerConnector;
@ -62,7 +61,8 @@ 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 org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
@ -93,32 +93,27 @@ public class MultiPartServletTest
tmpDirString = tmpDir.toAbsolutePath().toString();
}
private void start(HttpServlet servlet) throws Exception
private void start(HttpServlet servlet, MultipartConfigElement config, boolean eager) throws Exception
{
start(servlet, new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0));
}
private void start(HttpServlet servlet, MultipartConfigElement config) throws Exception
{
start(servlet, config, null);
}
private void start(HttpServlet servlet, MultipartConfigElement config, ByteBufferPool bufferPool) throws Exception
{
server = new Server(null, null, bufferPool);
config = config == null ? new MultipartConfigElement(tmpDirString, MAX_FILE_SIZE, -1, 0) : config;
server = new Server(null, null, null);
connector = new ServerConnector(server);
server.addConnector(connector);
ServletContextHandler contextHandler = new ServletContextHandler("/");
ServletContextHandler servletContextHandler = new ServletContextHandler("/");
ServletHolder servletHolder = new ServletHolder(servlet);
servletHolder.getRegistration().setMultipartConfig(config);
contextHandler.addServlet(servletHolder, "/");
servletContextHandler.addServlet(servletHolder, "/");
server.setHandler(servletContextHandler);
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.addIncludedMimeTypes("multipart/form-data");
gzipHandler.setMinGzipSize(32);
gzipHandler.setHandler(contextHandler);
server.setHandler(gzipHandler);
if (eager)
gzipHandler.setHandler(new EagerFormHandler());
servletContextHandler.insertHandler(gzipHandler);
server.start();
@ -134,17 +129,18 @@ public class MultiPartServletTest
IO.delete(tmpDir.toFile());
}
@Test
public void testLargePart() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testLargePart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
protected void service(HttpServletRequest req, HttpServletResponse resp)
{
req.getParameterMap();
}
}, new MultipartConfigElement(tmpDirString));
}, new MultipartConfigElement(tmpDirString), eager);
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
@ -170,22 +166,23 @@ public class MultiPartServletTest
assert400orEof(listener, responseContent ->
{
assertThat(responseContent, containsString("Unable to parse form content"));
assertThat(responseContent, containsString("Form is larger than max length"));
assertThat(responseContent, containsString("400: bad"));
assertThat(responseContent, containsString("Form is larger than max length"));
});
}
@Test
public void testManyParts() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testManyParts(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
protected void service(HttpServletRequest req, HttpServletResponse resp)
{
req.getParameterMap();
}
}, new MultipartConfigElement(tmpDirString));
}, new MultipartConfigElement(tmpDirString), eager);
byte[] byteArray = new byte[1024];
Arrays.fill(byteArray, (byte)1);
@ -208,13 +205,14 @@ public class MultiPartServletTest
assert400orEof(listener, responseContent ->
{
assertThat(responseContent, containsString("Unable to parse form content"));
assertThat(responseContent, containsString("400: bad"));
assertThat(responseContent, containsString("Form with too many keys"));
});
}
@Test
public void testMaxRequestSize() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testMaxRequestSize(boolean eager) throws Exception
{
start(new HttpServlet()
{
@ -223,7 +221,7 @@ public class MultiPartServletTest
{
req.getParameterMap();
}
}, new MultipartConfigElement(tmpDirString, -1, 1024, 1024 * 1024 * 8));
}, new MultipartConfigElement(tmpDirString, -1, 1024, 1024 * 1024 * 8), eager);
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
@ -282,13 +280,14 @@ public class MultiPartServletTest
checkbody.accept(responseContent);
}
@Test
public void testSimpleMultiPart() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testSimpleMultiPart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
protected void service(HttpServletRequest request, HttpServletResponse response1) throws ServletException, IOException
{
Collection<Part> parts = request.getParts();
assertNotNull(parts);
@ -298,10 +297,10 @@ public class MultiPartServletTest
Collection<String> headerNames = part.getHeaderNames();
assertNotNull(headerNames);
assertEquals(2, headerNames.size());
String content = IO.toString(part.getInputStream(), UTF_8);
assertEquals("content1", content);
String content1 = IO.toString(part.getInputStream(), UTF_8);
assertEquals("content1", content1);
}
});
}, null, eager);
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
{
@ -333,21 +332,23 @@ public class MultiPartServletTest
}
}
@Test
public void testTempFilesDeletedOnError() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testTempFilesDeletedOnError(boolean eager) 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 HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
protected void service(HttpServletRequest request, HttpServletResponse response1) throws ServletException, IOException
{
// Should throw as the max file size is exceeded.
request.getParts();
}
});
}, null, eager);
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addPart(new MultiPart.ContentSourcePart("largePart", "largeFile.bin", HttpFields.EMPTY, new BytesRequestContent(bytes)));
@ -361,7 +362,7 @@ public class MultiPartServletTest
.body(multiPart)
.send();
assertEquals(500, response.getStatus());
assertEquals(400, response.getStatus());
assertThat(response.getContentAsString(), containsString("max file size exceeded"));
}
@ -370,18 +371,34 @@ public class MultiPartServletTest
assertThat(fileList.length, is(0));
}
@Test
public void testMultiPartGzip() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testMultiPartGzip(boolean eager) throws Exception
{
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
protected void service(HttpServletRequest request, HttpServletResponse response1) throws IOException, ServletException
{
response.setContentType(request.getContentType());
IO.copy(request.getInputStream(), response.getOutputStream());
String contentType1 = request.getContentType();
response1.setContentType(contentType1);
response1.flushBuffer();
MultiPartRequestContent echoParts = new MultiPartRequestContent(MultiPart.extractBoundary(contentType1));
Collection<Part> servletParts = request.getParts();
for (Part part : servletParts)
{
HttpFields.Mutable partHeaders = HttpFields.build();
for (String h1 : part.getHeaderNames())
partHeaders.add(h1, part.getHeader(h1));
echoParts.addPart(new MultiPart.ContentSourcePart(part.getName(), part.getSubmittedFileName(), partHeaders, new InputStreamContentSource(part.getInputStream())));
}
echoParts.close();
IO.copy(Content.Source.asInputStream(echoParts), response1.getOutputStream());
}
});
}, null, eager);
// Do not automatically handle gzip.
client.getContentDecoderFactories().clear();
@ -409,19 +426,18 @@ public class MultiPartServletTest
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
String boundary = MultiPart.extractBoundary(contentType);
MultiPartFormData formData = new MultiPartFormData(boundary);
formData.setMaxParts(1);
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
formData.parse(Content.Chunk.from(ByteBuffer.wrap(IO.readBytes(inputStream)), true));
MultiPartFormData.Parts parts = formData.join();
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
formData.setMaxParts(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
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testDoubleReadFromPart(boolean eager) throws Exception
{
start(new HttpServlet()
{
@ -435,7 +451,7 @@ public class MultiPartServletTest
resp.getWriter().println("Part: name=" + part.getName() + ", size=" + part.getSize() + ", content=" + IO.toString(part.getInputStream()));
}
}
});
}, null, eager);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";
@ -455,8 +471,9 @@ public class MultiPartServletTest
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
}
@Test
public void testPartAsParameter() throws Exception
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testPartAsParameter(boolean eager) throws Exception
{
start(new HttpServlet()
{
@ -471,7 +488,7 @@ public class MultiPartServletTest
resp.getWriter().println("Parameter: " + entry.getKey() + "=" + entry.getValue()[0]);
}
}
});
}, null, eager);
String contentString = "the quick brown fox jumps over the lazy dog, " +
"the quick brown fox jumps over the lazy dog";

View File

@ -588,7 +588,7 @@ public class StandardDescriptorProcessor extends IterativeDescriptorProcessor
case WebFragment:
{
//another fragment set the value, this fragment's values must match exactly or it is an error
MultipartConfigElement cfg = ((ServletHolder.Registration)holder.getRegistration()).getMultipartConfig();
MultipartConfigElement cfg = holder.getRegistration().getMultipartConfigElement();
if (cfg.getMaxFileSize() != element.getMaxFileSize())
throw new IllegalStateException("Conflicting multipart-config max-file-size for servlet " + name + " in " + descriptor.getURI());