Merge pull request #9344 from eclipse/jetty-10.0.x-multipartCleanups

multipart cleanups jetty-10
This commit is contained in:
Lachlan 2023-02-14 12:15:21 +11:00 committed by GitHub
commit 622befbd0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 206 additions and 9 deletions

View File

@ -38,6 +38,7 @@ import javax.servlet.ServletInputStream;
import javax.servlet.http.Part;
import org.eclipse.jetty.server.MultiParts.NonCompliance;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.ByteArrayOutputStream2;
import org.eclipse.jetty.util.MultiException;
@ -98,6 +99,8 @@ public class MultiPartFormInputStream
private final MultipartConfigElement _config;
private final File _contextTmpDir;
private final String _contentType;
private final int _maxParts;
private int _numParts;
private volatile Throwable _err;
private volatile Path _tmpDir;
private volatile boolean _deleteOnExit;
@ -367,6 +370,18 @@ public class MultiPartFormInputStream
* @param contextTmpDir javax.servlet.context.tempdir
*/
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
{
this(in, contentType, config, contextTmpDir, ContextHandler.DEFAULT_MAX_FORM_KEYS);
}
/**
* @param in Request input stream
* @param contentType Content-Type header
* @param config MultipartConfigElement
* @param contextTmpDir javax.servlet.context.tempdir
* @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
*/
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
{
// Must be a multipart request.
_contentType = contentType;
@ -375,6 +390,7 @@ public class MultiPartFormInputStream
_contextTmpDir = (contextTmpDir != null) ? contextTmpDir : new File(System.getProperty("java.io.tmpdir"));
_config = (config != null) ? config : new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
_maxParts = maxParts;
if (in instanceof ServletInputStream)
{
@ -797,6 +813,9 @@ public class MultiPartFormInputStream
public void startPart()
{
reset();
_numParts++;
if (_maxParts >= 0 && _numParts > _maxParts)
throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
}
@Override

View File

@ -41,6 +41,7 @@ import javax.servlet.ServletInputStream;
import javax.servlet.http.Part;
import org.eclipse.jetty.server.MultiParts.NonCompliance;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.ByteArrayOutputStream2;
import org.eclipse.jetty.util.LazyList;
import org.eclipse.jetty.util.MultiException;
@ -67,7 +68,9 @@ public class MultiPartInputStreamParser
{
private static final Logger LOG = LoggerFactory.getLogger(MultiPartInputStreamParser.class);
public static final MultipartConfigElement __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
public static final MultiMap<Part> EMPTY_MAP = new MultiMap(Collections.emptyMap());
public static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
private final int _maxParts;
private int _numParts;
protected InputStream _in;
protected MultipartConfigElement _config;
protected String _contentType;
@ -375,10 +378,23 @@ public class MultiPartInputStreamParser
* @param contextTmpDir javax.servlet.context.tempdir
*/
public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
{
this (in, contentType, config, contextTmpDir, ContextHandler.DEFAULT_MAX_FORM_KEYS);
}
/**
* @param in Request input stream
* @param contentType Content-Type header
* @param config MultipartConfigElement
* @param contextTmpDir javax.servlet.context.tempdir
* @param maxParts the maximum number of parts that can be parsed from the multipart content (0 for no parts allowed, -1 for unlimited parts).
*/
public MultiPartInputStreamParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, int maxParts)
{
_contentType = contentType;
_config = config;
_contextTmpDir = contextTmpDir;
_maxParts = maxParts;
if (_contextTmpDir == null)
_contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
@ -674,6 +690,11 @@ public class MultiPartInputStreamParser
continue;
}
// Check if we can create a new part.
_numParts++;
if (_maxParts >= 0 && _numParts > _maxParts)
throw new IllegalStateException(String.format("Form with too many parts [%d > %d]", _numParts, _maxParts));
//Have a new Part
MultiPart part = new MultiPart(name, filename);
part.setHeaders(headers);

View File

@ -74,7 +74,12 @@ public interface MultiParts extends Closeable
public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
{
_httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir);
this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
}
public MultiPartsHttpParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
{
_httpParser = new MultiPartFormInputStream(in, contentType, config, contextTmpDir, maxParts);
_context = request.getContext();
_request = request;
}
@ -145,7 +150,12 @@ public interface MultiParts extends Closeable
public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request) throws IOException
{
_utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir);
this(in, contentType, config, contextTmpDir, request, ContextHandler.DEFAULT_MAX_FORM_KEYS);
}
public MultiPartsUtilParser(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir, Request request, int maxParts) throws IOException
{
_utilParser = new MultiPartInputStreamParser(in, contentType, config, contextTmpDir, maxParts);
_context = request.getContext();
_request = request;
}

View File

