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..aa5faa3d2b0246af82a09232e716fb27afa6646c |
--- /dev/null |
+++ b/lib/src/sync_http.dart |
@@ -0,0 +1,596 @@ |
+// 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'; |
+ |
+ /// The length of the request body. Is set to null when no body exists. |
+ int get contentLength => hasBody ? _body.length : null; |
+ |
+ HttpHeaders _headers; |
+ |
+ /// The headers associated with the HTTP request. |
+ HttpHeaders get headers { |
+ if (_headers == null) { |
+ _headers = new _SyncHttpClientRequestHeaders(this); |
+ } |
+ return _headers; |
+ } |
+ |
+ /// The type of HTTP request being made. |
+ final String method; |
+ |
+ /// The Uri the HTTP request will be sent to. |
+ final Uri uri; |
+ |
+ /// The default encoding for the HTTP request (UTF8). |
+ final Encoding encoding = UTF8; |
+ |
+ /// The body of the HTTP request. This can be empty if there is no body |
+ /// associated with the request. |
+ final BytesBuilder _body; |
+ |
+ /// The synchronous socket used to initiate the HTTP request. |
+ 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 HTTP request. |
+ void write(Object obj) { |
+ if (hasBody) { |
+ _body.add(encoding.encoder.convert(obj.toString())); |
+ } else { |
+ throw new StateError('write not allowed for method $method'); |
+ } |
+ } |
+ |
+ /// Specifies whether or not the HTTP request has a body. |
+ 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 { |
+ /// The length of the body associated with the HTTP response. |
+ int get contentLength => headers.contentLength; |
+ |
+ /// The headers associated with the HTTP response. |
+ final HttpHeaders headers; |
+ |
+ /// A short textual description of the status code associated with the HTTP |
+ /// response. |
+ final String reasonPhrase; |
+ |
+ /// The resulting HTTP status code associated with the HTTP response. |
+ final int statusCode; |
+ |
+ /// The body of the HTTP response. |
+ final String body; |
+ |
+ /// Creates an instance of [SyncHttpClientResponse] that contains the response |
+ /// sent by the HTTP server over [socket]. |
+ 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'); |
+ } |
+} |