Chromium Code Reviews| Index: lib/src/sync_http.dart |
| diff --git a/lib/src/sync_http.dart b/lib/src/sync_http.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e74beb4525f2ff7c06d1353b935ea9585d713983 |
| --- /dev/null |
| +++ b/lib/src/sync_http.dart |
| @@ -0,0 +1,576 @@ |
| +// Copyright (c) 2017, 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. |
| + |
| +part of sync.http; |
| + |
| +/// A simple synchronous HTTP client. |
| +/// |
| +/// This is a two-step process. When a [SyncHttpClientRequest] is returned the |
| +/// underlying network connection has been established, but no data has yet been |
| +/// sent. The HTTP headers and body can be set on the request, and close is |
| +/// called to send it to the server and get the [SyncHttpClientResponse]. |
| +abstract class SyncHttpClient { |
| + /// Send a GET request to the provided URL. |
| + static SyncHttpClientRequest getUrl(Uri uri) => |
| + new SyncHttpClientRequest._('GET', uri, false); |
| + |
| + /// Send a POST request to the provided URL. |
| + static SyncHttpClientRequest postUrl(uri) => |
| + new SyncHttpClientRequest._('POST', uri, true); |
| + |
| + /// Send a DELETE request to the provided URL. |
| + static SyncHttpClientRequest deleteUrl(uri) => |
| + new SyncHttpClientRequest._('DELETE', uri, false); |
| + |
| + /// Send a PUT request to the provided URL. |
| + static SyncHttpClientRequest putUrl(uri) => |
| + new SyncHttpClientRequest._('PUT', uri, true); |
| +} |
| + |
| +/// HTTP request for a synchronous client connection. |
| +class SyncHttpClientRequest { |
| + static const String _protocolVersion = '1.1'; |
| + |
| + 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?).
|
| + |
| + HttpHeaders _headers; |
| + |
| + /// The headers associated with the HTTP request. |
| + HttpHeaders get headers { |
| + if (_headers == null) { |
| + _headers = new _SyncHttpClientRequestHeaders(this); |
| + } |
| + return _headers; |
| + } |
| + |
| + final String method; |
| + |
| + final Uri uri; |
| + |
| + final Encoding encoding = UTF8; |
| + |
| + final BytesBuilder _body; |
| + |
| + final RawSynchronousSocket _socket; |
| + |
| + SyncHttpClientRequest._(this.method, Uri uri, bool body) |
| + : this.uri = uri, |
| + this._body = body ? new BytesBuilder() : null, |
| + this._socket = RawSynchronousSocket.connectSync(uri.host, uri.port); |
| + |
| + /// Write content into the body of the request. |
| + void write(Object obj) { |
| + if (hasBody) { |
| + _body.add(encoding.encoder.convert(obj.toString())); |
| + } else { |
| + throw new StateError('write not allowed for method $method'); |
| + } |
| + } |
| + |
| + bool get hasBody => _body != null; |
| + |
| + /// Send the HTTP request and get the response. |
| + SyncHttpClientResponse close() { |
| + StringBuffer buffer = new StringBuffer(); |
| + buffer.write('$method ${uri.path} HTTP/$_protocolVersion\r\n'); |
| + headers.forEach((name, values) { |
| + values.forEach((value) { |
| + buffer.write('$name: $value\r\n'); |
| + }); |
| + }); |
| + buffer.write('\r\n'); |
| + if (hasBody) { |
| + buffer.write(new String.fromCharCodes(_body.takeBytes())); |
| + } |
| + _socket.writeFromSync(buffer.toString().codeUnits); |
| + return new SyncHttpClientResponse(_socket); |
| + } |
| +} |
| + |
| +class _SyncHttpClientRequestHeaders implements HttpHeaders { |
| + Map<String, List> _headers = <String, List<String>>{}; |
| + |
| + final SyncHttpClientRequest _request; |
| + ContentType contentType; |
| + |
| + _SyncHttpClientRequestHeaders(this._request); |
| + |
| + @override |
| + List<String> operator [](String name) { |
| + switch (name) { |
| + case HttpHeaders.ACCEPT_CHARSET: |
| + return ['utf-8']; |
| + case HttpHeaders.ACCEPT_ENCODING: |
| + return ['identity']; |
| + case HttpHeaders.CONNECTION: |
| + return ['close']; |
| + case HttpHeaders.CONTENT_LENGTH: |
| + if (!_request.hasBody) { |
| + return null; |
| + } |
| + return [contentLength.toString()]; |
| + case HttpHeaders.CONTENT_TYPE: |
| + if (contentType == null) { |
| + return null; |
| + } |
| + return [contentType.toString()]; |
| + case HttpHeaders.HOST: |
| + return ['$host:$port']; |
| + default: |
| + var values = _headers[name]; |
| + if (values == null || values.isEmpty) { |
| + return null; |
| + } |
| + return values.map((e) => e.toString()).toList(growable: false); |
| + } |
| + } |
| + |
| + /// Add [value] to the list of values associated with header [name]. |
| + @override |
| + void add(String name, Object value) { |
| + switch (name) { |
| + case HttpHeaders.ACCEPT_CHARSET: |
| + case HttpHeaders.ACCEPT_ENCODING: |
| + case HttpHeaders.CONNECTION: |
| + case HttpHeaders.CONTENT_LENGTH: |
| + case HttpHeaders.DATE: |
| + case HttpHeaders.EXPIRES: |
| + case HttpHeaders.IF_MODIFIED_SINCE: |
| + case HttpHeaders.HOST: |
| + throw new UnsupportedError('Unsupported or immutable property: $name'); |
| + case HttpHeaders.CONTENT_TYPE: |
| + contentType = value; |
| + break; |
| + default: |
| + if (_headers[name] == null) { |
| + _headers[name] = []; |
| + } |
| + _headers[name].add(value); |
| + } |
| + } |
| + |
| + /// Remove [value] from the list associated with header [name]. |
| + @override |
| + void remove(String name, Object value) { |
| + switch (name) { |
| + case HttpHeaders.ACCEPT_CHARSET: |
| + case HttpHeaders.ACCEPT_ENCODING: |
| + case HttpHeaders.CONNECTION: |
| + case HttpHeaders.CONTENT_LENGTH: |
| + case HttpHeaders.DATE: |
| + case HttpHeaders.EXPIRES: |
| + case HttpHeaders.IF_MODIFIED_SINCE: |
| + case HttpHeaders.HOST: |
| + throw new UnsupportedError('Unsupported or immutable property: $name'); |
| + case HttpHeaders.CONTENT_TYPE: |
| + if (contentType == value) { |
| + contentType = null; |
| + } |
| + break; |
| + default: |
| + if (_headers[name] != null) { |
| + _headers[name].remove(value); |
| + if (_headers[name].isEmpty) { |
| + _headers.remove(name); |
| + } |
| + } |
| + } |
| + } |
| + |
| + /// Remove all headers associated with key [name]. |
| + @override |
| + void removeAll(String name) { |
| + switch (name) { |
| + case HttpHeaders.ACCEPT_CHARSET: |
| + case HttpHeaders.ACCEPT_ENCODING: |
| + case HttpHeaders.CONNECTION: |
| + case HttpHeaders.CONTENT_LENGTH: |
| + case HttpHeaders.DATE: |
| + case HttpHeaders.EXPIRES: |
| + case HttpHeaders.IF_MODIFIED_SINCE: |
| + case HttpHeaders.HOST: |
| + throw new UnsupportedError('Unsupported or immutable property: $name'); |
| + case HttpHeaders.CONTENT_TYPE: |
| + contentType = null; |
| + break; |
| + default: |
| + _headers.remove(name); |
| + } |
| + } |
| + |
| + /// Replace values associated with key [name] with [value]. |
| + @override |
| + void set(String name, Object value) { |
| + removeAll(name); |
| + add(name, value); |
| + } |
| + |
| + /// Returns the values associated with key [name], if it exists, otherwise |
| + /// returns null. |
| + @override |
| + String value(String name) { |
| + var val = this[name]; |
| + if (val == null || val.isEmpty) { |
| + return null; |
| + } else if (val.length == 1) { |
| + return val[0]; |
| + } else { |
| + throw new HttpException('header $name has more than one value'); |
| + } |
| + } |
| + |
| + /// Iterates over all header key-value pairs and applies [f]. |
| + @override |
| + void forEach(void f(String name, List<String> values)) { |
| + var forEachFunc = (String name) { |
| + var values = this[name]; |
| + if (values != null && values.isNotEmpty) { |
| + f(name, values); |
| + } |
| + }; |
| + |
| + [ |
| + HttpHeaders.ACCEPT_CHARSET, |
| + HttpHeaders.ACCEPT_ENCODING, |
| + HttpHeaders.CONNECTION, |
| + HttpHeaders.CONTENT_LENGTH, |
| + HttpHeaders.CONTENT_TYPE, |
| + HttpHeaders.HOST |
| + ].forEach(forEachFunc); |
| + _headers.keys.forEach(forEachFunc); |
| + } |
| + |
| + @override |
| + bool get chunkedTransferEncoding => null; |
| + |
| + @override |
| + void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { |
| + throw new UnsupportedError('chunked transfer is unsupported'); |
| + } |
| + |
| + @override |
| + int get contentLength => _request.contentLength; |
| + |
| + @override |
| + void set contentLength(int _contentLength) { |
| + throw new UnsupportedError('content length is automatically set'); |
| + } |
| + |
| + @override |
| + void set date(DateTime _date) { |
| + throw new UnsupportedError('date is unsupported'); |
| + } |
| + |
| + @override |
| + DateTime get date => null; |
| + |
| + @override |
| + void set expires(DateTime _expires) { |
| + throw new UnsupportedError('expires is unsupported'); |
| + } |
| + |
| + @override |
| + DateTime get expires => null; |
| + |
| + @override |
| + void set host(String _host) { |
| + throw new UnsupportedError('host is automatically set'); |
| + } |
| + |
| + @override |
| + String get host => _request.uri.host; |
| + |
| + @override |
| + DateTime get ifModifiedSince => null; |
| + |
| + @override |
| + void set ifModifiedSince(DateTime _ifModifiedSince) { |
| + throw new UnsupportedError('if modified since is unsupported'); |
| + } |
| + |
| + @override |
| + void noFolding(String name) { |
| + throw new UnsupportedError('no folding is unsupported'); |
| + } |
| + |
| + @override |
| + bool get persistentConnection => false; |
| + |
| + @override |
| + void set persistentConnection(bool _persistentConnection) { |
| + throw new UnsupportedError('persistence connections are unsupported'); |
| + } |
| + |
| + @override |
| + void set port(int _port) { |
| + throw new UnsupportedError('port is automatically set'); |
| + } |
| + |
| + @override |
| + int get port => _request.uri.port; |
| + |
| + /// Clear all header key-value pairs. |
| + @override |
| + void clear() { |
| + contentType = null; |
| + _headers.clear(); |
| + } |
| +} |
| + |
| +/// HTTP response for a client connection. |
| +class SyncHttpClientResponse { |
| + int get contentLength => headers.contentLength; |
| + final HttpHeaders headers; |
| + final String reasonPhrase; |
| + final int statusCode; |
| + final String body; |
| + |
| + factory SyncHttpClientResponse(RawSynchronousSocket socket) { |
| + int statusCode; |
| + String reasonPhrase; |
| + StringBuffer body = new StringBuffer(); |
| + Map<String, List<String>> headers = {}; |
| + |
| + bool inHeader = false; |
| + bool inBody = false; |
| + int contentLength = 0; |
| + int contentRead = 0; |
| + |
| + void processLine(String line, int bytesRead, _LineDecoder decoder) { |
| + if (inBody) { |
| + body.write(line); |
| + contentRead += bytesRead; |
| + } else if (inHeader) { |
| + if (line.trim().isEmpty) { |
| + inBody = true; |
| + if (contentLength > 0) { |
| + decoder.expectedByteCount = contentLength; |
| + } |
| + return; |
| + } |
| + int separator = line.indexOf(':'); |
| + String name = line.substring(0, separator).toLowerCase().trim(); |
| + String value = line.substring(separator + 1).trim(); |
| + if (name == HttpHeaders.TRANSFER_ENCODING && |
| + value.toLowerCase() != 'identity') { |
| + throw new UnsupportedError( |
| + 'only identity transfer encoding is accepted'); |
| + } |
| + if (name == HttpHeaders.CONTENT_LENGTH) { |
| + contentLength = int.parse(value); |
| + } |
| + if (!headers.containsKey(name)) { |
| + headers[name] = []; |
| + } |
| + headers[name].add(value); |
| + } else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) { |
| + statusCode = int |
| + .parse(line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length)); |
| + reasonPhrase = line.substring('HTTP/1.x xxx '.length); |
| + inHeader = true; |
| + } else { |
| + throw new UnsupportedError('unsupported http response format'); |
| + } |
| + } |
| + |
| + var lineDecoder = new _LineDecoder.withCallback(processLine); |
| + |
| + try { |
| + while (!inHeader || |
| + !inBody || |
| + ((contentRead + lineDecoder.bufferedBytes) < contentLength)) { |
| + var bytes = socket.readSync(1024); |
| + |
| + if (bytes == null || bytes.length == 0) { |
| + break; |
| + } |
| + lineDecoder.add(bytes); |
| + } |
| + } finally { |
| + try { |
| + lineDecoder.close(); |
| + } finally { |
| + socket.closeSync(); |
| + } |
| + } |
| + |
| + return new SyncHttpClientResponse._( |
| + reasonPhrase: reasonPhrase, |
| + statusCode: statusCode, |
| + body: body.toString(), |
| + headers: headers); |
| + } |
| + |
| + SyncHttpClientResponse._( |
| + {this.reasonPhrase, this.statusCode, this.body, headers}) |
| + : this.headers = new _SyncHttpClientResponseHeaders(headers); |
| +} |
| + |
| +class _SyncHttpClientResponseHeaders implements HttpHeaders { |
| + final Map<String, List<String>> _headers; |
| + |
| + _SyncHttpClientResponseHeaders(this._headers); |
| + |
| + @override |
| + List<String> operator [](String name) => _headers[name]; |
| + |
| + @override |
| + void add(String name, Object value) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + bool get chunkedTransferEncoding => null; |
| + |
| + @override |
| + void set chunkedTransferEncoding(bool _chunkedTransferEncoding) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + int get contentLength { |
| + String val = value(HttpHeaders.CONTENT_LENGTH); |
| + if (val != null) { |
| + return int.parse(val, onError: (_) => null); |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void set contentLength(int _contentLength) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + ContentType get contentType { |
| + var val = value(HttpHeaders.CONTENT_TYPE); |
| + if (val != null) { |
| + return ContentType.parse(val); |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void set contentType(ContentType _contentType) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + void set date(DateTime _date) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + DateTime get date { |
| + var val = value(HttpHeaders.DATE); |
| + if (val != null) { |
| + return DateTime.parse(val); |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void set expires(DateTime _expires) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + DateTime get expires { |
| + var val = value(HttpHeaders.EXPIRES); |
| + if (val != null) { |
| + return DateTime.parse(val); |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void forEach(void f(String name, List<String> values)) => _headers.forEach(f); |
| + |
| + @override |
| + void set host(String _host) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + String get host { |
| + var val = value(HttpHeaders.HOST); |
| + if (val != null) { |
| + return Uri.parse(val).host; |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + DateTime get ifModifiedSince { |
| + var val = value(HttpHeaders.IF_MODIFIED_SINCE); |
| + if (val != null) { |
| + return DateTime.parse(val); |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void set ifModifiedSince(DateTime _ifModifiedSince) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + void noFolding(String name) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + bool get persistentConnection => false; |
| + |
| + @override |
| + void set persistentConnection(bool _persistentConnection) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + void set port(int _port) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + int get port { |
| + var val = value(HttpHeaders.HOST); |
| + if (val != null) { |
| + return Uri.parse(val).port; |
| + } |
| + return null; |
| + } |
| + |
| + @override |
| + void remove(String name, Object value) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + void removeAll(String name) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + void set(String name, Object value) { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| + |
| + @override |
| + String value(String name) { |
| + var val = this[name]; |
| + if (val == null || val.isEmpty) { |
| + return null; |
| + } else if (val.length == 1) { |
| + return val[0]; |
| + } else { |
| + throw new HttpException('header $name has more than one value'); |
| + } |
| + } |
| + |
| + @override |
| + void clear() { |
| + throw new UnsupportedError('Response headers are immutable'); |
| + } |
| +} |