From 709be30442ff160eb7571bcf340da1e347f198bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20G=2E=20Stan?= Date: Sun, 22 Dec 2013 03:25:00 +0200 Subject: [PATCH] Add an HTML5 encoder mode for HttpPostRequestEncoder --- .../multipart/HttpPostRequestEncoder.java | 34 ++- .../multipart/HttpPostRequestEncoderTest.java | 219 ++++++++++++++++++ codec-http/src/test/resources/file-01.txt | 1 + codec-http/src/test/resources/file-02.txt | 1 + 4 files changed, 245 insertions(+), 10 deletions(-) create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java create mode 100644 codec-http/src/test/resources/file-01.txt create mode 100644 codec-http/src/test/resources/file-02.txt diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java index 8fbbfb77a1..45dd7d8339 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoder.java @@ -54,15 +54,26 @@ public class HttpPostRequestEncoder implements ChunkedInput { */ public enum EncoderMode { /** - * Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use - * {@link EncoderMode#RFC3986}. The W3C form recommentations this for submitting post form data. + * Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use + * {@link EncoderMode#RFC3986}. The W3C form recommentations this for submitting post form data. */ RFC1738, /** * Mode which is more new and is used for OAUTH */ - RFC3986 + RFC3986, + + /** + * The HTML5 spec disallows mixed mode in multipart/form-data + * requests. More concretely this means that more files submitted + * under the same name will not be encoded using mixed mode, but + * will be treated as distinct fields. + * + * Reference: + * http://www.w3.org/TR/html5/forms.html#multipart-form-data + */ + HTML5 } private static final Map percentEncodings = new HashMap(); @@ -371,7 +382,7 @@ public class HttpPostRequestEncoder implements ChunkedInput { } /** - * Add a series of Files associated with one File parameter (implied Mixed mode in Multipart) + * Add a series of Files associated with one File parameter * * @param name * the name of the parameter @@ -533,7 +544,8 @@ public class HttpPostRequestEncoder implements ChunkedInput { duringMixedMode = false; } } else { - if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) { + if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null + && currentFileUpload.getName().equals(fileUpload.getName())) { // create a new mixed mode (from previous file) // change multipart body header of previous file into @@ -552,21 +564,23 @@ public class HttpPostRequestEncoder implements ChunkedInput { // * Content-Type: multipart/mixed; boundary=BbC04y // * // * --BbC04y - // * Content-Disposition: file; filename="file1.txt" + // * Content-Disposition: attachment; filename="file1.txt" // Content-Type: text/plain initMixedMultipart(); InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas .size() - 2); // remove past size globalBodySize -= pastAttribute.size(); - String replacement = HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FORM_DATA + String replacement = "--" + multipartDataBoundary + "\r\n"; + replacement += HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FORM_DATA + "; " + HttpPostBodyUtil.NAME + "=\"" + fileUpload.getName() + "\"\r\n"; replacement += HttpHeaders.Names.CONTENT_TYPE + ": " + HttpPostBodyUtil.MULTIPART_MIXED + "; " + HttpHeaders.Values.BOUNDARY + '=' + multipartMixedBoundary + "\r\n\r\n"; replacement += "--" + multipartMixedBoundary + "\r\n"; - replacement += HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FILE + "; " + replacement += HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.ATTACHMENT + "; " + HttpPostBodyUtil.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"; pastAttribute.setValue(replacement, 1); + pastAttribute.setValue("", 2); // update past size globalBodySize += pastAttribute.size(); @@ -590,8 +604,8 @@ public class HttpPostRequestEncoder implements ChunkedInput { // add mixedmultipart delimiter, mixedmultipart body header and // Data to multipart list internal.addValue("--" + multipartMixedBoundary + "\r\n"); - // Content-Disposition: file; filename="file1.txt" - internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.FILE + "; " + // Content-Disposition: attachment; filename="file1.txt" + internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " + HttpPostBodyUtil.ATTACHMENT + "; " + HttpPostBodyUtil.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n"); } else { internal.addValue("--" + multipartDataBoundary + "\r\n"); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java new file mode 100644 index 0000000000..3e478c2b94 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/multipart/HttpPostRequestEncoderTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2013 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.multipart; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.List; + +import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder.EncoderMode; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.CharsetUtil; + +/** {@link HttpPostRequestEncoder} test case. */ +public class HttpPostRequestEncoderTest { + + @Test + public void testSingleFileUpload() throws Exception { + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, "http://localhost"); + + HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(request, true); + File file1 = new File(getClass().getResource("/file-01.txt").toURI()); + encoder.addBodyAttribute("foo", "bar"); + encoder.addBodyFileUpload("quux", file1, "text/plain", false); + + String multipartDataBoundary = getEncoderField(encoder, "multipartDataBoundary"); + String content = getRequestBody(encoder); + + String expected = "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"foo\"" + "\r\n" + + "Content-Type: text/plain; charset=UTF-8" + "\r\n" + + "\r\n" + + "bar" + + "\r\n" + + "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 01\n" + + "\r\n" + + "--" + multipartDataBoundary + "--" + "\r\n"; + + assertEquals(expected, content); + } + + @Test + public void testMultiFileUploadInMixedMode() throws Exception { + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, "http://localhost"); + + HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(request, true); + File file1 = new File(getClass().getResource("/file-01.txt").toURI()); + File file2 = new File(getClass().getResource("/file-02.txt").toURI()); + encoder.addBodyAttribute("foo", "bar"); + encoder.addBodyFileUpload("quux", file1, "text/plain", false); + encoder.addBodyFileUpload("quux", file2, "text/plain", false); + + // We have to query the value of these two fields before finalizing + // the request, which unsets one of them. + String multipartDataBoundary = getEncoderField(encoder, "multipartDataBoundary"); + String multipartMixedBoundary = getEncoderField(encoder, "multipartMixedBoundary"); + String content = getRequestBody(encoder); + + String expected = "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"foo\"" + "\r\n" + + "Content-Type: text/plain; charset=UTF-8" + "\r\n" + + "\r\n" + + "bar" + "\r\n" + + "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"quux\"" + "\r\n" + + "Content-Type: multipart/mixed; boundary=" + multipartMixedBoundary + "\r\n" + + "\r\n" + + "--" + multipartMixedBoundary + "\r\n" + + "Content-Disposition: attachment; filename=\"file-02.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 01\n" + + "\r\n" + + "--" + multipartMixedBoundary + "\r\n" + + "Content-Disposition: attachment; filename=\"file-02.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 02\n" + + "\r\n" + + "--" + multipartMixedBoundary + "--" + "\r\n" + + "--" + multipartDataBoundary + "--" + "\r\n"; + + assertEquals(expected, content); + } + + @Test + public void testSinleFileUploadInHtml5Mode() throws Exception { + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, "http://localhost"); + + DefaultHttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); + + HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(factory, + request, true, CharsetUtil.UTF_8, EncoderMode.HTML5); + File file1 = new File(getClass().getResource("/file-01.txt").toURI()); + File file2 = new File(getClass().getResource("/file-02.txt").toURI()); + encoder.addBodyAttribute("foo", "bar"); + encoder.addBodyFileUpload("quux", file1, "text/plain", false); + encoder.addBodyFileUpload("quux", file2, "text/plain", false); + + String multipartDataBoundary = getEncoderField(encoder, "multipartDataBoundary"); + String content = getRequestBody(encoder); + + String expected = "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"foo\"" + "\r\n" + + "Content-Type: text/plain; charset=UTF-8" + "\r\n" + + "\r\n" + + "bar" + "\r\n" + + "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 01\n" + "\r\n" + + "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"quux\"; filename=\"file-02.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 02\n" + + "\r\n" + + "--" + multipartDataBoundary + "--" + "\r\n"; + + assertEquals(expected, content); + } + + @Test + public void testMultiFileUploadInHtml5Mode() throws Exception { + DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, + HttpMethod.POST, "http://localhost"); + + DefaultHttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); + + HttpPostRequestEncoder encoder = new HttpPostRequestEncoder(factory, + request, true, CharsetUtil.UTF_8, EncoderMode.HTML5); + File file1 = new File(getClass().getResource("/file-01.txt").toURI()); + encoder.addBodyAttribute("foo", "bar"); + encoder.addBodyFileUpload("quux", file1, "text/plain", false); + + String multipartDataBoundary = getEncoderField(encoder, "multipartDataBoundary"); + String content = getRequestBody(encoder); + + String expected = "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"foo\"" + "\r\n" + + "Content-Type: text/plain; charset=UTF-8" + "\r\n" + + "\r\n" + + "bar" + + "\r\n" + + "--" + multipartDataBoundary + "\r\n" + + "Content-Disposition: form-data; name=\"quux\"; filename=\"file-01.txt\"" + "\r\n" + + "Content-Type: text/plain" + "\r\n" + + "Content-Transfer-Encoding: binary" + "\r\n" + + "\r\n" + + "File 01\n" + + "\r\n" + + "--" + multipartDataBoundary + "--" + "\r\n"; + + assertEquals(expected, content); + } + + private String getRequestBody(HttpPostRequestEncoder encoder) throws Exception { + encoder.finalizeRequest(); + + List chunks = getEncoderField(encoder, "multipartHttpDatas"); + ByteBuf[] buffers = new ByteBuf[chunks.size()]; + + for (int i = 0; i < buffers.length; i++) { + InterfaceHttpData data = chunks.get(i); + if (data instanceof InternalAttribute) { + buffers[i] = ((InternalAttribute) data).toByteBuf(); + } else if (data instanceof HttpData) { + buffers[i] = ((HttpData) data).getByteBuf(); + } + } + + return Unpooled.wrappedBuffer(buffers).toString(CharsetUtil.UTF_8); + } + + private A getEncoderField(HttpPostRequestEncoder encoder, String fieldName) throws Exception { + return this.getField(encoder, HttpPostRequestEncoder.class, fieldName); + } + + @SuppressWarnings("unchecked") + private A getField(T instance, Class klass, String fieldName) throws Exception { + Field requestField = klass.getDeclaredField(fieldName); + requestField.setAccessible(true); + return (A) requestField.get(instance); + } +} diff --git a/codec-http/src/test/resources/file-01.txt b/codec-http/src/test/resources/file-01.txt new file mode 100644 index 0000000000..a94c45f2bf --- /dev/null +++ b/codec-http/src/test/resources/file-01.txt @@ -0,0 +1 @@ +File 01 diff --git a/codec-http/src/test/resources/file-02.txt b/codec-http/src/test/resources/file-02.txt new file mode 100644 index 0000000000..e2e0c12c45 --- /dev/null +++ b/codec-http/src/test/resources/file-02.txt @@ -0,0 +1 @@ +File 02