| 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');
|
| + }
|
| +}
|
|
|