Copying sources from dart:io
diff --git a/.analysis_options b/.analysis_options
new file mode 100644
index 0000000..a10d4c5
--- /dev/null
+++ b/.analysis_options
@@ -0,0 +1,2 @@
+analyzer:
+ strong-mode: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7dbf035
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
\ No newline at end of file
diff --git a/.status b/.status
new file mode 100644
index 0000000..a5f8562
--- /dev/null
+++ b/.status
@@ -0,0 +1,25 @@
+# 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.
+
+# Skip non-test files ending with "_test".
+packages/*: Skip
+*/packages/*: Skip
+*/*/packages/*: Skip
+*/*/*/packages/*: Skip
+*/*/*/*packages/*: Skip
+*/*/*/*/*packages/*: Skip
+
+# Only run tests from the build directory, since we don't care about the
+# difference between transformed an untransformed code.
+test/*: Skip
+
+[ $browser ]
+build/test/io/*: Fail, OK # Uses dart:io.
+
+[ $runtime == vm ]
+build/test/html/*: Skip # Uses dart:html.
+
+[ $runtime == drt ]
+build/test/html/client_test: Skip # Issue 18566
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..356667a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.10.0
+
+Initial check-in.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..389ce98
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2017, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..861945b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# http_io
+
+The HTTP APIs in `dart:io` are pure Dart code. To enable faster development and
+bug fixes, these APIS are moving out of `dart:io` into this package.
diff --git a/lib/http_io.dart b/lib/http_io.dart
new file mode 100644
index 0000000..cca0267
--- /dev/null
+++ b/lib/http_io.dart
@@ -0,0 +1,2034 @@
+// Copyright (c) 2013, 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.
+
+library http_io;
+
+import 'dart:async';
+import 'dart:collection'
+ show
+ HashMap,
+ HashSet,
+ Queue,
+ ListQueue,
+ LinkedList,
+ LinkedListEntry,
+ UnmodifiableMapView;
+import 'dart:convert';
+import 'dart:developer' hide log;
+import 'dart:math';
+import 'dart:io';
+import 'dart:typed_data';
+
+part 'src/crypto.dart';
+part 'src/http_date.dart';
+part 'src/http_headers.dart';
+part 'src/http_impl.dart';
+part 'src/http_parser.dart';
+part 'src/http_session.dart';
+part 'src/overrides.dart';
+
+/**
+ * HTTP status codes.
+ */
+abstract class HttpStatus {
+ static const int CONTINUE = 100;
+ static const int SWITCHING_PROTOCOLS = 101;
+ static const int OK = 200;
+ static const int CREATED = 201;
+ static const int ACCEPTED = 202;
+ static const int NON_AUTHORITATIVE_INFORMATION = 203;
+ static const int NO_CONTENT = 204;
+ static const int RESET_CONTENT = 205;
+ static const int PARTIAL_CONTENT = 206;
+ static const int MULTIPLE_CHOICES = 300;
+ static const int MOVED_PERMANENTLY = 301;
+ static const int FOUND = 302;
+ static const int MOVED_TEMPORARILY = 302; // Common alias for FOUND.
+ static const int SEE_OTHER = 303;
+ static const int NOT_MODIFIED = 304;
+ static const int USE_PROXY = 305;
+ static const int TEMPORARY_REDIRECT = 307;
+ static const int BAD_REQUEST = 400;
+ static const int UNAUTHORIZED = 401;
+ static const int PAYMENT_REQUIRED = 402;
+ static const int FORBIDDEN = 403;
+ static const int NOT_FOUND = 404;
+ static const int METHOD_NOT_ALLOWED = 405;
+ static const int NOT_ACCEPTABLE = 406;
+ static const int PROXY_AUTHENTICATION_REQUIRED = 407;
+ static const int REQUEST_TIMEOUT = 408;
+ static const int CONFLICT = 409;
+ static const int GONE = 410;
+ static const int LENGTH_REQUIRED = 411;
+ static const int PRECONDITION_FAILED = 412;
+ static const int REQUEST_ENTITY_TOO_LARGE = 413;
+ static const int REQUEST_URI_TOO_LONG = 414;
+ static const int UNSUPPORTED_MEDIA_TYPE = 415;
+ static const int REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+ static const int EXPECTATION_FAILED = 417;
+ static const int UPGRADE_REQUIRED = 426;
+ static const int INTERNAL_SERVER_ERROR = 500;
+ static const int NOT_IMPLEMENTED = 501;
+ static const int BAD_GATEWAY = 502;
+ static const int SERVICE_UNAVAILABLE = 503;
+ static const int GATEWAY_TIMEOUT = 504;
+ static const int HTTP_VERSION_NOT_SUPPORTED = 505;
+ // Client generated status code.
+ static const int NETWORK_CONNECT_TIMEOUT_ERROR = 599;
+}
+
+/**
+ * A server that delivers content, such as web pages, using the HTTP protocol.
+ *
+ * The HttpServer is a [Stream] that provides [HttpRequest] objects. Each
+ * HttpRequest has an associated [HttpResponse] object.
+ * The server responds to a request by writing to that HttpResponse object.
+ * The following example shows how to bind an HttpServer to an IPv6
+ * [InternetAddress] on port 80 (the standard port for HTTP servers)
+ * and how to listen for requests.
+ * Port 80 is the default HTTP port. However, on most systems accessing
+ * this requires super-user privileges. For local testing consider
+ * using a non-reserved port (1024 and above).
+ *
+ * import 'dart:io';
+ *
+ * main() {
+ * HttpServer
+ * .bind(InternetAddress.ANY_IP_V6, 80)
+ * .then((server) {
+ * server.listen((HttpRequest request) {
+ * request.response.write('Hello, world!');
+ * request.response.close();
+ * });
+ * });
+ * }
+ *
+ * Incomplete requests, in which all or part of the header is missing, are
+ * ignored, and no exceptions or HttpRequest objects are generated for them.
+ * Likewise, when writing to an HttpResponse, any [Socket] exceptions are
+ * ignored and any future writes are ignored.
+ *
+ * The HttpRequest exposes the request headers and provides the request body,
+ * if it exists, as a Stream of data. If the body is unread, it is drained
+ * when the server writes to the HttpResponse or closes it.
+ *
+ * ## Bind with a secure HTTPS connection
+ *
+ * Use [bindSecure] to create an HTTPS server.
+ *
+ * The server presents a certificate to the client. The certificate
+ * chain and the private key are set in the [SecurityContext]
+ * object that is passed to [bindSecure].
+ *
+ * import 'dart:io';
+ * import "dart:isolate";
+ *
+ * main() {
+ * SecurityContext context = new SecurityContext();
+ * var chain =
+ * Platform.script.resolve('certificates/server_chain.pem')
+ * .toFilePath();
+ * var key =
+ * Platform.script.resolve('certificates/server_key.pem')
+ * .toFilePath();
+ * context.useCertificateChain(chain);
+ * context.usePrivateKey(key, password: 'dartdart');
+ *
+ * HttpServer
+ * .bindSecure(InternetAddress.ANY_IP_V6,
+ * 443,
+ * context)
+ * .then((server) {
+ * server.listen((HttpRequest request) {
+ * request.response.write('Hello, world!');
+ * request.response.close();
+ * });
+ * });
+ * }
+ *
+ * The certificates and keys are PEM files, which can be created and
+ * managed with the tools in OpenSSL.
+ *
+ * ## Connect to a server socket
+ *
+ * You can use the [listenOn] constructor to attach an HTTP server to
+ * a [ServerSocket].
+ *
+ * import 'dart:io';
+ *
+ * main() {
+ * ServerSocket.bind(InternetAddress.ANY_IP_V6, 80)
+ * .then((serverSocket) {
+ * HttpServer httpserver = new HttpServer.listenOn(serverSocket);
+ * serverSocket.listen((Socket socket) {
+ * socket.write('Hello, client.');
+ * });
+ * });
+ * }
+ *
+ * ## Other resources
+ *
+ * * HttpServer is a Stream. Refer to the [Stream] class for information
+ * about the streaming qualities of an HttpServer.
+ * Pausing the subscription of the stream, pauses at the OS level.
+ *
+ * * The [shelf](https://pub.dartlang.org/packages/shelf)
+ * package on pub.dartlang.org contains a set of high-level classes that,
+ * together with this class, makes it easy to provide content through HTTP
+ * servers.
+ */
+abstract class HttpServer implements Stream<HttpRequest> {
+ /**
+ * Gets and sets the default value of the `Server` header for all responses
+ * generated by this [HttpServer].
+ *
+ * If [serverHeader] is `null`, no `Server` header will be added to each
+ * response.
+ *
+ * The default value is `null`.
+ */
+ String serverHeader;
+
+ /**
+ * Default set of headers added to all response objects.
+ *
+ * By default the following headers are in this set:
+ *
+ * Content-Type: text/plain; charset=utf-8
+ * X-Frame-Options: SAMEORIGIN
+ * X-Content-Type-Options: nosniff
+ * X-XSS-Protection: 1; mode=block
+ *
+ * If the `Server` header is added here and the `serverHeader` is set as
+ * well then the value of `serverHeader` takes precedence.
+ */
+ HttpHeaders get defaultResponseHeaders;
+
+ /**
+ * Whether the [HttpServer] should compress the content, if possible.
+ *
+ * The content can only be compressed when the response is using
+ * chunked Transfer-Encoding and the incoming request has `gzip`
+ * as an accepted encoding in the Accept-Encoding header.
+ *
+ * The default value is `false` (compression disabled).
+ * To enable, set `autoCompress` to `true`.
+ */
+ bool autoCompress;
+
+ /**
+ * Gets or sets the timeout used for idle keep-alive connections. If no
+ * further request is seen within [idleTimeout] after the previous request was
+ * completed, the connection is dropped.
+ *
+ * Default is 120 seconds.
+ *
+ * Note that it may take up to `2 * idleTimeout` before a idle connection is
+ * aborted.
+ *
+ * To disable, set [idleTimeout] to `null`.
+ */
+ Duration idleTimeout;
+
+ /**
+ * Starts listening for HTTP requests on the specified [address] and
+ * [port].
+ *
+ * The [address] can either be a [String] or an
+ * [InternetAddress]. If [address] is a [String], [bind] will
+ * perform a [InternetAddress.lookup] and use the first value in the
+ * list. To listen on the loopback adapter, which will allow only
+ * incoming connections from the local host, use the value
+ * [InternetAddress.LOOPBACK_IP_V4] or
+ * [InternetAddress.LOOPBACK_IP_V6]. To allow for incoming
+ * connection from the network use either one of the values
+ * [InternetAddress.ANY_IP_V4] or [InternetAddress.ANY_IP_V6] to
+ * bind to all interfaces or the IP address of a specific interface.
+ *
+ * If an IP version 6 (IPv6) address is used, both IP version 6
+ * (IPv6) and version 4 (IPv4) connections will be accepted. To
+ * restrict this to version 6 (IPv6) only, use [v6Only] to set
+ * version 6 only. However, if the address is
+ * [InternetAddress.LOOPBACK_IP_V6], only IP version 6 (IPv6) connections
+ * will be accepted.
+ *
+ * If [port] has the value [:0:] an ephemeral port will be chosen by
+ * the system. The actual port used can be retrieved using the
+ * [port] getter.
+ *
+ * The optional argument [backlog] can be used to specify the listen
+ * backlog for the underlying OS listen setup. If [backlog] has the
+ * value of [:0:] (the default) a reasonable value will be chosen by
+ * the system.
+ *
+ * The optional argument [shared] specifies whether additional HttpServer
+ * objects can bind to the same combination of `address`, `port` and `v6Only`.
+ * If `shared` is `true` and more `HttpServer`s from this isolate or other
+ * isolates are bound to the port, then the incoming connections will be
+ * distributed among all the bound `HttpServer`s. Connections can be
+ * distributed over multiple isolates this way.
+ */
+ static Future<HttpServer> bind(address, int port,
+ {int backlog: 0, bool v6Only: false, bool shared: false}) =>
+ _HttpServer.bind(address, port, backlog, v6Only, shared);
+
+ /**
+ * The [address] can either be a [String] or an
+ * [InternetAddress]. If [address] is a [String], [bind] will
+ * perform a [InternetAddress.lookup] and use the first value in the
+ * list. To listen on the loopback adapter, which will allow only
+ * incoming connections from the local host, use the value
+ * [InternetAddress.LOOPBACK_IP_V4] or
+ * [InternetAddress.LOOPBACK_IP_V6]. To allow for incoming
+ * connection from the network use either one of the values
+ * [InternetAddress.ANY_IP_V4] or [InternetAddress.ANY_IP_V6] to
+ * bind to all interfaces or the IP address of a specific interface.
+ *
+ * If an IP version 6 (IPv6) address is used, both IP version 6
+ * (IPv6) and version 4 (IPv4) connections will be accepted. To
+ * restrict this to version 6 (IPv6) only, use [v6Only] to set
+ * version 6 only.
+ *
+ * If [port] has the value [:0:] an ephemeral port will be chosen by
+ * the system. The actual port used can be retrieved using the
+ * [port] getter.
+ *
+ * The optional argument [backlog] can be used to specify the listen
+ * backlog for the underlying OS listen setup. If [backlog] has the
+ * value of [:0:] (the default) a reasonable value will be chosen by
+ * the system.
+ *
+ * If [requestClientCertificate] is true, the server will
+ * request clients to authenticate with a client certificate.
+ * The server will advertise the names of trusted issuers of client
+ * certificates, getting them from a [SecurityContext], where they have been
+ * set using [SecurityContext.setClientAuthorities].
+ *
+ * The optional argument [shared] specifies whether additional HttpServer
+ * objects can bind to the same combination of `address`, `port` and `v6Only`.
+ * If `shared` is `true` and more `HttpServer`s from this isolate or other
+ * isolates are bound to the port, then the incoming connections will be
+ * distributed among all the bound `HttpServer`s. Connections can be
+ * distributed over multiple isolates this way.
+ */
+
+ static Future<HttpServer> bindSecure(
+ address, int port, SecurityContext context,
+ {int backlog: 0,
+ bool v6Only: false,
+ bool requestClientCertificate: false,
+ bool shared: false}) =>
+ _HttpServer.bindSecure(address, port, context, backlog, v6Only,
+ requestClientCertificate, shared);
+
+ /**
+ * Attaches the HTTP server to an existing [ServerSocket]. When the
+ * [HttpServer] is closed, the [HttpServer] will just detach itself,
+ * closing current connections but not closing [serverSocket].
+ */
+ factory HttpServer.listenOn(ServerSocket serverSocket) =>
+ new _HttpServer.listenOn(serverSocket);
+
+ /**
+ * Permanently stops this [HttpServer] from listening for new
+ * connections. This closes the [Stream] of [HttpRequest]s with a
+ * done event. The returned future completes when the server is
+ * stopped. For a server started using [bind] or [bindSecure] this
+ * means that the port listened on no longer in use.
+ *
+ * If [force] is `true`, active connections will be closed immediately.
+ */
+ Future close({bool force: false});
+
+ /**
+ * Returns the port that the server is listening on. This can be
+ * used to get the actual port used when a value of 0 for [:port:] is
+ * specified in the [bind] or [bindSecure] call.
+ */
+ int get port;
+
+ /**
+ * Returns the address that the server is listening on. This can be
+ * used to get the actual address used, when the address is fetched by
+ * a lookup from a hostname.
+ */
+ InternetAddress get address;
+
+ /**
+ * Sets the timeout, in seconds, for sessions of this [HttpServer].
+ * The default timeout is 20 minutes.
+ */
+ set sessionTimeout(int timeout);
+
+ /**
+ * Returns an [HttpConnectionsInfo] object summarizing the number of
+ * current connections handled by the server.
+ */
+ HttpConnectionsInfo connectionsInfo();
+}
+
+/**
+ * Summary statistics about an [HttpServer]s current socket connections.
+ */
+class HttpConnectionsInfo {
+ /**
+ * Total number of socket connections.
+ */
+ int total = 0;
+
+ /**
+ * Number of active connections where actual request/response
+ * processing is active.
+ */
+ int active = 0;
+
+ /**
+ * Number of idle connections held by clients as persistent connections.
+ */
+ int idle = 0;
+
+ /**
+ * Number of connections which are preparing to close. Note: These
+ * connections are also part of the [:active:] count as they might
+ * still be sending data to the client before finally closing.
+ */
+ int closing = 0;
+}
+
+/**
+ * Headers for HTTP requests and responses.
+ *
+ * In some situations, headers are immutable:
+ *
+ * * HttpRequest and HttpClientResponse always have immutable headers.
+ *
+ * * HttpResponse and HttpClientRequest have immutable headers
+ * from the moment the body is written to.
+ *
+ * In these situations, the mutating methods throw exceptions.
+ *
+ * For all operations on HTTP headers the header name is
+ * case-insensitive.
+ *
+ * To set the value of a header use the `set()` method:
+ *
+ * request.headers.set(HttpHeaders.CACHE_CONTROL,
+ * 'max-age=3600, must-revalidate');
+ *
+ * To retrieve the value of a header use the `value()` method:
+ *
+ * print(request.headers.value(HttpHeaders.USER_AGENT));
+ *
+ * An HttpHeaders object holds a list of values for each name
+ * as the standard allows. In most cases a name holds only a single value,
+ * The most common mode of operation is to use `set()` for setting a value,
+ * and `value()` for retrieving a value.
+ */
+abstract class HttpHeaders {
+ static const ACCEPT = "accept";
+ static const ACCEPT_CHARSET = "accept-charset";
+ static const ACCEPT_ENCODING = "accept-encoding";
+ static const ACCEPT_LANGUAGE = "accept-language";
+ static const ACCEPT_RANGES = "accept-ranges";
+ static const AGE = "age";
+ static const ALLOW = "allow";
+ static const AUTHORIZATION = "authorization";
+ static const CACHE_CONTROL = "cache-control";
+ static const CONNECTION = "connection";
+ static const CONTENT_ENCODING = "content-encoding";
+ static const CONTENT_LANGUAGE = "content-language";
+ static const CONTENT_LENGTH = "content-length";
+ static const CONTENT_LOCATION = "content-location";
+ static const CONTENT_MD5 = "content-md5";
+ static const CONTENT_RANGE = "content-range";
+ static const CONTENT_TYPE = "content-type";
+ static const DATE = "date";
+ static const ETAG = "etag";
+ static const EXPECT = "expect";
+ static const EXPIRES = "expires";
+ static const FROM = "from";
+ static const HOST = "host";
+ static const IF_MATCH = "if-match";
+ static const IF_MODIFIED_SINCE = "if-modified-since";
+ static const IF_NONE_MATCH = "if-none-match";
+ static const IF_RANGE = "if-range";
+ static const IF_UNMODIFIED_SINCE = "if-unmodified-since";
+ static const LAST_MODIFIED = "last-modified";
+ static const LOCATION = "location";
+ static const MAX_FORWARDS = "max-forwards";
+ static const PRAGMA = "pragma";
+ static const PROXY_AUTHENTICATE = "proxy-authenticate";
+ static const PROXY_AUTHORIZATION = "proxy-authorization";
+ static const RANGE = "range";
+ static const REFERER = "referer";
+ static const RETRY_AFTER = "retry-after";
+ static const SERVER = "server";
+ static const TE = "te";
+ static const TRAILER = "trailer";
+ static const TRANSFER_ENCODING = "transfer-encoding";
+ static const UPGRADE = "upgrade";
+ static const USER_AGENT = "user-agent";
+ static const VARY = "vary";
+ static const VIA = "via";
+ static const WARNING = "warning";
+ static const WWW_AUTHENTICATE = "www-authenticate";
+
+ // Cookie headers from RFC 6265.
+ static const COOKIE = "cookie";
+ static const SET_COOKIE = "set-cookie";
+
+ static const GENERAL_HEADERS = const [
+ CACHE_CONTROL,
+ CONNECTION,
+ DATE,
+ PRAGMA,
+ TRAILER,
+ TRANSFER_ENCODING,
+ UPGRADE,
+ VIA,
+ WARNING
+ ];
+
+ static const ENTITY_HEADERS = const [
+ ALLOW,
+ CONTENT_ENCODING,
+ CONTENT_LANGUAGE,
+ CONTENT_LENGTH,
+ CONTENT_LOCATION,
+ CONTENT_MD5,
+ CONTENT_RANGE,
+ CONTENT_TYPE,
+ EXPIRES,
+ LAST_MODIFIED
+ ];
+
+ static const RESPONSE_HEADERS = const [
+ ACCEPT_RANGES,
+ AGE,
+ ETAG,
+ LOCATION,
+ PROXY_AUTHENTICATE,
+ RETRY_AFTER,
+ SERVER,
+ VARY,
+ WWW_AUTHENTICATE
+ ];
+
+ static const REQUEST_HEADERS = const [
+ ACCEPT,
+ ACCEPT_CHARSET,
+ ACCEPT_ENCODING,
+ ACCEPT_LANGUAGE,
+ AUTHORIZATION,
+ EXPECT,
+ FROM,
+ HOST,
+ IF_MATCH,
+ IF_MODIFIED_SINCE,
+ IF_NONE_MATCH,
+ IF_RANGE,
+ IF_UNMODIFIED_SINCE,
+ MAX_FORWARDS,
+ PROXY_AUTHORIZATION,
+ RANGE,
+ REFERER,
+ TE,
+ USER_AGENT
+ ];
+
+ /**
+ * Gets and sets the date. The value of this property will
+ * reflect the 'date' header.
+ */
+ DateTime date;
+
+ /**
+ * Gets and sets the expiry date. The value of this property will
+ * reflect the 'expires' header.
+ */
+ DateTime expires;
+
+ /**
+ * Gets and sets the "if-modified-since" date. The value of this property will
+ * reflect the "if-modified-since" header.
+ */
+ DateTime ifModifiedSince;
+
+ /**
+ * Gets and sets the host part of the 'host' header for the
+ * connection.
+ */
+ String host;
+
+ /**
+ * Gets and sets the port part of the 'host' header for the
+ * connection.
+ */
+ int port;
+
+ /**
+ * Gets and sets the content type. Note that the content type in the
+ * header will only be updated if this field is set
+ * directly. Mutating the returned current value will have no
+ * effect.
+ */
+ ContentType contentType;
+
+ /**
+ * Gets and sets the content length header value.
+ */
+ int contentLength;
+
+ /**
+ * Gets and sets the persistent connection header value.
+ */
+ bool persistentConnection;
+
+ /**
+ * Gets and sets the chunked transfer encoding header value.
+ */
+ bool chunkedTransferEncoding;
+
+ /**
+ * Returns the list of values for the header named [name]. If there
+ * is no header with the provided name, [:null:] will be returned.
+ */
+ List<String> operator [](String name);
+
+ /**
+ * Convenience method for the value for a single valued header. If
+ * there is no header with the provided name, [:null:] will be
+ * returned. If the header has more than one value an exception is
+ * thrown.
+ */
+ String value(String name);
+
+ /**
+ * Adds a header value. The header named [name] will have the value
+ * [value] added to its list of values. Some headers are single
+ * valued, and for these adding a value will replace the previous
+ * value. If the value is of type DateTime a HTTP date format will be
+ * applied. If the value is a [:List:] each element of the list will
+ * be added separately. For all other types the default [:toString:]
+ * method will be used.
+ */
+ void add(String name, Object value);
+
+ /**
+ * Sets a header. The header named [name] will have all its values
+ * cleared before the value [value] is added as its value.
+ */
+ void set(String name, Object value);
+
+ /**
+ * Removes a specific value for a header name. Some headers have
+ * system supplied values and for these the system supplied values
+ * will still be added to the collection of values for the header.
+ */
+ void remove(String name, Object value);
+
+ /**
+ * Removes all values for the specified header name. Some headers
+ * have system supplied values and for these the system supplied
+ * values will still be added to the collection of values for the
+ * header.
+ */
+ void removeAll(String name);
+
+ /**
+ * Enumerates the headers, applying the function [f] to each
+ * header. The header name passed in [:name:] will be all lower
+ * case.
+ */
+ void forEach(void f(String name, List<String> values));
+
+ /**
+ * Disables folding for the header named [name] when sending the HTTP
+ * header. By default, multiple header values are folded into a
+ * single header line by separating the values with commas. The
+ * 'set-cookie' header has folding disabled by default.
+ */
+ void noFolding(String name);
+
+ /**
+ * Remove all headers. Some headers have system supplied values and
+ * for these the system supplied values will still be added to the
+ * collection of values for the header.
+ */
+ void clear();
+}
+
+/**
+ * Representation of a header value in the form:
+ *
+ * [:value; parameter1=value1; parameter2=value2:]
+ *
+ * [HeaderValue] can be used to conveniently build and parse header
+ * values on this form.
+ *
+ * To build an [:accepts:] header with the value
+ *
+ * text/plain; q=0.3, text/html
+ *
+ * use code like this:
+ *
+ * HttpClientRequest request = ...;
+ * var v = new HeaderValue("text/plain", {"q": "0.3"});
+ * request.headers.add(HttpHeaders.ACCEPT, v);
+ * request.headers.add(HttpHeaders.ACCEPT, "text/html");
+ *
+ * To parse the header values use the [:parse:] static method.
+ *
+ * HttpRequest request = ...;
+ * List<String> values = request.headers[HttpHeaders.ACCEPT];
+ * values.forEach((value) {
+ * HeaderValue v = HeaderValue.parse(value);
+ * // Use v.value and v.parameters
+ * });
+ *
+ * An instance of [HeaderValue] is immutable.
+ */
+abstract class HeaderValue {
+ /**
+ * Creates a new header value object setting the value and parameters.
+ */
+ factory HeaderValue([String value = "", Map<String, String> parameters]) {
+ return new _HeaderValue(value, parameters);
+ }
+
+ /**
+ * Creates a new header value object from parsing a header value
+ * string with both value and optional parameters.
+ */
+ static HeaderValue parse(String value,
+ {String parameterSeparator: ";",
+ String valueSeparator: null,
+ bool preserveBackslash: false}) {
+ return _HeaderValue.parse(value,
+ parameterSeparator: parameterSeparator,
+ valueSeparator: valueSeparator,
+ preserveBackslash: preserveBackslash);
+ }
+
+ /**
+ * Gets the header value.
+ */
+ String get value;
+
+ /**
+ * Gets the map of parameters.
+ *
+ * This map cannot be modified. Invoking any operation which would
+ * modify the map will throw [UnsupportedError].
+ */
+ Map<String, String> get parameters;
+
+ /**
+ * Returns the formatted string representation in the form:
+ *
+ * value; parameter1=value1; parameter2=value2
+ */
+ String toString();
+}
+
+abstract class HttpSession implements Map {
+ /**
+ * Gets the id for the current session.
+ */
+ String get id;
+
+ /**
+ * Destroys the session. This will terminate the session and any further
+ * connections with this id will be given a new id and session.
+ */
+ void destroy();
+
+ /**
+ * Sets a callback that will be called when the session is timed out.
+ */
+ void set onTimeout(void callback());
+
+ /**
+ * Is true if the session has not been sent to the client yet.
+ */
+ bool get isNew;
+}
+
+/**
+ * Representation of a content type. An instance of [ContentType] is
+ * immutable.
+ */
+abstract class ContentType implements HeaderValue {
+ /**
+ * Content type for plain text using UTF-8 encoding.
+ *
+ * text/plain; charset=utf-8
+ */
+ static final TEXT = new ContentType("text", "plain", charset: "utf-8");
+
+ /**
+ * Content type for HTML using UTF-8 encoding.
+ *
+ * text/html; charset=utf-8
+ */
+ static final HTML = new ContentType("text", "html", charset: "utf-8");
+
+ /**
+ * Content type for JSON using UTF-8 encoding.
+ *
+ * application/json; charset=utf-8
+ */
+ static final JSON = new ContentType("application", "json", charset: "utf-8");
+
+ /**
+ * Content type for binary data.
+ *
+ * application/octet-stream
+ */
+ static final BINARY = new ContentType("application", "octet-stream");
+
+ /**
+ * Creates a new content type object setting the primary type and
+ * sub type. The charset and additional parameters can also be set
+ * using [charset] and [parameters]. If charset is passed and
+ * [parameters] contains charset as well the passed [charset] will
+ * override the value in parameters. Keys passed in parameters will be
+ * converted to lower case. The `charset` entry, whether passed as `charset`
+ * or in `parameters`, will have its value converted to lower-case.
+ */
+ factory ContentType(String primaryType, String subType,
+ {String charset, Map<String, String> parameters}) {
+ return new _ContentType(primaryType, subType, charset, parameters);
+ }
+
+ /**
+ * Creates a new content type object from parsing a Content-Type
+ * header value. As primary type, sub type and parameter names and
+ * values are not case sensitive all these values will be converted
+ * to lower case. Parsing this string
+ *
+ * text/html; charset=utf-8
+ *
+ * will create a content type object with primary type [:text:], sub
+ * type [:html:] and parameter [:charset:] with value [:utf-8:].
+ */
+ static ContentType parse(String value) {
+ return _ContentType.parse(value);
+ }
+
+ /**
+ * Gets the mime-type, without any parameters.
+ */
+ String get mimeType;
+
+ /**
+ * Gets the primary type.
+ */
+ String get primaryType;
+
+ /**
+ * Gets the sub type.
+ */
+ String get subType;
+
+ /**
+ * Gets the character set.
+ */
+ String get charset;
+}
+
+/**
+ * Representation of a cookie. For cookies received by the server as
+ * Cookie header values only [:name:] and [:value:] fields will be
+ * set. When building a cookie for the 'set-cookie' header in the server
+ * and when receiving cookies in the client as 'set-cookie' headers all
+ * fields can be used.
+ */
+abstract class Cookie {
+ /**
+ * Gets and sets the name.
+ */
+ String name;
+
+ /**
+ * Gets and sets the value.
+ */
+ String value;
+
+ /**
+ * Gets and sets the expiry date.
+ */
+ DateTime expires;
+
+ /**
+ * Gets and sets the max age. A value of [:0:] means delete cookie
+ * now.
+ */
+ int maxAge;
+
+ /**
+ * Gets and sets the domain.
+ */
+ String domain;
+
+ /**
+ * Gets and sets the path.
+ */
+ String path;
+
+ /**
+ * Gets and sets whether this cookie is secure.
+ */
+ bool secure;
+
+ /**
+ * Gets and sets whether this cookie is HTTP only.
+ */
+ bool httpOnly;
+
+ /**
+ * Creates a new cookie optionally setting the name and value.
+ *
+ * By default the value of `httpOnly` will be set to `true`.
+ */
+ factory Cookie([String name, String value]) => new _Cookie(name, value);
+
+ /**
+ * Creates a new cookie by parsing a header value from a 'set-cookie'
+ * header.
+ */
+ factory Cookie.fromSetCookieValue(String value) {
+ return new _Cookie.fromSetCookieValue(value);
+ }
+
+ /**
+ * Returns the formatted string representation of the cookie. The
+ * string representation can be used for for setting the Cookie or
+ * 'set-cookie' headers
+ */
+ String toString();
+}
+
+/**
+ * A server-side object
+ * that contains the content of and information about an HTTP request.
+ *
+ * __Note__: Check out the
+ * [http_server](http://pub.dartlang.org/packages/http_server)
+ * package, which makes working with the low-level
+ * dart:io HTTP server subsystem easier.
+ *
+ * `HttpRequest` objects are generated by an [HttpServer],
+ * which listens for HTTP requests on a specific host and port.
+ * For each request received, the HttpServer, which is a [Stream],
+ * generates an `HttpRequest` object and adds it to the stream.
+ *
+ * An `HttpRequest` object delivers the body content of the request
+ * as a stream of byte lists.
+ * The object also contains information about the request,
+ * such as the method, URI, and headers.
+ *
+ * In the following code, an HttpServer listens
+ * for HTTP requests. When the server receives a request,
+ * it uses the HttpRequest object's `method` property to dispatch requests.
+ *
+ * final HOST = InternetAddress.LOOPBACK_IP_V4;
+ * final PORT = 80;
+ *
+ * HttpServer.bind(HOST, PORT).then((_server) {
+ * _server.listen((HttpRequest request) {
+ * switch (request.method) {
+ * case 'GET':
+ * handleGetRequest(request);
+ * break;
+ * case 'POST':
+ * ...
+ * }
+ * },
+ * onError: handleError); // listen() failed.
+ * }).catchError(handleError);
+ *
+ * An HttpRequest object provides access to the associated [HttpResponse]
+ * object through the response property.
+ * The server writes its response to the body of the HttpResponse object.
+ * For example, here's a function that responds to a request:
+ *
+ * void handleGetRequest(HttpRequest req) {
+ * HttpResponse res = req.response;
+ * res.write('Received request ${req.method}: ${req.uri.path}');
+ * res.close();
+ * }
+ */
+abstract class HttpRequest implements Stream<List<int>> {
+ /**
+ * The content length of the request body.
+ *
+ * If the size of the request body is not known in advance,
+ * this value is -1.
+ */
+ int get contentLength;
+
+ /**
+ * The method, such as 'GET' or 'POST', for the request.
+ */
+ String get method;
+
+ /**
+ * The URI for the request.
+ *
+ * This provides access to the
+ * path and query string for the request.
+ */
+ Uri get uri;
+
+ /**
+ * The requested URI for the request.
+ *
+ * The returned URI is reconstructed by using http-header fields, to access
+ * otherwise lost information, e.g. host and scheme.
+ *
+ * To reconstruct the scheme, first 'X-Forwarded-Proto' is checked, and then
+ * falling back to server type.
+ *
+ * To reconstruct the host, first 'X-Forwarded-Host' is checked, then 'Host'
+ * and finally calling back to server.
+ */
+ Uri get requestedUri;
+
+ /**
+ * The request headers.
+ *
+ * The returned [HttpHeaders] are immutable.
+ */
+ HttpHeaders get headers;
+
+ /**
+ * The cookies in the request, from the Cookie headers.
+ */
+ List<Cookie> get cookies;
+
+ /**
+ * The persistent connection state signaled by the client.
+ */
+ bool get persistentConnection;
+
+ /**
+ * The client certificate of the client making the request.
+ *
+ * This value is null if the connection is not a secure TLS or SSL connection,
+ * or if the server does not request a client certificate, or if the client
+ * does not provide one.
+ */
+ X509Certificate get certificate;
+
+ /**
+ * The session for the given request.
+ *
+ * If the session is
+ * being initialized by this call, [:isNew:] is true for the returned
+ * session.
+ * See [HttpServer.sessionTimeout] on how to change default timeout.
+ */
+ HttpSession get session;
+
+ /**
+ * The HTTP protocol version used in the request,
+ * either "1.0" or "1.1".
+ */
+ String get protocolVersion;
+
+ /**
+ * Information about the client connection.
+ *
+ * Returns [:null:] if the socket is not available.
+ */
+ HttpConnectionInfo get connectionInfo;
+
+ /**
+ * The [HttpResponse] object, used for sending back the response to the
+ * client.
+ *
+ * If the [contentLength] of the body isn't 0, and the body isn't being read,
+ * any write calls on the [HttpResponse] automatically drain the request
+ * body.
+ */
+ HttpResponse get response;
+}
+
+/**
+ * An HTTP response, which returns the headers and data
+ * from the server to the client in response to an HTTP request.
+ *
+ * Every HttpRequest object provides access to the associated [HttpResponse]
+ * object through the `response` property.
+ * The server sends its response to the client by writing to the
+ * HttpResponse object.
+ *
+ * ## Writing the response
+ *
+ * This class implements [IOSink].
+ * After the header has been set up, the methods
+ * from IOSink, such as `writeln()`, can be used to write
+ * the body of the HTTP response.
+ * Use the `close()` method to close the response and send it to the client.
+ *
+ * server.listen((HttpRequest request) {
+ * request.response.write('Hello, world!');
+ * request.response.close();
+ * });
+ *
+ * When one of the IOSink methods is used for the
+ * first time, the request header is sent. Calling any methods that
+ * change the header after it is sent throws an exception.
+ *
+ * ## Setting the headers
+ *
+ * The HttpResponse object has a number of properties for setting up
+ * the HTTP headers of the response.
+ * When writing string data through the IOSink, the encoding used
+ * is determined from the "charset" parameter of the
+ * "Content-Type" header.
+ *
+ * HttpResponse response = ...
+ * response.headers.contentType
+ * = new ContentType("application", "json", charset: "utf-8");
+ * response.write(...); // Strings written will be UTF-8 encoded.
+ *
+ * If no charset is provided the default of ISO-8859-1 (Latin 1) will
+ * be used.
+ *
+ * HttpResponse response = ...
+ * response.headers.add(HttpHeaders.CONTENT_TYPE, "text/plain");
+ * response.write(...); // Strings written will be ISO-8859-1 encoded.
+ *
+ * An exception is thrown if you use the `write()` method
+ * while an unsupported content-type is set.
+ */
+abstract class HttpResponse implements IOSink {
+ // TODO(ajohnsen): Add documentation of how to pipe a file to the response.
+ /**
+ * Gets and sets the content length of the response. If the size of
+ * the response is not known in advance set the content length to
+ * -1 - which is also the default if not set.
+ */
+ int contentLength;
+
+ /**
+ * Gets and sets the status code. Any integer value is accepted. For
+ * the official HTTP status codes use the fields from
+ * [HttpStatus]. If no status code is explicitly set the default
+ * value [HttpStatus.OK] is used.
+ *
+ * The status code must be set before the body is written
+ * to. Setting the status code after writing to the response body or
+ * closing the response will throw a `StateError`.
+ */
+ int statusCode;
+
+ /**
+ * Gets and sets the reason phrase. If no reason phrase is explicitly
+ * set a default reason phrase is provided.
+ *
+ * The reason phrase must be set before the body is written
+ * to. Setting the reason phrase after writing to the response body
+ * or closing the response will throw a `StateError`.
+ */
+ String reasonPhrase;
+
+ /**
+ * Gets and sets the persistent connection state. The initial value
+ * of this property is the persistent connection state from the
+ * request.
+ */
+ bool persistentConnection;
+
+ /**
+ * Set and get the [deadline] for the response. The deadline is timed from the
+ * time it's set. Setting a new deadline will override any previous deadline.
+ * When a deadline is exceeded, the response will be closed and any further
+ * data ignored.
+ *
+ * To disable a deadline, set the [deadline] to `null`.
+ *
+ * The [deadline] is `null` by default.
+ */
+ Duration deadline;
+
+ /**
+ * Gets or sets if the [HttpResponse] should buffer output.
+ *
+ * Default value is `true`.
+ *
+ * __Note__: Disabling buffering of the output can result in very poor
+ * performance, when writing many small chunks.
+ */
+ bool bufferOutput;
+
+ /**
+ * Returns the response headers.
+ *
+ * The response headers can be modified until the response body is
+ * written to or closed. After that they become immutable.
+ */
+ HttpHeaders get headers;
+
+ /**
+ * Cookies to set in the client (in the 'set-cookie' header).
+ */
+ List<Cookie> get cookies;
+
+ /**
+ * Respond with a redirect to [location].
+ *
+ * The URI in [location] should be absolute, but there are no checks
+ * to enforce that.
+ *
+ * By default the HTTP status code `HttpStatus.MOVED_TEMPORARILY`
+ * (`302`) is used for the redirect, but an alternative one can be
+ * specified using the [status] argument.
+ *
+ * This method will also call `close`, and the returned future is
+ * the future returned by `close`.
+ */
+ Future redirect(Uri location, {int status: HttpStatus.MOVED_TEMPORARILY});
+
+ /**
+ * Detaches the underlying socket from the HTTP server. When the
+ * socket is detached the HTTP server will no longer perform any
+ * operations on it.
+ *
+ * This is normally used when a HTTP upgrade request is received
+ * and the communication should continue with a different protocol.
+ *
+ * If [writeHeaders] is `true`, the status line and [headers] will be written
+ * to the socket before it's detached. If `false`, the socket is detached
+ * immediately, without any data written to the socket. Default is `true`.
+ */
+ Future<Socket> detachSocket({bool writeHeaders: true});
+
+ /**
+ * Gets information about the client connection. Returns [:null:] if the
+ * socket is not available.
+ */
+ HttpConnectionInfo get connectionInfo;
+}
+
+/**
+ * A client that receives content, such as web pages, from
+ * a server using the HTTP protocol.
+ *
+ * HttpClient contains a number of methods to send an [HttpClientRequest]
+ * to an Http server and receive an [HttpClientResponse] back.
+ * For example, you can use the [get], [getUrl], [post], and [postUrl] methods
+ * for GET and POST requests, respectively.
+ *
+ * ## Making a simple GET request: an example
+ *
+ * A `getUrl` request is a two-step process, triggered by two [Future]s.
+ * When the first future completes with a [HttpClientRequest], the underlying
+ * network connection has been established, but no data has been sent.
+ * In the callback function for the first future, the HTTP headers and body
+ * can be set on the request. Either the first write to the request object
+ * or a call to [close] sends the request to the server.
+ *
+ * When the HTTP response is received from the server,
+ * the second future, which is returned by close,
+ * completes with an [HttpClientResponse] object.
+ * This object provides access to the headers and body of the response.
+ * The body is available as a stream implemented by HttpClientResponse.
+ * If a body is present, it must be read. Otherwise, it leads to resource
+ * leaks. Consider using [HttpClientResponse.drain] if the body is unused.
+ *
+ * HttpClient client = new HttpClient();
+ * client.getUrl(Uri.parse("http://www.example.com/"))
+ * .then((HttpClientRequest request) {
+ * // Optionally set up headers...
+ * // Optionally write to the request object...
+ * // Then call close.
+ * ...
+ * return request.close();
+ * })
+ * .then((HttpClientResponse response) {
+ * // Process the response.
+ * ...
+ * });
+ *
+ * The future for [HttpClientRequest] is created by methods such as
+ * [getUrl] and [open].
+ *
+ * ## HTTPS connections
+ *
+ * An HttpClient can make HTTPS requests, connecting to a server using
+ * the TLS (SSL) secure networking protocol. Calling [getUrl] with an
+ * https: scheme will work automatically, if the server's certificate is
+ * signed by a root CA (certificate authority) on the default list of
+ * well-known trusted CAs, compiled by Mozilla.
+ *
+ * To add a custom trusted certificate authority, or to send a client
+ * certificate to servers that request one, pass a [SecurityContext] object
+ * as the optional `context` argument to the `HttpClient` constructor.
+ * The desired security options can be set on the [SecurityContext] object.
+ *
+ * ## Headers
+ *
+ * All HttpClient requests set the following header by default:
+ *
+ * Accept-Encoding: gzip
+ *
+ * This allows the HTTP server to use gzip compression for the body if
+ * possible. If this behavior is not desired set the
+ * `Accept-Encoding` header to something else.
+ * To turn off gzip compression of the response, clear this header:
+ *
+ * request.headers.removeAll(HttpHeaders.ACCEPT_ENCODING)
+ *
+ * ## Closing the HttpClient
+ *
+ * The HttpClient supports persistent connections and caches network
+ * connections to reuse them for multiple requests whenever
+ * possible. This means that network connections can be kept open for
+ * some time after a request has completed. Use HttpClient.close
+ * to force the HttpClient object to shut down and to close the idle
+ * network connections.
+ *
+ * ## Turning proxies on and off
+ *
+ * By default the HttpClient uses the proxy configuration available
+ * from the environment, see [findProxyFromEnvironment]. To turn off
+ * the use of proxies set the [findProxy] property to
+ * [:null:].
+ *
+ * HttpClient client = new HttpClient();
+ * client.findProxy = null;
+ */
+abstract class HttpClient {
+ static const int DEFAULT_HTTP_PORT = 80;
+ static const int DEFAULT_HTTPS_PORT = 443;
+
+ /// Gets and sets the idle timeout of non-active persistent (keep-alive)
+ /// connections.
+ ///
+ /// The default value is 15 seconds.
+ Duration idleTimeout;
+
+ /**
+ * Gets and sets the maximum number of live connections, to a single host.
+ *
+ * Increasing this number may lower performance and take up unwanted
+ * system resources.
+ *
+ * To disable, set to `null`.
+ *
+ * Default is `null`.
+ */
+ int maxConnectionsPerHost;
+
+ /**
+ * Gets and sets whether the body of a response will be automatically
+ * uncompressed.
+ *
+ * The body of an HTTP response can be compressed. In most
+ * situations providing the un-compressed body is most
+ * convenient. Therefore the default behavior is to un-compress the
+ * body. However in some situations (e.g. implementing a transparent
+ * proxy) keeping the uncompressed stream is required.
+ *
+ * NOTE: Headers in the response are never modified. This means
+ * that when automatic un-compression is turned on the value of the
+ * header `Content-Length` will reflect the length of the original
+ * compressed body. Likewise the header `Content-Encoding` will also
+ * have the original value indicating compression.
+ *
+ * NOTE: Automatic un-compression is only performed if the
+ * `Content-Encoding` header value is `gzip`.
+ *
+ * This value affects all responses produced by this client after the
+ * value is changed.
+ *
+ * To disable, set to `false`.
+ *
+ * Default is `true`.
+ */
+ bool autoUncompress;
+
+ /// Gets and sets the default value of the `User-Agent` header for all requests
+ /// generated by this [HttpClient].
+ ///
+ /// The default value is `Dart/<version> (dart:io)`.
+ ///
+ /// If the userAgent is set to `null`, no default `User-Agent` header will be
+ /// added to each request.
+ String userAgent;
+
+ factory HttpClient({SecurityContext context}) {
+ HttpOverrides overrides = HttpOverrides.current;
+ if (overrides == null) {
+ return new _HttpClient(context);
+ }
+ return overrides.createHttpClient(context);
+ }
+
+ /**
+ * Opens a HTTP connection.
+ *
+ * The HTTP method to use is specified in [method], the server is
+ * specified using [host] and [port], and the path (including
+ * a possible query) is specified using [path].
+ * The path may also contain a URI fragment, which will be ignored.
+ *
+ * The `Host` header for the request will be set to the value
+ * [host]:[port]. This can be overridden through the
+ * [HttpClientRequest] interface before the request is sent. NOTE
+ * if [host] is an IP address this will still be set in the `Host`
+ * header.
+ *
+ * For additional information on the sequence of events during an
+ * HTTP transaction, and the objects returned by the futures, see
+ * the overall documentation for the class [HttpClient].
+ */
+ Future<HttpClientRequest> open(
+ String method, String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection.
+ *
+ * The HTTP method is specified in [method] and the URL to use in
+ * [url].
+ *
+ * The `Host` header for the request will be set to the value
+ * [Uri.host]:[Uri.port] from [url]. This can be overridden through the
+ * [HttpClientRequest] interface before the request is sent. NOTE
+ * if [Uri.host] is an IP address this will still be set in the `Host`
+ * header.
+ *
+ * For additional information on the sequence of events during an
+ * HTTP transaction, and the objects returned by the futures, see
+ * the overall documentation for the class [HttpClient].
+ */
+ Future<HttpClientRequest> openUrl(String method, Uri url);
+
+ /**
+ * Opens a HTTP connection using the GET method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using
+ * [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> get(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the GET method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> getUrl(Uri url);
+
+ /**
+ * Opens a HTTP connection using the POST method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using
+ * [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> post(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the POST method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> postUrl(Uri url);
+
+ /**
+ * Opens a HTTP connection using the PUT method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> put(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the PUT method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> putUrl(Uri url);
+
+ /**
+ * Opens a HTTP connection using the DELETE method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> delete(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the DELETE method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> deleteUrl(Uri url);
+
+ /**
+ * Opens a HTTP connection using the PATCH method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> patch(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the PATCH method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> patchUrl(Uri url);
+
+ /**
+ * Opens a HTTP connection using the HEAD method.
+ *
+ * The server is specified using [host] and [port], and the path
+ * (including a possible query) is specified using [path].
+ *
+ * See [open] for details.
+ */
+ Future<HttpClientRequest> head(String host, int port, String path);
+
+ /**
+ * Opens a HTTP connection using the HEAD method.
+ *
+ * The URL to use is specified in [url].
+ *
+ * See [openUrl] for details.
+ */
+ Future<HttpClientRequest> headUrl(Uri url);
+
+ /**
+ * Sets the function to be called when a site is requesting
+ * authentication. The URL requested and the security realm from the
+ * server are passed in the arguments [url] and [realm].
+ *
+ * The function returns a [Future] which should complete when the
+ * authentication has been resolved. If credentials cannot be
+ * provided the [Future] should complete with [:false:]. If
+ * credentials are available the function should add these using
+ * [addCredentials] before completing the [Future] with the value
+ * [:true:].
+ *
+ * If the [Future] completes with true the request will be retried
+ * using the updated credentials. Otherwise response processing will
+ * continue normally.
+ */
+ set authenticate(Future<bool> f(Uri url, String scheme, String realm));
+
+ /**
+ * Add credentials to be used for authorizing HTTP requests.
+ */
+ void addCredentials(Uri url, String realm, HttpClientCredentials credentials);
+
+ /**
+ * Sets the function used to resolve the proxy server to be used for
+ * opening a HTTP connection to the specified [url]. If this
+ * function is not set, direct connections will always be used.
+ *
+ * The string returned by [f] must be in the format used by browser
+ * PAC (proxy auto-config) scripts. That is either
+ *
+ * "DIRECT"
+ *
+ * for using a direct connection or
+ *
+ * "PROXY host:port"
+ *
+ * for using the proxy server [:host:] on port [:port:].
+ *
+ * A configuration can contain several configuration elements
+ * separated by semicolons, e.g.
+ *
+ * "PROXY host:port; PROXY host2:port2; DIRECT"
+ *
+ * The static function [findProxyFromEnvironment] on this class can
+ * be used to implement proxy server resolving based on environment
+ * variables.
+ */
+ set findProxy(String f(Uri url));
+
+ /**
+ * Function for resolving the proxy server to be used for a HTTP
+ * connection from the proxy configuration specified through
+ * environment variables.
+ *
+ * The following environment variables are taken into account:
+ *
+ * http_proxy
+ * https_proxy
+ * no_proxy
+ * HTTP_PROXY
+ * HTTPS_PROXY
+ * NO_PROXY
+ *
+ * [:http_proxy:] and [:HTTP_PROXY:] specify the proxy server to use for
+ * http:// urls. Use the format [:hostname:port:]. If no port is used a
+ * default of 1080 will be used. If both are set the lower case one takes
+ * precedence.
+ *
+ * [:https_proxy:] and [:HTTPS_PROXY:] specify the proxy server to use for
+ * https:// urls. Use the format [:hostname:port:]. If no port is used a
+ * default of 1080 will be used. If both are set the lower case one takes
+ * precedence.
+ *
+ * [:no_proxy:] and [:NO_PROXY:] specify a comma separated list of
+ * postfixes of hostnames for which not to use the proxy
+ * server. E.g. the value "localhost,127.0.0.1" will make requests
+ * to both "localhost" and "127.0.0.1" not use a proxy. If both are set
+ * the lower case one takes precedence.
+ *
+ * To activate this way of resolving proxies assign this function to
+ * the [findProxy] property on the [HttpClient].
+ *
+ * HttpClient client = new HttpClient();
+ * client.findProxy = HttpClient.findProxyFromEnvironment;
+ *
+ * If you don't want to use the system environment you can use a
+ * different one by wrapping the function.
+ *
+ * HttpClient client = new HttpClient();
+ * client.findProxy = (url) {
+ * return HttpClient.findProxyFromEnvironment(
+ * url, environment: {"http_proxy": ..., "no_proxy": ...});
+ * }
+ *
+ * If a proxy requires authentication it is possible to configure
+ * the username and password as well. Use the format
+ * [:username:password@hostname:port:] to include the username and
+ * password. Alternatively the API [addProxyCredentials] can be used
+ * to set credentials for proxies which require authentication.
+ */
+ static String findProxyFromEnvironment(Uri url,
+ {Map<String, String> environment}) {
+ HttpOverrides overrides = HttpOverrides.current;
+ if (overrides == null) {
+ return _HttpClient._findProxyFromEnvironment(url, environment);
+ }
+ return overrides.findProxyFromEnvironment(url, environment);
+ }
+
+ /**
+ * Sets the function to be called when a proxy is requesting
+ * authentication. Information on the proxy in use and the security
+ * realm for the authentication are passed in the arguments [host],
+ * [port] and [realm].
+ *
+ * The function returns a [Future] which should complete when the
+ * authentication has been resolved. If credentials cannot be
+ * provided the [Future] should complete with [:false:]. If
+ * credentials are available the function should add these using
+ * [addProxyCredentials] before completing the [Future] with the value
+ * [:true:].
+ *
+ * If the [Future] completes with [:true:] the request will be retried
+ * using the updated credentials. Otherwise response processing will
+ * continue normally.
+ */
+ set authenticateProxy(
+ Future<bool> f(String host, int port, String scheme, String realm));
+
+ /**
+ * Add credentials to be used for authorizing HTTP proxies.
+ */
+ void addProxyCredentials(
+ String host, int port, String realm, HttpClientCredentials credentials);
+
+ /**
+ * Sets a callback that will decide whether to accept a secure connection
+ * with a server certificate that cannot be authenticated by any of our
+ * trusted root certificates.
+ *
+ * When an secure HTTP request if made, using this HttpClient, and the
+ * server returns a server certificate that cannot be authenticated, the
+ * callback is called asynchronously with the [X509Certificate] object and
+ * the server's hostname and port. If the value of [badCertificateCallback]
+ * is [:null:], the bad certificate is rejected, as if the callback
+ * returned [:false:]
+ *
+ * If the callback returns true, the secure connection is accepted and the
+ * [:Future<HttpClientRequest>:] that was returned from the call making the
+ * request completes with a valid HttpRequest object. If the callback returns
+ * false, the [:Future<HttpClientRequest>:] completes with an exception.
+ *
+ * If a bad certificate is received on a connection attempt, the library calls
+ * the function that was the value of badCertificateCallback at the time
+ * the request is made, even if the value of badCertificateCallback
+ * has changed since then.
+ */
+ set badCertificateCallback(
+ bool callback(X509Certificate cert, String host, int port));
+
+ /// Shuts down the HTTP client.
+ ///
+ /// If [force] is `false` (the default) the [HttpClient] will be kept alive
+ /// until all active connections are done. If [force] is `true` any active
+ /// connections will be closed to immediately release all resources. These
+ /// closed connections will receive an error event to indicate that the client
+ /// was shut down. In both cases trying to establish a new connection after
+ /// calling [close] will throw an exception.
+ void close({bool force: false});
+}
+
+/**
+ * HTTP request for a client connection.
+ *
+ * To set up a request, set the headers using the headers property
+ * provided in this class and write the data to the body of the request.
+ * HttpClientRequest is an [IOSink]. Use the methods from IOSink,
+ * such as writeCharCode(), to write the body of the HTTP
+ * request. When one of the IOSink methods is used for the first
+ * time, the request header is sent. Calling any methods that
+ * change the header after it is sent throws an exception.
+ *
+ * When writing string data through the [IOSink] the
+ * encoding used is determined from the "charset" parameter of
+ * the "Content-Type" header.
+ *
+ * HttpClientRequest request = ...
+ * request.headers.contentType
+ * = new ContentType("application", "json", charset: "utf-8");
+ * request.write(...); // Strings written will be UTF-8 encoded.
+ *
+ * If no charset is provided the default of ISO-8859-1 (Latin 1) is
+ * be used.
+ *
+ * HttpClientRequest request = ...
+ * request.headers.add(HttpHeaders.CONTENT_TYPE, "text/plain");
+ * request.write(...); // Strings written will be ISO-8859-1 encoded.
+ *
+ * An exception is thrown if you use an unsupported encoding and the
+ * `write()` method being used takes a string parameter.
+ */
+abstract class HttpClientRequest implements IOSink {
+ /**
+ * Gets and sets the requested persistent connection state.
+ *
+ * The default value is [:true:].
+ */
+ bool persistentConnection;
+
+ /**
+ * Set this property to [:true:] if this request should
+ * automatically follow redirects. The default is [:true:].
+ *
+ * Automatic redirect will only happen for "GET" and "HEAD" requests
+ * and only for the status codes [:HttpStatus.MOVED_PERMANENTLY:]
+ * (301), [:HttpStatus.FOUND:] (302),
+ * [:HttpStatus.MOVED_TEMPORARILY:] (302, alias for
+ * [:HttpStatus.FOUND:]), [:HttpStatus.SEE_OTHER:] (303) and
+ * [:HttpStatus.TEMPORARY_REDIRECT:] (307). For
+ * [:HttpStatus.SEE_OTHER:] (303) automatic redirect will also happen
+ * for "POST" requests with the method changed to "GET" when
+ * following the redirect.
+ *
+ * All headers added to the request will be added to the redirection
+ * request(s). However, any body send with the request will not be
+ * part of the redirection request(s).
+ */
+ bool followRedirects;
+
+ /**
+ * Set this property to the maximum number of redirects to follow
+ * when [followRedirects] is `true`. If this number is exceeded
+ * an error event will be added with a [RedirectException].
+ *
+ * The default value is 5.
+ */
+ int maxRedirects;
+
+ /**
+ * The method of the request.
+ */
+ String get method;
+
+ /**
+ * The uri of the request.
+ */
+ Uri get uri;
+
+ /// Gets and sets the content length of the request.
+ ///
+ /// If the size of the request is not known in advance set content length to
+ /// -1, which is also the default.
+ int contentLength;
+
+ /**
+ * Gets or sets if the [HttpClientRequest] should buffer output.
+ *
+ * Default value is `true`.
+ *
+ * __Note__: Disabling buffering of the output can result in very poor
+ * performance, when writing many small chunks.
+ */
+ bool bufferOutput;
+
+ /**
+ * Returns the client request headers.
+ *
+ * The client request headers can be modified until the client
+ * request body is written to or closed. After that they become
+ * immutable.
+ */
+ HttpHeaders get headers;
+
+ /**
+ * Cookies to present to the server (in the 'cookie' header).
+ */
+ List<Cookie> get cookies;
+
+ /// A [HttpClientResponse] future that will complete once the response is
+ /// available.
+ ///
+ /// If an error occurs before the response is available, this future will
+ /// complete with an error.
+ Future<HttpClientResponse> get done;
+
+ /**
+ * Close the request for input. Returns the value of [done].
+ */
+ Future<HttpClientResponse> close();
+
+ /// Gets information about the client connection.
+ ///
+ /// Returns [:null:] if the socket is not available.
+ HttpConnectionInfo get connectionInfo;
+}
+
+/**
+ * HTTP response for a client connection.
+ *
+ * The body of a [HttpClientResponse] object is a
+ * [Stream] of data from the server. Listen to the body to handle
+ * the data and be notified when the entire body is received.
+ *
+ * new HttpClient().get('localhost', 80, '/file.txt')
+ * .then((HttpClientRequest request) => request.close())
+ * .then((HttpClientResponse response) {
+ * response.transform(utf8.decoder).listen((contents) {
+ * // handle data
+ * });
+ * });
+ */
+abstract class HttpClientResponse implements Stream<List<int>> {
+ /**
+ * Returns the status code.
+ *
+ * The status code must be set before the body is written
+ * to. Setting the status code after writing to the body will throw
+ * a `StateError`.
+ */
+ int get statusCode;
+
+ /**
+ * Returns the reason phrase associated with the status code.
+ *
+ * The reason phrase must be set before the body is written
+ * to. Setting the reason phrase after writing to the body will throw
+ * a `StateError`.
+ */
+ String get reasonPhrase;
+
+ /**
+ * Returns the content length of the response body. Returns -1 if the size of
+ * the response body is not known in advance.
+ *
+ * If the content length needs to be set, it must be set before the
+ * body is written to. Setting the reason phrase after writing to
+ * the body will throw a `StateError`.
+ */
+ int get contentLength;
+
+ /**
+ * Gets the persistent connection state returned by the server.
+ *
+ * if the persistent connection state needs to be set, it must be
+ * set before the body is written to. Setting the reason phrase
+ * after writing to the body will throw a `StateError`.
+ */
+ bool get persistentConnection;
+
+ /**
+ * Returns whether the status code is one of the normal redirect
+ * codes [HttpStatus.MOVED_PERMANENTLY], [HttpStatus.FOUND],
+ * [HttpStatus.MOVED_TEMPORARILY], [HttpStatus.SEE_OTHER] and
+ * [HttpStatus.TEMPORARY_REDIRECT].
+ */
+ bool get isRedirect;
+
+ /**
+ * Returns the series of redirects this connection has been through. The
+ * list will be empty if no redirects were followed. [redirects] will be
+ * updated both in the case of an automatic and a manual redirect.
+ */
+ List<RedirectInfo> get redirects;
+
+ /**
+ * Redirects this connection to a new URL. The default value for
+ * [method] is the method for the current request. The default value
+ * for [url] is the value of the [HttpHeaders.LOCATION] header of
+ * the current response. All body data must have been read from the
+ * current response before calling [redirect].
+ *
+ * All headers added to the request will be added to the redirection
+ * request. However, any body sent with the request will not be
+ * part of the redirection request.
+ *
+ * If [followLoops] is set to [:true:], redirect will follow the redirect,
+ * even if the URL was already visited. The default value is [:false:].
+ *
+ * The method will ignore [HttpClientRequest.maxRedirects]
+ * and will always perform the redirect.
+ */
+ Future<HttpClientResponse> redirect(
+ [String method, Uri url, bool followLoops]);
+
+ /**
+ * Returns the client response headers.
+ *
+ * The client response headers are immutable.
+ */
+ HttpHeaders get headers;
+
+ /**
+ * Detach the underlying socket from the HTTP client. When the
+ * socket is detached the HTTP client will no longer perform any
+ * operations on it.
+ *
+ * This is normally used when a HTTP upgrade is negotiated and the
+ * communication should continue with a different protocol.
+ */
+ Future<Socket> detachSocket();
+
+ /**
+ * Cookies set by the server (from the 'set-cookie' header).
+ */
+ List<Cookie> get cookies;
+
+ /**
+ * Returns the certificate of the HTTPS server providing the response.
+ * Returns null if the connection is not a secure TLS or SSL connection.
+ */
+ X509Certificate get certificate;
+
+ /**
+ * Gets information about the client connection. Returns [:null:] if the socket
+ * is not available.
+ */
+ HttpConnectionInfo get connectionInfo;
+}
+
+abstract class HttpClientCredentials {}
+
+/**
+ * Represents credentials for basic authentication.
+ */
+abstract class HttpClientBasicCredentials extends HttpClientCredentials {
+ factory HttpClientBasicCredentials(String username, String password) =>
+ new _HttpClientBasicCredentials(username, password);
+}
+
+/**
+ * Represents credentials for digest authentication. Digest
+ * authentication is only supported for servers using the MD5
+ * algorithm and quality of protection (qop) of either "none" or
+ * "auth".
+ */
+abstract class HttpClientDigestCredentials extends HttpClientCredentials {
+ factory HttpClientDigestCredentials(String username, String password) =>
+ new _HttpClientDigestCredentials(username, password);
+}
+
+/**
+ * Information about an [HttpRequest], [HttpResponse], [HttpClientRequest], or
+ * [HttpClientResponse] connection.
+ */
+abstract class HttpConnectionInfo {
+ InternetAddress get remoteAddress;
+ int get remotePort;
+ int get localPort;
+}
+
+/**
+ * Redirect information.
+ */
+abstract class RedirectInfo {
+ /**
+ * Returns the status code used for the redirect.
+ */
+ int get statusCode;
+
+ /**
+ * Returns the method used for the redirect.
+ */
+ String get method;
+
+ /**
+ * Returns the location for the redirect.
+ */
+ Uri get location;
+}
+
+/**
+ * When detaching a socket from either the [:HttpServer:] or the
+ * [:HttpClient:] due to a HTTP connection upgrade there might be
+ * unparsed data already read from the socket. This unparsed data
+ * together with the detached socket is returned in an instance of
+ * this class.
+ */
+abstract class DetachedSocket {
+ Socket get socket;
+ List<int> get unparsedData;
+}
+
+class HttpException implements IOException {
+ final String message;
+ final Uri uri;
+
+ const HttpException(this.message, {this.uri});
+
+ String toString() {
+ var b = new StringBuffer()..write('HttpException: ')..write(message);
+ if (uri != null) {
+ b.write(', uri = $uri');
+ }
+ return b.toString();
+ }
+}
+
+class RedirectException implements HttpException {
+ final String message;
+ final List<RedirectInfo> redirects;
+
+ const RedirectException(this.message, this.redirects);
+
+ String toString() => "RedirectException: $message";
+
+ Uri get uri => redirects.last.location;
+}
diff --git a/lib/http_overrides.dart b/lib/http_overrides.dart
new file mode 100644
index 0000000..7f9e689
--- /dev/null
+++ b/lib/http_overrides.dart
@@ -0,0 +1,118 @@
+// 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 dart._http;
+
+final _httpOverridesToken = new Object();
+
+const _asyncRunZoned = runZoned;
+
+/// This class facilitates overriding [HttpClient] with a mock implementation.
+/// It should be extended by another class in client code with overrides
+/// that construct a mock implementation. The implementation in this base class
+/// defaults to the actual [HttpClient] implementation. For example:
+///
+/// ```
+/// class MyHttpClient implements HttpClient {
+/// ...
+/// // An implementation of the HttpClient interface
+/// ...
+/// }
+///
+/// main() {
+/// HttpOverrides.runZoned(() {
+/// ...
+/// // Operations will use MyHttpClient instead of the real HttpClient
+/// // implementation whenever HttpClient is used.
+/// ...
+/// }, createHttpClient: (SecurityContext c) => new MyHttpClient(c));
+/// }
+/// ```
+abstract class HttpOverrides {
+ static HttpOverrides _global;
+
+ static HttpOverrides get current {
+ return Zone.current[_httpOverridesToken] ?? _global;
+ }
+
+ /// The [HttpOverrides] to use in the root [Zone].
+ ///
+ /// These are the [HttpOverrides] that will be used in the root Zone, and in
+ /// Zone's that do not set [HttpOverrides] and whose ancestors up to the root
+ /// Zone do not set [HttpOverrides].
+ static set global(HttpOverrides overrides) {
+ _global = overrides;
+ }
+
+ /// Runs [body] in a fresh [Zone] using the provided overrides.
+ static R runZoned<R>(R body(),
+ {HttpClient Function(SecurityContext) createHttpClient,
+ String Function(Uri uri, Map<String, String> environment)
+ findProxyFromEnvironment,
+ ZoneSpecification zoneSpecification,
+ Function onError}) {
+ HttpOverrides overrides =
+ new _HttpOverridesScope(createHttpClient, findProxyFromEnvironment);
+ return _asyncRunZoned<R>(body,
+ zoneValues: {_httpOverridesToken: overrides},
+ zoneSpecification: zoneSpecification,
+ onError: onError);
+ }
+
+ /// Runs [body] in a fresh [Zone] using the overrides found in [overrides].
+ ///
+ /// Note that [overrides] should be an instance of a class that extends
+ /// [HttpOverrides].
+ static R runWithHttpOverrides<R>(R body(), HttpOverrides overrides,
+ {ZoneSpecification zoneSpecification, Function onError}) {
+ return _asyncRunZoned<R>(body,
+ zoneValues: {_httpOverridesToken: overrides},
+ zoneSpecification: zoneSpecification,
+ onError: onError);
+ }
+
+ /// Returns a new [HttpClient] using the given [context].
+ ///
+ /// When this override is installed, this function overrides the behavior of
+ /// `new HttpClient`.
+ HttpClient createHttpClient(SecurityContext context) {
+ return new _HttpClient(context);
+ }
+
+ /// Resolves the proxy server to be used for HTTP connections.
+ ///
+ /// When this override is installed, this function overrides the behavior of
+ /// `HttpClient.findProxyFromEnvironment`.
+ String findProxyFromEnvironment(Uri url, Map<String, String> environment) {
+ return _HttpClient._findProxyFromEnvironment(url, environment);
+ }
+}
+
+class _HttpOverridesScope extends HttpOverrides {
+ final HttpOverrides _previous = HttpOverrides.current;
+
+ final HttpClient Function(SecurityContext) _createHttpClient;
+ final String Function(Uri uri, Map<String, String> environment)
+ _findProxyFromEnvironment;
+
+ _HttpOverridesScope(this._createHttpClient, this._findProxyFromEnvironment);
+
+ @override
+ HttpClient createHttpClient(SecurityContext context) {
+ if (_createHttpClient != null) return _createHttpClient(context);
+ if (_previous != null) return _previous.createHttpClient(context);
+ return super.createHttpClient(context);
+ }
+
+ @override
+ String findProxyFromEnvironment(Uri url, Map<String, String> environment) {
+ if (_findProxyFromEnvironment != null) {
+ return _findProxyFromEnvironment(url, environment);
+ }
+ if (_previous != null) {
+ return _previous.findProxyFromEnvironment(url, environment);
+ }
+ return super.findProxyFromEnvironment(url, environment);
+ }
+}
diff --git a/lib/src/crypto.dart b/lib/src/crypto.dart
new file mode 100644
index 0000000..56c16dd
--- /dev/null
+++ b/lib/src/crypto.dart
@@ -0,0 +1,458 @@
+// Copyright (c) 2012, 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 http_io;
+
+class _CryptoUtils {
+ static const int PAD = 61; // '='
+ static const int CR = 13; // '\r'
+ static const int LF = 10; // '\n'
+ static const int LINE_LENGTH = 76;
+
+ static const String _encodeTable =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ static const String _encodeTableUrlSafe =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+ // Lookup table used for finding Base 64 alphabet index of a given byte.
+ // -2 : Outside Base 64 alphabet.
+ // -1 : '\r' or '\n'
+ // 0 : = (Padding character).
+ // >0 : Base 64 alphabet index of given byte.
+ static const List<int> _decodeTable = const [
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -1, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, 62, -2, 63, //
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, 00, -2, -2, //
+ -2, 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, //
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, 63, //
+ -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, //
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, //
+ -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
+ ];
+
+ static Random _rng = new Random.secure();
+
+ static Uint8List getRandomBytes(int count) {
+ final Uint8List result = new Uint8List(count);
+ for (int i = 0; i < count; i++) {
+ result[i] = _rng.nextInt(0xff);
+ }
+ return result;
+ }
+
+ static String bytesToHex(List<int> bytes) {
+ var result = new StringBuffer();
+ for (var part in bytes) {
+ result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
+ }
+ return result.toString();
+ }
+
+ static String bytesToBase64(List<int> bytes,
+ [bool urlSafe = false, bool addLineSeparator = false]) {
+ int len = bytes.length;
+ if (len == 0) {
+ return "";
+ }
+ final String lookup = urlSafe ? _encodeTableUrlSafe : _encodeTable;
+ // Size of 24 bit chunks.
+ final int remainderLength = len.remainder(3);
+ final int chunkLength = len - remainderLength;
+ // Size of base output.
+ int outputLen = ((len ~/ 3) * 4) + ((remainderLength > 0) ? 4 : 0);
+ // Add extra for line separators.
+ if (addLineSeparator) {
+ outputLen += ((outputLen - 1) ~/ LINE_LENGTH) << 1;
+ }
+ List<int> out = new List<int>(outputLen);
+
+ // Encode 24 bit chunks.
+ int j = 0, i = 0, c = 0;
+ while (i < chunkLength) {
+ int x = ((bytes[i++] << 16) & 0xFFFFFF) |
+ ((bytes[i++] << 8) & 0xFFFFFF) |
+ bytes[i++];
+ out[j++] = lookup.codeUnitAt(x >> 18);
+ out[j++] = lookup.codeUnitAt((x >> 12) & 0x3F);
+ out[j++] = lookup.codeUnitAt((x >> 6) & 0x3F);
+ out[j++] = lookup.codeUnitAt(x & 0x3f);
+ // Add optional line separator for each 76 char output.
+ if (addLineSeparator && ++c == 19 && j < outputLen - 2) {
+ out[j++] = CR;
+ out[j++] = LF;
+ c = 0;
+ }
+ }
+
+ // If input length if not a multiple of 3, encode remaining bytes and
+ // add padding.
+ if (remainderLength == 1) {
+ int x = bytes[i];
+ out[j++] = lookup.codeUnitAt(x >> 2);
+ out[j++] = lookup.codeUnitAt((x << 4) & 0x3F);
+ out[j++] = PAD;
+ out[j++] = PAD;
+ } else if (remainderLength == 2) {
+ int x = bytes[i];
+ int y = bytes[i + 1];
+ out[j++] = lookup.codeUnitAt(x >> 2);
+ out[j++] = lookup.codeUnitAt(((x << 4) | (y >> 4)) & 0x3F);
+ out[j++] = lookup.codeUnitAt((y << 2) & 0x3F);
+ out[j++] = PAD;
+ }
+
+ return new String.fromCharCodes(out);
+ }
+
+ static List<int> base64StringToBytes(String input,
+ [bool ignoreInvalidCharacters = true]) {
+ int len = input.length;
+ if (len == 0) {
+ return new List<int>(0);
+ }
+
+ // Count '\r', '\n' and illegal characters, For illegal characters,
+ // if [ignoreInvalidCharacters] is false, throw an exception.
+ int extrasLen = 0;
+ for (int i = 0; i < len; i++) {
+ int c = _decodeTable[input.codeUnitAt(i)];
+ if (c < 0) {
+ extrasLen++;
+ if (c == -2 && !ignoreInvalidCharacters) {
+ throw new FormatException('Invalid character: ${input[i]}');
+ }
+ }
+ }
+
+ if ((len - extrasLen) % 4 != 0) {
+ throw new FormatException('''Size of Base 64 characters in Input
+ must be a multiple of 4. Input: $input''');
+ }
+
+ // Count pad characters, ignore illegal characters at the end.
+ int padLength = 0;
+ for (int i = len - 1; i >= 0; i--) {
+ int currentCodeUnit = input.codeUnitAt(i);
+ if (_decodeTable[currentCodeUnit] > 0) break;
+ if (currentCodeUnit == PAD) padLength++;
+ }
+ int outputLen = (((len - extrasLen) * 6) >> 3) - padLength;
+ List<int> out = new List<int>(outputLen);
+
+ for (int i = 0, o = 0; o < outputLen;) {
+ // Accumulate 4 valid 6 bit Base 64 characters into an int.
+ int x = 0;
+ for (int j = 4; j > 0;) {
+ int c = _decodeTable[input.codeUnitAt(i++)];
+ if (c >= 0) {
+ x = ((x << 6) & 0xFFFFFF) | c;
+ j--;
+ }
+ }
+ out[o++] = x >> 16;
+ if (o < outputLen) {
+ out[o++] = (x >> 8) & 0xFF;
+ if (o < outputLen) out[o++] = x & 0xFF;
+ }
+ }
+ return out;
+ }
+}
+
+// Constants.
+const _MASK_8 = 0xff;
+const _MASK_32 = 0xffffffff;
+const _BITS_PER_BYTE = 8;
+const _BYTES_PER_WORD = 4;
+
+// Base class encapsulating common behavior for cryptographic hash
+// functions.
+abstract class _HashBase {
+ // Hasher state.
+ final int _chunkSizeInWords;
+ final int _digestSizeInWords;
+ final bool _bigEndianWords;
+ int _lengthInBytes = 0;
+ List<int> _pendingData;
+ List<int> _currentChunk;
+ List<int> _h;
+ bool _digestCalled = false;
+
+ _HashBase(
+ this._chunkSizeInWords, this._digestSizeInWords, this._bigEndianWords)
+ : _pendingData = [] {
+ _currentChunk = new List(_chunkSizeInWords);
+ _h = new List(_digestSizeInWords);
+ }
+
+ // Update the hasher with more data.
+ add(List<int> data) {
+ if (_digestCalled) {
+ throw new StateError(
+ 'Hash update method called after digest was retrieved');
+ }
+ _lengthInBytes += data.length;
+ _pendingData.addAll(data);
+ _iterate();
+ }
+
+ // Finish the hash computation and return the digest string.
+ List<int> close() {
+ if (_digestCalled) {
+ return _resultAsBytes();
+ }
+ _digestCalled = true;
+ _finalizeData();
+ _iterate();
+ assert(_pendingData.length == 0);
+ return _resultAsBytes();
+ }
+
+ // Returns the block size of the hash in bytes.
+ int get blockSize {
+ return _chunkSizeInWords * _BYTES_PER_WORD;
+ }
+
+ // Create a fresh instance of this Hash.
+ newInstance();
+
+ // One round of the hash computation.
+ _updateHash(List<int> m);
+
+ // Helper methods.
+ _add32(x, y) => (x + y) & _MASK_32;
+ _roundUp(val, n) => (val + n - 1) & -n;
+
+ // Rotate left limiting to unsigned 32-bit values.
+ int _rotl32(int val, int shift) {
+ var mod_shift = shift & 31;
+ return ((val << mod_shift) & _MASK_32) |
+ ((val & _MASK_32) >> (32 - mod_shift));
+ }
+
+ // Compute the final result as a list of bytes from the hash words.
+ List<int> _resultAsBytes() {
+ var result = <int>[];
+ for (var i = 0; i < _h.length; i++) {
+ result.addAll(_wordToBytes(_h[i]));
+ }
+ return result;
+ }
+
+ // Converts a list of bytes to a chunk of 32-bit words.
+ _bytesToChunk(List<int> data, int dataIndex) {
+ assert((data.length - dataIndex) >= (_chunkSizeInWords * _BYTES_PER_WORD));
+
+ for (var wordIndex = 0; wordIndex < _chunkSizeInWords; wordIndex++) {
+ var w3 = _bigEndianWords ? data[dataIndex] : data[dataIndex + 3];
+ var w2 = _bigEndianWords ? data[dataIndex + 1] : data[dataIndex + 2];
+ var w1 = _bigEndianWords ? data[dataIndex + 2] : data[dataIndex + 1];
+ var w0 = _bigEndianWords ? data[dataIndex + 3] : data[dataIndex];
+ dataIndex += 4;
+ var word = (w3 & 0xff) << 24;
+ word |= (w2 & _MASK_8) << 16;
+ word |= (w1 & _MASK_8) << 8;
+ word |= (w0 & _MASK_8);
+ _currentChunk[wordIndex] = word;
+ }
+ }
+
+ // Convert a 32-bit word to four bytes.
+ List<int> _wordToBytes(int word) {
+ List<int> bytes = new List(_BYTES_PER_WORD);
+ bytes[0] = (word >> (_bigEndianWords ? 24 : 0)) & _MASK_8;
+ bytes[1] = (word >> (_bigEndianWords ? 16 : 8)) & _MASK_8;
+ bytes[2] = (word >> (_bigEndianWords ? 8 : 16)) & _MASK_8;
+ bytes[3] = (word >> (_bigEndianWords ? 0 : 24)) & _MASK_8;
+ return bytes;
+ }
+
+ // Iterate through data updating the hash computation for each
+ // chunk.
+ _iterate() {
+ var len = _pendingData.length;
+ var chunkSizeInBytes = _chunkSizeInWords * _BYTES_PER_WORD;
+ if (len >= chunkSizeInBytes) {
+ var index = 0;
+ for (; (len - index) >= chunkSizeInBytes; index += chunkSizeInBytes) {
+ _bytesToChunk(_pendingData, index);
+ _updateHash(_currentChunk);
+ }
+ _pendingData = _pendingData.sublist(index, len);
+ }
+ }
+
+ // Finalize the data. Add a 1 bit to the end of the message. Expand with
+ // 0 bits and add the length of the message.
+ _finalizeData() {
+ _pendingData.add(0x80);
+ var contentsLength = _lengthInBytes + 9;
+ var chunkSizeInBytes = _chunkSizeInWords * _BYTES_PER_WORD;
+ var finalizedLength = _roundUp(contentsLength, chunkSizeInBytes);
+ var zeroPadding = finalizedLength - contentsLength;
+ for (var i = 0; i < zeroPadding; i++) {
+ _pendingData.add(0);
+ }
+ var lengthInBits = _lengthInBytes * _BITS_PER_BYTE;
+ assert(lengthInBits < pow(2, 32));
+ if (_bigEndianWords) {
+ _pendingData.addAll(_wordToBytes(0));
+ _pendingData.addAll(_wordToBytes(lengthInBits & _MASK_32));
+ } else {
+ _pendingData.addAll(_wordToBytes(lengthInBits & _MASK_32));
+ _pendingData.addAll(_wordToBytes(0));
+ }
+ }
+}
+
+// The MD5 hasher is used to compute an MD5 message digest.
+class _MD5 extends _HashBase {
+ _MD5() : super(16, 4, false) {
+ _h[0] = 0x67452301;
+ _h[1] = 0xefcdab89;
+ _h[2] = 0x98badcfe;
+ _h[3] = 0x10325476;
+ }
+
+ // Returns a new instance of this Hash.
+ _MD5 newInstance() {
+ return new _MD5();
+ }
+
+ static const _k = const [
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, //
+ 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, //
+ 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, //
+ 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, //
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, //
+ 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, //
+ 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, //
+ 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, //
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, //
+ 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, //
+ 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
+ ];
+
+ static const _r = const [
+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, //
+ 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, //
+ 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, //
+ 10, 15, 21, 6, 10, 15, 21
+ ];
+
+ // Compute one iteration of the MD5 algorithm with a chunk of
+ // 16 32-bit pieces.
+ void _updateHash(List<int> m) {
+ assert(m.length == 16);
+
+ var a = _h[0];
+ var b = _h[1];
+ var c = _h[2];
+ var d = _h[3];
+
+ var t0;
+ var t1;
+
+ for (var i = 0; i < 64; i++) {
+ if (i < 16) {
+ t0 = (b & c) | ((~b & _MASK_32) & d);
+ t1 = i;
+ } else if (i < 32) {
+ t0 = (d & b) | ((~d & _MASK_32) & c);
+ t1 = ((5 * i) + 1) % 16;
+ } else if (i < 48) {
+ t0 = b ^ c ^ d;
+ t1 = ((3 * i) + 5) % 16;
+ } else {
+ t0 = c ^ (b | (~d & _MASK_32));
+ t1 = (7 * i) % 16;
+ }
+
+ var temp = d;
+ d = c;
+ c = b;
+ b = _add32(
+ b, _rotl32(_add32(_add32(a, t0), _add32(_k[i], m[t1])), _r[i]));
+ a = temp;
+ }
+
+ _h[0] = _add32(a, _h[0]);
+ _h[1] = _add32(b, _h[1]);
+ _h[2] = _add32(c, _h[2]);
+ _h[3] = _add32(d, _h[3]);
+ }
+}
+
+// The SHA1 hasher is used to compute an SHA1 message digest.
+class _SHA1 extends _HashBase {
+ // Construct a SHA1 hasher object.
+ _SHA1()
+ : _w = new List(80),
+ super(16, 5, true) {
+ _h[0] = 0x67452301;
+ _h[1] = 0xEFCDAB89;
+ _h[2] = 0x98BADCFE;
+ _h[3] = 0x10325476;
+ _h[4] = 0xC3D2E1F0;
+ }
+
+ // Returns a new instance of this Hash.
+ _SHA1 newInstance() {
+ return new _SHA1();
+ }
+
+ // Compute one iteration of the SHA1 algorithm with a chunk of
+ // 16 32-bit pieces.
+ void _updateHash(List<int> m) {
+ assert(m.length == 16);
+
+ var a = _h[0];
+ var b = _h[1];
+ var c = _h[2];
+ var d = _h[3];
+ var e = _h[4];
+
+ for (var i = 0; i < 80; i++) {
+ if (i < 16) {
+ _w[i] = m[i];
+ } else {
+ var n = _w[i - 3] ^ _w[i - 8] ^ _w[i - 14] ^ _w[i - 16];
+ _w[i] = _rotl32(n, 1);
+ }
+ var t = _add32(_add32(_rotl32(a, 5), e), _w[i]);
+ if (i < 20) {
+ t = _add32(_add32(t, (b & c) | (~b & d)), 0x5A827999);
+ } else if (i < 40) {
+ t = _add32(_add32(t, (b ^ c ^ d)), 0x6ED9EBA1);
+ } else if (i < 60) {
+ t = _add32(_add32(t, (b & c) | (b & d) | (c & d)), 0x8F1BBCDC);
+ } else {
+ t = _add32(_add32(t, b ^ c ^ d), 0xCA62C1D6);
+ }
+
+ e = d;
+ d = c;
+ c = _rotl32(b, 30);
+ b = a;
+ a = t & _MASK_32;
+ }
+
+ _h[0] = _add32(a, _h[0]);
+ _h[1] = _add32(b, _h[1]);
+ _h[2] = _add32(c, _h[2]);
+ _h[3] = _add32(d, _h[3]);
+ _h[4] = _add32(e, _h[4]);
+ }
+
+ List<int> _w;
+}
diff --git a/lib/src/http_date.dart b/lib/src/http_date.dart
new file mode 100644
index 0000000..aba09cd
--- /dev/null
+++ b/lib/src/http_date.dart
@@ -0,0 +1,388 @@
+// Copyright (c) 2012, 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 http_io;
+
+/**
+ * Utility functions for working with dates with HTTP specific date
+ * formats.
+ */
+class HttpDate {
+ // From RFC-2616 section "3.3.1 Full Date",
+ // http://tools.ietf.org/html/rfc2616#section-3.3.1
+ //
+ // HTTP-date = rfc1123-date | rfc850-date | asctime-date
+ // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
+ // rfc850-date = weekday "," SP date2 SP time SP "GMT"
+ // asctime-date = wkday SP date3 SP time SP 4DIGIT
+ // date1 = 2DIGIT SP month SP 4DIGIT
+ // ; day month year (e.g., 02 Jun 1982)
+ // date2 = 2DIGIT "-" month "-" 2DIGIT
+ // ; day-month-year (e.g., 02-Jun-82)
+ // date3 = month SP ( 2DIGIT | ( SP 1DIGIT ))
+ // ; month day (e.g., Jun 2)
+ // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
+ // ; 00:00:00 - 23:59:59
+ // wkday = "Mon" | "Tue" | "Wed"
+ // | "Thu" | "Fri" | "Sat" | "Sun"
+ // weekday = "Monday" | "Tuesday" | "Wednesday"
+ // | "Thursday" | "Friday" | "Saturday" | "Sunday"
+ // month = "Jan" | "Feb" | "Mar" | "Apr"
+ // | "May" | "Jun" | "Jul" | "Aug"
+ // | "Sep" | "Oct" | "Nov" | "Dec"
+
+ /**
+ * Format a date according to
+ * [RFC-1123](http://tools.ietf.org/html/rfc1123 "RFC-1123"),
+ * e.g. `Thu, 1 Jan 1970 00:00:00 GMT`.
+ */
+ static String format(DateTime date) {
+ const List wkday = const ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ const List month = const [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ];
+
+ DateTime d = date.toUtc();
+ StringBuffer sb = new StringBuffer()
+ ..write(wkday[d.weekday - 1])
+ ..write(", ")
+ ..write(d.day <= 9 ? "0" : "")
+ ..write(d.day.toString())
+ ..write(" ")
+ ..write(month[d.month - 1])
+ ..write(" ")
+ ..write(d.year.toString())
+ ..write(d.hour <= 9 ? " 0" : " ")
+ ..write(d.hour.toString())
+ ..write(d.minute <= 9 ? ":0" : ":")
+ ..write(d.minute.toString())
+ ..write(d.second <= 9 ? ":0" : ":")
+ ..write(d.second.toString())
+ ..write(" GMT");
+ return sb.toString();
+ }
+
+ /**
+ * Parse a date string in either of the formats
+ * [RFC-1123](http://tools.ietf.org/html/rfc1123 "RFC-1123"),
+ * [RFC-850](http://tools.ietf.org/html/rfc850 "RFC-850") or
+ * ANSI C's asctime() format. These formats are listed here.
+ *
+ * Thu, 1 Jan 1970 00:00:00 GMT
+ * Thursday, 1-Jan-1970 00:00:00 GMT
+ * Thu Jan 1 00:00:00 1970
+ *
+ * For more information see [RFC-2616 section
+ * 3.1.1](http://tools.ietf.org/html/rfc2616#section-3.3.1
+ * "RFC-2616 section 3.1.1").
+ */
+ static DateTime parse(String date) {
+ final int SP = 32;
+ const List wkdays = const ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
+ const List weekdays = const [
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday"
+ ];
+ const List months = const [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ];
+ const List wkdaysLowerCase = const [
+ "mon",
+ "tue",
+ "wed",
+ "thu",
+ "fri",
+ "sat",
+ "sun"
+ ];
+ const List weekdaysLowerCase = const [
+ "monday",
+ "tuesday",
+ "wednesday",
+ "thursday",
+ "friday",
+ "saturday",
+ "sunday"
+ ];
+ const List monthsLowerCase = const [
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec"
+ ];
+
+ final int formatRfc1123 = 0;
+ final int formatRfc850 = 1;
+ final int formatAsctime = 2;
+
+ int index = 0;
+ String tmp;
+ int format;
+
+ void expect(String s) {
+ if (date.length - index < s.length) {
+ throw new HttpException("Invalid HTTP date $date");
+ }
+ String tmp = date.substring(index, index + s.length);
+ if (tmp != s) {
+ throw new HttpException("Invalid HTTP date $date");
+ }
+ index += s.length;
+ }
+
+ int expectWeekday() {
+ int weekday;
+ // The formatting of the weekday signals the format of the date string.
+ int pos = date.indexOf(",", index);
+ if (pos == -1) {
+ int pos = date.indexOf(" ", index);
+ if (pos == -1) throw new HttpException("Invalid HTTP date $date");
+ tmp = date.substring(index, pos);
+ index = pos + 1;
+ weekday = wkdays.indexOf(tmp);
+ if (weekday != -1) {
+ format = formatAsctime;
+ return weekday;
+ }
+ } else {
+ tmp = date.substring(index, pos);
+ index = pos + 1;
+ weekday = wkdays.indexOf(tmp);
+ if (weekday != -1) {
+ format = formatRfc1123;
+ return weekday;
+ }
+ weekday = weekdays.indexOf(tmp);
+ if (weekday != -1) {
+ format = formatRfc850;
+ return weekday;
+ }
+ }
+ throw new HttpException("Invalid HTTP date $date");
+ }
+
+ int expectMonth(String separator) {
+ int pos = date.indexOf(separator, index);
+ if (pos - index != 3) throw new HttpException("Invalid HTTP date $date");
+ tmp = date.substring(index, pos);
+ index = pos + 1;
+ int month = months.indexOf(tmp);
+ if (month != -1) return month;
+ throw new HttpException("Invalid HTTP date $date");
+ }
+
+ int expectNum(String separator) {
+ int pos;
+ if (separator.length > 0) {
+ pos = date.indexOf(separator, index);
+ } else {
+ pos = date.length;
+ }
+ String tmp = date.substring(index, pos);
+ index = pos + separator.length;
+ try {
+ int value = int.parse(tmp);
+ return value;
+ } on FormatException catch (e) {
+ throw new HttpException("Invalid HTTP date $date");
+ }
+ }
+
+ void expectEnd() {
+ if (index != date.length) {
+ throw new HttpException("Invalid HTTP date $date");
+ }
+ }
+
+ int weekday = expectWeekday();
+ int day;
+ int month;
+ int year;
+ int hours;
+ int minutes;
+ int seconds;
+ if (format == formatAsctime) {
+ month = expectMonth(" ");
+ if (date.codeUnitAt(index) == SP) index++;
+ day = expectNum(" ");
+ hours = expectNum(":");
+ minutes = expectNum(":");
+ seconds = expectNum(" ");
+ year = expectNum("");
+ } else {
+ expect(" ");
+ day = expectNum(format == formatRfc1123 ? " " : "-");
+ month = expectMonth(format == formatRfc1123 ? " " : "-");
+ year = expectNum(" ");
+ hours = expectNum(":");
+ minutes = expectNum(":");
+ seconds = expectNum(" ");
+ expect("GMT");
+ }
+ expectEnd();
+ return new DateTime.utc(year, month + 1, day, hours, minutes, seconds, 0);
+ }
+
+ // Parse a cookie date string.
+ static DateTime _parseCookieDate(String date) {
+ const List monthsLowerCase = const [
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec"
+ ];
+
+ int position = 0;
+
+ void error() {
+ throw new HttpException("Invalid cookie date $date");
+ }
+
+ bool isEnd() => position == date.length;
+
+ bool isDelimiter(String s) {
+ int char = s.codeUnitAt(0);
+ if (char == 0x09) return true;
+ if (char >= 0x20 && char <= 0x2F) return true;
+ if (char >= 0x3B && char <= 0x40) return true;
+ if (char >= 0x5B && char <= 0x60) return true;
+ if (char >= 0x7B && char <= 0x7E) return true;
+ return false;
+ }
+
+ bool isNonDelimiter(String s) {
+ int char = s.codeUnitAt(0);
+ if (char >= 0x00 && char <= 0x08) return true;
+ if (char >= 0x0A && char <= 0x1F) return true;
+ if (char >= 0x30 && char <= 0x39) return true; // Digit
+ if (char == 0x3A) return true; // ':'
+ if (char >= 0x41 && char <= 0x5A) return true; // Alpha
+ if (char >= 0x61 && char <= 0x7A) return true; // Alpha
+ if (char >= 0x7F && char <= 0xFF) return true; // Alpha
+ return false;
+ }
+
+ bool isDigit(String s) {
+ int char = s.codeUnitAt(0);
+ if (char > 0x2F && char < 0x3A) return true;
+ return false;
+ }
+
+ int getMonth(String month) {
+ if (month.length < 3) return -1;
+ return monthsLowerCase.indexOf(month.substring(0, 3));
+ }
+
+ int toInt(String s) {
+ int index = 0;
+ for (; index < s.length && isDigit(s[index]); index++);
+ return int.parse(s.substring(0, index));
+ }
+
+ var tokens = [];
+ while (!isEnd()) {
+ while (!isEnd() && isDelimiter(date[position])) position++;
+ int start = position;
+ while (!isEnd() && isNonDelimiter(date[position])) position++;
+ tokens.add(date.substring(start, position).toLowerCase());
+ while (!isEnd() && isDelimiter(date[position])) position++;
+ }
+
+ String timeStr;
+ String dayOfMonthStr;
+ String monthStr;
+ String yearStr;
+
+ for (var token in tokens) {
+ if (token.length < 1) continue;
+ if (timeStr == null &&
+ token.length >= 5 &&
+ isDigit(token[0]) &&
+ (token[1] == ":" || (isDigit(token[1]) && token[2] == ":"))) {
+ timeStr = token;
+ } else if (dayOfMonthStr == null && isDigit(token[0])) {
+ dayOfMonthStr = token;
+ } else if (monthStr == null && getMonth(token) >= 0) {
+ monthStr = token;
+ } else if (yearStr == null &&
+ token.length >= 2 &&
+ isDigit(token[0]) &&
+ isDigit(token[1])) {
+ yearStr = token;
+ }
+ }
+
+ if (timeStr == null ||
+ dayOfMonthStr == null ||
+ monthStr == null ||
+ yearStr == null) {
+ error();
+ }
+
+ int year = toInt(yearStr);
+ if (year >= 70 && year <= 99)
+ year += 1900;
+ else if (year >= 0 && year <= 69) year += 2000;
+ if (year < 1601) error();
+
+ int dayOfMonth = toInt(dayOfMonthStr);
+ if (dayOfMonth < 1 || dayOfMonth > 31) error();
+
+ int month = getMonth(monthStr) + 1;
+
+ var timeList = timeStr.split(":");
+ if (timeList.length != 3) error();
+ int hour = toInt(timeList[0]);
+ int minute = toInt(timeList[1]);
+ int second = toInt(timeList[2]);
+ if (hour > 23) error();
+ if (minute > 59) error();
+ if (second > 59) error();
+
+ return new DateTime.utc(year, month, dayOfMonth, hour, minute, second, 0);
+ }
+}
diff --git a/lib/src/http_headers.dart b/lib/src/http_headers.dart
new file mode 100644
index 0000000..6f28da4
--- /dev/null
+++ b/lib/src/http_headers.dart
@@ -0,0 +1,1000 @@
+// Copyright (c) 2013, 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 http_io;
+
+class _HttpHeaders implements HttpHeaders {
+ final Map<String, List<String>> _headers;
+ final String protocolVersion;
+
+ bool _mutable = true; // Are the headers currently mutable?
+ List<String> _noFoldingHeaders;
+
+ int _contentLength = -1;
+ bool _persistentConnection = true;
+ bool _chunkedTransferEncoding = false;
+ String _host;
+ int _port;
+
+ final int _defaultPortForScheme;
+
+ _HttpHeaders(this.protocolVersion,
+ {int defaultPortForScheme: HttpClient.DEFAULT_HTTP_PORT,
+ _HttpHeaders initialHeaders})
+ : _headers = new HashMap<String, List<String>>(),
+ _defaultPortForScheme = defaultPortForScheme {
+ if (initialHeaders != null) {
+ initialHeaders._headers.forEach((name, value) => _headers[name] = value);
+ _contentLength = initialHeaders._contentLength;
+ _persistentConnection = initialHeaders._persistentConnection;
+ _chunkedTransferEncoding = initialHeaders._chunkedTransferEncoding;
+ _host = initialHeaders._host;
+ _port = initialHeaders._port;
+ }
+ if (protocolVersion == "1.0") {
+ _persistentConnection = false;
+ _chunkedTransferEncoding = false;
+ }
+ }
+
+ List<String> operator [](String name) => _headers[name.toLowerCase()];
+
+ String value(String name) {
+ name = name.toLowerCase();
+ List<String> values = _headers[name];
+ if (values == null) return null;
+ if (values.length > 1) {
+ throw new HttpException("More than one value for header $name");
+ }
+ return values[0];
+ }
+
+ void add(String name, value) {
+ _checkMutable();
+ _addAll(_validateField(name), value);
+ }
+
+ void _addAll(String name, value) {
+ assert(name == _validateField(name));
+ if (value is Iterable) {
+ for (var v in value) {
+ _add(name, _validateValue(v));
+ }
+ } else {
+ _add(name, _validateValue(value));
+ }
+ }
+
+ void set(String name, Object value) {
+ _checkMutable();
+ name = _validateField(name);
+ _headers.remove(name);
+ if (name == HttpHeaders.TRANSFER_ENCODING) {
+ _chunkedTransferEncoding = false;
+ }
+ _addAll(name, value);
+ }
+
+ void remove(String name, Object value) {
+ _checkMutable();
+ name = _validateField(name);
+ value = _validateValue(value);
+ List<String> values = _headers[name];
+ if (values != null) {
+ int index = values.indexOf(value);
+ if (index != -1) {
+ values.removeRange(index, index + 1);
+ }
+ if (values.length == 0) _headers.remove(name);
+ }
+ if (name == HttpHeaders.TRANSFER_ENCODING && value == "chunked") {
+ _chunkedTransferEncoding = false;
+ }
+ }
+
+ void removeAll(String name) {
+ _checkMutable();
+ name = _validateField(name);
+ _headers.remove(name);
+ }
+
+ void forEach(void f(String name, List<String> values)) {
+ _headers.forEach(f);
+ }
+
+ void noFolding(String name) {
+ if (_noFoldingHeaders == null) _noFoldingHeaders = new List<String>();
+ _noFoldingHeaders.add(name);
+ }
+
+ bool get persistentConnection => _persistentConnection;
+
+ void set persistentConnection(bool persistentConnection) {
+ _checkMutable();
+ if (persistentConnection == _persistentConnection) return;
+ if (persistentConnection) {
+ if (protocolVersion == "1.1") {
+ remove(HttpHeaders.CONNECTION, "close");
+ } else {
+ if (_contentLength == -1) {
+ throw new HttpException(
+ "Trying to set 'Connection: Keep-Alive' on HTTP 1.0 headers with "
+ "no ContentLength");
+ }
+ add(HttpHeaders.CONNECTION, "keep-alive");
+ }
+ } else {
+ if (protocolVersion == "1.1") {
+ add(HttpHeaders.CONNECTION, "close");
+ } else {
+ remove(HttpHeaders.CONNECTION, "keep-alive");
+ }
+ }
+ _persistentConnection = persistentConnection;
+ }
+
+ int get contentLength => _contentLength;
+
+ void set contentLength(int contentLength) {
+ _checkMutable();
+ if (protocolVersion == "1.0" &&
+ persistentConnection &&
+ contentLength == -1) {
+ throw new HttpException(
+ "Trying to clear ContentLength on HTTP 1.0 headers with "
+ "'Connection: Keep-Alive' set");
+ }
+ if (_contentLength == contentLength) return;
+ _contentLength = contentLength;
+ if (_contentLength >= 0) {
+ if (chunkedTransferEncoding) chunkedTransferEncoding = false;
+ _set(HttpHeaders.CONTENT_LENGTH, contentLength.toString());
+ } else {
+ removeAll(HttpHeaders.CONTENT_LENGTH);
+ if (protocolVersion == "1.1") {
+ chunkedTransferEncoding = true;
+ }
+ }
+ }
+
+ bool get chunkedTransferEncoding => _chunkedTransferEncoding;
+
+ void set chunkedTransferEncoding(bool chunkedTransferEncoding) {
+ _checkMutable();
+ if (chunkedTransferEncoding && protocolVersion == "1.0") {
+ throw new HttpException(
+ "Trying to set 'Transfer-Encoding: Chunked' on HTTP 1.0 headers");
+ }
+ if (chunkedTransferEncoding == _chunkedTransferEncoding) return;
+ if (chunkedTransferEncoding) {
+ List<String> values = _headers[HttpHeaders.TRANSFER_ENCODING];
+ if ((values == null || values.last != "chunked")) {
+ // Headers does not specify chunked encoding - add it if set.
+ _addValue(HttpHeaders.TRANSFER_ENCODING, "chunked");
+ }
+ contentLength = -1;
+ } else {
+ // Headers does specify chunked encoding - remove it if not set.
+ remove(HttpHeaders.TRANSFER_ENCODING, "chunked");
+ }
+ _chunkedTransferEncoding = chunkedTransferEncoding;
+ }
+
+ String get host => _host;
+
+ void set host(String host) {
+ _checkMutable();
+ _host = host;
+ _updateHostHeader();
+ }
+
+ int get port => _port;
+
+ void set port(int port) {
+ _checkMutable();
+ _port = port;
+ _updateHostHeader();
+ }
+
+ DateTime get ifModifiedSince {
+ List<String> values = _headers[HttpHeaders.IF_MODIFIED_SINCE];
+ if (values != null) {
+ try {
+ return HttpDate.parse(values[0]);
+ } on Exception catch (e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ void set ifModifiedSince(DateTime ifModifiedSince) {
+ _checkMutable();
+ // Format "ifModifiedSince" header with date in Greenwich Mean Time (GMT).
+ String formatted = HttpDate.format(ifModifiedSince.toUtc());
+ _set(HttpHeaders.IF_MODIFIED_SINCE, formatted);
+ }
+
+ DateTime get date {
+ List<String> values = _headers[HttpHeaders.DATE];
+ if (values != null) {
+ try {
+ return HttpDate.parse(values[0]);
+ } on Exception catch (e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ void set date(DateTime date) {
+ _checkMutable();
+ // Format "DateTime" header with date in Greenwich Mean Time (GMT).
+ String formatted = HttpDate.format(date.toUtc());
+ _set("date", formatted);
+ }
+
+ DateTime get expires {
+ List<String> values = _headers[HttpHeaders.EXPIRES];
+ if (values != null) {
+ try {
+ return HttpDate.parse(values[0]);
+ } on Exception catch (e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ void set expires(DateTime expires) {
+ _checkMutable();
+ // Format "Expires" header with date in Greenwich Mean Time (GMT).
+ String formatted = HttpDate.format(expires.toUtc());
+ _set(HttpHeaders.EXPIRES, formatted);
+ }
+
+ ContentType get contentType {
+ var values = _headers["content-type"];
+ if (values != null) {
+ return ContentType.parse(values[0]);
+ } else {
+ return null;
+ }
+ }
+
+ void set contentType(ContentType contentType) {
+ _checkMutable();
+ _set(HttpHeaders.CONTENT_TYPE, contentType.toString());
+ }
+
+ void clear() {
+ _checkMutable();
+ _headers.clear();
+ _contentLength = -1;
+ _persistentConnection = true;
+ _chunkedTransferEncoding = false;
+ _host = null;
+ _port = null;
+ }
+
+ // [name] must be a lower-case version of the name.
+ void _add(String name, value) {
+ assert(name == _validateField(name));
+ // Use the length as index on what method to call. This is notable
+ // faster than computing hash and looking up in a hash-map.
+ switch (name.length) {
+ case 4:
+ if (HttpHeaders.DATE == name) {
+ _addDate(name, value);
+ return;
+ }
+ if (HttpHeaders.HOST == name) {
+ _addHost(name, value);
+ return;
+ }
+ break;
+ case 7:
+ if (HttpHeaders.EXPIRES == name) {
+ _addExpires(name, value);
+ return;
+ }
+ break;
+ case 10:
+ if (HttpHeaders.CONNECTION == name) {
+ _addConnection(name, value);
+ return;
+ }
+ break;
+ case 12:
+ if (HttpHeaders.CONTENT_TYPE == name) {
+ _addContentType(name, value);
+ return;
+ }
+ break;
+ case 14:
+ if (HttpHeaders.CONTENT_LENGTH == name) {
+ _addContentLength(name, value);
+ return;
+ }
+ break;
+ case 17:
+ if (HttpHeaders.TRANSFER_ENCODING == name) {
+ _addTransferEncoding(name, value);
+ return;
+ }
+ if (HttpHeaders.IF_MODIFIED_SINCE == name) {
+ _addIfModifiedSince(name, value);
+ return;
+ }
+ }
+ _addValue(name, value);
+ }
+
+ void _addContentLength(String name, value) {
+ if (value is int) {
+ contentLength = value;
+ } else if (value is String) {
+ contentLength = int.parse(value);
+ } else {
+ throw new HttpException("Unexpected type for header named $name");
+ }
+ }
+
+ void _addTransferEncoding(String name, value) {
+ if (value == "chunked") {
+ chunkedTransferEncoding = true;
+ } else {
+ _addValue(HttpHeaders.TRANSFER_ENCODING, value);
+ }
+ }
+
+ void _addDate(String name, value) {
+ if (value is DateTime) {
+ date = value;
+ } else if (value is String) {
+ _set(HttpHeaders.DATE, value);
+ } else {
+ throw new HttpException("Unexpected type for header named $name");
+ }
+ }
+
+ void _addExpires(String name, value) {
+ if (value is DateTime) {
+ expires = value;
+ } else if (value is String) {
+ _set(HttpHeaders.EXPIRES, value);
+ } else {
+ throw new HttpException("Unexpected type for header named $name");
+ }
+ }
+
+ void _addIfModifiedSince(String name, value) {
+ if (value is DateTime) {
+ ifModifiedSince = value;
+ } else if (value is String) {
+ _set(HttpHeaders.IF_MODIFIED_SINCE, value);
+ } else {
+ throw new HttpException("Unexpected type for header named $name");
+ }
+ }
+
+ void _addHost(String name, value) {
+ if (value is String) {
+ int pos = value.indexOf(":");
+ if (pos == -1) {
+ _host = value;
+ _port = HttpClient.DEFAULT_HTTP_PORT;
+ } else {
+ if (pos > 0) {
+ _host = value.substring(0, pos);
+ } else {
+ _host = null;
+ }
+ if (pos + 1 == value.length) {
+ _port = HttpClient.DEFAULT_HTTP_PORT;
+ } else {
+ try {
+ _port = int.parse(value.substring(pos + 1));
+ } on FormatException catch (e) {
+ _port = null;
+ }
+ }
+ }
+ _set(HttpHeaders.HOST, value);
+ } else {
+ throw new HttpException("Unexpected type for header named $name");
+ }
+ }
+
+ void _addConnection(String name, value) {
+ var lowerCaseValue = value.toLowerCase();
+ if (lowerCaseValue == 'close') {
+ _persistentConnection = false;
+ } else if (lowerCaseValue == 'keep-alive') {
+ _persistentConnection = true;
+ }
+ _addValue(name, value);
+ }
+
+ void _addContentType(String name, value) {
+ _set(HttpHeaders.CONTENT_TYPE, value);
+ }
+
+ void _addValue(String name, Object value) {
+ List<String> values = _headers[name];
+ if (values == null) {
+ values = new List<String>();
+ _headers[name] = values;
+ }
+ if (value is DateTime) {
+ values.add(HttpDate.format(value));
+ } else if (value is String) {
+ values.add(value);
+ } else {
+ values.add(_validateValue(value.toString()));
+ }
+ }
+
+ void _set(String name, String value) {
+ assert(name == _validateField(name));
+ List<String> values = new List<String>();
+ _headers[name] = values;
+ values.add(value);
+ }
+
+ _checkMutable() {
+ if (!_mutable) throw new HttpException("HTTP headers are not mutable");
+ }
+
+ _updateHostHeader() {
+ bool defaultPort = _port == null || _port == _defaultPortForScheme;
+ _set("host", defaultPort ? host : "$host:$_port");
+ }
+
+ _foldHeader(String name) {
+ if (name == HttpHeaders.SET_COOKIE ||
+ (_noFoldingHeaders != null && _noFoldingHeaders.indexOf(name) != -1)) {
+ return false;
+ }
+ return true;
+ }
+
+ void _finalize() {
+ _mutable = false;
+ }
+
+ void _build(BytesBuilder builder) {
+ for (String name in _headers.keys) {
+ List<String> values = _headers[name];
+ bool fold = _foldHeader(name);
+ var nameData = name.codeUnits;
+ builder.add(nameData);
+ builder.addByte(_CharCode.COLON);
+ builder.addByte(_CharCode.SP);
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ if (fold) {
+ builder.addByte(_CharCode.COMMA);
+ builder.addByte(_CharCode.SP);
+ } else {
+ builder.addByte(_CharCode.CR);
+ builder.addByte(_CharCode.LF);
+ builder.add(nameData);
+ builder.addByte(_CharCode.COLON);
+ builder.addByte(_CharCode.SP);
+ }
+ }
+ builder.add(values[i].codeUnits);
+ }
+ builder.addByte(_CharCode.CR);
+ builder.addByte(_CharCode.LF);
+ }
+ }
+
+ String toString() {
+ StringBuffer sb = new StringBuffer();
+ _headers.forEach((String name, List<String> values) {
+ sb..write(name)..write(": ");
+ bool fold = _foldHeader(name);
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ if (fold) {
+ sb.write(", ");
+ } else {
+ sb..write("\n")..write(name)..write(": ");
+ }
+ }
+ sb.write(values[i]);
+ }
+ sb.write("\n");
+ });
+ return sb.toString();
+ }
+
+ List<Cookie> _parseCookies() {
+ // Parse a Cookie header value according to the rules in RFC 6265.
+ var cookies = new List<Cookie>();
+ void parseCookieString(String s) {
+ int index = 0;
+
+ bool done() => index == -1 || index == s.length;
+
+ void skipWS() {
+ while (!done()) {
+ if (s[index] != " " && s[index] != "\t") return;
+ index++;
+ }
+ }
+
+ String parseName() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == " " || s[index] == "\t" || s[index] == "=") break;
+ index++;
+ }
+ return s.substring(start, index);
+ }
+
+ String parseValue() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == " " || s[index] == "\t" || s[index] == ";") break;
+ index++;
+ }
+ return s.substring(start, index);
+ }
+
+ bool expect(String expected) {
+ if (done()) return false;
+ if (s[index] != expected) return false;
+ index++;
+ return true;
+ }
+
+ while (!done()) {
+ skipWS();
+ if (done()) return;
+ String name = parseName();
+ skipWS();
+ if (!expect("=")) {
+ index = s.indexOf(';', index);
+ continue;
+ }
+ skipWS();
+ String value = parseValue();
+ try {
+ cookies.add(new _Cookie(name, value));
+ } catch (_) {
+ // Skip it, invalid cookie data.
+ }
+ skipWS();
+ if (done()) return;
+ if (!expect(";")) {
+ index = s.indexOf(';', index);
+ continue;
+ }
+ }
+ }
+
+ List<String> values = _headers[HttpHeaders.COOKIE];
+ if (values != null) {
+ values.forEach((headerValue) => parseCookieString(headerValue));
+ }
+ return cookies;
+ }
+
+ static String _validateField(String field) {
+ for (var i = 0; i < field.length; i++) {
+ if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) {
+ throw new FormatException(
+ "Invalid HTTP header field name: ${json.encode(field)}");
+ }
+ }
+ return field.toLowerCase();
+ }
+
+ static _validateValue(value) {
+ if (value is! String) return value;
+ for (var i = 0; i < value.length; i++) {
+ if (!_HttpParser._isValueChar(value.codeUnitAt(i))) {
+ throw new FormatException(
+ "Invalid HTTP header field value: ${json.encode(value)}");
+ }
+ }
+ return value;
+ }
+}
+
+class _HeaderValue implements HeaderValue {
+ String _value;
+ Map<String, String> _parameters;
+ Map<String, String> _unmodifiableParameters;
+
+ _HeaderValue([this._value = "", Map<String, String> parameters]) {
+ if (parameters != null) {
+ _parameters = new HashMap<String, String>.from(parameters);
+ }
+ }
+
+ static _HeaderValue parse(String value,
+ {parameterSeparator: ";",
+ valueSeparator: null,
+ preserveBackslash: false}) {
+ // Parse the string.
+ var result = new _HeaderValue();
+ result._parse(value, parameterSeparator, valueSeparator, preserveBackslash);
+ return result;
+ }
+
+ String get value => _value;
+
+ void _ensureParameters() {
+ if (_parameters == null) {
+ _parameters = new HashMap<String, String>();
+ }
+ }
+
+ Map<String, String> get parameters {
+ _ensureParameters();
+ if (_unmodifiableParameters == null) {
+ _unmodifiableParameters = new UnmodifiableMapView(_parameters);
+ }
+ return _unmodifiableParameters;
+ }
+
+ String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb.write(_value);
+ if (parameters != null && parameters.length > 0) {
+ _parameters.forEach((String name, String value) {
+ sb..write("; ")..write(name)..write("=")..write(value);
+ });
+ }
+ return sb.toString();
+ }
+
+ void _parse(String s, String parameterSeparator, String valueSeparator,
+ bool preserveBackslash) {
+ int index = 0;
+
+ bool done() => index == s.length;
+
+ void skipWS() {
+ while (!done()) {
+ if (s[index] != " " && s[index] != "\t") return;
+ index++;
+ }
+ }
+
+ String parseValue() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == " " ||
+ s[index] == "\t" ||
+ s[index] == valueSeparator ||
+ s[index] == parameterSeparator) break;
+ index++;
+ }
+ return s.substring(start, index);
+ }
+
+ void expect(String expected) {
+ if (done() || s[index] != expected) {
+ throw new HttpException("Failed to parse header value");
+ }
+ index++;
+ }
+
+ void maybeExpect(String expected) {
+ if (s[index] == expected) index++;
+ }
+
+ void parseParameters() {
+ var parameters = new HashMap<String, String>();
+ _parameters = new UnmodifiableMapView(parameters);
+
+ String parseParameterName() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == " " ||
+ s[index] == "\t" ||
+ s[index] == "=" ||
+ s[index] == parameterSeparator ||
+ s[index] == valueSeparator) break;
+ index++;
+ }
+ return s.substring(start, index).toLowerCase();
+ }
+
+ String parseParameterValue() {
+ if (!done() && s[index] == "\"") {
+ // Parse quoted value.
+ StringBuffer sb = new StringBuffer();
+ index++;
+ while (!done()) {
+ if (s[index] == "\\") {
+ if (index + 1 == s.length) {
+ throw new HttpException("Failed to parse header value");
+ }
+ if (preserveBackslash && s[index + 1] != "\"") {
+ sb.write(s[index]);
+ }
+ index++;
+ } else if (s[index] == "\"") {
+ index++;
+ break;
+ }
+ sb.write(s[index]);
+ index++;
+ }
+ return sb.toString();
+ } else {
+ // Parse non-quoted value.
+ var val = parseValue();
+ return val == "" ? null : val;
+ }
+ }
+
+ while (!done()) {
+ skipWS();
+ if (done()) return;
+ String name = parseParameterName();
+ skipWS();
+ if (done()) {
+ parameters[name] = null;
+ return;
+ }
+ maybeExpect("=");
+ skipWS();
+ if (done()) {
+ parameters[name] = null;
+ return;
+ }
+ String value = parseParameterValue();
+ if (name == 'charset' && this is _ContentType && value != null) {
+ // Charset parameter of ContentTypes are always lower-case.
+ value = value.toLowerCase();
+ }
+ parameters[name] = value;
+ skipWS();
+ if (done()) return;
+ // TODO: Implement support for multi-valued parameters.
+ if (s[index] == valueSeparator) return;
+ expect(parameterSeparator);
+ }
+ }
+
+ skipWS();
+ _value = parseValue();
+ skipWS();
+ if (done()) return;
+ maybeExpect(parameterSeparator);
+ parseParameters();
+ }
+}
+
+class _ContentType extends _HeaderValue implements ContentType {
+ String _primaryType = "";
+ String _subType = "";
+
+ _ContentType(String primaryType, String subType, String charset,
+ Map<String, String> parameters)
+ : _primaryType = primaryType,
+ _subType = subType,
+ super("") {
+ if (_primaryType == null) _primaryType = "";
+ if (_subType == null) _subType = "";
+ _value = "$_primaryType/$_subType";
+ if (parameters != null) {
+ _ensureParameters();
+ parameters.forEach((String key, String value) {
+ String lowerCaseKey = key.toLowerCase();
+ if (lowerCaseKey == "charset") {
+ value = value.toLowerCase();
+ }
+ this._parameters[lowerCaseKey] = value;
+ });
+ }
+ if (charset != null) {
+ _ensureParameters();
+ this._parameters["charset"] = charset.toLowerCase();
+ }
+ }
+
+ _ContentType._();
+
+ static _ContentType parse(String value) {
+ var result = new _ContentType._();
+ result._parse(value, ";", null, false);
+ int index = result._value.indexOf("/");
+ if (index == -1 || index == (result._value.length - 1)) {
+ result._primaryType = result._value.trim().toLowerCase();
+ result._subType = "";
+ } else {
+ result._primaryType =
+ result._value.substring(0, index).trim().toLowerCase();
+ result._subType = result._value.substring(index + 1).trim().toLowerCase();
+ }
+ return result;
+ }
+
+ String get mimeType => '$primaryType/$subType';
+
+ String get primaryType => _primaryType;
+
+ String get subType => _subType;
+
+ String get charset => parameters["charset"];
+}
+
+class _Cookie implements Cookie {
+ String name;
+ String value;
+ DateTime expires;
+ int maxAge;
+ String domain;
+ String path;
+ bool httpOnly = false;
+ bool secure = false;
+
+ _Cookie([this.name, this.value]) {
+ // Default value of httponly is true.
+ httpOnly = true;
+ _validate();
+ }
+
+ _Cookie.fromSetCookieValue(String value) {
+ // Parse the 'set-cookie' header value.
+ _parseSetCookieValue(value);
+ }
+
+ // Parse a 'set-cookie' header value according to the rules in RFC 6265.
+ void _parseSetCookieValue(String s) {
+ int index = 0;
+
+ bool done() => index == s.length;
+
+ String parseName() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == "=") break;
+ index++;
+ }
+ return s.substring(start, index).trim();
+ }
+
+ String parseValue() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == ";") break;
+ index++;
+ }
+ return s.substring(start, index).trim();
+ }
+
+ void expect(String expected) {
+ if (done()) throw new HttpException("Failed to parse header value [$s]");
+ if (s[index] != expected) {
+ throw new HttpException("Failed to parse header value [$s]");
+ }
+ index++;
+ }
+
+ void parseAttributes() {
+ String parseAttributeName() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == "=" || s[index] == ";") break;
+ index++;
+ }
+ return s.substring(start, index).trim().toLowerCase();
+ }
+
+ String parseAttributeValue() {
+ int start = index;
+ while (!done()) {
+ if (s[index] == ";") break;
+ index++;
+ }
+ return s.substring(start, index).trim().toLowerCase();
+ }
+
+ while (!done()) {
+ String name = parseAttributeName();
+ String value = "";
+ if (!done() && s[index] == "=") {
+ index++; // Skip the = character.
+ value = parseAttributeValue();
+ }
+ if (name == "expires") {
+ expires = HttpDate._parseCookieDate(value);
+ } else if (name == "max-age") {
+ maxAge = int.parse(value);
+ } else if (name == "domain") {
+ domain = value;
+ } else if (name == "path") {
+ path = value;
+ } else if (name == "httponly") {
+ httpOnly = true;
+ } else if (name == "secure") {
+ secure = true;
+ }
+ if (!done()) index++; // Skip the ; character
+ }
+ }
+
+ name = parseName();
+ if (done() || name.length == 0) {
+ throw new HttpException("Failed to parse header value [$s]");
+ }
+ index++; // Skip the = character.
+ value = parseValue();
+ _validate();
+ if (done()) return;
+ index++; // Skip the ; character.
+ parseAttributes();
+ }
+
+ String toString() {
+ StringBuffer sb = new StringBuffer();
+ sb..write(name)..write("=")..write(value);
+ if (expires != null) {
+ sb..write("; Expires=")..write(HttpDate.format(expires));
+ }
+ if (maxAge != null) {
+ sb..write("; Max-Age=")..write(maxAge);
+ }
+ if (domain != null) {
+ sb..write("; Domain=")..write(domain);
+ }
+ if (path != null) {
+ sb..write("; Path=")..write(path);
+ }
+ if (secure) sb.write("; Secure");
+ if (httpOnly) sb.write("; HttpOnly");
+ return sb.toString();
+ }
+
+ void _validate() {
+ const SEPERATORS = const [
+ "(",
+ ")",
+ "<",
+ ">",
+ "@",
+ ",",
+ ";",
+ ":",
+ "\\",
+ '"',
+ "/",
+ "[",
+ "]",
+ "?",
+ "=",
+ "{",
+ "}"
+ ];
+ for (int i = 0; i < name.length; i++) {
+ int codeUnit = name.codeUnits[i];
+ if (codeUnit <= 32 ||
+ codeUnit >= 127 ||
+ SEPERATORS.indexOf(name[i]) >= 0) {
+ throw new FormatException(
+ "Invalid character in cookie name, code unit: '$codeUnit'");
+ }
+ }
+ for (int i = 0; i < value.length; i++) {
+ int codeUnit = value.codeUnits[i];
+ if (!(codeUnit == 0x21 ||
+ (codeUnit >= 0x23 && codeUnit <= 0x2B) ||
+ (codeUnit >= 0x2D && codeUnit <= 0x3A) ||
+ (codeUnit >= 0x3C && codeUnit <= 0x5B) ||
+ (codeUnit >= 0x5D && codeUnit <= 0x7E))) {
+ throw new FormatException(
+ "Invalid character in cookie value, code unit: '$codeUnit'");
+ }
+ }
+ }
+}
diff --git a/lib/src/http_impl.dart b/lib/src/http_impl.dart
new file mode 100644
index 0000000..d7b7628
--- /dev/null
+++ b/lib/src/http_impl.dart
@@ -0,0 +1,3137 @@
+// Copyright (c) 2013, 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 http_io;
+
+int _nextServiceId = 1;
+
+// TODO(ajohnsen): Use other way of getting a unique id.
+abstract class _ServiceObject {
+ int __serviceId = 0;
+ int get _serviceId {
+ if (__serviceId == 0) __serviceId = _nextServiceId++;
+ return __serviceId;
+ }
+
+ Map _toJSON(bool ref);
+
+ String get _servicePath => "$_serviceTypePath/$_serviceId";
+
+ String get _serviceTypePath;
+
+ String get _serviceTypeName;
+
+ String _serviceType(bool ref) {
+ if (ref) return "@$_serviceTypeName";
+ return _serviceTypeName;
+ }
+}
+
+class _CopyingBytesBuilder implements BytesBuilder {
+ // Start with 1024 bytes.
+ static const int _INIT_SIZE = 1024;
+
+ static final _emptyList = new Uint8List(0);
+
+ int _length = 0;
+ Uint8List _buffer;
+
+ _CopyingBytesBuilder([int initialCapacity = 0])
+ : _buffer = (initialCapacity <= 0)
+ ? _emptyList
+ : new Uint8List(_pow2roundup(initialCapacity));
+
+ void add(List<int> bytes) {
+ int bytesLength = bytes.length;
+ if (bytesLength == 0) return;
+ int required = _length + bytesLength;
+ if (_buffer.length < required) {
+ _grow(required);
+ }
+ assert(_buffer.length >= required);
+ if (bytes is Uint8List) {
+ _buffer.setRange(_length, required, bytes);
+ } else {
+ for (int i = 0; i < bytesLength; i++) {
+ _buffer[_length + i] = bytes[i];
+ }
+ }
+ _length = required;
+ }
+
+ void addByte(int byte) {
+ if (_buffer.length == _length) {
+ // The grow algorithm always at least doubles.
+ // If we added one to _length it would quadruple unnecessarily.
+ _grow(_length);
+ }
+ assert(_buffer.length > _length);
+ _buffer[_length] = byte;
+ _length++;
+ }
+
+ void _grow(int required) {
+ // We will create a list in the range of 2-4 times larger than
+ // required.
+ int newSize = required * 2;
+ if (newSize < _INIT_SIZE) {
+ newSize = _INIT_SIZE;
+ } else {
+ newSize = _pow2roundup(newSize);
+ }
+ var newBuffer = new Uint8List(newSize);
+ newBuffer.setRange(0, _buffer.length, _buffer);
+ _buffer = newBuffer;
+ }
+
+ List<int> takeBytes() {
+ if (_length == 0) return _emptyList;
+ var buffer = new Uint8List.view(_buffer.buffer, 0, _length);
+ clear();
+ return buffer;
+ }
+
+ List<int> toBytes() {
+ if (_length == 0) return _emptyList;
+ return new Uint8List.fromList(
+ new Uint8List.view(_buffer.buffer, 0, _length));
+ }
+
+ int get length => _length;
+
+ bool get isEmpty => _length == 0;
+
+ bool get isNotEmpty => _length != 0;
+
+ void clear() {
+ _length = 0;
+ _buffer = _emptyList;
+ }
+
+ static int _pow2roundup(int x) {
+ assert(x > 0);
+ --x;
+ x |= x >> 1;
+ x |= x >> 2;
+ x |= x >> 4;
+ x |= x >> 8;
+ x |= x >> 16;
+ return x + 1;
+ }
+}
+
+const int _OUTGOING_BUFFER_SIZE = 8 * 1024;
+
+typedef void _BytesConsumer(List<int> bytes);
+
+class _HttpIncoming extends Stream<List<int>> {
+ final int _transferLength;
+ final Completer _dataCompleter = new Completer();
+ Stream<List<int>> _stream;
+
+ bool fullBodyRead = false;
+
+ // Common properties.
+ final _HttpHeaders headers;
+ bool upgraded = false;
+
+ // ClientResponse properties.
+ int statusCode;
+ String reasonPhrase;
+
+ // Request properties.
+ String method;
+ Uri uri;
+
+ bool hasSubscriber = false;
+
+ // The transfer length if the length of the message body as it
+ // appears in the message (RFC 2616 section 4.4). This can be -1 if
+ // the length of the massage body is not known due to transfer
+ // codings.
+ int get transferLength => _transferLength;
+
+ _HttpIncoming(this.headers, this._transferLength, this._stream);
+
+ StreamSubscription<List<int>> listen(void onData(List<int> event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ hasSubscriber = true;
+ return _stream.handleError((error) {
+ throw new HttpException(error.message, uri: uri);
+ }).listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ // Is completed once all data have been received.
+ Future get dataDone => _dataCompleter.future;
+
+ void close(bool closing) {
+ fullBodyRead = true;
+ hasSubscriber = true;
+ _dataCompleter.complete(closing);
+ }
+}
+
+abstract class _HttpInboundMessage extends Stream<List<int>> {
+ final _HttpIncoming _incoming;
+ List<Cookie> _cookies;
+
+ _HttpInboundMessage(this._incoming);
+
+ List<Cookie> get cookies {
+ if (_cookies != null) return _cookies;
+ return _cookies = headers._parseCookies();
+ }
+
+ _HttpHeaders get headers => _incoming.headers;
+ String get protocolVersion => headers.protocolVersion;
+ int get contentLength => headers.contentLength;
+ bool get persistentConnection => headers.persistentConnection;
+}
+
+class _HttpRequest extends _HttpInboundMessage implements HttpRequest {
+ final HttpResponse response;
+
+ final _HttpServer _httpServer;
+
+ final _HttpConnection _httpConnection;
+
+ _HttpSession _session;
+
+ Uri _requestedUri;
+
+ _HttpRequest(this.response, _HttpIncoming _incoming, this._httpServer,
+ this._httpConnection)
+ : super(_incoming) {
+ if (headers.protocolVersion == "1.1") {
+ response.headers
+ ..chunkedTransferEncoding = true
+ ..persistentConnection = headers.persistentConnection;
+ }
+
+ if (_httpServer._sessionManagerInstance != null) {
+ // Map to session if exists.
+ var sessionIds = cookies
+ .where((cookie) => cookie.name.toUpperCase() == _DART_SESSION_ID)
+ .map((cookie) => cookie.value);
+ for (var sessionId in sessionIds) {
+ _session = _httpServer._sessionManager.getSession(sessionId);
+ if (_session != null) {
+ _session._markSeen();
+ break;
+ }
+ }
+ }
+ }
+
+ StreamSubscription<List<int>> listen(void onData(List<int> event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ return _incoming.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ Uri get uri => _incoming.uri;
+
+ Uri get requestedUri {
+ if (_requestedUri == null) {
+ var proto = headers['x-forwarded-proto'];
+ var scheme = proto != null
+ ? proto.first
+ : _httpConnection._socket is SecureSocket ? "https" : "http";
+ var hostList = headers['x-forwarded-host'];
+ String host;
+ if (hostList != null) {
+ host = hostList.first;
+ } else {
+ hostList = headers['host'];
+ if (hostList != null) {
+ host = hostList.first;
+ } else {
+ host = "${_httpServer.address.host}:${_httpServer.port}";
+ }
+ }
+ _requestedUri = Uri.parse("$scheme://$host$uri");
+ }
+ return _requestedUri;
+ }
+
+ String get method => _incoming.method;
+
+ HttpSession get session {
+ if (_session != null) {
+ if (_session._destroyed) {
+ // It's destroyed, clear it.
+ _session = null;
+ // Create new session object by calling recursive.
+ return session;
+ }
+ // It's already mapped, use it.
+ return _session;
+ }
+ // Create session, store it in connection, and return.
+ return _session = _httpServer._sessionManager.createSession();
+ }
+
+ HttpConnectionInfo get connectionInfo => _httpConnection.connectionInfo;
+
+ X509Certificate get certificate {
+ var socket = _httpConnection._socket;
+ if (socket is SecureSocket) return socket.peerCertificate;
+ return null;
+ }
+}
+
+class _HttpClientResponse extends _HttpInboundMessage
+ implements HttpClientResponse {
+ List<RedirectInfo> get redirects => _httpRequest._responseRedirects;
+
+ // The HttpClient this response belongs to.
+ final _HttpClient _httpClient;
+
+ // The HttpClientRequest of this response.
+ final _HttpClientRequest _httpRequest;
+
+ _HttpClientResponse(
+ _HttpIncoming _incoming, this._httpRequest, this._httpClient)
+ : super(_incoming) {
+ // Set uri for potential exceptions.
+ _incoming.uri = _httpRequest.uri;
+ }
+
+ int get statusCode => _incoming.statusCode;
+ String get reasonPhrase => _incoming.reasonPhrase;
+
+ X509Certificate get certificate {
+ var socket = _httpRequest._httpClientConnection._socket;
+ if (socket is SecureSocket) return socket.peerCertificate;
+ throw new UnsupportedError("Socket is not a SecureSocket");
+ }
+
+ List<Cookie> get cookies {
+ if (_cookies != null) return _cookies;
+ _cookies = new List<Cookie>();
+ List<String> values = headers[HttpHeaders.SET_COOKIE];
+ if (values != null) {
+ values.forEach((value) {
+ _cookies.add(new Cookie.fromSetCookieValue(value));
+ });
+ }
+ return _cookies;
+ }
+
+ bool get isRedirect {
+ if (_httpRequest.method == "GET" || _httpRequest.method == "HEAD") {
+ return statusCode == HttpStatus.MOVED_PERMANENTLY ||
+ statusCode == HttpStatus.FOUND ||
+ statusCode == HttpStatus.SEE_OTHER ||
+ statusCode == HttpStatus.TEMPORARY_REDIRECT;
+ } else if (_httpRequest.method == "POST") {
+ return statusCode == HttpStatus.SEE_OTHER;
+ }
+ return false;
+ }
+
+ Future<HttpClientResponse> redirect(
+ [String method, Uri url, bool followLoops]) {
+ if (method == null) {
+ // Set method as defined by RFC 2616 section 10.3.4.
+ if (statusCode == HttpStatus.SEE_OTHER && _httpRequest.method == "POST") {
+ method = "GET";
+ } else {
+ method = _httpRequest.method;
+ }
+ }
+ if (url == null) {
+ String location = headers.value(HttpHeaders.LOCATION);
+ if (location == null) {
+ throw new StateError("Response has no Location header for redirect");
+ }
+ url = Uri.parse(location);
+ }
+ if (followLoops != true) {
+ for (var redirect in redirects) {
+ if (redirect.location == url) {
+ return new Future.error(
+ new RedirectException("Redirect loop detected", redirects));
+ }
+ }
+ }
+ return _httpClient
+ ._openUrlFromRequest(method, url, _httpRequest)
+ .then((request) {
+ request._responseRedirects
+ ..addAll(this.redirects)
+ ..add(new _RedirectInfo(statusCode, method, url));
+ return request.close();
+ });
+ }
+
+ StreamSubscription<List<int>> listen(void onData(List<int> event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ if (_incoming.upgraded) {
+ // If upgraded, the connection is already 'removed' form the client.
+ // Since listening to upgraded data is 'bogus', simply close and
+ // return empty stream subscription.
+ _httpRequest._httpClientConnection.destroy();
+ return new Stream<List<int>>.empty().listen(null, onDone: onDone);
+ }
+ var stream = _incoming;
+ if (_httpClient.autoUncompress &&
+ headers.value(HttpHeaders.CONTENT_ENCODING) == "gzip") {
+ stream = stream.transform(GZIP.decoder);
+ }
+ return stream.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ Future<Socket> detachSocket() {
+ _httpClient._connectionClosed(_httpRequest._httpClientConnection);
+ return _httpRequest._httpClientConnection.detachSocket();
+ }
+
+ HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo;
+
+ bool get _shouldAuthenticateProxy {
+ // Only try to authenticate if there is a challenge in the response.
+ List<String> challenge = headers[HttpHeaders.PROXY_AUTHENTICATE];
+ return statusCode == HttpStatus.PROXY_AUTHENTICATION_REQUIRED &&
+ challenge != null &&
+ challenge.length == 1;
+ }
+
+ bool get _shouldAuthenticate {
+ // Only try to authenticate if there is a challenge in the response.
+ List<String> challenge = headers[HttpHeaders.WWW_AUTHENTICATE];
+ return statusCode == HttpStatus.UNAUTHORIZED &&
+ challenge != null &&
+ challenge.length == 1;
+ }
+
+ Future<HttpClientResponse> _authenticate(bool proxyAuth) {
+ Future<HttpClientResponse> retry() {
+ // Drain body and retry.
+ return drain().then((_) {
+ return _httpClient
+ ._openUrlFromRequest(
+ _httpRequest.method, _httpRequest.uri, _httpRequest)
+ .then((request) => request.close());
+ });
+ }
+
+ List<String> authChallenge() {
+ return proxyAuth
+ ? headers[HttpHeaders.PROXY_AUTHENTICATE]
+ : headers[HttpHeaders.WWW_AUTHENTICATE];
+ }
+
+ _Credentials findCredentials(_AuthenticationScheme scheme) {
+ return proxyAuth
+ ? _httpClient._findProxyCredentials(_httpRequest._proxy, scheme)
+ : _httpClient._findCredentials(_httpRequest.uri, scheme);
+ }
+
+ void removeCredentials(_Credentials cr) {
+ if (proxyAuth) {
+ _httpClient._removeProxyCredentials(cr);
+ } else {
+ _httpClient._removeCredentials(cr);
+ }
+ }
+
+ Future requestAuthentication(_AuthenticationScheme scheme, String realm) {
+ if (proxyAuth) {
+ if (_httpClient._authenticateProxy == null) {
+ return new Future.value(false);
+ }
+ var proxy = _httpRequest._proxy;
+ return _httpClient._authenticateProxy(
+ proxy.host, proxy.port, scheme.toString(), realm);
+ } else {
+ if (_httpClient._authenticate == null) {
+ return new Future.value(false);
+ }
+ return _httpClient._authenticate(
+ _httpRequest.uri, scheme.toString(), realm);
+ }
+ }
+
+ List<String> challenge = authChallenge();
+ assert(challenge != null || challenge.length == 1);
+ _HeaderValue header =
+ _HeaderValue.parse(challenge[0], parameterSeparator: ",");
+ _AuthenticationScheme scheme =
+ new _AuthenticationScheme.fromString(header.value);
+ String realm = header.parameters["realm"];
+
+ // See if any matching credentials are available.
+ _Credentials cr = findCredentials(scheme);
+ if (cr != null) {
+ // For basic authentication don't retry already used credentials
+ // as they must have already been added to the request causing
+ // this authenticate response.
+ if (cr.scheme == _AuthenticationScheme.BASIC && !cr.used) {
+ // Credentials where found, prepare for retrying the request.
+ return retry();
+ }
+
+ // Digest authentication only supports the MD5 algorithm.
+ if (cr.scheme == _AuthenticationScheme.DIGEST &&
+ (header.parameters["algorithm"] == null ||
+ header.parameters["algorithm"].toLowerCase() == "md5")) {
+ if (cr.nonce == null || cr.nonce == header.parameters["nonce"]) {
+ // If the nonce is not set then this is the first authenticate
+ // response for these credentials. Set up authentication state.
+ if (cr.nonce == null) {
+ cr
+ ..nonce = header.parameters["nonce"]
+ ..algorithm = "MD5"
+ ..qop = header.parameters["qop"]
+ ..nonceCount = 0;
+ }
+ // Credentials where found, prepare for retrying the request.
+ return retry();
+ } else if (header.parameters["stale"] != null &&
+ header.parameters["stale"].toLowerCase() == "true") {
+ // If stale is true retry with new nonce.
+ cr.nonce = header.parameters["nonce"];
+ // Credentials where found, prepare for retrying the request.
+ return retry();
+ }
+ }
+ }
+
+ // Ask for more credentials if none found or the one found has
+ // already been used. If it has already been used it must now be
+ // invalid and is removed.
+ if (cr != null) {
+ removeCredentials(cr);
+ cr = null;
+ }
+ return requestAuthentication(scheme, realm).then((credsAvailable) {
+ if (credsAvailable) {
+ cr = _httpClient._findCredentials(_httpRequest.uri, scheme);
+ return retry();
+ } else {
+ // No credentials available, complete with original response.
+ return this;
+ }
+ });
+ }
+}
+
+class _StreamSinkImpl<T> implements StreamSink<T> {
+ final StreamConsumer<T> _target;
+ final Completer _doneCompleter = new Completer();
+ StreamController<T> _controllerInstance;
+ Completer _controllerCompleter;
+ bool _isClosed = false;
+ bool _isBound = false;
+ bool _hasError = false;
+
+ _StreamSinkImpl(this._target);
+
+ void _reportClosedSink() {
+ stderr.writeln("StreamSink is closed and adding to it is an error.");
+ stderr.writeln(" See http://dartbug.com/29554.");
+ stderr.writeln(StackTrace.current);
+ }
+
+ void add(T data) {
+ if (_isClosed) {
+ _reportClosedSink();
+ return;
+ }
+ _controller.add(data);
+ }
+
+ void addError(error, [StackTrace stackTrace]) {
+ if (_isClosed) {
+ _reportClosedSink();
+ return;
+ }
+ _controller.addError(error, stackTrace);
+ }
+
+ Future addStream(Stream<T> stream) {
+ if (_isBound) {
+ throw new StateError("StreamSink is already bound to a stream");
+ }
+ _isBound = true;
+ if (_hasError) return done;
+ // Wait for any sync operations to complete.
+ Future targetAddStream() {
+ return _target.addStream(stream).whenComplete(() {
+ _isBound = false;
+ });
+ }
+
+ if (_controllerInstance == null) return targetAddStream();
+ var future = _controllerCompleter.future;
+ _controllerInstance.close();
+ return future.then((_) => targetAddStream());
+ }
+
+ Future flush() {
+ if (_isBound) {
+ throw new StateError("StreamSink is bound to a stream");
+ }
+ if (_controllerInstance == null) return new Future.value(this);
+ // Adding an empty stream-controller will return a future that will complete
+ // when all data is done.
+ _isBound = true;
+ var future = _controllerCompleter.future;
+ _controllerInstance.close();
+ return future.whenComplete(() {
+ _isBound = false;
+ });
+ }
+
+ Future close() {
+ if (_isBound) {
+ throw new StateError("StreamSink is bound to a stream");
+ }
+ if (!_isClosed) {
+ _isClosed = true;
+ if (_controllerInstance != null) {
+ _controllerInstance.close();
+ } else {
+ _closeTarget();
+ }
+ }
+ return done;
+ }
+
+ void _closeTarget() {
+ _target.close().then(_completeDoneValue, onError: _completeDoneError);
+ }
+
+ Future get done => _doneCompleter.future;
+
+ void _completeDoneValue(value) {
+ if (!_doneCompleter.isCompleted) {
+ _doneCompleter.complete(value);
+ }
+ }
+
+ void _completeDoneError(error, StackTrace stackTrace) {
+ if (!_doneCompleter.isCompleted) {
+ _hasError = true;
+ _doneCompleter.completeError(error, stackTrace);
+ }
+ }
+
+ StreamController<T> get _controller {
+ if (_isBound) {
+ throw new StateError("StreamSink is bound to a stream");
+ }
+ if (_isClosed) {
+ throw new StateError("StreamSink is closed");
+ }
+ if (_controllerInstance == null) {
+ _controllerInstance = new StreamController<T>(sync: true);
+ _controllerCompleter = new Completer();
+ _target.addStream(_controller.stream).then((_) {
+ if (_isBound) {
+ // A new stream takes over - forward values to that stream.
+ _controllerCompleter.complete(this);
+ _controllerCompleter = null;
+ _controllerInstance = null;
+ } else {
+ // No new stream, .close was called. Close _target.
+ _closeTarget();
+ }
+ }, onError: (error, stackTrace) {
+ if (_isBound) {
+ // A new stream takes over - forward errors to that stream.
+ _controllerCompleter.completeError(error, stackTrace);
+ _controllerCompleter = null;
+ _controllerInstance = null;
+ } else {
+ // No new stream. No need to close target, as it has already
+ // failed.
+ _completeDoneError(error, stackTrace);
+ }
+ });
+ }
+ return _controllerInstance;
+ }
+}
+
+class _IOSinkImpl extends _StreamSinkImpl<List<int>> implements IOSink {
+ Encoding _encoding;
+ bool _encodingMutable = true;
+
+ _IOSinkImpl(StreamConsumer<List<int>> target, this._encoding) : super(target);
+
+ Encoding get encoding => _encoding;
+
+ void set encoding(Encoding value) {
+ if (!_encodingMutable) {
+ throw new StateError("IOSink encoding is not mutable");
+ }
+ _encoding = value;
+ }
+
+ void write(Object obj) {
+ String string = '$obj';
+ if (string.isEmpty) return;
+ add(_encoding.encode(string));
+ }
+
+ void writeAll(Iterable objects, [String separator = ""]) {
+ Iterator iterator = objects.iterator;
+ if (!iterator.moveNext()) return;
+ if (separator.isEmpty) {
+ do {
+ write(iterator.current);
+ } while (iterator.moveNext());
+ } else {
+ write(iterator.current);
+ while (iterator.moveNext()) {
+ write(separator);
+ write(iterator.current);
+ }
+ }
+ }
+
+ void writeln([Object object = ""]) {
+ write(object);
+ write("\n");
+ }
+
+ void writeCharCode(int charCode) {
+ write(new String.fromCharCode(charCode));
+ }
+}
+
+abstract class _HttpOutboundMessage<T> extends _IOSinkImpl {
+ // Used to mark when the body should be written. This is used for HEAD
+ // requests and in error handling.
+ bool _encodingSet = false;
+
+ bool _bufferOutput = true;
+
+ final Uri _uri;
+ final _HttpOutgoing _outgoing;
+
+ final _HttpHeaders headers;
+
+ _HttpOutboundMessage(Uri uri, String protocolVersion, _HttpOutgoing outgoing,
+ {_HttpHeaders initialHeaders})
+ : _uri = uri,
+ headers = new _HttpHeaders(protocolVersion,
+ defaultPortForScheme: uri.scheme == 'https'
+ ? HttpClient.DEFAULT_HTTPS_PORT
+ : HttpClient.DEFAULT_HTTP_PORT,
+ initialHeaders: initialHeaders),
+ _outgoing = outgoing,
+ super(outgoing, null) {
+ _outgoing.outbound = this;
+ _encodingMutable = false;
+ }
+
+ int get contentLength => headers.contentLength;
+ void set contentLength(int contentLength) {
+ headers.contentLength = contentLength;
+ }
+
+ bool get persistentConnection => headers.persistentConnection;
+ void set persistentConnection(bool p) {
+ headers.persistentConnection = p;
+ }
+
+ bool get bufferOutput => _bufferOutput;
+ void set bufferOutput(bool bufferOutput) {
+ if (_outgoing.headersWritten) throw new StateError("Header already sent");
+ _bufferOutput = bufferOutput;
+ }
+
+ Encoding get encoding {
+ if (_encodingSet && _outgoing.headersWritten) {
+ return _encoding;
+ }
+ var charset;
+ if (headers.contentType != null && headers.contentType.charset != null) {
+ charset = headers.contentType.charset;
+ } else {
+ charset = "iso-8859-1";
+ }
+ return Encoding.getByName(charset);
+ }
+
+ void add(List<int> data) {
+ if (data.length == 0) return;
+ super.add(data);
+ }
+
+ void write(Object obj) {
+ if (!_encodingSet) {
+ _encoding = encoding;
+ _encodingSet = true;
+ }
+ super.write(obj);
+ }
+
+ void _writeHeader();
+
+ bool get _isConnectionClosed => false;
+}
+
+class _HttpResponse extends _HttpOutboundMessage<HttpResponse>
+ implements HttpResponse {
+ int _statusCode = 200;
+ String _reasonPhrase;
+ List<Cookie> _cookies;
+ _HttpRequest _httpRequest;
+ Duration _deadline;
+ Timer _deadlineTimer;
+
+ _HttpResponse(Uri uri, String protocolVersion, _HttpOutgoing outgoing,
+ HttpHeaders defaultHeaders, String serverHeader)
+ : super(uri, protocolVersion, outgoing, initialHeaders: defaultHeaders) {
+ if (serverHeader != null) headers.set('server', serverHeader);
+ }
+
+ bool get _isConnectionClosed => _httpRequest._httpConnection._isClosing;
+
+ List<Cookie> get cookies {
+ if (_cookies == null) _cookies = new List<Cookie>();
+ return _cookies;
+ }
+
+ int get statusCode => _statusCode;
+ void set statusCode(int statusCode) {
+ if (_outgoing.headersWritten) throw new StateError("Header already sent");
+ _statusCode = statusCode;
+ }
+
+ String get reasonPhrase => _findReasonPhrase(statusCode);
+ void set reasonPhrase(String reasonPhrase) {
+ if (_outgoing.headersWritten) throw new StateError("Header already sent");
+ _reasonPhrase = reasonPhrase;
+ }
+
+ Future redirect(Uri location, {int status: HttpStatus.MOVED_TEMPORARILY}) {
+ if (_outgoing.headersWritten) throw new StateError("Header already sent");
+ statusCode = status;
+ headers.set("location", location.toString());
+ return close();
+ }
+
+ Future<Socket> detachSocket({bool writeHeaders: true}) {
+ if (_outgoing.headersWritten) throw new StateError("Headers already sent");
+ deadline = null; // Be sure to stop any deadline.
+ var future = _httpRequest._httpConnection.detachSocket();
+ if (writeHeaders) {
+ var headersFuture =
+ _outgoing.writeHeaders(drainRequest: false, setOutgoing: false);
+ assert(headersFuture == null);
+ } else {
+ // Imitate having written the headers.
+ _outgoing.headersWritten = true;
+ }
+ // Close connection so the socket is 'free'.
+ close();
+ done.catchError((_) {
+ // Catch any error on done, as they automatically will be
+ // propagated to the websocket.
+ });
+ return future;
+ }
+
+ HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo;
+
+ Duration get deadline => _deadline;
+
+ void set deadline(Duration d) {
+ if (_deadlineTimer != null) _deadlineTimer.cancel();
+ _deadline = d;
+
+ if (_deadline == null) return;
+ _deadlineTimer = new Timer(_deadline, () {
+ _httpRequest._httpConnection.destroy();
+ });
+ }
+
+ void _writeHeader() {
+ BytesBuilder buffer = new _CopyingBytesBuilder(_OUTGOING_BUFFER_SIZE);
+
+ // Write status line.
+ if (headers.protocolVersion == "1.1") {
+ buffer.add(_Const.HTTP11);
+ } else {
+ buffer.add(_Const.HTTP10);
+ }
+ buffer.addByte(_CharCode.SP);
+ buffer.add(statusCode.toString().codeUnits);
+ buffer.addByte(_CharCode.SP);
+ buffer.add(reasonPhrase.codeUnits);
+ buffer.addByte(_CharCode.CR);
+ buffer.addByte(_CharCode.LF);
+
+ var session = _httpRequest._session;
+ if (session != null && !session._destroyed) {
+ // Mark as not new.
+ session._isNew = false;
+ // Make sure we only send the current session id.
+ bool found = false;
+ for (int i = 0; i < cookies.length; i++) {
+ if (cookies[i].name.toUpperCase() == _DART_SESSION_ID) {
+ cookies[i]
+ ..value = session.id
+ ..httpOnly = true
+ ..path = "/";
+ found = true;
+ }
+ }
+ if (!found) {
+ var cookie = new Cookie(_DART_SESSION_ID, session.id);
+ cookies.add(cookie
+ ..httpOnly = true
+ ..path = "/");
+ }
+ }
+ // Add all the cookies set to the headers.
+ if (_cookies != null) {
+ _cookies.forEach((cookie) {
+ headers.add(HttpHeaders.SET_COOKIE, cookie);
+ });
+ }
+
+ headers._finalize();
+
+ // Write headers.
+ headers._build(buffer);
+ buffer.addByte(_CharCode.CR);
+ buffer.addByte(_CharCode.LF);
+ Uint8List headerBytes = buffer.takeBytes();
+ _outgoing.setHeader(headerBytes, headerBytes.length);
+ }
+
+ String _findReasonPhrase(int statusCode) {
+ if (_reasonPhrase != null) {
+ return _reasonPhrase;
+ }
+
+ switch (statusCode) {
+ case HttpStatus.CONTINUE:
+ return "Continue";
+ case HttpStatus.SWITCHING_PROTOCOLS:
+ return "Switching Protocols";
+ case HttpStatus.OK:
+ return "OK";
+ case HttpStatus.CREATED:
+ return "Created";
+ case HttpStatus.ACCEPTED:
+ return "Accepted";
+ case HttpStatus.NON_AUTHORITATIVE_INFORMATION:
+ return "Non-Authoritative Information";
+ case HttpStatus.NO_CONTENT:
+ return "No Content";
+ case HttpStatus.RESET_CONTENT:
+ return "Reset Content";
+ case HttpStatus.PARTIAL_CONTENT:
+ return "Partial Content";
+ case HttpStatus.MULTIPLE_CHOICES:
+ return "Multiple Choices";
+ case HttpStatus.MOVED_PERMANENTLY:
+ return "Moved Permanently";
+ case HttpStatus.FOUND:
+ return "Found";
+ case HttpStatus.SEE_OTHER:
+ return "See Other";
+ case HttpStatus.NOT_MODIFIED:
+ return "Not Modified";
+ case HttpStatus.USE_PROXY:
+ return "Use Proxy";
+ case HttpStatus.TEMPORARY_REDIRECT:
+ return "Temporary Redirect";
+ case HttpStatus.BAD_REQUEST:
+ return "Bad Request";
+ case HttpStatus.UNAUTHORIZED:
+ return "Unauthorized";
+ case HttpStatus.PAYMENT_REQUIRED:
+ return "Payment Required";
+ case HttpStatus.FORBIDDEN:
+ return "Forbidden";
+ case HttpStatus.NOT_FOUND:
+ return "Not Found";
+ case HttpStatus.METHOD_NOT_ALLOWED:
+ return "Method Not Allowed";
+ case HttpStatus.NOT_ACCEPTABLE:
+ return "Not Acceptable";
+ case HttpStatus.PROXY_AUTHENTICATION_REQUIRED:
+ return "Proxy Authentication Required";
+ case HttpStatus.REQUEST_TIMEOUT:
+ return "Request Time-out";
+ case HttpStatus.CONFLICT:
+ return "Conflict";
+ case HttpStatus.GONE:
+ return "Gone";
+ case HttpStatus.LENGTH_REQUIRED:
+ return "Length Required";
+ case HttpStatus.PRECONDITION_FAILED:
+ return "Precondition Failed";
+ case HttpStatus.REQUEST_ENTITY_TOO_LARGE:
+ return "Request Entity Too Large";
+ case HttpStatus.REQUEST_URI_TOO_LONG:
+ return "Request-URI Too Large";
+ case HttpStatus.UNSUPPORTED_MEDIA_TYPE:
+ return "Unsupported Media Type";
+ case HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE:
+ return "Requested range not satisfiable";
+ case HttpStatus.EXPECTATION_FAILED:
+ return "Expectation Failed";
+ case HttpStatus.INTERNAL_SERVER_ERROR:
+ return "Internal Server Error";
+ case HttpStatus.NOT_IMPLEMENTED:
+ return "Not Implemented";
+ case HttpStatus.BAD_GATEWAY:
+ return "Bad Gateway";
+ case HttpStatus.SERVICE_UNAVAILABLE:
+ return "Service Unavailable";
+ case HttpStatus.GATEWAY_TIMEOUT:
+ return "Gateway Time-out";
+ case HttpStatus.HTTP_VERSION_NOT_SUPPORTED:
+ return "Http Version not supported";
+ default:
+ return "Status $statusCode";
+ }
+ }
+}
+
+class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse>
+ implements HttpClientRequest {
+ final String method;
+ final Uri uri;
+ final List<Cookie> cookies = new List<Cookie>();
+
+ // The HttpClient this request belongs to.
+ final _HttpClient _httpClient;
+ final _HttpClientConnection _httpClientConnection;
+
+ final Completer<HttpClientResponse> _responseCompleter =
+ new Completer<HttpClientResponse>();
+
+ final _Proxy _proxy;
+
+ Future<HttpClientResponse> _response;
+
+ // TODO(ajohnsen): Get default value from client?
+ bool _followRedirects = true;
+
+ int _maxRedirects = 5;
+
+ List<RedirectInfo> _responseRedirects = [];
+
+ _HttpClientRequest(_HttpOutgoing outgoing, Uri uri, this.method, this._proxy,
+ this._httpClient, this._httpClientConnection)
+ : uri = uri,
+ super(uri, "1.1", outgoing) {
+ // GET and HEAD have 'content-length: 0' by default.
+ if (method == "GET" || method == "HEAD") {
+ contentLength = 0;
+ } else {
+ headers.chunkedTransferEncoding = true;
+ }
+ }
+
+ Future<HttpClientResponse> get done {
+ if (_response == null) {
+ _response = Future.wait([_responseCompleter.future, super.done],
+ eagerError: true).then((list) => list[0]);
+ }
+ return _response;
+ }
+
+ Future<HttpClientResponse> close() {
+ super.close();
+ return done;
+ }
+
+ int get maxRedirects => _maxRedirects;
+ void set maxRedirects(int maxRedirects) {
+ if (_outgoing.headersWritten) throw new StateError("Request already sent");
+ _maxRedirects = maxRedirects;
+ }
+
+ bool get followRedirects => _followRedirects;
+ void set followRedirects(bool followRedirects) {
+ if (_outgoing.headersWritten) throw new StateError("Request already sent");
+ _followRedirects = followRedirects;
+ }
+
+ HttpConnectionInfo get connectionInfo => _httpClientConnection.connectionInfo;
+
+ void _onIncoming(_HttpIncoming incoming) {
+ var response = new _HttpClientResponse(incoming, this, _httpClient);
+ Future<HttpClientResponse> future;
+ if (followRedirects && response.isRedirect) {
+ if (response.redirects.length < maxRedirects) {
+ // Redirect and drain response.
+ future = response
+ .drain()
+ .then<HttpClientResponse>((_) => response.redirect());
+ } else {
+ // End with exception, too many redirects.
+ future = response.drain().then<HttpClientResponse>((_) {
+ return new Future<HttpClientResponse>.error(new RedirectException(
+ "Redirect limit exceeded", response.redirects));
+ });
+ }
+ } else if (response._shouldAuthenticateProxy) {
+ future = response._authenticate(true);
+ } else if (response._shouldAuthenticate) {
+ future = response._authenticate(false);
+ } else {
+ future = new Future<HttpClientResponse>.value(response);
+ }
+ future.then((v) => _responseCompleter.complete(v),
+ onError: _responseCompleter.completeError);
+ }
+
+ void _onError(error, StackTrace stackTrace) {
+ _responseCompleter.completeError(error, stackTrace);
+ }
+
+ // Generate the request URI based on the method and proxy.
+ String _requestUri() {
+ // Generate the request URI starting from the path component.
+ String uriStartingFromPath() {
+ String result = uri.path;
+ if (result.isEmpty) result = "/";
+ if (uri.hasQuery) {
+ result = "${result}?${uri.query}";
+ }
+ return result;
+ }
+
+ if (_proxy.isDirect) {
+ return uriStartingFromPath();
+ } else {
+ if (method == "CONNECT") {
+ // For the connect method the request URI is the host:port of
+ // the requested destination of the tunnel (see RFC 2817
+ // section 5.2)
+ return "${uri.host}:${uri.port}";
+ } else {
+ if (_httpClientConnection._proxyTunnel) {
+ return uriStartingFromPath();
+ } else {
+ return uri.removeFragment().toString();
+ }
+ }
+ }
+ }
+
+ void _writeHeader() {
+ BytesBuilder buffer = new _CopyingBytesBuilder(_OUTGOING_BUFFER_SIZE);
+
+ // Write the request method.
+ buffer.add(method.codeUnits);
+ buffer.addByte(_CharCode.SP);
+ // Write the request URI.
+ buffer.add(_requestUri().codeUnits);
+ buffer.addByte(_CharCode.SP);
+ // Write HTTP/1.1.
+ buffer.add(_Const.HTTP11);
+ buffer.addByte(_CharCode.CR);
+ buffer.addByte(_CharCode.LF);
+
+ // Add the cookies to the headers.
+ if (!cookies.isEmpty) {
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < cookies.length; i++) {
+ if (i > 0) sb.write("; ");
+ sb..write(cookies[i].name)..write("=")..write(cookies[i].value);
+ }
+ headers.add(HttpHeaders.COOKIE, sb.toString());
+ }
+
+ headers._finalize();
+
+ // Write headers.
+ headers._build(buffer);
+ buffer.addByte(_CharCode.CR);
+ buffer.addByte(_CharCode.LF);
+ Uint8List headerBytes = buffer.takeBytes();
+ _outgoing.setHeader(headerBytes, headerBytes.length);
+ }
+}
+
+// Used by _HttpOutgoing as a target of a chunked converter for gzip
+// compression.
+class _HttpGZipSink extends ByteConversionSink {
+ final _BytesConsumer _consume;
+ _HttpGZipSink(this._consume);
+
+ void add(List<int> chunk) {
+ _consume(chunk);
+ }
+
+ void addSlice(List<int> chunk, int start, int end, bool isLast) {
+ if (chunk is Uint8List) {
+ _consume(new Uint8List.view(chunk.buffer, start, end - start));
+ } else {
+ _consume(chunk.sublist(start, end - start));
+ }
+ }
+
+ void close() {}
+}
+
+// The _HttpOutgoing handles all of the following:
+// - Buffering
+// - GZip compression
+// - Content-Length validation.
+// - Errors.
+//
+// Most notable is the GZip compression, that uses a double-buffering system,
+// one before gzip (_gzipBuffer) and one after (_buffer).
+class _HttpOutgoing implements StreamConsumer<List<int>> {
+ static const List<int> _footerAndChunk0Length = const [
+ _CharCode.CR,
+ _CharCode.LF,
+ 0x30,
+ _CharCode.CR,
+ _CharCode.LF,
+ _CharCode.CR,
+ _CharCode.LF
+ ];
+
+ static const List<int> _chunk0Length = const [
+ 0x30,
+ _CharCode.CR,
+ _CharCode.LF,
+ _CharCode.CR,
+ _CharCode.LF
+ ];
+
+ final Completer<Socket> _doneCompleter = new Completer<Socket>();
+ final Socket socket;
+
+ bool ignoreBody = false;
+ bool headersWritten = false;
+
+ Uint8List _buffer;
+ int _length = 0;
+
+ Future _closeFuture;
+
+ bool chunked = false;
+ int _pendingChunkedFooter = 0;
+
+ int contentLength;
+ int _bytesWritten = 0;
+
+ bool _gzip = false;
+ ByteConversionSink _gzipSink;
+ // _gzipAdd is set iff the sink is being added to. It's used to specify where
+ // gzipped data should be taken (sometimes a controller, sometimes a socket).
+ _BytesConsumer _gzipAdd;
+ Uint8List _gzipBuffer;
+ int _gzipBufferLength = 0;
+
+ bool _socketError = false;
+
+ _HttpOutboundMessage outbound;
+
+ _HttpOutgoing(this.socket);
+
+ // Returns either a future or 'null', if it was able to write headers
+ // immediately.
+ Future writeHeaders({bool drainRequest: true, bool setOutgoing: true}) {
+ if (headersWritten) return null;
+ headersWritten = true;
+ Future drainFuture;
+ bool gzip = false;
+ if (outbound is _HttpResponse) {
+ // Server side.
+ _HttpResponse response = outbound;
+ if (response._httpRequest._httpServer.autoCompress &&
+ outbound.bufferOutput &&
+ outbound.headers.chunkedTransferEncoding) {
+ List acceptEncodings =
+ response._httpRequest.headers[HttpHeaders.ACCEPT_ENCODING];
+ List contentEncoding = outbound.headers[HttpHeaders.CONTENT_ENCODING];
+ if (acceptEncodings != null &&
+ acceptEncodings
+ .expand((list) => list.split(","))
+ .any((encoding) => encoding.trim().toLowerCase() == "gzip") &&
+ contentEncoding == null) {
+ outbound.headers.set(HttpHeaders.CONTENT_ENCODING, "gzip");
+ gzip = true;
+ }
+ }
+ if (drainRequest && !response._httpRequest._incoming.hasSubscriber) {
+ drainFuture = response._httpRequest.drain().catchError((_) {});
+ }
+ } else {
+ drainRequest = false;
+ }
+ if (!ignoreBody) {
+ if (setOutgoing) {
+ int contentLength = outbound.headers.contentLength;
+ if (outbound.headers.chunkedTransferEncoding) {
+ chunked = true;
+ if (gzip) this.gzip = true;
+ } else if (contentLength >= 0) {
+ this.contentLength = contentLength;
+ }
+ }
+ if (drainFuture != null) {
+ return drainFuture.then((_) => outbound._writeHeader());
+ }
+ }
+ outbound._writeHeader();
+ return null;
+ }
+
+ Future addStream(Stream<List<int>> stream) {
+ if (_socketError) {
+ stream.listen(null).cancel();
+ return new Future.value(outbound);
+ }
+ if (ignoreBody) {
+ stream.drain().catchError((_) {});
+ var future = writeHeaders();
+ if (future != null) {
+ return future.then((_) => close());
+ }
+ return close();
+ }
+ StreamSubscription<List<int>> sub;
+ // Use new stream so we are able to pause (see below listen). The
+ // alternative is to use stream.extand, but that won't give us a way of
+ // pausing.
+ var controller = new StreamController<List<int>>(
+ onPause: () => sub.pause(), onResume: () => sub.resume(), sync: true);
+
+ void onData(List<int> data) {
+ if (_socketError) return;
+ if (data.length == 0) return;
+ if (chunked) {
+ if (_gzip) {
+ _gzipAdd = controller.add;
+ _addGZipChunk(data, _gzipSink.add);
+ _gzipAdd = null;
+ return;
+ }
+ _addChunk(_chunkHeader(data.length), controller.add);
+ _pendingChunkedFooter = 2;
+ } else {
+ if (contentLength != null) {
+ _bytesWritten += data.length;
+ if (_bytesWritten > contentLength) {
+ controller.addError(new HttpException(
+ "Content size exceeds specified contentLength. "
+ "$_bytesWritten bytes written while expected "
+ "$contentLength. "
+ "[${new String.fromCharCodes(data)}]"));
+ return;
+ }
+ }
+ }
+ _addChunk(data, controller.add);
+ }
+
+ sub = stream.listen(onData,
+ onError: controller.addError,
+ onDone: controller.close,
+ cancelOnError: true);
+ // Write headers now that we are listening to the stream.
+ if (!headersWritten) {
+ var future = writeHeaders();
+ if (future != null) {
+ // While incoming is being drained, the pauseFuture is non-null. Pause
+ // output until it's drained.
+ sub.pause(future);
+ }
+ }
+ return socket.addStream(controller.stream).then((_) {
+ return outbound;
+ }, onError: (error, stackTrace) {
+ // Be sure to close it in case of an error.
+ if (_gzip) _gzipSink.close();
+ _socketError = true;
+ _doneCompleter.completeError(error, stackTrace);
+ if (_ignoreError(error)) {
+ return outbound;
+ } else {
+ throw error;
+ }
+ });
+ }
+
+ Future close() {
+ // If we are already closed, return that future.
+ if (_closeFuture != null) return _closeFuture;
+ // If we earlier saw an error, return immediate. The notification to
+ // _Http*Connection is already done.
+ if (_socketError) return new Future.value(outbound);
+ if (outbound._isConnectionClosed) return new Future.value(outbound);
+ if (!headersWritten && !ignoreBody) {
+ if (outbound.headers.contentLength == -1) {
+ // If no body was written, ignoreBody is false (it's not a HEAD
+ // request) and the content-length is unspecified, set contentLength to
+ // 0.
+ outbound.headers.chunkedTransferEncoding = false;
+ outbound.headers.contentLength = 0;
+ } else if (outbound.headers.contentLength > 0) {
+ var error = new HttpException(
+ "No content even though contentLength was specified to be "
+ "greater than 0: ${outbound.headers.contentLength}.",
+ uri: outbound._uri);
+ _doneCompleter.completeError(error);
+ return _closeFuture = new Future.error(error);
+ }
+ }
+ // If contentLength was specified, validate it.
+ if (contentLength != null) {
+ if (_bytesWritten < contentLength) {
+ var error = new HttpException(
+ "Content size below specified contentLength. "
+ " $_bytesWritten bytes written but expected "
+ "$contentLength.",
+ uri: outbound._uri);
+ _doneCompleter.completeError(error);
+ return _closeFuture = new Future.error(error);
+ }
+ }
+
+ Future finalize() {
+ // In case of chunked encoding (and gzip), handle remaining gzip data and
+ // append the 'footer' for chunked encoding.
+ if (chunked) {
+ if (_gzip) {
+ _gzipAdd = socket.add;
+ if (_gzipBufferLength > 0) {
+ _gzipSink.add(
+ new Uint8List.view(_gzipBuffer.buffer, 0, _gzipBufferLength));
+ }
+ _gzipBuffer = null;
+ _gzipSink.close();
+ _gzipAdd = null;
+ }
+ _addChunk(_chunkHeader(0), socket.add);
+ }
+ // Add any remaining data in the buffer.
+ if (_length > 0) {
+ socket.add(new Uint8List.view(_buffer.buffer, 0, _length));
+ }
+ // Clear references, for better GC.
+ _buffer = null;
+ // And finally flush it. As we support keep-alive, never close it from
+ // here. Once the socket is flushed, we'll be able to reuse it (signaled
+ // by the 'done' future).
+ return socket.flush().then((_) {
+ _doneCompleter.complete(socket);
+ return outbound;
+ }, onError: (error, stackTrace) {
+ _doneCompleter.completeError(error, stackTrace);
+ if (_ignoreError(error)) {
+ return outbound;
+ } else {
+ throw error;
+ }
+ });
+ }
+
+ var future = writeHeaders();
+ if (future != null) {
+ return _closeFuture = future.whenComplete(finalize);
+ }
+ return _closeFuture = finalize();
+ }
+
+ Future<Socket> get done => _doneCompleter.future;
+
+ void setHeader(List<int> data, int length) {
+ assert(_length == 0);
+ _buffer = data;
+ _length = length;
+ }
+
+ void set gzip(bool value) {
+ _gzip = value;
+ if (_gzip) {
+ _gzipBuffer = new Uint8List(_OUTGOING_BUFFER_SIZE);
+ assert(_gzipSink == null);
+ _gzipSink = new ZLibEncoder(gzip: true)
+ .startChunkedConversion(new _HttpGZipSink((data) {
+ // We are closing down prematurely, due to an error. Discard.
+ if (_gzipAdd == null) return;
+ _addChunk(_chunkHeader(data.length), _gzipAdd);
+ _pendingChunkedFooter = 2;
+ _addChunk(data, _gzipAdd);
+ }));
+ }
+ }
+
+ bool _ignoreError(error) =>
+ (error is SocketException || error is TlsException) &&
+ outbound is HttpResponse;
+
+ void _addGZipChunk(List<int> chunk, void add(List<int> data)) {
+ if (!outbound.bufferOutput) {
+ add(chunk);
+ return;
+ }
+ if (chunk.length > _gzipBuffer.length - _gzipBufferLength) {
+ add(new Uint8List.view(_gzipBuffer.buffer, 0, _gzipBufferLength));
+ _gzipBuffer = new Uint8List(_OUTGOING_BUFFER_SIZE);
+ _gzipBufferLength = 0;
+ }
+ if (chunk.length > _OUTGOING_BUFFER_SIZE) {
+ add(chunk);
+ } else {
+ _gzipBuffer.setRange(
+ _gzipBufferLength, _gzipBufferLength + chunk.length, chunk);
+ _gzipBufferLength += chunk.length;
+ }
+ }
+
+ void _addChunk(List<int> chunk, void add(List<int> data)) {
+ if (!outbound.bufferOutput) {
+ if (_buffer != null) {
+ // If _buffer is not null, we have not written the header yet. Write
+ // it now.
+ add(new Uint8List.view(_buffer.buffer, 0, _length));
+ _buffer = null;
+ _length = 0;
+ }
+ add(chunk);
+ return;
+ }
+ if (chunk.length > _buffer.length - _length) {
+ add(new Uint8List.view(_buffer.buffer, 0, _length));
+ _buffer = new Uint8List(_OUTGOING_BUFFER_SIZE);
+ _length = 0;
+ }
+ if (chunk.length > _OUTGOING_BUFFER_SIZE) {
+ add(chunk);
+ } else {
+ _buffer.setRange(_length, _length + chunk.length, chunk);
+ _length += chunk.length;
+ }
+ }
+
+ List<int> _chunkHeader(int length) {
+ const hexDigits = const [
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x41,
+ 0x42,
+ 0x43,
+ 0x44,
+ 0x45,
+ 0x46
+ ];
+ if (length == 0) {
+ if (_pendingChunkedFooter == 2) return _footerAndChunk0Length;
+ return _chunk0Length;
+ }
+ int size = _pendingChunkedFooter;
+ int len = length;
+ // Compute a fast integer version of (log(length + 1) / log(16)).ceil().
+ while (len > 0) {
+ size++;
+ len >>= 4;
+ }
+ var footerAndHeader = new Uint8List(size + 2);
+ if (_pendingChunkedFooter == 2) {
+ footerAndHeader[0] = _CharCode.CR;
+ footerAndHeader[1] = _CharCode.LF;
+ }
+ int index = size;
+ while (index > _pendingChunkedFooter) {
+ footerAndHeader[--index] = hexDigits[length & 15];
+ length = length >> 4;
+ }
+ footerAndHeader[size + 0] = _CharCode.CR;
+ footerAndHeader[size + 1] = _CharCode.LF;
+ return footerAndHeader;
+ }
+}
+
+class _HttpClientConnection {
+ final String key;
+ final Socket _socket;
+ final bool _proxyTunnel;
+ final SecurityContext _context;
+ final _HttpParser _httpParser;
+ StreamSubscription _subscription;
+ final _HttpClient _httpClient;
+ bool _dispose = false;
+ Timer _idleTimer;
+ bool closed = false;
+ Uri _currentUri;
+
+ Completer<_HttpIncoming> _nextResponseCompleter;
+ Future<Socket> _streamFuture;
+
+ _HttpClientConnection(this.key, this._socket, this._httpClient,
+ [this._proxyTunnel = false, this._context])
+ : _httpParser = new _HttpParser.responseParser() {
+ _httpParser.listenToStream(_socket);
+
+ // Set up handlers on the parser here, so we are sure to get 'onDone' from
+ // the parser.
+ _subscription = _httpParser.listen((incoming) {
+ // Only handle one incoming response at the time. Keep the
+ // stream paused until the response have been processed.
+ _subscription.pause();
+ // We assume the response is not here, until we have send the request.
+ if (_nextResponseCompleter == null) {
+ throw new HttpException(
+ "Unexpected response (unsolicited response without request).",
+ uri: _currentUri);
+ }
+
+ // Check for status code '100 Continue'. In that case just
+ // consume that response as the final response will follow
+ // it. There is currently no API for the client to wait for
+ // the '100 Continue' response.
+ if (incoming.statusCode == 100) {
+ incoming.drain().then((_) {
+ _subscription.resume();
+ }).catchError((error, [StackTrace stackTrace]) {
+ _nextResponseCompleter.completeError(
+ new HttpException(error.message, uri: _currentUri), stackTrace);
+ _nextResponseCompleter = null;
+ });
+ } else {
+ _nextResponseCompleter.complete(incoming);
+ _nextResponseCompleter = null;
+ }
+ }, onError: (error, [StackTrace stackTrace]) {
+ if (_nextResponseCompleter != null) {
+ _nextResponseCompleter.completeError(
+ new HttpException(error.message, uri: _currentUri), stackTrace);
+ _nextResponseCompleter = null;
+ }
+ }, onDone: () {
+ if (_nextResponseCompleter != null) {
+ _nextResponseCompleter.completeError(new HttpException(
+ "Connection closed before response was received",
+ uri: _currentUri));
+ _nextResponseCompleter = null;
+ }
+ close();
+ });
+ }
+
+ _HttpClientRequest send(Uri uri, int port, String method, _Proxy proxy) {
+ if (closed) {
+ throw new HttpException("Socket closed before request was sent",
+ uri: uri);
+ }
+ _currentUri = uri;
+ // Start with pausing the parser.
+ _subscription.pause();
+ _ProxyCredentials proxyCreds; // Credentials used to authorize proxy.
+ _SiteCredentials creds; // Credentials used to authorize this request.
+ var outgoing = new _HttpOutgoing(_socket);
+ // Create new request object, wrapping the outgoing connection.
+ var request =
+ new _HttpClientRequest(outgoing, uri, method, proxy, _httpClient, this);
+ // For the Host header an IPv6 address must be enclosed in []'s.
+ var host = uri.host;
+ if (host.contains(':')) host = "[$host]";
+ request.headers
+ ..host = host
+ ..port = port
+ .._add(HttpHeaders.ACCEPT_ENCODING, "gzip");
+ if (_httpClient.userAgent != null) {
+ request.headers._add('user-agent', _httpClient.userAgent);
+ }
+ if (proxy.isAuthenticated) {
+ // If the proxy configuration contains user information use that
+ // for proxy basic authorization.
+ String auth = _CryptoUtils
+ .bytesToBase64(utf8.encode("${proxy.username}:${proxy.password}"));
+ request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, "Basic $auth");
+ } else if (!proxy.isDirect && _httpClient._proxyCredentials.length > 0) {
+ proxyCreds = _httpClient._findProxyCredentials(proxy);
+ if (proxyCreds != null) {
+ proxyCreds.authorize(request);
+ }
+ }
+ if (uri.userInfo != null && !uri.userInfo.isEmpty) {
+ // If the URL contains user information use that for basic
+ // authorization.
+ String auth = _CryptoUtils.bytesToBase64(utf8.encode(uri.userInfo));
+ request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth");
+ } else {
+ // Look for credentials.
+ creds = _httpClient._findCredentials(uri);
+ if (creds != null) {
+ creds.authorize(request);
+ }
+ }
+ // Start sending the request (lazy, delayed until the user provides
+ // data).
+ _httpParser.isHead = method == "HEAD";
+ _streamFuture = outgoing.done.then<Socket>((Socket s) {
+ // Request sent, set up response completer.
+ _nextResponseCompleter = new Completer<_HttpIncoming>();
+
+ // Listen for response.
+ _nextResponseCompleter.future.then((incoming) {
+ _currentUri = null;
+ incoming.dataDone.then((closing) {
+ if (incoming.upgraded) {
+ _httpClient._connectionClosed(this);
+ startTimer();
+ return;
+ }
+ if (closed) return;
+ if (!closing &&
+ !_dispose &&
+ incoming.headers.persistentConnection &&
+ request.persistentConnection) {
+ // Return connection, now we are done.
+ _httpClient._returnConnection(this);
+ _subscription.resume();
+ } else {
+ destroy();
+ }
+ });
+ // For digest authentication if proxy check if the proxy
+ // requests the client to start using a new nonce for proxy
+ // authentication.
+ if (proxyCreds != null &&
+ proxyCreds.scheme == _AuthenticationScheme.DIGEST) {
+ var authInfo = incoming.headers["proxy-authentication-info"];
+ if (authInfo != null && authInfo.length == 1) {
+ var header =
+ _HeaderValue.parse(authInfo[0], parameterSeparator: ',');
+ var nextnonce = header.parameters["nextnonce"];
+ if (nextnonce != null) proxyCreds.nonce = nextnonce;
+ }
+ }
+ // For digest authentication check if the server requests the
+ // client to start using a new nonce.
+ if (creds != null && creds.scheme == _AuthenticationScheme.DIGEST) {
+ var authInfo = incoming.headers["authentication-info"];
+ if (authInfo != null && authInfo.length == 1) {
+ var header =
+ _HeaderValue.parse(authInfo[0], parameterSeparator: ',');
+ var nextnonce = header.parameters["nextnonce"];
+ if (nextnonce != null) creds.nonce = nextnonce;
+ }
+ }
+ request._onIncoming(incoming);
+ })
+ // If we see a state error, we failed to get the 'first'
+ // element.
+ .catchError((error) {
+ throw new HttpException("Connection closed before data was received",
+ uri: uri);
+ }, test: (error) => error is StateError).catchError((error, stackTrace) {
+ // We are done with the socket.
+ destroy();
+ request._onError(error, stackTrace);
+ });
+
+ // Resume the parser now we have a handler.
+ _subscription.resume();
+ return s;
+ }, onError: (e) {
+ destroy();
+ });
+ return request;
+ }
+
+ Future<Socket> detachSocket() {
+ return _streamFuture.then(
+ (_) => new _DetachedSocket(_socket, _httpParser.detachIncoming()));
+ }
+
+ void destroy() {
+ closed = true;
+ _httpClient._connectionClosed(this);
+ _socket.destroy();
+ }
+
+ void close() {
+ closed = true;
+ _httpClient._connectionClosed(this);
+ _streamFuture
+ // TODO(ajohnsen): Add timeout.
+ .then((_) => _socket.destroy());
+ }
+
+ Future<_HttpClientConnection> createProxyTunnel(String host, int port,
+ _Proxy proxy, bool callback(X509Certificate certificate)) {
+ _HttpClientRequest request =
+ send(new Uri(host: host, port: port), port, "CONNECT", proxy);
+ if (proxy.isAuthenticated) {
+ // If the proxy configuration contains user information use that
+ // for proxy basic authorization.
+ String auth = _CryptoUtils
+ .bytesToBase64(utf8.encode("${proxy.username}:${proxy.password}"));
+ request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, "Basic $auth");
+ }
+ return request.close().then((response) {
+ if (response.statusCode != HttpStatus.OK) {
+ throw "Proxy failed to establish tunnel "
+ "(${response.statusCode} ${response.reasonPhrase})";
+ }
+ var socket = (response as _HttpClientResponse)
+ ._httpRequest
+ ._httpClientConnection
+ ._socket;
+ return SecureSocket.secure(socket,
+ host: host, context: _context, onBadCertificate: callback);
+ }).then((secureSocket) {
+ String key = _HttpClientConnection.makeKey(true, host, port);
+ return new _HttpClientConnection(
+ key, secureSocket, request._httpClient, true);
+ });
+ }
+
+ HttpConnectionInfo get connectionInfo => _HttpConnectionInfo.create(_socket);
+
+ static makeKey(bool isSecure, String host, int port) {
+ return isSecure ? "ssh:$host:$port" : "$host:$port";
+ }
+
+ void stopTimer() {
+ if (_idleTimer != null) {
+ _idleTimer.cancel();
+ _idleTimer = null;
+ }
+ }
+
+ void startTimer() {
+ assert(_idleTimer == null);
+ _idleTimer = new Timer(_httpClient.idleTimeout, () {
+ _idleTimer = null;
+ close();
+ });
+ }
+}
+
+class _ConnectionInfo {
+ final _HttpClientConnection connection;
+ final _Proxy proxy;
+
+ _ConnectionInfo(this.connection, this.proxy);
+}
+
+class _ConnectionTarget {
+ // Unique key for this connection target.
+ final String key;
+ final String host;
+ final int port;
+ final bool isSecure;
+ final SecurityContext context;
+ final Set<_HttpClientConnection> _idle = new HashSet();
+ final Set<_HttpClientConnection> _active = new HashSet();
+ final Queue _pending = new ListQueue();
+ int _connecting = 0;
+
+ _ConnectionTarget(
+ this.key, this.host, this.port, this.isSecure, this.context);
+
+ bool get isEmpty => _idle.isEmpty && _active.isEmpty && _connecting == 0;
+
+ bool get hasIdle => _idle.isNotEmpty;
+
+ bool get hasActive => _active.isNotEmpty || _connecting > 0;
+
+ _HttpClientConnection takeIdle() {
+ assert(hasIdle);
+ _HttpClientConnection connection = _idle.first;
+ _idle.remove(connection);
+ connection.stopTimer();
+ _active.add(connection);
+ return connection;
+ }
+
+ _checkPending() {
+ if (_pending.isNotEmpty) {
+ _pending.removeFirst()();
+ }
+ }
+
+ void addNewActive(_HttpClientConnection connection) {
+ _active.add(connection);
+ }
+
+ void returnConnection(_HttpClientConnection connection) {
+ assert(_active.contains(connection));
+ _active.remove(connection);
+ _idle.add(connection);
+ connection.startTimer();
+ _checkPending();
+ }
+
+ void connectionClosed(_HttpClientConnection connection) {
+ assert(!_active.contains(connection) || !_idle.contains(connection));
+ _active.remove(connection);
+ _idle.remove(connection);
+ _checkPending();
+ }
+
+ void close(bool force) {
+ for (var c in _idle.toList()) {
+ c.close();
+ }
+ if (force) {
+ for (var c in _active.toList()) {
+ c.destroy();
+ }
+ }
+ }
+
+ Future<_ConnectionInfo> connect(
+ String uriHost, int uriPort, _Proxy proxy, _HttpClient client) {
+ if (hasIdle) {
+ var connection = takeIdle();
+ client._connectionsChanged();
+ return new Future.value(new _ConnectionInfo(connection, proxy));
+ }
+ if (client.maxConnectionsPerHost != null &&
+ _active.length + _connecting >= client.maxConnectionsPerHost) {
+ var completer = new Completer<_ConnectionInfo>();
+ _pending.add(() {
+ completer.complete(connect(uriHost, uriPort, proxy, client));
+ });
+ return completer.future;
+ }
+ var currentBadCertificateCallback = client._badCertificateCallback;
+
+ bool callback(X509Certificate certificate) {
+ if (currentBadCertificateCallback == null) return false;
+ return currentBadCertificateCallback(certificate, uriHost, uriPort);
+ }
+
+ Future socketFuture = (isSecure && proxy.isDirect
+ ? SecureSocket.connect(host, port,
+ context: context, onBadCertificate: callback)
+ : Socket.connect(host, port));
+ _connecting++;
+ return socketFuture.then((socket) {
+ _connecting--;
+ socket.setOption(SocketOption.TCP_NODELAY, true);
+ var connection =
+ new _HttpClientConnection(key, socket, client, false, context);
+ if (isSecure && !proxy.isDirect) {
+ connection._dispose = true;
+ return connection
+ .createProxyTunnel(uriHost, uriPort, proxy, callback)
+ .then((tunnel) {
+ client
+ ._getConnectionTarget(uriHost, uriPort, true)
+ .addNewActive(tunnel);
+ return new _ConnectionInfo(tunnel, proxy);
+ });
+ } else {
+ addNewActive(connection);
+ return new _ConnectionInfo(connection, proxy);
+ }
+ }, onError: (error) {
+ _connecting--;
+ _checkPending();
+ throw error;
+ });
+ }
+}
+
+typedef bool BadCertificateCallback(X509Certificate cr, String host, int port);
+
+class _HttpClient implements HttpClient {
+ bool _closing = false;
+ bool _closingForcefully = false;
+ final Map<String, _ConnectionTarget> _connectionTargets =
+ new HashMap<String, _ConnectionTarget>();
+ final List<_Credentials> _credentials = [];
+ final List<_ProxyCredentials> _proxyCredentials = [];
+ final SecurityContext _context;
+ Function _authenticate;
+ Function _authenticateProxy;
+ Function _findProxy = HttpClient.findProxyFromEnvironment;
+ Duration _idleTimeout = const Duration(seconds: 15);
+ BadCertificateCallback _badCertificateCallback;
+
+ Duration get idleTimeout => _idleTimeout;
+
+ int maxConnectionsPerHost;
+
+ bool autoUncompress = true;
+
+ String userAgent = _getHttpVersion();
+
+ _HttpClient(this._context);
+
+ void set idleTimeout(Duration timeout) {
+ _idleTimeout = timeout;
+ for (var c in _connectionTargets.values) {
+ for (var idle in c._idle) {
+ // Reset timer. This is fine, as it's not happening often.
+ idle.stopTimer();
+ idle.startTimer();
+ }
+ }
+ }
+
+ set badCertificateCallback(
+ bool callback(X509Certificate cert, String host, int port)) {
+ _badCertificateCallback = callback;
+ }
+
+ Future<HttpClientRequest> open(
+ String method, String host, int port, String path) {
+ const int hashMark = 0x23;
+ const int questionMark = 0x3f;
+ int fragmentStart = path.length;
+ int queryStart = path.length;
+ for (int i = path.length - 1; i >= 0; i--) {
+ var char = path.codeUnitAt(i);
+ if (char == hashMark) {
+ fragmentStart = i;
+ queryStart = i;
+ } else if (char == questionMark) {
+ queryStart = i;
+ }
+ }
+ String query = null;
+ if (queryStart < fragmentStart) {
+ query = path.substring(queryStart + 1, fragmentStart);
+ path = path.substring(0, queryStart);
+ }
+ Uri uri = new Uri(
+ scheme: "http", host: host, port: port, path: path, query: query);
+ return _openUrl(method, uri);
+ }
+
+ Future<HttpClientRequest> openUrl(String method, Uri url) =>
+ _openUrl(method, url);
+
+ Future<HttpClientRequest> get(String host, int port, String path) =>
+ open("get", host, port, path);
+
+ Future<HttpClientRequest> getUrl(Uri url) => _openUrl("get", url);
+
+ Future<HttpClientRequest> post(String host, int port, String path) =>
+ open("post", host, port, path);
+
+ Future<HttpClientRequest> postUrl(Uri url) => _openUrl("post", url);
+
+ Future<HttpClientRequest> put(String host, int port, String path) =>
+ open("put", host, port, path);
+
+ Future<HttpClientRequest> putUrl(Uri url) => _openUrl("put", url);
+
+ Future<HttpClientRequest> delete(String host, int port, String path) =>
+ open("delete", host, port, path);
+
+ Future<HttpClientRequest> deleteUrl(Uri url) => _openUrl("delete", url);
+
+ Future<HttpClientRequest> head(String host, int port, String path) =>
+ open("head", host, port, path);
+
+ Future<HttpClientRequest> headUrl(Uri url) => _openUrl("head", url);
+
+ Future<HttpClientRequest> patch(String host, int port, String path) =>
+ open("patch", host, port, path);
+
+ Future<HttpClientRequest> patchUrl(Uri url) => _openUrl("patch", url);
+
+ void close({bool force: false}) {
+ _closing = true;
+ _closingForcefully = force;
+ _closeConnections(_closingForcefully);
+ assert(!_connectionTargets.values.any((s) => s.hasIdle));
+ assert(
+ !force || !_connectionTargets.values.any((s) => s._active.isNotEmpty));
+ }
+
+ set authenticate(Future<bool> f(Uri url, String scheme, String realm)) {
+ _authenticate = f;
+ }
+
+ void addCredentials(Uri url, String realm, HttpClientCredentials cr) {
+ _credentials.add(new _SiteCredentials(url, realm, cr));
+ }
+
+ set authenticateProxy(
+ Future<bool> f(String host, int port, String scheme, String realm)) {
+ _authenticateProxy = f;
+ }
+
+ void addProxyCredentials(
+ String host, int port, String realm, HttpClientCredentials cr) {
+ _proxyCredentials.add(new _ProxyCredentials(host, port, realm, cr));
+ }
+
+ set findProxy(String f(Uri uri)) => _findProxy = f;
+
+ Future<_HttpClientRequest> _openUrl(String method, Uri uri) {
+ // Ignore any fragments on the request URI.
+ uri = uri.removeFragment();
+
+ if (method == null) {
+ throw new ArgumentError(method);
+ }
+ if (method != "CONNECT") {
+ if (uri.host.isEmpty) {
+ throw new ArgumentError("No host specified in URI $uri");
+ } else if (uri.scheme != "http" && uri.scheme != "https") {
+ throw new ArgumentError(
+ "Unsupported scheme '${uri.scheme}' in URI $uri");
+ }
+ }
+
+ bool isSecure = (uri.scheme == "https");
+ int port = uri.port;
+ if (port == 0) {
+ port = isSecure
+ ? HttpClient.DEFAULT_HTTPS_PORT
+ : HttpClient.DEFAULT_HTTP_PORT;
+ }
+ // Check to see if a proxy server should be used for this connection.
+ var proxyConf = const _ProxyConfiguration.direct();
+ if (_findProxy != null) {
+ // TODO(sgjesse): Keep a map of these as normally only a few
+ // configuration strings will be used.
+ try {
+ proxyConf = new _ProxyConfiguration(_findProxy(uri));
+ } catch (error, stackTrace) {
+ return new Future.error(error, stackTrace);
+ }
+ }
+ return _getConnection(uri.host, port, proxyConf, isSecure)
+ .then((_ConnectionInfo info) {
+ _HttpClientRequest send(_ConnectionInfo info) {
+ return info.connection
+ .send(uri, port, method.toUpperCase(), info.proxy);
+ }
+
+ // If the connection was closed before the request was sent, create
+ // and use another connection.
+ if (info.connection.closed) {
+ return _getConnection(uri.host, port, proxyConf, isSecure).then(send);
+ }
+ return send(info);
+ });
+ }
+
+ Future<_HttpClientRequest> _openUrlFromRequest(
+ String method, Uri uri, _HttpClientRequest previous) {
+ // If the new URI is relative (to either '/' or some sub-path),
+ // construct a full URI from the previous one.
+ Uri resolved = previous.uri.resolveUri(uri);
+ return _openUrl(method, resolved).then((_HttpClientRequest request) {
+ request
+ // Only follow redirects if initial request did.
+ ..followRedirects = previous.followRedirects
+ // Allow same number of redirects.
+ ..maxRedirects = previous.maxRedirects;
+ // Copy headers.
+ for (var header in previous.headers._headers.keys) {
+ if (request.headers[header] == null) {
+ request.headers.set(header, previous.headers[header]);
+ }
+ }
+ return request
+ ..headers.chunkedTransferEncoding = false
+ ..contentLength = 0;
+ });
+ }
+
+ // Return a live connection to the idle pool.
+ void _returnConnection(_HttpClientConnection connection) {
+ _connectionTargets[connection.key].returnConnection(connection);
+ _connectionsChanged();
+ }
+
+ // Remove a closed connection from the active set.
+ void _connectionClosed(_HttpClientConnection connection) {
+ connection.stopTimer();
+ var connectionTarget = _connectionTargets[connection.key];
+ if (connectionTarget != null) {
+ connectionTarget.connectionClosed(connection);
+ if (connectionTarget.isEmpty) {
+ _connectionTargets.remove(connection.key);
+ }
+ _connectionsChanged();
+ }
+ }
+
+ void _connectionsChanged() {
+ if (_closing) {
+ _closeConnections(_closingForcefully);
+ }
+ }
+
+ void _closeConnections(bool force) {
+ for (var connectionTarget in _connectionTargets.values.toList()) {
+ connectionTarget.close(force);
+ }
+ }
+
+ _ConnectionTarget _getConnectionTarget(String host, int port, bool isSecure) {
+ String key = _HttpClientConnection.makeKey(isSecure, host, port);
+ return _connectionTargets.putIfAbsent(key, () {
+ return new _ConnectionTarget(key, host, port, isSecure, _context);
+ });
+ }
+
+ // Get a new _HttpClientConnection, from the matching _ConnectionTarget.
+ Future<_ConnectionInfo> _getConnection(String uriHost, int uriPort,
+ _ProxyConfiguration proxyConf, bool isSecure) {
+ Iterator<_Proxy> proxies = proxyConf.proxies.iterator;
+
+ Future<_ConnectionInfo> connect(error) {
+ if (!proxies.moveNext()) return new Future.error(error);
+ _Proxy proxy = proxies.current;
+ String host = proxy.isDirect ? uriHost : proxy.host;
+ int port = proxy.isDirect ? uriPort : proxy.port;
+ return _getConnectionTarget(host, port, isSecure)
+ .connect(uriHost, uriPort, proxy, this)
+ // On error, continue with next proxy.
+ .catchError(connect);
+ }
+
+ // Make sure we go through the event loop before taking a
+ // connection from the pool. For long-running synchronous code the
+ // server might have closed the connection, so this lowers the
+ // probability of getting a connection that was already closed.
+ return new Future<_ConnectionInfo>(
+ () => connect(new HttpException("No proxies given")));
+ }
+
+ _SiteCredentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) {
+ // Look for credentials.
+ _SiteCredentials cr =
+ _credentials.fold(null, (_SiteCredentials prev, value) {
+ var siteCredentials = value as _SiteCredentials;
+ if (siteCredentials.applies(url, scheme)) {
+ if (prev == null) return value;
+ return siteCredentials.uri.path.length > prev.uri.path.length
+ ? siteCredentials
+ : prev;
+ } else {
+ return prev;
+ }
+ });
+ return cr;
+ }
+
+ _ProxyCredentials _findProxyCredentials(_Proxy proxy,
+ [_AuthenticationScheme scheme]) {
+ // Look for credentials.
+ var it = _proxyCredentials.iterator;
+ while (it.moveNext()) {
+ if (it.current.applies(proxy, scheme)) {
+ return it.current;
+ }
+ }
+ return null;
+ }
+
+ void _removeCredentials(_Credentials cr) {
+ int index = _credentials.indexOf(cr);
+ if (index != -1) {
+ _credentials.removeAt(index);
+ }
+ }
+
+ void _removeProxyCredentials(_Credentials cr) {
+ int index = _proxyCredentials.indexOf(cr);
+ if (index != -1) {
+ _proxyCredentials.removeAt(index);
+ }
+ }
+
+ static String _findProxyFromEnvironment(
+ Uri url, Map<String, String> environment) {
+ checkNoProxy(String option) {
+ if (option == null) return null;
+ Iterator<String> names = option.split(",").map((s) => s.trim()).iterator;
+ while (names.moveNext()) {
+ var name = names.current;
+ if ((name.startsWith("[") &&
+ name.endsWith("]") &&
+ "[${url.host}]" == name) ||
+ (name.isNotEmpty && url.host.endsWith(name))) {
+ return "DIRECT";
+ }
+ }
+ return null;
+ }
+
+ checkProxy(String option) {
+ if (option == null) return null;
+ option = option.trim();
+ if (option.isEmpty) return null;
+ int pos = option.indexOf("://");
+ if (pos >= 0) {
+ option = option.substring(pos + 3);
+ }
+ pos = option.indexOf("/");
+ if (pos >= 0) {
+ option = option.substring(0, pos);
+ }
+ // Add default port if no port configured.
+ if (option.indexOf("[") == 0) {
+ var pos = option.lastIndexOf(":");
+ if (option.indexOf("]") > pos) option = "$option:1080";
+ } else {
+ if (option.indexOf(":") == -1) option = "$option:1080";
+ }
+ return "PROXY $option";
+ }
+
+ // Default to using the process current environment.
+ if (environment == null) environment = _platformEnvironmentCache;
+
+ String proxyCfg;
+
+ String noProxy = environment["no_proxy"];
+ if (noProxy == null) noProxy = environment["NO_PROXY"];
+ if ((proxyCfg = checkNoProxy(noProxy)) != null) {
+ return proxyCfg;
+ }
+
+ if (url.scheme == "http") {
+ String proxy = environment["http_proxy"];
+ if (proxy == null) proxy = environment["HTTP_PROXY"];
+ if ((proxyCfg = checkProxy(proxy)) != null) {
+ return proxyCfg;
+ }
+ } else if (url.scheme == "https") {
+ String proxy = environment["https_proxy"];
+ if (proxy == null) proxy = environment["HTTPS_PROXY"];
+ if ((proxyCfg = checkProxy(proxy)) != null) {
+ return proxyCfg;
+ }
+ }
+ return "DIRECT";
+ }
+
+ static Map<String, String> _platformEnvironmentCache = Platform.environment;
+}
+
+class _HttpConnection extends LinkedListEntry<_HttpConnection>
+ with _ServiceObject {
+ static const _ACTIVE = 0;
+ static const _IDLE = 1;
+ static const _CLOSING = 2;
+ static const _DETACHED = 3;
+
+ // Use HashMap, as we don't need to keep order.
+ static Map<int, _HttpConnection> _connections =
+ new HashMap<int, _HttpConnection>();
+
+ final /*_ServerSocket*/ _socket;
+ final _HttpServer _httpServer;
+ final _HttpParser _httpParser;
+ int _state = _IDLE;
+ StreamSubscription _subscription;
+ bool _idleMark = false;
+ Future _streamFuture;
+
+ _HttpConnection(this._socket, this._httpServer)
+ : _httpParser = new _HttpParser.requestParser() {
+ _connections[_serviceId] = this;
+ _httpParser.listenToStream(_socket);
+ _subscription = _httpParser.listen((incoming) {
+ _httpServer._markActive(this);
+ // If the incoming was closed, close the connection.
+ incoming.dataDone.then((closing) {
+ if (closing) destroy();
+ });
+ // Only handle one incoming request at the time. Keep the
+ // stream paused until the request has been send.
+ _subscription.pause();
+ _state = _ACTIVE;
+ var outgoing = new _HttpOutgoing(_socket);
+ var response = new _HttpResponse(
+ incoming.uri,
+ incoming.headers.protocolVersion,
+ outgoing,
+ _httpServer.defaultResponseHeaders,
+ _httpServer.serverHeader);
+ var request = new _HttpRequest(response, incoming, _httpServer, this);
+ _streamFuture = outgoing.done.then((_) {
+ response.deadline = null;
+ if (_state == _DETACHED) return;
+ if (response.persistentConnection &&
+ request.persistentConnection &&
+ incoming.fullBodyRead &&
+ !_httpParser.upgrade &&
+ !_httpServer.closed) {
+ _state = _IDLE;
+ _idleMark = false;
+ _httpServer._markIdle(this);
+ // Resume the subscription for incoming requests as the
+ // request is now processed.
+ _subscription.resume();
+ } else {
+ // Close socket, keep-alive not used or body sent before
+ // received data was handled.
+ destroy();
+ }
+ }, onError: (_) {
+ destroy();
+ });
+ outgoing.ignoreBody = request.method == "HEAD";
+ response._httpRequest = request;
+ _httpServer._handleRequest(request);
+ }, onDone: () {
+ destroy();
+ }, onError: (error) {
+ // Ignore failed requests that was closed before headers was received.
+ destroy();
+ });
+ }
+
+ void markIdle() {
+ _idleMark = true;
+ }
+
+ bool get isMarkedIdle => _idleMark;
+
+ void destroy() {
+ if (_state == _CLOSING || _state == _DETACHED) return;
+ _state = _CLOSING;
+ _socket.destroy();
+ _httpServer._connectionClosed(this);
+ _connections.remove(_serviceId);
+ }
+
+ Future<Socket> detachSocket() {
+ _state = _DETACHED;
+ // Remove connection from server.
+ _httpServer._connectionClosed(this);
+
+ _HttpDetachedIncoming detachedIncoming = _httpParser.detachIncoming();
+
+ return _streamFuture.then((_) {
+ _connections.remove(_serviceId);
+ return new _DetachedSocket(_socket, detachedIncoming);
+ });
+ }
+
+ HttpConnectionInfo get connectionInfo => _HttpConnectionInfo.create(_socket);
+
+ bool get _isActive => _state == _ACTIVE;
+ bool get _isIdle => _state == _IDLE;
+ bool get _isClosing => _state == _CLOSING;
+ bool get _isDetached => _state == _DETACHED;
+
+ String get _serviceTypePath => 'io/http/serverconnections';
+ String get _serviceTypeName => 'HttpServerConnection';
+
+ Map _toJSON(bool ref) {
+ var name = "${_socket.address.host}:${_socket.port} <-> "
+ "${_socket.remoteAddress.host}:${_socket.remotePort}";
+ var r = <String, dynamic>{
+ 'id': _servicePath,
+ 'type': _serviceType(ref),
+ 'name': name,
+ 'user_name': name,
+ };
+ if (ref) {
+ return r;
+ }
+ r['server'] = _httpServer._toJSON(true);
+ try {
+ r['socket'] = _socket._toJSON(true);
+ } catch (_) {
+ r['socket'] = {
+ 'id': _servicePath,
+ 'type': '@Socket',
+ 'name': 'UserSocket',
+ 'user_name': 'UserSocket',
+ };
+ }
+ switch (_state) {
+ case _ACTIVE:
+ r['state'] = "Active";
+ break;
+ case _IDLE:
+ r['state'] = "Idle";
+ break;
+ case _CLOSING:
+ r['state'] = "Closing";
+ break;
+ case _DETACHED:
+ r['state'] = "Detached";
+ break;
+ default:
+ r['state'] = 'Unknown';
+ break;
+ }
+ return r;
+ }
+}
+
+// HTTP server waiting for socket connections.
+class _HttpServer extends Stream<HttpRequest>
+ with _ServiceObject
+ implements HttpServer {
+ // Use default Map so we keep order.
+ static Map<int, _HttpServer> _servers = new Map<int, _HttpServer>();
+
+ String serverHeader;
+ final HttpHeaders defaultResponseHeaders = _initDefaultResponseHeaders();
+ bool autoCompress = false;
+
+ Duration _idleTimeout;
+ Timer _idleTimer;
+
+ static Future<HttpServer> bind(
+ address, int port, int backlog, bool v6Only, bool shared) {
+ return ServerSocket
+ .bind(address, port, backlog: backlog, v6Only: v6Only, shared: shared)
+ .then<HttpServer>((socket) {
+ return new _HttpServer._(socket, true);
+ });
+ }
+
+ static Future<HttpServer> bindSecure(
+ address,
+ int port,
+ SecurityContext context,
+ int backlog,
+ bool v6Only,
+ bool requestClientCertificate,
+ bool shared) {
+ return SecureServerSocket
+ .bind(address, port, context,
+ backlog: backlog,
+ v6Only: v6Only,
+ requestClientCertificate: requestClientCertificate,
+ shared: shared)
+ .then<HttpServer>((socket) {
+ return new _HttpServer._(socket, true);
+ });
+ }
+
+ _HttpServer._(this._serverSocket, this._closeServer) {
+ _controller =
+ new StreamController<HttpRequest>(sync: true, onCancel: close);
+ idleTimeout = const Duration(seconds: 120);
+ _servers[_serviceId] = this;
+ }
+
+ _HttpServer.listenOn(this._serverSocket) : _closeServer = false {
+ _controller =
+ new StreamController<HttpRequest>(sync: true, onCancel: close);
+ idleTimeout = const Duration(seconds: 120);
+ _servers[_serviceId] = this;
+ }
+
+ static HttpHeaders _initDefaultResponseHeaders() {
+ var defaultResponseHeaders = new _HttpHeaders('1.1');
+ defaultResponseHeaders.contentType = ContentType.TEXT;
+ defaultResponseHeaders.set('X-Frame-Options', 'SAMEORIGIN');
+ defaultResponseHeaders.set('X-Content-Type-Options', 'nosniff');
+ defaultResponseHeaders.set('X-XSS-Protection', '1; mode=block');
+ return defaultResponseHeaders;
+ }
+
+ Duration get idleTimeout => _idleTimeout;
+
+ void set idleTimeout(Duration duration) {
+ if (_idleTimer != null) {
+ _idleTimer.cancel();
+ _idleTimer = null;
+ }
+ _idleTimeout = duration;
+ if (_idleTimeout != null) {
+ _idleTimer = new Timer.periodic(_idleTimeout, (_) {
+ for (var idle in _idleConnections.toList()) {
+ if (idle.isMarkedIdle) {
+ idle.destroy();
+ } else {
+ idle.markIdle();
+ }
+ }
+ });
+ }
+ }
+
+ StreamSubscription<HttpRequest> listen(void onData(HttpRequest event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ _serverSocket.listen((Socket socket) {
+ socket.setOption(SocketOption.TCP_NODELAY, true);
+ // Accept the client connection.
+ _HttpConnection connection = new _HttpConnection(socket, this);
+ _idleConnections.add(connection);
+ }, onError: (error, stackTrace) {
+ // Ignore HandshakeExceptions as they are bound to a single request,
+ // and are not fatal for the server.
+ if (error is! HandshakeException) {
+ _controller.addError(error, stackTrace);
+ }
+ }, onDone: _controller.close);
+ return _controller.stream.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ Future close({bool force: false}) {
+ closed = true;
+ Future result;
+ if (_serverSocket != null && _closeServer) {
+ result = _serverSocket.close();
+ } else {
+ result = new Future.value();
+ }
+ idleTimeout = null;
+ if (force) {
+ for (var c in _activeConnections.toList()) {
+ c.destroy();
+ }
+ assert(_activeConnections.isEmpty);
+ }
+ for (var c in _idleConnections.toList()) {
+ c.destroy();
+ }
+ _maybePerformCleanup();
+ return result;
+ }
+
+ void _maybePerformCleanup() {
+ if (closed &&
+ _idleConnections.isEmpty &&
+ _activeConnections.isEmpty &&
+ _sessionManagerInstance != null) {
+ _sessionManagerInstance.close();
+ _sessionManagerInstance = null;
+ _servers.remove(_serviceId);
+ }
+ }
+
+ int get port {
+ if (closed) throw new HttpException("HttpServer is not bound to a socket");
+ return _serverSocket.port;
+ }
+
+ InternetAddress get address {
+ if (closed) throw new HttpException("HttpServer is not bound to a socket");
+ return _serverSocket.address;
+ }
+
+ set sessionTimeout(int timeout) {
+ _sessionManager.sessionTimeout = timeout;
+ }
+
+ void _handleRequest(_HttpRequest request) {
+ if (!closed) {
+ _controller.add(request);
+ } else {
+ request._httpConnection.destroy();
+ }
+ }
+
+ void _connectionClosed(_HttpConnection connection) {
+ // Remove itself from either idle or active connections.
+ connection.unlink();
+ _maybePerformCleanup();
+ }
+
+ void _markIdle(_HttpConnection connection) {
+ _activeConnections.remove(connection);
+ _idleConnections.add(connection);
+ }
+
+ void _markActive(_HttpConnection connection) {
+ _idleConnections.remove(connection);
+ _activeConnections.add(connection);
+ }
+
+ _HttpSessionManager get _sessionManager {
+ // Lazy init.
+ if (_sessionManagerInstance == null) {
+ _sessionManagerInstance = new _HttpSessionManager();
+ }
+ return _sessionManagerInstance;
+ }
+
+ HttpConnectionsInfo connectionsInfo() {
+ HttpConnectionsInfo result = new HttpConnectionsInfo();
+ result.total = _activeConnections.length + _idleConnections.length;
+ _activeConnections.forEach((_HttpConnection conn) {
+ if (conn._isActive) {
+ result.active++;
+ } else {
+ assert(conn._isClosing);
+ result.closing++;
+ }
+ });
+ _idleConnections.forEach((_HttpConnection conn) {
+ result.idle++;
+ assert(conn._isIdle);
+ });
+ return result;
+ }
+
+ String get _serviceTypePath => 'io/http/servers';
+ String get _serviceTypeName => 'HttpServer';
+
+ Map<String, dynamic> _toJSON(bool ref) {
+ var r = <String, dynamic>{
+ 'id': _servicePath,
+ 'type': _serviceType(ref),
+ 'name': '${address.host}:$port',
+ 'user_name': '${address.host}:$port',
+ };
+ if (ref) {
+ return r;
+ }
+ try {
+ r['socket'] = _serverSocket._toJSON(true);
+ } catch (_) {
+ r['socket'] = {
+ 'id': _servicePath,
+ 'type': '@Socket',
+ 'name': 'UserSocket',
+ 'user_name': 'UserSocket',
+ };
+ }
+ r['port'] = port;
+ r['address'] = address.host;
+ r['active'] = _activeConnections.map((c) => c._toJSON(true)).toList();
+ r['idle'] = _idleConnections.map((c) => c._toJSON(true)).toList();
+ r['closed'] = closed;
+ return r;
+ }
+
+ _HttpSessionManager _sessionManagerInstance;
+
+ // Indicated if the http server has been closed.
+ bool closed = false;
+
+ // The server listen socket. Untyped as it can be both ServerSocket and
+ // SecureServerSocket.
+ final dynamic /*ServerSocket|SecureServerSocket*/ _serverSocket;
+ final bool _closeServer;
+
+ // Set of currently connected clients.
+ final LinkedList<_HttpConnection> _activeConnections =
+ new LinkedList<_HttpConnection>();
+ final LinkedList<_HttpConnection> _idleConnections =
+ new LinkedList<_HttpConnection>();
+ StreamController<HttpRequest> _controller;
+}
+
+class _ProxyConfiguration {
+ static const String PROXY_PREFIX = "PROXY ";
+ static const String DIRECT_PREFIX = "DIRECT";
+
+ _ProxyConfiguration(String configuration) : proxies = new List<_Proxy>() {
+ if (configuration == null) {
+ throw new HttpException("Invalid proxy configuration $configuration");
+ }
+ List<String> list = configuration.split(";");
+ list.forEach((String proxy) {
+ proxy = proxy.trim();
+ if (!proxy.isEmpty) {
+ if (proxy.startsWith(PROXY_PREFIX)) {
+ String username;
+ String password;
+ // Skip the "PROXY " prefix.
+ proxy = proxy.substring(PROXY_PREFIX.length).trim();
+ // Look for proxy authentication.
+ int at = proxy.indexOf("@");
+ if (at != -1) {
+ String userinfo = proxy.substring(0, at).trim();
+ proxy = proxy.substring(at + 1).trim();
+ int colon = userinfo.indexOf(":");
+ if (colon == -1 || colon == 0 || colon == proxy.length - 1) {
+ throw new HttpException(
+ "Invalid proxy configuration $configuration");
+ }
+ username = userinfo.substring(0, colon).trim();
+ password = userinfo.substring(colon + 1).trim();
+ }
+ // Look for proxy host and port.
+ int colon = proxy.lastIndexOf(":");
+ if (colon == -1 || colon == 0 || colon == proxy.length - 1) {
+ throw new HttpException(
+ "Invalid proxy configuration $configuration");
+ }
+ String host = proxy.substring(0, colon).trim();
+ if (host.startsWith("[") && host.endsWith("]")) {
+ host = host.substring(1, host.length - 1);
+ }
+ String portString = proxy.substring(colon + 1).trim();
+ int port;
+ try {
+ port = int.parse(portString);
+ } on FormatException catch (e) {
+ throw new HttpException(
+ "Invalid proxy configuration $configuration, "
+ "invalid port '$portString'");
+ }
+ proxies.add(new _Proxy(host, port, username, password));
+ } else if (proxy.trim() == DIRECT_PREFIX) {
+ proxies.add(new _Proxy.direct());
+ } else {
+ throw new HttpException("Invalid proxy configuration $configuration");
+ }
+ }
+ });
+ }
+
+ const _ProxyConfiguration.direct() : proxies = const [const _Proxy.direct()];
+
+ final List<_Proxy> proxies;
+}
+
+class _Proxy {
+ final String host;
+ final int port;
+ final String username;
+ final String password;
+ final bool isDirect;
+
+ const _Proxy(this.host, this.port, this.username, this.password)
+ : isDirect = false;
+ const _Proxy.direct()
+ : host = null,
+ port = null,
+ username = null,
+ password = null,
+ isDirect = true;
+
+ bool get isAuthenticated => username != null;
+}
+
+class _HttpConnectionInfo implements HttpConnectionInfo {
+ InternetAddress remoteAddress;
+ int remotePort;
+ int localPort;
+
+ static _HttpConnectionInfo create(Socket socket) {
+ if (socket == null) return null;
+ try {
+ _HttpConnectionInfo info = new _HttpConnectionInfo();
+ return info
+ ..remoteAddress = socket.remoteAddress
+ ..remotePort = socket.remotePort
+ ..localPort = socket.port;
+ } catch (e) {}
+ return null;
+ }
+}
+
+class _DetachedSocket extends Stream<List<int>> implements Socket {
+ final Stream<List<int>> _incoming;
+ final Socket _socket;
+
+ _DetachedSocket(this._socket, this._incoming);
+
+ StreamSubscription<List<int>> listen(void onData(List<int> event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ return _incoming.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ Encoding get encoding => _socket.encoding;
+
+ void set encoding(Encoding value) {
+ _socket.encoding = value;
+ }
+
+ void write(Object obj) {
+ _socket.write(obj);
+ }
+
+ void writeln([Object obj = ""]) {
+ _socket.writeln(obj);
+ }
+
+ void writeCharCode(int charCode) {
+ _socket.writeCharCode(charCode);
+ }
+
+ void writeAll(Iterable objects, [String separator = ""]) {
+ _socket.writeAll(objects, separator);
+ }
+
+ void add(List<int> bytes) {
+ _socket.add(bytes);
+ }
+
+ void addError(error, [StackTrace stackTrace]) =>
+ _socket.addError(error, stackTrace);
+
+ Future addStream(Stream<List<int>> stream) {
+ return _socket.addStream(stream);
+ }
+
+ void destroy() {
+ _socket.destroy();
+ }
+
+ Future flush() => _socket.flush();
+
+ Future close() => _socket.close();
+
+ Future get done => _socket.done;
+
+ int get port => _socket.port;
+
+ InternetAddress get address => _socket.address;
+
+ InternetAddress get remoteAddress => _socket.remoteAddress;
+
+ int get remotePort => _socket.remotePort;
+
+ bool setOption(SocketOption option, bool enabled) {
+ return _socket.setOption(option, enabled);
+ }
+
+ Map _toJSON(bool ref) {
+ return (_socket as dynamic)._toJSON(ref);
+ }
+}
+
+class _AuthenticationScheme {
+ final int _scheme;
+
+ static const UNKNOWN = const _AuthenticationScheme(-1);
+ static const BASIC = const _AuthenticationScheme(0);
+ static const DIGEST = const _AuthenticationScheme(1);
+
+ const _AuthenticationScheme(this._scheme);
+
+ factory _AuthenticationScheme.fromString(String scheme) {
+ if (scheme.toLowerCase() == "basic") return BASIC;
+ if (scheme.toLowerCase() == "digest") return DIGEST;
+ return UNKNOWN;
+ }
+
+ String toString() {
+ if (this == BASIC) return "Basic";
+ if (this == DIGEST) return "Digest";
+ return "Unknown";
+ }
+}
+
+abstract class _Credentials {
+ _HttpClientCredentials credentials;
+ String realm;
+ bool used = false;
+
+ // Digest specific fields.
+ String ha1;
+ String nonce;
+ String algorithm;
+ String qop;
+ int nonceCount;
+
+ _Credentials(this.credentials, this.realm) {
+ if (credentials.scheme == _AuthenticationScheme.DIGEST) {
+ // Calculate the H(A1) value once. There is no mentioning of
+ // username/password encoding in RFC 2617. However there is an
+ // open draft for adding an additional accept-charset parameter to
+ // the WWW-Authenticate and Proxy-Authenticate headers, see
+ // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For
+ // now always use UTF-8 encoding.
+ _HttpClientDigestCredentials creds = credentials;
+ var hasher = new _MD5()
+ ..add(utf8.encode(creds.username))
+ ..add([_CharCode.COLON])
+ ..add(realm.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(utf8.encode(creds.password));
+ ha1 = _CryptoUtils.bytesToHex(hasher.close());
+ }
+ }
+
+ _AuthenticationScheme get scheme => credentials.scheme;
+
+ void authorize(HttpClientRequest request);
+}
+
+class _SiteCredentials extends _Credentials {
+ Uri uri;
+
+ _SiteCredentials(this.uri, realm, _HttpClientCredentials creds)
+ : super(creds, realm);
+
+ bool applies(Uri uri, _AuthenticationScheme scheme) {
+ if (scheme != null && credentials.scheme != scheme) return false;
+ if (uri.host != this.uri.host) return false;
+ int thisPort =
+ this.uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : this.uri.port;
+ int otherPort = uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : uri.port;
+ if (otherPort != thisPort) return false;
+ return uri.path.startsWith(this.uri.path);
+ }
+
+ void authorize(HttpClientRequest request) {
+ // Digest credentials cannot be used without a nonce from the
+ // server.
+ if (credentials.scheme == _AuthenticationScheme.DIGEST && nonce == null) {
+ return;
+ }
+ credentials.authorize(this, request);
+ used = true;
+ }
+}
+
+class _ProxyCredentials extends _Credentials {
+ String host;
+ int port;
+
+ _ProxyCredentials(this.host, this.port, realm, _HttpClientCredentials creds)
+ : super(creds, realm);
+
+ bool applies(_Proxy proxy, _AuthenticationScheme scheme) {
+ if (scheme != null && credentials.scheme != scheme) return false;
+ return proxy.host == host && proxy.port == port;
+ }
+
+ void authorize(HttpClientRequest request) {
+ // Digest credentials cannot be used without a nonce from the
+ // server.
+ if (credentials.scheme == _AuthenticationScheme.DIGEST && nonce == null) {
+ return;
+ }
+ credentials.authorizeProxy(this, request);
+ }
+}
+
+abstract class _HttpClientCredentials implements HttpClientCredentials {
+ _AuthenticationScheme get scheme;
+ void authorize(_Credentials credentials, HttpClientRequest request);
+ void authorizeProxy(_ProxyCredentials credentials, HttpClientRequest request);
+}
+
+class _HttpClientBasicCredentials extends _HttpClientCredentials
+ implements HttpClientBasicCredentials {
+ String username;
+ String password;
+
+ _HttpClientBasicCredentials(this.username, this.password);
+
+ _AuthenticationScheme get scheme => _AuthenticationScheme.BASIC;
+
+ String authorization() {
+ // There is no mentioning of username/password encoding in RFC
+ // 2617. However there is an open draft for adding an additional
+ // accept-charset parameter to the WWW-Authenticate and
+ // Proxy-Authenticate headers, see
+ // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For
+ // now always use UTF-8 encoding.
+ String auth =
+ _CryptoUtils.bytesToBase64(utf8.encode("$username:$password"));
+ return "Basic $auth";
+ }
+
+ void authorize(_Credentials _, HttpClientRequest request) {
+ request.headers.set(HttpHeaders.AUTHORIZATION, authorization());
+ }
+
+ void authorizeProxy(_ProxyCredentials _, HttpClientRequest request) {
+ request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, authorization());
+ }
+}
+
+class _HttpClientDigestCredentials extends _HttpClientCredentials
+ implements HttpClientDigestCredentials {
+ String username;
+ String password;
+
+ _HttpClientDigestCredentials(this.username, this.password);
+
+ _AuthenticationScheme get scheme => _AuthenticationScheme.DIGEST;
+
+ String authorization(_Credentials credentials, _HttpClientRequest request) {
+ String requestUri = request._requestUri();
+ _MD5 hasher = new _MD5()
+ ..add(request.method.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(requestUri.codeUnits);
+ var ha2 = _CryptoUtils.bytesToHex(hasher.close());
+
+ String qop;
+ String cnonce;
+ String nc;
+ var x;
+ hasher = new _MD5()..add(credentials.ha1.codeUnits)..add([_CharCode.COLON]);
+ if (credentials.qop == "auth") {
+ qop = credentials.qop;
+ cnonce = _CryptoUtils.bytesToHex(_CryptoUtils.getRandomBytes(4));
+ ++credentials.nonceCount;
+ nc = credentials.nonceCount.toRadixString(16);
+ nc = "00000000".substring(0, 8 - nc.length + 1) + nc;
+ hasher
+ ..add(credentials.nonce.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(nc.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(cnonce.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(credentials.qop.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(ha2.codeUnits);
+ } else {
+ hasher
+ ..add(credentials.nonce.codeUnits)
+ ..add([_CharCode.COLON])
+ ..add(ha2.codeUnits);
+ }
+ var response = _CryptoUtils.bytesToHex(hasher.close());
+
+ StringBuffer buffer = new StringBuffer()
+ ..write('Digest ')
+ ..write('username="$username"')
+ ..write(', realm="${credentials.realm}"')
+ ..write(', nonce="${credentials.nonce}"')
+ ..write(', uri="$requestUri"')
+ ..write(', algorithm="${credentials.algorithm}"');
+ if (qop == "auth") {
+ buffer
+ ..write(', qop="$qop"')
+ ..write(', cnonce="$cnonce"')
+ ..write(', nc="$nc"');
+ }
+ buffer.write(', response="$response"');
+ return buffer.toString();
+ }
+
+ void authorize(_Credentials credentials, HttpClientRequest request) {
+ request.headers
+ .set(HttpHeaders.AUTHORIZATION, authorization(credentials, request));
+ }
+
+ void authorizeProxy(
+ _ProxyCredentials credentials, HttpClientRequest request) {
+ request.headers.set(
+ HttpHeaders.PROXY_AUTHORIZATION, authorization(credentials, request));
+ }
+}
+
+class _RedirectInfo implements RedirectInfo {
+ final int statusCode;
+ final String method;
+ final Uri location;
+ const _RedirectInfo(this.statusCode, this.method, this.location);
+}
+
+String _getHttpVersion() {
+ var version = Platform.version;
+ // Only include major and minor version numbers.
+ int index = version.indexOf('.', version.indexOf('.') + 1);
+ version = version.substring(0, index);
+ return 'Dart/$version (dart:io)';
+}
diff --git a/lib/src/http_parser.dart b/lib/src/http_parser.dart
new file mode 100644
index 0000000..369909b
--- /dev/null
+++ b/lib/src/http_parser.dart
@@ -0,0 +1,1062 @@
+// Copyright (c) 2013, 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 http_io;
+
+// Global constants.
+class _Const {
+ // Bytes for "HTTP".
+ static const HTTP = const [72, 84, 84, 80];
+ // Bytes for "HTTP/1.".
+ static const HTTP1DOT = const [72, 84, 84, 80, 47, 49, 46];
+ // Bytes for "HTTP/1.0".
+ static const HTTP10 = const [72, 84, 84, 80, 47, 49, 46, 48];
+ // Bytes for "HTTP/1.1".
+ static const HTTP11 = const [72, 84, 84, 80, 47, 49, 46, 49];
+
+ static const bool T = true;
+ static const bool F = false;
+ // Loopup-map for the following characters: '()<>@,;:\\"/[]?={} \t'.
+ static const SEPARATOR_MAP = const [
+ F, F, F, F, F, F, F, F, F, T, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, T, F, T, F, F, F, F, F, T, T, F, F, T, F, F, T, //
+ F, F, F, F, F, F, F, F, F, F, T, T, T, T, T, T, T, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, T, T, T, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, T, F, T, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, //
+ F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F
+ ];
+}
+
+// Frequently used character codes.
+class _CharCode {
+ static const int HT = 9;
+ static const int LF = 10;
+ static const int CR = 13;
+ static const int SP = 32;
+ static const int AMPERSAND = 38;
+ static const int COMMA = 44;
+ static const int DASH = 45;
+ static const int SLASH = 47;
+ static const int ZERO = 48;
+ static const int ONE = 49;
+ static const int COLON = 58;
+ static const int SEMI_COLON = 59;
+ static const int EQUAL = 61;
+}
+
+// States of the HTTP parser state machine.
+class _State {
+ static const int START = 0;
+ static const int METHOD_OR_RESPONSE_HTTP_VERSION = 1;
+ static const int RESPONSE_HTTP_VERSION = 2;
+ static const int REQUEST_LINE_METHOD = 3;
+ static const int REQUEST_LINE_URI = 4;
+ static const int REQUEST_LINE_HTTP_VERSION = 5;
+ static const int REQUEST_LINE_ENDING = 6;
+ static const int RESPONSE_LINE_STATUS_CODE = 7;
+ static const int RESPONSE_LINE_REASON_PHRASE = 8;
+ static const int RESPONSE_LINE_ENDING = 9;
+ static const int HEADER_START = 10;
+ static const int HEADER_FIELD = 11;
+ static const int HEADER_VALUE_START = 12;
+ static const int HEADER_VALUE = 13;
+ static const int HEADER_VALUE_FOLDING_OR_ENDING = 14;
+ static const int HEADER_VALUE_FOLD_OR_END = 15;
+ static const int HEADER_ENDING = 16;
+
+ static const int CHUNK_SIZE_STARTING_CR = 17;
+ static const int CHUNK_SIZE_STARTING_LF = 18;
+ static const int CHUNK_SIZE = 19;
+ static const int CHUNK_SIZE_EXTENSION = 20;
+ static const int CHUNK_SIZE_ENDING = 21;
+ static const int CHUNKED_BODY_DONE_CR = 22;
+ static const int CHUNKED_BODY_DONE_LF = 23;
+ static const int BODY = 24;
+ static const int CLOSED = 25;
+ static const int UPGRADED = 26;
+ static const int FAILURE = 27;
+
+ static const int FIRST_BODY_STATE = CHUNK_SIZE_STARTING_CR;
+}
+
+// HTTP version of the request or response being parsed.
+class _HttpVersion {
+ static const int UNDETERMINED = 0;
+ static const int HTTP10 = 1;
+ static const int HTTP11 = 2;
+}
+
+// States of the HTTP parser state machine.
+class _MessageType {
+ static const int UNDETERMINED = 0;
+ static const int REQUEST = 1;
+ static const int RESPONSE = 0;
+}
+
+/**
+ * The _HttpDetachedStreamSubscription takes a subscription and some extra data,
+ * and makes it possible to "inject" the data in from of other data events
+ * from the subscription.
+ *
+ * It does so by overriding pause/resume, so that once the
+ * _HttpDetachedStreamSubscription is resumed, it'll deliver the data before
+ * resuming the underlaying subscription.
+ */
+class _HttpDetachedStreamSubscription implements StreamSubscription<List<int>> {
+ StreamSubscription<List<int>> _subscription;
+ List<int> _injectData;
+ bool _isCanceled = false;
+ int _pauseCount = 1;
+ Function _userOnData;
+ bool _scheduled = false;
+
+ _HttpDetachedStreamSubscription(
+ this._subscription, this._injectData, this._userOnData);
+
+ bool get isPaused => _subscription.isPaused;
+
+ Future<T> asFuture<T>([T futureValue]) =>
+ _subscription.asFuture<T>(futureValue);
+
+ Future cancel() {
+ _isCanceled = true;
+ _injectData = null;
+ return _subscription.cancel();
+ }
+
+ void onData(void handleData(List<int> data)) {
+ _userOnData = handleData;
+ _subscription.onData(handleData);
+ }
+
+ void onDone(void handleDone()) {
+ _subscription.onDone(handleDone);
+ }
+
+ void onError(Function handleError) {
+ _subscription.onError(handleError);
+ }
+
+ void pause([Future resumeSignal]) {
+ if (_injectData == null) {
+ _subscription.pause(resumeSignal);
+ } else {
+ _pauseCount++;
+ if (resumeSignal != null) {
+ resumeSignal.whenComplete(resume);
+ }
+ }
+ }
+
+ void resume() {
+ if (_injectData == null) {
+ _subscription.resume();
+ } else {
+ _pauseCount--;
+ _maybeScheduleData();
+ }
+ }
+
+ void _maybeScheduleData() {
+ if (_scheduled) return;
+ if (_pauseCount != 0) return;
+ _scheduled = true;
+ scheduleMicrotask(() {
+ _scheduled = false;
+ if (_pauseCount > 0 || _isCanceled) return;
+ var data = _injectData;
+ _injectData = null;
+ // To ensure that 'subscription.isPaused' is false, we resume the
+ // subscription here. This is fine as potential events are delayed.
+ _subscription.resume();
+ if (_userOnData != null) {
+ _userOnData(data);
+ }
+ });
+ }
+}
+
+class _HttpDetachedIncoming extends Stream<List<int>> {
+ final StreamSubscription<List<int>> subscription;
+ final List<int> bufferedData;
+
+ _HttpDetachedIncoming(this.subscription, this.bufferedData);
+
+ StreamSubscription<List<int>> listen(void onData(List<int> event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ if (subscription != null) {
+ subscription
+ ..onData(onData)
+ ..onError(onError)
+ ..onDone(onDone);
+ if (bufferedData == null) {
+ return subscription..resume();
+ }
+ return new _HttpDetachedStreamSubscription(
+ subscription, bufferedData, onData)
+ ..resume();
+ } else {
+ // TODO(26379): add test for this branch.
+ return new Stream<List<int>>.fromIterable([bufferedData]).listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+ }
+}
+
+/**
+ * HTTP parser which parses the data stream given to [consume].
+ *
+ * If an HTTP parser error occurs, the parser will signal an error to either
+ * the current _HttpIncoming or the _parser itself.
+ *
+ * The connection upgrades (e.g. switching from HTTP/1.1 to the
+ * WebSocket protocol) is handled in a special way. If connection
+ * upgrade is specified in the headers, then on the callback to
+ * [:responseStart:] the [:upgrade:] property on the [:HttpParser:]
+ * object will be [:true:] indicating that from now on the protocol is
+ * not HTTP anymore and no more callbacks will happen, that is
+ * [:dataReceived:] and [:dataEnd:] are not called in this case as
+ * there is no more HTTP data. After the upgrade the method
+ * [:readUnparsedData:] can be used to read any remaining bytes in the
+ * HTTP parser which are part of the protocol the connection is
+ * upgrading to. These bytes cannot be processed by the HTTP parser
+ * and should be handled according to whatever protocol is being
+ * upgraded to.
+ */
+class _HttpParser extends Stream<_HttpIncoming> {
+ // State.
+ bool _parserCalled = false;
+
+ // The data that is currently being parsed.
+ Uint8List _buffer;
+ int _index;
+
+ final bool _requestParser;
+ int _state;
+ int _httpVersionIndex;
+ int _messageType;
+ int _statusCode = 0;
+ int _statusCodeLength = 0;
+ final List<int> _method = [];
+ final List<int> _uri_or_reason_phrase = [];
+ final List<int> _headerField = [];
+ final List<int> _headerValue = [];
+
+ int _httpVersion;
+ int _transferLength = -1;
+ bool _persistentConnection;
+ bool _connectionUpgrade;
+ bool _chunked;
+
+ bool _noMessageBody = false;
+ int _remainingContent = -1;
+
+ _HttpHeaders _headers;
+
+ // The current incoming connection.
+ _HttpIncoming _incoming;
+ StreamSubscription<List<int>> _socketSubscription;
+ bool _paused = true;
+ bool _bodyPaused = false;
+ StreamController<_HttpIncoming> _controller;
+ StreamController<List<int>> _bodyController;
+
+ factory _HttpParser.requestParser() {
+ return new _HttpParser._(true);
+ }
+
+ factory _HttpParser.responseParser() {
+ return new _HttpParser._(false);
+ }
+
+ _HttpParser._(this._requestParser) {
+ _controller = new StreamController<_HttpIncoming>(
+ sync: true,
+ onListen: () {
+ _paused = false;
+ },
+ onPause: () {
+ _paused = true;
+ _pauseStateChanged();
+ },
+ onResume: () {
+ _paused = false;
+ _pauseStateChanged();
+ },
+ onCancel: () {
+ if (_socketSubscription != null) {
+ _socketSubscription.cancel();
+ }
+ });
+ _reset();
+ }
+
+ StreamSubscription<_HttpIncoming> listen(void onData(_HttpIncoming event),
+ {Function onError, void onDone(), bool cancelOnError}) {
+ return _controller.stream.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ }
+
+ void listenToStream(Stream<List<int>> stream) {
+ // Listen to the stream and handle data accordingly. When a
+ // _HttpIncoming is created, _dataPause, _dataResume, _dataDone is
+ // given to provide a way of controlling the parser.
+ // TODO(ajohnsen): Remove _dataPause, _dataResume and _dataDone and clean up
+ // how the _HttpIncoming signals the parser.
+ _socketSubscription =
+ stream.listen(_onData, onError: _controller.addError, onDone: _onDone);
+ }
+
+ void _parse() {
+ try {
+ _doParse();
+ } catch (e, s) {
+ _state = _State.FAILURE;
+ _reportError(e, s);
+ }
+ }
+
+ // Process end of headers. Returns true if the parser should stop
+ // parsing and return. This will be in case of either an upgrade
+ // request or a request or response with an empty body.
+ bool _headersEnd() {
+ _headers._mutable = false;
+
+ _transferLength = _headers.contentLength;
+ // Ignore the Content-Length header if Transfer-Encoding
+ // is chunked (RFC 2616 section 4.4)
+ if (_chunked) _transferLength = -1;
+
+ // If a request message has neither Content-Length nor
+ // Transfer-Encoding the message must not have a body (RFC
+ // 2616 section 4.3).
+ if (_messageType == _MessageType.REQUEST &&
+ _transferLength < 0 &&
+ _chunked == false) {
+ _transferLength = 0;
+ }
+ if (_connectionUpgrade) {
+ _state = _State.UPGRADED;
+ _transferLength = 0;
+ }
+ _createIncoming(_transferLength);
+ if (_requestParser) {
+ _incoming.method = new String.fromCharCodes(_method);
+ _incoming.uri =
+ Uri.parse(new String.fromCharCodes(_uri_or_reason_phrase));
+ } else {
+ _incoming.statusCode = _statusCode;
+ _incoming.reasonPhrase = new String.fromCharCodes(_uri_or_reason_phrase);
+ }
+ _method.clear();
+ _uri_or_reason_phrase.clear();
+ if (_connectionUpgrade) {
+ _incoming.upgraded = true;
+ _parserCalled = false;
+ var tmp = _incoming;
+ _closeIncoming();
+ _controller.add(tmp);
+ return true;
+ }
+ if (_transferLength == 0 ||
+ (_messageType == _MessageType.RESPONSE && _noMessageBody)) {
+ _reset();
+ var tmp = _incoming;
+ _closeIncoming();
+ _controller.add(tmp);
+ return false;
+ } else if (_chunked) {
+ _state = _State.CHUNK_SIZE;
+ _remainingContent = 0;
+ } else if (_transferLength > 0) {
+ _remainingContent = _transferLength;
+ _state = _State.BODY;
+ } else {
+ // Neither chunked nor content length. End of body
+ // indicated by close.
+ _state = _State.BODY;
+ }
+ _parserCalled = false;
+ _controller.add(_incoming);
+ return true;
+ }
+
+ // From RFC 2616.
+ // generic-message = start-line
+ // *(message-header CRLF)
+ // CRLF
+ // [ message-body ]
+ // start-line = Request-Line | Status-Line
+ // Request-Line = Method SP Request-URI SP HTTP-Version CRLF
+ // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
+ // message-header = field-name ":" [ field-value ]
+ void _doParse() {
+ assert(!_parserCalled);
+ _parserCalled = true;
+ if (_state == _State.CLOSED) {
+ throw new HttpException("Data on closed connection");
+ }
+ if (_state == _State.FAILURE) {
+ throw new HttpException("Data on failed connection");
+ }
+ while (_buffer != null &&
+ _index < _buffer.length &&
+ _state != _State.FAILURE &&
+ _state != _State.UPGRADED) {
+ // Depending on _incoming, we either break on _bodyPaused or _paused.
+ if ((_incoming != null && _bodyPaused) ||
+ (_incoming == null && _paused)) {
+ _parserCalled = false;
+ return;
+ }
+ int byte = _buffer[_index++];
+ switch (_state) {
+ case _State.START:
+ if (byte == _Const.HTTP[0]) {
+ // Start parsing method or HTTP version.
+ _httpVersionIndex = 1;
+ _state = _State.METHOD_OR_RESPONSE_HTTP_VERSION;
+ } else {
+ // Start parsing method.
+ if (!_isTokenChar(byte)) {
+ throw new HttpException("Invalid request method");
+ }
+ _method.add(byte);
+ if (!_requestParser) {
+ throw new HttpException("Invalid response line");
+ }
+ _state = _State.REQUEST_LINE_METHOD;
+ }
+ break;
+
+ case _State.METHOD_OR_RESPONSE_HTTP_VERSION:
+ if (_httpVersionIndex < _Const.HTTP.length &&
+ byte == _Const.HTTP[_httpVersionIndex]) {
+ // Continue parsing HTTP version.
+ _httpVersionIndex++;
+ } else if (_httpVersionIndex == _Const.HTTP.length &&
+ byte == _CharCode.SLASH) {
+ // HTTP/ parsed. As method is a token this cannot be a
+ // method anymore.
+ _httpVersionIndex++;
+ if (_requestParser) {
+ throw new HttpException("Invalid request line");
+ }
+ _state = _State.RESPONSE_HTTP_VERSION;
+ } else {
+ // Did not parse HTTP version. Expect method instead.
+ for (int i = 0; i < _httpVersionIndex; i++) {
+ _method.add(_Const.HTTP[i]);
+ }
+ if (byte == _CharCode.SP) {
+ _state = _State.REQUEST_LINE_URI;
+ } else {
+ _method.add(byte);
+ _httpVersion = _HttpVersion.UNDETERMINED;
+ if (!_requestParser) {
+ throw new HttpException("Invalid response line");
+ }
+ _state = _State.REQUEST_LINE_METHOD;
+ }
+ }
+ break;
+
+ case _State.RESPONSE_HTTP_VERSION:
+ if (_httpVersionIndex < _Const.HTTP1DOT.length) {
+ // Continue parsing HTTP version.
+ _expect(byte, _Const.HTTP1DOT[_httpVersionIndex]);
+ _httpVersionIndex++;
+ } else if (_httpVersionIndex == _Const.HTTP1DOT.length &&
+ byte == _CharCode.ONE) {
+ // HTTP/1.1 parsed.
+ _httpVersion = _HttpVersion.HTTP11;
+ _persistentConnection = true;
+ _httpVersionIndex++;
+ } else if (_httpVersionIndex == _Const.HTTP1DOT.length &&
+ byte == _CharCode.ZERO) {
+ // HTTP/1.0 parsed.
+ _httpVersion = _HttpVersion.HTTP10;
+ _persistentConnection = false;
+ _httpVersionIndex++;
+ } else if (_httpVersionIndex == _Const.HTTP1DOT.length + 1) {
+ _expect(byte, _CharCode.SP);
+ // HTTP version parsed.
+ _state = _State.RESPONSE_LINE_STATUS_CODE;
+ } else {
+ throw new HttpException("Invalid response line");
+ }
+ break;
+
+ case _State.REQUEST_LINE_METHOD:
+ if (byte == _CharCode.SP) {
+ _state = _State.REQUEST_LINE_URI;
+ } else {
+ if (_Const.SEPARATOR_MAP[byte] ||
+ byte == _CharCode.CR ||
+ byte == _CharCode.LF) {
+ throw new HttpException("Invalid request method");
+ }
+ _method.add(byte);
+ }
+ break;
+
+ case _State.REQUEST_LINE_URI:
+ if (byte == _CharCode.SP) {
+ if (_uri_or_reason_phrase.length == 0) {
+ throw new HttpException("Invalid request URI");
+ }
+ _state = _State.REQUEST_LINE_HTTP_VERSION;
+ _httpVersionIndex = 0;
+ } else {
+ if (byte == _CharCode.CR || byte == _CharCode.LF) {
+ throw new HttpException("Invalid request URI");
+ }
+ _uri_or_reason_phrase.add(byte);
+ }
+ break;
+
+ case _State.REQUEST_LINE_HTTP_VERSION:
+ if (_httpVersionIndex < _Const.HTTP1DOT.length) {
+ _expect(byte, _Const.HTTP11[_httpVersionIndex]);
+ _httpVersionIndex++;
+ } else if (_httpVersionIndex == _Const.HTTP1DOT.length) {
+ if (byte == _CharCode.ONE) {
+ // HTTP/1.1 parsed.
+ _httpVersion = _HttpVersion.HTTP11;
+ _persistentConnection = true;
+ _httpVersionIndex++;
+ } else if (byte == _CharCode.ZERO) {
+ // HTTP/1.0 parsed.
+ _httpVersion = _HttpVersion.HTTP10;
+ _persistentConnection = false;
+ _httpVersionIndex++;
+ } else {
+ throw new HttpException("Invalid response line");
+ }
+ } else {
+ if (byte == _CharCode.CR) {
+ _state = _State.REQUEST_LINE_ENDING;
+ } else {
+ _expect(byte, _CharCode.LF);
+ _messageType = _MessageType.REQUEST;
+ _state = _State.HEADER_START;
+ }
+ }
+ break;
+
+ case _State.REQUEST_LINE_ENDING:
+ _expect(byte, _CharCode.LF);
+ _messageType = _MessageType.REQUEST;
+ _state = _State.HEADER_START;
+ break;
+
+ case _State.RESPONSE_LINE_STATUS_CODE:
+ if (byte == _CharCode.SP) {
+ _state = _State.RESPONSE_LINE_REASON_PHRASE;
+ } else if (byte == _CharCode.CR) {
+ // Some HTTP servers does not follow the spec. and send
+ // \r\n right after the status code.
+ _state = _State.RESPONSE_LINE_ENDING;
+ } else {
+ _statusCodeLength++;
+ if ((byte < 0x30 && 0x39 < byte) || _statusCodeLength > 3) {
+ throw new HttpException("Invalid response status code");
+ } else {
+ _statusCode = _statusCode * 10 + byte - 0x30;
+ }
+ }
+ break;
+
+ case _State.RESPONSE_LINE_REASON_PHRASE:
+ if (byte == _CharCode.CR) {
+ _state = _State.RESPONSE_LINE_ENDING;
+ } else {
+ if (byte == _CharCode.CR || byte == _CharCode.LF) {
+ throw new HttpException("Invalid response reason phrase");
+ }
+ _uri_or_reason_phrase.add(byte);
+ }
+ break;
+
+ case _State.RESPONSE_LINE_ENDING:
+ _expect(byte, _CharCode.LF);
+ _messageType == _MessageType.RESPONSE;
+ if (_statusCode < 100 || _statusCode > 599) {
+ throw new HttpException("Invalid response status code");
+ } else {
+ // Check whether this response will never have a body.
+ if (_statusCode <= 199 ||
+ _statusCode == 204 ||
+ _statusCode == 304) {
+ _noMessageBody = true;
+ }
+ }
+ _state = _State.HEADER_START;
+ break;
+
+ case _State.HEADER_START:
+ _headers = new _HttpHeaders(version);
+ if (byte == _CharCode.CR) {
+ _state = _State.HEADER_ENDING;
+ } else if (byte == _CharCode.LF) {
+ _state = _State.HEADER_ENDING;
+ _index--; // Make the new state see the LF again.
+ } else {
+ // Start of new header field.
+ _headerField.add(_toLowerCaseByte(byte));
+ _state = _State.HEADER_FIELD;
+ }
+ break;
+
+ case _State.HEADER_FIELD:
+ if (byte == _CharCode.COLON) {
+ _state = _State.HEADER_VALUE_START;
+ } else {
+ if (!_isTokenChar(byte)) {
+ throw new HttpException("Invalid header field name");
+ }
+ _headerField.add(_toLowerCaseByte(byte));
+ }
+ break;
+
+ case _State.HEADER_VALUE_START:
+ if (byte == _CharCode.CR) {
+ _state = _State.HEADER_VALUE_FOLDING_OR_ENDING;
+ } else if (byte == _CharCode.LF) {
+ _state = _State.HEADER_VALUE_FOLD_OR_END;
+ } else if (byte != _CharCode.SP && byte != _CharCode.HT) {
+ // Start of new header value.
+ _headerValue.add(byte);
+ _state = _State.HEADER_VALUE;
+ }
+ break;
+
+ case _State.HEADER_VALUE:
+ if (byte == _CharCode.CR) {
+ _state = _State.HEADER_VALUE_FOLDING_OR_ENDING;
+ } else if (byte == _CharCode.LF) {
+ _state = _State.HEADER_VALUE_FOLD_OR_END;
+ } else {
+ _headerValue.add(byte);
+ }
+ break;
+
+ case _State.HEADER_VALUE_FOLDING_OR_ENDING:
+ _expect(byte, _CharCode.LF);
+ _state = _State.HEADER_VALUE_FOLD_OR_END;
+ break;
+
+ case _State.HEADER_VALUE_FOLD_OR_END:
+ if (byte == _CharCode.SP || byte == _CharCode.HT) {
+ _state = _State.HEADER_VALUE_START;
+ } else {
+ String headerField = new String.fromCharCodes(_headerField);
+ String headerValue = new String.fromCharCodes(_headerValue);
+ if (headerField == "transfer-encoding" &&
+ _caseInsensitiveCompare("chunked".codeUnits, _headerValue)) {
+ _chunked = true;
+ }
+ if (headerField == "connection") {
+ List<String> tokens = _tokenizeFieldValue(headerValue);
+ final bool isResponse = _messageType == _MessageType.RESPONSE;
+ final bool isUpgradeCode =
+ (_statusCode == HttpStatus.UPGRADE_REQUIRED) ||
+ (_statusCode == HttpStatus.SWITCHING_PROTOCOLS);
+ for (int i = 0; i < tokens.length; i++) {
+ final bool isUpgrade = _caseInsensitiveCompare(
+ "upgrade".codeUnits, tokens[i].codeUnits);
+ if ((isUpgrade && !isResponse) ||
+ (isUpgrade && isResponse && isUpgradeCode)) {
+ _connectionUpgrade = true;
+ }
+ _headers._add(headerField, tokens[i]);
+ }
+ } else {
+ _headers._add(headerField, headerValue);
+ }
+ _headerField.clear();
+ _headerValue.clear();
+
+ if (byte == _CharCode.CR) {
+ _state = _State.HEADER_ENDING;
+ } else if (byte == _CharCode.LF) {
+ _state = _State.HEADER_ENDING;
+ _index--; // Make the new state see the LF again.
+ } else {
+ // Start of new header field.
+ _headerField.add(_toLowerCaseByte(byte));
+ _state = _State.HEADER_FIELD;
+ }
+ }
+ break;
+
+ case _State.HEADER_ENDING:
+ _expect(byte, _CharCode.LF);
+ if (_headersEnd()) {
+ return;
+ } else {
+ break;
+ }
+ return;
+
+ case _State.CHUNK_SIZE_STARTING_CR:
+ _expect(byte, _CharCode.CR);
+ _state = _State.CHUNK_SIZE_STARTING_LF;
+ break;
+
+ case _State.CHUNK_SIZE_STARTING_LF:
+ _expect(byte, _CharCode.LF);
+ _state = _State.CHUNK_SIZE;
+ break;
+
+ case _State.CHUNK_SIZE:
+ if (byte == _CharCode.CR) {
+ _state = _State.CHUNK_SIZE_ENDING;
+ } else if (byte == _CharCode.SEMI_COLON) {
+ _state = _State.CHUNK_SIZE_EXTENSION;
+ } else {
+ int value = _expectHexDigit(byte);
+ _remainingContent = _remainingContent * 16 + value;
+ }
+ break;
+
+ case _State.CHUNK_SIZE_EXTENSION:
+ if (byte == _CharCode.CR) {
+ _state = _State.CHUNK_SIZE_ENDING;
+ }
+ break;
+
+ case _State.CHUNK_SIZE_ENDING:
+ _expect(byte, _CharCode.LF);
+ if (_remainingContent > 0) {
+ _state = _State.BODY;
+ } else {
+ _state = _State.CHUNKED_BODY_DONE_CR;
+ }
+ break;
+
+ case _State.CHUNKED_BODY_DONE_CR:
+ _expect(byte, _CharCode.CR);
+ _state = _State.CHUNKED_BODY_DONE_LF;
+ break;
+
+ case _State.CHUNKED_BODY_DONE_LF:
+ _expect(byte, _CharCode.LF);
+ _reset();
+ _closeIncoming();
+ break;
+
+ case _State.BODY:
+ // The body is not handled one byte at a time but in blocks.
+ _index--;
+ int dataAvailable = _buffer.length - _index;
+ if (_remainingContent >= 0 && dataAvailable > _remainingContent) {
+ dataAvailable = _remainingContent;
+ }
+ // Always present the data as a view. This way we can handle all
+ // cases like this, and the user will not experience different data
+ // typed (which could lead to polymorphic user code).
+ List<int> data = new Uint8List.view(
+ _buffer.buffer, _buffer.offsetInBytes + _index, dataAvailable);
+ _bodyController.add(data);
+ if (_remainingContent != -1) {
+ _remainingContent -= data.length;
+ }
+ _index += data.length;
+ if (_remainingContent == 0) {
+ if (!_chunked) {
+ _reset();
+ _closeIncoming();
+ } else {
+ _state = _State.CHUNK_SIZE_STARTING_CR;
+ }
+ }
+ break;
+
+ case _State.FAILURE:
+ // Should be unreachable.
+ assert(false);
+ break;
+
+ default:
+ // Should be unreachable.
+ assert(false);
+ break;
+ }
+ }
+
+ _parserCalled = false;
+ if (_buffer != null && _index == _buffer.length) {
+ // If all data is parsed release the buffer and resume receiving
+ // data.
+ _releaseBuffer();
+ if (_state != _State.UPGRADED && _state != _State.FAILURE) {
+ _socketSubscription.resume();
+ }
+ }
+ }
+
+ void _onData(List<int> buffer) {
+ _socketSubscription.pause();
+ assert(_buffer == null);
+ _buffer = buffer;
+ _index = 0;
+ _parse();
+ }
+
+ void _onDone() {
+ // onDone cancels the subscription.
+ _socketSubscription = null;
+ if (_state == _State.CLOSED || _state == _State.FAILURE) return;
+
+ if (_incoming != null) {
+ if (_state != _State.UPGRADED &&
+ !(_state == _State.START && !_requestParser) &&
+ !(_state == _State.BODY && !_chunked && _transferLength == -1)) {
+ _bodyController.addError(
+ new HttpException("Connection closed while receiving data"));
+ }
+ _closeIncoming(true);
+ _controller.close();
+ return;
+ }
+ // If the connection is idle the HTTP stream is closed.
+ if (_state == _State.START) {
+ if (!_requestParser) {
+ _reportError(new HttpException(
+ "Connection closed before full header was received"));
+ }
+ _controller.close();
+ return;
+ }
+
+ if (_state == _State.UPGRADED) {
+ _controller.close();
+ return;
+ }
+
+ if (_state < _State.FIRST_BODY_STATE) {
+ _state = _State.FAILURE;
+ // Report the error through the error callback if any. Otherwise
+ // throw the error.
+ _reportError(new HttpException(
+ "Connection closed before full header was received"));
+ _controller.close();
+ return;
+ }
+
+ if (!_chunked && _transferLength == -1) {
+ _state = _State.CLOSED;
+ } else {
+ _state = _State.FAILURE;
+ // Report the error through the error callback if any. Otherwise
+ // throw the error.
+ _reportError(
+ new HttpException("Connection closed before full body was received"));
+ }
+ _controller.close();
+ }
+
+ String get version {
+ switch (_httpVersion) {
+ case _HttpVersion.HTTP10:
+ return "1.0";
+ case _HttpVersion.HTTP11:
+ return "1.1";
+ }
+ return null;
+ }
+
+ int get messageType => _messageType;
+ int get transferLength => _transferLength;
+ bool get upgrade => _connectionUpgrade && _state == _State.UPGRADED;
+ bool get persistentConnection => _persistentConnection;
+
+ void set isHead(bool value) {
+ if (value) _noMessageBody = true;
+ }
+
+ _HttpDetachedIncoming detachIncoming() {
+ // Simulate detached by marking as upgraded.
+ _state = _State.UPGRADED;
+ return new _HttpDetachedIncoming(_socketSubscription, readUnparsedData());
+ }
+
+ List<int> readUnparsedData() {
+ if (_buffer == null) return null;
+ if (_index == _buffer.length) return null;
+ var result = _buffer.sublist(_index);
+ _releaseBuffer();
+ return result;
+ }
+
+ void _reset() {
+ if (_state == _State.UPGRADED) return;
+ _state = _State.START;
+ _messageType = _MessageType.UNDETERMINED;
+ _headerField.clear();
+ _headerValue.clear();
+ _method.clear();
+ _uri_or_reason_phrase.clear();
+
+ _statusCode = 0;
+ _statusCodeLength = 0;
+
+ _httpVersion = _HttpVersion.UNDETERMINED;
+ _transferLength = -1;
+ _persistentConnection = false;
+ _connectionUpgrade = false;
+ _chunked = false;
+
+ _noMessageBody = false;
+ _remainingContent = -1;
+
+ _headers = null;
+ }
+
+ void _releaseBuffer() {
+ _buffer = null;
+ _index = null;
+ }
+
+ static bool _isTokenChar(int byte) {
+ return byte > 31 && byte < 128 && !_Const.SEPARATOR_MAP[byte];
+ }
+
+ static bool _isValueChar(int byte) {
+ return (byte > 31 && byte < 128) ||
+ (byte == _CharCode.SP) ||
+ (byte == _CharCode.HT);
+ }
+
+ static List<String> _tokenizeFieldValue(String headerValue) {
+ List<String> tokens = new List<String>();
+ int start = 0;
+ int index = 0;
+ while (index < headerValue.length) {
+ if (headerValue[index] == ",") {
+ tokens.add(headerValue.substring(start, index));
+ start = index + 1;
+ } else if (headerValue[index] == " " || headerValue[index] == "\t") {
+ start++;
+ }
+ index++;
+ }
+ tokens.add(headerValue.substring(start, index));
+ return tokens;
+ }
+
+ static int _toLowerCaseByte(int x) {
+ // Optimized version:
+ // - 0x41 is 'A'
+ // - 0x7f is ASCII mask
+ // - 26 is the number of alpha characters.
+ // - 0x20 is the delta between lower and upper chars.
+ return (((x - 0x41) & 0x7f) < 26) ? (x | 0x20) : x;
+ }
+
+ // expected should already be lowercase.
+ bool _caseInsensitiveCompare(List<int> expected, List<int> value) {
+ if (expected.length != value.length) return false;
+ for (int i = 0; i < expected.length; i++) {
+ if (expected[i] != _toLowerCaseByte(value[i])) return false;
+ }
+ return true;
+ }
+
+ int _expect(int val1, int val2) {
+ if (val1 != val2) {
+ throw new HttpException("Failed to parse HTTP");
+ }
+ }
+
+ int _expectHexDigit(int byte) {
+ if (0x30 <= byte && byte <= 0x39) {
+ return byte - 0x30; // 0 - 9
+ } else if (0x41 <= byte && byte <= 0x46) {
+ return byte - 0x41 + 10; // A - F
+ } else if (0x61 <= byte && byte <= 0x66) {
+ return byte - 0x61 + 10; // a - f
+ } else {
+ throw new HttpException("Failed to parse HTTP");
+ }
+ }
+
+ void _createIncoming(int transferLength) {
+ assert(_incoming == null);
+ assert(_bodyController == null);
+ assert(!_bodyPaused);
+ var incoming;
+ _bodyController = new StreamController<List<int>>(
+ sync: true,
+ onListen: () {
+ if (incoming != _incoming) return;
+ assert(_bodyPaused);
+ _bodyPaused = false;
+ _pauseStateChanged();
+ },
+ onPause: () {
+ if (incoming != _incoming) return;
+ assert(!_bodyPaused);
+ _bodyPaused = true;
+ _pauseStateChanged();
+ },
+ onResume: () {
+ if (incoming != _incoming) return;
+ assert(_bodyPaused);
+ _bodyPaused = false;
+ _pauseStateChanged();
+ },
+ onCancel: () {
+ if (incoming != _incoming) return;
+ if (_socketSubscription != null) {
+ _socketSubscription.cancel();
+ }
+ _closeIncoming(true);
+ _controller.close();
+ });
+ incoming = _incoming =
+ new _HttpIncoming(_headers, transferLength, _bodyController.stream);
+ _bodyPaused = true;
+ _pauseStateChanged();
+ }
+
+ void _closeIncoming([bool closing = false]) {
+ // Ignore multiple close (can happen in re-entrance).
+ if (_incoming == null) return;
+ var tmp = _incoming;
+ tmp.close(closing);
+ _incoming = null;
+ if (_bodyController != null) {
+ _bodyController.close();
+ _bodyController = null;
+ }
+ _bodyPaused = false;
+ _pauseStateChanged();
+ }
+
+ void _pauseStateChanged() {
+ if (_incoming != null) {
+ if (!_bodyPaused && !_parserCalled) {
+ _parse();
+ }
+ } else {
+ if (!_paused && !_parserCalled) {
+ _parse();
+ }
+ }
+ }
+
+ void _reportError(error, [stackTrace]) {
+ if (_socketSubscription != null) _socketSubscription.cancel();
+ _state = _State.FAILURE;
+ _controller.addError(error, stackTrace);
+ _controller.close();
+ }
+}
diff --git a/lib/src/http_session.dart b/lib/src/http_session.dart
new file mode 100644
index 0000000..9ef6395
--- /dev/null
+++ b/lib/src/http_session.dart
@@ -0,0 +1,184 @@
+// Copyright (c) 2013, 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 http_io;
+
+const String _DART_SESSION_ID = "DARTSESSID";
+
+// A _HttpSession is a node in a double-linked list, with _next and _prev being
+// the previous and next pointers.
+class _HttpSession implements HttpSession {
+ // Destroyed marked. Used by the http connection to see if a session is valid.
+ bool _destroyed = false;
+ bool _isNew = true;
+ DateTime _lastSeen;
+ Function _timeoutCallback;
+ _HttpSessionManager _sessionManager;
+ // Pointers in timeout queue.
+ _HttpSession _prev;
+ _HttpSession _next;
+ final String id;
+
+ final Map _data = new HashMap();
+
+ _HttpSession(this._sessionManager, this.id) : _lastSeen = new DateTime.now();
+
+ void destroy() {
+ _destroyed = true;
+ _sessionManager._removeFromTimeoutQueue(this);
+ _sessionManager._sessions.remove(id);
+ }
+
+ // Mark the session as seen. This will reset the timeout and move the node to
+ // the end of the timeout queue.
+ void _markSeen() {
+ _lastSeen = new DateTime.now();
+ _sessionManager._bumpToEnd(this);
+ }
+
+ DateTime get lastSeen => _lastSeen;
+
+ bool get isNew => _isNew;
+
+ void set onTimeout(void callback()) {
+ _timeoutCallback = callback;
+ }
+
+ // Map implementation:
+ bool containsValue(value) => _data.containsValue(value);
+ bool containsKey(key) => _data.containsKey(key);
+ operator [](key) => _data[key];
+ void operator []=(key, value) {
+ _data[key] = value;
+ }
+
+ putIfAbsent(key, ifAbsent) => _data.putIfAbsent(key, ifAbsent);
+ addAll(Map other) => _data.addAll(other);
+ remove(key) => _data.remove(key);
+ void clear() {
+ _data.clear();
+ }
+
+ void forEach(void f(key, value)) {
+ _data.forEach(f);
+ }
+
+ Iterable get keys => _data.keys;
+ Iterable get values => _data.values;
+ int get length => _data.length;
+ bool get isEmpty => _data.isEmpty;
+ bool get isNotEmpty => _data.isNotEmpty;
+
+ String toString() => 'HttpSession id:$id $_data';
+}
+
+// Private class used to manage all the active sessions. The sessions are stored
+// in two ways:
+//
+// * In a map, mapping from ID to HttpSession.
+// * In a linked list, used as a timeout queue.
+class _HttpSessionManager {
+ Map<String, _HttpSession> _sessions;
+ int _sessionTimeout = 20 * 60; // 20 mins.
+ _HttpSession _head;
+ _HttpSession _tail;
+ Timer _timer;
+
+ _HttpSessionManager() : _sessions = {};
+
+ String createSessionId() {
+ const int _KEY_LENGTH = 16; // 128 bits.
+ var data = _CryptoUtils.getRandomBytes(_KEY_LENGTH);
+ return _CryptoUtils.bytesToHex(data);
+ }
+
+ _HttpSession getSession(String id) => _sessions[id];
+
+ _HttpSession createSession() {
+ var id = createSessionId();
+ // TODO(ajohnsen): Consider adding a limit and throwing an exception.
+ // Should be very unlikely however.
+ while (_sessions.containsKey(id)) {
+ id = createSessionId();
+ }
+ var session = _sessions[id] = new _HttpSession(this, id);
+ _addToTimeoutQueue(session);
+ return session;
+ }
+
+ void set sessionTimeout(int timeout) {
+ _sessionTimeout = timeout;
+ _stopTimer();
+ _startTimer();
+ }
+
+ void close() {
+ _stopTimer();
+ }
+
+ void _bumpToEnd(_HttpSession session) {
+ _removeFromTimeoutQueue(session);
+ _addToTimeoutQueue(session);
+ }
+
+ void _addToTimeoutQueue(_HttpSession session) {
+ if (_head == null) {
+ assert(_tail == null);
+ _tail = _head = session;
+ _startTimer();
+ } else {
+ assert(_timer != null);
+ assert(_tail != null);
+ // Add to end.
+ _tail._next = session;
+ session._prev = _tail;
+ _tail = session;
+ }
+ }
+
+ void _removeFromTimeoutQueue(_HttpSession session) {
+ if (session._next != null) {
+ session._next._prev = session._prev;
+ }
+ if (session._prev != null) {
+ session._prev._next = session._next;
+ }
+ if (_head == session) {
+ // We removed the head element, start new timer.
+ _head = session._next;
+ _stopTimer();
+ _startTimer();
+ }
+ if (_tail == session) {
+ _tail = session._prev;
+ }
+ session._next = session._prev = null;
+ }
+
+ void _timerTimeout() {
+ _stopTimer(); // Clear timer.
+ assert(_head != null);
+ var session = _head;
+ session.destroy(); // Will remove the session from timeout queue and map.
+ if (session._timeoutCallback != null) {
+ session._timeoutCallback();
+ }
+ }
+
+ void _startTimer() {
+ assert(_timer == null);
+ if (_head != null) {
+ int seconds = new DateTime.now().difference(_head.lastSeen).inSeconds;
+ _timer = new Timer(
+ new Duration(seconds: _sessionTimeout - seconds), _timerTimeout);
+ }
+ }
+
+ void _stopTimer() {
+ if (_timer != null) {
+ _timer.cancel();
+ _timer = null;
+ }
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..695336a
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,7 @@
+name: http_io
+version: 0.10.0
+author: "Dart Team <misc@dartlang.org>"
+homepage: https://github.com/dart-lang/http_io
+description: HTTP Client and Server APIs.
+environment:
+ sdk: ">=2.0.0"