OLD | NEW |
---|---|
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library multipart_request; | 5 library multipart_request; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:convert'; | 8 import 'dart:convert'; |
9 import 'dart:math'; | 9 import 'dart:math'; |
10 | 10 |
11 import 'base_request.dart'; | 11 import 'base_request.dart'; |
12 import 'byte_stream.dart'; | 12 import 'byte_stream.dart'; |
13 import 'multipart_file.dart'; | 13 import 'multipart_file.dart'; |
14 import 'utils.dart'; | 14 import 'utils.dart'; |
15 | 15 |
16 final _newlineRegExp = new RegExp(r"\r\n|\r|\n"); | |
17 | |
16 /// A `multipart/form-data` request. Such a request has both string [fields], | 18 /// A `multipart/form-data` request. Such a request has both string [fields], |
17 /// which function as normal form fields, and (potentially streamed) binary | 19 /// which function as normal form fields, and (potentially streamed) binary |
18 /// [files]. | 20 /// [files]. |
19 /// | 21 /// |
20 /// This request automatically sets the Content-Type header to | 22 /// This request automatically sets the Content-Type header to |
21 /// `multipart/form-data` and the Content-Transfer-Encoding header to `binary`. | 23 /// `multipart/form-data` and the Content-Transfer-Encoding header to `binary`. |
22 /// These values will override any values set by the user. | 24 /// These values will override any values set by the user. |
23 /// | 25 /// |
24 /// var uri = Uri.parse("http://pub.dartlang.org/packages/create"); | 26 /// var uri = Uri.parse("http://pub.dartlang.org/packages/create"); |
25 /// var request = new http.MultipartRequest("POST", url); | 27 /// var request = new http.MultipartRequest("POST", url); |
(...skipping 28 matching lines...) Expand all Loading... | |
54 /// The list of files to upload for this request. | 56 /// The list of files to upload for this request. |
55 List<MultipartFile> get files => _files; | 57 List<MultipartFile> get files => _files; |
56 | 58 |
57 /// The total length of the request body, in bytes. This is calculated from | 59 /// The total length of the request body, in bytes. This is calculated from |
58 /// [fields] and [files] and cannot be set manually. | 60 /// [fields] and [files] and cannot be set manually. |
59 int get contentLength { | 61 int get contentLength { |
60 var length = 0; | 62 var length = 0; |
61 | 63 |
62 fields.forEach((name, value) { | 64 fields.forEach((name, value) { |
63 length += "--".length + _BOUNDARY_LENGTH + "\r\n".length + | 65 length += "--".length + _BOUNDARY_LENGTH + "\r\n".length + |
64 _headerForField(name, value).length + | 66 UTF8.encode(_headerForField(name, value)).length + |
65 UTF8.encode(value).length + "\r\n".length; | 67 UTF8.encode(value).length + "\r\n".length; |
66 }); | 68 }); |
67 | 69 |
68 for (var file in _files) { | 70 for (var file in _files) { |
69 length += "--".length + _BOUNDARY_LENGTH + "\r\n".length + | 71 length += "--".length + _BOUNDARY_LENGTH + "\r\n".length + |
70 _headerForFile(file).length + | 72 UTF8.encode(_headerForFile(file)).length + |
71 file.length + "\r\n".length; | 73 file.length + "\r\n".length; |
72 } | 74 } |
73 | 75 |
74 return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length; | 76 return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length; |
75 } | 77 } |
76 | 78 |
77 void set contentLength(int value) { | 79 void set contentLength(int value) { |
78 throw new UnsupportedError("Cannot set the contentLength property of " | 80 throw new UnsupportedError("Cannot set the contentLength property of " |
79 "multipart requests."); | 81 "multipart requests."); |
80 } | 82 } |
81 | 83 |
82 /// Freezes all mutable fields and returns a single-subscription [ByteStream] | 84 /// Freezes all mutable fields and returns a single-subscription [ByteStream] |
83 /// that will emit the request body. | 85 /// that will emit the request body. |
84 ByteStream finalize() { | 86 ByteStream finalize() { |
85 // TODO(nweiz): freeze fields and files | 87 // TODO(nweiz): freeze fields and files |
86 var boundary = _boundaryString(); | 88 var boundary = _boundaryString(); |
87 headers['content-type'] = 'multipart/form-data; boundary="$boundary"'; | 89 headers['content-type'] = 'multipart/form-data; boundary="$boundary"'; |
88 headers['content-transfer-encoding'] = 'binary'; | 90 headers['content-transfer-encoding'] = 'binary'; |
89 super.finalize(); | 91 super.finalize(); |
90 | 92 |
91 var controller = new StreamController<List<int>>(sync: true); | 93 var controller = new StreamController<List<int>>(sync: true); |
92 | 94 |
93 void writeAscii(String string) { | 95 void writeAscii(String string) { |
94 assert(isPlainAscii(string)); | 96 controller.add(UTF8.encode(string)); |
95 controller.add(string.codeUnits); | |
96 } | 97 } |
97 | 98 |
98 writeUtf8(String string) => controller.add(UTF8.encode(string)); | 99 writeUtf8(String string) => controller.add(UTF8.encode(string)); |
99 writeLine() => controller.add([13, 10]); // \r\n | 100 writeLine() => controller.add([13, 10]); // \r\n |
100 | 101 |
101 fields.forEach((name, value) { | 102 fields.forEach((name, value) { |
102 writeAscii('--$boundary\r\n'); | 103 writeAscii('--$boundary\r\n'); |
103 writeAscii(_headerForField(name, value)); | 104 writeAscii(_headerForField(name, value)); |
104 writeUtf8(value); | 105 writeUtf8(value); |
105 writeLine(); | 106 writeLine(); |
(...skipping 20 matching lines...) Expand all Loading... | |
126 39, 40, 41, 43, 95, 44, 45, 46, 47, 58, 61, 63, 48, 49, 50, 51, 52, 53, 54, | 127 39, 40, 41, 43, 95, 44, 45, 46, 47, 58, 61, 63, 48, 49, 50, 51, 52, 53, 54, |
127 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, | 128 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, |
128 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, | 129 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, |
129 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, | 130 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, |
130 119, 120, 121, 122 | 131 119, 120, 121, 122 |
131 ]; | 132 ]; |
132 | 133 |
133 /// Returns the header string for a field. The return value is guaranteed to | 134 /// Returns the header string for a field. The return value is guaranteed to |
134 /// contain only ASCII characters. | 135 /// contain only ASCII characters. |
135 String _headerForField(String name, String value) { | 136 String _headerForField(String name, String value) { |
136 // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for | |
137 // field names and file names, but in practice user agents seem to just | |
138 // URL-encode them so we do the same. | |
139 var header = | 137 var header = |
140 'content-disposition: form-data; name="${Uri.encodeFull(name)}"'; | 138 'content-disposition: form-data; name="${_browserEncode(name)}"'; |
141 if (!isPlainAscii(value)) { | 139 if (!isPlainAscii(value)) { |
142 header = '$header\r\ncontent-type: text/plain; charset=utf-8'; | 140 header = '$header\r\ncontent-type: text/plain; charset=utf-8'; |
143 } | 141 } |
144 return '$header\r\n\r\n'; | 142 return '$header\r\n\r\n'; |
145 } | 143 } |
146 | 144 |
147 /// Returns the header string for a file. The return value is guaranteed to | 145 /// Returns the header string for a file. The return value is guaranteed to |
148 /// contain only ASCII characters. | 146 /// contain only ASCII characters. |
149 String _headerForFile(MultipartFile file) { | 147 String _headerForFile(MultipartFile file) { |
150 var header = 'content-type: ${file.contentType}\r\n' | 148 var header = 'content-type: ${file.contentType}\r\n' |
151 'content-disposition: form-data; name="${Uri.encodeFull(file.field)}"'; | 149 'content-disposition: form-data; name="${_browserEncode(file.field)}"'; |
152 | 150 |
153 if (file.filename != null) { | 151 if (file.filename != null) { |
154 header = '$header; filename="${Uri.encodeFull(file.filename)}"'; | 152 header = '$header; filename="${_browserEncode(file.filename)}"'; |
155 } | 153 } |
156 return '$header\r\n\r\n'; | 154 return '$header\r\n\r\n'; |
157 } | 155 } |
158 | 156 |
157 /// Encode [value] in the same way browsers do. | |
158 String _browserEncode(String value) { | |
159 // http://tools.ietf.org/html/rfc2388 mandates some complex encodings for | |
160 // field names and file names, but in practice user agents seem not to | |
161 // follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as | |
162 // `\r\n`; URL-encode `"`; and do thing else (even for `%` or non-ASCII | |
Bob Nystrom
2014/04/01 19:45:26
thing -> nothing.
nweiz
2014/04/02 01:09:44
Done.
| |
163 // characters). We follow their behavior. | |
164 return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22"); | |
165 } | |
166 | |
159 /// Returns a randomly-generated multipart boundary string | 167 /// Returns a randomly-generated multipart boundary string |
160 String _boundaryString() { | 168 String _boundaryString() { |
161 var prefix = "dart-http-boundary-"; | 169 var prefix = "dart-http-boundary-"; |
162 var list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length, | 170 var list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length, |
163 (index) => | 171 (index) => |
164 _BOUNDARY_CHARACTERS[_random.nextInt(_BOUNDARY_CHARACTERS.length)], | 172 _BOUNDARY_CHARACTERS[_random.nextInt(_BOUNDARY_CHARACTERS.length)], |
165 growable: false); | 173 growable: false); |
166 return "$prefix${new String.fromCharCodes(list)}"; | 174 return "$prefix${new String.fromCharCodes(list)}"; |
167 } | 175 } |
168 } | 176 } |
OLD | NEW |