| Index: sdk/lib/io/http_impl.dart
 | 
| diff --git a/sdk/lib/io/http_impl.dart b/sdk/lib/io/http_impl.dart
 | 
| index ede39a2418e518c3fc6b1ea5dba2681fca5107ac..1ed571eadda474cb4de614cdaa1f765847e3b1b9 100644
 | 
| --- a/sdk/lib/io/http_impl.dart
 | 
| +++ b/sdk/lib/io/http_impl.dart
 | 
| @@ -4,520 +4,477 @@
 | 
|  
 | 
|  part of dart.io;
 | 
|  
 | 
| -// The close queue handles graceful closing of HTTP connections. When
 | 
| -// a connection is added to the queue it will enter a wait state
 | 
| -// waiting for all data written and possibly socket shutdown from
 | 
| -// peer.
 | 
| -class _CloseQueue {
 | 
| -  _CloseQueue() : _q = new Set<_HttpConnectionBase>();
 | 
| -
 | 
| -  void add(_HttpConnectionBase connection) {
 | 
| -    void closeIfDone() {
 | 
| -      // When either the client has closed or all data has been
 | 
| -      // written to the client we close the underlying socket
 | 
| -      // completely.
 | 
| -      if (connection._isWriteClosed || connection._isReadClosed) {
 | 
| -        _q.remove(connection);
 | 
| -        connection._socket.close();
 | 
| -        if (connection.onClosed != null) connection.onClosed();
 | 
| -      }
 | 
| -    }
 | 
| +class _HttpIncoming
 | 
| +    extends Stream<List<int>> implements StreamSink<List<int>> {
 | 
| +  final int _transferLength;
 | 
| +  final Completer _dataCompleter = new Completer();
 | 
| +  Stream<List<int>> _stream;
 | 
|  
 | 
| -    // If the connection is already fully closed don't insert it into
 | 
| -    // the queue.
 | 
| -    if (connection._isFullyClosed) {
 | 
| -      connection._socket.close();
 | 
| -      if (connection.onClosed != null) connection.onClosed();
 | 
| -      return;
 | 
| -    }
 | 
| +  bool fullBodyRead = false;
 | 
|  
 | 
| -    connection._state |= _HttpConnectionBase.CLOSING;
 | 
| -    _q.add(connection);
 | 
| -
 | 
| -    // If the output stream is not closed for writing, close it now and
 | 
| -    // wait for callback when closed.
 | 
| -    if (!connection._isWriteClosed) {
 | 
| -      connection._socket.outputStream.close();
 | 
| -      connection._socket.outputStream.onClosed = () {
 | 
| -        connection._state |= _HttpConnectionBase.WRITE_CLOSED;
 | 
| -        closeIfDone();
 | 
| -      };
 | 
| -    } else {
 | 
| -      connection._socket.outputStream.onClosed = () { assert(false); };
 | 
| -    }
 | 
| +  // Common properties.
 | 
| +  final _HttpHeaders headers;
 | 
| +  bool upgraded = false;
 | 
|  
 | 
| -    // If the request is not already fully read wait for the socket to close.
 | 
| -    // As the _isReadClosed state from the HTTP request processing indicate
 | 
| -    // that the response has been parsed this does not necesarily mean tha
 | 
| -    // the socket is closed.
 | 
| -    if (!connection._isReadClosed) {
 | 
| -      connection._socket.onClosed = () {
 | 
| -        connection._state |= _HttpConnectionBase.READ_CLOSED;
 | 
| -        closeIfDone();
 | 
| -      };
 | 
| -    }
 | 
| +  // ClientResponse properties.
 | 
| +  int statusCode;
 | 
| +  String reasonPhrase;
 | 
| +
 | 
| +  // Request properties.
 | 
| +  String method;
 | 
| +  Uri uri;
 | 
|  
 | 
| -    // Ignore any data on a socket in the close queue.
 | 
| -    connection._socket.onData = connection._socket.read;
 | 
| +  // 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;
 | 
|  
 | 
| -    // If an error occurs immediately close the socket.
 | 
| -    connection._socket.onError = (e) {
 | 
| -      connection._state |= _HttpConnectionBase.READ_CLOSED;
 | 
| -      connection._state |= _HttpConnectionBase.WRITE_CLOSED;
 | 
| -      closeIfDone();
 | 
| -    };
 | 
| +  _HttpIncoming(_HttpHeaders this.headers,
 | 
| +                int this._transferLength,
 | 
| +                Stream<List<int>> this._stream) {
 | 
|    }
 | 
|  
 | 
| -  void shutdown() {
 | 
| -    _q.forEach((_HttpConnectionBase connection) {
 | 
| -      connection._socket.close();
 | 
| -    });
 | 
| +  StreamSubscription<List<int>> listen(void onData(List<int> event),
 | 
| +                                       {void onError(AsyncError error),
 | 
| +                                        void onDone(),
 | 
| +                                        bool unsubscribeOnError}) {
 | 
| +    return _stream.listen(onData,
 | 
| +                          onError: onError,
 | 
| +                          onDone: onDone,
 | 
| +                          unsubscribeOnError: unsubscribeOnError);
 | 
|    }
 | 
|  
 | 
| -  final Set<_HttpConnectionBase> _q;
 | 
| +  // Is completed once all data have been received.
 | 
| +  Future get dataDone => _dataCompleter.future;
 | 
| +
 | 
| +  void close() {
 | 
| +    fullBodyRead = true;
 | 
| +    _dataCompleter.complete();
 | 
| +  }
 | 
|  }
 | 
|  
 | 
| +class _HttpInboundMessage extends Stream<List<int>> {
 | 
| +  final _HttpIncoming _incoming;
 | 
| +  List<Cookie> _cookies;
 | 
|  
 | 
| -class _HttpRequestResponseBase {
 | 
| -  static const int START = 0;
 | 
| -  static const int HEADER_SENT = 1;
 | 
| -  static const int DONE = 2;
 | 
| -  static const int UPGRADED = 3;
 | 
| +  _HttpInboundMessage(_HttpIncoming this._incoming);
 | 
|  
 | 
| -  _HttpRequestResponseBase(_HttpConnectionBase this._httpConnection)
 | 
| -      : _state = START, _headResponse = false;
 | 
| +  List<Cookie> get cookies {
 | 
| +    if (_cookies != null) return _cookies;
 | 
| +    return _cookies = headers._parseCookies();
 | 
| +  }
 | 
|  
 | 
| -  int get contentLength => _headers.contentLength;
 | 
| -  HttpHeaders get headers => _headers;
 | 
| +  HttpHeaders get headers => _incoming.headers;
 | 
| +  String get protocolVersion => headers.protocolVersion;
 | 
| +  int get contentLength => headers.contentLength;
 | 
| +  bool get persistentConnection => headers.persistentConnection;
 | 
| +}
 | 
|  
 | 
| -  bool get persistentConnection {
 | 
| -    List<String> connection = headers[HttpHeaders.CONNECTION];
 | 
| -    if (_protocolVersion == "1.1") {
 | 
| -      if (connection == null) return true;
 | 
| -      return !headers[HttpHeaders.CONNECTION].any(
 | 
| -          (value) => value.toLowerCase() == "close");
 | 
| -    } else {
 | 
| -      if (connection == null) return false;
 | 
| -      return headers[HttpHeaders.CONNECTION].any(
 | 
| -          (value) => value.toLowerCase() == "keep-alive");
 | 
| -    }
 | 
| -  }
 | 
|  
 | 
| -  X509Certificate get certificate {
 | 
| -    var socket = _httpConnection._socket as SecureSocket;
 | 
| -    return socket == null ? socket : socket.peerCertificate;
 | 
| -  }
 | 
| +class _HttpRequest extends _HttpInboundMessage implements HttpRequest {
 | 
| +  final HttpResponse response;
 | 
|  
 | 
| -  void set persistentConnection(bool persistentConnection) {
 | 
| -    if (_outputStream != null) throw new HttpException("Header already sent");
 | 
| +  // Lazy initialized parsed query parameters.
 | 
| +  Map<String, String> _queryParameters;
 | 
|  
 | 
| -    // Determine the value of the "Connection" header.
 | 
| -    headers.remove(HttpHeaders.CONNECTION, "close");
 | 
| -    headers.remove(HttpHeaders.CONNECTION, "keep-alive");
 | 
| -    if (_protocolVersion == "1.1" && !persistentConnection) {
 | 
| -      headers.add(HttpHeaders.CONNECTION, "close");
 | 
| -    } else if (_protocolVersion == "1.0" && persistentConnection) {
 | 
| -      headers.add(HttpHeaders.CONNECTION, "keep-alive");
 | 
| -    }
 | 
| -  }
 | 
| +  final _HttpServer _httpServer;
 | 
|  
 | 
| +  final _HttpConnection _httpConnection;
 | 
|  
 | 
| -  bool _write(List<int> data, bool copyBuffer) {
 | 
| -    if (_headResponse) return true;
 | 
| -    _ensureHeadersSent();
 | 
| -    bool allWritten = true;
 | 
| -    if (data.length > 0) {
 | 
| -      if (_headers.chunkedTransferEncoding) {
 | 
| -        // Write chunk size if transfer encoding is chunked.
 | 
| -        _writeHexString(data.length);
 | 
| -        _writeCRLF();
 | 
| -        _httpConnection._write(data, copyBuffer);
 | 
| -        allWritten = _writeCRLF();
 | 
| -      } else {
 | 
| -        _updateContentLength(data.length);
 | 
| -        allWritten = _httpConnection._write(data, copyBuffer);
 | 
| -      }
 | 
| -    }
 | 
| -    return allWritten;
 | 
| -  }
 | 
| -
 | 
| -  bool _writeList(List<int> data, int offset, int count) {
 | 
| -    if (_headResponse) return true;
 | 
| -    _ensureHeadersSent();
 | 
| -    bool allWritten = true;
 | 
| -    if (count > 0) {
 | 
| -      if (_headers.chunkedTransferEncoding) {
 | 
| -        // Write chunk size if transfer encoding is chunked.
 | 
| -        _writeHexString(count);
 | 
| -        _writeCRLF();
 | 
| -        _httpConnection._writeFrom(data, offset, count);
 | 
| -        allWritten = _writeCRLF();
 | 
| -      } else {
 | 
| -        _updateContentLength(count);
 | 
| -        allWritten = _httpConnection._writeFrom(data, offset, count);
 | 
| -      }
 | 
| -    }
 | 
| -    return allWritten;
 | 
| -  }
 | 
| +  HttpSession _session;
 | 
|  
 | 
| -  bool _writeDone() {
 | 
| -    bool allWritten = true;
 | 
| -    if (_headers.chunkedTransferEncoding) {
 | 
| -      // Terminate the content if transfer encoding is chunked.
 | 
| -      allWritten = _httpConnection._write(_Const.END_CHUNKED);
 | 
| -    } else {
 | 
| -      if (!_headResponse && _bodyBytesWritten < _headers.contentLength) {
 | 
| -        throw new HttpException("Sending less than specified content length");
 | 
| +  _HttpRequest(_HttpResponse this.response,
 | 
| +               _HttpIncoming _incoming,
 | 
| +               _HttpServer this._httpServer,
 | 
| +               _HttpConnection this._httpConnection)
 | 
| +      : super(_incoming) {
 | 
| +    response.headers.persistentConnection = headers.persistentConnection;
 | 
| +
 | 
| +    if (_httpServer._sessionManagerInstance != null) {
 | 
| +      // Map to session if exists.
 | 
| +      var sessionId = cookies.reduce(null, (last, cookie) {
 | 
| +        if (last != null) return last;
 | 
| +        return cookie.name.toUpperCase() == _DART_SESSION_ID ?
 | 
| +            cookie.value : null;
 | 
| +      });
 | 
| +      if (sessionId != null) {
 | 
| +        _session = _httpServer._sessionManager.getSession(sessionId);
 | 
| +        if (_session != null) {
 | 
| +          _session._markSeen();
 | 
| +        }
 | 
|        }
 | 
| -      assert(_headResponse || _bodyBytesWritten == _headers.contentLength);
 | 
|      }
 | 
| -    return allWritten;
 | 
|    }
 | 
|  
 | 
| -  bool _writeHeaders() {
 | 
| -    _headers._write(_httpConnection);
 | 
| -    // Terminate header.
 | 
| -    return _writeCRLF();
 | 
| +  StreamSubscription<List<int>> listen(void onData(List<int> event),
 | 
| +                                       {void onError(AsyncError error),
 | 
| +                                        void onDone(),
 | 
| +                                        bool unsubscribeOnError}) {
 | 
| +    return _incoming.listen(onData,
 | 
| +                            onError: onError,
 | 
| +                            onDone: onDone,
 | 
| +                            unsubscribeOnError: unsubscribeOnError);
 | 
|    }
 | 
|  
 | 
| -  bool _writeHexString(int x) {
 | 
| -    final List<int> hexDigits = [0x30, 0x31, 0x32, 0x33, 0x34,
 | 
| -                                 0x35, 0x36, 0x37, 0x38, 0x39,
 | 
| -                                 0x41, 0x42, 0x43, 0x44, 0x45, 0x46];
 | 
| -    List<int> hex = new Uint8List(10);
 | 
| -    int index = hex.length;
 | 
| -    while (x > 0) {
 | 
| -      index--;
 | 
| -      hex[index] = hexDigits[x % 16];
 | 
| -      x = x >> 4;
 | 
| +  Map<String, String> get queryParameters {
 | 
| +    if (_queryParameters == null) {
 | 
| +      _queryParameters = _HttpUtils.splitQueryString(uri.query);
 | 
|      }
 | 
| -    return _httpConnection._writeFrom(hex, index, hex.length - index);
 | 
| +    return _queryParameters;
 | 
|    }
 | 
|  
 | 
| -  bool _writeCRLF() {
 | 
| -    final CRLF = const [_CharCode.CR, _CharCode.LF];
 | 
| -    return _httpConnection._write(CRLF);
 | 
| -  }
 | 
| +  Uri get uri => _incoming.uri;
 | 
|  
 | 
| -  bool _writeSP() {
 | 
| -    final SP = const [_CharCode.SP];
 | 
| -    return _httpConnection._write(SP);
 | 
| -  }
 | 
| +  String get method => _incoming.method;
 | 
|  
 | 
| -  void _ensureHeadersSent() {
 | 
| -    // Ensure that headers are written.
 | 
| -    if (_state == START) {
 | 
| -      _writeHeader();
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  void _updateContentLength(int bytes) {
 | 
| -    if (_bodyBytesWritten + bytes > _headers.contentLength) {
 | 
| -      throw new HttpException("Writing more than specified content length");
 | 
| +  HttpSession get session {
 | 
| +    if (_session != null) {
 | 
| +      // It's already mapped, use it.
 | 
| +      return _session;
 | 
|      }
 | 
| -    _bodyBytesWritten += bytes;
 | 
| +    // Create session, store it in connection, and return.
 | 
| +    return _session = _httpServer._sessionManager.createSession();
 | 
|    }
 | 
|  
 | 
|    HttpConnectionInfo get connectionInfo => _httpConnection.connectionInfo;
 | 
|  
 | 
| -  bool get _done => _state == DONE;
 | 
| -
 | 
| -  int _state;
 | 
| -  bool _headResponse;
 | 
| -
 | 
| -  _HttpConnectionBase _httpConnection;
 | 
| -  _HttpHeaders _headers;
 | 
| -  List<Cookie> _cookies;
 | 
| -  String _protocolVersion = "1.1";
 | 
| -
 | 
| -  // Number of body bytes written. This is only actual body data not
 | 
| -  // including headers or chunk information of using chinked transfer
 | 
| -  // encoding.
 | 
| -  int _bodyBytesWritten = 0;
 | 
| +  X509Certificate get certificate {
 | 
| +    Socket socket = _httpConnection._socket;
 | 
| +    if (socket is SecureSocket) return socket.peerCertificate;
 | 
| +    return null;
 | 
| +  }
 | 
|  }
 | 
|  
 | 
|  
 | 
| -// Parsed HTTP request providing information on the HTTP headers.
 | 
| -class _HttpRequest extends _HttpRequestResponseBase implements HttpRequest {
 | 
| -  _HttpRequest(_HttpConnection connection) : super(connection);
 | 
| -
 | 
| -  String get method => _method;
 | 
| -  String get uri => _uri;
 | 
| -  String get path => _path;
 | 
| -  String get queryString => _queryString;
 | 
| -  Map get queryParameters => _queryParameters;
 | 
| -
 | 
| -  List<Cookie> get cookies {
 | 
| -    if (_cookies != null) return _cookies;
 | 
| -
 | 
| -    // Parse a Cookie header value according to the rules in RFC 6265.
 | 
| -   void _parseCookieString(String s) {
 | 
| -      int index = 0;
 | 
| +class _HttpClientResponse
 | 
| +    extends _HttpInboundMessage implements HttpClientResponse {
 | 
| +  List<RedirectInfo> get redirects => _httpRequest._responseRedirects;
 | 
|  
 | 
| -      bool done() => index == s.length;
 | 
| +  // The HttpClient this response belongs to.
 | 
| +  final _HttpClient _httpClient;
 | 
|  
 | 
| -      void skipWS() {
 | 
| -        while (!done()) {
 | 
| -         if (s[index] != " " && s[index] != "\t") return;
 | 
| -         index++;
 | 
| -        }
 | 
| -      }
 | 
| +  // The HttpClientRequest of this response.
 | 
| +  final _HttpClientRequest _httpRequest;
 | 
|  
 | 
| -      String parseName() {
 | 
| -        int start = index;
 | 
| -        while (!done()) {
 | 
| -          if (s[index] == " " || s[index] == "\t" || s[index] == "=") break;
 | 
| -          index++;
 | 
| -        }
 | 
| -        return s.substring(start, index).toLowerCase();
 | 
| -      }
 | 
| +  List<Cookie> _cookies;
 | 
|  
 | 
| -      String parseValue() {
 | 
| -        int start = index;
 | 
| -        while (!done()) {
 | 
| -          if (s[index] == " " || s[index] == "\t" || s[index] == ";") break;
 | 
| -          index++;
 | 
| -        }
 | 
| -        return s.substring(start, index).toLowerCase();
 | 
| -      }
 | 
| +  _HttpClientResponse(_HttpIncoming _incoming,
 | 
| +                      _HttpClientRequest this._httpRequest,
 | 
| +                      _HttpClient this._httpClient)
 | 
| +      : super(_incoming);
 | 
|  
 | 
| -      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++;
 | 
| -      }
 | 
| -
 | 
| -      while (!done()) {
 | 
| -        skipWS();
 | 
| -        if (done()) return;
 | 
| -        String name = parseName();
 | 
| -        skipWS();
 | 
| -        expect("=");
 | 
| -        skipWS();
 | 
| -        String value = parseValue();
 | 
| -        _cookies.add(new _Cookie(name, value));
 | 
| -        skipWS();
 | 
| -        if (done()) return;
 | 
| -        expect(";");
 | 
| -      }
 | 
| -    }
 | 
| +  int get statusCode => _incoming.statusCode;
 | 
| +  String get reasonPhrase => _incoming.reasonPhrase;
 | 
|  
 | 
| +  List<Cookie> get cookies {
 | 
| +    if (_cookies != null) return _cookies;
 | 
|      _cookies = new List<Cookie>();
 | 
| -    List<String> headerValues = headers["cookie"];
 | 
| -    if (headerValues != null) {
 | 
| -      headerValues.forEach((headerValue) => _parseCookieString(headerValue));
 | 
| +    List<String> values = headers["set-cookie"];
 | 
| +    if (values != null) {
 | 
| +      values.forEach((value) {
 | 
| +        _cookies.add(new Cookie.fromSetCookieValue(value));
 | 
| +      });
 | 
|      }
 | 
|      return _cookies;
 | 
|    }
 | 
|  
 | 
| -  InputStream get inputStream {
 | 
| -    if (_inputStream == null) {
 | 
| -      _inputStream = new _HttpInputStream(this);
 | 
| +  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 _inputStream;
 | 
| +    return false;
 | 
|    }
 | 
|  
 | 
| -  String get protocolVersion => _protocolVersion;
 | 
| -
 | 
| -  HttpSession session([init(HttpSession session)]) {
 | 
| -    if (_session != null) {
 | 
| -      // It's already mapped, use it.
 | 
| -      return _session;
 | 
| +  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;
 | 
| +      }
 | 
|      }
 | 
| -    // Create session, store it in connection, and return.
 | 
| -    var sessionManager = _httpConnection._server._sessionManager;
 | 
| -    return _session = sessionManager.createSession(init);
 | 
| -  }
 | 
| -
 | 
| -  void _onRequestReceived(String method,
 | 
| -                          String uri,
 | 
| -                          String version,
 | 
| -                          _HttpHeaders headers) {
 | 
| -    _method = method;
 | 
| -    _uri = uri;
 | 
| -    _parseRequestUri(uri);
 | 
| -    _headers = headers;
 | 
| -    if (_httpConnection._server._sessionManagerInstance != null) {
 | 
| -      // Map to session if exists.
 | 
| -      var sessionId = cookies.reduce(null, (last, cookie) {
 | 
| -        if (last != null) return last;
 | 
| -        return cookie.name.toUpperCase() == _DART_SESSION_ID ?
 | 
| -            cookie.value : null;
 | 
| -      });
 | 
| -      if (sessionId != null) {
 | 
| -        var sessionManager = _httpConnection._server._sessionManager;
 | 
| -        _session = sessionManager.getSession(sessionId);
 | 
| -        if (_session != null) {
 | 
| -          _session._markSeen();
 | 
| +    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.immediateError(
 | 
| +              new RedirectLoopException(redirects));
 | 
|          }
 | 
|        }
 | 
|      }
 | 
| -
 | 
| -    // Prepare for receiving data.
 | 
| -    _buffer = new _BufferList();
 | 
| +    return _httpClient._openUrlFromRequest(method, url, _httpRequest)
 | 
| +        .then((request) {
 | 
| +          request._responseRedirects.addAll(this.redirects);
 | 
| +          request._responseRedirects.add(new _RedirectInfo(statusCode,
 | 
| +                                                           method,
 | 
| +                                                           url));
 | 
| +          return request.close();
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _onDataReceived(List<int> data) {
 | 
| -    _buffer.add(data);
 | 
| -    if (_inputStream != null) _inputStream._dataReceived();
 | 
| +  StreamSubscription<List<int>> listen(void onData(List<int> event),
 | 
| +                                       {void onError(AsyncError error),
 | 
| +                                        void onDone(),
 | 
| +                                        bool unsubscribeOnError}) {
 | 
| +    return _incoming.listen(onData,
 | 
| +                            onError: onError,
 | 
| +                            onDone: onDone,
 | 
| +                            unsubscribeOnError: unsubscribeOnError);
 | 
|    }
 | 
|  
 | 
| -  void _onDataEnd() {
 | 
| -    if (_inputStream != null) {
 | 
| -      _inputStream._closeReceived();
 | 
| -    } else {
 | 
| -      inputStream._streamMarkedClosed = true;
 | 
| -    }
 | 
| +  Future<Socket> detachSocket() {
 | 
| +    _httpClient._connectionClosed(_httpRequest._httpClientConnection);
 | 
| +    return _httpRequest._httpClientConnection.detachSocket();
 | 
|    }
 | 
|  
 | 
| -  // Escaped characters in uri are expected to have been parsed.
 | 
| -  void _parseRequestUri(String uri) {
 | 
| -    int position;
 | 
| -    position = uri.indexOf("?", 0);
 | 
| -    if (position == -1) {
 | 
| -      _path = _HttpUtils.decodeUrlEncodedString(_uri);
 | 
| -      _queryString = null;
 | 
| -      _queryParameters = new Map();
 | 
| -    } else {
 | 
| -      _path = _HttpUtils.decodeUrlEncodedString(_uri.substring(0, position));
 | 
| -      _queryString = _uri.substring(position + 1);
 | 
| -      _queryParameters = _HttpUtils.splitQueryString(_queryString);
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  // Delegate functions for the HttpInputStream implementation.
 | 
| -  int _streamAvailable() {
 | 
| -    return _buffer.length;
 | 
| -  }
 | 
| +  HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo;
 | 
|  
 | 
| -  List<int> _streamRead(int bytesToRead) {
 | 
| -    return _buffer.readBytes(bytesToRead);
 | 
| +  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;
 | 
|    }
 | 
|  
 | 
| -  int _streamReadInto(List<int> buffer, int offset, int len) {
 | 
| -    List<int> data = _buffer.readBytes(len);
 | 
| -    buffer.setRange(offset, data.length, data);
 | 
| -    return data.length;
 | 
| -  }
 | 
| +  Future<HttpClientResponse> _authenticate() {
 | 
| +    Future<HttpClientResponse> retryWithCredentials(_Credentials cr) {
 | 
| +      if (cr != null) {
 | 
| +        // TODO(sgjesse): Support digest.
 | 
| +        if (cr.scheme == _AuthenticationScheme.BASIC) {
 | 
| +          // Drain body and retry.
 | 
| +          return reduce(null, (x, y) {}).then((_) {
 | 
| +              return _httpClient._openUrlFromRequest(_httpRequest.method,
 | 
| +                                                     _httpRequest.uri,
 | 
| +                                                     _httpRequest)
 | 
| +                  .then((request) => request.close());
 | 
| +            });
 | 
| +        }
 | 
| +      }
 | 
|  
 | 
| -  void _streamSetErrorHandler(callback(e)) {
 | 
| -    _streamErrorHandler = callback;
 | 
| +      // Fall through to here to perform normal response handling if
 | 
| +      // there is no sensible authorization handling.
 | 
| +      return new Future.immediate(this);
 | 
| +    }
 | 
| +
 | 
| +    List<String> challenge = headers[HttpHeaders.WWW_AUTHENTICATE];
 | 
| +    assert(challenge != null || challenge.length == 1);
 | 
| +    _HeaderValue header =
 | 
| +        new _HeaderValue.fromString(challenge[0], parameterSeparator: ",");
 | 
| +    _AuthenticationScheme scheme =
 | 
| +        new _AuthenticationScheme.fromString(header.value);
 | 
| +    String realm = header.parameters["realm"];
 | 
| +
 | 
| +    // See if any credentials are available.
 | 
| +    _Credentials cr = _httpClient._findCredentials(_httpRequest.uri, scheme);
 | 
| +
 | 
| +    if (cr != null && !cr.used) {
 | 
| +      // If credentials found prepare for retrying the request.
 | 
| +      return retryWithCredentials(cr);
 | 
| +    }
 | 
| +
 | 
| +    // 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) {
 | 
| +      _httpClient._removeCredentials(cr);
 | 
| +      cr = null;
 | 
| +    }
 | 
| +    if (_httpClient._authenticate != null) {
 | 
| +      Future authComplete = _httpClient._authenticate(_httpRequest.uri,
 | 
| +                                                      scheme.toString(),
 | 
| +                                                      realm);
 | 
| +      return authComplete.then((credsAvailable) {
 | 
| +        if (credsAvailable) {
 | 
| +          cr = _httpClient._findCredentials(_httpRequest.uri, scheme);
 | 
| +          return retryWithCredentials(cr);
 | 
| +        } else {
 | 
| +          // No credentials available, complete with original response.
 | 
| +          return this;
 | 
| +        }
 | 
| +      });
 | 
| +    }
 | 
| +    // No credentials were found and the callback was not set.
 | 
| +    return new Future.immediate(this);
 | 
|    }
 | 
| -
 | 
| -  String _method;
 | 
| -  String _uri;
 | 
| -  String _path;
 | 
| -  String _queryString;
 | 
| -  Map<String, String> _queryParameters;
 | 
| -  _HttpInputStream _inputStream;
 | 
| -  _BufferList _buffer;
 | 
| -  Function _streamErrorHandler;
 | 
| -  _HttpSession _session;
 | 
|  }
 | 
|  
 | 
|  
 | 
| -// HTTP response object for sending a HTTP response.
 | 
| -class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
 | 
| -  _HttpResponse(_HttpConnection httpConnection)
 | 
| -      : super(httpConnection),
 | 
| -        _statusCode = HttpStatus.OK {
 | 
| -    _headers = new _HttpHeaders();
 | 
| -  }
 | 
| +class _HttpOutboundMessage<T> extends IOSink {
 | 
| +  // Used to mark when the body should be written. This is used for HEAD
 | 
| +  // requests and in error handling.
 | 
| +  bool _ignoreBody = false;
 | 
| +
 | 
| +  _HttpOutboundMessage(String protocolVersion, _HttpOutgoing outgoing)
 | 
| +      : super(outgoing),
 | 
| +        _outgoing = outgoing,
 | 
| +        headers = new _HttpHeaders(protocolVersion);
 | 
|  
 | 
| +  int get contentLength => headers.contentLength;
 | 
|    void set contentLength(int contentLength) {
 | 
| -    if (_state >= _HttpRequestResponseBase.HEADER_SENT) {
 | 
| -      throw new HttpException("Header already sent");
 | 
| -    }
 | 
| -    _headers.contentLength = contentLength;
 | 
| +    headers.contentLength = contentLength;
 | 
|    }
 | 
|  
 | 
| -  int get statusCode => _statusCode;
 | 
| -  void set statusCode(int statusCode) {
 | 
| -    if (_outputStream != null) throw new HttpException("Header already sent");
 | 
| -    _statusCode = statusCode;
 | 
| +  bool get persistentConnection => headers.persistentConnection;
 | 
| +  bool set persistentConnection(bool p) {
 | 
| +    headers.persistentConnection = p;
 | 
|    }
 | 
|  
 | 
| -  String get reasonPhrase => _findReasonPhrase(_statusCode);
 | 
| -  void set reasonPhrase(String reasonPhrase) {
 | 
| -    if (_outputStream != null) throw new HttpException("Header already sent");
 | 
| -    _reasonPhrase = reasonPhrase;
 | 
| +  Future<T> consume(Stream<List<int>> stream) {
 | 
| +    _writeHeaders();
 | 
| +    if (_ignoreBody) return new Future.immediate(this);
 | 
| +    if (_chunked) {
 | 
| +      // Transform when chunked.
 | 
| +      stream = stream.transform(new _ChunkedTransformer());
 | 
| +    }
 | 
| +    return super.consume(stream).then((_) => this);
 | 
|    }
 | 
|  
 | 
| -  List<Cookie> get cookies {
 | 
| -    if (_cookies == null) _cookies = new List<Cookie>();
 | 
| -    return _cookies;
 | 
| +  void add(List<int> data) {
 | 
| +    _writeHeaders();
 | 
| +    if (_ignoreBody) return;
 | 
| +    if (_chunked) {
 | 
| +      _ChunkedTransformer._addChunk(data, super.add);
 | 
| +    } else {
 | 
| +      super.add(data);
 | 
| +    }
 | 
|    }
 | 
|  
 | 
| -  OutputStream get outputStream {
 | 
| -    if (_state >= _HttpRequestResponseBase.DONE) {
 | 
| -      throw new HttpException("Response closed");
 | 
| +  void close() {
 | 
| +    if (!_headersWritten && !_ignoreBody && headers.chunkedTransferEncoding) {
 | 
| +      // If no body was written, _ignoreBody is false (it's not a HEAD
 | 
| +      // request) and the content-length is unspecified, set contentLength to 0.
 | 
| +      headers.contentLength = 0;
 | 
|      }
 | 
| -    if (_outputStream == null) {
 | 
| -      _outputStream = new _HttpOutputStream(this);
 | 
| +    _writeHeaders();
 | 
| +    if (!_ignoreBody) {
 | 
| +      if (_chunked) {
 | 
| +        _ChunkedTransformer._addChunk([], super.add);
 | 
| +      }
 | 
|      }
 | 
| -    return _outputStream;
 | 
| +    super.close();
 | 
|    }
 | 
|  
 | 
| -  DetachedSocket detachSocket() {
 | 
| -    if (_state >= _HttpRequestResponseBase.DONE) {
 | 
| -      throw new HttpException("Response closed");
 | 
| +  void _writeHeaders() {
 | 
| +    if (_headersWritten) return;
 | 
| +    bool _tmpIgnoreBody = _ignoreBody;
 | 
| +    _ignoreBody = false;
 | 
| +    _headersWritten = true;
 | 
| +    _writeHeader();
 | 
| +    _ignoreBody = _tmpIgnoreBody;
 | 
| +    if (_ignoreBody) {
 | 
| +      super.close();
 | 
| +      return;
 | 
|      }
 | 
| -    // Ensure that headers are written.
 | 
| -    if (_state == _HttpRequestResponseBase.START) {
 | 
| -      _writeHeader();
 | 
| +    _chunked = headers.chunkedTransferEncoding;
 | 
| +    if (!_chunked) {
 | 
| +      _outgoing.setTransferLength(headers.contentLength);
 | 
|      }
 | 
| -    _state = _HttpRequestResponseBase.UPGRADED;
 | 
| -    // Ensure that any trailing data is written.
 | 
| -    _writeDone();
 | 
| -    // Indicate to the connection that the response handling is done.
 | 
| -    return _httpConnection._detachSocket();
 | 
|    }
 | 
|  
 | 
| -  // Delegate functions for the HttpOutputStream implementation.
 | 
| -  bool _streamWrite(List<int> buffer, bool copyBuffer) {
 | 
| -    if (_done) throw new HttpException("Response closed");
 | 
| -    return _write(buffer, copyBuffer);
 | 
| +  void _writeHeader();  // TODO(ajohnsen): Better name.
 | 
| +
 | 
| +  final _HttpHeaders headers;
 | 
| +
 | 
| +  final _HttpOutgoing _outgoing;
 | 
| +  bool _headersWritten = false;
 | 
| +  bool _chunked = false;
 | 
| +}
 | 
| +
 | 
| +
 | 
| +class _HttpResponse extends _HttpOutboundMessage<HttpResponse>
 | 
| +    implements HttpResponse {
 | 
| +  int statusCode = 200;
 | 
| +  String _reasonPhrase;
 | 
| +  List<Cookie> _cookies;
 | 
| +  _HttpRequest _httpRequest;
 | 
| +
 | 
| +  _HttpResponse(String protocolVersion,
 | 
| +                _HttpOutgoing _outgoing)
 | 
| +      : super(protocolVersion, _outgoing);
 | 
| +
 | 
| +  List<Cookie> get cookies {
 | 
| +    if (_cookies == null) _cookies = new List<Cookie>();
 | 
| +    return _cookies;
 | 
|    }
 | 
|  
 | 
| -  bool _streamWriteFrom(List<int> buffer, int offset, int len) {
 | 
| -    if (_done) throw new HttpException("Response closed");
 | 
| -    return _writeList(buffer, offset, len);
 | 
| +  String get reasonPhrase => _findReasonPhrase(statusCode);
 | 
| +  void set reasonPhrase(String reasonPhrase) {
 | 
| +    if (_headersWritten) throw new StateError("Header already sent");
 | 
| +    _reasonPhrase = reasonPhrase;
 | 
|    }
 | 
|  
 | 
| -  void _streamFlush() {
 | 
| -    _httpConnection._flush();
 | 
| +  Future<Socket> detachSocket() {
 | 
| +    if (_headersWritten) throw new StateError("Headers already sent");
 | 
| +    _writeHeaders();
 | 
| +    var future = _httpRequest._httpConnection.detachSocket();
 | 
| +    // Close connection so the socket is 'free'.
 | 
| +    close();
 | 
| +    return future;
 | 
|    }
 | 
|  
 | 
| -  void _streamClose() {
 | 
| -    _ensureHeadersSent();
 | 
| -    _state = _HttpRequestResponseBase.DONE;
 | 
| -    // Stop tracking no pending write events.
 | 
| -    _httpConnection._onNoPendingWrites = null;
 | 
| -    // Ensure that any trailing data is written.
 | 
| -    _writeDone();
 | 
| -    // Indicate to the connection that the response handling is done.
 | 
| -    _httpConnection._responseClosed();
 | 
| -    if (_streamClosedHandler != null) {
 | 
| -      Timer.run(_streamClosedHandler);
 | 
| +  HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo;
 | 
| +
 | 
| +  void _writeHeader() {
 | 
| +    writeSP() => add([_CharCode.SP]);
 | 
| +    writeCRLF() => add([_CharCode.CR, _CharCode.LF]);
 | 
| +
 | 
| +    // Write status line.
 | 
| +    if (headers.protocolVersion == "1.1") {
 | 
| +      add(_Const.HTTP11);
 | 
| +    } else {
 | 
| +      add(_Const.HTTP10);
 | 
|      }
 | 
| -  }
 | 
| +    writeSP();
 | 
| +    addString(statusCode.toString());
 | 
| +    writeSP();
 | 
| +    addString(reasonPhrase);
 | 
| +    writeCRLF();
 | 
|  
 | 
| -  void _streamSetNoPendingWriteHandler(callback()) {
 | 
| -    if (_state != _HttpRequestResponseBase.DONE) {
 | 
| -      _httpConnection._onNoPendingWrites = callback;
 | 
| +    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;
 | 
| +          cookies[i].httpOnly = true;
 | 
| +          found = true;
 | 
| +          break;
 | 
| +        }
 | 
| +      }
 | 
| +      if (!found) {
 | 
| +        cookies.add(new Cookie(_DART_SESSION_ID, session.id)..httpOnly = true);
 | 
| +      }
 | 
| +    }
 | 
| +    // Add all the cookies set to the headers.
 | 
| +    if (_cookies != null) {
 | 
| +      _cookies.forEach((cookie) {
 | 
| +        headers.add("set-cookie", cookie);
 | 
| +      });
 | 
|      }
 | 
| -  }
 | 
|  
 | 
| -  void _streamSetClosedHandler(callback()) {
 | 
| -    _streamClosedHandler = callback;
 | 
| -  }
 | 
| +    headers._finalize();
 | 
|  
 | 
| -  void _streamSetErrorHandler(callback(e)) {
 | 
| -    _streamErrorHandler = callback;
 | 
| +    // Write headers.
 | 
| +    headers._write(this);
 | 
| +    writeCRLF();
 | 
|    }
 | 
|  
 | 
|    String _findReasonPhrase(int statusCode) {
 | 
| @@ -574,1231 +531,915 @@ class _HttpResponse extends _HttpRequestResponseBase implements HttpResponse {
 | 
|        default: return "Status $statusCode";
 | 
|      }
 | 
|    }
 | 
| -
 | 
| -  bool _writeHeader() {
 | 
| -    List<int> data;
 | 
| -
 | 
| -    // Write status line.
 | 
| -    if (_protocolVersion == "1.1") {
 | 
| -      _httpConnection._write(_Const.HTTP11);
 | 
| -    } else {
 | 
| -      _httpConnection._write(_Const.HTTP10);
 | 
| -    }
 | 
| -    _writeSP();
 | 
| -    data = _statusCode.toString().charCodes;
 | 
| -    _httpConnection._write(data);
 | 
| -    _writeSP();
 | 
| -    data = reasonPhrase.charCodes;
 | 
| -    _httpConnection._write(data);
 | 
| -    _writeCRLF();
 | 
| -
 | 
| -    var session = _httpConnection._request._session;
 | 
| -    if (session != null && !session._destroyed) {
 | 
| -      // 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;
 | 
| -          cookies[i].httpOnly = true;
 | 
| -          found = true;
 | 
| -          break;
 | 
| -        }
 | 
| -      }
 | 
| -      if (!found) {
 | 
| -        cookies.add(new Cookie(_DART_SESSION_ID, session.id)..httpOnly = true);
 | 
| -      }
 | 
| -    }
 | 
| -    // Add all the cookies set to the headers.
 | 
| -    if (_cookies != null) {
 | 
| -      _cookies.forEach((cookie) {
 | 
| -        _headers.add("set-cookie", cookie);
 | 
| -      });
 | 
| -    }
 | 
| -
 | 
| -    // Write headers.
 | 
| -    _headers._finalize(_protocolVersion);
 | 
| -    bool allWritten = _writeHeaders();
 | 
| -    _state = _HttpRequestResponseBase.HEADER_SENT;
 | 
| -    return allWritten;
 | 
| -  }
 | 
| -
 | 
| -  int _statusCode;  // Response status code.
 | 
| -  String _reasonPhrase;  // Response reason phrase.
 | 
| -  _HttpOutputStream _outputStream;
 | 
| -  Function _streamClosedHandler;
 | 
| -  Function _streamErrorHandler;
 | 
|  }
 | 
|  
 | 
|  
 | 
| -class _HttpInputStream extends _BaseDataInputStream implements InputStream {
 | 
| -  _HttpInputStream(_HttpRequestResponseBase this._requestOrResponse) {
 | 
| -    _checkScheduleCallbacks();
 | 
| -  }
 | 
| -
 | 
| -  int available() {
 | 
| -    return _requestOrResponse._streamAvailable();
 | 
| -  }
 | 
| -
 | 
| -  void pipe(OutputStream output, {bool close: true}) {
 | 
| -    _pipe(this, output, close: close);
 | 
| -  }
 | 
| -
 | 
| -  List<int> _read(int bytesToRead) {
 | 
| -    List<int> result = _requestOrResponse._streamRead(bytesToRead);
 | 
| -    _checkScheduleCallbacks();
 | 
| -    return result;
 | 
| -  }
 | 
| -
 | 
| -  void set onError(void callback(e)) {
 | 
| -    _requestOrResponse._streamSetErrorHandler(callback);
 | 
| -  }
 | 
| -
 | 
| -  int _readInto(List<int> buffer, int offset, int len) {
 | 
| -    int result = _requestOrResponse._streamReadInto(buffer, offset, len);
 | 
| -    _checkScheduleCallbacks();
 | 
| -    return result;
 | 
| -  }
 | 
| -
 | 
| -  void _close() {
 | 
| -    // TODO(sgjesse): Handle this.
 | 
| -  }
 | 
| -
 | 
| -  void _dataReceived() {
 | 
| -    super._dataReceived();
 | 
| -  }
 | 
| -
 | 
| -  _HttpRequestResponseBase _requestOrResponse;
 | 
| -}
 | 
| -
 | 
| -
 | 
| -class _HttpOutputStream extends _BaseOutputStream implements OutputStream {
 | 
| -  _HttpOutputStream(_HttpRequestResponseBase this._requestOrResponse);
 | 
| -
 | 
| -  bool write(List<int> buffer, [bool copyBuffer = true]) {
 | 
| -    return _requestOrResponse._streamWrite(buffer, copyBuffer);
 | 
| -  }
 | 
| -
 | 
| -  bool writeFrom(List<int> buffer, [int offset = 0, int len]) {
 | 
| -    if (offset < 0 || offset >= buffer.length) throw new ArgumentError();
 | 
| -    len = len != null ? len : buffer.length - offset;
 | 
| -    if (len < 0) throw new ArgumentError();
 | 
| -    return _requestOrResponse._streamWriteFrom(buffer, offset, len);
 | 
| -  }
 | 
| -
 | 
| -  void flush() {
 | 
| -    _requestOrResponse._streamFlush();
 | 
| -  }
 | 
| -
 | 
| -  void close() {
 | 
| -    _requestOrResponse._streamClose();
 | 
| -  }
 | 
| -
 | 
| -  bool get closed => _requestOrResponse._done;
 | 
| -
 | 
| -  void destroy() {
 | 
| -    throw "Not implemented";
 | 
| -  }
 | 
| -
 | 
| -  void set onNoPendingWrites(void callback()) {
 | 
| -    _requestOrResponse._streamSetNoPendingWriteHandler(callback);
 | 
| -  }
 | 
| -
 | 
| -  void set onClosed(void callback()) {
 | 
| -    _requestOrResponse._streamSetClosedHandler(callback);
 | 
| -  }
 | 
| -
 | 
| -  void set onError(void callback(e)) {
 | 
| -    _requestOrResponse._streamSetErrorHandler(callback);
 | 
| -  }
 | 
| -
 | 
| -  _HttpRequestResponseBase _requestOrResponse;
 | 
| -}
 | 
| -
 | 
| -
 | 
| -abstract class _HttpConnectionBase {
 | 
| -  static const int IDLE = 0;
 | 
| -  static const int ACTIVE = 1;
 | 
| -  static const int CLOSING = 2;
 | 
| -  static const int REQUEST_DONE = 4;
 | 
| -  static const int RESPONSE_DONE = 8;
 | 
| -  static const int ALL_DONE = REQUEST_DONE | RESPONSE_DONE;
 | 
| -  static const int READ_CLOSED = 16;
 | 
| -  static const int WRITE_CLOSED = 32;
 | 
| -  static const int FULLY_CLOSED = READ_CLOSED | WRITE_CLOSED;
 | 
| -
 | 
| -  _HttpConnectionBase() : hashCode = _nextHashCode {
 | 
| -    _nextHashCode = (_nextHashCode + 1) & 0xFFFFFFF;
 | 
| -  }
 | 
| -
 | 
| -  bool get _isIdle => (_state & ACTIVE) == 0;
 | 
| -  bool get _isActive => (_state & ACTIVE) == ACTIVE;
 | 
| -  bool get _isClosing => (_state & CLOSING) == CLOSING;
 | 
| -  bool get _isRequestDone => (_state & REQUEST_DONE) == REQUEST_DONE;
 | 
| -  bool get _isResponseDone => (_state & RESPONSE_DONE) == RESPONSE_DONE;
 | 
| -  bool get _isAllDone => (_state & ALL_DONE) == ALL_DONE;
 | 
| -  bool get _isReadClosed => (_state & READ_CLOSED) == READ_CLOSED;
 | 
| -  bool get _isWriteClosed => (_state & WRITE_CLOSED) == WRITE_CLOSED;
 | 
| -  bool get _isFullyClosed => (_state & FULLY_CLOSED) == FULLY_CLOSED;
 | 
| -
 | 
| -  void _connectionEstablished(Socket socket) {
 | 
| -    _socket = socket;
 | 
| -    // Register handlers for socket events. All socket events are
 | 
| -    // passed to the HTTP parser.
 | 
| -    _socket.onData = () {
 | 
| -      List<int> buffer = _socket.read();
 | 
| -      if (buffer != null) {
 | 
| -        _httpParser.streamData(buffer);
 | 
| -      }
 | 
| -    };
 | 
| -    _socket.onClosed = _httpParser.streamDone;
 | 
| -    _socket.onError = _httpParser.streamError;
 | 
| -    _socket.outputStream.onError = _httpParser.streamError;
 | 
| -  }
 | 
| -
 | 
| -  bool _write(List<int> data, [bool copyBuffer = false]);
 | 
| -  bool _writeFrom(List<int> buffer, [int offset, int len]);
 | 
| -  bool _flush();
 | 
| -  bool _close();
 | 
| -  bool _destroy();
 | 
| -  DetachedSocket _detachSocket();
 | 
| -
 | 
| -  HttpConnectionInfo get connectionInfo {
 | 
| -    if (_socket == null) return null;
 | 
| -    try {
 | 
| -      _HttpConnectionInfo info = new _HttpConnectionInfo();
 | 
| -      info.remoteHost = _socket.remoteHost;
 | 
| -      info.remotePort = _socket.remotePort;
 | 
| -      info.localPort = _socket.port;
 | 
| -      return info;
 | 
| -    } catch (e) { }
 | 
| -    return null;
 | 
| -  }
 | 
| -
 | 
| -  void set _onNoPendingWrites(void callback()) {
 | 
| -    _socket.outputStream.onNoPendingWrites = callback;
 | 
| -  }
 | 
| -
 | 
| -  int _state = IDLE;
 | 
| +class _HttpClientRequest extends _HttpOutboundMessage<HttpClientRequest>
 | 
| +    implements HttpClientRequest {
 | 
| +  final String method;
 | 
| +  final Uri uri;
 | 
| +  final List<Cookie> cookies = new List<Cookie>();
 | 
|  
 | 
| -  Socket _socket;
 | 
| -  _HttpParser _httpParser;
 | 
| +  // The HttpClient this request belongs to.
 | 
| +  final _HttpClient _httpClient;
 | 
| +  final _HttpClientConnection _httpClientConnection;
 | 
|  
 | 
| -  // Callbacks.
 | 
| -  Function onDetach;
 | 
| -  Function onClosed;
 | 
| +  final Completer<HttpClientResponse> _responseCompleter
 | 
| +      = new Completer<HttpClientResponse>();
 | 
|  
 | 
| -  // Hash code for HTTP connection. Currently this is just a counter.
 | 
| -  final int hashCode;
 | 
| -  static int _nextHashCode = 0;
 | 
| -}
 | 
| +  final bool _usingProxy;
 | 
|  
 | 
| +  // TODO(ajohnsen): Get default value from client?
 | 
| +  bool _followRedirects = true;
 | 
|  
 | 
| -// HTTP server connection over a socket.
 | 
| -class _HttpConnection extends _HttpConnectionBase {
 | 
| -  _HttpConnection(HttpServer this._server) {
 | 
| -    _httpParser = new _HttpParser.requestParser();
 | 
| -    // Register HTTP parser callbacks.
 | 
| -    _httpParser.requestStart = _onRequestReceived;
 | 
| -    _httpParser.dataReceived = _onDataReceived;
 | 
| -    _httpParser.dataEnd = _onDataEnd;
 | 
| -    _httpParser.error = _onError;
 | 
| -    _httpParser.closed = _onClosed;
 | 
| -    _httpParser.responseStart = (statusCode, reasonPhrase, version) {
 | 
| -      assert(false);
 | 
| -    };
 | 
| -  }
 | 
| -
 | 
| -  void _bufferData(List<int> data, [bool copyBuffer = false]) {
 | 
| -    if (_buffer == null) _buffer = new _BufferList();
 | 
| -    if (copyBuffer) data = data.getRange(0, data.length);
 | 
| -    _buffer.add(data);
 | 
| -  }
 | 
| -
 | 
| -  void _writeBufferedResponse() {
 | 
| -    if (_buffer != null) {
 | 
| -      while (!_buffer.isEmpty) {
 | 
| -        var data = _buffer.first;
 | 
| -        _socket.outputStream.write(data, false);
 | 
| -        _buffer.removeBytes(data.length);
 | 
| -      }
 | 
| -      _buffer = null;
 | 
| -    }
 | 
| -  }
 | 
| +  int _maxRedirects = 5;
 | 
|  
 | 
| -  bool _write(List<int> data, [bool copyBuffer = false]) {
 | 
| -    if (_isRequestDone || !_hasBody || _httpParser.upgrade) {
 | 
| -      return _socket.outputStream.write(data, copyBuffer);
 | 
| -    } else {
 | 
| -      _bufferData(data, copyBuffer);
 | 
| -      return false;
 | 
| -    }
 | 
| -  }
 | 
| +  List<RedirectInfo> _responseRedirects = [];
 | 
|  
 | 
| -  bool _writeFrom(List<int> data, [int offset, int len]) {
 | 
| -    if (_isRequestDone || !_hasBody || _httpParser.upgrade) {
 | 
| -      return _socket.outputStream.writeFrom(data, offset, len);
 | 
| -    } else {
 | 
| -      if (offset == null) offset = 0;
 | 
| -      if (len == null) len = buffer.length - offset;
 | 
| -      _bufferData(data.getRange(offset, len), false);
 | 
| -      return false;
 | 
| +  _HttpClientRequest(_HttpOutgoing outgoing,
 | 
| +                     Uri this.uri,
 | 
| +                     String this.method,
 | 
| +                     bool this._usingProxy,
 | 
| +                     _HttpClient this._httpClient,
 | 
| +                     _HttpClientConnection this._httpClientConnection)
 | 
| +      : super("1.1", outgoing) {
 | 
| +    // GET and HEAD have 'content-length: 0' by default.
 | 
| +    if (method == "GET" || method == "HEAD") {
 | 
| +      contentLength = 0;
 | 
|      }
 | 
|    }
 | 
|  
 | 
| -  bool _flush() {
 | 
| -    _socket.outputStream.flush();
 | 
| -  }
 | 
| -
 | 
| -  bool _close() {
 | 
| -    _socket.outputStream.close();
 | 
| -  }
 | 
| +  Future<HttpClientResponse> get response => _responseCompleter.future;
 | 
|  
 | 
| -  bool _destroy() {
 | 
| -    _socket.close();
 | 
| +  Future<HttpClientResponse> close() {
 | 
| +    super.close();
 | 
| +    return response;
 | 
|    }
 | 
|  
 | 
| -  void _onClosed() {
 | 
| -    _state |= _HttpConnectionBase.READ_CLOSED;
 | 
| -    _checkDone();
 | 
| +  int get maxRedirects => _maxRedirects;
 | 
| +  void set maxRedirects(int maxRedirects) {
 | 
| +    if (_headersWritten) throw new StateError("Request already sent");
 | 
| +    _maxRedirects = maxRedirects;
 | 
|    }
 | 
|  
 | 
| -  DetachedSocket _detachSocket() {
 | 
| -    _socket.onData = null;
 | 
| -    _socket.onClosed = null;
 | 
| -    _socket.onError = null;
 | 
| -    _socket.outputStream.onNoPendingWrites = null;
 | 
| -    _writeBufferedResponse();
 | 
| -    Socket socket = _socket;
 | 
| -    _socket = null;
 | 
| -    if (onDetach != null) onDetach();
 | 
| -    return new _DetachedSocket(socket, _httpParser.readUnparsedData());
 | 
| +  bool get followRedirects => _followRedirects;
 | 
| +  void set followRedirects(bool followRedirects) {
 | 
| +    if (_headersWritten) throw new StateError("Request already sent");
 | 
| +    _followRedirects = followRedirects;
 | 
|    }
 | 
|  
 | 
| -  void _onError(e) {
 | 
| -    // Don't report errors for a request parser when HTTP parser is in
 | 
| -    // idle state. Clients can close the connection and cause a
 | 
| -    // connection reset by peer error which is OK.
 | 
| -    _onClosed();
 | 
| -    if (_state == _HttpConnectionBase.IDLE) return;
 | 
| +  HttpConnectionInfo get connectionInfo => _httpClientConnection.connectionInfo;
 | 
|  
 | 
| -    // Propagate the error to the streams.
 | 
| -    if (_request != null &&
 | 
| -        !_isRequestDone &&
 | 
| -        _request._streamErrorHandler != null) {
 | 
| -      _request._streamErrorHandler(e);
 | 
| -    } else if (_response != null &&
 | 
| -        !_isResponseDone &&
 | 
| -        _response._streamErrorHandler != null) {
 | 
| -      _response._streamErrorHandler(e);
 | 
| -    } else {
 | 
| -      onError(e);
 | 
| -    }
 | 
| -    if (_socket != null) _socket.close();
 | 
| -  }
 | 
| -
 | 
| -  void _onRequestReceived(String method,
 | 
| -                          String uri,
 | 
| -                          String version,
 | 
| -                          _HttpHeaders headers,
 | 
| -                          bool hasBody) {
 | 
| -    _state = _HttpConnectionBase.ACTIVE;
 | 
| -    // Create new request and response objects for this request.
 | 
| -    _request = new _HttpRequest(this);
 | 
| -    _response = new _HttpResponse(this);
 | 
| -    _request._onRequestReceived(method, uri, version, headers);
 | 
| -    _request._protocolVersion = version;
 | 
| -    _response._protocolVersion = version;
 | 
| -    _response._headResponse = method == "HEAD";
 | 
| -    _response.persistentConnection = _httpParser.persistentConnection;
 | 
| -    _hasBody = hasBody;
 | 
| -    if (onRequestReceived != null) {
 | 
| -      onRequestReceived(_request, _response);
 | 
| -    }
 | 
| -    _checkDone();
 | 
| -  }
 | 
| -
 | 
| -  void _onDataReceived(List<int> data) {
 | 
| -    _request._onDataReceived(data);
 | 
| -    _checkDone();
 | 
| -  }
 | 
| -
 | 
| -  void _checkDone() {
 | 
| -    if (_isReadClosed) {
 | 
| -      // If the client closes the conversation is ended.
 | 
| -      _server._closeQueue.add(this);
 | 
| -    } else if (_isAllDone) {
 | 
| -      // If we are done writing the response, and the connection is
 | 
| -      // not persistent, we must close. Also if using HTTP 1.0 and the
 | 
| -      // content length was not known we must close to indicate end of
 | 
| -      // body.
 | 
| -      bool close =
 | 
| -          !_response.persistentConnection ||
 | 
| -          (_response._protocolVersion == "1.0" && _response.contentLength < 0);
 | 
| -      _request = null;
 | 
| -      _response = null;
 | 
| -      if (close) {
 | 
| -        _httpParser.cancel();
 | 
| -        _server._closeQueue.add(this);
 | 
| +  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.reduce(null, (x, y) {})
 | 
| +          .then((_) => response.redirect());
 | 
|        } else {
 | 
| -        _state = _HttpConnectionBase.IDLE;
 | 
| -      }
 | 
| -    } else if (_isResponseDone && _hasBody) {
 | 
| -      // If the response is closed before the request is fully read
 | 
| -      // close this connection. If there is buffered output
 | 
| -      // (e.g. error response for invalid request where the server did
 | 
| -      // not care to read the request body) this is send.
 | 
| -      assert(!_isRequestDone);
 | 
| -      _writeBufferedResponse();
 | 
| -      _httpParser.cancel();
 | 
| -      _server._closeQueue.add(this);
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  void _onDataEnd(bool close) {
 | 
| -    // Start sending queued response if any.
 | 
| -    _state |= _HttpConnectionBase.REQUEST_DONE;
 | 
| -    _writeBufferedResponse();
 | 
| -    _request._onDataEnd();
 | 
| -    _checkDone();
 | 
| -  }
 | 
| -
 | 
| -  void _responseClosed() {
 | 
| -    _state |= _HttpConnectionBase.RESPONSE_DONE;
 | 
| -  }
 | 
| -
 | 
| -  HttpServer _server;
 | 
| -  HttpRequest _request;
 | 
| -  HttpResponse _response;
 | 
| -  bool _hasBody = false;
 | 
| -
 | 
| -  // Buffer for data written before full response has been processed.
 | 
| -  _BufferList _buffer;
 | 
| -
 | 
| -  // Callbacks.
 | 
| -  Function onRequestReceived;
 | 
| -  Function onError;
 | 
| -}
 | 
| -
 | 
| -
 | 
| -class _RequestHandlerRegistration {
 | 
| -  _RequestHandlerRegistration(Function this._matcher, Function this._handler);
 | 
| -  Function _matcher;
 | 
| -  Function _handler;
 | 
| -}
 | 
| -
 | 
| -// HTTP server waiting for socket connections. The connections are
 | 
| -// managed by the server and as requests are received the request.
 | 
| -// HTTPS connections are also supported, if the _HttpServer.httpsServer
 | 
| -// constructor is used and a certificate name is provided in listen,
 | 
| -// or a SecureServerSocket is provided to listenOn.
 | 
| -class _HttpServer implements HttpServer, HttpsServer {
 | 
| -  _HttpServer() : this._internal(isSecure: false);
 | 
| -
 | 
| -  _HttpServer.httpsServer() : this._internal(isSecure: true);
 | 
| -
 | 
| -  _HttpServer._internal({ bool isSecure: false })
 | 
| -      : _secure = isSecure,
 | 
| -        _connections = new Set<_HttpConnection>(),
 | 
| -        _handlers = new List<_RequestHandlerRegistration>(),
 | 
| -        _closeQueue = new _CloseQueue();
 | 
| -
 | 
| -  void listen(String host,
 | 
| -              int port,
 | 
| -              {int backlog: 128,
 | 
| -               String certificate_name,
 | 
| -               bool requestClientCertificate: false}) {
 | 
| -    if (_secure) {
 | 
| -      listenOn(new SecureServerSocket(
 | 
| -          host,
 | 
| -          port,
 | 
| -          backlog,
 | 
| -          certificate_name,
 | 
| -          requestClientCertificate: requestClientCertificate));
 | 
| -    } else {
 | 
| -      listenOn(new ServerSocket(host, port, backlog));
 | 
| -    }
 | 
| -    _closeServer = true;
 | 
| -  }
 | 
| -
 | 
| -  void listenOn(ServerSocket serverSocket) {
 | 
| -    if (_secure && serverSocket is! SecureServerSocket) {
 | 
| -        throw new HttpException(
 | 
| -            'HttpsServer.listenOn was called with non-secure server socket');
 | 
| -    } else if (!_secure && serverSocket is SecureServerSocket) {
 | 
| -        throw new HttpException(
 | 
| -            'HttpServer.listenOn was called with a secure server socket');
 | 
| -    }
 | 
| -    void onConnection(Socket socket) {
 | 
| -      // Accept the client connection.
 | 
| -      _HttpConnection connection = new _HttpConnection(this);
 | 
| -      connection._connectionEstablished(socket);
 | 
| -      _connections.add(connection);
 | 
| -      connection.onRequestReceived = _handleRequest;
 | 
| -      connection.onClosed = () => _connections.remove(connection);
 | 
| -      connection.onDetach = () => _connections.remove(connection);
 | 
| -      connection.onError = (e) {
 | 
| -        _connections.remove(connection);
 | 
| -        if (_onError != null) {
 | 
| -          _onError(e);
 | 
| -        } else {
 | 
| -          throw(e);
 | 
| -        }
 | 
| -      };
 | 
| -    }
 | 
| -    serverSocket.onConnection = onConnection;
 | 
| -    _server = serverSocket;
 | 
| -    _closeServer = false;
 | 
| -  }
 | 
| -
 | 
| -  addRequestHandler(bool matcher(HttpRequest request),
 | 
| -                    void handler(HttpRequest request, HttpResponse response)) {
 | 
| -    _handlers.add(new _RequestHandlerRegistration(matcher, handler));
 | 
| -  }
 | 
| -
 | 
| -  void set defaultRequestHandler(
 | 
| -      void handler(HttpRequest request, HttpResponse response)) {
 | 
| -    _defaultHandler = handler;
 | 
| -  }
 | 
| -
 | 
| -  void close() {
 | 
| -    _closeQueue.shutdown();
 | 
| -    if (_sessionManagerInstance != null) {
 | 
| -      _sessionManagerInstance.close();
 | 
| -      _sessionManagerInstance = null;
 | 
| -    }
 | 
| -    if (_server != null && _closeServer) {
 | 
| -      _server.close();
 | 
| -    }
 | 
| -    _server = null;
 | 
| -    for (_HttpConnection connection in _connections) {
 | 
| -      connection._destroy();
 | 
| -    }
 | 
| -    _connections.clear();
 | 
| -  }
 | 
| -
 | 
| -  int get port {
 | 
| -    if (_server == null) {
 | 
| -      throw new HttpException("The HttpServer is not listening on a port.");
 | 
| -    }
 | 
| -    return _server.port;
 | 
| -  }
 | 
| -
 | 
| -  void set onError(void callback(e)) {
 | 
| -    _onError = callback;
 | 
| -  }
 | 
| -
 | 
| -  set sessionTimeout(int timeout) {
 | 
| -    _sessionManager.sessionTimeout = timeout;
 | 
| -  }
 | 
| -
 | 
| -  void _handleRequest(HttpRequest request, HttpResponse response) {
 | 
| -    for (int i = 0; i < _handlers.length; i++) {
 | 
| -      if (_handlers[i]._matcher(request)) {
 | 
| -        Function handler = _handlers[i]._handler;
 | 
| -        try {
 | 
| -          handler(request, response);
 | 
| -        } catch (e) {
 | 
| -          if (_onError != null) {
 | 
| -            _onError(e);
 | 
| -          } else {
 | 
| -            throw e;
 | 
| -          }
 | 
| -        }
 | 
| -        return;
 | 
| +        // End with exception, too many redirects.
 | 
| +        future = response.reduce(null, (x, y) {})
 | 
| +            .then((_) => new Future.immediateError(
 | 
| +                new RedirectLimitExceededException(response.redirects)));
 | 
|        }
 | 
| -    }
 | 
| -
 | 
| -    if (_defaultHandler != null) {
 | 
| -      _defaultHandler(request, response);
 | 
| +    } else if (response._shouldAuthenticate) {
 | 
| +      future = response._authenticate();
 | 
|      } else {
 | 
| -      response.statusCode = HttpStatus.NOT_FOUND;
 | 
| -      response.contentLength = 0;
 | 
| -      response.outputStream.close();
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  _HttpSessionManager get _sessionManager {
 | 
| -    // Lazy init.
 | 
| -    if (_sessionManagerInstance == null) {
 | 
| -      _sessionManagerInstance = new _HttpSessionManager();
 | 
| -    }
 | 
| -    return _sessionManagerInstance;
 | 
| -  }
 | 
| -
 | 
| -  HttpConnectionsInfo connectionsInfo() {
 | 
| -    HttpConnectionsInfo result = new HttpConnectionsInfo();
 | 
| -    result.total = _connections.length;
 | 
| -    _connections.forEach((_HttpConnection conn) {
 | 
| -      if (conn._isActive) {
 | 
| -        result.active++;
 | 
| -      } else if (conn._isIdle) {
 | 
| -        result.idle++;
 | 
| -      } else {
 | 
| -        assert(result._isClosing);
 | 
| -        result.closing++;
 | 
| -      }
 | 
| -    });
 | 
| -    return result;
 | 
| -  }
 | 
| -
 | 
| -  ServerSocket _server;  // The server listen socket.
 | 
| -  bool _closeServer = false;
 | 
| -  bool _secure;
 | 
| -  Set<_HttpConnection> _connections;  // Set of currently connected clients.
 | 
| -  List<_RequestHandlerRegistration> _handlers;
 | 
| -  Object _defaultHandler;
 | 
| -  Function _onError;
 | 
| -  _CloseQueue _closeQueue;
 | 
| -  _HttpSessionManager _sessionManagerInstance;
 | 
| -}
 | 
| -
 | 
| -
 | 
| -class _HttpClientRequest
 | 
| -    extends _HttpRequestResponseBase implements HttpClientRequest {
 | 
| -  _HttpClientRequest(String this._method,
 | 
| -                     Uri this._uri,
 | 
| -                     _HttpClientConnection connection)
 | 
| -      : super(connection) {
 | 
| -    _headers = new _HttpHeaders();
 | 
| -    _connection = connection;
 | 
| -    // Default GET and HEAD requests to have no content.
 | 
| -    if (_method == "GET" || _method == "HEAD") {
 | 
| -      contentLength = 0;
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  void set contentLength(int contentLength) {
 | 
| -    if (_state >= _HttpRequestResponseBase.HEADER_SENT) {
 | 
| -      throw new HttpException("Header already sent");
 | 
| -    }
 | 
| -    _headers.contentLength = contentLength;
 | 
| -  }
 | 
| -
 | 
| -  List<Cookie> get cookies {
 | 
| -    if (_cookies == null) _cookies = new List<Cookie>();
 | 
| -    return _cookies;
 | 
| -  }
 | 
| -
 | 
| -  OutputStream get outputStream {
 | 
| -    if (_done) throw new HttpException("Request closed");
 | 
| -    if (_outputStream == null) {
 | 
| -      _outputStream = new _HttpOutputStream(this);
 | 
| -    }
 | 
| -    return _outputStream;
 | 
| -  }
 | 
| -
 | 
| -  // Delegate functions for the HttpOutputStream implementation.
 | 
| -  bool _streamWrite(List<int> buffer, bool copyBuffer) {
 | 
| -    if (_done) throw new HttpException("Request closed");
 | 
| -    _emptyBody = _emptyBody && buffer.length == 0;
 | 
| -    return _write(buffer, copyBuffer);
 | 
| -  }
 | 
| -
 | 
| -  bool _streamWriteFrom(List<int> buffer, int offset, int len) {
 | 
| -    if (_done) throw new HttpException("Request closed");
 | 
| -    _emptyBody = _emptyBody && buffer.length == 0;
 | 
| -    return _writeList(buffer, offset, len);
 | 
| -  }
 | 
| -
 | 
| -  void _streamFlush() {
 | 
| -    _httpConnection._flush();
 | 
| -  }
 | 
| -
 | 
| -  void _streamClose() {
 | 
| -    _ensureHeadersSent();
 | 
| -    _state = _HttpRequestResponseBase.DONE;
 | 
| -    // Stop tracking no pending write events.
 | 
| -    _httpConnection._onNoPendingWrites = null;
 | 
| -    // Ensure that any trailing data is written.
 | 
| -    _writeDone();
 | 
| -    _connection._requestClosed();
 | 
| -    if (_streamClosedHandler != null) {
 | 
| -      Timer.run(_streamClosedHandler);
 | 
| -    }
 | 
| -  }
 | 
| -
 | 
| -  void _streamSetNoPendingWriteHandler(callback()) {
 | 
| -    if (_state != _HttpRequestResponseBase.DONE) {
 | 
| -      _httpConnection._onNoPendingWrites = callback;
 | 
| +      future = new Future<HttpClientResponse>.immediate(response);
 | 
|      }
 | 
| +    future.then(
 | 
| +        (v) => _responseCompleter.complete(v),
 | 
| +        onError: (e) {
 | 
| +          _responseCompleter.completeError(e);
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _streamSetClosedHandler(callback()) {
 | 
| -    _streamClosedHandler = callback;
 | 
| -  }
 | 
| -
 | 
| -  void _streamSetErrorHandler(callback(e)) {
 | 
| -    _streamErrorHandler = callback;
 | 
| +  void _onError(AsyncError error) {
 | 
| +    _responseCompleter.completeError(error);
 | 
|    }
 | 
|  
 | 
|    void _writeHeader() {
 | 
| -    List<int> data;
 | 
| +    writeSP() => add([_CharCode.SP]);
 | 
| +    writeCRLF() => add([_CharCode.CR, _CharCode.LF]);
 | 
|  
 | 
| -    // Write request line.
 | 
| -    data = _method.toString().charCodes;
 | 
| -    _httpConnection._write(data);
 | 
| -    _writeSP();
 | 
| +    addString(method);
 | 
| +    writeSP();
 | 
|      // Send the path for direct connections and the whole URL for
 | 
|      // proxy connections.
 | 
| -    if (!_connection._usingProxy) {
 | 
| -      String path = _uri.path;
 | 
| +    if (!_usingProxy) {
 | 
| +      String path = uri.path;
 | 
|        if (path.length == 0) path = "/";
 | 
| -      if (_uri.query != "") {
 | 
| -        if (_uri.fragment != "") {
 | 
| -          path = "${path}?${_uri.query}#${_uri.fragment}";
 | 
| +      if (uri.query != "") {
 | 
| +        if (uri.fragment != "") {
 | 
| +          path = "${path}?${uri.query}#${uri.fragment}";
 | 
|          } else {
 | 
| -          path = "${path}?${_uri.query}";
 | 
| +          path = "${path}?${uri.query}";
 | 
|          }
 | 
|        }
 | 
| -      data = path.charCodes;
 | 
| +      addString(path);
 | 
|      } else {
 | 
| -      data = _uri.toString().charCodes;
 | 
| +      addString(uri.toString());
 | 
|      }
 | 
| -    _httpConnection._write(data);
 | 
| -    _writeSP();
 | 
| -    _httpConnection._write(_Const.HTTP11);
 | 
| -    _writeCRLF();
 | 
| +    writeSP();
 | 
| +    add(_Const.HTTP11);
 | 
| +    writeCRLF();
 | 
|  
 | 
|      // Add the cookies to the headers.
 | 
| -    if (_cookies != null) {
 | 
| +    if (!cookies.isEmpty) {
 | 
|        StringBuffer sb = new StringBuffer();
 | 
| -      for (int i = 0; i < _cookies.length; i++) {
 | 
| +      for (int i = 0; i < cookies.length; i++) {
 | 
|          if (i > 0) sb.add("; ");
 | 
| -        sb.add(_cookies[i].name);
 | 
| +        sb.add(cookies[i].name);
 | 
|          sb.add("=");
 | 
| -        sb.add(_cookies[i].value);
 | 
| +        sb.add(cookies[i].value);
 | 
|        }
 | 
| -      _headers.add("cookie", sb.toString());
 | 
| +      headers.add("cookie", sb.toString());
 | 
|      }
 | 
|  
 | 
| +    headers._finalize();
 | 
| +
 | 
|      // Write headers.
 | 
| -    _headers._finalize("1.1");
 | 
| -    _writeHeaders();
 | 
| -    _state = _HttpRequestResponseBase.HEADER_SENT;
 | 
| +    headers._write(this);
 | 
| +    writeCRLF();
 | 
|    }
 | 
| -
 | 
| -  String _method;
 | 
| -  Uri _uri;
 | 
| -  _HttpClientConnection _connection;
 | 
| -  _HttpOutputStream _outputStream;
 | 
| -  Function _streamClosedHandler;
 | 
| -  Function _streamErrorHandler;
 | 
| -  bool _emptyBody = true;
 | 
|  }
 | 
|  
 | 
| -class _HttpClientResponse
 | 
| -    extends _HttpRequestResponseBase
 | 
| -    implements HttpClientResponse {
 | 
| -  _HttpClientResponse(_HttpClientConnection connection)
 | 
| -      : super(connection) {
 | 
| -    _connection = connection;
 | 
| -  }
 | 
|  
 | 
| -  int get statusCode => _statusCode;
 | 
| -  String get reasonPhrase => _reasonPhrase;
 | 
| -
 | 
| -  bool get isRedirect {
 | 
| -    var method = _connection._request._method;
 | 
| -    if (method == "GET" || method == "HEAD") {
 | 
| -      return statusCode == HttpStatus.MOVED_PERMANENTLY ||
 | 
| -             statusCode == HttpStatus.FOUND ||
 | 
| -             statusCode == HttpStatus.SEE_OTHER ||
 | 
| -             statusCode == HttpStatus.TEMPORARY_REDIRECT;
 | 
| -    } else if (method == "POST") {
 | 
| -      return statusCode == HttpStatus.SEE_OTHER;
 | 
| +// Transformer that transforms data to HTTP Chunked Encoding.
 | 
| +class _ChunkedTransformer implements StreamTransformer<List<int>, List<int>> {
 | 
| +  final StreamController<List<int>> _controller
 | 
| +      = new StreamController<List<int>>();
 | 
| +
 | 
| +  Stream<List<int>> bind(Stream<List<int>> stream) {
 | 
| +    var subscription = stream.listen(
 | 
| +        (data) {
 | 
| +          if (data.length == 0) return;  // Avoid close on 0-bytes payload.
 | 
| +          _addChunk(data, _controller.add);
 | 
| +       },
 | 
| +       onDone: () {
 | 
| +          _addChunk([], _controller.add);
 | 
| +          _controller.close();
 | 
| +       });
 | 
| +    return _controller.stream;
 | 
| +  }
 | 
| +
 | 
| +  static void _addChunk(List<int> data, void add(List<int> data)) {
 | 
| +    add(_chunkHeader(data.length));
 | 
| +    if (data.length > 0) add(data);
 | 
| +    add(_chunkFooter);
 | 
| +  }
 | 
| +
 | 
| +  static List<int> _chunkHeader(int length) {
 | 
| +    const hexDigits = const [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
 | 
| +                             0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46];
 | 
| +    var header = [];
 | 
| +    if (length == 0) {
 | 
| +      header.add(hexDigits[length]);
 | 
| +    } else {
 | 
| +      while (length > 0) {
 | 
| +        header.insertRange(0, 1, hexDigits[length % 16]);
 | 
| +        length = length >> 4;
 | 
| +      }
 | 
|      }
 | 
| -    return false;
 | 
| +    header.add(_CharCode.CR);
 | 
| +    header.add(_CharCode.LF);
 | 
| +    return header;
 | 
|    }
 | 
|  
 | 
| -  List<Cookie> get cookies {
 | 
| -    if (_cookies != null) return _cookies;
 | 
| -    _cookies = new List<Cookie>();
 | 
| -    List<String> values = _headers["set-cookie"];
 | 
| -    if (values != null) {
 | 
| -      values.forEach((value) {
 | 
| -        _cookies.add(new Cookie.fromSetCookieValue(value));
 | 
| -      });
 | 
| -    }
 | 
| -    return _cookies;
 | 
| -  }
 | 
| +  // Footer is just a CRLF.
 | 
| +  static List<int> get _chunkFooter => const [_CharCode.CR, _CharCode.LF];
 | 
| +}
 | 
|  
 | 
| -  InputStream get inputStream {
 | 
| -    if (_inputStream == null) {
 | 
| -      _inputStream = new _HttpInputStream(this);
 | 
| -    }
 | 
| -    return _inputStream;
 | 
| +
 | 
| +// Transformer that invokes [_onDone] when completed.
 | 
| +class _DoneTransformer implements StreamTransformer<List<int>, List<int>> {
 | 
| +  final StreamController<List<int>> _controller
 | 
| +      = new StreamController<List<int>>();
 | 
| +  final Function _onDone;
 | 
| +
 | 
| +  _DoneTransformer(this._onDone);
 | 
| +
 | 
| +  Stream<List<int>> bind(Stream<List<int>> stream) {
 | 
| +    var subscription = stream.listen(
 | 
| +        _controller.add,
 | 
| +        onError: _controller.signalError,
 | 
| +        onDone: () {
 | 
| +          _onDone();
 | 
| +          _controller.close();
 | 
| +        });
 | 
| +    return _controller.stream;
 | 
|    }
 | 
| +}
 | 
|  
 | 
| -  void _onResponseReceived(int statusCode,
 | 
| -                           String reasonPhrase,
 | 
| -                           String version,
 | 
| -                           _HttpHeaders headers,
 | 
| -                           bool hasBody) {
 | 
| -    _statusCode = statusCode;
 | 
| -    _reasonPhrase = reasonPhrase;
 | 
| -    _headers = headers;
 | 
| -
 | 
| -    // Prepare for receiving data.
 | 
| -    _buffer = new _BufferList();
 | 
| -    if (isRedirect && _connection.followRedirects) {
 | 
| -      if (_connection._redirects == null ||
 | 
| -          _connection._redirects.length < _connection.maxRedirects) {
 | 
| -        // Check the location header.
 | 
| -        List<String> location = headers[HttpHeaders.LOCATION];
 | 
| -        if (location == null || location.length > 1) {
 | 
| -           throw new RedirectException("Invalid redirect",
 | 
| -                                       _connection._redirects);
 | 
| -        }
 | 
| -        // Check for redirect loop
 | 
| -        if (_connection._redirects != null) {
 | 
| -          Uri redirectUrl = Uri.parse(location[0]);
 | 
| -          for (int i = 0; i < _connection._redirects.length; i++) {
 | 
| -            if (_connection._redirects[i].location.toString() ==
 | 
| -                redirectUrl.toString()) {
 | 
| -              throw new RedirectLoopException(_connection._redirects);
 | 
| +// Transformer that validates the data written.
 | 
| +class _DataValidatorTransformer
 | 
| +    implements StreamTransformer<List<int>, List<int>> {
 | 
| +  final StreamController<List<int>> _controller
 | 
| +      = new StreamController<List<int>>();
 | 
| +  int _bytesWritten = 0;
 | 
| +  Completer _completer = new Completer();
 | 
| +
 | 
| +  int expectedTransferLength;
 | 
| +
 | 
| +  _DataValidatorTransformer();
 | 
| +
 | 
| +  Future get validatorFuture => _completer.future;
 | 
| +
 | 
| +  Stream<List<int>> bind(Stream<List<int>> stream) {
 | 
| +    var subscription;
 | 
| +    subscription = stream.listen(
 | 
| +        (data) {
 | 
| +          if (expectedTransferLength != null) {
 | 
| +            _bytesWritten += data.length;
 | 
| +            if (_bytesWritten > expectedTransferLength) {
 | 
| +              _controller.close();
 | 
| +              subscription.cancel();
 | 
| +              if (_completer != null) {
 | 
| +                _completer.completeError(new HttpException(
 | 
| +                    "Content size exceeds specified contentLength. "
 | 
| +                    "$_bytesWritten bytes written while expected "
 | 
| +                    "$expectedTransferLength."));
 | 
| +                _completer = null;
 | 
| +              }
 | 
| +              return;
 | 
|              }
 | 
|            }
 | 
| -        }
 | 
| -        if (!persistentConnection) {
 | 
| -          throw new RedirectException(
 | 
| -              "Non-persistent connections are currently not supported for "
 | 
| -              "redirects", _connection._redirects);
 | 
| -        }
 | 
| -        // Drain body and redirect.
 | 
| -        inputStream.onData = inputStream.read;
 | 
| -        if (_statusCode == HttpStatus.SEE_OTHER &&
 | 
| -            _connection._method == "POST") {
 | 
| -          _connection.redirect("GET");
 | 
| -        } else {
 | 
| -          _connection.redirect();
 | 
| -        }
 | 
| -      } else {
 | 
| -        throw new RedirectLimitExceededException(_connection._redirects);
 | 
| -      }
 | 
| -    } else if (statusCode == HttpStatus.UNAUTHORIZED) {
 | 
| -      _handleUnauthorized();
 | 
| -    } else if (_connection._onResponse != null) {
 | 
| -      _connection._onResponse(this);
 | 
| -    }
 | 
| +          _controller.add(data);
 | 
| +        },
 | 
| +        onError: (error) {
 | 
| +          _controller.close();
 | 
| +          if (_completer != null) {
 | 
| +            _completer.completeError(error);
 | 
| +            _completer = null;
 | 
| +          }
 | 
| +        },
 | 
| +        onDone: () {
 | 
| +          _controller.close();
 | 
| +          if (expectedTransferLength != null) {
 | 
| +            if (_bytesWritten < expectedTransferLength) {
 | 
| +              if (_completer != null) {
 | 
| +                _completer.completeError(new HttpException(
 | 
| +                    "Content size below specified contentLength. "
 | 
| +                    " $_bytesWritten bytes written while expected "
 | 
| +                    "$expectedTransferLength."));
 | 
| +                _completer = null;
 | 
| +                return;
 | 
| +              }
 | 
| +            }
 | 
| +          }
 | 
| +          if (_completer != null) {
 | 
| +            _completer.complete(this);
 | 
| +            _completer = null;
 | 
| +          }
 | 
| +        },
 | 
| +        unsubscribeOnError: true);
 | 
| +    return _controller.stream;
 | 
|    }
 | 
| +}
 | 
|  
 | 
| -  void _handleUnauthorized() {
 | 
| +// Extends StreamConsumer as this is an internal type, only used to pipe to.
 | 
| +class _HttpOutgoing implements StreamConsumer<List<int>, dynamic> {
 | 
| +  final Completer _dataCompleter = new Completer();
 | 
| +  final Completer _streamCompleter = new Completer();
 | 
| +  final _DataValidatorTransformer _validator = new _DataValidatorTransformer();
 | 
|  
 | 
| -    void retryRequest(_Credentials cr) {
 | 
| -      if (cr != null) {
 | 
| -        // Drain body and retry.
 | 
| -        // TODO(sgjesse): Support digest.
 | 
| -        if (cr.scheme == _AuthenticationScheme.BASIC) {
 | 
| -          inputStream.onData = inputStream.read;
 | 
| -          _connection._retry();
 | 
| -          return;
 | 
| -        }
 | 
| -      }
 | 
| +  // Future that completes when all data is written.
 | 
| +  Future get dataDone => _dataCompleter.future;
 | 
|  
 | 
| -      // Fall through to here to perform normal response handling if
 | 
| -      // there is no sensible authorization handling.
 | 
| -      if (_connection._onResponse != null) {
 | 
| -        _connection._onResponse(this);
 | 
| -      }
 | 
| -    }
 | 
| +  // Future that completes with the Stream, once the _HttpClientConnection is
 | 
| +  // bound to one.
 | 
| +  Future<Stream<List<int>>> get stream => _streamCompleter.future;
 | 
|  
 | 
| -    // Only try to authenticate if there is a challenge in the response.
 | 
| -    List<String> challenge = _headers[HttpHeaders.WWW_AUTHENTICATE];
 | 
| -    if (challenge != null && challenge.length == 1) {
 | 
| -      _HeaderValue header =
 | 
| -          new _HeaderValue.fromString(challenge[0], parameterSeparator: ",");
 | 
| -      _AuthenticationScheme scheme =
 | 
| -          new _AuthenticationScheme.fromString(header.value);
 | 
| -      String realm = header.parameters["realm"];
 | 
| -
 | 
| -      // See if any credentials are available.
 | 
| -      _Credentials cr =
 | 
| -          _connection._client._findCredentials(
 | 
| -              _connection._request._uri, scheme);
 | 
| -
 | 
| -      // 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 || cr.used) {
 | 
| -        if (cr != null) {
 | 
| -          _connection._client._removeCredentials(cr);
 | 
| -        }
 | 
| -        cr = null;
 | 
| -        if (_connection._client._authenticate != null) {
 | 
| -          Future authComplete =
 | 
| -              _connection._client._authenticate(
 | 
| -                  _connection._request._uri, scheme.toString(), realm);
 | 
| -          authComplete.then((credsAvailable) {
 | 
| -            if (credsAvailable) {
 | 
| -              cr = _connection._client._findCredentials(
 | 
| -                  _connection._request._uri, scheme);
 | 
| -              retryRequest(cr);
 | 
| -            } else {
 | 
| -              if (_connection._onResponse != null) {
 | 
| -                _connection._onResponse(this);
 | 
| -              }
 | 
| -            }
 | 
| -          });
 | 
| -          return;
 | 
| -        }
 | 
| -      } else {
 | 
| -        // If credentials found prepare for retrying the request.
 | 
| -        retryRequest(cr);
 | 
| -        return;
 | 
| -      }
 | 
| -    }
 | 
| +  void setTransferLength(int transferLength) {
 | 
| +    _validator.expectedTransferLength = transferLength;
 | 
| +  }
 | 
|  
 | 
| -    // Fall through to here to perform normal response handling if
 | 
| -    // there is no sensible authorization handling.
 | 
| -    if (_connection._onResponse != null) {
 | 
| -      _connection._onResponse(this);
 | 
| -    }
 | 
| +  Future consume(Stream<List<int>> stream) {
 | 
| +    stream = stream.transform(_validator);
 | 
| +    _streamCompleter.complete(stream);
 | 
| +    _validator.validatorFuture.catchError((e) {
 | 
| +      _dataCompleter.completeError(e);
 | 
| +    });
 | 
| +    return _validator.validatorFuture.then((v) {
 | 
| +      _dataCompleter.complete();
 | 
| +      return v;
 | 
| +    });
 | 
|    }
 | 
| +}
 | 
|  
 | 
| -  void _onDataReceived(List<int> data) {
 | 
| -    _buffer.add(data);
 | 
| -    if (_inputStream != null) _inputStream._dataReceived();
 | 
| +
 | 
| +class _HttpClientConnection {
 | 
| +  final String key;
 | 
| +  final Socket _socket;
 | 
| +  final _HttpParser _httpParser;
 | 
| +  StreamSubscription _subscription;
 | 
| +  final _HttpClient _httpClient;
 | 
| +
 | 
| +  Completer<_HttpIncoming> _nextResponseCompleter;
 | 
| +  Future _writeDoneFuture;
 | 
| +
 | 
| +  _HttpClientConnection(String this.key,
 | 
| +                        Socket this._socket,
 | 
| +                        _HttpClient this._httpClient)
 | 
| +      : _httpParser = new _HttpParser.responseParser() {
 | 
| +    _socket.pipe(_httpParser);
 | 
| +    _socket.done.catchError((e) { destroy(); });
 | 
| +
 | 
| +    // 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.
 | 
| +          assert(_nextResponseCompleter != null);
 | 
| +          _nextResponseCompleter.complete(incoming);
 | 
| +        },
 | 
| +        onError: (error) {
 | 
| +          if (_nextResponseCompleter != null) {
 | 
| +            _nextResponseCompleter.completeError(error);
 | 
| +          }
 | 
| +        },
 | 
| +        onDone: () {
 | 
| +          close();
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _onDataEnd() {
 | 
| -    if (_inputStream != null) {
 | 
| -      _inputStream._closeReceived();
 | 
| -    } else {
 | 
| -      inputStream._streamMarkedClosed = true;
 | 
| -    }
 | 
| +  Future<_HttpIncoming> sendRequest(_HttpOutgoing outgoing) {
 | 
| +    return outgoing.stream
 | 
| +      .then((stream) {
 | 
| +        // Close socket if output data is invalid.
 | 
| +        outgoing.dataDone.catchError((e) {
 | 
| +          close();
 | 
| +        });
 | 
| +        // Sending request, set up response completer.
 | 
| +        _nextResponseCompleter = new Completer();
 | 
| +        _writeDoneFuture = _socket.addStream(stream);
 | 
| +        // Listen for response.
 | 
| +        return _nextResponseCompleter.future
 | 
| +            .whenComplete(() {
 | 
| +               _nextResponseCompleter = null;
 | 
| +             })
 | 
| +            .then((incoming) {
 | 
| +              incoming.dataDone.then((_) {
 | 
| +                if (!incoming.headers.persistentConnection) {
 | 
| +                  close();
 | 
| +                } else {
 | 
| +                  // Wait for the socket to be done with writing, before we
 | 
| +                  // continue.
 | 
| +                  _writeDoneFuture.then((_) {
 | 
| +                    _subscription.resume();
 | 
| +                    // Return connection, now we are done.
 | 
| +                    _httpClient._returnConnection(this);
 | 
| +                  });
 | 
| +                }
 | 
| +              });
 | 
| +              // TODO(ajohnsen): Can there be an error on dataDone?
 | 
| +              return incoming;
 | 
| +            })
 | 
| +            // If we see a state error, we failed to get the 'first' element.
 | 
| +            // Transform the error to a HttpParserException, for consistency.
 | 
| +            .catchError((error) {
 | 
| +              throw new HttpParserException(
 | 
| +                  "Connection closed before data was received");
 | 
| +            }, test: (error) => error is StateError)
 | 
| +            .catchError((error) {
 | 
| +              // We are done with the socket.
 | 
| +              destroy();
 | 
| +              throw error;
 | 
| +            });
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  // Delegate functions for the HttpInputStream implementation.
 | 
| -  int _streamAvailable() {
 | 
| -    return _buffer.length;
 | 
| +  Future<Socket> detachSocket() {
 | 
| +    return _writeDoneFuture.then((_) =>
 | 
| +        new _DetachedSocket(_socket, _httpParser.detachIncoming()));
 | 
|    }
 | 
|  
 | 
| -  List<int> _streamRead(int bytesToRead) {
 | 
| -    return _buffer.readBytes(bytesToRead);
 | 
| +  void destroy() {
 | 
| +    _socket.destroy();
 | 
| +    _httpClient._connectionClosed(this);
 | 
|    }
 | 
|  
 | 
| -  int _streamReadInto(List<int> buffer, int offset, int len) {
 | 
| -    List<int> data = _buffer.readBytes(len);
 | 
| -    buffer.setRange(offset, data.length, data);
 | 
| -    return data.length;
 | 
| +  void close() {
 | 
| +    var future = _writeDoneFuture;
 | 
| +    if (future == null) future = new Future.immediate(null);
 | 
| +    _httpClient._connectionClosed(this);
 | 
| +    future.then((_) {
 | 
| +      _socket.close();
 | 
| +      // TODO(ajohnsen): Add timeout.
 | 
| +      // Delay destroy until socket is actually done writing.
 | 
| +      _socket.done.then((_) => _socket.destroy(),
 | 
| +                        onError: (_) => _socket.destroy());
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  void _streamSetErrorHandler(callback(e)) {
 | 
| -    _streamErrorHandler = callback;
 | 
| -  }
 | 
| +  HttpConnectionInfo get connectionInfo => _HttpConnectionInfo.create(_socket);
 | 
| +}
 | 
|  
 | 
| -  int _statusCode;
 | 
| -  String _reasonPhrase;
 | 
| +class _ConnnectionInfo {
 | 
| +  _ConnnectionInfo(_HttpClientConnection this.connection, _Proxy this.proxy);
 | 
| +  final _HttpClientConnection connection;
 | 
| +  final _Proxy proxy;
 | 
| +}
 | 
|  
 | 
| -  _HttpClientConnection _connection;
 | 
| -  _HttpInputStream _inputStream;
 | 
| -  _BufferList _buffer;
 | 
|  
 | 
| -  Function _streamErrorHandler;
 | 
| -}
 | 
| +class _HttpClient implements HttpClient {
 | 
| +  // TODO(ajohnsen): Use eviction timeout.
 | 
| +  static const int DEFAULT_EVICTION_TIMEOUT = 60000;
 | 
| +  bool _closing = false;
 | 
|  
 | 
| +  final Map<String, Queue<_HttpClientConnection>> _idleConnections
 | 
| +      = new Map<String, Queue<_HttpClientConnection>>();
 | 
| +  final Set<_HttpClientConnection> _activeConnections
 | 
| +      = new Set<_HttpClientConnection>();
 | 
| +  final List<_Credentials> _credentials = [];
 | 
| +  Function _authenticate;
 | 
| +  Function _findProxy;
 | 
|  
 | 
| -class _HttpClientConnection
 | 
| -    extends _HttpConnectionBase implements HttpClientConnection {
 | 
| +  Future<HttpClientRequest> open(String method,
 | 
| +                                 String host,
 | 
| +                                 int port,
 | 
| +                                 String path) {
 | 
| +    // TODO(sgjesse): The path set here can contain both query and
 | 
| +    // fragment. They should be cracked and set correctly.
 | 
| +    return _openUrl(method, new Uri.fromComponents(
 | 
| +        scheme: "http", domain: host, port: port, path: path));
 | 
| +  }
 | 
|  
 | 
| -  _HttpClientConnection(_HttpClient this._client) {
 | 
| -    _httpParser = new _HttpParser.responseParser();
 | 
| +  Future<HttpClientRequest> openUrl(String method, Uri url) {
 | 
| +    return _openUrl(method, url);
 | 
|    }
 | 
|  
 | 
| -  bool _write(List<int> data, [bool copyBuffer = false]) {
 | 
| -    return _socket.outputStream.write(data, copyBuffer);
 | 
| +  Future<HttpClientRequest> get(String host,
 | 
| +                                int port,
 | 
| +                                String path) {
 | 
| +    return open("get", host, port, path);
 | 
|    }
 | 
|  
 | 
| -  bool _writeFrom(List<int> data, [int offset, int len]) {
 | 
| -    return _socket.outputStream.writeFrom(data, offset, len);
 | 
| +  Future<HttpClientRequest> getUrl(Uri url) {
 | 
| +    return _openUrl("get", url);
 | 
|    }
 | 
|  
 | 
| -  bool _flush() {
 | 
| -    _socket.outputStream.flush();
 | 
| +  Future<HttpClientRequest> post(String host,
 | 
| +                                 int port,
 | 
| +                                 String path) {
 | 
| +    return open("post", host, port, path);
 | 
|    }
 | 
|  
 | 
| -  bool _close() {
 | 
| -    _socket.outputStream.close();
 | 
| +  Future<HttpClientRequest> postUrl(Uri url) {
 | 
| +    return _openUrl("post", url);
 | 
|    }
 | 
|  
 | 
| -  bool _destroy() {
 | 
| -    _socket.close();
 | 
| +  void close({bool force: false}) {
 | 
| +    _closing = true;
 | 
| +    // Create flattened copy of _idleConnections, as 'destory' will manipulate
 | 
| +    // it.
 | 
| +    var idle = _idleConnections.values.reduce(
 | 
| +        [],
 | 
| +        (l, e) {
 | 
| +          l.addAll(e);
 | 
| +          return l;
 | 
| +        });
 | 
| +    idle.forEach((e) {
 | 
| +      e.close();
 | 
| +    });
 | 
| +    assert(_idleConnections.isEmpty);
 | 
| +    if (force) {
 | 
| +      for (var connection in _activeConnections.toList()) {
 | 
| +        connection.destroy();
 | 
| +      }
 | 
| +      assert(_activeConnections.isEmpty);
 | 
| +      _activeConnections.clear();
 | 
| +    }
 | 
|    }
 | 
|  
 | 
| -  DetachedSocket _detachSocket() {
 | 
| -    _socket.onData = null;
 | 
| -    _socket.onClosed = null;
 | 
| -    _socket.onError = null;
 | 
| -    _socket.outputStream.onNoPendingWrites = null;
 | 
| -    Socket socket = _socket;
 | 
| -    _socket = null;
 | 
| -    if (onDetach != null) onDetach();
 | 
| -    return new _DetachedSocket(socket, _httpParser.readUnparsedData());
 | 
| +  set authenticate(Future<bool> f(Uri url, String scheme, String realm)) {
 | 
| +    _authenticate = f;
 | 
|    }
 | 
|  
 | 
| -  void _connectionEstablished(_SocketConnection socketConn) {
 | 
| -    super._connectionEstablished(socketConn._socket);
 | 
| -    _socketConn = socketConn;
 | 
| -    // Register HTTP parser callbacks.
 | 
| -    _httpParser.responseStart = _onResponseReceived;
 | 
| -    _httpParser.dataReceived = _onDataReceived;
 | 
| -    _httpParser.dataEnd = _onDataEnd;
 | 
| -    _httpParser.error = _onError;
 | 
| -    _httpParser.closed = _onClosed;
 | 
| -    _httpParser.requestStart = (method, uri, version) { assert(false); };
 | 
| -    _state = _HttpConnectionBase.ACTIVE;
 | 
| +  void addCredentials(Uri url, String realm, HttpClientCredentials cr) {
 | 
| +    _credentials.add(new _Credentials(url, realm, cr));
 | 
|    }
 | 
|  
 | 
| -  void _checkSocketDone() {
 | 
| -    if (_isAllDone) {
 | 
| -      // If we are done writing the response, and either the server
 | 
| -      // has closed or the connection is not persistent, we must
 | 
| -      // close.
 | 
| -      if (_isReadClosed || !_response.persistentConnection) {
 | 
| -        this.onClosed = () {
 | 
| -          _client._closedSocketConnection(_socketConn);
 | 
| -        };
 | 
| -        _client._closeQueue.add(this);
 | 
| -      } else if (_socket != null) {
 | 
| -        _client._returnSocketConnection(_socketConn);
 | 
| -        _socket = null;
 | 
| -        _socketConn = null;
 | 
| -        assert(_pendingRedirect == null || _pendingRetry == null);
 | 
| -        if (_pendingRedirect != null) {
 | 
| -          _doRedirect(_pendingRedirect);
 | 
| -          _pendingRedirect = null;
 | 
| -        } else if (_pendingRetry != null) {
 | 
| -          _doRetry(_pendingRetry);
 | 
| -          _pendingRetry = null;
 | 
| -        }
 | 
| +  set findProxy(String f(Uri uri)) => _findProxy = f;
 | 
| +
 | 
| +  Future<HttpClientRequest> _openUrl(String method, Uri uri) {
 | 
| +    if (method == null) {
 | 
| +      throw new ArgumentError(method);
 | 
| +    }
 | 
| +    if (uri.domain.isEmpty || (uri.scheme != "http" && uri.scheme != "https")) {
 | 
| +      throw new ArgumentError("Unsupported scheme '${uri.scheme}' in $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.immediateError(error, stackTrace);
 | 
|        }
 | 
|      }
 | 
| +    return _getConnection(uri.domain, port, proxyConf, isSecure).then((info) {
 | 
| +          // Create new internal outgoing connection.
 | 
| +          var outgoing = new _HttpOutgoing();
 | 
| +          // Create new request object, wrapping the outgoing connection.
 | 
| +          var request = new _HttpClientRequest(outgoing,
 | 
| +                                               uri,
 | 
| +                                               method.toUpperCase(),
 | 
| +                                               !info.proxy.isDirect,
 | 
| +                                               this,
 | 
| +                                               info.connection);
 | 
| +          request.headers.host = uri.domain;
 | 
| +          request.headers.port = port;
 | 
| +          if (uri.userInfo != null && !uri.userInfo.isEmpty) {
 | 
| +            // If the URL contains user information use that for basic
 | 
| +            // authorization
 | 
| +            String auth =
 | 
| +                CryptoUtils.bytesToBase64(_encodeString(uri.userInfo));
 | 
| +            request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth");
 | 
| +          } else {
 | 
| +            // Look for credentials.
 | 
| +            _Credentials cr = _findCredentials(uri);
 | 
| +            if (cr != null) {
 | 
| +              cr.authorize(request);
 | 
| +            }
 | 
| +          }
 | 
| +          // Start sending the request (lazy, delayed until the user provides
 | 
| +          // data).
 | 
| +          info.connection._httpParser.responseToMethod = method;
 | 
| +          info.connection.sendRequest(outgoing)
 | 
| +              .then((incoming) {
 | 
| +                // The full request have been sent and a response is received
 | 
| +                // containing status-code, headers and etc.
 | 
| +                request._onIncoming(incoming);
 | 
| +              })
 | 
| +              .catchError((error) {
 | 
| +                // An error occoured before the http-header was parsed. This
 | 
| +                // could be either a socket-error or parser-error.
 | 
| +                request._onError(error);
 | 
| +              });
 | 
| +          // Return the request to the user. Immediate socket errors are not
 | 
| +          // handled, thus forwarded to the user.
 | 
| +          return request;
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _requestClosed() {
 | 
| -    _state |= _HttpConnectionBase.REQUEST_DONE;
 | 
| -    _checkSocketDone();
 | 
| +  Future<HttpClientRequest> _openUrlFromRequest(String method,
 | 
| +                                                Uri uri,
 | 
| +                                                _HttpClientRequest previous) {
 | 
| +    return openUrl(method, uri).then((request) {
 | 
| +          // Only follow redirects if initial request did.
 | 
| +          request.followRedirects = previous.followRedirects;
 | 
| +          // Allow same number of redirects.
 | 
| +          request.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]);
 | 
| +            }
 | 
| +          }
 | 
| +          request.headers.chunkedTransferEncoding = false;
 | 
| +          request.contentLength = 0;
 | 
| +          return request;
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  HttpClientRequest open(String method, Uri uri) {
 | 
| -    _method = method;
 | 
| -    // Tell the HTTP parser the method it is expecting a response to.
 | 
| -    _httpParser.responseToMethod = method;
 | 
| -    // If the connection already have a request this is a retry of a
 | 
| -    // request. In this case the request object is reused to ensure
 | 
| -    // that the same headers are send.
 | 
| -    if (_request != null) {
 | 
| -      _request._method = method;
 | 
| -      _request._uri = uri;
 | 
| -      _request._headers._mutable = true;
 | 
| -      _request._state = _HttpRequestResponseBase.START;
 | 
| -    } else {
 | 
| -      _request = new _HttpClientRequest(method, uri, this);
 | 
| +  // Return a live connection to the idle pool.
 | 
| +  void _returnConnection(_HttpClientConnection connection) {
 | 
| +    _activeConnections.remove(connection);
 | 
| +    if (_closing) {
 | 
| +      connection.close();
 | 
| +      return;
 | 
|      }
 | 
| -    _response = new _HttpClientResponse(this);
 | 
| -    return _request;
 | 
| -  }
 | 
| -
 | 
| -  DetachedSocket detachSocket() {
 | 
| -    return _detachSocket();
 | 
| +    // TODO(ajohnsen): Listen for socket close events.
 | 
| +    if (!_idleConnections.containsKey(connection.key)) {
 | 
| +      _idleConnections[connection.key] = new Queue();
 | 
| +    }
 | 
| +    _idleConnections[connection.key].addLast(connection);
 | 
|    }
 | 
|  
 | 
| -  void _onClosed() {
 | 
| -    _state |= _HttpConnectionBase.READ_CLOSED;
 | 
| -    _checkSocketDone();
 | 
| +  // Remove a closed connnection from the active set.
 | 
| +  void _connectionClosed(_HttpClientConnection connection) {
 | 
| +    _activeConnections.remove(connection);
 | 
| +    if (_idleConnections.containsKey(connection.key)) {
 | 
| +      _idleConnections[connection.key].remove(connection);
 | 
| +      if (_idleConnections[connection.key].isEmpty) {
 | 
| +        _idleConnections.remove(connection.key);
 | 
| +      }
 | 
| +    }
 | 
|    }
 | 
|  
 | 
| -  void _onError(e) {
 | 
| -    // Cancel any pending data in the HTTP parser.
 | 
| -    _httpParser.cancel();
 | 
| -    if (_socketConn != null) {
 | 
| -      _client._closeSocketConnection(_socketConn);
 | 
| +  // Get a new _HttpClientConnection, either from the idle pool or created from
 | 
| +  // a new Socket.
 | 
| +  Future<_ConnnectionInfo> _getConnection(String uriHost,
 | 
| +                                          int uriPort,
 | 
| +                                          _ProxyConfiguration proxyConf,
 | 
| +                                          bool isSecure) {
 | 
| +    Iterator<_Proxy> proxies = proxyConf.proxies.iterator;
 | 
| +
 | 
| +    Future<_ConnnectionInfo> connect(error) {
 | 
| +      if (!proxies.moveNext()) return new Future.immediateError(error);
 | 
| +      _Proxy proxy = proxies.current;
 | 
| +      String host = proxy.isDirect ? uriHost: proxy.host;
 | 
| +      int port = proxy.isDirect ? uriPort: proxy.port;
 | 
| +      String key = isSecure ? "ssh:$host:$port" : "$host:$port";
 | 
| +      if (_idleConnections.containsKey(key)) {
 | 
| +        var connection = _idleConnections[key].removeFirst();
 | 
| +        if (_idleConnections[key].isEmpty) {
 | 
| +          _idleConnections.remove(key);
 | 
| +        }
 | 
| +        _activeConnections.add(connection);
 | 
| +        return new Future.immediate(new _ConnnectionInfo(connection, proxy));
 | 
| +      }
 | 
| +      return (isSecure && proxy.isDirect
 | 
| +                  ? SecureSocket.connect(host,
 | 
| +                                         port,
 | 
| +                                         sendClientCertificate: true)
 | 
| +                  : Socket.connect(host, port))
 | 
| +        .then((socket) {
 | 
| +          var connection = new _HttpClientConnection(key, socket, this);
 | 
| +          _activeConnections.add(connection);
 | 
| +          return new _ConnnectionInfo(connection, proxy);
 | 
| +        }, onError: (error) {
 | 
| +          // Continue with next proxy.
 | 
| +          return connect(error.error);
 | 
| +        });
 | 
|      }
 | 
| +    return connect(new HttpException("No proxies given"));
 | 
| +  }
 | 
|  
 | 
| -    // If it looks as if we got a bad connection from the connection
 | 
| -    // pool and the request can be retried do a retry.
 | 
| -    if (_socketConn != null && _socketConn._fromPool && _request._emptyBody) {
 | 
| -      String method = _request._method;
 | 
| -      Uri uri = _request._uri;
 | 
| -      _socketConn = null;
 | 
| +  _Credentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) {
 | 
| +    // Look for credentials.
 | 
| +    _Credentials cr =
 | 
| +        _credentials.reduce(null, (_Credentials prev, _Credentials value) {
 | 
| +          if (value.applies(url, scheme)) {
 | 
| +            if (prev == null) return value;
 | 
| +            return value.uri.path.length > prev.uri.path.length ? value : prev;
 | 
| +          } else {
 | 
| +            return prev;
 | 
| +          }
 | 
| +        });
 | 
| +    return cr;
 | 
| +  }
 | 
|  
 | 
| -      // Retry the URL using the same connection instance.
 | 
| -      _httpParser.restart();
 | 
| -      _client._openUrl(method, uri, this);
 | 
| -    } else {
 | 
| -      // Report the error.
 | 
| -      if (_response != null && _response._streamErrorHandler != null) {
 | 
| -         _response._streamErrorHandler(e);
 | 
| -      } else if (_onErrorCallback != null) {
 | 
| -        _onErrorCallback(e);
 | 
| -      } else {
 | 
| -        throw e;
 | 
| -      }
 | 
| +  void _removeCredentials(_Credentials cr) {
 | 
| +    int index = _credentials.indexOf(cr);
 | 
| +    if (index != -1) {
 | 
| +      _credentials.removeAt(index);
 | 
|      }
 | 
|    }
 | 
| +}
 | 
|  
 | 
| -  void _onResponseReceived(int statusCode,
 | 
| -                           String reasonPhrase,
 | 
| -                           String version,
 | 
| -                           _HttpHeaders headers,
 | 
| -                           bool hasBody) {
 | 
| -    _response._onResponseReceived(
 | 
| -        statusCode, reasonPhrase, version, headers, hasBody);
 | 
| -  }
 | 
|  
 | 
| -  void _onDataReceived(List<int> data) {
 | 
| -    _response._onDataReceived(data);
 | 
| +class _HttpConnection {
 | 
| +  static const _ACTIVE = 0;
 | 
| +  static const _IDLE = 1;
 | 
| +  static const _CLOSING = 2;
 | 
| +  static const _DETACHED = 3;
 | 
| +
 | 
| +  int _state = _IDLE;
 | 
| +
 | 
| +  final Socket _socket;
 | 
| +  final _HttpServer _httpServer;
 | 
| +  final _HttpParser _httpParser;
 | 
| +  StreamSubscription _subscription;
 | 
| +
 | 
| +  Future _writeDoneFuture;
 | 
| +
 | 
| +  _HttpConnection(Socket this._socket, _HttpServer this._httpServer)
 | 
| +      : _httpParser = new _HttpParser.requestParser() {
 | 
| +    _socket.pipe(_httpParser);
 | 
| +    _socket.done.catchError((e) => destroy());
 | 
| +    _subscription = _httpParser.listen(
 | 
| +        (incoming) {
 | 
| +          // 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();
 | 
| +          _writeDoneFuture = outgoing.stream.then(_socket.addStream);
 | 
| +          var response = new _HttpResponse(
 | 
| +              incoming.headers.protocolVersion,
 | 
| +              outgoing);
 | 
| +          var request = new _HttpRequest(response, incoming, _httpServer, this);
 | 
| +          response._ignoreBody = request.method == "HEAD";
 | 
| +          response._httpRequest = request;
 | 
| +          outgoing.dataDone.then((_) {
 | 
| +            if (_state == _DETACHED) return;
 | 
| +            if (response.headers.persistentConnection &&
 | 
| +                incoming.fullBodyRead) {
 | 
| +              // Wait for the socket to be done with writing, before we
 | 
| +              // continue.
 | 
| +              _writeDoneFuture.then((_) {
 | 
| +                _state = _IDLE;
 | 
| +                // 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.
 | 
| +              close();
 | 
| +            }
 | 
| +          }).catchError((e) {
 | 
| +            close();
 | 
| +          });
 | 
| +          _httpServer._handleRequest(request);
 | 
| +        },
 | 
| +        onDone: () {
 | 
| +          close();
 | 
| +        },
 | 
| +        onError: (error) {
 | 
| +          _httpServer._handleError(error);
 | 
| +          destroy();
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _onDataEnd(bool close) {
 | 
| -    _state |= _HttpConnectionBase.RESPONSE_DONE;
 | 
| -    _response._onDataEnd();
 | 
| -    _checkSocketDone();
 | 
| +  void destroy() {
 | 
| +    if (_state == _CLOSING || _state == _DETACHED) return;
 | 
| +    _state = _CLOSING;
 | 
| +    _socket.destroy();
 | 
| +    _httpServer._connectionClosed(this);
 | 
|    }
 | 
|  
 | 
| -  void _onClientShutdown() {
 | 
| -    if (!_isResponseDone) {
 | 
| -      _onError(new HttpException("Client shutdown"));
 | 
| -    }
 | 
| +  void close() {
 | 
| +    if (_state == _CLOSING || _state == _DETACHED) return;
 | 
| +    _state = _CLOSING;
 | 
| +    var future = _writeDoneFuture;
 | 
| +    if (future == null) future = new Future.immediate(null);
 | 
| +    _httpServer._connectionClosed(this);
 | 
| +    future.then((_) {
 | 
| +      _socket.close();
 | 
| +      // TODO(ajohnsen): Add timeout.
 | 
| +      // Delay destroy until socket is actually done writing.
 | 
| +      _socket.done.then((_) => _socket.destroy(),
 | 
| +                        onError: (_) => _socket.destroy());
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  void set onRequest(void handler(HttpClientRequest request)) {
 | 
| -    _onRequest = handler;
 | 
| +  Future<Socket> detachSocket() {
 | 
| +    _state = _DETACHED;
 | 
| +    // Remove connection from server.
 | 
| +    _httpServer._connectionClosed(this);
 | 
| +
 | 
| +    _HttpDetachedIncoming detachedIncoming = _httpParser.detachIncoming();
 | 
| +
 | 
| +    return _writeDoneFuture.then((_) {
 | 
| +      return new _DetachedSocket(_socket, detachedIncoming);
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  void set onResponse(void handler(HttpClientResponse response)) {
 | 
| -    _onResponse = handler;
 | 
| +  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;
 | 
| +}
 | 
| +
 | 
| +
 | 
| +// HTTP server waiting for socket connections.
 | 
| +class _HttpServer extends Stream<HttpRequest> implements HttpServer {
 | 
| +
 | 
| +  static Future<HttpServer> bind(String host, int port, int backlog) {
 | 
| +    return ServerSocket.bind(host, port, backlog).then((socket) {
 | 
| +      return new _HttpServer._(socket, true);
 | 
| +    });
 | 
|    }
 | 
|  
 | 
| -  void set onError(void callback(e)) {
 | 
| -    _onErrorCallback = callback;
 | 
| +  static Future<HttpServer> bindSecure(String host,
 | 
| +                                       int port,
 | 
| +                                       int backlog,
 | 
| +                                       String certificate_name,
 | 
| +                                       bool requestClientCertificate) {
 | 
| +    return SecureServerSocket.bind(
 | 
| +        host,
 | 
| +        port,
 | 
| +        backlog,
 | 
| +        certificate_name,
 | 
| +        requestClientCertificate: requestClientCertificate)
 | 
| +        .then((socket) {
 | 
| +          return new _HttpServer._(socket, true);
 | 
| +        });
 | 
|    }
 | 
|  
 | 
| -  void _doRetry(_RedirectInfo retry) {
 | 
| -    assert(_socketConn == null);
 | 
| +  _HttpServer._(this._serverSocket, this._closeServer);
 | 
|  
 | 
| -    // Retry the URL using the same connection instance.
 | 
| -    _state = _HttpConnectionBase.IDLE;
 | 
| -    _client._openUrl(retry.method, retry.location, this);
 | 
| +  _HttpServer.listenOn(ServerSocket this._serverSocket)
 | 
| +      : _closeServer = false;
 | 
| +
 | 
| +  StreamSubscription<HttpRequest> listen(void onData(HttpRequest event),
 | 
| +                                         {void onError(AsyncError error),
 | 
| +                                         void onDone(),
 | 
| +                                         bool unsubscribeOnError}) {
 | 
| +    _serverSocket.listen(
 | 
| +        (Socket socket) {
 | 
| +          // Accept the client connection.
 | 
| +          _HttpConnection connection = new _HttpConnection(socket, this);
 | 
| +          _connections.add(connection);
 | 
| +        },
 | 
| +        onError: _controller.signalError,
 | 
| +        onDone: _controller.close);
 | 
| +    return _controller.stream.listen(onData,
 | 
| +                                     onError: onError,
 | 
| +                                     onDone: onDone,
 | 
| +                                     unsubscribeOnError: unsubscribeOnError);
 | 
|    }
 | 
|  
 | 
| -  void _retry() {
 | 
| -    var retry = new _RedirectInfo(_response.statusCode, _method, _request._uri);
 | 
| -    // The actual retry is postponed until both response and request
 | 
| -    // are done.
 | 
| -    if (_isAllDone) {
 | 
| -      _doRetry(retry);
 | 
| -    } else {
 | 
| -      // Prepare for retry.
 | 
| -      assert(_pendingRedirect == null);
 | 
| -      _pendingRetry = retry;
 | 
| +  void close() {
 | 
| +    closed = true;
 | 
| +    if (_serverSocket != null && _closeServer) {
 | 
| +      _serverSocket.close();
 | 
| +    }
 | 
| +    if (_sessionManagerInstance != null) {
 | 
| +      _sessionManagerInstance.close();
 | 
| +      _sessionManagerInstance = null;
 | 
| +    }
 | 
| +    for (_HttpConnection connection in _connections.toList()) {
 | 
| +      connection.destroy();
 | 
|      }
 | 
| +    _connections.clear();
 | 
|    }
 | 
|  
 | 
| -  void _doRedirect(_RedirectInfo redirect) {
 | 
| -    assert(_socketConn == null);
 | 
| -
 | 
| -    if (_redirects == null) {
 | 
| -      _redirects = new List<_RedirectInfo>();
 | 
| -    }
 | 
| -    _redirects.add(redirect);
 | 
| -    _doRetry(redirect);
 | 
| +  int get port {
 | 
| +    if (closed) throw new HttpException("HttpServer is not bound to a socket");
 | 
| +    return _serverSocket.port;
 | 
|    }
 | 
|  
 | 
| -  void redirect([String method, Uri url]) {
 | 
| -    if (method == null) method = _method;
 | 
| -    if (url == null) {
 | 
| -      url = Uri.parse(_response.headers.value(HttpHeaders.LOCATION));
 | 
| -    }
 | 
| -    // Always set the content length to 0 for redirects.
 | 
| -    var mutable = _request._headers._mutable;
 | 
| -    _request._headers._mutable = true;
 | 
| -    _request._headers.contentLength = 0;
 | 
| -    _request._headers._mutable = mutable;
 | 
| -    _request._bodyBytesWritten = 0;
 | 
| -    var redirect = new _RedirectInfo(_response.statusCode, method, url);
 | 
| -    // The actual redirect is postponed until both response and
 | 
| -    // request are done.
 | 
| -    assert(_pendingRetry == null);
 | 
| -    _pendingRedirect = redirect;
 | 
| -  }
 | 
| -
 | 
| -  List<RedirectInfo> get redirects => _redirects;
 | 
| -
 | 
| -  Function _onRequest;
 | 
| -  Function _onResponse;
 | 
| -  Function _onErrorCallback;
 | 
| -
 | 
| -  _HttpClient _client;
 | 
| -  _SocketConnection _socketConn;
 | 
| -  HttpClientRequest _request;
 | 
| -  HttpClientResponse _response;
 | 
| -  String _method;
 | 
| -  bool _usingProxy;
 | 
| -
 | 
| -  // Redirect handling
 | 
| -  bool followRedirects = true;
 | 
| -  int maxRedirects = 5;
 | 
| -  List<_RedirectInfo> _redirects;
 | 
| -  _RedirectInfo _pendingRedirect;
 | 
| -  _RedirectInfo _pendingRetry;
 | 
| -
 | 
| -  // Callbacks.
 | 
| -  var requestReceived;
 | 
| -}
 | 
| +  set sessionTimeout(int timeout) {
 | 
| +    _sessionManager.sessionTimeout = timeout;
 | 
| +  }
 | 
|  
 | 
| +  void _handleRequest(HttpRequest request) {
 | 
| +    _controller.add(request);
 | 
| +  }
 | 
|  
 | 
| -// Class for holding keep-alive sockets in the cache for the HTTP
 | 
| -// client together with the connection information.
 | 
| -class _SocketConnection {
 | 
| -  _SocketConnection(String this._host,
 | 
| -                    int this._port,
 | 
| -                    Socket this._socket);
 | 
| +  void _handleError(AsyncError error) {
 | 
| +    if (!closed) _controller.signalError(error);
 | 
| +  }
 | 
|  
 | 
| -  void _markReturned() {
 | 
| -    // Any activity on the socket while waiting in the pool will
 | 
| -    // invalidate the connection os that it is not reused.
 | 
| -    _socket.onData = _invalidate;
 | 
| -    _socket.onClosed = _invalidate;
 | 
| -    _socket.onError = (_) => _invalidate();
 | 
| -    _returnTime = new DateTime.now();
 | 
| -    _httpClientConnection = null;
 | 
| +  void _connectionClosed(_HttpConnection connection) {
 | 
| +    _connections.remove(connection);
 | 
|    }
 | 
|  
 | 
| -  void _markRetrieved() {
 | 
| -    _socket.onData = null;
 | 
| -    _socket.onClosed = null;
 | 
| -    _socket.onError = null;
 | 
| -    _httpClientConnection = null;
 | 
| +  _HttpSessionManager get _sessionManager {
 | 
| +    // Lazy init.
 | 
| +    if (_sessionManagerInstance == null) {
 | 
| +      _sessionManagerInstance = new _HttpSessionManager();
 | 
| +    }
 | 
| +    return _sessionManagerInstance;
 | 
|    }
 | 
|  
 | 
| -  void _close() {
 | 
| -    _socket.onData = null;
 | 
| -    _socket.onClosed = null;
 | 
| -    _socket.onError = null;
 | 
| -    _httpClientConnection = null;
 | 
| -    _socket.close();
 | 
| +  HttpConnectionsInfo connectionsInfo() {
 | 
| +    HttpConnectionsInfo result = new HttpConnectionsInfo();
 | 
| +    result.total = _connections.length;
 | 
| +    _connections.forEach((_HttpConnection conn) {
 | 
| +      if (conn._isActive) {
 | 
| +        result.active++;
 | 
| +      } else if (conn._isIdle) {
 | 
| +        result.idle++;
 | 
| +      } else {
 | 
| +        assert(conn._isClosing);
 | 
| +        result.closing++;
 | 
| +      }
 | 
| +    });
 | 
| +    return result;
 | 
|    }
 | 
|  
 | 
| -  Duration _idleTime(DateTime now) => now.difference(_returnTime);
 | 
| +  _HttpSessionManager _sessionManagerInstance;
 | 
|  
 | 
| -  bool get _fromPool => _returnTime != null;
 | 
| +  // Indicated if the http server has been closed.
 | 
| +  bool closed = false;
 | 
|  
 | 
| -  void _invalidate() {
 | 
| -    _valid = false;
 | 
| -    _close();
 | 
| -  }
 | 
| +  // The server listen socket.
 | 
| +  final ServerSocket _serverSocket;
 | 
| +  final bool _closeServer;
 | 
|  
 | 
| -  int get hashCode => _socket.hashCode;
 | 
| +  // Set of currently connected clients.
 | 
| +  final Set<_HttpConnection> _connections = new Set<_HttpConnection>();
 | 
| +  final StreamController<HttpRequest> _controller
 | 
| +      = new StreamController<HttpRequest>();
 | 
|  
 | 
| -  String _host;
 | 
| -  int _port;
 | 
| -  Socket _socket;
 | 
| -  DateTime _returnTime;
 | 
| -  bool _valid = true;
 | 
| -  HttpClientConnection _httpClientConnection;
 | 
| +  // TODO(ajohnsen): Use close queue?
 | 
|  }
 | 
|  
 | 
| +
 | 
|  class _ProxyConfiguration {
 | 
|    static const String PROXY_PREFIX = "PROXY ";
 | 
|    static const String DIRECT_PREFIX = "DIRECT";
 | 
| @@ -1844,6 +1485,7 @@ class _ProxyConfiguration {
 | 
|    final List<_Proxy> proxies;
 | 
|  }
 | 
|  
 | 
| +
 | 
|  class _Proxy {
 | 
|    const _Proxy(this.host, this.port) : isDirect = false;
 | 
|    const _Proxy.direct() : host = null, port = null, isDirect = true;
 | 
| @@ -1853,376 +1495,59 @@ class _Proxy {
 | 
|    final bool isDirect;
 | 
|  }
 | 
|  
 | 
| -class _HttpClient implements HttpClient {
 | 
| -  static const int DEFAULT_EVICTION_TIMEOUT = 60000;
 | 
| -
 | 
| -  _HttpClient() : _openSockets = new Map(),
 | 
| -                  _activeSockets = new Set(),
 | 
| -                  _closeQueue = new _CloseQueue(),
 | 
| -                  credentials = new List<_Credentials>(),
 | 
| -                  _shutdown = false;
 | 
| -
 | 
| -  HttpClientConnection open(
 | 
| -      String method, String host, int port, String path) {
 | 
| -    // TODO(sgjesse): The path set here can contain both query and
 | 
| -    // fragment. They should be cracked and set correctly.
 | 
| -    return _open(method, new Uri.fromComponents(
 | 
| -        scheme: "http", domain: host, port: port, path: path));
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection _open(String method,
 | 
| -                             Uri uri,
 | 
| -                             [_HttpClientConnection connection]) {
 | 
| -    if (_shutdown) throw new HttpException("HttpClient shutdown");
 | 
| -    if (method == null || uri.domain.isEmpty) {
 | 
| -      throw new ArgumentError(null);
 | 
| -    }
 | 
| -    return _prepareHttpClientConnection(method, uri, connection);
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection openUrl(String method, Uri url) {
 | 
| -    return _openUrl(method, url);
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection _openUrl(String method,
 | 
| -                                Uri url,
 | 
| -                                [_HttpClientConnection connection]) {
 | 
| -    if (url.scheme != "http" && url.scheme != "https") {
 | 
| -      throw new HttpException("Unsupported URL scheme ${url.scheme}");
 | 
| -    }
 | 
| -    return _open(method, url, connection);
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection get(String host, int port, String path) {
 | 
| -    return open("GET", host, port, path);
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection getUrl(Uri url) => _openUrl("GET", url);
 | 
| -
 | 
| -  HttpClientConnection post(String host, int port, String path) {
 | 
| -    return open("POST", host, port, path);
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection postUrl(Uri url) => _openUrl("POST", url);
 | 
| -
 | 
| -  set authenticate(Future<bool> f(Uri url, String scheme, String realm)) {
 | 
| -    _authenticate = f;
 | 
| -  }
 | 
| -
 | 
| -  void addCredentials(
 | 
| -      Uri url, String realm, HttpClientCredentials cr) {
 | 
| -    credentials.add(new _Credentials(url, realm, cr));
 | 
| -  }
 | 
| -
 | 
| -  set sendClientCertificate(bool send) => _sendClientCertificate = send;
 | 
| -
 | 
| -  set clientCertificate(String nickname) => _clientCertificate = nickname;
 | 
| -
 | 
| -  set findProxy(String f(Uri uri)) => _findProxy = f;
 | 
| -
 | 
| -  void shutdown({bool force: false}) {
 | 
| -    if (force) _closeQueue.shutdown();
 | 
| -    new Map.from(_openSockets).forEach(
 | 
| -        (String key, Queue<_SocketConnection> connections) {
 | 
| -      while (!connections.isEmpty) {
 | 
| -        _SocketConnection socketConn = connections.removeFirst();
 | 
| -        socketConn._socket.close();
 | 
| -      }
 | 
| -    });
 | 
| -    if (force) {
 | 
| -      _activeSockets.toList().forEach((_SocketConnection socketConn) {
 | 
| -        socketConn._httpClientConnection._onClientShutdown();
 | 
| -        socketConn._close();
 | 
| -      });
 | 
| -    }
 | 
| -    if (_evictionTimer != null) _cancelEvictionTimer();
 | 
| -    _shutdown = true;
 | 
| -  }
 | 
| -
 | 
| -  void _cancelEvictionTimer() {
 | 
| -    _evictionTimer.cancel();
 | 
| -    _evictionTimer = null;
 | 
| -  }
 | 
| -
 | 
| -  String _connectionKey(String host, int port) {
 | 
| -    return "$host:$port";
 | 
| -  }
 | 
| -
 | 
| -  HttpClientConnection _prepareHttpClientConnection(
 | 
| -      String method,
 | 
| -      Uri url,
 | 
| -      [_HttpClientConnection connection]) {
 | 
| -
 | 
| -    void _establishConnection(String host,
 | 
| -                              int port,
 | 
| -                              _ProxyConfiguration proxyConfiguration,
 | 
| -                              int proxyIndex,
 | 
| -                              bool reusedConnection,
 | 
| -                              bool secure) {
 | 
| -
 | 
| -      void _connectionOpened(_SocketConnection socketConn,
 | 
| -                             _HttpClientConnection connection,
 | 
| -                             bool usingProxy) {
 | 
| -        socketConn._httpClientConnection = connection;
 | 
| -        connection._usingProxy = usingProxy;
 | 
| -        connection._connectionEstablished(socketConn);
 | 
| -        HttpClientRequest request = connection.open(method, url);
 | 
| -        request.headers.host = host;
 | 
| -        request.headers.port = port;
 | 
| -        if (url.userInfo != null && !url.userInfo.isEmpty) {
 | 
| -          // If the URL contains user information use that for basic
 | 
| -          // authorization
 | 
| -          _UTF8Encoder encoder = new _UTF8Encoder();
 | 
| -          String auth =
 | 
| -              CryptoUtils.bytesToBase64(encoder.encodeString(url.userInfo));
 | 
| -          request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth");
 | 
| -        } else {
 | 
| -          // Look for credentials.
 | 
| -          _Credentials cr = _findCredentials(url);
 | 
| -          if (cr != null) {
 | 
| -            cr.authorize(request);
 | 
| -          }
 | 
| -        }
 | 
| -        // A reused connection is indicating either redirect or retry
 | 
| -        // where the onRequest callback should not be issued again.
 | 
| -        if (connection._onRequest != null && !reusedConnection) {
 | 
| -          connection._onRequest(request);
 | 
| -        } else {
 | 
| -          request.outputStream.close();
 | 
| -        }
 | 
| -      }
 | 
| -
 | 
| -      assert(proxyIndex < proxyConfiguration.proxies.length);
 | 
| -
 | 
| -      // Determine the actual host to connect to.
 | 
| -      String connectHost;
 | 
| -      int connectPort;
 | 
| -      _Proxy proxy = proxyConfiguration.proxies[proxyIndex];
 | 
| -      if (proxy.isDirect) {
 | 
| -        connectHost = host;
 | 
| -        connectPort = port;
 | 
| -      } else {
 | 
| -        connectHost = proxy.host;
 | 
| -        connectPort = proxy.port;
 | 
| -      }
 | 
| -
 | 
| -      // If there are active connections for this key get the first one
 | 
| -      // otherwise create a new one.
 | 
| -      String key = _connectionKey(connectHost, connectPort);
 | 
| -      Queue socketConnections = _openSockets[key];
 | 
| -      // Remove active connections that are not valid any more or of
 | 
| -      // the wrong type (HTTP or HTTPS).
 | 
| -      if (socketConnections != null) {
 | 
| -        while (!socketConnections.isEmpty) {
 | 
| -          if (socketConnections.first._valid) {
 | 
| -            // If socket has the same properties, exit loop with found socket.
 | 
| -            var socket = socketConnections.first._socket;
 | 
| -            if (!secure && socket is! SecureSocket) break;
 | 
| -            if (secure && socket is SecureSocket &&
 | 
| -                 _sendClientCertificate == socket.sendClientCertificate &&
 | 
| -                 _clientCertificate == socket.certificateName) break;
 | 
| -          }
 | 
| -          socketConnections.removeFirst()._close();
 | 
| -        }
 | 
| -      }
 | 
| -      if (socketConnections == null || socketConnections.isEmpty) {
 | 
| -        Socket socket = secure && proxy.isDirect ?
 | 
| -            new SecureSocket(connectHost,
 | 
| -                             connectPort,
 | 
| -                             sendClientCertificate: _sendClientCertificate,
 | 
| -                             certificateName: _clientCertificate) :
 | 
| -            new Socket(connectHost, connectPort);
 | 
| -        // Until the connection is established handle connection errors
 | 
| -        // here as the HttpClientConnection object is not yet associated
 | 
| -        // with the socket.
 | 
| -        socket.onError = (e) {
 | 
| -          proxyIndex++;
 | 
| -          if (proxyIndex < proxyConfiguration.proxies.length) {
 | 
| -            // Try the next proxy in the list.
 | 
| -            _establishConnection(
 | 
| -                host, port, proxyConfiguration, proxyIndex, false, secure);
 | 
| -          } else {
 | 
| -            // Report the error through the HttpClientConnection object to
 | 
| -            // the client.
 | 
| -            connection._onError(e);
 | 
| -          }
 | 
| -        };
 | 
| -        socket.onConnect = () {
 | 
| -          // When the connection is established, clear the error
 | 
| -          // callback as it will now be handled by the
 | 
| -          // HttpClientConnection object which will be associated with
 | 
| -          // the connected socket.
 | 
| -          socket.onError = null;
 | 
| -          _SocketConnection socketConn =
 | 
| -              new _SocketConnection(connectHost, connectPort, socket);
 | 
| -          _activeSockets.add(socketConn);
 | 
| -          _connectionOpened(socketConn, connection, !proxy.isDirect);
 | 
| -        };
 | 
| -      } else {
 | 
| -        _SocketConnection socketConn = socketConnections.removeFirst();
 | 
| -        socketConn._markRetrieved();
 | 
| -        _activeSockets.add(socketConn);
 | 
| -        Timer.run(() =>
 | 
| -                  _connectionOpened(socketConn, connection, !proxy.isDirect));
 | 
| -
 | 
| -        // Get rid of eviction timer if there are no more active connections.
 | 
| -        if (socketConnections.isEmpty) _openSockets.remove(key);
 | 
| -        if (_openSockets.isEmpty) _cancelEvictionTimer();
 | 
| -      }
 | 
| -    }
 | 
| -
 | 
| -    // Find out if we want a secure socket.
 | 
| -    bool is_secure = (url.scheme == "https");
 | 
| -
 | 
| -    // Find the TCP host and port.
 | 
| -    String host = url.domain;
 | 
| -    int port = url.port;
 | 
| -    if (port == 0) {
 | 
| -      port = is_secure ?
 | 
| -          HttpClient.DEFAULT_HTTPS_PORT :
 | 
| -          HttpClient.DEFAULT_HTTP_PORT;
 | 
| -    }
 | 
| -    // Create a new connection object if we are not re-using an existing one.
 | 
| -    var reusedConnection = false;
 | 
| -    if (connection == null) {
 | 
| -      connection = new _HttpClientConnection(this);
 | 
| -    } else {
 | 
| -      reusedConnection = true;
 | 
| -    }
 | 
| -    connection.onDetach = () => _activeSockets.remove(connection._socketConn);
 | 
| -
 | 
| -    // Check to see if a proxy server should be used for this connection.
 | 
| -    _ProxyConfiguration proxyConfiguration = const _ProxyConfiguration.direct();
 | 
| -    if (_findProxy != null) {
 | 
| -      // TODO(sgjesse): Keep a map of these as normally only a few
 | 
| -      // configuration strings will be used.
 | 
| -      proxyConfiguration = new _ProxyConfiguration(_findProxy(url));
 | 
| -    }
 | 
| -
 | 
| -    // Establish the connection starting with the first proxy configured.
 | 
| -    _establishConnection(host,
 | 
| -                         port,
 | 
| -                         proxyConfiguration,
 | 
| -                         0,
 | 
| -                         reusedConnection,
 | 
| -                         is_secure);
 | 
|  
 | 
| -    return connection;
 | 
| +class _HttpConnectionInfo implements HttpConnectionInfo {
 | 
| +  static _HttpConnectionInfo create(Socket socket) {
 | 
| +    if (socket == null) return null;
 | 
| +    try {
 | 
| +      _HttpConnectionInfo info = new _HttpConnectionInfo._();
 | 
| +      info.remoteHost = socket.remoteHost;
 | 
| +      info.remotePort = socket.remotePort;
 | 
| +      info.localPort = socket.port;
 | 
| +      return info;
 | 
| +    } catch (e) { }
 | 
| +    return null;
 | 
|    }
 | 
|  
 | 
| -  void _returnSocketConnection(_SocketConnection socketConn) {
 | 
| -    // If the HTTP client is being shutdown don't return the connection.
 | 
| -    if (_shutdown) {
 | 
| -      socketConn._close();
 | 
| -      return;
 | 
| -    };
 | 
| -
 | 
| -    // Mark socket as returned to unregister from the old connection.
 | 
| -    socketConn._markReturned();
 | 
| -
 | 
| -    String key = _connectionKey(socketConn._host, socketConn._port);
 | 
| +  _HttpConnectionInfo._();
 | 
|  
 | 
| -    // Get or create the connection list for this key.
 | 
| -    Queue sockets = _openSockets[key];
 | 
| -    if (sockets == null) {
 | 
| -      sockets = new Queue();
 | 
| -      _openSockets[key] = sockets;
 | 
| -    }
 | 
| -
 | 
| -    // If there is currently no eviction timer start one.
 | 
| -    if (_evictionTimer == null) {
 | 
| -      void _handleEviction(Timer timer) {
 | 
| -        DateTime now = new DateTime.now();
 | 
| -        List<String> emptyKeys = new List<String>();
 | 
| -        _openSockets.forEach(
 | 
| -            (String key, Queue<_SocketConnection> connections) {
 | 
| -              // As returned connections are added at the head of the
 | 
| -              // list remove from the tail.
 | 
| -              while (!connections.isEmpty) {
 | 
| -                _SocketConnection socketConn = connections.last;
 | 
| -                if (socketConn._idleTime(now).inMilliseconds >
 | 
| -                    DEFAULT_EVICTION_TIMEOUT) {
 | 
| -                  connections.removeLast();
 | 
| -                  socketConn._socket.close();
 | 
| -                  if (connections.isEmpty) emptyKeys.add(key);
 | 
| -                } else {
 | 
| -                  break;
 | 
| -                }
 | 
| -              }
 | 
| -            });
 | 
| +  String remoteHost;
 | 
| +  int remotePort;
 | 
| +  int localPort;
 | 
| +}
 | 
|  
 | 
| -        // Remove the keys for which here are no more open connections.
 | 
| -        emptyKeys.forEach((String key) => _openSockets.remove(key));
 | 
|  
 | 
| -        // If all connections where evicted cancel the eviction timer.
 | 
| -        if (_openSockets.isEmpty) _cancelEvictionTimer();
 | 
| -      }
 | 
| -      _evictionTimer = new Timer.repeating(const Duration(seconds: 10),
 | 
| -                                           _handleEviction);
 | 
| -    }
 | 
| +class _DetachedSocket implements Socket {
 | 
| +  final Stream<List<int>> _incoming;
 | 
| +  final Socket _socket;
 | 
|  
 | 
| -    // Return connection.
 | 
| -    _activeSockets.remove(socketConn);
 | 
| -    sockets.addFirst(socketConn);
 | 
| -  }
 | 
| +  _DetachedSocket(this._socket, this._incoming);
 | 
|  
 | 
| -  void _closeSocketConnection(_SocketConnection socketConn) {
 | 
| -    socketConn._close();
 | 
| -    _activeSockets.remove(socketConn);
 | 
| +  StreamSubscription<List<int>> listen(void onData(List<int> event),
 | 
| +                                       {void onError(AsyncError error),
 | 
| +                                        void onDone(),
 | 
| +                                        bool unsubscribeOnError}) {
 | 
| +    return _incoming.listen(onData,
 | 
| +                            onError: onError,
 | 
| +                            onDone: onDone,
 | 
| +                            unsubscribeOnError: unsubscribeOnError);
 | 
|    }
 | 
|  
 | 
| -  void _closedSocketConnection(_SocketConnection socketConn) {
 | 
| -    _activeSockets.remove(socketConn);
 | 
| +  Future<Socket> consume(Stream<List<int>> stream) {
 | 
| +    return _socket.consume(stream);
 | 
|    }
 | 
|  
 | 
| -  _Credentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) {
 | 
| -    // Look for credentials.
 | 
| -    _Credentials cr =
 | 
| -        credentials.reduce(null, (_Credentials prev, _Credentials value) {
 | 
| -          if (value.applies(url, scheme)) {
 | 
| -            if (prev == null) return value;
 | 
| -            return value.uri.path.length > prev.uri.path.length ? value : prev;
 | 
| -          } else {
 | 
| -            return prev;
 | 
| -          }
 | 
| -        });
 | 
| -    return cr;
 | 
| +  Future<Socket> addStream(Stream<List<int>> stream) {
 | 
| +    return _socket.addStream(stream);
 | 
|    }
 | 
|  
 | 
| -  void _removeCredentials(_Credentials cr) {
 | 
| -    int index = credentials.indexOf(cr);
 | 
| -    if (index != -1) {
 | 
| -      credentials.removeAt(index);
 | 
| -    }
 | 
| +  void addString(String string, [Encoding encoding = Encoding.UTF_8]) {
 | 
| +    return _socket.addString(string, encoding);
 | 
|    }
 | 
|  
 | 
| -  Function _onOpen;
 | 
| -  Map<String, Queue<_SocketConnection>> _openSockets;
 | 
| -  Set<_SocketConnection> _activeSockets;
 | 
| -  _CloseQueue _closeQueue;
 | 
| -  List<_Credentials> credentials;
 | 
| -  Timer _evictionTimer;
 | 
| -  Function _findProxy;
 | 
| -  Function _authenticate;
 | 
| -  bool _sendClientCertificate = false;
 | 
| -  String _clientCertificate;
 | 
| -  bool _shutdown;  // Has this HTTP client been shutdown?
 | 
| -}
 | 
| -
 | 
| -
 | 
| -class _HttpConnectionInfo implements HttpConnectionInfo {
 | 
| -  String remoteHost;
 | 
| -  int remotePort;
 | 
| -  int localPort;
 | 
| -}
 | 
| -
 | 
| -
 | 
| -class _DetachedSocket implements DetachedSocket {
 | 
| -  _DetachedSocket(this._socket, this._unparsedData);
 | 
| -  Socket get socket => _socket;
 | 
| -  List<int> get unparsedData => _unparsedData;
 | 
| -  Socket _socket;
 | 
| -  List<int> _unparsedData;
 | 
| +  void destroy() => _socket.destroy();
 | 
| +  void add(List<int> data) => _socket.add(data);
 | 
| +  Future<Socket> close() => _socket.close();
 | 
|  }
 | 
|  
 | 
|  
 | 
| @@ -2300,10 +1625,8 @@ class _HttpClientBasicCredentials implements HttpClientBasicCredentials {
 | 
|      // Proxy-Authenticate headers, see
 | 
|      // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For
 | 
|      // now always use UTF-8 encoding.
 | 
| -    _UTF8Encoder encoder = new _UTF8Encoder();
 | 
|      String auth =
 | 
| -        CryptoUtils.bytesToBase64(encoder.encodeString(
 | 
| -            "$username:$password"));
 | 
| +        CryptoUtils.bytesToBase64(_encodeString("$username:$password"));
 | 
|      request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth");
 | 
|    }
 | 
|  
 | 
| @@ -2328,7 +1651,6 @@ class _HttpClientDigestCredentials implements HttpClientDigestCredentials {
 | 
|  }
 | 
|  
 | 
|  
 | 
| -
 | 
|  class _RedirectInfo implements RedirectInfo {
 | 
|    const _RedirectInfo(int this.statusCode,
 | 
|                        String this.method,
 | 
| 
 |