From df83cca0100b9b2a7935cbc100d6d18336ea615e Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Sun, 18 Mar 2018 21:53:54 +1100 Subject: [PATCH] Rewrote the parse method to use the MultiPartParser, some error handling not implemented so only passing half of the tests. Signed-off-by: Lachlan Roberts --- .../http/MultiPartInputStreamParser.java | 358 +++++++----------- .../eclipse/jetty/http/MultiPartParser.java | 8 +- .../jetty/http/MultiPartParserTest.java | 52 ++- 3 files changed, 176 insertions(+), 242 deletions(-) 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 01703a8785f..bfa01590f23 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 @@ -29,6 +29,7 @@ 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; @@ -44,7 +45,9 @@ 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; import org.eclipse.jetty.util.MultiException; @@ -208,7 +211,7 @@ public class MultiPartInputStreamParser */ @Override public String getHeader(String name) - { + { if (name == null) return null; return _headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); @@ -514,17 +517,14 @@ public class MultiPartInputStreamParser //initialize - long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize _parts = new MultiMap<>(); //if its not a multipart request, don't parse it if (_contentType == null || !_contentType.startsWith("multipart/form-data")) return; - try - { + //sort out the location to which to write the files - if (_config.getLocation() == null) _tmpDir = _contextTmpDir; else if ("".equals(_config.getLocation())) @@ -550,94 +550,95 @@ public class MultiPartInputStreamParser contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim()); } - String boundary="--"+contentTypeBoundary; - String lastBoundary=boundary+"--"; - byte[] byteBoundary=lastBoundary.getBytes(StandardCharsets.ISO_8859_1); - - // Get first boundary - String line = null; - try + + // ============ MY CODE ============= // + 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; + + + while(true) { - line=((ReadLineInputStream)_in).readLine(); - } - catch (IOException e) - { - LOG.warn("Badly formatted multipart request"); - throw e; - } - - if (line == null) - throw new IOException("Missing content for multipart request"); - - boolean badFormatLogged = false; - line=line.trim(); - while (line != null && !line.equals(boundary) && !line.equals(lastBoundary)) - { - if (!badFormatLogged) + try { - LOG.warn("Badly formatted multipart request"); - badFormatLogged = true; + len = _in.read(data); + } + catch (IOException e) + { + _err = e; + return; + } + + if(len > 0) + { + ByteBuffer buffer = BufferUtil.toBuffer(data); + buffer.limit(len); + parser.parse(buffer, false); + } + else if (len == -1) + { + parser.parse(BufferUtil.EMPTY_BUFFER, true); + break; } - line=((ReadLineInputStream)_in).readLine(); - line=(line==null?line:line.trim()); } - - if (line == null || line.length() == 0) - throw new IOException("Missing initial multi part boundary"); - - // Empty multipart. - if (line.equals(lastBoundary)) + + //check for exceptions + if(_err != null) + { return; - - // Read each part - boolean lastPart=false; - - outer:while(!lastPart) + } + + //check we read to the end of the message + if(parser.getState() != MultiPartParser.State.END) { - String contentDisposition=null; - String contentType=null; - String contentTransferEncoding=null; + _err = new IllegalStateException("Parser Not in End State: "+parser.getState()); + } + + } + + 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; + private MultiMap 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(key.toLowerCase(Locale.ENGLISH), value); + if (key.equalsIgnoreCase("content-disposition")) + contentDisposition=value; + else if (key.equalsIgnoreCase("content-type")) + contentType = value; - MultiMap headers = new MultiMap<>(); - while(true) - { - line=((ReadLineInputStream)_in).readLine(); - - //No more input - if(line==null) - break outer; - - //end of headers: - if("".equals(line)) - break; - - total += line.length(); - if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize()) - throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")"); - - //get content-disposition and content-type - int c=line.indexOf(':',0); - if(c>0) - { - String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH); - String value=line.substring(c+1,line.length()).trim(); - headers.put(key, value); - if (key.equalsIgnoreCase("content-disposition")) - contentDisposition=value; - if (key.equalsIgnoreCase("content-type")) - contentType = value; - if(key.equals("content-transfer-encoding")) - contentTransferEncoding=value; - } - } + } + @Override + public boolean headerComplete() { + + try { + // Extract content-disposition boolean form_data=false; if(contentDisposition==null) { throw new IOException("Missing content-disposition"); } - + QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true); String name=null; String filename=null; @@ -652,11 +653,11 @@ public class MultiPartInputStreamParser else if(tl.startsWith("filename=")) filename=filenameValue(t); } - + // Check disposition if(!form_data) { - continue; + return false; } //It is valid for reset and submit buttons to have an empty name. //If no name is supplied, the browser skips sending the info for that field. @@ -665,163 +666,68 @@ public class MultiPartInputStreamParser //have not yet seen a name field. if(name==null) { - continue; + return false; } - + //Have a new Part - MultiPart part = new MultiPart(name, filename); - part.setHeaders(headers); - part.setContentType(contentType); - _parts.add(name, part); - part.open(); - - InputStream partInput = null; - if ("base64".equalsIgnoreCase(contentTransferEncoding)) - { - partInput = new Base64InputStream((ReadLineInputStream)_in); - } - else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) - { - partInput = new FilterInputStream(_in) - { - @Override - public int read() throws IOException - { - int c = in.read(); - if (c >= 0 && c == '=') - { - int hi = in.read(); - int lo = in.read(); - if (hi < 0 || lo < 0) - { - throw new IOException("Unexpected end to quoted-printable byte"); - } - char[] chars = new char[] { (char)hi, (char)lo }; - c = Integer.parseInt(new String(chars),16); - } - return c; - } - }; - } - else - partInput = _in; - - + System.out.println("-------------------------"); + System.out.println("Creating New Part"); + System.out.println("Name: "+name); + System.out.println("Filename: "+filename); + System.out.println("Headers:\n"+headers); + System.out.println("ContentType:\n"+contentType); + System.out.println("-------------------------"); + + _part = new MultiPart(name, filename); + _part.setHeaders(headers); + _part.setContentType(contentType); + _parts.add(name, _part); + } + catch (Exception e) + { + _err = e; + return true; + } + + return false; + } + + @Override + public boolean content(ByteBuffer buffer, boolean last) + { + if (BufferUtil.hasContent(buffer)) + { try { - int state=-2; - int c; - boolean cr=false; - boolean lf=false; - - // loop for all lines - while(true) - { - int b=0; - while((c=(state!=-2)?state:partInput.read())!=-1) - { - total ++; - if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize()) - throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")"); - - state=-2; - - // look for CR and/or LF - if(c==13||c==10) - { - if(c==13) - { - partInput.mark(1); - int tmp=partInput.read(); - if (tmp!=10) - partInput.reset(); - else - state=tmp; - } - break; - } - - // Look for boundary - if(b>=0&&b0) - part.write(byteBoundary,0,b); - - b=-1; - part.write(c); - } - } - - // Check for incomplete boundary match, writing out the chars we matched along the way - if((b>0&&b0||c==-1) - { - - if(b==byteBoundary.length) - lastPart=true; - if(state==10) - state=-2; - break; - } - - // handle CR LF - if(cr) - part.write(13); - - if(lf) - part.write(10); - - cr=(c==13); - lf=(c==10||state==10); - if(state==10) - state=-2; - } + //write the content data to the part + _part.open(); + _part.write(buffer.array(),buffer.arrayOffset()+buffer.position(),buffer.remaining()); + _part.close(); } - finally + catch (IOException e) { - part.close(); + _err = e; + return true; } } - if (lastPart) - { - while(line!=null) - line=((ReadLineInputStream)_in).readLine(); - } - else - throw new IOException("Incomplete parts"); + + if (last) + reset(); + + return false; } - catch (Exception e) + + public void reset() { - _err = e; + _part = null; + contentDisposition = null; + contentType = null; + headers = new MultiMap<>(); } + } - + + public void setDeleteOnExit(boolean deleteOnExit) { _deleteOnExit = deleteOnExit; 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 b813a52532b..847a807e6c6 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 @@ -354,20 +354,20 @@ public class MultiPartParser } } - if(last) + if(last && BufferUtil.isEmpty(buffer)) { if(_state == State.EPILOGUE) { _state = State.END; return _handler.messageComplete(); } - else if(BufferUtil.isEmpty(buffer)) + else { _handler.earlyEOF(); return true; } - } + return handle; } @@ -649,7 +649,7 @@ public class MultiPartParser } else { - //print up to _partialBoundary of the search pattern + //output up to _partialBoundary of the search pattern ByteBuffer content = _patternBuffer.slice(); if (_state==State.FIRST_OCTETS) { 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 965e86618a1..aaa6f08f5ac 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 @@ -371,10 +371,6 @@ public class MultiPartParserTest assertThat(handler.fields,Matchers.contains("name: value", "<>")); assertThat(handler.content,Matchers.contains("Hello","<>")); - parser.parse(data,false); - assertThat(parser.getState(), is(State.EPILOGUE)); - assertThat(data.remaining(),is(0)); - parser.parse(data,true); assertThat(parser.getState(), is(State.END)); } @@ -412,17 +408,51 @@ public class MultiPartParserTest assertThat(handler.fields, Matchers.contains("name: value", "<>","powerLevel: 9001","<>")); assertThat(handler.content, Matchers.contains("Hello","<>","secondary\r\ncontent","<>")); - /* Test Progression to EPILOGUE State */ - parser.parse(data,false); - assertThat(parser.getState(), is(State.EPILOGUE)); - assertThat(data.remaining(),is(0)); - /* Test Progression to END State */ parser.parse(data,true); assertThat(parser.getState(), is(State.END)); assertThat(data.remaining(),is(0)); } + + + @Test + public void testEndState() { + 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\n"+ + "Joe Blow\r\n"+ + "--AaB03x\r\n"+ + "content-disposition: form-data; name=\"stuff\"; filename=\"" + "foo.txt" + "\"\r\n"+ + "Content-Type: text/plain\r\n"+ + "\r\n"+"aaaa"+ + "bbbbb"+"\r\n" + + "--AaB03x--\r\n"); + + + /* Test Progression to END State */ + parser.parse(data,true); + assertThat(parser.getState(), is(State.END)); + assertThat(data.remaining(),is(0)); + + } + + + @Test public void splitTest() { @@ -504,7 +534,7 @@ public class MultiPartParserTest int length = data.remaining(); - for(int i = 0; i>")); - System.out.println("iteration "+i); - System.out.println(handler.content); assertThat(handler.contentString(), is(new String("text default"+"<>" + "Content of a.txt.\n"+"<>" + "Content of a.html.\n"+"<>"