View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.entity.mime;
29  
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.File;
33  import java.nio.charset.StandardCharsets;
34  import java.util.ArrayList;
35  import java.util.List;
36  
37  import org.apache.hc.core5.http.ContentType;
38  import org.apache.hc.core5.http.HeaderElement;
39  import org.apache.hc.core5.http.NameValuePair;
40  import org.apache.hc.core5.http.message.BasicHeaderValueParser;
41  import org.apache.hc.core5.http.message.BasicNameValuePair;
42  import org.apache.hc.core5.http.message.ParserCursor;
43  import org.junit.jupiter.api.Assertions;
44  import org.junit.jupiter.api.Test;
45  
46  class TestMultipartEntityBuilder {
47  
48      @Test
49      void testBasics() {
50          final MultipartFormEntity entity = MultipartEntityBuilder.create().buildEntity();
51          Assertions.assertNotNull(entity);
52          Assertions.assertTrue(entity.getMultipart() instanceof HttpStrictMultipart);
53          Assertions.assertEquals(0, entity.getMultipart().getParts().size());
54      }
55  
56      @Test
57      void testMultipartOptions() {
58          final MultipartFormEntity entity = MultipartEntityBuilder.create()
59                  .setBoundary("blah-blah")
60                  .setCharset(StandardCharsets.UTF_8)
61                  .setLaxMode()
62                  .buildEntity();
63          Assertions.assertNotNull(entity);
64          Assertions.assertTrue(entity.getMultipart() instanceof LegacyMultipart);
65          Assertions.assertEquals("blah-blah", entity.getMultipart().boundary);
66          Assertions.assertEquals(StandardCharsets.UTF_8, entity.getMultipart().charset);
67      }
68  
69      @Test
70      void testAddBodyParts() {
71          final MultipartFormEntity entity = MultipartEntityBuilder.create()
72                  .addTextBody("p1", "stuff")
73                  .addBinaryBody("p2", new File("stuff"))
74                  .addBinaryBody("p3", new byte[]{})
75                  .addBinaryBody("p4", new ByteArrayInputStream(new byte[]{}))
76                  .addBinaryBody("p5", new ByteArrayInputStream(new byte[]{}), ContentType.DEFAULT_BINARY, "filename")
77                  .buildEntity();
78          Assertions.assertNotNull(entity);
79          final List<MultipartPart> bodyParts = entity.getMultipart().getParts();
80          Assertions.assertNotNull(bodyParts);
81          Assertions.assertEquals(5, bodyParts.size());
82      }
83  
84  
85      @Test
86      void testMultipartCustomContentType() {
87          final MultipartFormEntity entity = MultipartEntityBuilder.create()
88                  .setContentType(ContentType.APPLICATION_XML)
89                  .setBoundary("blah-blah")
90                  .setCharset(StandardCharsets.UTF_8)
91                  .setLaxMode()
92                  .buildEntity();
93          Assertions.assertNotNull(entity);
94          Assertions.assertEquals("application/xml; charset=UTF-8; boundary=blah-blah", entity.getContentType());
95      }
96  
97      @Test
98      void testMultipartContentTypeParameter() {
99          final MultipartFormEntity entity = MultipartEntityBuilder.create()
100                 .setContentType(ContentType.MULTIPART_FORM_DATA.withParameters(
101                         new BasicNameValuePair("boundary", "yada-yada"),
102                         new BasicNameValuePair("charset", "ascii")))
103                 .buildEntity();
104         Assertions.assertNotNull(entity);
105         Assertions.assertEquals("multipart/form-data; boundary=yada-yada; charset=ascii", entity.getContentType());
106         Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
107         Assertions.assertEquals(StandardCharsets.US_ASCII, entity.getMultipart().charset);
108     }
109 
110     @Test
111     void testMultipartDefaultContentTypeOmitsCharset() {
112         final MultipartFormEntity entity = MultipartEntityBuilder.create()
113                 .setCharset(StandardCharsets.UTF_8)
114                 .setBoundary("yada-yada")
115                 .buildEntity();
116         Assertions.assertNotNull(entity);
117         Assertions.assertEquals("multipart/mixed; boundary=yada-yada", entity.getContentType());
118         Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
119     }
120 
121     @Test
122     void testMultipartFormDataContentTypeOmitsCharset() {
123         // Note: org.apache.hc.core5.http.ContentType.MULTIPART_FORM_DATA uses StandardCharsets.ISO_8859_1,
124         // so we create a custom ContentType here
125         final MultipartFormEntity entity = MultipartEntityBuilder.create()
126                 .setContentType(ContentType.create("multipart/form-data"))
127                 .setCharset(StandardCharsets.UTF_8)
128                 .setBoundary("yada-yada")
129                 .buildEntity();
130         Assertions.assertNotNull(entity);
131         Assertions.assertEquals("multipart/form-data; boundary=yada-yada", entity.getContentType());
132         Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
133     }
134 
135     @Test
136     void testMultipartCustomContentTypeParameterOverrides() {
137         final MultipartFormEntity entity = MultipartEntityBuilder.create()
138                 .setContentType(ContentType.MULTIPART_FORM_DATA.withParameters(
139                         new BasicNameValuePair("boundary", "yada-yada"),
140                         new BasicNameValuePair("charset", "ascii"),
141                         new BasicNameValuePair("my", "stuff")))
142                 .setBoundary("blah-blah")
143                 .setCharset(StandardCharsets.UTF_8)
144                 .setLaxMode()
145                 .buildEntity();
146         Assertions.assertNotNull(entity);
147         Assertions.assertEquals("multipart/form-data; boundary=blah-blah; charset=ascii; my=stuff",
148                 entity.getContentType());
149     }
150 
151     @Test
152     void testMultipartCustomContentTypeUsingAddParameter() {
153         final MultipartEntityBuilder eb = MultipartEntityBuilder.create();
154         eb.setMimeSubtype("related");
155         eb.addParameter(new BasicNameValuePair("boundary", "yada-yada"));
156         eb.addParameter(new BasicNameValuePair("charset", "ascii"));
157         eb.addParameter(new BasicNameValuePair("my", "stuff"));
158         eb.buildEntity();
159         final MultipartFormEntity entity = eb.buildEntity();
160         Assertions.assertNotNull(entity);
161         Assertions.assertEquals("multipart/related; boundary=yada-yada; charset=ascii; my=stuff",
162                 entity.getContentType());
163     }
164 
165     @Test
166     void testMultipartWriteTo() throws Exception {
167         final String helloWorld = "hello world";
168         final List<NameValuePair> parameters = new ArrayList<>();
169         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
170         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld));
171         final MultipartFormEntity entity = MultipartEntityBuilder.create()
172                 .setStrictMode()
173                 .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
174                 .addPart(new FormBodyPartBuilder()
175                         .setName("test")
176                         .setBody(new StringBody("hello world", ContentType.TEXT_PLAIN))
177                         .addField("Content-Disposition", "multipart/form-data", parameters)
178                         .build())
179                 .buildEntity();
180 
181 
182         final ByteArrayOutputStream out = new ByteArrayOutputStream();
183         entity.writeTo(out);
184         out.close();
185         Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
186                 "Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello world\"\r\n" +
187                 "Content-Type: text/plain; charset=UTF-8\r\n" +
188                 "\r\n" +
189                 helloWorld + "\r\n" +
190                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
191     }
192 
193     @Test
194     void testMultipartWriteToRFC7578Mode() throws Exception {
195         final String helloWorld = "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%";
196         final List<NameValuePair> parameters = new ArrayList<>();
197         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
198         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld));
199 
200         final MultipartFormEntity entity = MultipartEntityBuilder.create()
201                 .setMode(HttpMultipartMode.EXTENDED)
202                 .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
203                 .addPart(new FormBodyPartBuilder()
204                         .setName("test")
205                         .setBody(new StringBody(helloWorld, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)))
206                         .addField("Content-Disposition", "multipart/form-data", parameters)
207                         .build())
208                 .buildEntity();
209 
210         final ByteArrayOutputStream out = new ByteArrayOutputStream();
211         entity.writeTo(out);
212         out.close();
213         Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
214                 "Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello%20%CE%BA%CF%8C%CF%83%CE%BC%CE%B5!%25\"\r\n" +
215                 "Content-Type: text/plain; charset=UTF-8\r\n" +
216                 "\r\n" +
217                 "hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\r\n" +
218                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name()));
219     }
220 
221     @Test
222     void testMultipartWriteToRFC6532Mode() throws Exception {
223         final String helloWorld = "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%";
224         final List<NameValuePair> parameters = new ArrayList<>();
225         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
226         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld));
227 
228         final MultipartFormEntity entity = MultipartEntityBuilder.create()
229                 .setMode(HttpMultipartMode.EXTENDED)
230                 .setContentType(ContentType.create("multipart/other"))
231                 .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
232                 .addPart(new FormBodyPartBuilder()
233                         .setName("test")
234                         .setBody(new StringBody(helloWorld, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)))
235                         .addField("Content-Disposition", "multipart/form-data", parameters)
236                         .build())
237                 .buildEntity();
238 
239         final ByteArrayOutputStream out = new ByteArrayOutputStream();
240         entity.writeTo(out);
241         out.close();
242         Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
243                 "Content-Disposition: multipart/form-data; name=\"test\"; " +
244                 "filename=\"hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\"\r\n" +
245                 "Content-Type: text/plain; charset=UTF-8\r\n" +
246                 "\r\n" +
247                 "hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\r\n" +
248                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name()));
249     }
250 
251     @Test
252     void testMultipartWriteToWithPreambleAndEpilogue() throws Exception {
253         final String helloWorld = "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%";
254         final List<NameValuePair> parameters = new ArrayList<>();
255         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
256         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld));
257 
258         final MultipartFormEntity entity = MultipartEntityBuilder.create()
259                 .setMode(HttpMultipartMode.EXTENDED)
260                 .setContentType(ContentType.create("multipart/other"))
261                 .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
262                 .addPart(new FormBodyPartBuilder()
263                         .setName("test")
264                         .setBody(new StringBody(helloWorld, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)))
265                         .addField("Content-Disposition", "multipart/form-data", parameters)
266                         .build())
267                 .addPreamble("This is the preamble.")
268                 .addEpilogue("This is the epilogue.")
269                 .buildEntity();
270 
271         final ByteArrayOutputStream out = new ByteArrayOutputStream();
272         entity.writeTo(out);
273         out.close();
274         Assertions.assertEquals("This is the preamble.\r\n" +
275                 "--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
276                 "Content-Disposition: multipart/form-data; name=\"test\"; " +
277                 "filename=\"hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\"\r\n" +
278                 "Content-Type: text/plain; charset=UTF-8\r\n" +
279                 "\r\n" +
280                 "hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\r\n" +
281                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n" +
282                 "This is the epilogue.\r\n", out.toString(StandardCharsets.ISO_8859_1.name()));
283     }
284 
285     @Test
286     void testMultipartWriteToRFC7578ModeWithFilenameStar() throws Exception {
287         final String helloWorld = "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%";
288         final List<NameValuePair> parameters = new ArrayList<>();
289         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
290         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME_START, helloWorld));
291 
292         final MultipartFormEntity entity = MultipartEntityBuilder.create()
293                 .setMode(HttpMultipartMode.EXTENDED)
294                 .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
295                 .addPart(new FormBodyPartBuilder()
296                         .setName("test")
297                         .setBody(new StringBody(helloWorld, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)))
298                         .addField("Content-Disposition", "multipart/form-data", parameters)
299                         .build())
300                 .buildEntity();
301 
302         final ByteArrayOutputStream out = new ByteArrayOutputStream();
303         entity.writeTo(out);
304         out.close();
305         Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
306                 "Content-Disposition: multipart/form-data; name=\"test\"; filename*=\"UTF-8''hello%20%CE%BA%CF%8C%CF%83%CE%BC%CE%B5!%25\"\r\n" +
307                 "Content-Type: text/plain; charset=UTF-8\r\n" +
308                 "\r\n" +
309                 "hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\r\n" +
310                 "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name()));
311     }
312 
313     @Test
314     void testRandomBoundary() {
315         final MultipartFormEntity entity = MultipartEntityBuilder.create()
316                 .buildEntity();
317         final NameValuePair boundaryParam = extractBoundary(entity.getContentType(), "multipart/mixed");
318         final String boundary = boundaryParam.getValue();
319         Assertions.assertNotNull(boundary);
320         Assertions.assertEquals(56, boundary.length());
321         Assertions.assertTrue(boundary.startsWith("httpclient_boundary_"));
322         Assertions.assertTrue(boundary.substring(20).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"));
323     }
324 
325     @Test
326     void testRandomBoundaryWriteTo() throws Exception {
327         final String helloWorld = "hello world";
328         final List<NameValuePair> parameters = new ArrayList<>();
329         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test"));
330         parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld));
331         final MultipartFormEntity entity = MultipartEntityBuilder.create()
332                 .setStrictMode()
333                 .addPart(new FormBodyPartBuilder()
334                         .setName("test")
335                         .setBody(new StringBody("hello world", ContentType.TEXT_PLAIN))
336                         .addField("Content-Disposition", "multipart/form-data", parameters)
337                         .build())
338                 .buildEntity();
339 
340         final NameValuePair boundaryParam = extractBoundary(entity.getContentType(), "multipart/form-data");
341         final String boundary = boundaryParam.getValue();
342         Assertions.assertNotNull(boundary);
343         Assertions.assertEquals(56, boundary.length());
344         Assertions.assertTrue(boundary.startsWith("httpclient_boundary_"));
345         Assertions.assertTrue(boundary.substring(20).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"));
346 
347         final ByteArrayOutputStream out = new ByteArrayOutputStream();
348         entity.writeTo(out);
349         out.close();
350         Assertions.assertEquals("--" + boundary + "\r\n" +
351                 "Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello world\"\r\n" +
352                 "Content-Type: text/plain; charset=UTF-8\r\n" +
353                 "\r\n" +
354                 helloWorld + "\r\n" +
355                 "--" + boundary + "--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
356     }
357 
358     private NameValuePair extractBoundary(final String contentType, final String expectedName) {
359         final BasicHeaderValueParser parser = BasicHeaderValueParser.INSTANCE;
360         final ParserCursor cursor = new ParserCursor(0, contentType.length());
361         final HeaderElement elem = parser.parseHeaderElement(contentType, cursor);
362         Assertions.assertEquals(expectedName, elem.getName());
363         return elem.getParameterByName("boundary");
364     }
365 
366 }