Merge pull request #2476 from lachlan-roberts/jetty-9.4.x-1027-MultiPart-Cleanup

MultiPart Cleanup
This commit is contained in:
Greg Wilkins 2018-05-04 21:10:50 +10:00 committed by GitHub
commit c865aa1be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1268 additions and 1413 deletions

View File

@ -447,7 +447,7 @@ public class HttpParser
/* ------------------------------------------------------------------------------- */
enum CharState { ILLEGAL, CR, LF, LEGAL }
private final static CharState[] __charState;
public final static CharState[] TOKEN_CHAR;
static
{
// token = 1*tchar
@ -462,38 +462,38 @@ public class HttpParser
// 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;
TOKEN_CHAR =new CharState[256];
Arrays.fill(TOKEN_CHAR,CharState.ILLEGAL);
TOKEN_CHAR[LINE_FEED]=CharState.LF;
TOKEN_CHAR[CARRIAGE_RETURN]=CharState.CR;
TOKEN_CHAR[TAB]=CharState.LEGAL;
TOKEN_CHAR[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;
TOKEN_CHAR['!']=CharState.LEGAL;
TOKEN_CHAR['#']=CharState.LEGAL;
TOKEN_CHAR['$']=CharState.LEGAL;
TOKEN_CHAR['%']=CharState.LEGAL;
TOKEN_CHAR['&']=CharState.LEGAL;
TOKEN_CHAR['\'']=CharState.LEGAL;
TOKEN_CHAR['*']=CharState.LEGAL;
TOKEN_CHAR['+']=CharState.LEGAL;
TOKEN_CHAR['-']=CharState.LEGAL;
TOKEN_CHAR['.']=CharState.LEGAL;
TOKEN_CHAR['^']=CharState.LEGAL;
TOKEN_CHAR['_']=CharState.LEGAL;
TOKEN_CHAR['`']=CharState.LEGAL;
TOKEN_CHAR['|']=CharState.LEGAL;
TOKEN_CHAR['~']=CharState.LEGAL;
__charState['"']=CharState.LEGAL;
TOKEN_CHAR['"']=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);
TOKEN_CHAR['\\']=CharState.LEGAL;
TOKEN_CHAR['(']=CharState.LEGAL;
TOKEN_CHAR[')']=CharState.LEGAL;
Arrays.fill(TOKEN_CHAR,0x21,0x27+1,CharState.LEGAL);
Arrays.fill(TOKEN_CHAR,0x2A,0x5B+1,CharState.LEGAL);
Arrays.fill(TOKEN_CHAR,0x5D,0x7E+1,CharState.LEGAL);
Arrays.fill(TOKEN_CHAR,0x80,0xFF+1,CharState.LEGAL);
}
@ -502,7 +502,7 @@ public class HttpParser
{
byte ch = buffer.get();
CharState s = __charState[0xff & ch];
CharState s = TOKEN_CHAR[0xff & ch];
switch(s)
{
case ILLEGAL:

View File

@ -52,28 +52,27 @@ import org.eclipse.jetty.util.log.Logger;
/**
* MultiPartInputStream
*
* <p>
* 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;
private static final MultiMap<Part> EMPTY_MAP = new MultiMap<>(Collections.emptyMap());
private InputStream _in;
private MultipartConfigElement _config;
private String _contentType;
private MultiMap<Part> _parts;
private Throwable _err;
private File _tmpDir;
private File _contextTmpDir;
private boolean _deleteOnExit;
private boolean _writeFilesWithFilenames;
private boolean _parsed;
private int _bufferSize = 16 * 1024;
public class MultiPart implements Part
{
protected String _name;
@ -85,24 +84,24 @@ public class MultiPartFormInputStream
protected MultiMap<String> _headers;
protected long _size = 0;
protected boolean _temporary = true;
public MultiPart(String name, String filename) throws IOException
public MultiPart(String name, String filename)
{
_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);
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
@ -119,38 +118,38 @@ public class MultiPartFormInputStream
_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);
_out.write(bytes, offset, length);
_size += length;
}
protected void createFile() throws IOException
{
/*
@ -158,16 +157,16 @@ public class MultiPartFormInputStream
*/
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
_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
@ -178,53 +177,38 @@ public class MultiPartFormInputStream
_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);
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
{
@ -236,68 +220,52 @@ public class MultiPartFormInputStream
else
{
// part content is in memory
return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
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
_file = new File(_tmpDir, fileName);
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(_file)))
{
bos = new BufferedOutputStream(new FileOutputStream(_file));
_bout.writeTo(bos);
bos.flush();
}
finally
{
if (bos != null)
bos.close();
_bout = null;
}
}
@ -305,51 +273,50 @@ public class MultiPartFormInputStream
{
// 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);
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();
if (!_file.delete())
throw new IOException("Could Not Delete File");
}
/**
* Only remove tmp files.
*
* @throws IOException
* if unable to delete the file
* @throws IOException if unable to delete the file
*/
public void cleanUp() throws IOException
{
if (_temporary && _file != null && _file.exists())
_file.delete();
if (!_file.delete())
throw new IOException("Could Not Delete File");
}
/**
* 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()
@ -357,16 +324,12 @@ public class MultiPartFormInputStream
return _filename;
}
}
/**
* @param in
* Request input stream
* @param contentType
* Content-Type header
* @param config
* MultipartConfigElement
* @param contextTmpDir
* javax.servlet.context.tempdir
* @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)
{
@ -375,10 +338,10 @@ public class MultiPartFormInputStream
_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())
@ -390,7 +353,7 @@ public class MultiPartFormInputStream
}
_in = new BufferedInputStream(in);
}
/**
* @return whether the list of parsed parts is empty
*/
@ -402,16 +365,16 @@ public class MultiPartFormInputStream
Collection<List<Part>> values = _parts.values();
for (List<Part> partList : values)
{
if(partList.size() != 0)
if (partList.size() != 0)
return false;
}
return true;
}
/**
* Get the already parsed parts.
*
*
* @return the parts that were parsed
*/
@Deprecated
@ -419,25 +382,25 @@ public class MultiPartFormInputStream
{
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);
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;
return;
Collection<Part> parts;
try
{
@ -447,8 +410,8 @@ public class MultiPartFormInputStream
{
throw new RuntimeException(e);
}
MultiException err = new MultiException();
MultiException err = null;
for (Part p : parts)
{
try
@ -457,59 +420,58 @@ public class MultiPartFormInputStream
}
catch (Exception e)
{
if (err == null)
err = new MultiException();
err.add(e);
}
}
_parts.clear();
err.ifExceptionThrowRuntime();
if (err != null)
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
* @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);
List<Part> asList = LazyList.getList(o, false);
parts.addAll(asList);
}
return parts;
}
/**
* Get the named Part.
*
* @param name
* the part name
* @param name the part name
* @return the parts
* @throws IOException
* if unable to get the part
* @throws IOException if unable to get the part
*/
public Part getPart(String name) throws IOException
{
if(!_parsed)
if (!_parsed)
parse();
throwIfError();
return _parts.getValue(name,0);
return _parts.getValue(name, 0);
}
/**
* Throws an exception if one has been latched.
*
* @throws IOException
* the exception (if present)
*
* @throws IOException the exception (if present)
*/
protected void throwIfError() throws IOException
{
@ -523,10 +485,9 @@ public class MultiPartFormInputStream
throw new IllegalStateException(_err);
}
}
/**
* Parse, if necessary, the multipart stream.
*
*/
protected void parse()
{
@ -537,14 +498,13 @@ public class MultiPartFormInputStream
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;
@ -556,70 +516,67 @@ public class MultiPartFormInputStream
if (f.isAbsolute())
_tmpDir = f;
else
_tmpDir = new File(_contextTmpDir,_config.getLocation());
_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());
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 //
MultiPartParser parser = new MultiPartParser(handler, contentTypeBoundary);
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
*/
int len;
long total = 0;
while (true)
{
len = _in.read(data);
if (len > 0)
{
// keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
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))
if (parser.parse(buffer, false))
break;
if(buffer.hasRemaining())
if (buffer.hasRemaining())
throw new IllegalStateException("Buffer did not fully consume");
}
else if (len == -1)
{
parser.parse(BufferUtil.EMPTY_BUFFER,true);
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)
{
@ -628,39 +585,37 @@ public class MultiPartFormInputStream
else
_err = new IOException("Incomplete Multipart");
}
if (LOG.isDebugEnabled())
{
LOG.debug("Parsing Complete {} err={}",parser,_err);
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);
headers.put(StringUtil.asciiToLowerCase(key), value);
if (key.equalsIgnoreCase("content-disposition"))
contentDisposition = value;
else if (key.equalsIgnoreCase("content-type"))
@ -668,27 +623,27 @@ public class MultiPartFormInputStream
// 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())
if (LOG.isDebugEnabled())
{
LOG.debug("headerComplete {}",this);
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);
QuotedStringTokenizer tok = new QuotedStringTokenizer(contentDisposition, ";", false, true);
String name = null;
String filename = null;
while (tok.hasMoreTokens())
@ -702,7 +657,7 @@ public class MultiPartFormInputStream
else if (tl.startsWith("filename="))
filename = filenameValue(t);
}
// Check disposition
if (!form_data)
throw new IOException("Part not form-data");
@ -714,13 +669,13 @@ public class MultiPartFormInputStream
// 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 = new MultiPart(name, filename);
_part.setHeaders(headers);
_part.setContentType(contentType);
_parts.add(name,_part);
_parts.add(name, _part);
try
{
@ -737,21 +692,21 @@ public class MultiPartFormInputStream
_err = e;
return true;
}
return false;
}
@Override
public boolean content(ByteBuffer buffer, boolean last)
{
if(_part == null)
{
if (_part == null)
return false;
if (BufferUtil.hasContent(buffer))
{
try
{
_part.write(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
_part.write(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
}
catch (IOException e)
{
@ -759,7 +714,7 @@ public class MultiPartFormInputStream
return true;
}
}
if (last)
{
try
@ -772,12 +727,12 @@ public class MultiPartFormInputStream
return true;
}
}
return false;
}
@Override
public void startPart()
public void startPart()
{
reset();
}
@ -786,9 +741,9 @@ public class MultiPartFormInputStream
public void earlyEOF()
{
if (LOG.isDebugEnabled())
LOG.debug("Early EOF {}",MultiPartFormInputStream.this);
LOG.debug("Early EOF {}", MultiPartFormInputStream.this);
}
public void reset()
{
_part = null;
@ -797,41 +752,41 @@ public class MultiPartFormInputStream
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)
private static String value(String nameEqualsValue)
{
int idx = nameEqualsValue.indexOf('=');
String value = nameEqualsValue.substring(idx + 1).trim();
return QuotedStringTokenizer.unquoteOnly(value);
}
/* ------------------------------------------------------------ */
private String filenameValue(String nameEqualsValue)
private static 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
@ -841,8 +796,8 @@ public class MultiPartFormInputStream
value = value.substring(1);
char last = value.charAt(value.length() - 1);
if (last == '"' || last == '\'')
value = value.substring(0,value.length() - 1);
value = value.substring(0, value.length() - 1);
return value;
}
else
@ -850,7 +805,22 @@ public class MultiPartFormInputStream
// 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);
return QuotedStringTokenizer.unquoteOnly(value, true);
}
/**
* @return the size of buffer used to read data from the input stream
*/
public int getBufferSize()
{
return _bufferSize;
}
/**
* @param bufferSize the size of buffer used to read data from the input stream
*/
public void setBufferSize(int bufferSize)
{
_bufferSize = bufferSize;
}
}

