From 810ff3802fafb0e3546fa6f11db45e0dfcfcc61b Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Sat, 22 Dec 2012 12:00:54 +1100 Subject: [PATCH 1/3] 397110 Accept %uXXXX encodings in URIs --- .../java/org/eclipse/jetty/http/HttpURI.java | 90 +++++++--- .../org/eclipse/jetty/server/HttpURITest.java | 10 +- .../java/org/eclipse/jetty/util/TypeUtil.java | 33 ++-- .../org/eclipse/jetty/util/UrlEncoded.java | 170 +++++++++++++----- .../org/eclipse/jetty/util/TypeUtilTest.java | 19 +- .../eclipse/jetty/util/URLEncodedTest.java | 29 +-- 6 files changed, 250 insertions(+), 101 deletions(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java index 3a419db1ea8..258fbb8ac80 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java @@ -558,8 +558,7 @@ public class HttpURI return null; int length = _param-_path; - byte[] bytes=null; - int n=0; + boolean decoding=false; for (int i=_path;i<_param;i++) { @@ -567,35 +566,49 @@ public class HttpURI if (b=='%') { + if (!decoding) + { + _utf8b.reset(); + _utf8b.append(_raw,_path,i-_path); + decoding=true; + } + if ((i+2)>=_param) throw new IllegalArgumentException("Bad % encoding: "+this); - b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16)); - i+=2; - } - else if (bytes==null) - { - n++; + if (_raw[i+1]=='u') + { + if ((i+5)>=_param) + throw new IllegalArgumentException("Bad %u encoding: "+this); + try + { + String unicode = new String(Character.toChars(TypeUtil.parseInt(_raw,i+2,4,16))); + _utf8b.getStringBuilder().append(unicode); + i+=5; + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + else + { + b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16)); + _utf8b.append(b); + i+=2; + } continue; } - - if (bytes==null) + else if (decoding) { - bytes=new byte[length]; - System.arraycopy(_raw,_path,bytes,0,n); + _utf8b.append(b); } - - bytes[n++]=b; } - if (bytes==null) + if (!decoding) return toUtf8String(_path,length); - - _utf8b.reset(); - _utf8b.append(bytes,0,n); return _utf8b.toString(); } - public String getDecodedPath(String encoding) { if (_path==_param) @@ -611,10 +624,39 @@ public class HttpURI if (b=='%') { + if (bytes==null) + { + bytes=new byte[length]; + System.arraycopy(_raw,_path,bytes,0,n); + } + if ((i+2)>=_param) throw new IllegalArgumentException("Bad % encoding: "+this); - b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16)); - i+=2; + if (_raw[i+1]=='u') + { + if ((i+5)>=_param) + throw new IllegalArgumentException("Bad %u encoding: "+this); + + try + { + String unicode = new String(Character.toChars(TypeUtil.parseInt(_raw,i+2,4,16))); + byte[] encoded = unicode.getBytes(encoding); + System.arraycopy(encoded,0,bytes,n,encoded.length); + n+=encoded.length; + i+=5; + } + catch(Exception e) + { + throw new RuntimeException(e); + } + } + else + { + b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16)); + bytes[n++]=b; + i+=2; + } + continue; } else if (bytes==null) { @@ -622,12 +664,6 @@ public class HttpURI continue; } - if (bytes==null) - { - bytes=new byte[length]; - System.arraycopy(_raw,_path,bytes,0,n); - } - bytes[n++]=b; } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpURITest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpURITest.java index b996bca42a9..5ebea9fad48 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/HttpURITest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/HttpURITest.java @@ -195,7 +195,8 @@ public class HttpURITest /* 1*/ {"/path/%69nfo","/path/info", "UTF-8"}, /* 2*/ {"http://host/path/%69nfo","/path/info", "UTF-8"}, /* 3*/ {"http://host/path/%69nf%c2%a4","/path/inf\u00a4", "UTF-8"}, - /* 4*/ {"http://host/path/%E5", "/path/\u00e5", "ISO-8859-1"} + /* 4*/ {"http://host/path/%E5", "/path/\u00e5", "ISO-8859-1"}, + /* 5*/ {"/foo/%u30ED/bar%3Fabc%3D123%26xyz%3D456","/foo/\u30ed/bar?abc=123&xyz=456","UTF-8"} }; @Test @@ -207,11 +208,12 @@ public class HttpURITest { uri.parse(encoding_tests[t][0]); assertEquals(""+t,encoding_tests[t][1],uri.getDecodedPath(encoding_tests[t][2])); - + + if ("UTF-8".equalsIgnoreCase(encoding_tests[t][2])) + assertEquals(""+t,encoding_tests[t][1],uri.getDecodedPath()); } } - @Test public void testNoPercentEncodingOfQueryUsingNonUTF8() throws Exception { @@ -314,7 +316,7 @@ public class HttpURITest @Test public void testUnicodeErrors() throws UnsupportedEncodingException { - String uri="http://server/path?invalid=data%u2021here"; + String uri="http://server/path?invalid=data%uXXXXhere%u000"; try { URLDecoder.decode(uri,"UTF-8"); diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java index 5bc4ec19e92..69b9d90e053 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/TypeUtil.java @@ -278,13 +278,7 @@ public class TypeUtil { char c=s.charAt(offset+i); - int digit=c-'0'; - if (digit<0 || digit>=base || digit>=10) - { - digit=10+c-'A'; - if (digit<10 || digit>=base) - digit=10+c-'a'; - } + int digit=convertHexDigit((int)c); if (digit<0 || digit>=base) throw new NumberFormatException(s.substring(offset,offset+length)); value=value*base+digit; @@ -358,15 +352,28 @@ public class TypeUtil /* ------------------------------------------------------------ */ /** - * @param b An ASCII encoded character 0-9 a-f A-F + * @param c An ASCII encoded character 0-9 a-f A-F * @return The byte value of the character 0-16. */ - public static byte convertHexDigit( byte b ) + public static byte convertHexDigit( byte c ) { - if ((b >= '0') && (b <= '9')) return (byte)(b - '0'); - if ((b >= 'a') && (b <= 'f')) return (byte)(b - 'a' + 10); - if ((b >= 'A') && (b <= 'F')) return (byte)(b - 'A' + 10); - throw new IllegalArgumentException("!hex:"+Integer.toHexString(0xff&b)); + byte b = (byte)((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (b<0 || b>15) + throw new IllegalArgumentException("!hex "+c); + return b; + } + + /* ------------------------------------------------------------ */ + /** + * @param c An ASCII encoded character 0-9 a-f A-F + * @return The byte value of the character 0-16. + */ + public static int convertHexDigit( int c ) + { + int d= ((c & 0x1f) + ((c >> 6) * 0x19) - 0x10); + if (d<0 || d>15) + throw new NumberFormatException("!hex "+c); + return d; } /* ------------------------------------------------------------ */ diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java index b1a47f6c3a3..635d8880c5b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java @@ -18,6 +18,8 @@ package org.eclipse.jetty.util; +import static org.eclipse.jetty.util.TypeUtil.convertHexDigit; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -328,7 +330,16 @@ public class UrlEncoded extends MultiMap implements Cloneable case '%': if (i+2=0) + { + int code2=in.read(); + if (code2>=0) + { + int code3=in.read(); + if (code3>=0) + buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))); + } + } + } + else if (code0>=0) + { + int code1=in.read(); + if (code1>=0) + buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1))); + } break; + default: buffer.append((char)b); break; @@ -495,12 +523,29 @@ public class UrlEncoded extends MultiMap implements Cloneable break; case '%': - int dh=in.read(); - int dl=in.read(); - if (dh<0||dl<0) - break; - buffer.append((byte)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl))); + int code0=in.read(); + if ('u'==code0) + { + int code1=in.read(); + if (code1>=0) + { + int code2=in.read(); + if (code2>=0) + { + int code3=in.read(); + if (code3>=0) + buffer.getStringBuilder().append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))); + } + } + } + else if (code0>=0) + { + int code1=in.read(); + if (code1>=0) + buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1))); + } break; + default: buffer.append((byte)b); break; @@ -576,8 +621,6 @@ public class UrlEncoded extends MultiMap implements Cloneable String value = null; int c; - int digit=0; - int digits=0; int totalLength = 0; ByteArrayOutputStream2 output = new ByteArrayOutputStream2(); @@ -619,21 +662,31 @@ public class UrlEncoded extends MultiMap implements Cloneable output.write(' '); break; case '%': - digits=2; + int code0=in.read(); + if ('u'==code0) + { + int code1=in.read(); + if (code1>=0) + { + int code2=in.read(); + if (code2>=0) + { + int code3=in.read(); + if (code3>=0) + output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset)); + } + } + + } + else if (code0>=0) + { + int code1=in.read(); + if (code1>=0) + output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1)); + } break; default: - if (digits==2) - { - digit=TypeUtil.convertHexDigit((byte)c); - digits=1; - } - else if (digits==1) - { - output.write((digit<<4) + TypeUtil.convertHexDigit((byte)c)); - digits=0; - } - else - output.write(c); + output.write(c); break; } @@ -688,24 +741,45 @@ public class UrlEncoded extends MultiMap implements Cloneable buffer.getStringBuffer().append(' '); } - else if (c=='%' && (i+2) m2 = new MultiMap(); UrlEncoded.decodeTo(in2, m2, "Shift_JIS", -1,-1); assertEquals("stream length",1,m2.size()); assertEquals("stream name","\u30c6\u30b9\u30c8",m2.getString("name")); From c9135e34c9bd095fd37a3602ae20470416c863ea Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Sat, 22 Dec 2012 12:38:28 +1100 Subject: [PATCH 2/3] 397111 Allow multipart bodies with leading blank lines --- .../jetty/servlets/MultiPartFilter.java | 13 +++- .../jetty/servlets/MultipartFilterTest.java | 66 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java index b2ac85b3051..f66c7ef425d 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java @@ -151,7 +151,18 @@ public class MultiPartFilter implements Filter if (line == null || line.length() == 0) throw new IOException("Missing content for multipart request"); - if (!line.equals(boundary)) + boolean badFormatLogged = false; + while (line != null && !line.equals(boundary)) + { + if (!badFormatLogged) + { + LOG.warn("Badly formatted multipart request"); + badFormatLogged = true; + } + line=((ReadLineInputStream)in).readLine(); + } + + if (line == null || line.length() == 0) throw new IOException("Missing initial multi part boundary"); // Read each part diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java index 19e144c1fc4..1236cf533a3 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java @@ -648,7 +648,73 @@ public class MultipartFilterTest assertTrue(response.getReason().startsWith("Missing initial")); } + @Test + public void testLeadingWhitespaceBodyWithCRLF() + throws Exception + { + String boundary = "AaB03x"; + String body = " \n\n\n\r\n\r\n\r\n\r\n"+ + "--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=\"fileup\"; filename=\"test.upload\"\r\n"+ + "Content-Type: application/octet-stream\r\n"+ + "\r\n" + + "aaaa,bbbbb"+"\r\n" + + "--AaB03x--\r\n"; + + // generated and parsed test + HttpTester request = new HttpTester(); + HttpTester response = new HttpTester(); + request.setMethod("POST"); + request.setVersion("HTTP/1.0"); + request.setHeader("Host","tester"); + request.setURI("/context/dump"); + request.setHeader("Content-Type","multipart/form-data; boundary="+boundary); + request.setContent(body); + + response.parse(tester.getResponses(request.generate())); + assertTrue(response.getMethod()==null); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertTrue(response.getContent().contains("aaaa,bbbbb")); + } + + @Test + public void testLeadingWhitespaceBodyWithoutCRLF() + throws Exception + { + String boundary = "AaB03x"; + + String body = " "+ + "--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=\"fileup\"; filename=\"test.upload\"\r\n"+ + "Content-Type: application/octet-stream\r\n"+ + "\r\n" + + "aaaa,bbbbb"+"\r\n" + + "--AaB03x--\r\n"; + + // generated and parsed test + HttpTester request = new HttpTester(); + HttpTester response = new HttpTester(); + request.setMethod("POST"); + request.setVersion("HTTP/1.0"); + request.setHeader("Host","tester"); + request.setURI("/context/dump"); + request.setHeader("Content-Type","multipart/form-data; boundary="+boundary); + request.setContent(body); + + response.parse(tester.getResponses(request.generate())); + assertTrue(response.getMethod()==null); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertTrue(response.getContent().contains("aaaa,bbbbb")); + } /* From 0420365f82ce4005d7fe42db6f65a6119e28310b Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Sat, 22 Dec 2012 14:00:46 +1100 Subject: [PATCH 3/3] 397111 Allow multipart bodies with leading blank lines Also trim any leading space from FIRST boundary --- .../main/java/org/eclipse/jetty/servlets/MultiPartFilter.java | 2 ++ .../java/org/eclipse/jetty/servlets/MultipartFilterTest.java | 1 + 2 files changed, 3 insertions(+) diff --git a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java index f66c7ef425d..e37ffef6c59 100644 --- a/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java +++ b/jetty-servlets/src/main/java/org/eclipse/jetty/servlets/MultiPartFilter.java @@ -151,6 +151,7 @@ public class MultiPartFilter implements Filter if (line == null || line.length() == 0) throw new IOException("Missing content for multipart request"); + line = line.trim(); boolean badFormatLogged = false; while (line != null && !line.equals(boundary)) { @@ -160,6 +161,7 @@ public class MultiPartFilter implements Filter badFormatLogged = true; } line=((ReadLineInputStream)in).readLine(); + line=(line==null?line:line.trim()); } if (line == null || line.length() == 0) diff --git a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java index 1236cf533a3..bd526e1d6a0 100644 --- a/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java +++ b/jetty-servlets/src/test/java/org/eclipse/jetty/servlets/MultipartFilterTest.java @@ -67,6 +67,7 @@ public class MultipartFilterTest @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + System.err.println(req.getParameter("field1")); assertNotNull(req.getParameter("fileup")); assertNotNull(req.getParameter("fileup"+MultiPartFilter.CONTENT_TYPE_SUFFIX)); assertEquals(req.getParameter("fileup"+MultiPartFilter.CONTENT_TYPE_SUFFIX), "application/octet-stream");