Improve support for MultiPart in jetty-core
Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
parent
1f78946a5c
commit
76cf685763
|
@ -98,8 +98,7 @@ public class MultiPartFormData
|
|||
*/
|
||||
public static CompletableFuture<Parts> from(Attributes attributes, MultiPartCompliance compliance, ComplianceViolation.Listener listener, String boundary, Function<Parser, CompletableFuture<Parts>> parse)
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
CompletableFuture<Parts> futureParts = (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
|
||||
CompletableFuture<Parts> futureParts = get(attributes);
|
||||
if (futureParts == null)
|
||||
{
|
||||
futureParts = parse.apply(new Parser(boundary, compliance, listener));
|
||||
|
@ -108,6 +107,18 @@ public class MultiPartFormData
|
|||
return futureParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code multipart/form-data} parts if they have already been created.
|
||||
*
|
||||
* @param attributes the attributes where the futureParts are tracked
|
||||
* @return the future parts
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static CompletableFuture<Parts> get(Attributes attributes)
|
||||
{
|
||||
return (CompletableFuture<Parts>)attributes.getAttribute(MultiPartFormData.class.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>An ordered list of {@link MultiPart.Part}s that can
|
||||
* be accessed by index or by name, or iterated over.</p>
|
||||
|
@ -221,7 +232,7 @@ public class MultiPartFormData
|
|||
private final MultiPart.Parser parser;
|
||||
private final MultiPartCompliance compliance;
|
||||
private final ComplianceViolation.Listener complianceListener;
|
||||
private boolean useFilesForPartsWithoutFileName;
|
||||
private boolean useFilesForPartsWithoutFileName = true;
|
||||
private Path filesDirectory;
|
||||
private long maxFileSize = -1;
|
||||
private long maxMemoryFileSize;
|
||||
|
@ -450,17 +461,17 @@ public class MultiPartFormData
|
|||
public void onPartContent(Content.Chunk chunk)
|
||||
{
|
||||
ByteBuffer buffer = chunk.getByteBuffer();
|
||||
long maxFileSize = getMaxFileSize();
|
||||
fileSize += buffer.remaining();
|
||||
if (maxFileSize >= 0 && fileSize > maxFileSize)
|
||||
{
|
||||
onFailure(new IllegalStateException("max file size exceeded: %d".formatted(maxFileSize)));
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
long maxMemoryFileSize = getMaxMemoryFileSize();
|
||||
if (maxMemoryFileSize >= 0)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.file.Path;
|
||||
|
||||
import org.eclipse.jetty.http.ComplianceViolation;
|
||||
import org.eclipse.jetty.http.MultiPartCompliance;
|
||||
|
||||
import static org.eclipse.jetty.http.ComplianceViolation.Listener.NOOP;
|
||||
|
||||
public class MultiPartConfig
|
||||
{
|
||||
public static MultiPartConfig from(Request request, Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
|
||||
{
|
||||
HttpChannel httpChannel = HttpChannel.from(request);
|
||||
HttpConfiguration httpConfiguration = request.getConnectionMetaData().getHttpConfiguration();
|
||||
MultiPartCompliance multiPartCompliance = httpConfiguration.getMultiPartCompliance();
|
||||
ComplianceViolation.Listener complianceViolationListener = httpChannel.getComplianceViolationListener();
|
||||
int maxHeaderSize = httpConfiguration.getRequestHeaderSize();
|
||||
|
||||
if (location == null)
|
||||
location = request.getContext().getTempDirectory().toPath();
|
||||
|
||||
return new MultiPartConfig(location, maxFormKeys, maxRequestSize,
|
||||
maxFileSize, fileSizeThreshold, maxHeaderSize, multiPartCompliance, complianceViolationListener);
|
||||
}
|
||||
|
||||
private final Path location;
|
||||
private final long fileSizeThreshold;
|
||||
private final long maxFileSize;
|
||||
private final long maxRequestSize;
|
||||
private final int maxFormKeys;
|
||||
private final int maxHeadersSize;
|
||||
private final MultiPartCompliance compliance;
|
||||
private final ComplianceViolation.Listener listener;
|
||||
|
||||
|
||||
public MultiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold)
|
||||
{
|
||||
this(location, maxFormKeys, maxRequestSize, maxFileSize, fileSizeThreshold, 8 * 1024, MultiPartCompliance.RFC7578, null);
|
||||
}
|
||||
|
||||
public MultiPartConfig(Path location, int maxFormKeys, long maxRequestSize, long maxFileSize, long fileSizeThreshold,
|
||||
int maxHeadersSize, MultiPartCompliance compliance, ComplianceViolation.Listener listener)
|
||||
{
|
||||
this.location = location;
|
||||
this.maxFormKeys = maxFormKeys;
|
||||
this.maxRequestSize = maxRequestSize;
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.fileSizeThreshold = fileSizeThreshold;
|
||||
this.maxHeadersSize = maxHeadersSize;
|
||||
this.compliance = compliance;
|
||||
this.listener = listener == null ? NOOP : listener;
|
||||
}
|
||||
|
||||
public Path getLocation()
|
||||
{
|
||||
return location;
|
||||
}
|
||||
|
||||
public long getFileSizeThreshold()
|
||||
{
|
||||
return fileSizeThreshold;
|
||||
}
|
||||
|
||||
public long getMaxFileSize()
|
||||
{
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
public long getMaxRequestSize()
|
||||
{
|
||||
return maxRequestSize;
|
||||
}
|
||||
|
||||
public int getMaxFormKeys()
|
||||
{
|
||||
return maxFormKeys;
|
||||
}
|
||||
|
||||
public int getMaxHeadersSize()
|
||||
{
|
||||
return maxHeadersSize;
|
||||
}
|
||||
|
||||
public MultiPartCompliance getMultiPartCompliance()
|
||||
{
|
||||
return compliance;
|
||||
}
|
||||
|
||||
public ComplianceViolation.Listener getViolationListener()
|
||||
{
|
||||
return listener;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// 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.util.concurrent.CompletableFuture;
|
||||
|
||||
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.Content;
|
||||
import org.eclipse.jetty.util.Attributes;
|
||||
|
||||
public class MultiPartFormFields
|
||||
{
|
||||
public static CompletableFuture<MultiPartFormData.Parts> from(Request request, MultiPartConfig config)
|
||||
{
|
||||
return from(request, request, config);
|
||||
}
|
||||
|
||||
public static CompletableFuture<MultiPartFormData.Parts> from(Request request, Content.Source source, MultiPartConfig config)
|
||||
{
|
||||
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
|
||||
|
||||
return from(source, request, contentType, config);
|
||||
}
|
||||
|
||||
public static CompletableFuture<MultiPartFormData.Parts> from(Content.Source content, Attributes attributes, String contentType, MultiPartConfig config)
|
||||
{
|
||||
// Look for an existing future (we use the future here rather than the parts as it can remember any failure).
|
||||
CompletableFuture<MultiPartFormData.Parts> futureParts = MultiPartFormData.get(attributes);
|
||||
if (futureParts == null)
|
||||
{
|
||||
// No existing parts, so we need to try to read them ourselves
|
||||
|
||||
// Are we the right content type to produce our own parts?
|
||||
if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.getValueParameters(contentType, null)))
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("Not multipart Content-Type"));
|
||||
|
||||
// Do we have a boundary?
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
if (boundary == null)
|
||||
return CompletableFuture.failedFuture(new IllegalStateException("No multipart boundary parameter in Content-Type"));
|
||||
|
||||
// Look for an existing future MultiPartFormData.Parts
|
||||
futureParts = MultiPartFormData.from(attributes, config.getMultiPartCompliance(), config.getViolationListener(), boundary, parser ->
|
||||
{
|
||||
try
|
||||
{
|
||||
// No existing core parts, so we need to configure the parser.
|
||||
parser.setMaxParts(config.getMaxFormKeys());
|
||||
parser.setMaxMemoryFileSize(config.getFileSizeThreshold());
|
||||
parser.setMaxFileSize(config.getMaxFileSize());
|
||||
parser.setMaxLength(config.getMaxRequestSize());
|
||||
parser.setPartHeadersMaxLength(config.getMaxHeadersSize());
|
||||
if (config.getLocation() != null)
|
||||
parser.setFilesDirectory(config.getLocation());
|
||||
|
||||
// parse the core parts.
|
||||
return parser.parse(content);
|
||||
}
|
||||
catch (Throwable failure)
|
||||
{
|
||||
return CompletableFuture.failedFuture(failure);
|
||||
}
|
||||
});
|
||||
}
|
||||
return futureParts;
|
||||
}
|
||||
}
|
|
@ -472,6 +472,7 @@ public class MultiPartServletTest
|
|||
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setMaxParts(1);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
|
||||
|
||||
assertThat(parts.size(), is(1));
|
||||
|
|
|
@ -0,0 +1,543 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.eclipse.jetty.client.BytesRequestContent;
|
||||
import org.eclipse.jetty.client.ContentResponse;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.MultiPartRequestContent;
|
||||
import org.eclipse.jetty.client.OutputStreamRequestContent;
|
||||
import org.eclipse.jetty.client.StringRequestContent;
|
||||
import org.eclipse.jetty.http.BadMessageException;
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.http.HttpFields;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpScheme;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.http.MultiPart;
|
||||
import org.eclipse.jetty.http.MultiPartFormData;
|
||||
import org.eclipse.jetty.io.Content;
|
||||
import org.eclipse.jetty.io.EofException;
|
||||
import org.eclipse.jetty.io.content.InputStreamContentSource;
|
||||
import org.eclipse.jetty.logging.StacklessLogging;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.HttpChannel;
|
||||
import org.eclipse.jetty.server.MultiPartConfig;
|
||||
import org.eclipse.jetty.server.MultiPartFormFields;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
public class CoreMultiPartTest
|
||||
{
|
||||
private static final int MAX_FILE_SIZE = 512 * 1024;
|
||||
|
||||
private Server server;
|
||||
private ServerConnector connector;
|
||||
private HttpClient client;
|
||||
private Path tmpDir;
|
||||
private MultiPartConfig config;
|
||||
|
||||
@BeforeEach
|
||||
public void before() throws Exception
|
||||
{
|
||||
tmpDir = Files.createTempDirectory(CoreMultiPartTest.class.getSimpleName());
|
||||
}
|
||||
|
||||
private void start(Handler handler, MultiPartConfig config) throws Exception
|
||||
{
|
||||
this.config = config == null ? new MultiPartConfig(tmpDir, -1, -1, MAX_FILE_SIZE, 0) : config;
|
||||
server = new Server(null, null, null);
|
||||
connector = new ServerConnector(server);
|
||||
server.addConnector(connector);
|
||||
server.setHandler(handler);
|
||||
|
||||
GzipHandler gzipHandler = new GzipHandler();
|
||||
gzipHandler.addIncludedMimeTypes("multipart/form-data");
|
||||
gzipHandler.setMinGzipSize(32);
|
||||
server.insertHandler(gzipHandler);
|
||||
|
||||
server.start();
|
||||
client = new HttpClient();
|
||||
client.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stop() throws Exception
|
||||
{
|
||||
LifeCycle.stop(client);
|
||||
LifeCycle.stop(server);
|
||||
IO.delete(tmpDir.toFile());
|
||||
}
|
||||
|
||||
private MultiPartFormData.Parts getParts(Request request, MultiPartConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
return MultiPartFormFields.from(request, config).get();
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
throw new BadMessageException("bad multipart", t.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargePart() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
getParts(request, config);
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, new MultiPartConfig(null, -1, -1, 1024 * 1024, -1));
|
||||
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
// The write side will eventually throw because connection is closed.
|
||||
assertThrows(Throwable.class, () ->
|
||||
{
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 2; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
content.close();
|
||||
});
|
||||
|
||||
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManyParts() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
getParts(request, config);
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, new MultiPartConfig(null, 1024, -1, -1, -1));
|
||||
|
||||
byte[] byteArray = new byte[1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
BytesRequestContent content = new BytesRequestContent(byteArray);
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("part" + i, null, null, content));
|
||||
}
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/defaultConfig")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
assert400orEof(listener, responseContent -> assertThat(responseContent, containsString("400")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxRequestSize() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
getParts(request, config);
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, new MultiPartConfig(tmpDir, -1, 1024, -1, 1024 * 1024 * 8));
|
||||
|
||||
OutputStreamRequestContent content = new OutputStreamRequestContent();
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("param", null, null, content));
|
||||
multiPart.close();
|
||||
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/requestSizeLimit")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send(listener);
|
||||
|
||||
Throwable writeError = null;
|
||||
try
|
||||
{
|
||||
// Write large amount of content to the part.
|
||||
byte[] byteArray = new byte[1024 * 1024];
|
||||
Arrays.fill(byteArray, (byte)1);
|
||||
for (int i = 0; i < 1024 * 1024; i++)
|
||||
{
|
||||
content.getOutputStream().write(byteArray);
|
||||
}
|
||||
fail("We should never be able to write all the content.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
writeError = e;
|
||||
}
|
||||
|
||||
assertThat(writeError, instanceOf(EofException.class));
|
||||
|
||||
assert400orEof(listener, null);
|
||||
}
|
||||
|
||||
private static void assert400orEof(InputStreamResponseListener listener, Consumer<String> checkbody) throws InterruptedException, TimeoutException
|
||||
{
|
||||
// There is a race here, either we fail trying to write some more content OR
|
||||
// we get 400 response, for some reason reading the content throws EofException.
|
||||
String responseContent = null;
|
||||
try
|
||||
{
|
||||
org.eclipse.jetty.client.Response response = listener.get(60, TimeUnit.SECONDS);
|
||||
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
|
||||
responseContent = IO.toString(listener.getInputStream());
|
||||
}
|
||||
catch (ExecutionException | IOException e)
|
||||
{
|
||||
Throwable cause = e.getCause();
|
||||
assertThat(cause, instanceOf(EofException.class));
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkbody != null)
|
||||
checkbody.accept(responseContent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleMultiPart() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
MultiPartFormData.Parts parts = getParts(request, config);
|
||||
assertNotNull(parts);
|
||||
assertEquals(1, parts.size());
|
||||
MultiPart.Part part = parts.iterator().next();
|
||||
assertEquals("part1", part.getName());
|
||||
HttpFields fields = part.getHeaders();
|
||||
assertNotNull(fields);
|
||||
assertEquals(2, fields.size());
|
||||
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
|
||||
String content1 = IO.toString(inputStream, UTF_8);
|
||||
assertEquals("content1", content1);
|
||||
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, null);
|
||||
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String content = """
|
||||
--A1B2C3
|
||||
Content-Disposition: form-data; name="part1"
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
content1
|
||||
--A1B2C3--
|
||||
""";
|
||||
String header = """
|
||||
POST / HTTP/1.1
|
||||
Host: localhost
|
||||
Content-Type: multipart/form-data; boundary="A1B2C3"
|
||||
Content-Length: $L
|
||||
|
||||
""".replace("$L", String.valueOf(content.length()));
|
||||
|
||||
output.write(header.getBytes(UTF_8));
|
||||
output.write(content.getBytes(UTF_8));
|
||||
output.flush();
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
|
||||
assertNotNull(response);
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTempFilesDeletedOnError() throws Exception
|
||||
{
|
||||
byte[] bytes = new byte[2 * MAX_FILE_SIZE];
|
||||
Arrays.fill(bytes, (byte)1);
|
||||
|
||||
// Should throw as the max file size is exceeded.
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
getParts(request, config);
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, null);
|
||||
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("largePart", "largeFile.bin", HttpFields.EMPTY, new BytesRequestContent(bytes)));
|
||||
multiPart.close();
|
||||
|
||||
try (StacklessLogging ignored = new StacklessLogging(HttpChannel.class))
|
||||
{
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send();
|
||||
|
||||
assertEquals(400, response.getStatus());
|
||||
}
|
||||
|
||||
String[] fileList = tmpDir.toFile().list();
|
||||
assertNotNull(fileList);
|
||||
assertThat(fileList.length, is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultTempDirectory() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
MultiPartConfig conf = MultiPartConfig.from(request, config.getLocation(),
|
||||
config.getMaxFormKeys(), config.getMaxRequestSize(),
|
||||
config.getMaxFileSize(), config.getFileSizeThreshold());
|
||||
MultiPartFormData.Parts parts = getParts(request, conf);
|
||||
assertNotNull(parts);
|
||||
assertEquals(1, parts.size());
|
||||
MultiPart.Part part = parts.iterator().next();
|
||||
assertEquals("part1", part.getName());
|
||||
HttpFields headers = part.getHeaders();
|
||||
assertNotNull(headers);
|
||||
assertEquals(2, headers.size());
|
||||
InputStream inputStream = Content.Source.asInputStream(part.getContentSource());
|
||||
String content1 = IO.toString(inputStream, UTF_8);
|
||||
assertEquals("content1", content1);
|
||||
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, new MultiPartConfig(null, -1, MAX_FILE_SIZE, -1, 0));
|
||||
|
||||
try (Socket socket = new Socket("localhost", connector.getLocalPort()))
|
||||
{
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String content = """
|
||||
--A1B2C3
|
||||
Content-Disposition: form-data; name="part1"
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
content1
|
||||
--A1B2C3--
|
||||
""";
|
||||
String header = """
|
||||
POST / HTTP/1.1
|
||||
Host: localhost
|
||||
Content-Type: multipart/form-data; boundary="A1B2C3"
|
||||
Content-Length: $L
|
||||
|
||||
""".replace("$L", String.valueOf(content.length()));
|
||||
|
||||
output.write(header.getBytes(UTF_8));
|
||||
output.write(content.getBytes(UTF_8));
|
||||
output.flush();
|
||||
|
||||
HttpTester.Response response = HttpTester.parseResponse(socket.getInputStream());
|
||||
assertNotNull(response);
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiPartGzip() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, contentType);
|
||||
|
||||
MultiPartRequestContent echoParts = new MultiPartRequestContent(MultiPart.extractBoundary(contentType));
|
||||
MultiPartFormData.Parts servletParts = getParts(request, config);
|
||||
for (MultiPart.Part part : servletParts)
|
||||
{
|
||||
HttpFields.Mutable partHeaders = HttpFields.build();
|
||||
for (HttpField field : part.getHeaders())
|
||||
partHeaders.add(field);
|
||||
|
||||
echoParts.addPart(new MultiPart.ContentSourcePart(part.getName(), part.getFileName(), partHeaders, part.getContentSource()));
|
||||
}
|
||||
echoParts.close();
|
||||
IO.copy(Content.Source.asInputStream(echoParts), Content.Sink.asOutputStream(response));
|
||||
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, null);
|
||||
|
||||
// Do not automatically handle gzip.
|
||||
client.getContentDecoderFactories().clear();
|
||||
|
||||
String contentString = "the quick brown fox jumps over the lazy dog, " +
|
||||
"the quick brown fox jumps over the lazy dog";
|
||||
StringRequestContent content = new StringRequestContent(contentString);
|
||||
|
||||
MultiPartRequestContent multiPartContent = new MultiPartRequestContent();
|
||||
multiPartContent.addPart(new MultiPart.ContentSourcePart("stringPart", null, HttpFields.EMPTY, content));
|
||||
multiPartContent.close();
|
||||
|
||||
InputStreamResponseListener responseStream = new InputStreamResponseListener();
|
||||
client.newRequest("localhost", connector.getLocalPort())
|
||||
.path("/echo")
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.headers(h -> h.add(HttpHeader.ACCEPT_ENCODING, "gzip"))
|
||||
.body(multiPartContent)
|
||||
.send(responseStream);
|
||||
|
||||
org.eclipse.jetty.client.Response response = responseStream.get(5, TimeUnit.SECONDS);
|
||||
HttpFields headers = response.getHeaders();
|
||||
assertThat(headers.get(HttpHeader.CONTENT_TYPE), startsWith("multipart/form-data"));
|
||||
assertThat(headers.get(HttpHeader.CONTENT_ENCODING), is("gzip"));
|
||||
|
||||
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
|
||||
String boundary = MultiPart.extractBoundary(contentType);
|
||||
InputStream inputStream = new GZIPInputStream(responseStream.getInputStream());
|
||||
MultiPartFormData.Parser formData = new MultiPartFormData.Parser(boundary);
|
||||
formData.setMaxParts(1);
|
||||
formData.setMaxMemoryFileSize(-1);
|
||||
MultiPartFormData.Parts parts = formData.parse(new InputStreamContentSource(inputStream)).join();
|
||||
|
||||
assertThat(parts.size(), is(1));
|
||||
assertThat(parts.get(0).getContentAsString(UTF_8), is(contentString));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleReadFromPart() throws Exception
|
||||
{
|
||||
start(new Handler.Abstract()
|
||||
{
|
||||
@Override
|
||||
public boolean handle(Request request, Response response, Callback callback) throws Exception
|
||||
{
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain");
|
||||
PrintWriter writer = new PrintWriter(Content.Sink.asOutputStream(response));
|
||||
for (MultiPart.Part part : getParts(request, config))
|
||||
{
|
||||
String partContent = IO.toString(Content.Source.asInputStream(part.getContentSource()));
|
||||
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
|
||||
|
||||
// We can only consume the getContentSource() once so we must use newContentSource().
|
||||
partContent = IO.toString(Content.Source.asInputStream(part.newContentSource()));
|
||||
writer.println("Part: name=" + part.getName() + ", size=" + part.getLength() + ", content=" + partContent);
|
||||
}
|
||||
|
||||
writer.close();
|
||||
callback.succeeded();
|
||||
return true;
|
||||
}
|
||||
}, null);
|
||||
|
||||
String contentString = "the quick brown fox jumps over the lazy dog, " +
|
||||
"the quick brown fox jumps over the lazy dog";
|
||||
StringRequestContent content = new StringRequestContent(contentString);
|
||||
MultiPartRequestContent multiPart = new MultiPartRequestContent();
|
||||
multiPart.addPart(new MultiPart.ContentSourcePart("myPart", null, HttpFields.EMPTY, content));
|
||||
multiPart.close();
|
||||
|
||||
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
|
||||
.scheme(HttpScheme.HTTP.asString())
|
||||
.method(HttpMethod.POST)
|
||||
.body(multiPart)
|
||||
.send();
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertThat(response.getContentAsString(), containsString("Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog\n" +
|
||||
"Part: name=myPart, size=88, content=the quick brown fox jumps over the lazy dog, the quick brown fox jumps over the lazy dog"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue