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:
parent
dd44b30c3e
commit
ec2dbe73a8
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue