You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2017/09/30 21:54:05 UTC

[2/3] httpcomponents-client git commit: HTTPCLIENT-293 Implemented the percent encoding of the filename parameter of the Content-Disposition header based on RFC7578 sections 2 and 4.2. In the new MultipartForm implementation I included a PercentCodec tha

HTTPCLIENT-293 Implemented the percent encoding of the filename parameter of the Content-Disposition header based on RFC7578 sections 2 and 4.2. In the new MultipartForm implementation I included a PercentCodec that performs encoding/decoding to/from the percent encoding as described in RFC7578 and RFC3986.


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/a424709d
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/a424709d
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/a424709d

Branch: refs/heads/master
Commit: a424709d89504aadd7b3d59129902666d79d0c15
Parents: 9560aef
Author: Ioannis Sermetziadis <se...@gmail.com>
Authored: Fri Sep 29 00:12:13 2017 +0300
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Sat Sep 30 23:51:43 2017 +0200

----------------------------------------------------------------------
 .../http/entity/mime/AbstractMultipartForm.java |  18 +-
 .../http/entity/mime/HttpMultipartMode.java     |   4 +-
 .../http/entity/mime/HttpRFC7578Multipart.java  | 192 +++++++++++++++++++
 .../client5/http/entity/mime/MinimalField.java  |   7 +
 .../entity/mime/MultipartEntityBuilder.java     |   6 +
 .../entity/mime/TestMultipartEntityBuilder.java |  29 ++-
 6 files changed, 245 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java
