From 09791252954bfc620669ef2d95c04efff9efd48d Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 26 Oct 2015 17:59:07 +0100 Subject: [PATCH 1/3] Improved logging of FastCGI variables sent to the server. --- .../fcgi/server/proxy/FastCGIProxyServlet.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java index d80124e6e5a..20468019e2b 100644 --- a/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java +++ b/jetty-fcgi/fcgi-server/src/main/java/org/eclipse/jetty/fcgi/server/proxy/FastCGIProxyServlet.java @@ -20,8 +20,11 @@ package org.eclipse.jetty.fcgi.server.proxy; import java.net.URI; import java.util.List; +import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; + import javax.servlet.RequestDispatcher; import javax.servlet.ServletConfig; import javax.servlet.ServletException; @@ -32,6 +35,7 @@ import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.fcgi.FCGI; import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI; +import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; @@ -212,6 +216,16 @@ public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent { super.customize(request, fastCGIHeaders); customizeFastCGIHeaders(request, fastCGIHeaders); + if (_log.isDebugEnabled()) + { + TreeMap fcgi = new TreeMap<>(); + for (HttpField field : fastCGIHeaders) + fcgi.put(field.getName(), field.getValue()); + String eol = System.lineSeparator(); + _log.debug("FastCGI variables{}{}", eol, fcgi.entrySet().stream() + .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue())) + .collect(Collectors.joining(eol))); + } } } } From 487d0f2d5c007375340f25fc2523c64569411b12 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 27 Oct 2015 15:29:56 +0100 Subject: [PATCH 2/3] 480764 - Error parsing empty multipart. Fixed by checking the presence of the last boundary as the first line. --- .../util/MultiPartInputStreamParser.java | 107 +++++++++--------- 1 file changed, 56 insertions(+), 51 deletions(-) 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 179b6f318db..755e6bd8307 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 @@ -127,7 +127,7 @@ public class MultiPartInputStreamParser if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null) createFile(); - + _out.write(bytes, offset, length); _size += length; } @@ -136,7 +136,7 @@ public class MultiPartInputStreamParser throws IOException { _file = File.createTempFile("MultiPart", "", MultiPartInputStreamParser.this._tmpDir); - + if (_deleteOnExit) _file.deleteOnExit(); FileOutputStream fos = new FileOutputStream(_file); @@ -175,7 +175,7 @@ public class MultiPartInputStreamParser { if (name == null) return null; - return (String)_headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); + return _headers.getValue(name.toLowerCase(Locale.ENGLISH), 0); } /** @@ -211,8 +211,8 @@ public class MultiPartInputStreamParser } } - - /** + + /** * @see javax.servlet.http.Part#getSubmittedFileName() */ @Override @@ -241,7 +241,7 @@ public class MultiPartInputStreamParser */ public long getSize() { - return _size; + return _size; } /** @@ -252,7 +252,7 @@ public class MultiPartInputStreamParser if (_file == null) { _temporary = false; - + //part data is only in the ByteArrayOutputStream and never been written to disk _file = new File (_tmpDir, fileName); @@ -290,12 +290,12 @@ public class MultiPartInputStreamParser public void delete() throws IOException { if (_file != null && _file.exists()) - _file.delete(); + _file.delete(); } - + /** * Only remove tmp files. - * + * * @throws IOException if unable to delete the file */ public void cleanUp() throws IOException @@ -342,7 +342,7 @@ public class MultiPartInputStreamParser _contextTmpDir = contextTmpDir; if (_contextTmpDir == null) _contextTmpDir = new File (System.getProperty("java.io.tmpdir")); - + if (_config == null) _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath()); } @@ -357,7 +357,7 @@ public class MultiPartInputStreamParser return Collections.emptyList(); Collection> values = _parts.values(); - List parts = new ArrayList(); + List parts = new ArrayList<>(); for (List o: values) { List asList = LazyList.getList(o, false); @@ -368,7 +368,7 @@ public class MultiPartInputStreamParser /** * Delete any tmp storage for parts, and clear out the parts list. - * + * * @throws MultiException if unable to delete the parts */ public void deleteParts () @@ -381,22 +381,22 @@ public class MultiPartInputStreamParser try { ((MultiPartInputStreamParser.MultiPart)p).cleanUp(); - } + } catch(Exception e) - { - err.add(e); + { + err.add(e); } } _parts.clear(); - + err.ifExceptionThrowMulti(); } - + /** * Parse, if necessary, the multipart data and return the list of Parts. - * - * @return the parts + * + * @return the parts * @throws IOException if unable to get the parts */ public Collection getParts() @@ -404,7 +404,7 @@ public class MultiPartInputStreamParser { parse(); Collection> values = _parts.values(); - List parts = new ArrayList(); + List parts = new ArrayList<>(); for (List o: values) { List asList = LazyList.getList(o, false); @@ -416,7 +416,7 @@ public class MultiPartInputStreamParser /** * Get the named Part. - * + * * @param name the part name * @return the parts * @throws IOException if unable to get the part @@ -425,13 +425,13 @@ public class MultiPartInputStreamParser throws IOException { parse(); - return (Part)_parts.getValue(name, 0); + return _parts.getValue(name, 0); } /** * Parse, if necessary, the multipart stream. - * + * * @throws IOException if unable to parse */ protected void parse () @@ -443,7 +443,7 @@ 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(); + _parts = new MultiMap<>(); //if its not a multipart request, don't parse it if (_contentType == null || !_contentType.startsWith("multipart/form-data")) @@ -475,28 +475,29 @@ public class MultiPartInputStreamParser bend = (bend < 0? _contentType.length(): bend); contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim()); } - + String boundary="--"+contentTypeBoundary; - byte[] byteBoundary=(boundary+"--").getBytes(StandardCharsets.ISO_8859_1); + String lastBoundary=boundary+"--"; + byte[] byteBoundary=lastBoundary.getBytes(StandardCharsets.ISO_8859_1); // Get first boundary String line = null; try { - line=((ReadLineInputStream)_in).readLine(); + 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)) + while (line != null && !line.equals(boundary) && !line.equals(lastBoundary)) { if (!badFormatLogged) { @@ -510,6 +511,10 @@ public class MultiPartInputStreamParser if (line == null) throw new IOException("Missing initial multi part boundary"); + // Empty multipart. + if (line.equals(lastBoundary)) + return; + // Read each part boolean lastPart=false; @@ -518,20 +523,20 @@ public class MultiPartInputStreamParser String contentDisposition=null; String contentType=null; String contentTransferEncoding=null; - - MultiMap headers = new MultiMap(); + + 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()+")"); @@ -595,7 +600,7 @@ public class MultiPartInputStreamParser part.setContentType(contentType); _parts.add(name, part); part.open(); - + InputStream partInput = null; if ("base64".equalsIgnoreCase(contentTransferEncoding)) { @@ -627,7 +632,7 @@ public class MultiPartInputStreamParser else partInput = _in; - + try { int state=-2; @@ -646,7 +651,7 @@ public class MultiPartInputStreamParser throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")"); state=-2; - + // look for CR and/or LF if(c==13||c==10) { @@ -661,7 +666,7 @@ public class MultiPartInputStreamParser } break; } - + // Look for boundary if(b>=0&&b0&&b0||c==-1) { - + if(b==byteBoundary.length) lastPart=true; if(state==10) state=-2; break; } - + // handle CR LF if(cr) part.write(13); @@ -733,7 +738,7 @@ public class MultiPartInputStreamParser if (!lastPart) throw new IOException("Incomplete parts"); } - + public void setDeleteOnExit(boolean deleteOnExit) { _deleteOnExit = deleteOnExit; @@ -753,8 +758,8 @@ public class MultiPartInputStreamParser String value = nameEqualsValue.substring(idx+1).trim(); return QuotedStringTokenizer.unquoteOnly(value); } - - + + /* ------------------------------------------------------------ */ private String filenameValue(String nameEqualsValue) { @@ -782,7 +787,7 @@ public class MultiPartInputStreamParser return QuotedStringTokenizer.unquoteOnly(value, true); } - + private static class Base64InputStream extends InputStream { @@ -791,7 +796,7 @@ public class MultiPartInputStreamParser byte[] _buffer; int _pos; - + public Base64InputStream(ReadLineInputStream rlis) { _in = rlis; @@ -806,7 +811,7 @@ public class MultiPartInputStreamParser //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(); + _line = _in.readLine(); if (_line==null) return -1; //nothing left if (_line.startsWith("--")) @@ -824,7 +829,7 @@ public class MultiPartInputStreamParser _pos=0; } - + return _buffer[_pos++]; } } From 4134b1eca8f4656f1139006c6d7ba72f32cff79c Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 27 Oct 2015 15:32:26 +0100 Subject: [PATCH 3/3] 419966 - Add ContentProvider that submits multipart/form-data. Implemented this functionality. --- .../client/util/MultiPartContentProvider.java | Bin 0 -> 15796 bytes .../util/MultiPartContentProviderTest.java | 270 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java create mode 100644 jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..2d60c97fa31b4e401e893f6b0bb95be29a6f9adf GIT binary patch literal 15796 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 literal 0 HcmV?d00001 diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java new file mode 100644 index 00000000000..daa91cf7d4f --- /dev/null +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/util/MultiPartContentProviderTest.java @@ -0,0 +1,270 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.client.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +import javax.servlet.MultipartConfigElement; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.Part; + +import org.eclipse.jetty.client.AbstractHttpClientServerTest; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.MavenTestingUtils; +import org.eclipse.jetty.util.Fields; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.Assert; +import org.junit.Test; + +public class MultiPartContentProviderTest extends AbstractHttpClientServerTest +{ + public MultiPartContentProviderTest(SslContextFactory sslContextFactory) + { + super(sslContextFactory); + } + + @Test + public void testEmptyMultiPart() throws Exception + { + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + Assert.assertEquals(0, parts.size()); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testSimpleField() throws Exception + { + String name = "field"; + String value = "value"; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals(value, IO.toString(part.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + multiPart.addPart(new MultiPartContentProvider.FieldPart(name, value, null)); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testFieldWithContentType() throws Exception + { + String name = "field"; + String value = "\u20ac"; + Charset encoding = StandardCharsets.UTF_8; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + String contentType = part.getContentType(); + Assert.assertNotNull(contentType); + int equal = contentType.lastIndexOf('='); + Charset charset = Charset.forName(contentType.substring(equal + 1)); + Assert.assertEquals(encoding, charset); + Assert.assertEquals(value, IO.toString(part.getInputStream(), charset)); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + multiPart.addPart(new MultiPartContentProvider.FieldPart(name, value, encoding)); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + } + + @Test + public void testOnlyFile() throws Exception + { + // Prepare a file to upload. + String data = "multipart_test_\u20ac"; + Path tmpDir = MavenTestingUtils.getTargetTestingPath(); + Path tmpPath = Files.createTempFile(tmpDir, "multipart_", ".txt"); + Charset encoding = StandardCharsets.UTF_8; + try (BufferedWriter writer = Files.newBufferedWriter(tmpPath, encoding, StandardOpenOption.CREATE)) + { + writer.write(data); + } + + String name = "file"; + String contentType = "text/plain; charset=" + encoding.name(); + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + Collection parts = request.getParts(); + Assert.assertEquals(1, parts.size()); + Part part = parts.iterator().next(); + Assert.assertEquals(name, part.getName()); + Assert.assertEquals(contentType, part.getContentType()); + Assert.assertEquals(tmpPath.getFileName().toString(), part.getSubmittedFileName()); + Assert.assertEquals(Files.size(tmpPath), part.getSize()); + Assert.assertEquals(data, IO.toString(part.getInputStream(), encoding)); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + multiPart.addPart(new MultiPartContentProvider.PathPart(name, tmpPath, contentType)); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + + Files.delete(tmpPath); + } + + @Test + public void testFieldWithFile() throws Exception + { + // Prepare a file to upload. + byte[] data = new byte[1024]; + new Random().nextBytes(data); + Path tmpDir = MavenTestingUtils.getTargetTestingPath(); + Path tmpPath = Files.createTempFile(tmpDir, "multipart_", ".txt"); + try (OutputStream output = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE)) + { + output.write(data); + } + + String field = "field"; + String value = "\u20ac"; + String fileField = "file"; + Charset encoding = StandardCharsets.UTF_8; + String contentType = "text/plain; charset=" + encoding.name(); + String headerName = "foo"; + String headerValue = "bar"; + start(new AbstractMultiPartHandler() + { + @Override + protected void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + { + List parts = new ArrayList<>(request.getParts()); + Assert.assertEquals(2, parts.size()); + Part fieldPart = parts.get(0); + Part filePart = parts.get(1); + if (!field.equals(fieldPart.getName())) + { + Part swap = filePart; + filePart = fieldPart; + fieldPart = swap; + } + + Assert.assertEquals(field, fieldPart.getName()); + Assert.assertEquals(contentType, fieldPart.getContentType()); + Assert.assertEquals(value, IO.toString(fieldPart.getInputStream(), encoding)); + Assert.assertEquals(headerValue, fieldPart.getHeader(headerName)); + + Assert.assertEquals(fileField, filePart.getName()); + Assert.assertEquals("application/octet-stream", filePart.getContentType()); + Assert.assertEquals(tmpPath.getFileName().toString(), filePart.getSubmittedFileName()); + Assert.assertEquals(Files.size(tmpPath), filePart.getSize()); + Assert.assertArrayEquals(data, IO.readBytes(filePart.getInputStream())); + } + }); + + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + Fields fields = new Fields(); + fields.put("Content-Type", contentType); + fields.put(headerName, headerValue); + multiPart.addPart(new MultiPartContentProvider.FieldPart(field, encoding.encode(value), fields)); + multiPart.addPart(new MultiPartContentProvider.PathPart(fileField, tmpPath)); + ContentResponse response = client.newRequest("localhost", connector.getLocalPort()) + .scheme(scheme) + .method(HttpMethod.POST) + .content(multiPart) + .send(); + + Assert.assertEquals(200, response.getStatus()); + + Files.delete(tmpPath); + } + + private static abstract class AbstractMultiPartHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + baseRequest.setHandled(true); + File tmpDir = MavenTestingUtils.getTargetTestingDir(); + request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement(tmpDir.getAbsolutePath())); + handle(request, response); + } + + protected abstract void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException; + } +}