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