index 0ef2266..b36afc8 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java
@@ -48,7 +48,7 @@ import org.apache.hc.core5.util.ByteArrayBuffer;
  */
 abstract class AbstractMultipartForm {
 
-    private static ByteArrayBuffer encode(
+    static ByteArrayBuffer encode(
             final Charset charset, final String string) {
         final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
         final ByteArrayBuffer bab = new ByteArrayBuffer(encoded.remaining());
@@ -56,24 +56,24 @@ abstract class AbstractMultipartForm {
         return bab;
     }
 
-    private static void writeBytes(
+    static void writeBytes(
             final ByteArrayBuffer b, final OutputStream out) throws IOException {
         out.write(b.array(), 0, b.length());
     }
 
-    private static void writeBytes(
+    static void writeBytes(
             final String s, final Charset charset, final OutputStream out) throws IOException {
         final ByteArrayBuffer b = encode(charset, s);
         writeBytes(b, out);
     }
 
-    private static void writeBytes(
+    static void writeBytes(
             final String s, final OutputStream out) throws IOException {
         final ByteArrayBuffer b = encode(StandardCharsets.ISO_8859_1, s);
         writeBytes(b, out);
     }
 
-    protected static void writeField(
+    static void writeField(
             final MinimalField field, final OutputStream out) throws IOException {
         writeBytes(field.getName(), out);
         writeBytes(FIELD_SEP, out);
@@ -81,7 +81,7 @@ abstract class AbstractMultipartForm {
         writeBytes(CR_LF, out);
     }
 
-    protected static void writeField(
+    static void writeField(
             final MinimalField field, final Charset charset, final OutputStream out) throws IOException {
         writeBytes(field.getName(), charset, out);
         writeBytes(FIELD_SEP, out);
@@ -89,9 +89,9 @@ abstract class AbstractMultipartForm {
         writeBytes(CR_LF, out);
     }
 
-    private static final ByteArrayBuffer FIELD_SEP = encode(StandardCharsets.ISO_8859_1, ": ");
-    private static final ByteArrayBuffer CR_LF = encode(StandardCharsets.ISO_8859_1, "\r\n");
-    private static final ByteArrayBuffer TWO_DASHES = encode(StandardCharsets.ISO_8859_1, "--");
+    static final ByteArrayBuffer FIELD_SEP = encode(StandardCharsets.ISO_8859_1, ": ");
+    static final ByteArrayBuffer CR_LF = encode(StandardCharsets.ISO_8859_1, "\r\n");
+    static final ByteArrayBuffer TWO_DASHES = encode(StandardCharsets.ISO_8859_1, "--");
 
     final Charset charset;
     final String boundary;

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpMultipartMode.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpMultipartMode.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpMultipartMode.java
index c749436..c4f7ceb 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpMultipartMode.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpMultipartMode.java
@@ -38,6 +38,8 @@ public enum HttpMultipartMode {
     /** browser-compatible mode, i.e. only write Content-Disposition; use content charset */
     BROWSER_COMPATIBLE,
     /** RFC 6532 compliant */
-    RFC6532
+    RFC6532,
+    /** RFC 7578 compliant */
+    RFC7578
 
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java
new file mode 100644
index 0000000..e3437f5
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java
@@ -0,0 +1,192 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.entity.mime;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+import java.util.List;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.util.ByteArrayBuffer;
+
+public class HttpRFC7578Multipart extends AbstractMultipartForm {
+
+    private static final PercentCodec PERCENT_CODEC = new PercentCodec();
+
+    private final List<FormBodyPart> parts;
+
+    public HttpRFC7578Multipart(
+        final Charset charset,
+        final String boundary,
+        final List<FormBodyPart> parts) {
+        super(charset, boundary);
+        this.parts = parts;
+    }
+
+    @Override
+    public List<FormBodyPart> getBodyParts() {
+        return parts;
+    }
+
+    @Override
+    protected void formatMultipartHeader(final FormBodyPart part, final OutputStream out) throws IOException {
+        for (final MinimalField field: part.getHeader()) {
+            if (MIME.CONTENT_DISPOSITION.equalsIgnoreCase(field.getName())) {
+                writeBytes(field.getName(), charset, out);
+                writeBytes(FIELD_SEP, out);
+                writeBytes(field.getValue(), out);
+                final List<NameValuePair> parameters = field.getParameters();
+                for (int i = 0; i < parameters.size(); i++) {
+                    final NameValuePair parameter = parameters.get(i);
+                    final String name = parameter.getName();
+                    final String value = parameter.getValue();
+                    writeBytes("; ", out);
+                    writeBytes(name, out);
+                    writeBytes("=\"", out);
+                    if (value != null) {
+                        if (name.equalsIgnoreCase(MIME.FIELD_PARAM_FILENAME)) {
+                            out.write(PERCENT_CODEC.encode(value.getBytes(charset)));
+                        } else {
+                            writeBytes(value, out);
+                        }
+                    }
+                    writeBytes("\"", out);
+                }
+                writeBytes(CR_LF, out);
+            } else {
+                writeField(field, charset, out);
+            }
+        }
+    }
+
+    static class PercentCodec {
+
+        private static final byte ESCAPE_CHAR = '%';
+
+        private static final BitSet ALWAYSENCODECHARS = new BitSet();
+
+        static {
+            ALWAYSENCODECHARS.set(' ');
+            ALWAYSENCODECHARS.set('%');
+        }
+
+        /**
+         * Percent-Encoding implementation based on RFC 3986
+         */
+        public byte[] encode(final byte[] bytes) {
+            if (bytes == null) {
+                return null;
+            }
+
+            final CharsetEncoder characterSetEncoder = StandardCharsets.US_ASCII.newEncoder();
+            final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+            for (final byte c : bytes) {
+                int b = c;
+                if (b < 0) {
+                    b = 256 + b;
+                }
+                if (characterSetEncoder.canEncode((char) b) && !ALWAYSENCODECHARS.get(c)) {
+                    buffer.write(b);
+                } else {
+                    buffer.write(ESCAPE_CHAR);
+                    final char hex1 = Utils.hexDigit(b >> 4);
+                    final char hex2 = Utils.hexDigit(b);
+                    buffer.write(hex1);
+                    buffer.write(hex2);
+                }
+            }
+            return buffer.toByteArray();
+        }
+
+        public byte[] decode(final byte[] bytes) throws DecoderException {
+            if (bytes == null) {
+                return null;
+            }
+            final ByteArrayBuffer buffer = new ByteArrayBuffer(bytes.length);
+            for (int i = 0; i < bytes.length; i++) {
+                final int b = bytes[i];
+                if (b == ESCAPE_CHAR) {
+                    try {
+                        final int u = Utils.digit16(bytes[++i]);
+                        final int l = Utils.digit16(bytes[++i]);
+                        buffer.append((char) ((u << 4) + l));
+                    } catch (final ArrayIndexOutOfBoundsException e) {
+                        throw new DecoderException("Invalid URL encoding: ", e);
+                    }
+                } else {
+                    buffer.append(b);
+                }
+            }
+            return buffer.toByteArray();
+        }
+    }
+
+    static class Utils {
+
+        /**
+         * Radix used in encoding and decoding.
+         */
+        private static final int RADIX = 16;
+
+        /**
+         * Returns the numeric value of the character <code>b</code> in radix 16.
+         *
+         * @param b
+         *            The byte to be converted.
+         * @return The numeric value represented by the character in radix 16.
+         *
+         * @throws DecoderException
+         *             Thrown when the byte is not valid per {@link Character#digit(char,int)}
+         */
+        static int digit16(final byte b) throws DecoderException {
+            final int i = Character.digit((char) b, RADIX);
+            if (i == -1) {
+                throw new DecoderException("Invalid URL encoding: not a valid digit (radix " + RADIX + "): " + b);
+            }
+            return i;
+        }
+
+        /**
+         * Returns the upper case hex digit of the lower 4 bits of the int.
+         *
+         * @param b the input int
+         * @return the upper case hex digit of the lower 4 bits of the int.
+         */
+        static char hexDigit(final int b) {
+            return Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MinimalField.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MinimalField.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MinimalField.java
index ef1e872..437330f 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MinimalField.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MinimalField.java
@@ -69,6 +69,13 @@ public class MinimalField {
         return this.name;
     }
 
+    /**
+     * @since 4.6
+     */
+    public String getValue() {
+        return this.value;
+    }
+
     public String getBody() {
         final StringBuilder sb = new StringBuilder();
         sb.append(this.value);

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java
index 57197ad..441b1c5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java
@@ -216,6 +216,12 @@ public class MultipartEntityBuilder {
             case RFC6532:
                 form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, bodyPartsCopy);
                 break;
+            case RFC7578:
+                if (charsetCopy == null) {
+                    charsetCopy = StandardCharsets.UTF_8;
+                }
+                form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, bodyPartsCopy);
+                break;
             default:
                 form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, bodyPartsCopy);
         }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/a424709d/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java
----------------------------------------------------------------------
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java
index dd779c1..6182899 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java
@@ -135,7 +135,7 @@ public class TestMultipartEntityBuilder {
 
 
         final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        entity.getMultipart().writeTo(out);
+        entity.writeTo(out);
         out.close();
         Assert.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
                 "Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello world\"\r\n" +
@@ -145,5 +145,32 @@ public class TestMultipartEntityBuilder {
                 "hello world\r\n" +
                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
     }
+    @Test
+    public void testMultipartWriteToRFC7578Mode() throws Exception {
+        final List<NameValuePair> parameters = new ArrayList<NameValuePair>();
+        parameters.add(new BasicNameValuePair(MIME.FIELD_PARAM_NAME, "test"));
+        parameters.add(new BasicNameValuePair(MIME.FIELD_PARAM_FILENAME, "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%"));
+
+        final MultipartFormEntity entity = MultipartEntityBuilder.create()
+                .setMode(HttpMultipartMode.RFC7578)
+                .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
+                .addPart(new FormBodyPartBuilder()
+                        .setName("test")
+                        .setBody(new StringBody("hello world", ContentType.TEXT_PLAIN))
+                        .addField("Content-Disposition", "multipart/form-data", parameters)
+                        .build())
+                .buildEntity();
+
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        entity.writeTo(out);
+        out.close();
+        Assert.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
+                "Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello%20%CE%BA%CF%8C%CF%83%CE%BC%CE%B5!%25\"\r\n" +
+                "Content-Type: text/plain; charset=ISO-8859-1\r\n" +
+                "Content-Transfer-Encoding: 8bit\r\n" +
+                "\r\n" +
+                "hello world\r\n" +
+                "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
+    }
 
 }