View File

@ -20,7 +20,6 @@ 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;
@ -31,202 +30,146 @@ import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/* ------------------------------------------------------------ */
/** A parser for MultiPart content type.
*
/**
* 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)';';
private static final byte COLON = (byte)':';
private static final byte TAB = 0x09;
private static final byte LINE_FEED = 0x0A;
private static final byte CARRIAGE_RETURN = 0x0D;
private static final byte SPACE = 0x20;
// States
public enum FieldState
{
FIELD,
IN_NAME,
AFTER_NAME,
AFTER_NAME,
VALUE,
IN_VALUE
}
// States
public enum State
{
PREAMBLE,
DELIMITER,
DELIMITER_PADDING,
DELIMITER_PADDING,
DELIMITER_CLOSE,
BODY_PART,
FIRST_OCTETS,
FIRST_OCTETS,
OCTETS,
EPILOGUE,
EPILOGUE,
END
}
private final static EnumSet<State> __delimiterStates = EnumSet.of(State.DELIMITER,State.DELIMITER_CLOSE,State.DELIMITER_PADDING);
private final static EnumSet<State> __delimiterStates = EnumSet.of(State.DELIMITER, State.DELIMITER_CLOSE, State.DELIMITER_PADDING);
private final static int MAX_HEADER_LINE_LENGTH = 998;
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)
private static boolean hasNextByte(ByteBuffer buffer)
{
return BufferUtil.hasContent(buffer);
}
/* ------------------------------------------------------------------------------- */
private byte getNextByte(ByteBuffer buffer)
{
byte ch = buffer.get();
CharState s = __charState[0xff & ch];
HttpParser.CharState s = HttpParser.TOKEN_CHAR[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);
throw new IllegalCharacterException(_state, ch, buffer);
}
}
/* ------------------------------------------------------------------------------- */
private void setString(String s)
{
@ -234,7 +177,7 @@ public class MultiPartParser
_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
@ -243,91 +186,92 @@ public class MultiPartParser
{
String s = _string.toString();
// trim trailing whitespace.
if (s.length()>_length)
s = s.substring(0,_length);
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
* @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))
while (!handle && 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())
if (LOG.isDebugEnabled())
LOG.debug("messageComplete {}", this);
return _handler.messageComplete();
}
else
{
if(LOG.isDebugEnabled())
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);
int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
if (partial > 0)
{
if (partial == _delimiterSearch.getLength())
@ -337,29 +281,27 @@ public class MultiPartParser
setState(State.DELIMITER);
return;
}
_partialBoundary = partial;
BufferUtil.clear(buffer);
return;
}
_partialBoundary = 0;
}
int delimiter = _delimiterSearch.match(buffer.array(),buffer.arrayOffset() + buffer.position(),buffer.remaining());
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());
_partialBoundary = _delimiterSearch.endsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
BufferUtil.clear(buffer);
return;
}
/* ------------------------------------------------------------------------------- */
private void parseDelimiter(ByteBuffer buffer)
{
@ -368,18 +310,18 @@ public class MultiPartParser
byte b = getNextByte(buffer);
if (b == 0)
return;
if (b == '\n')
{
setState(State.BODY_PART);
if(LOG.isDebugEnabled())
LOG.debug("startPart {}",this);
if (LOG.isDebugEnabled())
LOG.debug("startPart {}", this);
_handler.startPart();
return;
}
switch (_state)
{
case DELIMITER:
@ -388,7 +330,7 @@ public class MultiPartParser
else
setState(State.DELIMITER_PADDING);
continue;
case DELIMITER_CLOSE:
if (b == '-')
{
@ -397,14 +339,13 @@ public class MultiPartParser
}
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
@ -418,13 +359,13 @@ public class MultiPartParser
byte b = getNextByte(buffer);
if (b == 0)
break;
if (b != LINE_FEED)
_totalHeaderLineLength++;
if (_totalHeaderLineLength > _maxHeaderLineLength)
if (_totalHeaderLineLength > MAX_HEADER_LINE_LENGTH)
throw new IllegalStateException("Header Line Exceeded Max Length");
switch (_fieldState)
{
case FIELD:
@ -434,10 +375,10 @@ public class MultiPartParser
case TAB:
{
// Folded field value!
if (_fieldName == null)
throw new IllegalStateException("First field folded");
if (_fieldValue == null)
{
_string.reset();
@ -453,26 +394,26 @@ public class MultiPartParser
setState(FieldState.VALUE);
break;
}
case LINE_FEED:
{
handleField();
setState(State.FIRST_OCTETS);
_partialBoundary = 2; // CRLF is option for empty parts
if(LOG.isDebugEnabled())
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();
@ -481,7 +422,7 @@ public class MultiPartParser
}
}
break;
case IN_NAME:
switch (b)
{
@ -490,29 +431,29 @@ public class MultiPartParser
_length = -1;
setState(FieldState.VALUE);
break;
case SPACE:
// Ignore trailing whitespaces
setState(FieldState.AFTER_NAME);
break;
case LINE_FEED:
{
if(LOG.isDebugEnabled())
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)
{
@ -521,22 +462,22 @@ public class MultiPartParser
_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);
throw new IllegalCharacterException(_state, b, buffer);
}
break;
case VALUE:
switch (b)
{
@ -544,14 +485,14 @@ public class MultiPartParser
_string.reset();
_fieldValue = "";
_length = -1;
setState(FieldState.FIELD);
break;
case SPACE:
case TAB:
break;
default:
_string.append(b);
_length = _string.length();
@ -559,14 +500,14 @@ public class MultiPartParser
break;
}
break;
case IN_VALUE:
switch (b)
{
case SPACE:
_string.append(b);
break;
case LINE_FEED:
if (_length > 0)
{
@ -576,7 +517,7 @@ public class MultiPartParser
}
setState(FieldState.FIELD);
break;
default:
_string.append(b);
if (b > SPACE || b < 0)
@ -584,35 +525,35 @@ public class MultiPartParser
break;
}
break;
default:
throw new IllegalStateException(_state.toString());
}
}
return false;
}
/* ------------------------------------------------------------------------------- */
private void handleField()
{
if(LOG.isDebugEnabled())
if (LOG.isDebugEnabled())
LOG.debug("parsedField: _fieldName={} _fieldValue={} {}", _fieldName, _fieldValue, this);
if (_fieldName != null && _fieldValue != null)
_handler.parsedField(_fieldName,_fieldValue);
_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);
int partial = _delimiterSearch.startsWith(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), _partialBoundary);
if (partial > 0)
{
if (partial == _delimiterSearch.getLength())
@ -621,12 +562,12 @@ public class MultiPartParser
setState(State.DELIMITER);
_partialBoundary = 0;
if(LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER),true,this);
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(BufferUtil.EMPTY_BUFFER), true, this);
return _handler.content(BufferUtil.EMPTY_BUFFER,true);
return _handler.content(BufferUtil.EMPTY_BUFFER, true);
}
_partialBoundary = partial;
BufferUtil.clear(buffer);
return false;
@ -642,78 +583,78 @@ public class MultiPartParser
}
content.limit(_partialBoundary);
_partialBoundary = 0;
if(LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}",BufferUtil.toDetailString(content),false,this);
if (_handler.content(content,false))
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());
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);
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());
_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);
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
BufferUtil.clear(buffer);
return _handler.content(content,false);
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);
if (LOG.isDebugEnabled())
LOG.debug("Content={}, Last={} {}", BufferUtil.toDetailString(content), false, this);
BufferUtil.clear(buffer);
return _handler.content(content,false);
return _handler.content(content, false);
}
/* ------------------------------------------------------------------------------- */
private void setState(State state)
{
if (DEBUG)
LOG.debug("{} --> {}",_state,state);
LOG.debug("{} --> {}", _state, state);
_state = state;
}
/* ------------------------------------------------------------------------------- */
private void setState(FieldState state)
{
if (DEBUG)
LOG.debug("{}:{} --> {}",_state,_fieldState,state);
LOG.debug("{}:{} --> {}", _state, _fieldState, state);
_fieldState = state;
}
/* ------------------------------------------------------------------------------- */
@Override
public String toString()
{
return String.format("%s{s=%s}",getClass().getSimpleName(),_state);
return String.format("%s{s=%s}", getClass().getSimpleName(), _state);
}
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
/* ------------------------------------------------------------ */
@ -724,43 +665,45 @@ public class MultiPartParser
*/
public interface Handler
{
public default void startPart()
default void startPart()
{
}
public default void parsedField(String name, String value)
@SuppressWarnings("unused")
default void parsedField(String name, String value)
{
}
public default boolean headerComplete()
default boolean headerComplete()
{
return false;
}
public default boolean content(ByteBuffer item, boolean last)
@SuppressWarnings("unused")
default boolean content(ByteBuffer item, boolean last)
{
return false;
}
public default boolean messageComplete()
default boolean messageComplete()
{
return false;
}
public default void earlyEOF()
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));
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)));
LOG.warn(String.format("Illegal character 0x%X in state=%s for buffer %s", ch, state, BufferUtil.toDetailString(buffer)));
}
}
}

