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 00000000000..2d60c97fa31 Binary files /dev/null and b/jetty-client/src/main/java/org/eclipse/jetty/client/util/MultiPartContentProvider.java differ 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; + } +} 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))); + } } } } 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++]; } }