multipart_request.dart 6.42 KB
Newer Older
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'base_request.dart';
import 'byte_stream.dart';
import 'multipart_file.dart';
import 'utils.dart';

final RegExp _newlineRegExp = new RegExp(r"\r\n|\r|\n");

/// A `multipart/form-data` request. Such a request has both string [fields],
/// which function as normal form fields, and (potentially streamed) binary
/// [files].
///
/// This request automatically sets the Content-Type header to
/// `multipart/form-data`. This value will override any value set by the user.
///
///     var uri = Uri.parse("http://pub.dartlang.org/packages/create");
///     var request = new http.MultipartRequest("POST", url);
///     request.fields['user'] = 'nweiz@google.com';
///     request.files.add(new http.MultipartFile.fromFile(
///         'package',
///         new File('build/package.tar.gz'),
///         contentType: new MediaType('application', 'x-tar'));
///     request.send().then((response) {
///       if (response.statusCode == 200) print("Uploaded!");
///     });
class MultipartRequest extends BaseRequest {
  /// The total length of the multipart boundaries used when building the
  /// request body. According to http://tools.ietf.org/html/rfc1341.html, this
  /// can't be longer than 70.
  static const int _BOUNDARY_LENGTH = 70;

  static final Random _random = new Random();

  /// The form fields to send for this request.
  final Map<String, String> fields;

  /// The private version of [files].
  final List<MultipartFile> _files;

  /// Creates a new [MultipartRequest].
  MultipartRequest(String method, Uri url)
    : fields = <String, String>{},
      _files = <MultipartFile>[],
      super(method, url);

  /// The list of files to upload for this request.
  List<MultipartFile> get files => _files;

  /// The total length of the request body, in bytes. This is calculated from
  /// [fields] and [files] and cannot be set manually.
  @override
  int get contentLength {
    int length = 0;

    fields.forEach((String name, String value) {
      length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
          UTF8.encode(_headerForField(name, value)).length +
          UTF8.encode(value).length + "\r\n".length;
    });

    for (MultipartFile file in _files) {
      length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
          UTF8.encode(_headerForFile(file)).length +
          file.length + "\r\n".length;
    }

    return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length;
  }

  @override
  set contentLength(int value) {
    throw new UnsupportedError("Cannot set the contentLength property of "
        "multipart requests.");
  }

  /// Freezes all mutable fields and returns a single-subscription [ByteStream]
  /// that will emit the request body.
  @override
  ByteStream finalize() {
    // TODO(nweiz): freeze fields and files
    String boundary = _boundaryString();
    headers['content-type'] = 'multipart/form-data; boundary="$boundary"';
    super.finalize();

    StreamController<List<int>> controller = new StreamController<List<int>>(sync: true);

    void writeAscii(String string) {
      controller.add(UTF8.encode(string));
    }

    dynamic writeUtf8(String string) => controller.add(UTF8.encode(string));
    dynamic writeLine() => controller.add(<int>[13, 10]); // \r\n

    fields.forEach((String name, String value) {
      writeAscii('--$boundary\r\n');
      writeAscii(_headerForField(name, value));
      writeUtf8(value);
      writeLine();
    });

    Future.forEach(_files, (MultipartFile file) {
      writeAscii('--$boundary\r\n');
      writeAscii(_headerForFile(file));
      return writeStreamToSink(file.finalize(), controller)
        .then((_) => writeLine());
    }).then((_) {
      // TODO(nweiz): pass any errors propagated through this future on to
      // the stream. See issue 3657.
      writeAscii('--$boundary--\r\n');
      controller.close();
    });

    return new ByteStream(controller.stream);
  }

  /// All character codes that are valid in multipart boundaries. From
  /// http://tools.ietf.org/html/rfc2046#section-5.1.1.
  static const List<int> _BOUNDARY_CHARACTERS = const <int>[
    39, 40, 41, 43, 95, 44, 45, 46, 47, 58, 61, 63, 48, 49, 50, 51, 52, 53, 54,
    55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
    81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103,
    104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
    119, 120, 121, 122
  ];

  /// Returns the header string for a field. The return value is guaranteed to
  /// contain only ASCII characters.
  String _headerForField(String name, String value) {
    String header =
        'content-disposition: form-data; name="${_browserEncode(name)}"';
    if (!isPlainAscii(value)) {
      header = '$header\r\n'
          'content-type: text/plain; charset=utf-8\r\n'
          'content-transfer-encoding: binary';
    }
    return '$header\r\n\r\n';
  }

  /// Returns the header string for a file. The return value is guaranteed to
  /// contain only ASCII characters.
  String _headerForFile(MultipartFile file) {
    String header = 'content-type: ${file.contentType}\r\n'
      'content-disposition: form-data; name="${_browserEncode(file.field)}"';

    if (file.filename != null) {
      header = '$header; filename="${_browserEncode(file.filename)}"';
    }
    return '$header\r\n\r\n';
  }

  /// Encode [value] in the same way browsers do.
  String _browserEncode(String value) {
    // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
    // field names and file names, but in practice user agents seem not to
    // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
    // `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
    // characters). We follow their behavior.
    return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22");
  }

  /// Returns a randomly-generated multipart boundary string
  String _boundaryString() {
    String prefix = "dart-http-boundary-";
    List<int> list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length,
        (int index) =>
            _BOUNDARY_CHARACTERS[_random.nextInt(_BOUNDARY_CHARACTERS.length)],
        growable: false);
    return "$prefix${new String.fromCharCodes(list)}";
  }
}