Index: pkg/dev_compiler/tool/input_sdk/lib/io/http_impl.dart |
diff --git a/pkg/dev_compiler/tool/input_sdk/lib/io/http_impl.dart b/pkg/dev_compiler/tool/input_sdk/lib/io/http_impl.dart |
deleted file mode 100644 |
index dff98f04f64c30e13a008bc1de37d282f7082a16..0000000000000000000000000000000000000000 |
--- a/pkg/dev_compiler/tool/input_sdk/lib/io/http_impl.dart |
+++ /dev/null |
@@ -1,2890 +0,0 @@ |
-// 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 dart.io; |
- |
-const int _OUTGOING_BUFFER_SIZE = 8 * 1024; |
- |
-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.fromIterable([]).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; |
- } |
- }); |
- } |
-} |
- |
- |
-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() { |
- Uint8List buffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
- int offset = 0; |
- |
- void write(List<int> bytes) { |
- int len = bytes.length; |
- for (int i = 0; i < len; i++) { |
- buffer[offset + i] = bytes[i]; |
- } |
- offset += len; |
- } |
- |
- // Write status line. |
- if (headers.protocolVersion == "1.1") { |
- write(_Const.HTTP11); |
- } else { |
- write(_Const.HTTP10); |
- } |
- buffer[offset++] = _CharCode.SP; |
- write(statusCode.toString().codeUnits); |
- buffer[offset++] = _CharCode.SP; |
- write(reasonPhrase.codeUnits); |
- buffer[offset++] = _CharCode.CR; |
- buffer[offset++] = _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. |
- offset = headers._write(buffer, offset); |
- buffer[offset++] = _CharCode.CR; |
- buffer[offset++] = _CharCode.LF; |
- _outgoing.setHeader(buffer, offset); |
- } |
- |
- 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((_) => response.redirect()); |
- } else { |
- // End with exception, too many redirects. |
- future = response.drain() |
- .then((_) => new Future.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() { |
- Uint8List buffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
- int offset = 0; |
- |
- void write(List<int> bytes) { |
- int len = bytes.length; |
- for (int i = 0; i < len; i++) { |
- buffer[offset + i] = bytes[i]; |
- } |
- offset += len; |
- } |
- |
- // Write the request method. |
- write(method.codeUnits); |
- buffer[offset++] = _CharCode.SP; |
- // Write the request URI. |
- write(_requestUri().codeUnits); |
- buffer[offset++] = _CharCode.SP; |
- // Write HTTP/1.1. |
- write(_Const.HTTP11); |
- buffer[offset++] = _CharCode.CR; |
- buffer[offset++] = _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. |
- offset = headers._write(buffer, offset); |
- buffer[offset++] = _CharCode.CR; |
- buffer[offset++] = _CharCode.LF; |
- _outgoing.setHeader(buffer, offset); |
- } |
-} |
- |
-// Used by _HttpOutgoing as a target of a chunked converter for gzip |
-// compression. |
-class _HttpGZipSink extends ByteConversionSink { |
- final Function _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 compressionm |
-// - 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 _doneCompleter = new Completer(); |
- 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). |
- Function _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}) { |
- Future write() { |
- try { |
- outbound._writeHeader(); |
- } catch (_) { |
- // Headers too large. |
- return new Future.error(new HttpException( |
- "Headers size exceeded the of '$_OUTGOING_BUFFER_SIZE'" |
- " bytes")); |
- } |
- return null; |
- } |
- |
- 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) { |
- return write(); |
- } |
- 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((_) => write()); |
- } |
- return write(); |
- } |
- |
- |
- 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(); |
- } |
- var 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( |
- onPause: () => sub.pause(), |
- onResume: () => sub.resume(), |
- sync: true); |
- |
- void onData(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 get done => _doneCompleter.future; |
- |
- void setHeader(List<int> data, int length) { |
- assert(_length == 0); |
- assert(data.length == _OUTGOING_BUFFER_SIZE); |
- _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(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(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 _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((s) { |
- // Request sent, set up response completer. |
- _nextResponseCompleter = new Completer(); |
- |
- // 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(host, port, proxy, callback) { |
- _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(); |
- _pending.add(() { |
- connect(uriHost, uriPort, proxy, client) |
- .then(completer.complete, onError: completer.completeError); |
- }); |
- 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(SecurityContext 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 connnection 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(() => 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 _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() { |
- try { _socket._owner = this; } catch (_) { print(_); } |
- _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((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((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; |
- _serverSocket._owner = this; |
- } |
- |
- _HttpServer.listenOn(this._serverSocket) : _closeServer = false { |
- _controller = new StreamController<HttpRequest>(sync: true, |
- onCancel: close); |
- idleTimeout = const Duration(seconds: 120); |
- _servers[_serviceId] = this; |
- try { _serverSocket._owner = this; } catch (_) {} |
- } |
- |
- 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 _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; |
- |
- _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<Socket> addStream(Stream<List<int>> stream) { |
- return _socket.addStream(stream); |
- } |
- |
- void destroy() { _socket.destroy(); } |
- |
- Future flush() => _socket.flush(); |
- |
- Future close() => _socket.close(); |
- |
- Future<Socket> 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) => _socket._toJSON(ref); |
- void set _owner(owner) { _socket._owner = owner; } |
-} |
- |
- |
-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(_IOCrypto.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)'; |
-} |