View File

@ -35,119 +35,127 @@ import org.junit.Test;
public class MultiPartParserTest
{
@Test
public void testEmptyPreamble()
{
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
{
}, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
parser.parse(data,false);
assertThat(parser.getState(),is(State.PREAMBLE));
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("");
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
{
}, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY \r\n");
data = BufferUtil.toBuffer("--BOUNDARY \r\n");
parser.parse(data,false);
parser.parse(data, false);
assertTrue(parser.isState(State.BODY_PART));
assertThat(data.remaining(),is(0));
assertThat(data.remaining(), is(0));
}
@Test
public void testPreamble()
{
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler(){},"BOUNDARY");
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));
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));
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));
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));
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));
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;
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
{
}, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("This is not part of a part\r\n--BOUNDARY \r\n");
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));
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));
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
{
}, "BOUNDARY");
ByteBuffer 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));
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));
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));
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));
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));
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));
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("");
MultiPartParser parser = new MultiPartParser(new MultiPartParser.Handler()
{
}, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n\r\n");
data = BufferUtil.toBuffer("--BOUNDARY\r\n\r\n");
parser.parse(data,false);
assertThat(parser.getState(),is(State.FIRST_OCTETS));
assertThat(data.remaining(),is(0));
parser.parse(data, false);
assertThat(parser.getState(), is(State.FIRST_OCTETS));
assertThat(data.remaining(), is(0));
}
@Test
@ -162,149 +170,136 @@ public class MultiPartParserTest
return true;
}
};
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer 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>>"));
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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n"
+ "name: value\r\n"
+ "\r\n"
+ "\r\n"
+ "--BOUNDARY");
parser.parse(data,false);
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>>"));
}
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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n"
+ "name: value\r\n"
+ "\r\n"
+ "--BOUNDARY");
parser.parse(data,false);
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>>"));
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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n"
+ "name: value\r\n"
+ "\r\n"
+ "-");
parser.parse(data,false);
parser.parse(data, false);
data = BufferUtil.toBuffer("Content!");
parser.parse(data,false);
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!"));
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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer 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"));
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\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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer data = BufferUtil.toBuffer("--BOUNDARY\r\n"
+ "name: value\n"
+ "\r\n"
+ "Hello\r\n"
+ "--BOUNDARY");
parser.parse(data,false);
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>>"));
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");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer("");
data = BufferUtil.toBuffer("--BOUNDARY\r\n"
ByteBuffer 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"
@ -312,25 +307,23 @@ public class MultiPartParserTest
+ "The quick brown fox jumped over the lazy dog.\r\n"
+ "\r\n"
+ "--BOUNDARY");
parser.parse(data,false);
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"
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>>"));
+ "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("");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
//boundary still requires carriage return
data = BufferUtil.toBuffer("--BOUNDARY\n"
ByteBuffer 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"
@ -338,15 +331,15 @@ public class MultiPartParserTest
+ "The quick brown fox jumped over the lazy dog.\n"
+ "\r\n"
+ "--BOUNDARY");
parser.parse(data,false);
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"
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>>"));
+ "The quick brown fox jumped over the lazy dog.\n", "<<LAST>>"));
}
@Test
public void testBinaryPart()
@ -361,30 +354,31 @@ public class MultiPartParserTest
@Override
public boolean content(ByteBuffer buffer, boolean last)
{
BufferUtil.append(bytes,buffer);
BufferUtil.append(bytes, buffer);
return last;
}
};
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
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));
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);
parser.parse(data, true);
assertThat(parser.getState(), is(State.DELIMITER));
assertThat(data.remaining(),is(19));
assertThat(bytes.array(),is(random));
assertThat(data.remaining(), is(19));
assertThat(bytes.array(), is(random));
}
@Test
public void testEpilogue() {
public void testEpilogue()
{
TestHandler handler = new TestHandler();
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer(""
+ "--BOUNDARY\r\n"
@ -399,20 +393,21 @@ public class MultiPartParserTest
+ "--BOUNDARY");
parser.parse(data,false);
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>>"));
assertThat(handler.fields, Matchers.contains("name: value", "<<COMPLETE>>"));
assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>"));
parser.parse(data,true);
parser.parse(data, true);
assertThat(parser.getState(), is(State.END));
}
@Test
public void testMultipleContent() {
public void testMultipleContent()
{
TestHandler handler = new TestHandler();
MultiPartParser parser = new MultiPartParser(handler,"BOUNDARY");
MultiPartParser parser = new MultiPartParser(handler, "BOUNDARY");
ByteBuffer data = BufferUtil.toBuffer(""
+ "--BOUNDARY\r\n"
@ -430,211 +425,213 @@ public class MultiPartParserTest
+ "epilogue here");
/* Test First Content Section */
parser.parse(data,false);
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>>"));
assertThat(handler.content, Matchers.contains("Hello", "<<LAST>>"));
/* Test Second Content Section */
parser.parse(data,false);
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>>"));
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);
parser.parse(data, true);
assertThat(parser.getState(), is(State.END));
assertThat(data.remaining(),is(0));
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()
public void testCrAsLineTermination()
{
TestHandler handler = new TestHandler()
TestHandler handler = new TestHandler()
{
@Override
public boolean messageComplete()
{
return true;
}
@Override
public boolean content(ByteBuffer buffer, boolean last)
{
super.content(buffer,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" +
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" +
"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" +
"-----------------------------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();
}
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("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()
public void testGeneratedForm()
{
TestHandler handler = new TestHandler()
TestHandler handler = new TestHandler()
{
@Override
public boolean messageComplete()
{
return true;
}
@Override
public boolean content(ByteBuffer buffer, boolean last)
{
super.content(buffer,last);
super.content(buffer, last);
return false;
}
@ -645,24 +642,24 @@ public class MultiPartParserTest
}
};
MultiPartParser parser = new MultiPartParser(handler,"WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW");
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ミxVam﾿t\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
"Content-Disposition: form-data; name=\"part2\"\r\n" +
"\r\n" +
"&ᄈᄎ￙ᅱᅢO\r\n" +
+ "Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"part1\"\r\n" +
"\n" +
"wNfミxVam﾿t\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\n" +
"Content-Disposition: form-data; name=\"part2\"\r\n" +
"\r\n" +
"&ᄈᄎ￙ᅱᅢO\r\n" +
"--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW--");
parser.parse(data,true);
parser.parse(data, true);
assertThat(parser.getState(), is(State.END));
assertThat(handler.fields.size(), is(2));
}
@ -670,27 +667,28 @@ public class MultiPartParserTest
{
List<String> fields = new ArrayList<>();
List<String> content = new ArrayList<>();
@Override
public void parsedField(String name, String value)
{
fields.add(name+": "+value);
fields.add(name + ": " + value);
}
public String contentString()
{
StringBuilder sb = new StringBuilder();
for(String s : content) sb.append(s);
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)
{
@ -701,11 +699,10 @@ public class MultiPartParserTest
return last;
}
public void clear() {
public void clear()
{
fields.clear();
content.clear();
}
}
}

View File

@ -20,7 +20,6 @@ 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;
@ -34,11 +33,6 @@ 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;
@ -48,14 +42,13 @@ 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 class MultiPartBenchmark
{
public static final int MAX_FILE_SIZE = Integer.MAX_VALUE;
@ -67,13 +60,14 @@ public class MultiPartBenchmark
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");
@ -83,15 +77,15 @@ public class MultiPartBenchmark
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-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");
@ -101,7 +95,7 @@ public class MultiPartBenchmark
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");
@ -109,19 +103,19 @@ public class MultiPartBenchmark
data.add("browser-capture-form-fileupload-alt-msie");
data.add("browser-capture-form-fileupload-alt-safari");
}
@Param({"UTIL","HTTP"})
@Param({"UTIL", "HTTP"})
public static String parserType;
@Setup(Level.Trial)
public static void setupTrial() throws Exception
{
_file = File.createTempFile("test01",null);
_file = File.createTempFile("test01", null);
_file.deleteOnExit();
_numSections = 1;
_numBytesPerSection = 1024*1024*10;
_numBytesPerSection = 1024 * 1024 * 10;
_contentType = "multipart/form-data, boundary=WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW";
String initialBoundary = "--WebKitFormBoundary7MA4YWf7OaKlSxkTrZu0gW\r\n";
@ -130,17 +124,18 @@ public class MultiPartBenchmark
String headerStart = "Content-Disposition: form-data; name=\"";
for(int i=0; i<_numSections; i++) {
for (int i = 0; i < _numSections; i++)
{
//boundary and headers
if(i==0)
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);
Files.write(_file.toPath(), ("part" + (i + 1)).getBytes(), StandardOpenOption.APPEND);
Files.write(_file.toPath(), ("\"\r\n\r\n").getBytes(), StandardOpenOption.APPEND);
//append random data
byte[] data = new byte[_numBytesPerSection];
new Random().nextBytes(data);
@ -149,25 +144,9 @@ public class MultiPartBenchmark
//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")
@ -175,44 +154,44 @@ public class MultiPartBenchmark
{
Path multipartRawFile = _file.toPath();
Path outputDir = new File("/tmp").toPath();
MultipartConfigElement config = newMultipartConfigElement(outputDir);
try (InputStream in = Files.newInputStream(multipartRawFile))
{
switch(parserType)
switch (parserType)
{
case "HTTP":
{
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, _contentType, config, outputDir.toFile());
if(parser.getParts().size() != _numSections)
if (parser.getParts().size() != _numSections)
throw new IllegalStateException("Incorrect Parsing");
for(Part p : parser.getParts()) {
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)
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, _contentType, config, outputDir.toFile());
if (parser.getParts().size() != _numSections)
throw new IllegalStateException("Incorrect Parsing");
for(Part p : parser.getParts()) {
for (Part p : parser.getParts())
{
count += p.getSize();
}
}
break;
default:
throw new IllegalStateException("Unknown parserType Parameter");
}
}
return count;
}
@TearDown(Level.Trial)
@ -220,7 +199,7 @@ public class MultiPartBenchmark
{
_file = null;
}
private MultipartConfigElement newMultipartConfigElement(Path path)
{
return new MultipartConfigElement(path.toString(), MAX_FILE_SIZE, MAX_REQUEST_SIZE, FILE_SIZE_THRESHOLD);
@ -231,48 +210,48 @@ public class MultiPartBenchmark
@SuppressWarnings("deprecation")
public long testParser() throws Exception
{
for(String multiPart : data)
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)
switch (parserType)
{
case "HTTP":
{
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
for (Part p : parser.getParts())
{
MultiPartFormInputStream parser = new MultiPartFormInputStream(in, multipartExpectations.contentType, config, outputDir.toFile());
for(Part p : parser.getParts()) {
count += p.getSize();
}
count += p.getSize();
}
break;
}
break;
case "UTIL":
{
org.eclipse.jetty.util.MultiPartInputStreamParser parser = new org.eclipse.jetty.util.MultiPartInputStreamParser(in, multipartExpectations.contentType, config, outputDir.toFile());
for (Part p : parser.getParts())
{
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();
}
count += p.getSize();
}
break;
}
break;
default:
throw new IllegalStateException("Unknown parserType Parameter");
}
}
}
return count;
}
public static void main(String[] args) throws RunnerException
public static void main(String[] args) throws RunnerException
{
Options opt = new OptionsBuilder()
.include(MultiPartBenchmark.class.getSimpleName())
@ -280,14 +259,6 @@ public class MultiPartBenchmark
.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();