1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
124
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 }