Home | History | Annotate | Download | only in okhttp
      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