Merge pull request #2332 from eclipse/jetty-9.4.x-1027-Multipart
## SearchPattern New class which does a fast search for patterns within strings and arrays of bytes using an implementation of the Boyer–Moore–Horspool algorithm. This was written to be used in the new MultiPartParser class to search for delimeter boundaries. ## MultiPartParser New class which uses the SearchPattern to parse a MultiPart Mime given a ByteBuffer. Written in a non-blocking style so can be used asynchronously (although not currently be being used this way). ## MultiPartFormInputStream New class which uses the MultiPartParser to parse a MultiPart Mime input stream into a Collection of Parts. This class is in org/eclipse/jetty/http and is designed to replace org/eclipse/jetty/util/MultiPartInputStreamParser. ## MultiPartInputStreamParser and Non Compliances This class has been deprecated and replaced by org.eclipse.jetty.http.MultiPartFormInputStream. It accepts formats non compliant with the RFC that the new MultiPartFormInputStream does not accept. When this occurs violations are recorded by the method getNonComplianceWarnings(). ## MultiParts New interface to allow switching between the different implementations. This allows MultiParts to function in two different modes. The LEGACY implementation using the UTIL parser which may parse forms containing non compliances with the RFC, and the RFC7578 implementation using the new and faster HTTP parser. This file contains the implementations of MultiParts for HTTP and UTIL parsers as nested classes which are used by Request. ## Request Changed to use the new MultiParts interface instead of the MultiPartInputStreamParser class. with a method called newMultiParts which will construct a MultiPart using one of the HTTP or UTIL implementations depending on what compliance mode is set. ## Jetty Test Webapp Dump Servlet Code added to display parts while running the dump test webapp if MuliPart form is submitted. ## MultiPartBenchMark JMH Benchmark of the HTTP multipart parser vs the UTIL multipart parser. testLargeGenerated parses a 10MB file of random binary data. testParser parses a series of small multipart forms captured by a browser. ``` # Run complete. Total time: 00:02:09 Benchmark (parserType) Mode Cnt Score Error Units MultiPartBenchmark.testLargeGenerated UTIL avgt 10 0.252 ± 0.025 s/op MultiPartBenchmark.testLargeGenerated HTTP avgt 10 0.035 ± 0.004 s/op MultiPartBenchmark.testParser UTIL avgt 10 0.028 ± 0.005 s/op MultiPartBenchmark.testParser HTTP avgt 10 0.015 ± 0.006 s/op ```
This commit is contained in:
commit
c41b6b7aab
|
@ -5,4 +5,5 @@
|
|||
*.java eol=lf
|
||||
*.xml eol=lf
|
||||
Jenkinsfile eol=lf
|
||||
*.js eol=lf
|
||||
*.js eol=lf
|
||||
*.raw binary
|
|
@ -23,11 +23,28 @@
|
|||
<artifactId>jetty-io</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty.toolchain</groupId>
|
||||
<artifactId>jetty-test-helper</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
@ -64,7 +81,49 @@
|
|||
<onlyAnalyze>org.eclipse.jetty.http.*</onlyAnalyze>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>${jmhjar.name}</finalName>
|
||||
<shadeTestJar>true</shadeTestJar>
|
||||
<artifactSet>
|
||||
<includes>
|
||||
<include>org.openjdk.jmh:jmh-core</include>
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>org.openjdk.jmh:jmh-core</artifact>
|
||||
<includes>
|
||||
<include>**</include>
|
||||
</includes>
|
||||
</filter>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -49,7 +49,7 @@ import org.eclipse.jetty.util.log.Logger;
|
|||
* be altered in code and will affect all usages of the mode.
|
||||
*/
|
||||
public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so that extra custom modes can be defined dynamically
|
||||
{
|
||||
{
|
||||
/** A Legacy compliance mode to match jetty's behavior prior to RFC2616 and RFC7230. It only
|
||||
* contains {@link HttpComplianceSection#METHOD_CASE_SENSITIVE}
|
||||
*/
|
||||
|
@ -82,6 +82,8 @@ public enum HttpCompliance // TODO in Jetty-10 convert this enum to a class so t
|
|||
@Deprecated
|
||||
CUSTOM3(sectionsByProperty("CUSTOM3"));
|
||||
|
||||
public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations";
|
||||
|
||||
private static final Logger LOG = Log.getLogger(HttpParser.class);
|
||||
private static EnumSet<HttpComplianceSection> sectionsByProperty(String property)
|
||||
{
|
||||
|
|
|
@ -58,6 +58,7 @@ public class MimeTypes
|
|||
FORM_ENCODED("application/x-www-form-urlencoded"),
|
||||
MESSAGE_HTTP("message/http"),
|
||||
MULTIPART_BYTERANGES("multipart/byteranges"),
|
||||
MULTIPART_FORM_DATA("multipart/form-data"),
|
||||
|
||||
TEXT_HTML("text/html"),
|
||||
TEXT_PLAIN("text/plain"),
|
||||
|
|
|
@ -0,0 +1,856 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.ByteArrayOutputStream2;
|
||||
import org.eclipse.jetty.util.LazyList;
|
||||
import org.eclipse.jetty.util.MultiException;
|
||||
import org.eclipse.jetty.util.MultiMap;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/**
|
||||
* MultiPartInputStream
|
||||
*
|
||||
* Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc7578">https://tools.ietf.org/html/rfc7578</a>
|
||||
*/
|
||||
public class MultiPartFormInputStream
|
||||
{
|
||||
private static final Logger LOG = Log.getLogger(MultiPartFormInputStream.class);
|
||||
private final int _bufferSize = 16 * 1024;
|
||||
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());
|
||||
protected InputStream _in;
|
||||
protected MultipartConfigElement _config;
|
||||
protected String _contentType;
|
||||
protected MultiMap<Part> _parts;
|
||||
protected Throwable _err;
|
||||
protected File _tmpDir;
|
||||
protected File _contextTmpDir;
|
||||
protected boolean _deleteOnExit;
|
||||
protected boolean _writeFilesWithFilenames;
|
||||
protected boolean _parsed;
|
||||
|
||||
public class MultiPart implements Part
|
||||
{
|
||||
protected String _name;
|
||||
protected String _filename;
|
||||
protected File _file;
|
||||
protected OutputStream _out;
|
||||
protected ByteArrayOutputStream2 _bout;
|
||||
protected String _contentType;
|
||||
protected MultiMap<String> _headers;
|
||||
protected long _size = 0;
|
||||
protected boolean _temporary = true;
|
||||
|
||||
public MultiPart(String name, String filename) throws IOException
|
||||
{
|
||||
_name = name;
|
||||
_filename = filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("Part{n=%s,fn=%s,ct=%s,s=%d,tmp=%b,file=%s}",_name,_filename,_contentType,_size,_temporary,_file);
|
||||
}
|
||||
|
||||
protected void setContentType(String contentType)
|
||||
{
|
||||
_contentType = contentType;
|
||||
}
|
||||
|
||||
protected void open() throws IOException
|
||||
{
|
||||
// We will either be writing to a file, if it has a filename on the content-disposition
|
||||
// and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
|
||||
// will need to change to write to a file.
|
||||
if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0)
|
||||
{
|
||||
createFile();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write to a buffer in memory until we discover we've exceed the
|
||||
// MultipartConfig fileSizeThreshold
|
||||
_out = _bout = new ByteArrayOutputStream2();
|
||||
}
|
||||
}
|
||||
|
||||
protected void close() throws IOException
|
||||
{
|
||||
_out.close();
|
||||
}
|
||||
|
||||
protected void write(int b) throws IOException
|
||||
{
|
||||
if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartFormInputStream.this._config.getMaxFileSize())
|
||||
throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
|
||||
|
||||
if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartFormInputStream.this._config.getFileSizeThreshold()
|
||||
&& _file == null)
|
||||
createFile();
|
||||
|
||||
_out.write(b);
|
||||
_size++;
|
||||
}
|
||||
|
||||
protected void write(byte[] bytes, int offset, int length) throws IOException
|
||||
{
|
||||
if (MultiPartFormInputStream.this._config.getMaxFileSize() > 0 && _size + length > MultiPartFormInputStream.this._config.getMaxFileSize())
|
||||
throw new IllegalStateException("Multipart Mime part " + _name + " exceeds max filesize");
|
||||
|
||||
if (MultiPartFormInputStream.this._config.getFileSizeThreshold() > 0
|
||||
&& _size + length > MultiPartFormInputStream.this._config.getFileSizeThreshold() && _file == null)
|
||||
createFile();
|
||||
|
||||
_out.write(bytes,offset,length);
|
||||
_size += length;
|
||||
}
|
||||
|
||||
protected void createFile() throws IOException
|
||||
{
|
||||
/*
|
||||
* Some statics just to make the code below easier to understand This get optimized away during the compile anyway
|
||||
*/
|
||||
final boolean USER = true;
|
||||
final boolean WORLD = false;
|
||||
|
||||
_file = File.createTempFile("MultiPart","",MultiPartFormInputStream.this._tmpDir);
|
||||
_file.setReadable(false,WORLD); // (reset) disable it for everyone first
|
||||
_file.setReadable(true,USER); // enable for user only
|
||||
|
||||
if (_deleteOnExit)
|
||||
_file.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(_file);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||
|
||||
if (_size > 0 && _out != null)
|
||||
{
|
||||
// already written some bytes, so need to copy them into the file
|
||||
_out.flush();
|
||||
_bout.writeTo(bos);
|
||||
_out.close();
|
||||
}
|
||||
_bout = null;
|
||||
_out = bos;
|
||||
}
|
||||
|
||||
protected void setHeaders(MultiMap<String> headers)
|
||||
{
|
||||
_headers = headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getContentType()
|
||||
*/
|
||||
@Override
|
||||
public String getContentType()
|
||||
{
|
||||
return _contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeader(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public String getHeader(String name)
|
||||
{
|
||||
if (name == null)
|
||||
return null;
|
||||
return _headers.getValue(StringUtil.asciiToLowerCase(name),0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeaderNames()
|
||||
*/
|
||||
@Override
|
||||
public Collection<String> getHeaderNames()
|
||||
{
|
||||
return _headers.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getHeaders(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public Collection<String> getHeaders(String name)
|
||||
{
|
||||
return _headers.getValues(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getInputStream()
|
||||
*/
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException
|
||||
{
|
||||
if (_file != null)
|
||||
{
|
||||
// written to a file, whether temporary or not
|
||||
return new BufferedInputStream(new FileInputStream(_file));
|
||||
}
|
||||
else
|
||||
{
|
||||
// part content is in memory
|
||||
return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getSubmittedFileName()
|
||||
*/
|
||||
@Override
|
||||
public String getSubmittedFileName()
|
||||
{
|
||||
return getContentDispositionFilename();
|
||||
}
|
||||
|
||||
public byte[] getBytes()
|
||||
{
|
||||
if (_bout != null)
|
||||
return _bout.toByteArray();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getName()
|
||||
*/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#getSize()
|
||||
*/
|
||||
@Override
|
||||
public long getSize()
|
||||
{
|
||||
return _size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see javax.servlet.http.Part#write(java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public void write(String fileName) throws IOException
|
||||
{
|
||||
if (_file == null)
|
||||
{
|
||||
_temporary = false;
|
||||
|
||||
// part data is only in the ByteArrayOutputStream and never been written to disk
|
||||
_file = new File(_tmpDir,fileName);
|
||||
|
||||
BufferedOutputStream bos = null;
|
||||
try
|
||||
{
|
||||
bos = new BufferedOutputStream(new FileOutputStream(_file));
|
||||
_bout.writeTo(bos);
|
||||
bos.flush();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bos != null)
|
||||
bos.close();
|
||||
_bout = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// the part data is already written to a temporary file, just rename it
|
||||
_temporary = false;
|
||||
|
||||
Path src = _file.toPath();
|
||||
Path target = src.resolveSibling(fileName);
|
||||
Files.move(src,target,StandardCopyOption.REPLACE_EXISTING);
|
||||
_file = target.toFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the file, whether or not Part.write() was called on it (ie no longer temporary)
|
||||
*
|
||||
* @see javax.servlet.http.Part#delete()
|
||||
*/
|
||||
@Override
|
||||
public void delete() throws IOException
|
||||
{
|
||||
if (_file != null && _file.exists())
|
||||
_file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only remove tmp files.
|
||||
*
|
||||
* @throws IOException
|
||||
* if unable to delete the file
|
||||
*/
|
||||
public void cleanUp() throws IOException
|
||||
{
|
||||
if (_temporary && _file != null && _file.exists())
|
||||
_file.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file
|
||||
*
|
||||
* @return the file, if any, the data has been written to.
|
||||
*/
|
||||
public File getFile()
|
||||
{
|
||||
return _file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename from the content-disposition.
|
||||
*
|
||||
* @return null or the filename
|
||||
*/
|
||||
public String getContentDispositionFilename()
|
||||
{
|
||||
return _filename;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param in
|
||||
* Request input stream
|
||||
* @param contentType
|
||||
* Content-Type header
|
||||
* @param config
|
||||
* MultipartConfigElement
|
||||
* @param contextTmpDir
|
||||
* javax.servlet.context.tempdir
|
||||
*/
|
||||
public MultiPartFormInputStream(InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
|
||||
{
|
||||
_contentType = contentType;
|
||||
_config = config;
|
||||
_contextTmpDir = contextTmpDir;
|
||||
if (_contextTmpDir == null)
|
||||
_contextTmpDir = new File(System.getProperty("java.io.tmpdir"));
|
||||
|
||||
if (_config == null)
|
||||
_config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
|
||||
|
||||
if (in instanceof ServletInputStream)
|
||||
{
|
||||
if (((ServletInputStream)in).isFinished())
|
||||
{
|
||||
_parts = EMPTY_MAP;
|
||||
_parsed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
_in = new BufferedInputStream(in);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the list of parsed parts is empty
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
if (_parts == null)
|
||||
return true;
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
for (List<Part> partList : values)
|
||||
{
|
||||
if(partList.size() != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the already parsed parts.
|
||||
*
|
||||
* @return the parts that were parsed
|
||||
*/
|
||||
@Deprecated
|
||||
public Collection<Part> getParsedParts()
|
||||
{
|
||||
if (_parts == null)
|
||||
return Collections.emptyList();
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
List<Part> parts = new ArrayList<>();
|
||||
for (List<Part> o : values)
|
||||
{
|
||||
List<Part> asList = LazyList.getList(o,false);
|
||||
parts.addAll(asList);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any tmp storage for parts, and clear out the parts list.
|
||||
*/
|
||||
public void deleteParts()
|
||||
{
|
||||
if (!_parsed)
|
||||
return;
|
||||
|
||||
Collection<Part> parts;
|
||||
try
|
||||
{
|
||||
parts = getParts();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
MultiException err = new MultiException();
|
||||
|
||||
for (Part p : parts)
|
||||
{
|
||||
try
|
||||
{
|
||||
((MultiPart)p).cleanUp();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
err.add(e);
|
||||
}
|
||||
}
|
||||
_parts.clear();
|
||||
|
||||
err.ifExceptionThrowRuntime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse, if necessary, the multipart data and return the list of Parts.
|
||||
*
|
||||
* @return the parts
|
||||
* @throws IOException
|
||||
* if unable to get the parts
|
||||
*/
|
||||
public Collection<Part> getParts() throws IOException
|
||||
{
|
||||
if (!_parsed)
|
||||
parse();
|
||||
throwIfError();
|
||||
|
||||
Collection<List<Part>> values = _parts.values();
|
||||
List<Part> parts = new ArrayList<>();
|
||||
for (List<Part> o : values)
|
||||
{
|
||||
List<Part> asList = LazyList.getList(o,false);
|
||||
parts.addAll(asList);
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the named Part.
|
||||
*
|
||||
* @param name
|
||||
* the part name
|
||||
* @return the parts
|
||||
* @throws IOException
|
||||
* if unable to get the part
|
||||
*/
|
||||
public Part getPart(String name) throws IOException
|
||||
{
|
||||
if(!_parsed)
|
||||
parse();
|
||||
throwIfError();
|
||||
return _parts.getValue(name,0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception if one has been latched.
|
||||
*
|
||||
* @throws IOException
|
||||
* the exception (if present)
|
||||
*/
|
||||
protected void throwIfError() throws IOException
|
||||
{
|
||||
if (_err != null)
|
||||
{
|
||||
_err.addSuppressed(new Throwable());
|
||||
if (_err instanceof IOException)
|
||||
throw (IOException)_err;
|
||||
if (_err instanceof IllegalStateException)
|
||||
throw (IllegalStateException)_err;
|
||||
throw new IllegalStateException(_err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse, if necessary, the multipart stream.
|
||||
*
|
||||
*/
|
||||
protected void parse()
|
||||
{
|
||||
// have we already parsed the input?
|
||||
if (_parsed)
|
||||
return;
|
||||
_parsed = true;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// initialize
|
||||
_parts = new MultiMap<>();
|
||||
|
||||
// if its not a multipart request, don't parse it
|
||||
if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
|
||||
return;
|
||||
|
||||
// sort out the location to which to write the files
|
||||
if (_config.getLocation() == null)
|
||||
_tmpDir = _contextTmpDir;
|
||||
else if ("".equals(_config.getLocation()))
|
||||
_tmpDir = _contextTmpDir;
|
||||
else
|
||||
{
|
||||
File f = new File(_config.getLocation());
|
||||
if (f.isAbsolute())
|
||||
_tmpDir = f;
|
||||
else
|
||||
_tmpDir = new File(_contextTmpDir,_config.getLocation());
|
||||
}
|
||||
|
||||
if (!_tmpDir.exists())
|
||||
_tmpDir.mkdirs();
|
||||
|
||||
String contentTypeBoundary = "";
|
||||
int bstart = _contentType.indexOf("boundary=");
|
||||
if (bstart >= 0)
|
||||
{
|
||||
int bend = _contentType.indexOf(";",bstart);
|
||||
bend = (bend < 0?_contentType.length():bend);
|
||||
contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
|
||||
}
|
||||
|
||||
Handler handler = new Handler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,contentTypeBoundary);
|
||||
|
||||
// Create a buffer to store data from stream //
|
||||
byte[] data = new byte[_bufferSize];
|
||||
int len = 0;
|
||||
|
||||
/*
|
||||
* keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
|
||||
*/
|
||||
long total = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
len = _in.read(data);
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
total += len;
|
||||
if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
|
||||
{
|
||||
_err = new IllegalStateException("Request exceeds maxRequestSize (" + _config.getMaxRequestSize() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = BufferUtil.toBuffer(data);
|
||||
buffer.limit(len);
|
||||
if (parser.parse(buffer,false))
|
||||
break;
|
||||
|
||||
if(buffer.hasRemaining())
|
||||
throw new IllegalStateException("Buffer did not fully consume");
|
||||
|
||||
}
|
||||
else if (len == -1)
|
||||
{
|
||||
parser.parse(BufferUtil.EMPTY_BUFFER,true);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check for exceptions
|
||||
if (_err != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check we read to the end of the message
|
||||
if (parser.getState() != MultiPartParser.State.END)
|
||||
{
|
||||
if (parser.getState() == MultiPartParser.State.PREAMBLE)
|
||||
_err = new IOException("Missing initial multi part boundary");
|
||||
else
|
||||
_err = new IOException("Incomplete Multipart");
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("Parsing Complete {} err={}",parser,_err);
|
||||
}
|
||||
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
_err = e;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Handler implements MultiPartParser.Handler
|
||||
{
|
||||
private MultiPart _part = null;
|
||||
private String contentDisposition = null;
|
||||
private String contentType = null;
|
||||
private MultiMap<String> headers = new MultiMap<>();
|
||||
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parsedField(String key, String value)
|
||||
{
|
||||
// Add to headers and mark if one of these fields. //
|
||||
headers.put(StringUtil.asciiToLowerCase(key),value);
|
||||
if (key.equalsIgnoreCase("content-disposition"))
|
||||
contentDisposition = value;
|
||||
else if (key.equalsIgnoreCase("content-type"))
|
||||
contentType = value;
|
||||
|
||||
// Transfer encoding is not longer considers as it is deprecated as per
|
||||
// https://tools.ietf.org/html/rfc7578#section-4.7
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
{
|
||||
LOG.debug("headerComplete {}",this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Extract content-disposition
|
||||
boolean form_data = false;
|
||||
if (contentDisposition == null)
|
||||
{
|
||||
throw new IOException("Missing content-disposition");
|
||||
}
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition,";",false,true);
|
||||
String name = null;
|
||||
String filename = null;
|
||||
while (tok.hasMoreTokens())
|
||||
{
|
||||
String t = tok.nextToken().trim();
|
||||
String tl = StringUtil.asciiToLowerCase(t);
|
||||
if (tl.startsWith("form-data"))
|
||||
form_data = true;
|
||||
else if (tl.startsWith("name="))
|
||||
name = value(t);
|
||||
else if (tl.startsWith("filename="))
|
||||
filename = filenameValue(t);
|
||||
}
|
||||
|
||||
// Check disposition
|
||||
if (!form_data)
|
||||
throw new IOException("Part not form-data");
|
||||
|
||||
// It is valid for reset and submit buttons to have an empty name.
|
||||
// If no name is supplied, the browser skips sending the info for that field.
|
||||
// However, if you supply the empty string as the name, the browser sends the
|
||||
// field, with name as the empty string. So, only continue this loop if we
|
||||
// have not yet seen a name field.
|
||||
if (name == null)
|
||||
throw new IOException("No name in part");
|
||||
|
||||
|
||||
// create the new part
|
||||
_part = new MultiPart(name,filename);
|
||||
_part.setHeaders(headers);
|
||||
_part.setContentType(contentType);
|
||||
_parts.add(name,_part);
|
||||
|
||||
try
|
||||
{
|
||||
_part.open();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
if(_part == null)
|
||||
return false;
|
||||
|
||||
if (BufferUtil.hasContent(buffer))
|
||||
{
|
||||
try
|
||||
{
|
||||
_part.write(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (last)
|
||||
{
|
||||
try
|
||||
{
|
||||
_part.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_err = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startPart()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void earlyEOF()
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Early EOF {}",MultiPartFormInputStream.this);
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
_part = null;
|
||||
contentDisposition = null;
|
||||
contentType = null;
|
||||
headers = new MultiMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void setDeleteOnExit(boolean deleteOnExit)
|
||||
{
|
||||
_deleteOnExit = deleteOnExit;
|
||||
}
|
||||
|
||||
public void setWriteFilesWithFilenames(boolean writeFilesWithFilenames)
|
||||
{
|
||||
_writeFilesWithFilenames = writeFilesWithFilenames;
|
||||
}
|
||||
|
||||
public boolean isWriteFilesWithFilenames()
|
||||
{
|
||||
return _writeFilesWithFilenames;
|
||||
}
|
||||
|
||||
public boolean isDeleteOnExit()
|
||||
{
|
||||
return _deleteOnExit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
private String value(String nameEqualsValue)
|
||||
{
|
||||
int idx = nameEqualsValue.indexOf('=');
|
||||
String value = nameEqualsValue.substring(idx + 1).trim();
|
||||
return QuotedStringTokenizer.unquoteOnly(value);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
private String filenameValue(String nameEqualsValue)
|
||||
{
|
||||
int idx = nameEqualsValue.indexOf('=');
|
||||
String value = nameEqualsValue.substring(idx + 1).trim();
|
||||
|
||||
if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
|
||||
{
|
||||
// incorrectly escaped IE filenames that have the whole path
|
||||
// we just strip any leading & trailing quotes and leave it as is
|
||||
char first = value.charAt(0);
|
||||
if (first == '"' || first == '\'')
|
||||
value = value.substring(1);
|
||||
char last = value.charAt(value.length() - 1);
|
||||
if (last == '"' || last == '\'')
|
||||
value = value.substring(0,value.length() - 1);
|
||||
|
||||
return value;
|
||||
}
|
||||
else
|
||||
// unquote the string, but allow any backslashes that don't
|
||||
// form a valid escape sequence to remain as many browsers
|
||||
// even on *nix systems will not escape a filename containing
|
||||
// backslashes
|
||||
return QuotedStringTokenizer.unquoteOnly(value,true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,766 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.eclipse.jetty.http.HttpParser.RequestHandler;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.SearchPattern;
|
||||
import org.eclipse.jetty.util.Utf8StringBuilder;
|
||||
import org.eclipse.jetty.util.log.Log;
|
||||
import org.eclipse.jetty.util.log.Logger;
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/** A parser for MultiPart content type.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/html/rfc2046#section-5.1">https://tools.ietf.org/html/rfc2046#section-5.1</a>
|
||||
* @see <a href="https://tools.ietf.org/html/rfc2045">https://tools.ietf.org/html/rfc2045</a>
|
||||
*/
|
||||
public class MultiPartParser
|
||||
{
|
||||
public static final Logger LOG = Log.getLogger(MultiPartParser.class);
|
||||
|
||||
static final byte COLON = (byte)':';
|
||||
static final byte TAB = 0x09;
|
||||
static final byte LINE_FEED = 0x0A;
|
||||
static final byte CARRIAGE_RETURN = 0x0D;
|
||||
static final byte SPACE = 0x20;
|
||||
static final byte[] CRLF =
|
||||
{ CARRIAGE_RETURN, LINE_FEED };
|
||||
static final byte SEMI_COLON = (byte)';';
|
||||
|
||||
// States
|
||||
public enum FieldState
|
||||
{
|
||||
FIELD,
|
||||
IN_NAME,
|
||||
AFTER_NAME,
|
||||
VALUE,
|
||||
IN_VALUE
|
||||
}
|
||||
|
||||
// States
|
||||
public enum State
|
||||
{
|
||||
PREAMBLE,
|
||||
DELIMITER,
|
||||
DELIMITER_PADDING,
|
||||
DELIMITER_CLOSE,
|
||||
BODY_PART,
|
||||
FIRST_OCTETS,
|
||||
OCTETS,
|
||||
EPILOGUE,
|
||||
END
|
||||
}
|
||||
|
||||
private final static EnumSet<State> __delimiterStates = EnumSet.of(State.DELIMITER,State.DELIMITER_CLOSE,State.DELIMITER_PADDING);
|
||||
|
||||
private final boolean DEBUG = LOG.isDebugEnabled();
|
||||
private final Handler _handler;
|
||||
private final SearchPattern _delimiterSearch;
|
||||
|
||||
private String _fieldName;
|
||||
private String _fieldValue;
|
||||
|
||||
private State _state = State.PREAMBLE;
|
||||
private FieldState _fieldState = FieldState.FIELD;
|
||||
private int _partialBoundary = 2; // No CRLF if no preamble
|
||||
private boolean _cr;
|
||||
private ByteBuffer _patternBuffer;
|
||||
|
||||
private final Utf8StringBuilder _string = new Utf8StringBuilder();
|
||||
private int _length;
|
||||
|
||||
private int _totalHeaderLineLength = -1;
|
||||
private int _maxHeaderLineLength = 998;
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
public MultiPartParser(Handler handler, String boundary)
|
||||
{
|
||||
_handler = handler;
|
||||
|
||||
String delimiter = "\r\n--" + boundary;
|
||||
_patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII));
|
||||
_delimiterSearch = SearchPattern.compile(_patternBuffer.array());
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
_state = State.PREAMBLE;
|
||||
_fieldState = FieldState.FIELD;
|
||||
_partialBoundary = 2; // No CRLF if no preamble
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
public Handler getHandler()
|
||||
{
|
||||
return _handler;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
public State getState()
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
public boolean isState(State state)
|
||||
{
|
||||
return _state == state;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
enum CharState
|
||||
{
|
||||
ILLEGAL, CR, LF, LEGAL
|
||||
}
|
||||
private final static CharState[] __charState;
|
||||
static
|
||||
{
|
||||
// token = 1*tchar
|
||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||
// / DIGIT / ALPHA
|
||||
// ; any VCHAR, except delimiters
|
||||
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
||||
// qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
|
||||
// obs-text = %x80-FF
|
||||
// comment = "(" *( ctext / quoted-pair / comment ) ")"
|
||||
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
||||
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||
|
||||
__charState = new CharState[256];
|
||||
Arrays.fill(__charState,CharState.ILLEGAL);
|
||||
__charState[LINE_FEED] = CharState.LF;
|
||||
__charState[CARRIAGE_RETURN] = CharState.CR;
|
||||
__charState[TAB] = CharState.LEGAL;
|
||||
__charState[SPACE] = CharState.LEGAL;
|
||||
|
||||
__charState['!'] = CharState.LEGAL;
|
||||
__charState['#'] = CharState.LEGAL;
|
||||
__charState['$'] = CharState.LEGAL;
|
||||
__charState['%'] = CharState.LEGAL;
|
||||
__charState['&'] = CharState.LEGAL;
|
||||
__charState['\''] = CharState.LEGAL;
|
||||
__charState['*'] = CharState.LEGAL;
|
||||
__charState['+'] = CharState.LEGAL;
|
||||
__charState['-'] = CharState.LEGAL;
|
||||
__charState['.'] = CharState.LEGAL;
|
||||
__charState['^'] = CharState.LEGAL;
|
||||
__charState['_'] = CharState.LEGAL;
|
||||
__charState['`'] = CharState.LEGAL;
|
||||
__charState['|'] = CharState.LEGAL;
|
||||
__charState['~'] = CharState.LEGAL;
|
||||
|
||||
__charState['"'] = CharState.LEGAL;
|
||||
|
||||
__charState['\\'] = CharState.LEGAL;
|
||||
__charState['('] = CharState.LEGAL;
|
||||
__charState[')'] = CharState.LEGAL;
|
||||
Arrays.fill(__charState,0x21,0x27 + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x2A,0x5B + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x5D,0x7E + 1,CharState.LEGAL);
|
||||
Arrays.fill(__charState,0x80,0xFF + 1,CharState.LEGAL);
|
||||
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private boolean hasNextByte(ByteBuffer buffer)
|
||||
{
|
||||
return BufferUtil.hasContent(buffer);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private byte getNextByte(ByteBuffer buffer)
|
||||
{
|
||||
|
||||
byte ch = buffer.get();
|
||||
|
||||
CharState s = __charState[0xff & ch];
|
||||
switch (s)
|
||||
{
|
||||
case LF:
|
||||
_cr = false;
|
||||
return ch;
|
||||
|
||||
case CR:
|
||||
if (_cr)
|
||||
throw new BadMessageException("Bad EOL");
|
||||
|
||||
_cr = true;
|
||||
if (buffer.hasRemaining())
|
||||
return getNextByte(buffer);
|
||||
|
||||
// Can return 0 here to indicate the need for more characters,
|
||||
// because a real 0 in the buffer would cause a BadMessage below
|
||||
return 0;
|
||||
|
||||
case LEGAL:
|
||||
if (_cr)
|
||||
throw new BadMessageException("Bad EOL");
|
||||
|
||||
return ch;
|
||||
|
||||
case ILLEGAL:
|
||||
default:
|
||||
throw new IllegalCharacterException(_state,ch,buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void setString(String s)
|
||||
{
|
||||
_string.reset();
|
||||
_string.append(s);
|
||||
_length = s.length();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
/*
|
||||
* Mime Field strings are treated as UTF-8 as per https://tools.ietf.org/html/rfc7578#section-5.1
|
||||
*/
|
||||
private String takeString()
|
||||
{
|
||||
String s = _string.toString();
|
||||
// trim trailing whitespace.
|
||||
if (s.length()>_length)
|
||||
s = s.substring(0,_length);
|
||||
_string.reset();
|
||||
_length = -1;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
/**
|
||||
* Parse until next Event.
|
||||
*
|
||||
* @param buffer the buffer to parse
|
||||
* @param last whether this buffer contains last bit of content
|
||||
* @return True if an {@link RequestHandler} method was called and it returned true;
|
||||
*/
|
||||
public boolean parse(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
boolean handle = false;
|
||||
while (handle == false && BufferUtil.hasContent(buffer))
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case PREAMBLE:
|
||||
parsePreamble(buffer);
|
||||
continue;
|
||||
|
||||
case DELIMITER:
|
||||
case DELIMITER_PADDING:
|
||||
case DELIMITER_CLOSE:
|
||||
parseDelimiter(buffer);
|
||||
continue;
|
||||
|
||||
case BODY_PART:
|
||||
handle = parseMimePartHeaders(buffer);
|
||||
break;
|
||||
|
||||
case FIRST_OCTETS:
|
||||
case OCTETS:
|
||||
handle = parseOctetContent(buffer);
|
||||
break;
|
||||
|
||||
case EPILOGUE:
|
||||
BufferUtil.clear(buffer);
|
||||
break;
|
||||
|
||||
case END:
|
||||
handle = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (last && BufferUtil.isEmpty(buffer))
|
||||
{
|
||||
if (_state == State.EPILOGUE)
|
||||
{
|
||||
_state = State.END;
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("messageComplete {}", this);
|
||||
|
||||
return _handler.messageComplete();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("earlyEOF {}", this);
|
||||
|
||||
_handler.earlyEOF();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void parsePreamble(ByteBuffer buffer)
|
||||
{
|
||||
if (_partialBoundary > 0)
|
||||
{
|
||||
int partial = _delimiterSearch.startsWith(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining(),_partialBoundary);
|
||||
if (partial > 0)
|
||||
{
|
||||
if (partial == _delimiterSearch.getLength())
|
||||
{
|
||||
buffer.position(buffer.position() + partial - _partialBoundary);
|
||||
_partialBoundary = 0;
|
||||
setState(State.DELIMITER);
|
||||
return;
|
||||
}
|
||||
|
||||
_partialBoundary = partial;
|
||||
BufferUtil.clear(buffer);
|
||||
return;
|
||||
}
|
||||
|
||||
_partialBoundary = 0;
|
||||
}
|
||||
|
||||
int delimiter = _delimiterSearch.match(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
if (delimiter >= 0)
|
||||
{
|
||||
buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
|
||||
setState(State.DELIMITER);
|
||||
return;
|
||||
}
|
||||
|
||||
_partialBoundary = _delimiterSearch.endsWith(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
BufferUtil.clear(buffer);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void parseDelimiter(ByteBuffer buffer)
|
||||
{
|
||||
while (__delimiterStates.contains(_state) && hasNextByte(buffer))
|
||||
{
|
||||
byte b = getNextByte(buffer);
|
||||
if (b == 0)
|
||||
return;
|
||||
|
||||
if (b == '\n')
|
||||
{
|
||||
setState(State.BODY_PART);
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("startPart {}",this);
|
||||
|
||||
_handler.startPart();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_state)
|
||||
{
|
||||
case DELIMITER:
|
||||
if (b == '-')
|
||||
setState(State.DELIMITER_CLOSE);
|
||||
else
|
||||
setState(State.DELIMITER_PADDING);
|
||||
continue;
|
||||
|
||||
case DELIMITER_CLOSE:
|
||||
if (b == '-')
|
||||
{
|
||||
setState(State.EPILOGUE);
|
||||
return;
|
||||
}
|
||||
setState(State.DELIMITER_PADDING);
|
||||
continue;
|
||||
|
||||
case DELIMITER_PADDING:
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
/*
|
||||
* Parse the message headers and return true if the handler has signaled for a return
|
||||
*/
|
||||
protected boolean parseMimePartHeaders(ByteBuffer buffer)
|
||||
{
|
||||
// Process headers
|
||||
while (_state == State.BODY_PART && hasNextByte(buffer))
|
||||
{
|
||||
// process each character
|
||||
byte b = getNextByte(buffer);
|
||||
if (b == 0)
|
||||
break;
|
||||
|
||||
if (b != LINE_FEED)
|
||||
_totalHeaderLineLength++;
|
||||
|
||||
if (_totalHeaderLineLength > _maxHeaderLineLength)
|
||||
throw new IllegalStateException("Header Line Exceeded Max Length");
|
||||
|
||||
switch (_fieldState)
|
||||
{
|
||||
case FIELD:
|
||||
switch (b)
|
||||
{
|
||||
case SPACE:
|
||||
case TAB:
|
||||
{
|
||||
// Folded field value!
|
||||
|
||||
if (_fieldName == null)
|
||||
throw new IllegalStateException("First field folded");
|
||||
|
||||
if (_fieldValue == null)
|
||||
{
|
||||
_string.reset();
|
||||
_length = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
setString(_fieldValue);
|
||||
_string.append(' ');
|
||||
_length++;
|
||||
_fieldValue = null;
|
||||
}
|
||||
setState(FieldState.VALUE);
|
||||
break;
|
||||
}
|
||||
|
||||
case LINE_FEED:
|
||||
{
|
||||
handleField();
|
||||
setState(State.FIRST_OCTETS);
|
||||
_partialBoundary = 2; // CRLF is option for empty parts
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("headerComplete {}", this);
|
||||
|
||||
if (_handler.headerComplete())
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// process previous header
|
||||
handleField();
|
||||
|
||||
// New header
|
||||
setState(FieldState.IN_NAME);
|
||||
_string.reset();
|
||||
_string.append(b);
|
||||
_length = 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case IN_NAME:
|
||||
switch (b)
|
||||
{
|
||||
case COLON:
|
||||
_fieldName = takeString();
|
||||
_length = -1;
|
||||
setState(FieldState.VALUE);
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
// Ignore trailing whitespaces
|
||||
setState(FieldState.AFTER_NAME);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Line Feed in Name {}", this);
|
||||
|
||||
handleField();
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
_length = _string.length();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case AFTER_NAME:
|
||||
switch (b)
|
||||
{
|
||||
case COLON:
|
||||
_fieldName = takeString();
|
||||
_length = -1;
|
||||
setState(FieldState.VALUE);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
_fieldName = takeString();
|
||||
_string.reset();
|
||||
_fieldValue = "";
|
||||
_length = -1;
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalCharacterException(_state,b,buffer);
|
||||
}
|
||||
break;
|
||||
|
||||
case VALUE:
|
||||
switch (b)
|
||||
{
|
||||
case LINE_FEED:
|
||||
_string.reset();
|
||||
_fieldValue = "";
|
||||
_length = -1;
|
||||
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
|
||||
case SPACE:
|
||||
case TAB:
|
||||
break;
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
_length = _string.length();
|
||||
setState(FieldState.IN_VALUE);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case IN_VALUE:
|
||||
switch (b)
|
||||
{
|
||||
case SPACE:
|
||||
_string.append(b);
|
||||
break;
|
||||
|
||||
case LINE_FEED:
|
||||
if (_length > 0)
|
||||
{
|
||||
_fieldValue = takeString();
|
||||
_length = -1;
|
||||
_totalHeaderLineLength = -1;
|
||||
}
|
||||
setState(FieldState.FIELD);
|
||||
break;
|
||||
|
||||
default:
|
||||
_string.append(b);
|
||||
if (b > SPACE || b < 0)
|
||||
_length = _string.length();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(_state.toString());
|
||||
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void handleField()
|
||||
{
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", _fieldName, _fieldValue, this);
|
||||
|
||||
if (_fieldName != null && _fieldValue != null)
|
||||
_handler.parsedField(_fieldName,_fieldValue);
|
||||
_fieldName = _fieldValue = null;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
|
||||
protected boolean parseOctetContent(ByteBuffer buffer)
|
||||
{
|
||||
|
||||
// Starts With
|
||||
if (_partialBoundary > 0)
|
||||
{
|
||||
int partial = _delimiterSearch.startsWith(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining(),_partialBoundary);
|
||||
if (partial > 0)
|
||||
{
|
||||
if (partial == _delimiterSearch.getLength())
|
||||
{
|
||||
buffer.position(buffer.position() + _delimiterSearch.getLength() - _partialBoundary);
|
||||
setState(State.DELIMITER);
|
||||
_partialBoundary = 0;
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER),true,this);
|
||||
|
||||
return _handler.content(BufferUtil.EMPTY_BUFFER,true);
|
||||
}
|
||||
|
||||
_partialBoundary = partial;
|
||||
BufferUtil.clear(buffer);
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// output up to _partialBoundary of the search pattern
|
||||
ByteBuffer content = _patternBuffer.slice();
|
||||
if (_state == State.FIRST_OCTETS)
|
||||
{
|
||||
setState(State.OCTETS);
|
||||
content.position(2);
|
||||
}
|
||||
content.limit(_partialBoundary);
|
||||
_partialBoundary = 0;
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(content),false,this);
|
||||
|
||||
if (_handler.content(content,false))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Contains
|
||||
int delimiter = _delimiterSearch.match(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
if (delimiter >= 0)
|
||||
{
|
||||
ByteBuffer content = buffer.slice();
|
||||
content.limit(delimiter - buffer.arrayOffset() - buffer.position());
|
||||
|
||||
buffer.position(delimiter - buffer.arrayOffset() + _delimiterSearch.getLength());
|
||||
setState(State.DELIMITER);
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(content),true,this);
|
||||
|
||||
return _handler.content(content,true);
|
||||
}
|
||||
|
||||
// Ends With
|
||||
_partialBoundary = _delimiterSearch.endsWith(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
|
||||
if (_partialBoundary > 0)
|
||||
{
|
||||
ByteBuffer content = buffer.slice();
|
||||
content.limit(content.limit() - _partialBoundary);
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(content),false,this);
|
||||
|
||||
BufferUtil.clear(buffer);
|
||||
return _handler.content(content,false);
|
||||
}
|
||||
|
||||
// There is normal content with no delimiter
|
||||
ByteBuffer content = buffer.slice();
|
||||
|
||||
if(LOG.isDebugEnabled())
|
||||
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(content),false,this);
|
||||
|
||||
BufferUtil.clear(buffer);
|
||||
return _handler.content(content,false);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void setState(State state)
|
||||
{
|
||||
if (DEBUG)
|
||||
LOG.debug("{} --> {}",_state,state);
|
||||
_state = state;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
private void setState(FieldState state)
|
||||
{
|
||||
if (DEBUG)
|
||||
LOG.debug("{}:{} --> {}",_state,_fieldState,state);
|
||||
_fieldState = state;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s{s=%s}",getClass().getSimpleName(),_state);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
* Event Handler interface These methods return true if the caller should process the events so far received (eg return from parseNext and call
|
||||
* HttpChannel.handle). If multiple callbacks are called in sequence (eg headerComplete then messageComplete) from the same point in the parsing then it is
|
||||
* sufficient for the caller to process the events only once.
|
||||
*/
|
||||
public interface Handler
|
||||
{
|
||||
public default void startPart()
|
||||
{
|
||||
}
|
||||
|
||||
public default void parsedField(String name, String value)
|
||||
{
|
||||
}
|
||||
|
||||
public default boolean headerComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default boolean content(ByteBuffer item, boolean last)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default boolean messageComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public default void earlyEOF()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------- */
|
||||
@SuppressWarnings("serial")
|
||||
private static class IllegalCharacterException extends IllegalArgumentException
|
||||
{
|
||||
private IllegalCharacterException(State state, byte ch, ByteBuffer buffer)
|
||||
{
|
||||
super(String.format("Illegal character 0x%X",ch));
|
||||
// Bug #460642 - don't reveal buffers to end user
|
||||
LOG.warn(String.format("Illegal character 0x%X in state=%s for buffer %s",ch,state,BufferUtil.toDetailString(buffer)));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,404 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.toolchain.test.Hex;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.IO;
|
||||
import org.eclipse.jetty.util.QuotedStringTokenizer;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class MultiPartCaptureTest
|
||||
{
|
||||
|
||||
public static final int MAX_FILE_SIZE = 2 * 1024 * 1024;
|
||||
public static final int MAX_REQUEST_SIZE = MAX_FILE_SIZE + (60 * 1024);
|
||||
public static final int FILE_SIZE_THRESHOLD = 50;
|
||||
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
public static List<Object[]> data()
|
||||
{
|
||||
List<Object[]> ret = new ArrayList<>();
|
||||
|
||||
// == Arbitrary / Non-Standard Examples ==
|
||||
|
||||
ret.add(new String[]{"multipart-uppercase"});
|
||||
// ret.add(new String[]{"multipart-base64"}); // base64 transfer encoding deprecated
|
||||
// ret.add(new String[]{"multipart-base64-long"}); // base64 transfer encoding deprecated
|
||||
|
||||
// == Capture of raw request body contents from Apache HttpClient 4.5.5 ==
|
||||
|
||||
ret.add(new String[]{"browser-capture-company-urlencoded-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-complex-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-duplicate-names-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-encoding-mess-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-nested-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-nested-binary-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-number-only2-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-number-only-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-sjis-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-strange-quoting-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-text-files-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-unicode-names-apache-httpcomp"});
|
||||
ret.add(new String[]{"browser-capture-zalgo-text-plain-apache-httpcomp"});
|
||||
|
||||
// == Capture of raw request body contents from Eclipse Jetty Http Client 9.4.9 ==
|
||||
|
||||
ret.add(new String[]{"browser-capture-complex-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-duplicate-names-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-encoding-mess-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-nested-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-number-only-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-sjis-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-text-files-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-unicode-names-jetty-client"});
|
||||
ret.add(new String[]{"browser-capture-whitespace-only-jetty-client"});
|
||||
|
||||
// == Capture of raw request body contents from various browsers ==
|
||||
|
||||
// simple form - 2 fields
|
||||
ret.add(new String[]{"browser-capture-form1-android-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form1-android-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form1-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form1-edge"});
|
||||
ret.add(new String[]{"browser-capture-form1-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form1-ios-safari"});
|
||||
ret.add(new String[]{"browser-capture-form1-msie"});
|
||||
ret.add(new String[]{"browser-capture-form1-osx-safari"});
|
||||
|
||||
// form submitted as shift-jis
|
||||
ret.add(new String[]{"browser-capture-sjis-form-edge"});
|
||||
ret.add(new String[]{"browser-capture-sjis-form-msie"});
|
||||
// TODO: these might be addressable via Issue #2398
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-android-chrome"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-android-firefox"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-chrome"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-firefox"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-ios-safari"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
// ret.add(new String[]{"browser-capture-sjis-form-safari"}); // contains html encoded character and unspecified charset defaults to utf-8
|
||||
|
||||
// form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-android-chrome"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-android-firefox"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-chrome"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-edge"});
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-firefox"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-ios-safari"}); // contains html encoded character
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-msie"});
|
||||
ret.add(new String[]{"browser-capture-sjis-charset-form-safari"}); // contains html encoded character
|
||||
|
||||
// form submitted with simple file upload
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-android-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-android-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-edge"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-ios-safari"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-msie"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-safari"});
|
||||
|
||||
// form submitted with 2 files (1 binary, 1 text) and 2 text fields
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-chrome"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-edge"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-firefox"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-msie"});
|
||||
ret.add(new String[]{"browser-capture-form-fileupload-alt-safari"});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Rule
|
||||
public TestingDir testingDir = new TestingDir();
|
||||
|
||||
private final Path multipartRawFile;
|
||||
private final MultipartExpectations multipartExpectations;
|
||||
|
||||
public MultiPartCaptureTest(String rawPrefix) throws IOException
|
||||
{
|
||||
multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".raw");
|
||||
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + rawPrefix + ".expected.txt");
|
||||
multipartExpectations = new MultipartExpectations(expectationPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUtilParse() throws Exception
|
||||
{
|
||||
Path outputDir = testingDir.getEmptyPathDir();
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in,multipartExpectations.contentType,config,outputDir.toFile());
|
||||
|
||||
checkParts(parser.getParts(),s->
|
||||
{
|
||||
try
|
||||
{
|
||||
return parser.getPart(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpParse() throws Exception
|
||||
{
|
||||
Path outputDir = testingDir.getEmptyPathDir();
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
|
||||
|
||||
checkParts(parser.getParts(),s->
|
||||
{
|
||||
try
|
||||
{
|
||||
return parser.getPart(s);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void checkParts(Collection<Part> parts, Function<String, Part> getPart) throws Exception
|
||||
{
|
||||
// Evaluate Count
|
||||
if (multipartExpectations.partCount >= 0)
|
||||
{
|
||||
assertThat("Mulitpart.parts.size", parts.size(), is(multipartExpectations.partCount));
|
||||
}
|
||||
|
||||
String defaultCharset = UTF_8.toString();
|
||||
Part charSetPart = getPart.apply("_charset_");
|
||||
if(charSetPart != null)
|
||||
{
|
||||
defaultCharset = IO.toString(charSetPart.getInputStream());
|
||||
}
|
||||
|
||||
|
||||
// Evaluate expected Contents
|
||||
for (NameValue expected : multipartExpectations.partContainsContents)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
try (InputStream partInputStream = part.getInputStream())
|
||||
{
|
||||
String charset = getCharsetFromContentType(part.getContentType(), defaultCharset);
|
||||
String contents = IO.toString(partInputStream, charset);
|
||||
assertThat("Part[" + expected.name + "].contents", contents, containsString(expected.value));
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate expected filenames
|
||||
for (NameValue expected : multipartExpectations.partFilenames)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
assertThat("Part[" + expected.name + "]", part.getSubmittedFileName(), is(expected.value));
|
||||
}
|
||||
|
||||
// Evaluate expected contents checksums
|
||||
for (NameValue expected : multipartExpectations.partSha1sums)
|
||||
{
|
||||
Part part = getPart.apply(expected.name);
|
||||
assertThat("Part[" + expected.name + "]", part, is(notNullValue()));
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA1");
|
||||
try (InputStream partInputStream = part.getInputStream();
|
||||
NoOpOutputStream noop = new NoOpOutputStream();
|
||||
DigestOutputStream digester = new DigestOutputStream(noop, digest))
|
||||
{
|
||||
IO.copy(partInputStream, digester);
|
||||
String actualSha1sum = Hex.asHex(digest.digest()).toLowerCase(Locale.US);
|
||||
assertThat("Part[" + expected.name + "].sha1sum", actualSha1sum, Matchers.equalToIgnoringCase(expected.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MultipartConfigElement newMultipartConfigElement(Path path)
|
||||
{
|
||||
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
|
||||
}
|
||||
|
||||
private String getCharsetFromContentType(String contentType, String defaultCharset)
|
||||
{
|
||||
if(StringUtil.isBlank(contentType))
|
||||
{
|
||||
return defaultCharset;
|
||||
}
|
||||
|
||||
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentType, ";", false, false);
|
||||
while(tok.hasMoreTokens())
|
||||
{
|
||||
String str = tok.nextToken().trim();
|
||||
if(str.startsWith("charset="))
|
||||
{
|
||||
return str.substring("charset=".length());
|
||||
}
|
||||
}
|
||||
|
||||
return defaultCharset;
|
||||
}
|
||||
|
||||
public static class NameValue
|
||||
{
|
||||
public String name;
|
||||
public String value;
|
||||
}
|
||||
|
||||
public static class MultipartExpectations
|
||||
{
|
||||
public final String contentType;
|
||||
public final int partCount;
|
||||
public final List<NameValue> partFilenames = new ArrayList<>();
|
||||
public final List<NameValue> partSha1sums = new ArrayList<>();
|
||||
public final List<NameValue> partContainsContents = new ArrayList<>();
|
||||
|
||||
public MultipartExpectations(Path expectationsPath) throws IOException
|
||||
{
|
||||
String parsedContentType = null;
|
||||
String parsedPartCount = "-1";
|
||||
|
||||
try (BufferedReader reader = Files.newBufferedReader(expectationsPath))
|
||||
{
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null)
|
||||
{
|
||||
line = line.trim();
|
||||
if (StringUtil.isBlank(line) || line.startsWith("#"))
|
||||
{
|
||||
// skip blanks and comments
|
||||
continue;
|
||||
}
|
||||
|
||||
String split[] = line.split("\\|");
|
||||
switch (split[0])
|
||||
{
|
||||
case "Request-Header":
|
||||
if(split[1].equalsIgnoreCase("Content-Type"))
|
||||
{
|
||||
parsedContentType = split[2];
|
||||
}
|
||||
break;
|
||||
case "Content-Type":
|
||||
parsedContentType = split[1];
|
||||
break;
|
||||
case "Parts-Count":
|
||||
parsedPartCount = split[1];
|
||||
break;
|
||||
case "Part-ContainsContents":
|
||||
{
|
||||
NameValue pair = new NameValue();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partContainsContents.add(pair);
|
||||
break;
|
||||
}
|
||||
case "Part-Filename":
|
||||
{
|
||||
NameValue pair = new NameValue();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partFilenames.add(pair);
|
||||
break;
|
||||
}
|
||||
case "Part-Sha1sum":
|
||||
{
|
||||
NameValue pair = new NameValue();
|
||||
pair.name = split[1];
|
||||
pair.value = split[2];
|
||||
partSha1sums.add(pair);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new IOException("Bad Line in " + expectationsPath + ": " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Objects.requireNonNull(parsedContentType, "Missing required 'Content-Type' declaration: " + expectationsPath);
|
||||
this.contentType = parsedContentType;
|
||||
this.partCount = Integer.parseInt(parsedPartCount);
|
||||
}
|
||||
}
|
||||
|
||||
class NoOpOutputStream extends OutputStream
|
||||
{
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,711 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import org.eclipse.jetty.http.MultiPartParser.State;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
public class MultiPartParserTest
|
||||
{
|
||||
|
||||
@Test
|
||||
public void testEmptyPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY \r\n");
|
||||
parser.parse(data,false);
|
||||
assertTrue(parser.isState(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreamble()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("More data that almost includes \n--BOUNDARY but no CR before.");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("Could be a boundary \r\n--BOUNDAR");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("but not it isn't \r\n--BOUN");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
|
||||
data = BufferUtil.toBuffer("DARX nor is this");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreambleCompleteBoundary()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n--BOUNDARY \r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPreambleSplitBoundary()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data;
|
||||
|
||||
data = BufferUtil.toBuffer("This is not part of a part\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("B");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.PREAMBLE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("OUNDARY-");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.DELIMITER_CLOSE));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("ignore\r");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.DELIMITER_PADDING));
|
||||
assertThat(data.remaining(),is(0));
|
||||
data = BufferUtil.toBuffer("\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.BODY_PART));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoFields()
|
||||
{
|
||||
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.FIRST_OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartFields()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
super.headerComplete();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name0: value0\r\n"
|
||||
+ "name1 :value1 \r\n"
|
||||
+ "name2:value\r\n"
|
||||
+ " 2\r\n"
|
||||
+ "\r\n"
|
||||
+ "Content");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.FIRST_OCTETS));
|
||||
assertThat(data.remaining(),is(7));
|
||||
assertThat(handler.fields,Matchers.contains("name0: value0","name1: value1", "name2: value 2", "<<COMPLETE>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("<<LAST>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartNoContentNoCRLF()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("<<LAST>>"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartContentLookingLikeNoCRLF()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\r\n"
|
||||
+ "\r\n"
|
||||
+ "-");
|
||||
parser.parse(data,false);
|
||||
data = BufferUtil.toBuffer("Content!");
|
||||
parser.parse(data,false);
|
||||
|
||||
|
||||
assertThat(parser.getState(), is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("-","Content!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartPartialContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello"));
|
||||
|
||||
data = BufferUtil.toBuffer(
|
||||
"Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "this is not a --BOUNDARY\r\n");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(),is(State.OCTETS));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","\r\n","Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "this is not a --BOUNDARY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartShortContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","<<LAST>>"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testFirstPartLongContent()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Now is the time for all good ment to come to the aid of the party.\r\n"
|
||||
+ "How now brown cow.\r\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\r\n","<<LAST>>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstPartLongContentNoCarriageReturn()
|
||||
{
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer("");
|
||||
|
||||
//boundary still requires carriage return
|
||||
data = BufferUtil.toBuffer("--BOUNDARY\n"
|
||||
+ "name: value\n"
|
||||
+ "\n"
|
||||
+ "Now is the time for all good men to come to the aid of the party.\n"
|
||||
+ "How now brown cow.\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\n"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(0));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Now is the time for all good men to come to the aid of the party.\n"
|
||||
+ "How now brown cow.\n"
|
||||
+ "The quick brown fox jumped over the lazy dog.\n","<<LAST>>"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBinaryPart()
|
||||
{
|
||||
byte[] random = new byte[8192];
|
||||
final ByteBuffer bytes = BufferUtil.allocate(random.length);
|
||||
ThreadLocalRandom.current().nextBytes(random);
|
||||
// Arrays.fill(random,(byte)'X');
|
||||
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
BufferUtil.append(bytes,buffer);
|
||||
return last;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
String preamble = "Blah blah blah\r\n--BOUNDARY\r\n\r\n";
|
||||
String epilogue = "\r\n--BOUNDARY\r\nBlah blah blah!\r\n";
|
||||
|
||||
ByteBuffer data = BufferUtil.allocate(preamble.length()+random.length+epilogue.length());
|
||||
BufferUtil.append(data,BufferUtil.toBuffer(preamble));
|
||||
BufferUtil.append(data,ByteBuffer.wrap(random));
|
||||
BufferUtil.append(data,BufferUtil.toBuffer(epilogue));
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(data.remaining(),is(19));
|
||||
assertThat(bytes.array(),is(random));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEpilogue() {
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello\r\n"
|
||||
+ "--BOUNDARY--"
|
||||
+ "epilogue here:"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY--"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY");
|
||||
|
||||
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields,Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content,Matchers.contains("Hello","<<LAST>>"));
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMultipleContent() {
|
||||
TestHandler handler = new TestHandler();
|
||||
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "name: value\n"
|
||||
+ "\r\n"
|
||||
+ "Hello"
|
||||
+ "\r\n"
|
||||
+ "--BOUNDARY\r\n"
|
||||
+ "powerLevel: 9001\n"
|
||||
+ "\r\n"
|
||||
+ "secondary"
|
||||
+ "\r\n"
|
||||
+ "content"
|
||||
+ "\r\n--BOUNDARY--"
|
||||
+ "epilogue here");
|
||||
|
||||
/* Test First Content Section */
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
|
||||
assertThat(handler.content, Matchers.contains("Hello","<<LAST>>"));
|
||||
|
||||
/* Test Second Content Section */
|
||||
parser.parse(data,false);
|
||||
assertThat(parser.getState(), is(State.DELIMITER));
|
||||
assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>","powerLevel: 9001","<<COMPLETE>>"));
|
||||
assertThat(handler.content, Matchers.contains("Hello","<<LAST>>","secondary\r\ncontent","<<LAST>>"));
|
||||
|
||||
/* Test Progression to END State */
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
assertThat(data.remaining(),is(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void testCrAsLineTermination() {
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override public boolean messageComplete(){ return true; }
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
MultiPartParser parser = new MultiPartParser(handler,"AaB03x");
|
||||
|
||||
ByteBuffer data = BufferUtil.toBuffer(
|
||||
"--AaB03x\r\n"+
|
||||
"content-disposition: form-data; name=\"field1\"\r\n"+
|
||||
"\r"+
|
||||
"Joe Blow\r\n"+
|
||||
"--AaB03x--\r\n");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
parser.parse(data,true);
|
||||
fail("Invalid End of Line");
|
||||
}
|
||||
catch(BadMessageException e) {
|
||||
assertTrue(e.getMessage().contains("Bad EOL"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void splitTest()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
MultiPartParser parser = new MultiPartParser(handler,"---------------------------9051914041544843365972754266");
|
||||
ByteBuffer data = BufferUtil.toBuffer(""+
|
||||
"POST / HTTP/1.1\n" +
|
||||
"Host: localhost:8000\n" +
|
||||
"User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:29.0) Gecko/20100101 Firefox/29.0\n" +
|
||||
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\n" +
|
||||
"Accept-Language: en-US,en;q=0.5\n" +
|
||||
"Accept-Encoding: gzip, deflate\n" +
|
||||
"Cookie: __atuvc=34%7C7; permanent=0; _gitlab_session=226ad8a0be43681acf38c2fab9497240; __profilin=p%3Dt; request_method=GET\n" +
|
||||
"Connection: keep-alive\n" +
|
||||
"Content-Type: multipart/form-data; boundary=---------------------------9051914041544843365972754266\n" +
|
||||
"Content-Length: 554\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"text\"\n" +
|
||||
"\n" +
|
||||
"text default\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\n" +
|
||||
"Content-Type: text/plain\n" +
|
||||
"\n" +
|
||||
"Content of a.txt.\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\n" +
|
||||
"Content-Type: text/html\n" +
|
||||
"\n" +
|
||||
"<!DOCTYPE html><title>Content of a.html.</title>\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Field1: value1\n" +
|
||||
"Field2: value2\n" +
|
||||
"Field3: value3\n" +
|
||||
"Field4: value4\n" +
|
||||
"Field5: value5\n" +
|
||||
"Field6: value6\n" +
|
||||
"Field7: value7\n" +
|
||||
"Field8: value8\n" +
|
||||
"Field9: value\n" +
|
||||
" 9\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266\n" +
|
||||
"Field1: value1\n" +
|
||||
"\r\n"+
|
||||
"But the amount of denudation which the strata have\n" +
|
||||
"in many places suffered, independently of the rate\n" +
|
||||
"of accumulation of the degraded matter, probably\n" +
|
||||
"offers the best evidence of the lapse of time. I remember\n" +
|
||||
"having been much struck with the evidence of\n" +
|
||||
"denudation, when viewing volcanic islands, which\n" +
|
||||
"have been worn by the waves and pared all round\n" +
|
||||
"into perpendicular cliffs of one or two thousand feet\n" +
|
||||
"in height; for the gentle slope of the lava-streams,\n" +
|
||||
"due to their formerly liquid state, showed at a glance\n" +
|
||||
"how far the hard, rocky beds had once extended into\n" +
|
||||
"the open ocean.\n" +
|
||||
"\r\n" +
|
||||
"-----------------------------9051914041544843365972754266--" +
|
||||
"===== ajlkfja;lkdj;lakjd;lkjf ==== epilogue here ==== kajflajdfl;kjafl;kjl;dkfja ====\n\r\n\r\r\r\n\n\n");
|
||||
|
||||
|
||||
int length = data.remaining();
|
||||
for(int i = 0; i<length-1; i++){
|
||||
//partition 0 to i
|
||||
ByteBuffer dataSeg = data.slice();
|
||||
dataSeg.position(0);
|
||||
dataSeg.limit(i);
|
||||
assertThat("First "+i,parser.parse(dataSeg,false),is(false));
|
||||
|
||||
//partition i
|
||||
dataSeg = data.slice();
|
||||
dataSeg.position(i);
|
||||
dataSeg.limit(i+1);
|
||||
assertThat("Second "+i,parser.parse(dataSeg,false),is(false));
|
||||
|
||||
//partition i to length
|
||||
dataSeg = data.slice();
|
||||
dataSeg.position(i+1);
|
||||
dataSeg.limit(length);
|
||||
assertThat("Third "+i,parser.parse(dataSeg,true),is(true));
|
||||
|
||||
assertThat(handler.fields, Matchers.contains( "Content-Disposition: form-data; name=\"text\"","<<COMPLETE>>"
|
||||
, "Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""
|
||||
, "Content-Type: text/plain","<<COMPLETE>>"
|
||||
, "Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\""
|
||||
, "Content-Type: text/html","<<COMPLETE>>"
|
||||
, "Field1: value1", "Field2: value2", "Field3: value3"
|
||||
, "Field4: value4", "Field5: value5", "Field6: value6"
|
||||
, "Field7: value7", "Field8: value8", "Field9: value 9", "<<COMPLETE>>"
|
||||
, "Field1: value1","<<COMPLETE>>"));
|
||||
|
||||
|
||||
assertThat(handler.contentString(), is(new String("text default"+"<<LAST>>"
|
||||
+ "Content of a.txt.\n"+"<<LAST>>"
|
||||
+ "<!DOCTYPE html><title>Content of a.html.</title>\n"+"<<LAST>>"
|
||||
+ "<<LAST>>"
|
||||
+ "But the amount of denudation which the strata have\n" +
|
||||
"in many places suffered, independently of the rate\n" +
|
||||
"of accumulation of the degraded matter, probably\n" +
|
||||
"offers the best evidence of the lapse of time. I remember\n" +
|
||||
"having been much struck with the evidence of\n" +
|
||||
"denudation, when viewing volcanic islands, which\n" +
|
||||
"have been worn by the waves and pared all round\n" +
|
||||
"into perpendicular cliffs of one or two thousand feet\n" +
|
||||
"in height; for the gentle slope of the lava-streams,\n" +
|
||||
"due to their formerly liquid state, showed at a glance\n" +
|
||||
"how far the hard, rocky beds had once extended into\n" +
|
||||
"the open ocean.\n"+ "<<LAST>>")));
|
||||
|
||||
handler.clear();
|
||||
parser.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testGeneratedForm()
|
||||
{
|
||||
TestHandler handler = new TestHandler()
|
||||
{
|
||||
@Override
|
||||
public boolean messageComplete()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
super.content(buffer,last);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
MultiPartParser parser = new MultiPartParser(handler,"WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW");
|
||||
ByteBuffer data = BufferUtil.toBuffer(""
|
||||
+ "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
|
||||
"\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
|
||||
"Content-Disposition: form-data; name=\"part1\"\r\n" +
|
||||
"\n" +
|
||||
"wNfミxVamt\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
|
||||
"Content-Disposition: form-data; name=\"part2\"\r\n" +
|
||||
"\r\n" +
|
||||
"&ᄈ취ᅢO\r\n" +
|
||||
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--");
|
||||
|
||||
parser.parse(data,true);
|
||||
assertThat(parser.getState(), is(State.END));
|
||||
assertThat(handler.fields.size(), is(2));
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class TestHandler implements MultiPartParser.Handler
|
||||
{
|
||||
List<String> fields = new ArrayList<>();
|
||||
List<String> content = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void parsedField(String name, String value)
|
||||
{
|
||||
fields.add(name+": "+value);
|
||||
}
|
||||
|
||||
public String contentString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(String s : content) sb.append(s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean headerComplete()
|
||||
{
|
||||
fields.add("<<COMPLETE>>");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean content(ByteBuffer buffer, boolean last)
|
||||
{
|
||||
if (BufferUtil.hasContent(buffer))
|
||||
content.add(BufferUtil.toString(buffer));
|
||||
if (last)
|
||||
content.add("<<LAST>>");
|
||||
return last;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
fields.clear();
|
||||
content.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
|
||||
// ------------------------------------------------------------------------
|
||||
// All rights reserved. This program and the accompanying materials
|
||||
// are made available under the terms of the Eclipse Public License v1.0
|
||||
// and Apache License v2.0 which accompanies this distribution.
|
||||
//
|
||||
// The Eclipse Public License is available at
|
||||
// http://www.eclipse.org/legal/epl-v10.html
|
||||
//
|
||||
// The Apache License v2.0 is available at
|
||||
// http://www.opensource.org/licenses/apache2.0.php
|
||||
//
|
||||
// You may elect to redistribute this code under either of these licenses.
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.http.jmh;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import javax.servlet.MultipartConfigElement;
|
||||
import javax.servlet.http.Part;
|
||||
|
||||
import org.eclipse.jetty.http.MultiPartFormInputStream;
|
||||
import org.eclipse.jetty.http.MultiPartCaptureTest.MultipartExpectations;
|
||||
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
|
||||
import org.eclipse.jetty.toolchain.test.TestingDir;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.junit.Rule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Param;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.TearDown;
|
||||
import org.openjdk.jmh.profile.CompilerProfiler;
|
||||
import org.openjdk.jmh.runner.Runner;
|
||||
import org.openjdk.jmh.runner.RunnerException;
|
||||
import org.openjdk.jmh.runner.options.Options;
|
||||
import org.openjdk.jmh.runner.options.OptionsBuilder;
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
public class MultiPartBenchmark
|
||||
{
|
||||
|
||||
public static final int MAX_FILE_SIZE = Integer.MAX_VALUE;
|
||||
public static final int MAX_REQUEST_SIZE = Integer.MAX_VALUE;
|
||||
public static final int FILE_SIZE_THRESHOLD = 50;
|
||||
|
||||
public int count = 0;
|
||||
static String _contentType;
|
||||
static File _file;
|
||||
static int _numSections;
|
||||
static int _numBytesPerSection;
|
||||
|
||||
|
||||
public static List<String> data = new ArrayList<>();
|
||||
static
|
||||
{
|
||||
// Capture of raw request body contents from various browsers
|
||||
|
||||
// simple form - 2 fields
|
||||
data.add("browser-capture-form1-android-chrome");
|
||||
data.add("browser-capture-form1-android-firefox");
|
||||
data.add("browser-capture-form1-chrome");
|
||||
data.add("browser-capture-form1-edge");
|
||||
data.add("browser-capture-form1-firefox");
|
||||
data.add("browser-capture-form1-ios-safari");
|
||||
data.add("browser-capture-form1-msie");
|
||||
data.add("browser-capture-form1-osx-safari");
|
||||
|
||||
// form submitted as shift-jis
|
||||
data.add("browser-capture-sjis-form-edge");
|
||||
data.add("browser-capture-sjis-form-msie");
|
||||
|
||||
// form submitted as shift-jis (with HTML5 specific hidden _charset_ field)
|
||||
data.add("browser-capture-sjis-charset-form-edge");
|
||||
data.add("browser-capture-sjis-charset-form-msie");
|
||||
|
||||
// form submitted with simple file upload
|
||||
data.add("browser-capture-form-fileupload-android-chrome");
|
||||
data.add("browser-capture-form-fileupload-android-firefox");
|
||||
data.add("browser-capture-form-fileupload-chrome");
|
||||
data.add("browser-capture-form-fileupload-edge");
|
||||
data.add("browser-capture-form-fileupload-firefox");
|
||||
data.add("browser-capture-form-fileupload-ios-safari");
|
||||
data.add("browser-capture-form-fileupload-msie");
|
||||
data.add("browser-capture-form-fileupload-safari");
|
||||
|
||||
// form submitted with 2 files (1 binary, 1 text) and 2 text fields
|
||||
data.add("browser-capture-form-fileupload-alt-chrome");
|
||||
data.add("browser-capture-form-fileupload-alt-edge");
|
||||
data.add("browser-capture-form-fileupload-alt-firefox");
|
||||
data.add("browser-capture-form-fileupload-alt-msie");
|
||||
data.add("browser-capture-form-fileupload-alt-safari");
|
||||
}
|
||||
|
||||
|
||||
@Param({"UTIL","HTTP"})
|
||||
public static String parserType;
|
||||
|
||||
@Setup(Level.Trial)
|
||||
public static void setupTrial() throws Exception
|
||||
{
|
||||
_file = File.createTempFile("test01",null);
|
||||
_file.deleteOnExit();
|
||||
|
||||
_numSections = 1;
|
||||
_numBytesPerSection = 1024*1024*10;
|
||||
|
||||
_contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
|
||||
String initialBoundary = "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
|
||||
String boundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
|
||||
String closingBoundary = "\r\n--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--\r\n";
|
||||
String headerStart = "Content-Disposition: form-data; name=\"";
|
||||
|
||||
|
||||
for(int i=0; i<_numSections; i++) {
|
||||
//boundary and headers
|
||||
if(i==0)
|
||||
Files.write(_file.toPath(), initialBoundary.getBytes(), StandardOpenOption.APPEND);
|
||||
else
|
||||
Files.write(_file.toPath(), boundary.getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
Files.write(_file.toPath(), headerStart.getBytes(), StandardOpenOption.APPEND);
|
||||
Files.write(_file.toPath(), new String("part"+(i+1)).getBytes(), StandardOpenOption.APPEND);
|
||||
Files.write(_file.toPath(), new String("\"\r\n\r\n").getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
//append random data
|
||||
byte[] data = new byte[_numBytesPerSection];
|
||||
new Random().nextBytes(data);
|
||||
Files.write(_file.toPath(), data, StandardOpenOption.APPEND);
|
||||
}
|
||||
|
||||
//closing boundary
|
||||
Files.write(_file.toPath(), closingBoundary.getBytes(), StandardOpenOption.APPEND);
|
||||
|
||||
/*
|
||||
// print out file to verify that it contains valid contents (just for testing)
|
||||
InputStream in = Files.newInputStream(_file.toPath());
|
||||
System.out.println();
|
||||
while(in.available()>0) {
|
||||
byte b[] = new byte[100];
|
||||
int read = in.read(b,0,100);
|
||||
for(int i=0; i<read; i++)
|
||||
System.out.print((char)b[i]);
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
//exit
|
||||
throw new RuntimeException("Stop Here");
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode({Mode.AverageTime})
|
||||
@SuppressWarnings("deprecation")
|
||||
public long testLargeGenerated() throws Exception
|
||||
{
|
||||
Path multipartRawFile = _file.toPath();
|
||||
Path outputDir = new File("/tmp").toPath();
|
||||
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
switch(parserType)
|
||||
{
|
||||
case "HTTP":
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, _contentType, config, outputDir.toFile());
|
||||
if(parser.getParts().size() != _numSections)
|
||||
throw new IllegalStateException("Incorrect Parsing");
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "UTIL":
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, _contentType,config,outputDir.toFile());
|
||||
|
||||
// TODO this is using the http version of part (which should be the same anyway)
|
||||
if(parser.getParts().size() != _numSections)
|
||||
throw new IllegalStateException("Incorrect Parsing");
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown parserType Parameter");
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@TearDown(Level.Trial)
|
||||
public static void stopTrial() throws Exception
|
||||
{
|
||||
_file = null;
|
||||
}
|
||||
|
||||
private MultipartConfigElement newMultipartConfigElement(Path path)
|
||||
{
|
||||
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
@BenchmarkMode({Mode.AverageTime})
|
||||
@SuppressWarnings("deprecation")
|
||||
public long testParser() throws Exception
|
||||
{
|
||||
for(String multiPart : data)
|
||||
{
|
||||
Path multipartRawFile = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".raw");
|
||||
Path expectationPath = MavenTestingUtils.getTestResourcePathFile("multipart/" + multiPart + ".expected.txt");
|
||||
|
||||
Path outputDir = new File("/tmp").toPath();
|
||||
|
||||
MultipartExpectations multipartExpectations = new MultipartExpectations(expectationPath);
|
||||
MultipartConfigElement config = newMultipartConfigElement(outputDir);
|
||||
|
||||
try (InputStream in = Files.newInputStream(multipartRawFile))
|
||||
{
|
||||
switch(parserType)
|
||||
{
|
||||
case "HTTP":
|
||||
{
|
||||
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "UTIL":
|
||||
{
|
||||
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in,multipartExpectations.contentType,config,outputDir.toFile());
|
||||
|
||||
// TODO this is using the http version of part (which should be the same anyway)
|
||||
for(Part p : parser.getParts()) {
|
||||
count += p.getSize();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown parserType Parameter");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws RunnerException
|
||||
{
|
||||
Options opt = new OptionsBuilder()
|
||||
.include(MultiPartBenchmark.class.getSimpleName())
|
||||
.warmupIterations(20)
|
||||
.measurementIterations(10)
|
||||
.forks(1)
|
||||
.threads(1)
|
||||
// .syncIterations(true) // Don't start all threads at same time
|
||||
// .warmupTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
|
||||
// .measurementTime(new TimeValue(10000,TimeUnit.MILLISECONDS))
|
||||
// .addProfiler(CompilerProfiler.class)
|
||||
// .addProfiler(LinuxPerfProfiler.class)
|
||||
// .addProfiler(LinuxPerfNormProfiler.class)
|
||||
// .addProfiler(LinuxPerfAsmProfiler.class)
|
||||
// .resultFormat(ResultFormatType.CSV)
|
||||
.build();
|
||||
|
||||
new Runner(opt).run();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|248
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=DHbU6ChASebwm4iE8z9Lakv4ybMmkp
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|1
|
||||
Part-ContainsContents|company|bob+%26+frank%27s+shoe+repair
|
|
@ -0,0 +1,7 @@
|
|||
--DHbU6ChASebwm4iE8z9Lakv4ybMmkp
|
||||
Content-Disposition: form-data; name="company"
|
||||
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
bob+%26+frank%27s+shoe+repair
|
||||
--DHbU6ChASebwm4iE8z9Lakv4ybMmkp--
|
|
@ -0,0 +1,15 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|22940
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=owr6UQGvVNunA_sx2AsizBtyq_uK-OjsQXrF
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|6
|
||||
Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
|
||||
Part-ContainsContents|company|bob & frank's shoe repair
|
||||
Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
|
||||
Part-ContainsContents|japanese|オープンソース
|
||||
Part-ContainsContents|hello|日食桟橋
|
||||
Part-Filename|upload_file|filename
|
||||
Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
|
Binary file not shown.
|
@ -0,0 +1,15 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1275gffetpxz8o0q
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|6
|
||||
Part-ContainsContents|pi|3.14159265358979323846264338327950288419716939937510
|
||||
Part-ContainsContents|company|bob & frank's shoe repair
|
||||
Part-ContainsContents|power|ꬵо𝗋ⲥ𝖾
|
||||
Part-ContainsContents|japanese|オープンソース
|
||||
Part-ContainsContents|hello|日食桟橋
|
||||
Part-Filename|upload_file|filename
|
||||
Part-Sha1sum|upload_file|e75b73644afe9b234d70da9ff225229de68cdff8
|
Binary file not shown.
|
@ -0,0 +1,8 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|1815
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=QW3F8Fg64P2J2dpfEKGKlX0Q9QF2a8SK_7YH
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|10
|
Binary file not shown.
|
@ -0,0 +1,8 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary14beb4to333d91v8
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|10
|
|
@ -0,0 +1,51 @@
|
|||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14159265358979323846264338327950288419716939937510
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14159
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
%CF%80
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="pi"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
π = C/d
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="π"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="%CF%80"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
Approximately 3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8
|
||||
Content-Disposition: form-data; name="%FE%FF%03%C0"
|
||||
Content-Type: text/plain;charset=UTF-8
|
||||
|
||||
Approximately 3.14
|
||||
--JettyHttpClientBoundary14beb4to333d91v8--
|
|
@ -0,0 +1,11 @@
|
|||
Request-Header|Accept-Encoding|gzip,deflate
|
||||
Request-Header|Connection|keep-alive
|
||||
Request-Header|Content-Length|31148
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=qqr2YBBR31U4xVib4vaVuIsrwNY1iw
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|User-Agent|Apache-HttpClient/4.5.5 (Java/1.8.0_162)
|
||||
Request-Header|X-BrowserId|apache-httpcomp
|
||||
Parts-Count|169
|
||||
Part-ContainsContents|count|168
|
||||
Part-ContainsContents|persian-UTF-8|برج بابل
|
||||
Part-ContainsContents|persian-CESU-8|برج بابل
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
Request-Header|Accept-Encoding|gzip
|
||||
Request-Header|Connection|close
|
||||
Request-Header|Content-Type|multipart/form-data; boundary=JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Request-Header|Host|localhost:9090
|
||||
Request-Header|Transfer-Encoding|chunked
|
||||
Request-Header|User-Agent|Jetty/9.4.9.v20180320
|
||||
Request-Header|X-BrowserId|jetty-client
|
||||
Parts-Count|169
|
||||
Part-ContainsContents|count|168
|
||||
Part-ContainsContents|persian-UTF-8|برج بابل
|
||||
Part-ContainsContents|persian-CESU-8|برج بابل
|
|
@ -0,0 +1,846 @@
|
|||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Big5"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Big5-HKSCS"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-CESU-8"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-EUC-JP"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-EUC-KR"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GB18030"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD>1<EFBFBD>0<EFBFBD>1<EFBFBD>9<EFBFBD>1<EFBFBD>4 <20>1<EFBFBD>0<EFBFBD>1<EFBFBD>9<EFBFBD>1<EFBFBD>0<EFBFBD>1<EFBFBD>8
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GB2312"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-GBK"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM-Thai"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM00858"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01140"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01141"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01142"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01143"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01144"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01145"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01146"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01147"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01148"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM01149"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM037"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM1026"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM1047"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM273"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM277"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM278"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM280"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM284"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM285"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM290"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM297"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM420"
|
||||
Content-Type: text/plain
|
||||
|
||||
Xug@XVX<56>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM424"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM437"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM500"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM775"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM850"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM852"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM855"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM857"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM860"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM861"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM862"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM863"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM864"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM865"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM866"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM868"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM869"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM870"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM871"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-IBM918"
|
||||
Content-Type: text/plain
|
||||
|
||||
???@????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-JP"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-JP-2"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-2022-KR"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-1"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-13"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-15"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-2"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-3"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-4"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-5"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-6"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-7"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-8"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-ISO-8859-9"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-JIS_X0201"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-JIS_X0212-1990"
|
||||
Content-Type: text/plain
|
||||
|
||||
"D"D"D"D"D"D"D"D
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-KOI8-R"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-KOI8-U"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-Shift_JIS"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-TIS-620"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-US-ASCII"
|
||||
Content-Type: text/plain
|
||||
|
||||
??? ????
|
||||
--JettyHttpClientBoundary1jcfdl0zps9nf362
|
||||
Content-Disposition: form-data; name="persian-UTF-16"
|
||||
Content-Type: text/plain
|
||||
|
||||
<EFBFBD><EFBFBD>(1, |