@ -2326,7 +2326,21 @@ public class Request implements HttpServletRequest
if (config == null)
throw new IllegalStateException("No multipart config for servlet");
_multiParts = newMultiParts(config);
int maxFormContentSize = ContextHandler.DEFAULT_MAX_FORM_CONTENT_SIZE;
int maxFormKeys = ContextHandler.DEFAULT_MAX_FORM_KEYS;
if (_context != null)
{
ContextHandler contextHandler = _context.getContextHandler();
maxFormContentSize = contextHandler.getMaxFormContentSize();
maxFormKeys = contextHandler.getMaxFormKeys();
}
else
{
maxFormContentSize = lookupServerAttribute(ContextHandler.MAX_FORM_CONTENT_SIZE_KEY, maxFormContentSize);
maxFormKeys = lookupServerAttribute(ContextHandler.MAX_FORM_KEYS_KEY, maxFormKeys);
}
_multiParts = newMultiParts(config, maxFormKeys);
Collection<Part> parts = _multiParts.getParts();
String formCharset = null;
@ -2337,7 +2351,7 @@ public class Request implements HttpServletRequest
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
IO.copy(is, os);
formCharset = new String(os.toByteArray(), StandardCharsets.UTF_8);
formCharset = os.toString(StandardCharsets.UTF_8);
}
}
@ -2359,11 +2373,16 @@ public class Request implements HttpServletRequest
else
defaultCharset = StandardCharsets.UTF_8;
long formContentSize = 0;
ByteArrayOutputStream os = null;
for (Part p : parts)
{
if (p.getSubmittedFileName() == null)
{
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)
@ -2375,7 +2394,7 @@ public class Request implements HttpServletRequest
os = new ByteArrayOutputStream();
IO.copy(is, os);
String content = new String(os.toByteArray(), charset == null ? defaultCharset : Charset.forName(charset));
String content = os.toString(charset == null ? defaultCharset : Charset.forName(charset));
if (_contentParameters == null)
_contentParameters = params == null ? new MultiMap<>() : params;
_contentParameters.add(p.getName(), content);
@ -2388,7 +2407,7 @@ public class Request implements HttpServletRequest
return _multiParts.getParts();
}
private MultiParts newMultiParts(MultipartConfigElement config) throws IOException
private MultiParts newMultiParts(MultipartConfigElement config, int maxParts) throws IOException
{
MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
if (LOG.isDebugEnabled())
@ -2398,12 +2417,12 @@ public class Request implements HttpServletRequest
{
case RFC7578:
return new MultiParts.MultiPartsHttpParser(getInputStream(), getContentType(), config,
(_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
(_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this, maxParts);
case LEGACY:
default:
return new MultiParts.MultiPartsUtilParser(getInputStream(), getContentType(), config,
(_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this);
(_context != null ? (File)_context.getAttribute("javax.servlet.context.tempdir") : null), this, maxParts);
}
}

View File

@ -35,12 +35,15 @@ import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.BytesRequestContent;
import org.eclipse.jetty.client.util.InputStreamResponseListener;
import org.eclipse.jetty.client.util.MultiPartRequestContent;
import org.eclipse.jetty.client.util.OutputStreamRequestContent;
import org.eclipse.jetty.client.util.StringRequestContent;
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.MimeTypes;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.MultiPartFormInputStream;
@ -54,6 +57,8 @@ import org.junit.jupiter.api.Test;
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;
@ -67,8 +72,22 @@ public class MultiPartServletTest
private Path tmpDir;
private static final int MAX_FILE_SIZE = 512 * 1024;
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 8;
private static final int LARGE_MESSAGE_SIZE = 1024 * 1024;
public static class RequestParameterServlet extends HttpServlet
{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
req.getParameterMap();
req.getParts();
resp.setStatus(200);
resp.getWriter().print("success");
resp.getWriter().close();
}
}
public static class MultiPartServlet extends HttpServlet
{
@Override
@ -118,11 +137,19 @@ public class MultiPartServletTest
MultipartConfigElement config = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
MAX_FILE_SIZE, -1, 1);
MultipartConfigElement requestSizedConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
-1, MAX_REQUEST_SIZE, 1);
MultipartConfigElement defaultConfig = new MultipartConfigElement(tmpDir.toAbsolutePath().toString(),
-1, -1, 1);
ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath("/");
ServletHolder servletHolder = contextHandler.addServlet(MultiPartServlet.class, "/");
servletHolder.getRegistration().setMultipartConfig(config);
servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/defaultConfig");
servletHolder.getRegistration().setMultipartConfig(defaultConfig);
servletHolder = contextHandler.addServlet(RequestParameterServlet.class, "/requestSizeLimit");
servletHolder.getRegistration().setMultipartConfig(requestSizedConfig);
servletHolder = contextHandler.addServlet(MultiPartEchoServlet.class, "/echo");
servletHolder.getRegistration().setMultipartConfig(config);
@ -148,6 +175,107 @@ public class MultiPartServletTest
IO.delete(tmpDir.toFile());
}
@Test
public void testLargePart() throws Exception
{
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addFieldPart("param", content, null);
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
// 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();
Response response = listener.get(30, TimeUnit.SECONDS);
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
String responseContent = IO.toString(listener.getInputStream());
assertThat(responseContent, containsString("Unable to parse form content"));
assertThat(responseContent, containsString("Form is larger than max length"));
}
@Test
public void testManyParts() throws Exception
{
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.addFieldPart("part" + i, content, null);
}
multiPart.close();
InputStreamResponseListener listener = new InputStreamResponseListener();
client.newRequest("localhost", connector.getLocalPort())
.path("/defaultConfig")
.scheme(HttpScheme.HTTP.asString())
.method(HttpMethod.POST)
.body(multiPart)
.send(listener);
Response response = listener.get(30, TimeUnit.SECONDS);
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
String responseContent = IO.toString(listener.getInputStream());
assertThat(responseContent, containsString("Unable to parse form content"));
assertThat(responseContent, containsString("Form with too many parts"));
}
@Test
public void testMaxRequestSize() throws Exception
{
OutputStreamRequestContent content = new OutputStreamRequestContent();
MultiPartRequestContent multiPart = new MultiPartRequestContent();
multiPart.addFieldPart("param", content, null);
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 < 512; i++)
{
content.getOutputStream().write(byteArray);
}
}
catch (Exception e)
{
writeError = e;
}
if (writeError != null)
assertThat(writeError, instanceOf(EofException.class));
// We should get 400 response.
Response response = listener.get(30, TimeUnit.SECONDS);
assertThat(response.getStatus(), equalTo(HttpStatus.BAD_REQUEST_400));
}
@Test
public void testTempFilesDeletedOnError() throws Exception
{