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.File;
31  import java.io.InputStream;
32  import java.nio.charset.Charset;
33  import java.nio.charset.StandardCharsets;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.List;
37  import java.util.UUID;
38  
39  import org.apache.hc.core5.http.ContentType;
40  import org.apache.hc.core5.http.HttpEntity;
41  import org.apache.hc.core5.http.NameValuePair;
42  import org.apache.hc.core5.http.message.BasicNameValuePair;
43  import org.apache.hc.core5.util.Args;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * Builder for multipart {@link HttpEntity}s.
49   * <p>
50   * This class constructs multipart entities with a boundary determined by either a random UUID
51   * or an explicit boundary set via {@link #setBoundary(String)}.
52   * </p>
53   * <p>
54   *  IMPORTANT: it is responsibility of the caller to validate / sanitize content of body
55   *  parts. For instance, when using an explicit boundary, it's the caller's responsibility to
56   *  ensure the body parts do not contain the boundary value, which can prevent the consumer of
57   *  the entity from correctly parsing / processing the body parts.
58   * </p>
59   *
60   * @since 5.0
61   */
62      public class MultipartEntityBuilder {
63  
64      private ContentType contentType;
65      private HttpMultipartMode mode = HttpMultipartMode.STRICT;
66      private String boundary;
67      private Charset charset;
68      private List<MultipartPart> multipartParts;
69  
70  
71      private static final String BOUNDARY_PREFIX = "httpclient_boundary_";
72  
73      /**
74       * The logger for this class.
75       */
76      private static final Logger LOG = LoggerFactory.getLogger(MultipartEntityBuilder.class);
77  
78  
79      /**
80       * The preamble of the multipart message.
81       * This field stores the optional preamble that should be added at the beginning of the multipart message.
82       * It can be {@code null} if no preamble is needed.
83       */
84      private String preamble;
85  
86      /**
87       * The epilogue of the multipart message.
88       * This field stores the optional epilogue that should be added at the end of the multipart message.
89       * It can be {@code null} if no epilogue is needed.
90       */
91      private String epilogue;
92  
93      /**
94       * An empty immutable {@code NameValuePair} array.
95       */
96      private static final NameValuePair[] EMPTY_NAME_VALUE_ARRAY = {};
97  
98      public static MultipartEntityBuilder create() {
99          return new MultipartEntityBuilder();
100     }
101 
102     MultipartEntityBuilder() {
103     }
104 
105     public MultipartEntityBuilder setMode(final HttpMultipartMode mode) {
106         this.mode = mode;
107         return this;
108     }
109 
110     public MultipartEntityBuilder setLaxMode() {
111         this.mode = HttpMultipartMode.LEGACY;
112         return this;
113     }
114 
115     public MultipartEntityBuilder setStrictMode() {
116         this.mode = HttpMultipartMode.STRICT;
117         return this;
118     }
119 
120     /**
121      * Sets a custom boundary string for the multipart entity.
122      * <p>
123      * If {@code null} is provided, the builder reverts to its default logic of using a random UUID.
124      * </p>
125      * <p>
126      * IMPORTANT: when setting an explicit boundary, it is responsibility of the caller to validate / sanitize content
127      * of body parts to ensure they do not contain the boundary value.
128      * </p>
129      *
130      * @param boundary the boundary string, or {@code null} to use a random UUID.
131      * @return this builder instance
132      */
133     public MultipartEntityBuilder setBoundary(final String boundary) {
134         this.boundary = boundary;
135         return this;
136     }
137 
138     /**
139      * @since 4.4
140      */
141     public MultipartEntityBuilder setMimeSubtype(final String subType) {
142         Args.notBlank(subType, "MIME subtype");
143         this.contentType = ContentType.create("multipart/" + subType);
144         return this;
145     }
146 
147     /**
148      * @return this instance.
149      * @since 4.5
150      */
151     public MultipartEntityBuilder setContentType(final ContentType contentType) {
152         Args.notNull(contentType, "Content type");
153         this.contentType = contentType;
154         return this;
155     }
156     /**
157      *  Add parameter to the current {@link ContentType}.
158      *
159      * @param parameter The name-value pair parameter to add to the {@link ContentType}.
160      * @return this instance.
161      * @since 5.2
162      */
163     public MultipartEntityBuilder addParameter(final BasicNameValuePair parameter) {
164         this.contentType = contentType.withParameters(parameter);
165         return this;
166     }
167 
168     public MultipartEntityBuilder setCharset(final Charset charset) {
169         this.charset = charset;
170         return this;
171     }
172 
173     /**
174      * @since 4.4
175      */
176     public MultipartEntityBuilder addPart(final MultipartPart multipartPart) {
177         if (multipartPart == null) {
178             return this;
179         }
180         if (this.multipartParts == null) {
181             this.multipartParts = new ArrayList<>();
182         }
183         this.multipartParts.add(multipartPart);
184         return this;
185     }
186 
187     public MultipartEntityBuilder addPart(final String name, final ContentBody contentBody) {
188         Args.notNull(name, "Name");
189         Args.notNull(contentBody, "Content body");
190         return addPart(FormBodyPartBuilder.create(name, contentBody).build());
191     }
192 
193     public MultipartEntityBuilder addTextBody(
194             final String name, final String text, final ContentType contentType) {
195         return addPart(name, new StringBody(text, contentType));
196     }
197 
198     public MultipartEntityBuilder addTextBody(
199             final String name, final String text) {
200         return addTextBody(name, text, ContentType.DEFAULT_TEXT);
201     }
202 
203     public MultipartEntityBuilder addBinaryBody(
204             final String name, final byte[] b, final ContentType contentType, final String filename) {
205         return addPart(name, new ByteArrayBody(b, contentType, filename));
206     }
207 
208     public MultipartEntityBuilder addBinaryBody(
209             final String name, final byte[] b) {
210         return addPart(name, new ByteArrayBody(b, ContentType.DEFAULT_BINARY));
211     }
212 
213     public MultipartEntityBuilder addBinaryBody(
214             final String name, final File file, final ContentType contentType, final String filename) {
215         return addPart(name, new FileBody(file, contentType, filename));
216     }
217 
218     public MultipartEntityBuilder addBinaryBody(
219             final String name, final File file) {
220         return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
221     }
222 
223     public MultipartEntityBuilder addBinaryBody(
224             final String name, final InputStream stream, final ContentType contentType,
225             final String filename) {
226         return addPart(name, new InputStreamBody(stream, contentType, filename));
227     }
228 
229     public MultipartEntityBuilder addBinaryBody(final String name, final InputStream stream) {
230         return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
231     }
232 
233     /**
234      * Generates a random boundary using UUID. The UUID is a v4 random UUID generated from a cryptographically-secure
235      * random source.
236      * <p>
237      * A cryptographically-secure random number source is used to generate the UUID, to avoid a malicious actor crafting
238      * a body part that contains the boundary value to tamper with the entity structure.
239      * </p>
240      */
241     private String getRandomBoundary() {
242         return BOUNDARY_PREFIX + UUID.randomUUID();
243     }
244 
245     /**
246      * Adds a preamble to the multipart entity being constructed. The preamble is the text that appears before the first
247      * boundary delimiter. The preamble is optional and may be null.
248      *
249      * @param preamble The preamble text to add to the multipart entity
250      * @return this instance.
251      *
252      * @since 5.3
253      */
254     public MultipartEntityBuilder addPreamble(final String preamble) {
255         this.preamble = preamble;
256         return this;
257     }
258 
259     /**
260      * Adds an epilogue to the multipart entity being constructed. The epilogue is the text that appears after the last
261      * boundary delimiter. The epilogue is optional and may be null.
262      *
263      * @param epilogue The epilogue text to add to the multipart entity
264      * @return this instance.
265      * @since 5.3
266      */
267     public MultipartEntityBuilder addEpilogue(final String epilogue) {
268         this.epilogue = epilogue;
269         return this;
270     }
271 
272     MultipartFormEntity buildEntity() {
273         String boundaryCopy = boundary;
274         if (boundaryCopy == null && contentType != null) {
275             boundaryCopy = contentType.getParameter("boundary");
276         }
277         if (boundaryCopy == null) {
278             boundaryCopy = getRandomBoundary();
279         }
280         Charset charsetCopy = charset;
281         if (charsetCopy == null && contentType != null) {
282             charsetCopy = contentType.getCharset();
283         }
284         final NameValuePair[] params = new NameValuePair[]{new BasicNameValuePair("boundary", boundaryCopy)};
285 
286         final ContentType contentTypeCopy;
287         if (contentType != null) {
288             contentTypeCopy = contentType.withParameters(params);
289         } else {
290             boolean formData = false;
291             if (multipartParts != null) {
292                 for (final MultipartPart multipartPart : multipartParts) {
293                     if (multipartPart instanceof FormBodyPart) {
294                         formData = true;
295                         break;
296                     }
297                 }
298             }
299 
300             if (formData) {
301                 contentTypeCopy = ContentType.MULTIPART_FORM_DATA.withParameters(params);
302             } else {
303                 contentTypeCopy = ContentType.create("multipart/mixed", params);
304             }
305         }
306         final List<MultipartPart> multipartPartsCopy = multipartParts != null ? new ArrayList<>(multipartParts) :
307                 Collections.emptyList();
308         final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT;
309         final AbstractMultipartFormat form;
310         switch (modeCopy) {
311             case LEGACY:
312                 form = new LegacyMultipart(charsetCopy, boundaryCopy, multipartPartsCopy);
313                 break;
314             case EXTENDED:
315                 if (contentTypeCopy.isSameMimeType(ContentType.MULTIPART_FORM_DATA)) {
316                     if (charsetCopy == null) {
317                         charsetCopy = StandardCharsets.UTF_8;
318                     }
319                     form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue, modeCopy);
320                 } else {
321                     form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue);
322                 }
323                 break;
324             default:
325                 form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy, preamble, epilogue);
326         }
327         return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
328     }
329 
330     public HttpEntity build() {
331         return buildEntity();
332     }
333 
334 }