1 /* 2 * Copyright (C) 2014 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.okhttp; 17 18 import com.squareup.okhttp.internal.Util; 19 import java.io.IOException; 20 import java.util.ArrayList; 21 import java.util.List; 22 import java.util.UUID; 23 import okio.Buffer; 24 import okio.BufferedSink; 25 import okio.ByteString; 26 27 /** 28 * Fluent API to build <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC 29 * 2387</a>-compliant request bodies. 30 */ 31 public final class MultipartBuilder { 32 /** 33 * The "mixed" subtype of "multipart" is intended for use when the body 34 * parts are independent and need to be bundled in a particular order. Any 35 * "multipart" subtypes that an implementation does not recognize must be 36 * treated as being of subtype "mixed". 37 */ 38 public static final MediaType MIXED = MediaType.parse("multipart/mixed"); 39 40 /** 41 * The "multipart/alternative" type is syntactically identical to 42 * "multipart/mixed", but the semantics are different. In particular, each 43 * of the body parts is an "alternative" version of the same information. 44 */ 45 public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative"); 46 47 /** 48 * This type is syntactically identical to "multipart/mixed", but the 49 * semantics are different. In particular, in a digest, the default {@code 50 * Content-Type} value for a body part is changed from "text/plain" to 51 * "message/rfc822". 52 */ 53 public static final MediaType DIGEST = MediaType.parse("multipart/digest"); 54 55 /** 56 * This type is syntactically identical to "multipart/mixed", but the 57 * semantics are different. In particular, in a parallel entity, the order 58 * of body parts is not significant. 59 */ 60 public static final MediaType PARALLEL = MediaType.parse("multipart/parallel"); 61 62 /** 63 * The media-type multipart/form-data follows the rules of all multipart 64 * MIME data streams as outlined in RFC 2046. In forms, there are a series 65 * of fields to be supplied by the user who fills out the form. Each field 66 * has a name. Within a given form, the names are unique. 67 */ 68 public static final MediaType FORM = MediaType.parse("multipart/form-data"); 69 70 private static final byte[] COLONSPACE = { ':', ' ' }; 71 private static final byte[] CRLF = { '\r', '\n' }; 72 private static final byte[] DASHDASH = { '-', '-' }; 73 74 private final ByteString boundary; 75 private MediaType type = MIXED; 76 77 // Parallel lists of nullable headers and non-null bodies. 78 private final List<Headers> partHeaders = new ArrayList<>(); 79 private final List<RequestBody> partBodies = new ArrayList<>(); 80 81 /** Creates a new multipart builder that uses a random boundary token. */ 82 public MultipartBuilder() { 83 this(UUID.randomUUID().toString()); 84 } 85 86 /** 87 * Creates a new multipart builder that uses {@code boundary} to separate 88 * parts. Prefer the no-argument constructor to defend against injection 89 * attacks. 90 */ 91 public MultipartBuilder(String boundary) { 92 this.boundary = ByteString.encodeUtf8(boundary); 93 } 94 95 /** 96 * Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the 97 * default), {@link #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and 98 * {@link #FORM}. 99 */ 100 public MultipartBuilder type(MediaType type) { 101 if (type == null) { 102 throw new NullPointerException("type == null"); 103 } 104 if (!type.type().equals("multipart")) { 105 throw new IllegalArgumentException("multipart != " + type); 106 } 107 this.type = type; 108 return this; 109 } 110 111 /** Add a part to the body. */ 112 public MultipartBuilder addPart(RequestBody body) { 113 return addPart(null, body); 114 } 115 116 /** Add a part to the body. */ 117 public MultipartBuilder addPart(Headers headers, RequestBody body) { 118 if (body == null) { 119 throw new NullPointerException("body == null"); 120 } 121 if (headers != null && headers.get("Content-Type") != null) { 122 throw new IllegalArgumentException("Unexpected header: Content-Type"); 123 } 124 if (headers != null && headers.get("Content-Length") != null) { 125 throw new IllegalArgumentException("Unexpected header: Content-Length"); 126 } 127 128 partHeaders.add(headers); 129 partBodies.add(body); 130 return this; 131 } 132 133 /** 134 * Appends a quoted-string to a StringBuilder. 135 * 136 * <p>RFC 2388 is rather vague about how one should escape special characters 137 * in form-data parameters, and as it turns out Firefox and Chrome actually 138 * do rather different things, and both say in their comments that they're 139 * not really sure what the right approach is. We go with Chrome's behavior 140 * (which also experimentally seems to match what IE does), but if you 141 * actually want to have a good chance of things working, please avoid 142 * double-quotes, newlines, percent signs, and the like in your field names. 143 */ 144 private static StringBuilder appendQuotedString(StringBuilder target, String key) { 145 target.append('"'); 146 for (int i = 0, len = key.length(); i < len; i++) { 147 char ch = key.charAt(i); 148 switch (ch) { 149 case '\n': 150 target.append("%0A"); 151 break; 152 case '\r': 153 target.append("%0D"); 154 break; 155 case '"': 156 target.append("%22"); 157 break; 158 default: 159 target.append(ch); 160 break; 161 } 162 } 163 target.append('"'); 164 return target; 165 } 166 167 /** Add a form data part to the body. */ 168 public MultipartBuilder addFormDataPart(String name, String value) { 169 return addFormDataPart(name, null, RequestBody.create(null, value)); 170 } 171 172 /** Add a form data part to the body. */ 173 public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) { 174 if (name == null) { 175 throw new NullPointerException("name == null"); 176 } 177 StringBuilder disposition = new StringBuilder("form-data; name="); 178 appendQuotedString(disposition, name); 179 180 if (filename != null) { 181 disposition.append("; filename="); 182 appendQuotedString(disposition, filename); 183 } 184 185 return addPart(Headers.of("Content-Disposition", disposition.toString()), value); 186 } 187 188 /** Assemble the specified parts into a request body. */ 189 public RequestBody build() { 190 if (partHeaders.isEmpty()) { 191 throw new IllegalStateException("Multipart body must have at least one part."); 192 } 193 return new MultipartRequestBody(type, boundary, partHeaders, partBodies); 194 } 195 196 private static final class MultipartRequestBody extends RequestBody { 197 private final ByteString boundary; 198 private final MediaType contentType; 199 private final List<Headers> partHeaders; 200 private final List<RequestBody> partBodies; 201 private long contentLength = -1L; 202 203 public MultipartRequestBody(MediaType type, ByteString boundary, List<Headers> partHeaders, 204 List<RequestBody> partBodies) { 205 if (type == null) throw new NullPointerException("type == null"); 206 207 this.boundary = boundary; 208 this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8()); 209 this.partHeaders = Util.immutableList(partHeaders); 210 this.partBodies = Util.immutableList(partBodies); 211 } 212 213 @Override public MediaType contentType() { 214 return contentType; 215 } 216 217 @Override public long contentLength() throws IOException { 218 long result = contentLength; 219 if (result != -1L) return result; 220 return contentLength = writeOrCountBytes(null, true); 221 } 222 223 /** 224 * Either writes this request to {@code sink} or measures its content length. We have one method 225 * do double-duty to make sure the counting and content are consistent, particularly when it 226 * comes to awkward operations like measuring the encoded length of header strings, or the 227 * length-in-digits of an encoded integer. 228 */ 229 private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException { 230 long byteCount = 0L; 231 232 Buffer byteCountBuffer = null; 233 if (countBytes) { 234 sink = byteCountBuffer = new Buffer(); 235 } 236 237 for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) { 238 Headers headers = partHeaders.get(p); 239 RequestBody body = partBodies.get(p); 240 241 sink.write(DASHDASH); 242 sink.write(boundary); 243 sink.write(CRLF); 244 245 if (headers != null) { 246 for (int h = 0, headerCount = headers.size(); h < headerCount; h++) { 247 sink.writeUtf8(headers.name(h)) 248 .write(COLONSPACE) 249 .writeUtf8(headers.value(h)) 250 .write(CRLF); 251 } 252 } 253 254 MediaType contentType = body.contentType(); 255 if (contentType != null) { 256 sink.writeUtf8("Content-Type: ") 257 .writeUtf8(contentType.toString()) 258 .write(CRLF); 259 } 260 261 long contentLength = body.contentLength(); 262 if (contentLength != -1) { 263 sink.writeUtf8("Content-Length: ") 264 .writeDecimalLong(contentLength) 265 .write(CRLF); 266 } else if (countBytes) { 267 // We can't measure the body's size without the sizes of its components. 268 byteCountBuffer.clear(); 269 return -1L; 270 } 271 272 sink.write(CRLF); 273 274 if (countBytes) { 275 byteCount += contentLength; 276 } else { 277 partBodies.get(p).writeTo(sink); 278 } 279 280 sink.write(CRLF); 281 } 282 283 sink.write(DASHDASH); 284 sink.write(boundary); 285 sink.write(DASHDASH); 286 sink.write(CRLF); 287 288 if (countBytes) { 289 byteCount += byteCountBuffer.size(); 290 byteCountBuffer.clear(); 291 } 292 293 return byteCount; 294 } 295 296 @Override public void writeTo(BufferedSink sink) throws IOException { 297 writeOrCountBytes(sink, false); 298 } 299 } 300 } 301