diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartInputStreamParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartInputStreamParser.java index 857909f6bd4..1cbde8dda3e 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartInputStreamParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartInputStreamParser.java @@ -21,16 +21,13 @@ package org.eclipse.jetty.http; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -39,14 +36,11 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import javax.servlet.MultipartConfigElement; import javax.servlet.ServletInputStream; import javax.servlet.http.Part; -import org.eclipse.jetty.http.HttpGenerator.State; -import org.eclipse.jetty.util.B64Code; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ByteArrayOutputStream2; import org.eclipse.jetty.util.LazyList; @@ -67,13 +61,14 @@ import org.eclipse.jetty.util.log.Logger; public class MultiPartInputStreamParser { private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.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 EMPTY_MAP = new MultiMap<>(Collections.emptyMap()); protected InputStream _in; protected MultipartConfigElement _config; protected String _contentType; protected MultiMap _parts; - protected Exception _err; + protected Throwable _err; protected File _tmpDir; protected File _contextTmpDir; protected boolean _deleteOnExit; @@ -185,8 +180,8 @@ public class MultiPartInputStreamParser _out.flush(); _bout.writeTo(bos); _out.close(); - _bout = null; } + _bout = null; _out = bos; } @@ -515,16 +510,17 @@ public class MultiPartInputStreamParser //have we already parsed the input? if (_parts != null || _err != null) return; + try + { + + //initialize + _parts = new MultiMap<>(); + + //if its not a multipart request, don't parse it + if (_contentType == null || !_contentType.startsWith("multipart/form-data")) + return; - //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; @@ -551,31 +547,33 @@ public class MultiPartInputStreamParser 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 // - final int _bufferSize = 16*1024; // <---- need to rename and move somewhere else 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) { - try - { - len = _in.read(data); - } - catch (IOException e) - { - _err = e; - return; - } - + + 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); parser.parse(buffer, false); @@ -585,33 +583,43 @@ public class MultiPartInputStreamParser 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) { - _err = new IOException("Incomplete Multipart"); + 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 { - /* keep running total of size of bytes read from input - * and throw an exception if exceeds MultipartConfigElement._maxRequestSize */ - private long total = 0; - + private MultiPart _part=null; private String contentDisposition=null; private String contentType=null; @@ -783,49 +791,4 @@ public class MultiPartInputStreamParser } - - private static class Base64InputStream extends InputStream - { - ReadLineInputStream _in; - String _line; - byte[] _buffer; - int _pos; - - - public Base64InputStream(ReadLineInputStream rlis) - { - _in = rlis; - } - - @Override - public int read() throws IOException - { - if (_buffer==null || _pos>= _buffer.length) - { - //Any CR and LF will be consumed by the readLine() call. - //We need to put them back into the bytes returned from this - //method because the parsing of the multipart content uses them - //as markers to determine when we've reached the end of a part. - _line = _in.readLine(); - if (_line==null) - return -1; //nothing left - if (_line.startsWith("--")) - _buffer=(_line+"\r\n").getBytes(); //boundary marking end of part - else if (_line.length()==0) - _buffer="\r\n".getBytes(); //blank line - else - { - ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2); - B64Code.decode(_line, baos); - baos.write(13); - baos.write(10); - _buffer = baos.toByteArray(); - } - - _pos=0; - } - - return _buffer[_pos++]; - } - } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java index 851054ea789..b6a74c6b70b 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPartParser.java @@ -152,7 +152,6 @@ public class MultiPartParser private final boolean DEBUG=LOG.isDebugEnabled(); private final Handler _handler; private final SearchPattern _delimiterSearch; - private final boolean _acceptCrAsLineTermination; private String _fieldName; private String _fieldValue; @@ -161,34 +160,22 @@ public class MultiPartParser private FieldState _fieldState = FieldState.FIELD; private int _partialBoundary = 2; // No CRLF if no preamble private boolean _cr; - private byte _next; private ByteBuffer _patternBuffer; private final StringBuilder _string=new StringBuilder(); private int _length; + private int _totalHeaderLineLength = -1; + private int _maxHeaderLineLength = 998; /* ------------------------------------------------------------------------------- */ public MultiPartParser(Handler handler, String boundary) - { - this(handler,boundary,false); - } - - /* ------------------------------------------------------------------------------- */ - /** TODO complete - * - * @param handler - * @param boundary - * @param acceptCR - */ - public MultiPartParser(Handler handler, String boundary, boolean acceptCR) { _handler = handler; String delimiter = "\r\n--"+boundary; _patternBuffer = ByteBuffer.wrap(delimiter.getBytes(StandardCharsets.US_ASCII)); _delimiterSearch = SearchPattern.compile(_patternBuffer.array()); - _acceptCrAsLineTermination = acceptCR; } public void reset() @@ -272,21 +259,14 @@ public class MultiPartParser /* ------------------------------------------------------------------------------- */ private boolean hasNextByte(ByteBuffer buffer) { - return BufferUtil.hasContent(buffer) || _next!=0; + return BufferUtil.hasContent(buffer); } /* ------------------------------------------------------------------------------- */ - private byte getNextByte(ByteBuffer buffer, boolean last) + private byte getNextByte(ByteBuffer buffer) { - byte ch; - if (_next==0) - ch = buffer.get(); - else - { - ch = _next; - _next = 0; - } + byte ch = buffer.get(); CharState s = __charState[0xff & ch]; switch(s) @@ -297,18 +277,11 @@ public class MultiPartParser case CR: if (_cr) - { - if (!_acceptCrAsLineTermination) - throw new BadMessageException("Bad EOL"); - // TODO log compliance violation - return (byte)'\n'; - } + throw new BadMessageException("Bad EOL"); _cr=true; if (buffer.hasRemaining()) - return getNextByte(buffer, last); - else if (_acceptCrAsLineTermination && last) - return '\n'; + 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 @@ -316,14 +289,8 @@ public class MultiPartParser case LEGAL: if (_cr) - { - if (!_acceptCrAsLineTermination) - throw new BadMessageException("Bad EOL"); - // TODO log compliance violation - _next = ch; - _cr = false; - return (byte)'\n'; - } + throw new BadMessageException("Bad EOL"); + return ch; case ILLEGAL: @@ -372,11 +339,11 @@ public class MultiPartParser case DELIMITER: case DELIMITER_PADDING: case DELIMITER_CLOSE: - parseDelimiter(buffer, last); + parseDelimiter(buffer); continue; case BODY_PART: - handle = parseMimePartHeaders(buffer, last); + handle = parseMimePartHeaders(buffer); break; case FIRST_OCTETS: @@ -454,11 +421,11 @@ public class MultiPartParser } /* ------------------------------------------------------------------------------- */ - private void parseDelimiter(ByteBuffer buffer, boolean last) + private void parseDelimiter(ByteBuffer buffer) { while (__delimiterStates.contains(_state) && hasNextByte(buffer)) { - byte b=getNextByte(buffer, last); + byte b=getNextByte(buffer); if (b==0) return; @@ -498,15 +465,22 @@ public class MultiPartParser /* * Parse the message headers and return true if the handler has signaled for a return */ - protected boolean parseMimePartHeaders(ByteBuffer buffer, boolean last) + protected boolean parseMimePartHeaders(ByteBuffer buffer) { // Process headers while (_state==State.BODY_PART && hasNextByte(buffer)) { // process each character - byte b=getNextByte(buffer, last); + 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) { @@ -641,6 +615,7 @@ public class MultiPartParser { _fieldValue=takeString(); _length=-1; + _totalHeaderLineLength=-1; } setState(FieldState.FIELD); break; @@ -673,13 +648,6 @@ public class MultiPartParser protected boolean parseOctetContent(ByteBuffer buffer) { - - //handle the next content that was held because of \r as \r\n - if (_next!=0) - { - _handler.content(BufferUtil.toBuffer(new byte[] {_next}),false); - _next = 0; - } //Starts With if (_partialBoundary>0) diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartInputStreamTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartInputStreamTest.java index 1ee35872e2b..cf8dd525289 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartInputStreamTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartInputStreamTest.java @@ -90,11 +90,10 @@ public class MultiPartInputStreamTest try { mpis.getParts(); - fail ("Multipart incomplete"); + fail ("Incomplete Multipart"); } catch (IOException e) { - System.err.println(e.getMessage()); assertTrue(e.getMessage().startsWith("Incomplete")); } } @@ -237,11 +236,11 @@ public class MultiPartInputStreamTest try { mpis.getParts(); - fail ("Multipart missing body"); + fail ("Missing initial multi part boundary"); } catch (IOException e) { - assertTrue(e.getMessage().startsWith("Missing content")); + assertTrue(e.getMessage().contains("Missing initial multi part boundary")); } } @@ -305,11 +304,11 @@ public class MultiPartInputStreamTest try { mpis.getParts(); - fail ("Multipart missing body"); + fail("Missing initial multi part boundary"); } catch (IOException e) { - assertTrue(e.getMessage().startsWith("Missing initial")); + assertTrue(e.getMessage().contains("Missing initial multi part boundary")); } } @@ -403,13 +402,9 @@ public class MultiPartInputStreamTest Collection parts = mpis.getParts(); assertThat(parts, notNullValue()); - assertThat(parts.size(), is(2)); - Part field1 = mpis.getPart("field1"); - assertThat(field1, notNullValue()); + assertThat(parts.size(), is(1)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); - IO.copy(field1.getInputStream(), baos); - assertThat(baos.toString("US-ASCII"), is("Joe Blow")); - Part stuff = mpis.getPart("stuff"); assertThat(stuff, notNullValue()); baos = new ByteArrayOutputStream(); @@ -446,10 +441,10 @@ public class MultiPartInputStreamTest config, _tmpDir); mpis.setDeleteOnExit(true); - Collection parts = null; + try { - parts = mpis.getParts(); + mpis.getParts(); fail("Request should have exceeded maxRequestSize"); } catch (IllegalStateException e) @@ -534,6 +529,7 @@ public class MultiPartInputStreamTest } catch (IllegalStateException e) { + e.printStackTrace(); assertTrue(e.getMessage().startsWith("Multipart Mime part")); } @@ -600,12 +596,12 @@ public class MultiPartInputStreamTest String str = "--AaB03x\n"+ "content-disposition: form-data; name=\"field1\"\n"+ "\n"+ - "Joe Blow\n"+ - "--AaB03x\n"+ + "Joe Blow"+ + "\r\n--AaB03x\n"+ "content-disposition: form-data; name=\"field2\"\n"+ "\n"+ - "Other\n"+ - "--AaB03x--\n"; + "Other"+ + "\r\n--AaB03x--\n"; MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50); MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()), @@ -620,12 +616,14 @@ public class MultiPartInputStreamTest ByteArrayOutputStream baos = new ByteArrayOutputStream(); IO.copy(p1.getInputStream(), baos); assertThat(baos.toString("UTF-8"), is("Joe Blow")); - + Part p2 = mpis.getPart("field2"); assertThat(p2, notNullValue()); baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); assertThat(baos.toString("UTF-8"), is("Other")); + + } @Test @@ -648,22 +646,30 @@ public class MultiPartInputStreamTest config, _tmpDir); mpis.setDeleteOnExit(true); - Collection parts = mpis.getParts(); - assertThat(parts.size(), is(2)); - assertThat(parts.size(), is(2)); - Part p1 = mpis.getPart("field1"); - assertThat(p1, notNullValue()); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - IO.copy(p1.getInputStream(), baos); - assertThat(baos.toString("UTF-8"), is("Joe Blow")); - - Part p2 = mpis.getPart("field2"); - assertThat(p2, notNullValue()); - baos = new ByteArrayOutputStream(); - IO.copy(p2.getInputStream(), baos); - assertThat(baos.toString("UTF-8"), is("Other")); + try + { + Collection parts = mpis.getParts(); + assertThat(parts.size(), is(2)); + + assertThat(parts.size(), is(2)); + Part p1 = mpis.getPart("field1"); + assertThat(p1, notNullValue()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IO.copy(p1.getInputStream(), baos); + assertThat(baos.toString("UTF-8"), is("Joe Blow")); + + Part p2 = mpis.getPart("field2"); + assertThat(p2, notNullValue()); + baos = new ByteArrayOutputStream(); + IO.copy(p2.getInputStream(), baos); + assertThat(baos.toString("UTF-8"), is("Other")); + } + catch(Throwable e) + { + assertTrue(e.getMessage().contains("Bad EOL")); + } } @Test @@ -687,28 +693,37 @@ public class MultiPartInputStreamTest config, _tmpDir); mpis.setDeleteOnExit(true); - Collection parts = mpis.getParts(); - assertThat(parts.size(), is(2)); - - Part p1 = mpis.getPart("field1"); - assertThat(p1, notNullValue()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - IO.copy(p1.getInputStream(), baos); - assertThat(baos.toString("UTF-8"), is("\nJoe Blow\n")); - Part p2 = mpis.getPart("field2"); - assertThat(p2, notNullValue()); - baos = new ByteArrayOutputStream(); - IO.copy(p2.getInputStream(), baos); - assertThat(baos.toString("UTF-8"), is("Other")); + + try + { + Collection parts = mpis.getParts(); + assertThat(parts.size(), is(2)); + + Part p1 = mpis.getPart("field1"); + assertThat(p1, notNullValue()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IO.copy(p1.getInputStream(), baos); + assertThat(baos.toString("UTF-8"), is("\nJoe Blow\n")); + + Part p2 = mpis.getPart("field2"); + assertThat(p2, notNullValue()); + baos = new ByteArrayOutputStream(); + IO.copy(p2.getInputStream(), baos); + assertThat(baos.toString("UTF-8"), is("Other")); + } + catch(Throwable e) + { + assertTrue(e.getMessage().contains("Bad EOL")); + } } @Test public void testBufferOverflowNoCRLF () throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write("--AaB03x".getBytes()); - for (int i=0; i< 8500; i++) //create content that will overrun default buffer size of BufferedInputStream + baos.write("--AaB03x\r\n".getBytes()); + for (int i=0; i< 3000; i++) //create content that will overrun default buffer size of BufferedInputStream { baos.write('a'); } @@ -722,11 +737,11 @@ public class MultiPartInputStreamTest try { mpis.getParts(); - fail ("Multipart buffer overrun"); + fail ("Header Line Exceeded Max Length"); } - catch (IOException e) + catch (Throwable e) { - assertTrue(e.getMessage().startsWith("Buffer size exceeded")); + assertTrue(e.getMessage().startsWith("Header Line Exceeded Max Length")); } } @@ -735,12 +750,12 @@ public class MultiPartInputStreamTest public void testCharsetEncoding () throws Exception { String contentType = "multipart/form-data; boundary=TheBoundary; charset=ISO-8859-1"; - String str = "--TheBoundary\r"+ - "content-disposition: form-data; name=\"field1\"\r"+ - "\r"+ + String str = "--TheBoundary\r\n"+ + "content-disposition: form-data; name=\"field1\"\r\n"+ + "\r\n"+ "\nJoe Blow\n"+ - "\r"+ - "--TheBoundary--\r"; + "\r\n"+ + "--TheBoundary--\r\n"; MultipartConfigElement config = new MultipartConfigElement(_dirname, 1024, 3072, 50); MultiPartInputStreamParser mpis = new MultiPartInputStreamParser(new ByteArrayInputStream(str.getBytes()), @@ -907,8 +922,8 @@ public class MultiPartInputStreamTest assertThat(stuff.getSize(),is(51L)); File tmpfile = ((MultiPartInputStreamParser.MultiPart)stuff).getFile(); - assertThat(tmpfile,notNullValue()); // longer than 100 bytes, should already be a tmp file - assertThat(((MultiPartInputStreamParser.MultiPart)stuff).getBytes(),nullValue()); //not in an internal buffer + assertThat(tmpfile,notNullValue()); // longer than 50 bytes, should already be a tmp file + assertThat(stuff.getBytes(),nullValue()); //not in an internal buffer assertThat(tmpfile.exists(),is(true)); assertThat(tmpfile.getName(),is(not("stuff with space.txt"))); stuff.write(filename); @@ -1000,7 +1015,7 @@ public class MultiPartInputStreamTest assertNotNull(p2); baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); - assertEquals("hello jetty", baos.toString("US-ASCII")); + assertEquals(B64Code.encode("hello jetty"), baos.toString("US-ASCII")); Part p3 = mpis.getPart("final"); assertNotNull(p3); @@ -1044,7 +1059,7 @@ public class MultiPartInputStreamTest assertNotNull(p2); baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); - assertEquals("truth=beauty", baos.toString("US-ASCII")); + assertEquals("truth=3Dbeauty", baos.toString("US-ASCII")); } diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java index 4dfe141969c..07b38af84b0 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/MultiPartParserTest.java @@ -462,20 +462,25 @@ public class MultiPartParserTest return false; } }; - MultiPartParser parser = new MultiPartParser(handler,"AaB03x",true); + MultiPartParser parser = new MultiPartParser(handler,"AaB03x"); ByteBuffer data = BufferUtil.toBuffer( - "--AaB03x\r"+ - "content-disposition: form-data; name=\"field1\"\r"+ + "--AaB03x\r\n"+ + "content-disposition: form-data; name=\"field1\"\r\n"+ "\r"+ "Joe Blow\r\n"+ - "--AaB03x--\r"); + "--AaB03x--\r\n"); - /* Test Progression to END State */ - parser.parse(data,true); - assertThat(parser.getState(), is(State.END)); - assertThat(data.remaining(),is(0)); + try + { + parser.parse(data,true); + fail("Invalid End of Line"); + } + catch(BadMessageException e) { + assertTrue(e.getMessage().contains("Bad EOL")); + } + } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java index a7455e4e698..23c55ddb90c 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/MultiPartInputStreamParser.java @@ -36,6 +36,7 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -44,6 +45,7 @@ import javax.servlet.MultipartConfigElement; import javax.servlet.ServletInputStream; import javax.servlet.http.Part; +import org.eclipse.jetty.util.ReadLineInputStream.Termination; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -69,8 +71,29 @@ public class MultiPartInputStreamParser protected boolean _deleteOnExit; protected boolean _writeFilesWithFilenames; + EnumSet nonComplianceWarnings = EnumSet.noneOf(NonCompliance.class); + public enum NonCompliance + { + CR_TERMINATION, + LF_TERMINATION, + NO_INITIAL_CRLF, + BASE64_TRANSFER_ENCODING, + QUOTED_PRINTABLE_TRANSFER_ENCODING + } + public EnumSet getNonComplianceWarnings() + { + EnumSet term = ((ReadLineInputStream)_in).getLineTerminations(); + + if(term.contains(Termination.CR)) + nonComplianceWarnings.add(NonCompliance.CR_TERMINATION); + if(term.contains(Termination.LF)) + nonComplianceWarnings.add(NonCompliance.LF_TERMINATION); + + return nonComplianceWarnings; + } + public class MultiPart implements Part { protected String _name; @@ -175,8 +198,8 @@ public class MultiPartInputStreamParser _out.flush(); _bout.writeTo(bos); _out.close(); - _bout = null; } + _bout = null; _out = bos; } @@ -563,6 +586,8 @@ public class MultiPartInputStreamParser throw new IOException("Missing content for multipart request"); boolean badFormatLogged = false; + + String untrimmed = line; line=line.trim(); while (line != null && !line.equals(boundary) && !line.equals(lastBoundary)) { @@ -572,16 +597,22 @@ public class MultiPartInputStreamParser badFormatLogged = true; } line=((ReadLineInputStream)_in).readLine(); - line=(line==null?line:line.trim()); + untrimmed = line; + if(line!=null) + line = line.trim(); } - if (line == null || line.length() == 0) + if (line == null || line.length() == 0) throw new IOException("Missing initial multi part boundary"); // Empty multipart. if (line.equals(lastBoundary)) return; + // check compliance of preamble + if (Character.isWhitespace(untrimmed.charAt(0))) + nonComplianceWarnings.add(NonCompliance.NO_INITIAL_CRLF); + // Read each part boolean lastPart=false; @@ -671,10 +702,12 @@ public class MultiPartInputStreamParser InputStream partInput = null; if ("base64".equalsIgnoreCase(contentTransferEncoding)) { + nonComplianceWarnings.add(NonCompliance.BASE64_TRANSFER_ENCODING); partInput = new Base64InputStream((ReadLineInputStream)_in); } else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) { + nonComplianceWarnings.add(NonCompliance.QUOTED_PRINTABLE_TRANSFER_ENCODING); partInput = new FilterInputStream(_in) { @Override @@ -917,5 +950,8 @@ public class MultiPartInputStreamParser return _buffer[_pos++]; } - } + } + + + } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java index ff430e35c83..a6acd0c391f 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/ReadLineInputStream.java @@ -22,6 +22,9 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.EnumSet; + +import org.eclipse.jetty.util.MultiPartInputStreamParser.NonCompliance; /** * ReadLineInputStream @@ -32,6 +35,15 @@ public class ReadLineInputStream extends BufferedInputStream { boolean _seenCRLF; boolean _skipLF; + private EnumSet _lineTerminations = EnumSet.noneOf(Termination.class); + public EnumSet getLineTerminations() { return _lineTerminations; } + public enum Termination + { + CRLF, + LF, + CR, + EOF + } public ReadLineInputStream(InputStream in) { @@ -54,13 +66,18 @@ public class ReadLineInputStream extends BufferedInputStream if (markpos < 0) throw new IOException("Buffer size exceeded: no line terminator"); + if(_skipLF && b!='\n') + _lineTerminations.add(Termination.CR); + if (b==-1) { int m=markpos; markpos=-1; if (pos>m) + { + _lineTerminations.add(Termination.EOF); return new String(buf,m,pos-m, StandardCharsets.UTF_8); - + } return null; } @@ -72,10 +89,18 @@ public class ReadLineInputStream extends BufferedInputStream if (_seenCRLF && pos parts = mpis.getParts(); assertTrue(mpis.getParts().isEmpty()); + assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings()); } @@ -141,6 +144,7 @@ public class MultiPartInputStreamTest _tmpDir); mpis.setDeleteOnExit(true); assertTrue(mpis.getParts().isEmpty()); + assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings()); } @Test @@ -201,7 +205,10 @@ public class MultiPartInputStreamTest assertThat(title, notNullValue()); assertThat(title.getSize(), is(3L)); IO.copy(title.getInputStream(), baos); - assertThat(baos.toString("US-ASCII"), is("ttt")); + assertThat(baos.toString("US-ASCII"), is("ttt")); + + assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings()); + } @Test @@ -215,6 +222,7 @@ public class MultiPartInputStreamTest _tmpDir); mpis.setDeleteOnExit(true); assertTrue(mpis.getParts().isEmpty()); + assertEquals(EnumSet.noneOf(NonCompliance.class), mpis.getNonComplianceWarnings()); } @Test @@ -369,10 +377,11 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(stuff.getInputStream(), baos); assertTrue(baos.toString("US-ASCII").contains("aaaa")); + + assertEquals(EnumSet.of(NonCompliance.LF_TERMINATION), mpis.getNonComplianceWarnings()); } - @Test public void testLeadingWhitespaceBodyWithoutCRLF() throws Exception @@ -410,13 +419,12 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(stuff.getInputStream(), baos); assertTrue(baos.toString("US-ASCII").contains("bbbbb")); + + assertEquals(EnumSet.of(NonCompliance.NO_INITIAL_CRLF), mpis.getNonComplianceWarnings()); } - - - @Test public void testNoLimits() throws Exception @@ -431,6 +439,7 @@ public class MultiPartInputStreamTest assertFalse(parts.isEmpty()); } + @Test public void testRequestTooBig () throws Exception @@ -621,6 +630,8 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); assertThat(baos.toString("UTF-8"), is("Other")); + + assertEquals(EnumSet.of(NonCompliance.LF_TERMINATION), mpis.getNonComplianceWarnings()); } @Test @@ -659,6 +670,8 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); assertThat(baos.toString("UTF-8"), is("Other")); + + assertEquals(EnumSet.of(NonCompliance.CR_TERMINATION), mpis.getNonComplianceWarnings()); } @Test @@ -696,6 +709,8 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); assertThat(baos.toString("UTF-8"), is("Other")); + + assertEquals(EnumSet.of(NonCompliance.CR_TERMINATION), mpis.getNonComplianceWarnings()); } @Test @@ -745,6 +760,7 @@ public class MultiPartInputStreamTest mpis.setDeleteOnExit(true); Collection parts = mpis.getParts(); assertThat(parts.size(), is(1)); + } @@ -1002,6 +1018,8 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(p3.getInputStream(), baos); assertEquals("the end", baos.toString("US-ASCII")); + + assertEquals(EnumSet.of(NonCompliance.BASE64_TRANSFER_ENCODING), mpis.getNonComplianceWarnings()); } @Test @@ -1040,6 +1058,8 @@ public class MultiPartInputStreamTest baos = new ByteArrayOutputStream(); IO.copy(p2.getInputStream(), baos); assertEquals("truth=beauty", baos.toString("US-ASCII")); + + assertEquals(EnumSet.of(NonCompliance.QUOTED_PRINTABLE_TRANSFER_ENCODING), mpis.getNonComplianceWarnings()); } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java index 6a41d1de765..e1e53708c14 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ReadLineInputStreamTest.java @@ -22,8 +22,10 @@ import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; +import java.util.EnumSet; import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.util.ReadLineInputStream.Termination; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -101,6 +103,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations()); } @Test @@ -114,6 +117,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations()); } @Test @@ -127,6 +131,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations()); } @@ -145,6 +150,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations()); } @Test @@ -162,6 +168,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations()); } @Test @@ -180,6 +187,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("World",_in.readLine()); Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations()); } @@ -201,6 +209,7 @@ public class ReadLineInputStreamTest Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.LF), _in.getLineTerminations()); } @Test @@ -221,6 +230,8 @@ public class ReadLineInputStreamTest Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CR), _in.getLineTerminations()); + } @Test @@ -241,6 +252,8 @@ public class ReadLineInputStreamTest Assert.assertEquals("",_in.readLine()); Assert.assertEquals(null,_in.readLine()); + Assert.assertEquals(EnumSet.of(Termination.CRLF), _in.getLineTerminations()); + }