OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file | |
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. | |
4 | |
5 part of sync.http; | |
6 | |
7 /// A simple synchronous HTTP client. | |
8 /// | |
9 /// This is a two-step process. When a [SyncHttpClientRequest] is returned the | |
10 /// underlying network connection has been established, but no data has yet been | |
11 /// sent. The HTTP headers and body can be set on the request, and close is | |
12 /// called to send it to the server and get the [SyncHttpClientResponse]. | |
13 abstract class SyncHttpClient { | |
14 /// Send a GET request to the provided URL. | |
15 static SyncHttpClientRequest getUrl(Uri uri) => | |
16 new SyncHttpClientRequest._('GET', uri, false); | |
17 | |
18 /// Send a POST request to the provided URL. | |
19 static SyncHttpClientRequest postUrl(uri) => | |
20 new SyncHttpClientRequest._('POST', uri, true); | |
21 | |
22 /// Send a DELETE request to the provided URL. | |
23 static SyncHttpClientRequest deleteUrl(uri) => | |
24 new SyncHttpClientRequest._('DELETE', uri, false); | |
25 | |
26 /// Send a PUT request to the provided URL. | |
27 static SyncHttpClientRequest putUrl(uri) => | |
28 new SyncHttpClientRequest._('PUT', uri, true); | |
29 } | |
30 | |
31 /// HTTP request for a synchronous client connection. | |
32 class SyncHttpClientRequest { | |
33 static const String _protocolVersion = '1.1'; | |
34 | |
35 int get contentLength => hasBody ? _body.length : null; | |
zra
2017/04/19 22:42:43
Please document all public fields and methods. Her
bkonyi
2017/04/20 14:40:48
Done (I think I got them all?).
| |
36 | |
37 HttpHeaders _headers; | |
38 | |
39 /// The headers associated with the HTTP request. | |
40 HttpHeaders get headers { | |
41 if (_headers == null) { | |
42 _headers = new _SyncHttpClientRequestHeaders(this); | |
43 } | |
44 return _headers; | |
45 } | |
46 | |
47 final String method; | |
48 | |
49 final Uri uri; | |
50 | |
51 final Encoding encoding = UTF8; | |
52 | |
53 final BytesBuilder _body; | |
54 | |
55 final RawSynchronousSocket _socket; | |
56 | |
57 SyncHttpClientRequest._(this.method, Uri uri, bool body) | |
58 : this.uri = uri, | |
59 this._body = body ? new BytesBuilder() : null, | |
60 this._socket = RawSynchronousSocket.connectSync(uri.host, uri.port); | |
61 | |
62 /// Write content into the body of the request. | |
63 void write(Object obj) { | |
64 if (hasBody) { | |
65 _body.add(encoding.encoder.convert(obj.toString())); | |
66 } else { | |
67 throw new StateError('write not allowed for method $method'); | |
68 } | |
69 } | |
70 | |
71 bool get hasBody => _body != null; | |
72 | |
73 /// Send the HTTP request and get the response. | |
74 SyncHttpClientResponse close() { | |
75 StringBuffer buffer = new StringBuffer(); | |
76 buffer.write('$method ${uri.path} HTTP/$_protocolVersion\r\n'); | |
77 headers.forEach((name, values) { | |
78 values.forEach((value) { | |
79 buffer.write('$name: $value\r\n'); | |
80 }); | |
81 }); | |
82 buffer.write('\r\n'); | |
83 if (hasBody) { | |
84 buffer.write(new String.fromCharCodes(_body.takeBytes())); | |
85 } | |
86 _socket.writeFromSync(buffer.toString().codeUnits); | |
87 return new SyncHttpClientResponse(_socket); | |
88 } | |
89 } | |
90 | |
91 class _SyncHttpClientRequestHeaders implements HttpHeaders { | |
92 Map<String, List> _headers = <String, List<String>>{}; | |
93 | |
94 final SyncHttpClientRequest _request; | |
95 ContentType contentType; | |
96 | |
97 _SyncHttpClientRequestHeaders(this._request); | |
98 | |
99 @override | |
100 List<String> operator [](String name) { | |
101 switch (name) { | |
102 case HttpHeaders.ACCEPT_CHARSET: | |
103 return ['utf-8']; | |
104 case HttpHeaders.ACCEPT_ENCODING: | |
105 return ['identity']; | |
106 case HttpHeaders.CONNECTION: | |
107 return ['close']; | |
108 case HttpHeaders.CONTENT_LENGTH: | |
109 if (!_request.hasBody) { | |
110 return null; | |
111 } | |
112 return [contentLength.toString()]; | |
113 case HttpHeaders.CONTENT_TYPE: | |
114 if (contentType == null) { | |
115 return null; | |
116 } | |
117 return [contentType.toString()]; | |
118 case HttpHeaders.HOST: | |
119 return ['$host:$port']; | |
120 default: | |
121 var values = _headers[name]; | |
122 if (values == null || values.isEmpty) { | |
123 return null; | |
124 } | |
125 return values.map((e) => e.toString()).toList(growable: false); | |
126 } | |
127 } | |
128 | |
129 /// Add [value] to the list of values associated with header [name]. | |
130 @override | |
131 void add(String name, Object value) { | |
132 switch (name) { | |
133 case HttpHeaders.ACCEPT_CHARSET: | |
134 case HttpHeaders.ACCEPT_ENCODING: | |
135 case HttpHeaders.CONNECTION: | |
136 case HttpHeaders.CONTENT_LENGTH: | |
137 case HttpHeaders.DATE: | |
138 case HttpHeaders.EXPIRES: | |
139 case HttpHeaders.IF_MODIFIED_SINCE: | |
140 case HttpHeaders.HOST: | |
141 throw new UnsupportedError('Unsupported or immutable property: $name'); | |
142 case HttpHeaders.CONTENT_TYPE: | |
143 contentType = value; | |
144 break; | |
145 default: | |
146 if (_headers[name] == null) { | |
147 _headers[name] = []; | |
148 } | |
149 _headers[name].add(value); | |
150 } | |
151 } | |
152 | |
153 /// Remove [value] from the list associated with header [name]. | |
154 @override | |
155 void remove(String name, Object value) { | |
156 switch (name) { | |
157 case HttpHeaders.ACCEPT_CHARSET: | |
158 case HttpHeaders.ACCEPT_ENCODING: | |
159 case HttpHeaders.CONNECTION: | |
160 case HttpHeaders.CONTENT_LENGTH: | |
161 case HttpHeaders.DATE: | |
162 case HttpHeaders.EXPIRES: | |
163 case HttpHeaders.IF_MODIFIED_SINCE: | |
164 case HttpHeaders.HOST: | |
165 throw new UnsupportedError('Unsupported or immutable property: $name'); | |
166 case HttpHeaders.CONTENT_TYPE: | |
167 if (contentType == value) { | |
168 contentType = null; | |
169 } | |
170 break; | |
171 default: | |
172 if (_headers[name] != null) { | |
173 _headers[name].remove(value); | |
174 if (_headers[name].isEmpty) { | |
175 _headers.remove(name); | |
176 } | |
177 } | |
178 } | |
179 } | |
180 | |
181 /// Remove all headers associated with key [name]. | |
182 @override | |
183 void removeAll(String name) { | |
184 switch (name) { | |
185 case HttpHeaders.ACCEPT_CHARSET: | |
186 case HttpHeaders.ACCEPT_ENCODING: | |
187 case HttpHeaders.CONNECTION: | |
188 case HttpHeaders.CONTENT_LENGTH: | |
189 case HttpHeaders.DATE: | |
190 case HttpHeaders.EXPIRES: | |
191 case HttpHeaders.IF_MODIFIED_SINCE: | |
192 case HttpHeaders.HOST: | |
193 throw new UnsupportedError('Unsupported or immutable property: $name'); | |
194 case HttpHeaders.CONTENT_TYPE: | |
195 contentType = null; | |
196 break; | |
197 default: | |
198 _headers.remove(name); | |
199 } | |
200 } | |
201 | |
202 /// Replace values associated with key [name] with [value]. | |
203 @override | |
204 void set(String name, Object value) { | |
205 removeAll(name); | |
206 add(name, value); | |
207 } | |
208 | |
209 /// Returns the values associated with key [name], if it exists, otherwise | |
210 /// returns null. | |
211 @override | |
212 String value(String name) { | |
213 var val = this[name]; | |
214 if (val == null || val.isEmpty) { | |
215 return null; | |
216 } else if (val.length == 1) { | |
217 return val[0]; | |
218 } else { | |
219 throw new HttpException('header $name has more than one value'); | |
220 } | |
221 } | |
222 | |
223 /// Iterates over all header key-value pairs and applies [f]. | |
224 @override | |
225 void forEach(void f(String name, List<String> values)) { | |
226 var forEachFunc = (String name) { | |
227 var values = this[name]; | |
228 if (values != null && values.isNotEmpty) { | |
229 f(name, values); | |
230 } | |
231 }; | |
232 | |
233 [ | |
234 HttpHeaders.ACCEPT_CHARSET, | |
235 HttpHeaders.ACCEPT_ENCODING, | |
236 HttpHeaders.CONNECTION, | |
237 HttpHeaders.CONTENT_LENGTH, | |
238 HttpHeaders.CONTENT_TYPE, | |
239 HttpHeaders.HOST | |
240 ].forEach(forEachFunc); | |
241 _headers.keys.forEach(forEachFunc); | |
242 } | |
243 | |
244 @override | |
245 bool get chunkedTransferEncoding => null; | |
246 | |
247 @override | |
248 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { | |
249 throw new UnsupportedError('chunked transfer is unsupported'); | |
250 } | |
251 | |
252 @override | |
253 int get contentLength => _request.contentLength; | |
254 | |
255 @override | |
256 void set contentLength(int _contentLength) { | |
257 throw new UnsupportedError('content length is automatically set'); | |
258 } | |
259 | |
260 @override | |
261 void set date(DateTime _date) { | |
262 throw new UnsupportedError('date is unsupported'); | |
263 } | |
264 | |
265 @override | |
266 DateTime get date => null; | |
267 | |
268 @override | |
269 void set expires(DateTime _expires) { | |
270 throw new UnsupportedError('expires is unsupported'); | |
271 } | |
272 | |
273 @override | |
274 DateTime get expires => null; | |
275 | |
276 @override | |
277 void set host(String _host) { | |
278 throw new UnsupportedError('host is automatically set'); | |
279 } | |
280 | |
281 @override | |
282 String get host => _request.uri.host; | |
283 | |
284 @override | |
285 DateTime get ifModifiedSince => null; | |
286 | |
287 @override | |
288 void set ifModifiedSince(DateTime _ifModifiedSince) { | |
289 throw new UnsupportedError('if modified since is unsupported'); | |
290 } | |
291 | |
292 @override | |
293 void noFolding(String name) { | |
294 throw new UnsupportedError('no folding is unsupported'); | |
295 } | |
296 | |
297 @override | |
298 bool get persistentConnection => false; | |
299 | |
300 @override | |
301 void set persistentConnection(bool _persistentConnection) { | |
302 throw new UnsupportedError('persistence connections are unsupported'); | |
303 } | |
304 | |
305 @override | |
306 void set port(int _port) { | |
307 throw new UnsupportedError('port is automatically set'); | |
308 } | |
309 | |
310 @override | |
311 int get port => _request.uri.port; | |
312 | |
313 /// Clear all header key-value pairs. | |
314 @override | |
315 void clear() { | |
316 contentType = null; | |
317 _headers.clear(); | |
318 } | |
319 } | |
320 | |
321 /// HTTP response for a client connection. | |
322 class SyncHttpClientResponse { | |
323 int get contentLength => headers.contentLength; | |
324 final HttpHeaders headers; | |
325 final String reasonPhrase; | |
326 final int statusCode; | |
327 final String body; | |
328 | |
329 factory SyncHttpClientResponse(RawSynchronousSocket socket) { | |
330 int statusCode; | |
331 String reasonPhrase; | |
332 StringBuffer body = new StringBuffer(); | |
333 Map<String, List<String>> headers = {}; | |
334 | |
335 bool inHeader = false; | |
336 bool inBody = false; | |
337 int contentLength = 0; | |
338 int contentRead = 0; | |
339 | |
340 void processLine(String line, int bytesRead, _LineDecoder decoder) { | |
341 if (inBody) { | |
342 body.write(line); | |
343 contentRead += bytesRead; | |
344 } else if (inHeader) { | |
345 if (line.trim().isEmpty) { | |
346 inBody = true; | |
347 if (contentLength > 0) { | |
348 decoder.expectedByteCount = contentLength; | |
349 } | |
350 return; | |
351 } | |
352 int separator = line.indexOf(':'); | |
353 String name = line.substring(0, separator).toLowerCase().trim(); | |
354 String value = line.substring(separator + 1).trim(); | |
355 if (name == HttpHeaders.TRANSFER_ENCODING && | |
356 value.toLowerCase() != 'identity') { | |
357 throw new UnsupportedError( | |
358 'only identity transfer encoding is accepted'); | |
359 } | |
360 if (name == HttpHeaders.CONTENT_LENGTH) { | |
361 contentLength = int.parse(value); | |
362 } | |
363 if (!headers.containsKey(name)) { | |
364 headers[name] = []; | |
365 } | |
366 headers[name].add(value); | |
367 } else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) { | |
368 statusCode = int | |
369 .parse(line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length)); | |
370 reasonPhrase = line.substring('HTTP/1.x xxx '.length); | |
371 inHeader = true; | |
372 } else { | |
373 throw new UnsupportedError('unsupported http response format'); | |
374 } | |
375 } | |
376 | |
377 var lineDecoder = new _LineDecoder.withCallback(processLine); | |
378 | |
379 try { | |
380 while (!inHeader || | |
381 !inBody || | |
382 ((contentRead + lineDecoder.bufferedBytes) < contentLength)) { | |
383 var bytes = socket.readSync(1024); | |
384 | |
385 if (bytes == null || bytes.length == 0) { | |
386 break; | |
387 } | |
388 lineDecoder.add(bytes); | |
389 } | |
390 } finally { | |
391 try { | |
392 lineDecoder.close(); | |
393 } finally { | |
394 socket.closeSync(); | |
395 } | |
396 } | |
397 | |
398 return new SyncHttpClientResponse._( | |
399 reasonPhrase: reasonPhrase, | |
400 statusCode: statusCode, | |
401 body: body.toString(), | |
402 headers: headers); | |
403 } | |
404 | |
405 SyncHttpClientResponse._( | |
406 {this.reasonPhrase, this.statusCode, this.body, headers}) | |
407 : this.headers = new _SyncHttpClientResponseHeaders(headers); | |
408 } | |
409 | |
410 class _SyncHttpClientResponseHeaders implements HttpHeaders { | |
411 final Map<String, List<String>> _headers; | |
412 | |
413 _SyncHttpClientResponseHeaders(this._headers); | |
414 | |
415 @override | |
416 List<String> operator [](String name) => _headers[name]; | |
417 | |
418 @override | |
419 void add(String name, Object value) { | |
420 throw new UnsupportedError('Response headers are immutable'); | |
421 } | |
422 | |
423 @override | |
424 bool get chunkedTransferEncoding => null; | |
425 | |
426 @override | |
427 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { | |
428 throw new UnsupportedError('Response headers are immutable'); | |
429 } | |
430 | |
431 @override | |
432 int get contentLength { | |
433 String val = value(HttpHeaders.CONTENT_LENGTH); | |
434 if (val != null) { | |
435 return int.parse(val, onError: (_) => null); | |
436 } | |
437 return null; | |
438 } | |
439 | |
440 @override | |
441 void set contentLength(int _contentLength) { | |
442 throw new UnsupportedError('Response headers are immutable'); | |
443 } | |
444 | |
445 @override | |
446 ContentType get contentType { | |
447 var val = value(HttpHeaders.CONTENT_TYPE); | |
448 if (val != null) { | |
449 return ContentType.parse(val); | |
450 } | |
451 return null; | |
452 } | |
453 | |
454 @override | |
455 void set contentType(ContentType _contentType) { | |
456 throw new UnsupportedError('Response headers are immutable'); | |
457 } | |
458 | |
459 @override | |
460 void set date(DateTime _date) { | |
461 throw new UnsupportedError('Response headers are immutable'); | |
462 } | |
463 | |
464 @override | |
465 DateTime get date { | |
466 var val = value(HttpHeaders.DATE); | |
467 if (val != null) { | |
468 return DateTime.parse(val); | |
469 } | |
470 return null; | |
471 } | |
472 | |
473 @override | |
474 void set expires(DateTime _expires) { | |
475 throw new UnsupportedError('Response headers are immutable'); | |
476 } | |
477 | |
478 @override | |
479 DateTime get expires { | |
480 var val = value(HttpHeaders.EXPIRES); | |
481 if (val != null) { | |
482 return DateTime.parse(val); | |
483 } | |
484 return null; | |
485 } | |
486 | |
487 @override | |
488 void forEach(void f(String name, List<String> values)) => _headers.forEach(f); | |
489 | |
490 @override | |
491 void set host(String _host) { | |
492 throw new UnsupportedError('Response headers are immutable'); | |
493 } | |
494 | |
495 @override | |
496 String get host { | |
497 var val = value(HttpHeaders.HOST); | |
498 if (val != null) { | |
499 return Uri.parse(val).host; | |
500 } | |
501 return null; | |
502 } | |
503 | |
504 @override | |
505 DateTime get ifModifiedSince { | |
506 var val = value(HttpHeaders.IF_MODIFIED_SINCE); | |
507 if (val != null) { | |
508 return DateTime.parse(val); | |
509 } | |
510 return null; | |
511 } | |
512 | |
513 @override | |
514 void set ifModifiedSince(DateTime _ifModifiedSince) { | |
515 throw new UnsupportedError('Response headers are immutable'); | |
516 } | |
517 | |
518 @override | |
519 void noFolding(String name) { | |
520 throw new UnsupportedError('Response headers are immutable'); | |
521 } | |
522 | |
523 @override | |
524 bool get persistentConnection => false; | |
525 | |
526 @override | |
527 void set persistentConnection(bool _persistentConnection) { | |
528 throw new UnsupportedError('Response headers are immutable'); | |
529 } | |
530 | |
531 @override | |
532 void set port(int _port) { | |
533 throw new UnsupportedError('Response headers are immutable'); | |
534 } | |
535 | |
536 @override | |
537 int get port { | |
538 var val = value(HttpHeaders.HOST); | |
539 if (val != null) { | |
540 return Uri.parse(val).port; | |
541 } | |
542 return null; | |
543 } | |
544 | |
545 @override | |
546 void remove(String name, Object value) { | |
547 throw new UnsupportedError('Response headers are immutable'); | |
548 } | |
549 | |
550 @override | |
551 void removeAll(String name) { | |
552 throw new UnsupportedError('Response headers are immutable'); | |
553 } | |
554 | |
555 @override | |
556 void set(String name, Object value) { | |
557 throw new UnsupportedError('Response headers are immutable'); | |
558 } | |
559 | |
560 @override | |
561 String value(String name) { | |
562 var val = this[name]; | |
563 if (val == null || val.isEmpty) { | |
564 return null; | |
565 } else if (val.length == 1) { | |
566 return val[0]; | |
567 } else { | |
568 throw new HttpException('header $name has more than one value'); | |
569 } | |
570 } | |
571 | |
572 @override | |
573 void clear() { | |
574 throw new UnsupportedError('Response headers are immutable'); | |
575 } | |
576 } | |
OLD | NEW |