OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 part of dart.io; |
| 6 |
| 7 const int _OUTGOING_BUFFER_SIZE = 8 * 1024; |
| 8 |
| 9 class _HttpIncoming extends Stream<List<int>> { |
| 10 final int _transferLength; |
| 11 final Completer _dataCompleter = new Completer(); |
| 12 Stream<List<int>> _stream; |
| 13 |
| 14 bool fullBodyRead = false; |
| 15 |
| 16 // Common properties. |
| 17 final _HttpHeaders headers; |
| 18 bool upgraded = false; |
| 19 |
| 20 // ClientResponse properties. |
| 21 int statusCode; |
| 22 String reasonPhrase; |
| 23 |
| 24 // Request properties. |
| 25 String method; |
| 26 Uri uri; |
| 27 |
| 28 bool hasSubscriber = false; |
| 29 |
| 30 // The transfer length if the length of the message body as it |
| 31 // appears in the message (RFC 2616 section 4.4). This can be -1 if |
| 32 // the length of the massage body is not known due to transfer |
| 33 // codings. |
| 34 int get transferLength => _transferLength; |
| 35 |
| 36 _HttpIncoming(this.headers, this._transferLength, this._stream); |
| 37 |
| 38 StreamSubscription<List<int>> listen(void onData(List<int> event), |
| 39 {Function onError, |
| 40 void onDone(), |
| 41 bool cancelOnError}) { |
| 42 hasSubscriber = true; |
| 43 return _stream |
| 44 .handleError((error) { |
| 45 throw new HttpException(error.message, uri: uri); |
| 46 }) |
| 47 .listen(onData, |
| 48 onError: onError, |
| 49 onDone: onDone, |
| 50 cancelOnError: cancelOnError); |
| 51 } |
| 52 |
| 53 // Is completed once all data have been received. |
| 54 Future get dataDone => _dataCompleter.future; |
| 55 |
| 56 void close(bool closing) { |
| 57 fullBodyRead = true; |
| 58 hasSubscriber = true; |
| 59 _dataCompleter.complete(closing); |
| 60 } |
| 61 } |
| 62 |
| 63 abstract class _HttpInboundMessage extends Stream<List<int>> { |
| 64 final _HttpIncoming _incoming; |
| 65 List<Cookie> _cookies; |
| 66 |
| 67 _HttpInboundMessage(this._incoming); |
| 68 |
| 69 List<Cookie> get cookies { |
| 70 if (_cookies != null) return _cookies; |
| 71 return _cookies = headers._parseCookies(); |
| 72 } |
| 73 |
| 74 _HttpHeaders get headers => _incoming.headers; |
| 75 String get protocolVersion => headers.protocolVersion; |
| 76 int get contentLength => headers.contentLength; |
| 77 bool get persistentConnection => headers.persistentConnection; |
| 78 } |
| 79 |
| 80 |
| 81 class _HttpRequest extends _HttpInboundMessage implements HttpRequest { |
| 82 final HttpResponse response; |
| 83 |
| 84 final _HttpServer _httpServer; |
| 85 |
| 86 final _HttpConnection _httpConnection; |
| 87 |
| 88 _HttpSession _session; |
| 89 |
| 90 Uri _requestedUri; |
| 91 |
| 92 _HttpRequest(this.response, _HttpIncoming _incoming, this._httpServer, |
| 93 this._httpConnection) : super(_incoming) { |
| 94 if (headers.protocolVersion == "1.1") { |
| 95 response.headers |
| 96 ..chunkedTransferEncoding = true |
| 97 ..persistentConnection = headers.persistentConnection; |
| 98 } |
| 99 |
| 100 if (_httpServer._sessionManagerInstance != null) { |
| 101 // Map to session if exists. |
| 102 var sessionIds = cookies |
| 103 .where((cookie) => cookie.name.toUpperCase() == _DART_SESSION_ID) |
| 104 .map((cookie) => cookie.value); |
| 105 for (var sessionId in sessionIds) { |
| 106 _session = _httpServer._sessionManager.getSession(sessionId); |
| 107 if (_session != null) { |
| 108 _session._markSeen(); |
| 109 break; |
| 110 } |
| 111 } |
| 112 } |
| 113 } |
| 114 |
| 115 StreamSubscription<List<int>> listen(void onData(List<int> event), |
| 116 {Function onError, |
| 117 void onDone(), |
| 118 bool cancelOnError}) { |
| 119 return _incoming.listen(onData, |
| 120 onError: onError, |
| 121 onDone: onDone, |
| 122 cancelOnError: cancelOnError); |
| 123 } |
| 124 |
| 125 Uri get uri => _incoming.uri; |
| 126 |
| 127 Uri get requestedUri { |
| 128 if (_requestedUri == null) { |
| 129 var proto = headers['x-forwarded-proto']; |
| 130 var scheme = proto != null ? proto.first : |
| 131 _httpConnection._socket is SecureSocket ? "https" : "http"; |
| 132 var hostList = headers['x-forwarded-host']; |
| 133 String host; |
| 134 if (hostList != null) { |
| 135 host = hostList.first; |
| 136 } else { |
| 137 hostList = headers['host']; |
| 138 if (hostList != null) { |
| 139 host = hostList.first; |
| 140 } else { |
| 141 host = "${_httpServer.address.host}:${_httpServer.port}"; |
| 142 } |
| 143 } |
| 144 _requestedUri = Uri.parse("$scheme://$host$uri"); |
| 145 } |
| 146 return _requestedUri; |
| 147 } |
| 148 |
| 149 String get method => _incoming.method; |
| 150 |
| 151 HttpSession get session { |
| 152 if (_session != null) { |
| 153 if (_session._destroyed) { |
| 154 // It's destroyed, clear it. |
| 155 _session = null; |
| 156 // Create new session object by calling recursive. |
| 157 return session; |
| 158 } |
| 159 // It's already mapped, use it. |
| 160 return _session; |
| 161 } |
| 162 // Create session, store it in connection, and return. |
| 163 return _session = _httpServer._sessionManager.createSession(); |
| 164 } |
| 165 |
| 166 HttpConnectionInfo get connectionInfo => _httpConnection.connectionInfo; |
| 167 |
| 168 X509Certificate get certificate { |
| 169 var socket = _httpConnection._socket; |
| 170 if (socket is SecureSocket) return socket.peerCertificate; |
| 171 return null; |
| 172 } |
| 173 } |
| 174 |
| 175 |
| 176 class _HttpClientResponse |
| 177 extends _HttpInboundMessage implements HttpClientResponse { |
| 178 List<RedirectInfo> get redirects => _httpRequest._responseRedirects; |
| 179 |
| 180 // The HttpClient this response belongs to. |
| 181 final _HttpClient _httpClient; |
| 182 |
| 183 // The HttpClientRequest of this response. |
| 184 final _HttpClientRequest _httpRequest; |
| 185 |
| 186 _HttpClientResponse(_HttpIncoming _incoming, this._httpRequest, |
| 187 this._httpClient) : super(_incoming) { |
| 188 // Set uri for potential exceptions. |
| 189 _incoming.uri = _httpRequest.uri; |
| 190 } |
| 191 |
| 192 int get statusCode => _incoming.statusCode; |
| 193 String get reasonPhrase => _incoming.reasonPhrase; |
| 194 |
| 195 X509Certificate get certificate { |
| 196 var socket = _httpRequest._httpClientConnection._socket; |
| 197 if (socket is SecureSocket) return socket.peerCertificate; |
| 198 throw new UnsupportedError("Socket is not a SecureSocket"); |
| 199 } |
| 200 |
| 201 List<Cookie> get cookies { |
| 202 if (_cookies != null) return _cookies; |
| 203 _cookies = new List<Cookie>(); |
| 204 List<String> values = headers[HttpHeaders.SET_COOKIE]; |
| 205 if (values != null) { |
| 206 values.forEach((value) { |
| 207 _cookies.add(new Cookie.fromSetCookieValue(value)); |
| 208 }); |
| 209 } |
| 210 return _cookies; |
| 211 } |
| 212 |
| 213 bool get isRedirect { |
| 214 if (_httpRequest.method == "GET" || _httpRequest.method == "HEAD") { |
| 215 return statusCode == HttpStatus.MOVED_PERMANENTLY || |
| 216 statusCode == HttpStatus.FOUND || |
| 217 statusCode == HttpStatus.SEE_OTHER || |
| 218 statusCode == HttpStatus.TEMPORARY_REDIRECT; |
| 219 } else if (_httpRequest.method == "POST") { |
| 220 return statusCode == HttpStatus.SEE_OTHER; |
| 221 } |
| 222 return false; |
| 223 } |
| 224 |
| 225 Future<HttpClientResponse> redirect([String method, |
| 226 Uri url, |
| 227 bool followLoops]) { |
| 228 if (method == null) { |
| 229 // Set method as defined by RFC 2616 section 10.3.4. |
| 230 if (statusCode == HttpStatus.SEE_OTHER && _httpRequest.method == "POST") { |
| 231 method = "GET"; |
| 232 } else { |
| 233 method = _httpRequest.method; |
| 234 } |
| 235 } |
| 236 if (url == null) { |
| 237 String location = headers.value(HttpHeaders.LOCATION); |
| 238 if (location == null) { |
| 239 throw new StateError("Response has no Location header for redirect"); |
| 240 } |
| 241 url = Uri.parse(location); |
| 242 } |
| 243 if (followLoops != true) { |
| 244 for (var redirect in redirects) { |
| 245 if (redirect.location == url) { |
| 246 return new Future.error( |
| 247 new RedirectException("Redirect loop detected", redirects)); |
| 248 } |
| 249 } |
| 250 } |
| 251 return _httpClient._openUrlFromRequest(method, url, _httpRequest) |
| 252 .then((request) { |
| 253 request._responseRedirects |
| 254 ..addAll(this.redirects) |
| 255 ..add(new _RedirectInfo(statusCode, method, url)); |
| 256 return request.close(); |
| 257 }); |
| 258 } |
| 259 |
| 260 StreamSubscription<List<int>> listen(void onData(List<int> event), |
| 261 {Function onError, |
| 262 void onDone(), |
| 263 bool cancelOnError}) { |
| 264 if (_incoming.upgraded) { |
| 265 // If upgraded, the connection is already 'removed' form the client. |
| 266 // Since listening to upgraded data is 'bogus', simply close and |
| 267 // return empty stream subscription. |
| 268 _httpRequest._httpClientConnection.destroy(); |
| 269 return new Stream.fromIterable([]).listen(null, onDone: onDone); |
| 270 } |
| 271 var stream = _incoming; |
| 272 if (_httpClient.autoUncompress && |
| 273 headers.value(HttpHeaders.CONTENT_ENCODING) == "gzip") { |
| 274 stream = stream.transform(GZIP.decoder); |
| 275 } |
| 276 return stream.listen(onData, |
| 277 onError: onError, |
| 278 onDone: onDone, |
| 279 cancelOnError: cancelOnError); |
| 280 } |
| 281 |
| 282 Future<Socket> detachSocket() { |
| 283 _httpClient._connectionClosed(_httpRequest._httpClientConnection); |
| 284 return _httpRequest._httpClientConnection.detachSocket(); |
| 285 } |
| 286 |
| 287 HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo; |
| 288 |
| 289 bool get _shouldAuthenticateProxy { |
| 290 // Only try to authenticate if there is a challenge in the response. |
| 291 List<String> challenge = headers[HttpHeaders.PROXY_AUTHENTICATE]; |
| 292 return statusCode == HttpStatus.PROXY_AUTHENTICATION_REQUIRED && |
| 293 challenge != null && challenge.length == 1; |
| 294 } |
| 295 |
| 296 bool get _shouldAuthenticate { |
| 297 // Only try to authenticate if there is a challenge in the response. |
| 298 List<String> challenge = headers[HttpHeaders.WWW_AUTHENTICATE]; |
| 299 return statusCode == HttpStatus.UNAUTHORIZED && |
| 300 challenge != null && challenge.length == 1; |
| 301 } |
| 302 |
| 303 Future<HttpClientResponse> _authenticate(bool proxyAuth) { |
| 304 Future<HttpClientResponse> retry() { |
| 305 // Drain body and retry. |
| 306 return drain().then((_) { |
| 307 return _httpClient._openUrlFromRequest(_httpRequest.method, |
| 308 _httpRequest.uri, |
| 309 _httpRequest) |
| 310 .then((request) => request.close()); |
| 311 }); |
| 312 } |
| 313 |
| 314 List<String> authChallenge() { |
| 315 return proxyAuth ? headers[HttpHeaders.PROXY_AUTHENTICATE] |
| 316 : headers[HttpHeaders.WWW_AUTHENTICATE]; |
| 317 } |
| 318 |
| 319 _Credentials findCredentials(_AuthenticationScheme scheme) { |
| 320 return proxyAuth ? _httpClient._findProxyCredentials(_httpRequest._proxy, |
| 321 scheme) |
| 322 : _httpClient._findCredentials(_httpRequest.uri, scheme); |
| 323 } |
| 324 |
| 325 void removeCredentials(_Credentials cr) { |
| 326 if (proxyAuth) { |
| 327 _httpClient._removeProxyCredentials(cr); |
| 328 } else { |
| 329 _httpClient._removeCredentials(cr); |
| 330 } |
| 331 } |
| 332 |
| 333 Future requestAuthentication(_AuthenticationScheme scheme, String realm) { |
| 334 if (proxyAuth) { |
| 335 if (_httpClient._authenticateProxy == null) { |
| 336 return new Future.value(false); |
| 337 } |
| 338 var proxy = _httpRequest._proxy; |
| 339 return _httpClient._authenticateProxy(proxy.host, |
| 340 proxy.port, |
| 341 scheme.toString(), |
| 342 realm); |
| 343 } else { |
| 344 if (_httpClient._authenticate == null) { |
| 345 return new Future.value(false); |
| 346 } |
| 347 return _httpClient._authenticate(_httpRequest.uri, |
| 348 scheme.toString(), |
| 349 realm); |
| 350 } |
| 351 } |
| 352 |
| 353 List<String> challenge = authChallenge(); |
| 354 assert(challenge != null || challenge.length == 1); |
| 355 _HeaderValue header = |
| 356 _HeaderValue.parse(challenge[0], parameterSeparator: ","); |
| 357 _AuthenticationScheme scheme = |
| 358 new _AuthenticationScheme.fromString(header.value); |
| 359 String realm = header.parameters["realm"]; |
| 360 |
| 361 // See if any matching credentials are available. |
| 362 _Credentials cr = findCredentials(scheme); |
| 363 if (cr != null) { |
| 364 // For basic authentication don't retry already used credentials |
| 365 // as they must have already been added to the request causing |
| 366 // this authenticate response. |
| 367 if (cr.scheme == _AuthenticationScheme.BASIC && !cr.used) { |
| 368 // Credentials where found, prepare for retrying the request. |
| 369 return retry(); |
| 370 } |
| 371 |
| 372 // Digest authentication only supports the MD5 algorithm. |
| 373 if (cr.scheme == _AuthenticationScheme.DIGEST && |
| 374 (header.parameters["algorithm"] == null || |
| 375 header.parameters["algorithm"].toLowerCase() == "md5")) { |
| 376 if (cr.nonce == null || cr.nonce == header.parameters["nonce"]) { |
| 377 // If the nonce is not set then this is the first authenticate |
| 378 // response for these credentials. Set up authentication state. |
| 379 if (cr.nonce == null) { |
| 380 cr..nonce = header.parameters["nonce"] |
| 381 ..algorithm = "MD5" |
| 382 ..qop = header.parameters["qop"] |
| 383 ..nonceCount = 0; |
| 384 } |
| 385 // Credentials where found, prepare for retrying the request. |
| 386 return retry(); |
| 387 } else if (header.parameters["stale"] != null && |
| 388 header.parameters["stale"].toLowerCase() == "true") { |
| 389 // If stale is true retry with new nonce. |
| 390 cr.nonce = header.parameters["nonce"]; |
| 391 // Credentials where found, prepare for retrying the request. |
| 392 return retry(); |
| 393 } |
| 394 } |
| 395 } |
| 396 |
| 397 // Ask for more credentials if none found or the one found has |
| 398 // already been used. If it has already been used it must now be |
| 399 // invalid and is removed. |
| 400 if (cr != null) { |
| 401 removeCredentials(cr); |
| 402 cr = null; |
| 403 } |
| 404 return requestAuthentication(scheme, realm).then((credsAvailable) { |
| 405 if (credsAvailable) { |
| 406 cr = _httpClient._findCredentials(_httpRequest.uri, scheme); |
| 407 return retry(); |
| 408 } else { |
| 409 // No credentials available, complete with original response. |
| 410 return this; |
| 411 } |
| 412 }); |
| 413 } |
| 414 } |
| 415 |
| 416 |
| 417 abstract class _HttpOutboundMessage<T> extends _IOSinkImpl { |
| 418 // Used to mark when the body should be written. This is used for HEAD |
| 419 // requests and in error handling. |
| 420 bool _encodingSet = false; |
| 421 |
| 422 bool _bufferOutput = true; |
| 423 |
| 424 final Uri _uri; |
| 425 final _HttpOutgoing _outgoing; |
| 426 |
| 427 final _HttpHeaders headers; |
| 428 |
| 429 _HttpOutboundMessage(Uri uri, |
| 430 String protocolVersion, |
| 431 _HttpOutgoing outgoing, |
| 432 {_HttpHeaders initialHeaders}) |
| 433 : _uri = uri, |
| 434 headers = new _HttpHeaders( |
| 435 protocolVersion, |
| 436 defaultPortForScheme: uri.scheme == 'https' ? |
| 437 HttpClient.DEFAULT_HTTPS_PORT : |
| 438 HttpClient.DEFAULT_HTTP_PORT, |
| 439 initialHeaders: initialHeaders), |
| 440 _outgoing = outgoing, |
| 441 super(outgoing, null) { |
| 442 _outgoing.outbound = this; |
| 443 _encodingMutable = false; |
| 444 } |
| 445 |
| 446 int get contentLength => headers.contentLength; |
| 447 void set contentLength(int contentLength) { |
| 448 headers.contentLength = contentLength; |
| 449 } |
| 450 |
| 451 bool get persistentConnection => headers.persistentConnection; |
| 452 void set persistentConnection(bool p) { |
| 453 headers.persistentConnection = p; |
| 454 } |
| 455 |
| 456 bool get bufferOutput => _bufferOutput; |
| 457 void set bufferOutput(bool bufferOutput) { |
| 458 if (_outgoing.headersWritten) throw new StateError("Header already sent"); |
| 459 _bufferOutput = bufferOutput; |
| 460 } |
| 461 |
| 462 |
| 463 Encoding get encoding { |
| 464 if (_encodingSet && _outgoing.headersWritten) { |
| 465 return _encoding; |
| 466 } |
| 467 var charset; |
| 468 if (headers.contentType != null && headers.contentType.charset != null) { |
| 469 charset = headers.contentType.charset; |
| 470 } else { |
| 471 charset = "iso-8859-1"; |
| 472 } |
| 473 return Encoding.getByName(charset); |
| 474 } |
| 475 |
| 476 void add(List<int> data) { |
| 477 if (data.length == 0) return; |
| 478 super.add(data); |
| 479 } |
| 480 |
| 481 void write(Object obj) { |
| 482 if (!_encodingSet) { |
| 483 _encoding = encoding; |
| 484 _encodingSet = true; |
| 485 } |
| 486 super.write(obj); |
| 487 } |
| 488 |
| 489 void _writeHeader(); |
| 490 |
| 491 bool get _isConnectionClosed => false; |
| 492 } |
| 493 |
| 494 |
| 495 class _HttpResponse extends _HttpOutboundMessage<HttpResponse> |
| 496 implements HttpResponse { |
| 497 int _statusCode = 200; |
| 498 String _reasonPhrase; |
| 499 List<Cookie> _cookies; |
| 500 _HttpRequest _httpRequest; |
| 501 Duration _deadline; |
| 502 Timer _deadlineTimer; |
| 503 |
| 504 _HttpResponse(Uri uri, |
| 505 String protocolVersion, |
| 506 _HttpOutgoing outgoing, |
| 507 HttpHeaders defaultHeaders, |
| 508 String serverHeader) |
| 509 : super(uri, protocolVersion, outgoing, initialHeaders: defaultHeaders) { |
| 510 if (serverHeader != null) headers.set('server', serverHeader); |
| 511 } |
| 512 |
| 513 bool get _isConnectionClosed => _httpRequest._httpConnection._isClosing; |
| 514 |
| 515 List<Cookie> get cookies { |
| 516 if (_cookies == null) _cookies = new List<Cookie>(); |
| 517 return _cookies; |
| 518 } |
| 519 |
| 520 int get statusCode => _statusCode; |
| 521 void set statusCode(int statusCode) { |
| 522 if (_outgoing.headersWritten) throw new StateError("Header already sent"); |
| 523 _statusCode = statusCode; |
| 524 } |
| 525 |
| 526 String get reasonPhrase => _findReasonPhrase(statusCode); |
| 527 void set reasonPhrase(String reasonPhrase) { |
| 528 if (_outgoing.headersWritten) throw new StateError("Header already sent"); |
| 529 _reasonPhrase = reasonPhrase; |
| 530 } |
| 531 |
| 532 Future redirect(Uri location, {int status: HttpStatus.MOVED_TEMPORARILY}) { |
| 533 if (_outgoing.headersWritten) throw new StateError("Header already sent"); |
| 534 statusCode = status; |
| 535 headers.set("location", location.toString()); |
| 536 return close(); |
| 537 } |
| 538 |
| 539 Future<Socket> detachSocket({bool writeHeaders: true}) { |
| 540 if (_outgoing.headersWritten) throw new StateError("Headers already sent"); |
| 541 deadline = null; // Be sure to stop any deadline. |
| 542 var future = _httpRequest._httpConnection.detachSocket(); |
| 543 if (writeHeaders) { |
| 544 var headersFuture = _outgoing.writeHeaders(drainRequest: false, |
| 545 setOutgoing: false); |
| 546 assert(headersFuture == null); |
| 547 } else { |
| 548 // Imitate having written the headers. |
| 549 _outgoing.headersWritten = true; |
| 550 } |
| 551 // Close connection so the socket is 'free'. |
| 552 close(); |
| 553 done.catchError((_) { |
| 554 // Catch any error on done, as they automatically will be |
| 555 // propagated to the websocket. |
| 556 }); |
| 557 return future; |
| 558 } |
| 559 |
| 560 HttpConnectionInfo get connectionInfo => _httpRequest.connectionInfo; |
| 561 |
| 562 Duration get deadline => _deadline; |
| 563 |
| 564 void set deadline(Duration d) { |
| 565 if (_deadlineTimer != null) _deadlineTimer.cancel(); |
| 566 _deadline = d; |
| 567 |
| 568 if (_deadline == null) return; |
| 569 _deadlineTimer = new Timer(_deadline, () { |
| 570 _httpRequest._httpConnection.destroy(); |
| 571 }); |
| 572 } |
| 573 |
| 574 void _writeHeader() { |
| 575 Uint8List buffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
| 576 int offset = 0; |
| 577 |
| 578 void write(List<int> bytes) { |
| 579 int len = bytes.length; |
| 580 for (int i = 0; i < len; i++) { |
| 581 buffer[offset + i] = bytes[i]; |
| 582 } |
| 583 offset += len; |
| 584 } |
| 585 |
| 586 // Write status line. |
| 587 if (headers.protocolVersion == "1.1") { |
| 588 write(_Const.HTTP11); |
| 589 } else { |
| 590 write(_Const.HTTP10); |
| 591 } |
| 592 buffer[offset++] = _CharCode.SP; |
| 593 write(statusCode.toString().codeUnits); |
| 594 buffer[offset++] = _CharCode.SP; |
| 595 write(reasonPhrase.codeUnits); |
| 596 buffer[offset++] = _CharCode.CR; |
| 597 buffer[offset++] = _CharCode.LF; |
| 598 |
| 599 var session = _httpRequest._session; |
| 600 if (session != null && !session._destroyed) { |
| 601 // Mark as not new. |
| 602 session._isNew = false; |
| 603 // Make sure we only send the current session id. |
| 604 bool found = false; |
| 605 for (int i = 0; i < cookies.length; i++) { |
| 606 if (cookies[i].name.toUpperCase() == _DART_SESSION_ID) { |
| 607 cookies[i] |
| 608 ..value = session.id |
| 609 ..httpOnly = true |
| 610 ..path = "/"; |
| 611 found = true; |
| 612 } |
| 613 } |
| 614 if (!found) { |
| 615 var cookie = new Cookie(_DART_SESSION_ID, session.id); |
| 616 cookies.add(cookie |
| 617 ..httpOnly = true |
| 618 ..path = "/"); |
| 619 } |
| 620 } |
| 621 // Add all the cookies set to the headers. |
| 622 if (_cookies != null) { |
| 623 _cookies.forEach((cookie) { |
| 624 headers.add(HttpHeaders.SET_COOKIE, cookie); |
| 625 }); |
| 626 } |
| 627 |
| 628 headers._finalize(); |
| 629 |
| 630 // Write headers. |
| 631 offset = headers._write(buffer, offset); |
| 632 buffer[offset++] = _CharCode.CR; |
| 633 buffer[offset++] = _CharCode.LF; |
| 634 _outgoing.setHeader(buffer, offset); |
| 635 } |
| 636 |
| 637 String _findReasonPhrase(int statusCode) { |
| 638 if (_reasonPhrase != null) { |
| 639 return _reasonPhrase; |
| 640 } |
| 641 |
| 642 switch (statusCode) { |
| 643 case HttpStatus.CONTINUE: return "Continue"; |
| 644 case HttpStatus.SWITCHING_PROTOCOLS: return "Switching Protocols"; |
| 645 case HttpStatus.OK: return "OK"; |
| 646 case HttpStatus.CREATED: return "Created"; |
| 647 case HttpStatus.ACCEPTED: return "Accepted"; |
| 648 case HttpStatus.NON_AUTHORITATIVE_INFORMATION: |
| 649 return "Non-Authoritative Information"; |
| 650 case HttpStatus.NO_CONTENT: return "No Content"; |
| 651 case HttpStatus.RESET_CONTENT: return "Reset Content"; |
| 652 case HttpStatus.PARTIAL_CONTENT: return "Partial Content"; |
| 653 case HttpStatus.MULTIPLE_CHOICES: return "Multiple Choices"; |
| 654 case HttpStatus.MOVED_PERMANENTLY: return "Moved Permanently"; |
| 655 case HttpStatus.FOUND: return "Found"; |
| 656 case HttpStatus.SEE_OTHER: return "See Other"; |
| 657 case HttpStatus.NOT_MODIFIED: return "Not Modified"; |
| 658 case HttpStatus.USE_PROXY: return "Use Proxy"; |
| 659 case HttpStatus.TEMPORARY_REDIRECT: return "Temporary Redirect"; |
| 660 case HttpStatus.BAD_REQUEST: return "Bad Request"; |
| 661 case HttpStatus.UNAUTHORIZED: return "Unauthorized"; |
| 662 case HttpStatus.PAYMENT_REQUIRED: return "Payment Required"; |
| 663 case HttpStatus.FORBIDDEN: return "Forbidden"; |
| 664 case HttpStatus.NOT_FOUND: return "Not Found"; |
| 665 case HttpStatus.METHOD_NOT_ALLOWED: return "Method Not Allowed"; |
| 666 case HttpStatus.NOT_ACCEPTABLE: return "Not Acceptable"; |
| 667 case HttpStatus.PROXY_AUTHENTICATION_REQUIRED: |
| 668 return "Proxy Authentication Required"; |
| 669 case HttpStatus.REQUEST_TIMEOUT: return "Request Time-out"; |
| 670 case HttpStatus.CONFLICT: return "Conflict"; |
| 671 case HttpStatus.GONE: return "Gone"; |
| 672 case HttpStatus.LENGTH_REQUIRED: return "Length Required"; |
| 673 case HttpStatus.PRECONDITION_FAILED: return "Precondition Failed"; |
| 674 case HttpStatus.REQUEST_ENTITY_TOO_LARGE: |
| 675 return "Request Entity Too Large"; |
| 676 case HttpStatus.REQUEST_URI_TOO_LONG: return "Request-URI Too Large"; |
| 677 case HttpStatus.UNSUPPORTED_MEDIA_TYPE: return "Unsupported Media Type"; |
| 678 case HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE: |
| 679 return "Requested range not satisfiable"; |
| 680 case HttpStatus.EXPECTATION_FAILED: return "Expectation Failed"; |
| 681 case HttpStatus.INTERNAL_SERVER_ERROR: return "Internal Server Error"; |
| 682 case HttpStatus.NOT_IMPLEMENTED: return "Not Implemented"; |
| 683 case HttpStatus.BAD_GATEWAY: return "Bad Gateway"; |
| 684 case HttpStatus.SERVICE_UNAVAILABLE: return "Service Unavailable"; |
| 685 case HttpStatus.GATEWAY_TIMEOUT: return "Gateway Time-out"; |
| 686 case HttpStatus.HTTP_VERSION_NOT_SUPPORTED: |
| 687 return "Http Version not supported"; |
| 688 default: return "Status $statusCode"; |
| 689 } |
| 690 } |
| 691 } |
| 692 |
| 693 |
| 694 class _HttpClientRequest extends _HttpOutboundMessage<HttpClientResponse> |
| 695 implements HttpClientRequest { |
| 696 final String method; |
| 697 final Uri uri; |
| 698 final List<Cookie> cookies = new List<Cookie>(); |
| 699 |
| 700 // The HttpClient this request belongs to. |
| 701 final _HttpClient _httpClient; |
| 702 final _HttpClientConnection _httpClientConnection; |
| 703 |
| 704 final Completer<HttpClientResponse> _responseCompleter |
| 705 = new Completer<HttpClientResponse>(); |
| 706 |
| 707 final _Proxy _proxy; |
| 708 |
| 709 Future<HttpClientResponse> _response; |
| 710 |
| 711 // TODO(ajohnsen): Get default value from client? |
| 712 bool _followRedirects = true; |
| 713 |
| 714 int _maxRedirects = 5; |
| 715 |
| 716 List<RedirectInfo> _responseRedirects = []; |
| 717 |
| 718 _HttpClientRequest(_HttpOutgoing outgoing, Uri uri, this.method, this._proxy, |
| 719 this._httpClient, this._httpClientConnection) |
| 720 : uri = uri, |
| 721 super(uri, "1.1", outgoing) { |
| 722 // GET and HEAD have 'content-length: 0' by default. |
| 723 if (method == "GET" || method == "HEAD") { |
| 724 contentLength = 0; |
| 725 } else { |
| 726 headers.chunkedTransferEncoding = true; |
| 727 } |
| 728 } |
| 729 |
| 730 Future<HttpClientResponse> get done { |
| 731 if (_response == null) { |
| 732 _response = Future.wait([_responseCompleter.future, super.done], |
| 733 eagerError: true) |
| 734 .then((list) => list[0]); |
| 735 } |
| 736 return _response; |
| 737 } |
| 738 |
| 739 Future<HttpClientResponse> close() { |
| 740 super.close(); |
| 741 return done; |
| 742 } |
| 743 |
| 744 int get maxRedirects => _maxRedirects; |
| 745 void set maxRedirects(int maxRedirects) { |
| 746 if (_outgoing.headersWritten) throw new StateError("Request already sent"); |
| 747 _maxRedirects = maxRedirects; |
| 748 } |
| 749 |
| 750 bool get followRedirects => _followRedirects; |
| 751 void set followRedirects(bool followRedirects) { |
| 752 if (_outgoing.headersWritten) throw new StateError("Request already sent"); |
| 753 _followRedirects = followRedirects; |
| 754 } |
| 755 |
| 756 HttpConnectionInfo get connectionInfo => _httpClientConnection.connectionInfo; |
| 757 |
| 758 void _onIncoming(_HttpIncoming incoming) { |
| 759 var response = new _HttpClientResponse(incoming, this, _httpClient); |
| 760 Future<HttpClientResponse> future; |
| 761 if (followRedirects && response.isRedirect) { |
| 762 if (response.redirects.length < maxRedirects) { |
| 763 // Redirect and drain response. |
| 764 future = response.drain().then((_) => response.redirect()); |
| 765 } else { |
| 766 // End with exception, too many redirects. |
| 767 future = response.drain() |
| 768 .then((_) => new Future.error( |
| 769 new RedirectException("Redirect limit exceeded", |
| 770 response.redirects))); |
| 771 } |
| 772 } else if (response._shouldAuthenticateProxy) { |
| 773 future = response._authenticate(true); |
| 774 } else if (response._shouldAuthenticate) { |
| 775 future = response._authenticate(false); |
| 776 } else { |
| 777 future = new Future<HttpClientResponse>.value(response); |
| 778 } |
| 779 future.then( |
| 780 (v) => _responseCompleter.complete(v), |
| 781 onError: _responseCompleter.completeError); |
| 782 } |
| 783 |
| 784 void _onError(error, StackTrace stackTrace) { |
| 785 _responseCompleter.completeError(error, stackTrace); |
| 786 } |
| 787 |
| 788 // Generate the request URI based on the method and proxy. |
| 789 String _requestUri() { |
| 790 // Generate the request URI starting from the path component. |
| 791 String uriStartingFromPath() { |
| 792 String result = uri.path; |
| 793 if (result.isEmpty) result = "/"; |
| 794 if (uri.hasQuery) { |
| 795 result = "${result}?${uri.query}"; |
| 796 } |
| 797 return result; |
| 798 } |
| 799 |
| 800 if (_proxy.isDirect) { |
| 801 return uriStartingFromPath(); |
| 802 } else { |
| 803 if (method == "CONNECT") { |
| 804 // For the connect method the request URI is the host:port of |
| 805 // the requested destination of the tunnel (see RFC 2817 |
| 806 // section 5.2) |
| 807 return "${uri.host}:${uri.port}"; |
| 808 } else { |
| 809 if (_httpClientConnection._proxyTunnel) { |
| 810 return uriStartingFromPath(); |
| 811 } else { |
| 812 return uri.removeFragment().toString(); |
| 813 } |
| 814 } |
| 815 } |
| 816 } |
| 817 |
| 818 void _writeHeader() { |
| 819 Uint8List buffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
| 820 int offset = 0; |
| 821 |
| 822 void write(List<int> bytes) { |
| 823 int len = bytes.length; |
| 824 for (int i = 0; i < len; i++) { |
| 825 buffer[offset + i] = bytes[i]; |
| 826 } |
| 827 offset += len; |
| 828 } |
| 829 |
| 830 // Write the request method. |
| 831 write(method.codeUnits); |
| 832 buffer[offset++] = _CharCode.SP; |
| 833 // Write the request URI. |
| 834 write(_requestUri().codeUnits); |
| 835 buffer[offset++] = _CharCode.SP; |
| 836 // Write HTTP/1.1. |
| 837 write(_Const.HTTP11); |
| 838 buffer[offset++] = _CharCode.CR; |
| 839 buffer[offset++] = _CharCode.LF; |
| 840 |
| 841 // Add the cookies to the headers. |
| 842 if (!cookies.isEmpty) { |
| 843 StringBuffer sb = new StringBuffer(); |
| 844 for (int i = 0; i < cookies.length; i++) { |
| 845 if (i > 0) sb.write("; "); |
| 846 sb..write(cookies[i].name)..write("=")..write(cookies[i].value); |
| 847 } |
| 848 headers.add(HttpHeaders.COOKIE, sb.toString()); |
| 849 } |
| 850 |
| 851 headers._finalize(); |
| 852 |
| 853 // Write headers. |
| 854 offset = headers._write(buffer, offset); |
| 855 buffer[offset++] = _CharCode.CR; |
| 856 buffer[offset++] = _CharCode.LF; |
| 857 _outgoing.setHeader(buffer, offset); |
| 858 } |
| 859 } |
| 860 |
| 861 // Used by _HttpOutgoing as a target of a chunked converter for gzip |
| 862 // compression. |
| 863 class _HttpGZipSink extends ByteConversionSink { |
| 864 final Function _consume; |
| 865 _HttpGZipSink(this._consume); |
| 866 |
| 867 void add(List<int> chunk) { |
| 868 _consume(chunk); |
| 869 } |
| 870 |
| 871 void addSlice(List<int> chunk, int start, int end, bool isLast) { |
| 872 if (chunk is Uint8List) { |
| 873 _consume(new Uint8List.view(chunk.buffer, start, end - start)); |
| 874 } else { |
| 875 _consume(chunk.sublist(start, end - start)); |
| 876 } |
| 877 } |
| 878 |
| 879 void close() {} |
| 880 } |
| 881 |
| 882 |
| 883 // The _HttpOutgoing handles all of the following: |
| 884 // - Buffering |
| 885 // - GZip compressionm |
| 886 // - Content-Length validation. |
| 887 // - Errors. |
| 888 // |
| 889 // Most notable is the GZip compression, that uses a double-buffering system, |
| 890 // one before gzip (_gzipBuffer) and one after (_buffer). |
| 891 class _HttpOutgoing implements StreamConsumer<List<int>> { |
| 892 static const List<int> _footerAndChunk0Length = |
| 893 const [_CharCode.CR, _CharCode.LF, 0x30, _CharCode.CR, _CharCode.LF, |
| 894 _CharCode.CR, _CharCode.LF]; |
| 895 |
| 896 static const List<int> _chunk0Length = |
| 897 const [0x30, _CharCode.CR, _CharCode.LF, _CharCode.CR, _CharCode.LF]; |
| 898 |
| 899 final Completer _doneCompleter = new Completer(); |
| 900 final Socket socket; |
| 901 |
| 902 bool ignoreBody = false; |
| 903 bool headersWritten = false; |
| 904 |
| 905 Uint8List _buffer; |
| 906 int _length = 0; |
| 907 |
| 908 Future _closeFuture; |
| 909 |
| 910 bool chunked = false; |
| 911 int _pendingChunkedFooter = 0; |
| 912 |
| 913 int contentLength; |
| 914 int _bytesWritten = 0; |
| 915 |
| 916 bool _gzip = false; |
| 917 ByteConversionSink _gzipSink; |
| 918 // _gzipAdd is set iff the sink is being added to. It's used to specify where |
| 919 // gzipped data should be taken (sometimes a controller, sometimes a socket). |
| 920 Function _gzipAdd; |
| 921 Uint8List _gzipBuffer; |
| 922 int _gzipBufferLength = 0; |
| 923 |
| 924 bool _socketError = false; |
| 925 |
| 926 _HttpOutboundMessage outbound; |
| 927 |
| 928 _HttpOutgoing(this.socket); |
| 929 |
| 930 // Returns either a future or 'null', if it was able to write headers |
| 931 // immediately. |
| 932 Future writeHeaders({bool drainRequest: true, bool setOutgoing: true}) { |
| 933 Future write() { |
| 934 try { |
| 935 outbound._writeHeader(); |
| 936 } catch (_) { |
| 937 // Headers too large. |
| 938 return new Future.error(new HttpException( |
| 939 "Headers size exceeded the of '$_OUTGOING_BUFFER_SIZE'" |
| 940 " bytes")); |
| 941 } |
| 942 return null; |
| 943 } |
| 944 |
| 945 if (headersWritten) return null; |
| 946 headersWritten = true; |
| 947 Future drainFuture; |
| 948 bool gzip = false; |
| 949 if (outbound is _HttpResponse) { |
| 950 // Server side. |
| 951 _HttpResponse response = outbound; |
| 952 if (response._httpRequest._httpServer.autoCompress && |
| 953 outbound.bufferOutput && |
| 954 outbound.headers.chunkedTransferEncoding) { |
| 955 List acceptEncodings = |
| 956 response._httpRequest.headers[HttpHeaders.ACCEPT_ENCODING]; |
| 957 List contentEncoding = outbound.headers[HttpHeaders.CONTENT_ENCODING]; |
| 958 if (acceptEncodings != null && |
| 959 acceptEncodings |
| 960 .expand((list) => list.split(",")) |
| 961 .any((encoding) => encoding.trim().toLowerCase() == "gzip") && |
| 962 contentEncoding == null) { |
| 963 outbound.headers.set(HttpHeaders.CONTENT_ENCODING, "gzip"); |
| 964 gzip = true; |
| 965 } |
| 966 } |
| 967 if (drainRequest && !response._httpRequest._incoming.hasSubscriber) { |
| 968 drainFuture = response._httpRequest.drain().catchError((_) {}); |
| 969 } |
| 970 } else { |
| 971 drainRequest = false; |
| 972 } |
| 973 if (ignoreBody) { |
| 974 return write(); |
| 975 } |
| 976 if (setOutgoing) { |
| 977 int contentLength = outbound.headers.contentLength; |
| 978 if (outbound.headers.chunkedTransferEncoding) { |
| 979 chunked = true; |
| 980 if (gzip) this.gzip = true; |
| 981 } else if (contentLength >= 0) { |
| 982 this.contentLength = contentLength; |
| 983 } |
| 984 } |
| 985 if (drainFuture != null) { |
| 986 return drainFuture.then((_) => write()); |
| 987 } |
| 988 return write(); |
| 989 } |
| 990 |
| 991 |
| 992 Future addStream(Stream<List<int>> stream) { |
| 993 if (_socketError) { |
| 994 stream.listen(null).cancel(); |
| 995 return new Future.value(outbound); |
| 996 } |
| 997 if (ignoreBody) { |
| 998 stream.drain().catchError((_) {}); |
| 999 var future = writeHeaders(); |
| 1000 if (future != null) { |
| 1001 return future.then((_) => close()); |
| 1002 } |
| 1003 return close(); |
| 1004 } |
| 1005 var sub; |
| 1006 // Use new stream so we are able to pause (see below listen). The |
| 1007 // alternative is to use stream.extand, but that won't give us a way of |
| 1008 // pausing. |
| 1009 var controller = new StreamController( |
| 1010 onPause: () => sub.pause(), |
| 1011 onResume: () => sub.resume(), |
| 1012 sync: true); |
| 1013 |
| 1014 void onData(data) { |
| 1015 if (_socketError) return; |
| 1016 if (data.length == 0) return; |
| 1017 if (chunked) { |
| 1018 if (_gzip) { |
| 1019 _gzipAdd = controller.add; |
| 1020 _addGZipChunk(data, _gzipSink.add); |
| 1021 _gzipAdd = null; |
| 1022 return; |
| 1023 } |
| 1024 _addChunk(_chunkHeader(data.length), controller.add); |
| 1025 _pendingChunkedFooter = 2; |
| 1026 } else { |
| 1027 if (contentLength != null) { |
| 1028 _bytesWritten += data.length; |
| 1029 if (_bytesWritten > contentLength) { |
| 1030 controller.addError(new HttpException( |
| 1031 "Content size exceeds specified contentLength. " |
| 1032 "$_bytesWritten bytes written while expected " |
| 1033 "$contentLength. " |
| 1034 "[${new String.fromCharCodes(data)}]")); |
| 1035 return; |
| 1036 } |
| 1037 } |
| 1038 } |
| 1039 _addChunk(data, controller.add); |
| 1040 } |
| 1041 |
| 1042 sub = stream.listen( |
| 1043 onData, |
| 1044 onError: controller.addError, |
| 1045 onDone: controller.close, |
| 1046 cancelOnError: true); |
| 1047 // Write headers now that we are listening to the stream. |
| 1048 if (!headersWritten) { |
| 1049 var future = writeHeaders(); |
| 1050 if (future != null) { |
| 1051 // While incoming is being drained, the pauseFuture is non-null. Pause |
| 1052 // output until it's drained. |
| 1053 sub.pause(future); |
| 1054 } |
| 1055 } |
| 1056 return socket.addStream(controller.stream) |
| 1057 .then((_) { |
| 1058 return outbound; |
| 1059 }, onError: (error, stackTrace) { |
| 1060 // Be sure to close it in case of an error. |
| 1061 if (_gzip) _gzipSink.close(); |
| 1062 _socketError = true; |
| 1063 _doneCompleter.completeError(error, stackTrace); |
| 1064 if (_ignoreError(error)) { |
| 1065 return outbound; |
| 1066 } else { |
| 1067 throw error; |
| 1068 } |
| 1069 }); |
| 1070 } |
| 1071 |
| 1072 Future close() { |
| 1073 // If we are already closed, return that future. |
| 1074 if (_closeFuture != null) return _closeFuture; |
| 1075 // If we earlier saw an error, return immediate. The notification to |
| 1076 // _Http*Connection is already done. |
| 1077 if (_socketError) return new Future.value(outbound); |
| 1078 if (outbound._isConnectionClosed) return new Future.value(outbound); |
| 1079 if (!headersWritten && !ignoreBody) { |
| 1080 if (outbound.headers.contentLength == -1) { |
| 1081 // If no body was written, ignoreBody is false (it's not a HEAD |
| 1082 // request) and the content-length is unspecified, set contentLength to |
| 1083 // 0. |
| 1084 outbound.headers.chunkedTransferEncoding = false; |
| 1085 outbound.headers.contentLength = 0; |
| 1086 } else if (outbound.headers.contentLength > 0) { |
| 1087 var error = new HttpException( |
| 1088 "No content even though contentLength was specified to be " |
| 1089 "greater than 0: ${outbound.headers.contentLength}.", |
| 1090 uri: outbound._uri); |
| 1091 _doneCompleter.completeError(error); |
| 1092 return _closeFuture = new Future.error(error); |
| 1093 } |
| 1094 } |
| 1095 // If contentLength was specified, validate it. |
| 1096 if (contentLength != null) { |
| 1097 if (_bytesWritten < contentLength) { |
| 1098 var error = new HttpException( |
| 1099 "Content size below specified contentLength. " |
| 1100 " $_bytesWritten bytes written but expected " |
| 1101 "$contentLength.", |
| 1102 uri: outbound._uri); |
| 1103 _doneCompleter.completeError(error); |
| 1104 return _closeFuture = new Future.error(error); |
| 1105 } |
| 1106 } |
| 1107 |
| 1108 Future finalize() { |
| 1109 // In case of chunked encoding (and gzip), handle remaining gzip data and |
| 1110 // append the 'footer' for chunked encoding. |
| 1111 if (chunked) { |
| 1112 if (_gzip) { |
| 1113 _gzipAdd = socket.add; |
| 1114 if (_gzipBufferLength > 0) { |
| 1115 _gzipSink.add(new Uint8List.view( |
| 1116 _gzipBuffer.buffer, 0, _gzipBufferLength)); |
| 1117 } |
| 1118 _gzipBuffer = null; |
| 1119 _gzipSink.close(); |
| 1120 _gzipAdd = null; |
| 1121 } |
| 1122 _addChunk(_chunkHeader(0), socket.add); |
| 1123 } |
| 1124 // Add any remaining data in the buffer. |
| 1125 if (_length > 0) { |
| 1126 socket.add(new Uint8List.view(_buffer.buffer, 0, _length)); |
| 1127 } |
| 1128 // Clear references, for better GC. |
| 1129 _buffer = null; |
| 1130 // And finally flush it. As we support keep-alive, never close it from |
| 1131 // here. Once the socket is flushed, we'll be able to reuse it (signaled |
| 1132 // by the 'done' future). |
| 1133 return socket.flush() |
| 1134 .then((_) { |
| 1135 _doneCompleter.complete(socket); |
| 1136 return outbound; |
| 1137 }, onError: (error, stackTrace) { |
| 1138 _doneCompleter.completeError(error, stackTrace); |
| 1139 if (_ignoreError(error)) { |
| 1140 return outbound; |
| 1141 } else { |
| 1142 throw error; |
| 1143 } |
| 1144 }); |
| 1145 } |
| 1146 |
| 1147 var future = writeHeaders(); |
| 1148 if (future != null) { |
| 1149 return _closeFuture = future.whenComplete(finalize); |
| 1150 } |
| 1151 return _closeFuture = finalize(); |
| 1152 } |
| 1153 |
| 1154 Future get done => _doneCompleter.future; |
| 1155 |
| 1156 void setHeader(List<int> data, int length) { |
| 1157 assert(_length == 0); |
| 1158 assert(data.length == _OUTGOING_BUFFER_SIZE); |
| 1159 _buffer = data; |
| 1160 _length = length; |
| 1161 } |
| 1162 |
| 1163 void set gzip(bool value) { |
| 1164 _gzip = value; |
| 1165 if (_gzip) { |
| 1166 _gzipBuffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
| 1167 assert(_gzipSink == null); |
| 1168 _gzipSink = new ZLibEncoder(gzip: true) |
| 1169 .startChunkedConversion( |
| 1170 new _HttpGZipSink((data) { |
| 1171 // We are closing down prematurely, due to an error. Discard. |
| 1172 if (_gzipAdd == null) return; |
| 1173 _addChunk(_chunkHeader(data.length), _gzipAdd); |
| 1174 _pendingChunkedFooter = 2; |
| 1175 _addChunk(data, _gzipAdd); |
| 1176 })); |
| 1177 } |
| 1178 } |
| 1179 |
| 1180 bool _ignoreError(error) |
| 1181 => (error is SocketException || error is TlsException) && |
| 1182 outbound is HttpResponse; |
| 1183 |
| 1184 void _addGZipChunk(chunk, void add(List<int> data)) { |
| 1185 if (!outbound.bufferOutput) { |
| 1186 add(chunk); |
| 1187 return; |
| 1188 } |
| 1189 if (chunk.length > _gzipBuffer.length - _gzipBufferLength) { |
| 1190 add(new Uint8List.view( |
| 1191 _gzipBuffer.buffer, 0, _gzipBufferLength)); |
| 1192 _gzipBuffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
| 1193 _gzipBufferLength = 0; |
| 1194 } |
| 1195 if (chunk.length > _OUTGOING_BUFFER_SIZE) { |
| 1196 add(chunk); |
| 1197 } else { |
| 1198 _gzipBuffer.setRange(_gzipBufferLength, |
| 1199 _gzipBufferLength + chunk.length, |
| 1200 chunk); |
| 1201 _gzipBufferLength += chunk.length; |
| 1202 } |
| 1203 } |
| 1204 |
| 1205 void _addChunk(chunk, void add(List<int> data)) { |
| 1206 if (!outbound.bufferOutput) { |
| 1207 if (_buffer != null) { |
| 1208 // If _buffer is not null, we have not written the header yet. Write |
| 1209 // it now. |
| 1210 add(new Uint8List.view(_buffer.buffer, 0, _length)); |
| 1211 _buffer = null; |
| 1212 _length = 0; |
| 1213 } |
| 1214 add(chunk); |
| 1215 return; |
| 1216 } |
| 1217 if (chunk.length > _buffer.length - _length) { |
| 1218 add(new Uint8List.view(_buffer.buffer, 0, _length)); |
| 1219 _buffer = new Uint8List(_OUTGOING_BUFFER_SIZE); |
| 1220 _length = 0; |
| 1221 } |
| 1222 if (chunk.length > _OUTGOING_BUFFER_SIZE) { |
| 1223 add(chunk); |
| 1224 } else { |
| 1225 _buffer.setRange(_length, _length + chunk.length, chunk); |
| 1226 _length += chunk.length; |
| 1227 } |
| 1228 } |
| 1229 |
| 1230 List<int> _chunkHeader(int length) { |
| 1231 const hexDigits = const [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, |
| 1232 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46]; |
| 1233 if (length == 0) { |
| 1234 if (_pendingChunkedFooter == 2) return _footerAndChunk0Length; |
| 1235 return _chunk0Length; |
| 1236 } |
| 1237 int size = _pendingChunkedFooter; |
| 1238 int len = length; |
| 1239 // Compute a fast integer version of (log(length + 1) / log(16)).ceil(). |
| 1240 while (len > 0) { |
| 1241 size++; |
| 1242 len >>= 4; |
| 1243 } |
| 1244 var footerAndHeader = new Uint8List(size + 2); |
| 1245 if (_pendingChunkedFooter == 2) { |
| 1246 footerAndHeader[0] = _CharCode.CR; |
| 1247 footerAndHeader[1] = _CharCode.LF; |
| 1248 } |
| 1249 int index = size; |
| 1250 while (index > _pendingChunkedFooter) { |
| 1251 footerAndHeader[--index] = hexDigits[length & 15]; |
| 1252 length = length >> 4; |
| 1253 } |
| 1254 footerAndHeader[size + 0] = _CharCode.CR; |
| 1255 footerAndHeader[size + 1] = _CharCode.LF; |
| 1256 return footerAndHeader; |
| 1257 } |
| 1258 } |
| 1259 |
| 1260 class _HttpClientConnection { |
| 1261 final String key; |
| 1262 final Socket _socket; |
| 1263 final bool _proxyTunnel; |
| 1264 final SecurityContext _context; |
| 1265 final _HttpParser _httpParser; |
| 1266 StreamSubscription _subscription; |
| 1267 final _HttpClient _httpClient; |
| 1268 bool _dispose = false; |
| 1269 Timer _idleTimer; |
| 1270 bool closed = false; |
| 1271 Uri _currentUri; |
| 1272 |
| 1273 Completer<_HttpIncoming> _nextResponseCompleter; |
| 1274 Future _streamFuture; |
| 1275 |
| 1276 _HttpClientConnection(this.key, this._socket, this._httpClient, |
| 1277 [this._proxyTunnel = false, this._context]) |
| 1278 : _httpParser = new _HttpParser.responseParser() { |
| 1279 _httpParser.listenToStream(_socket); |
| 1280 |
| 1281 // Set up handlers on the parser here, so we are sure to get 'onDone' from |
| 1282 // the parser. |
| 1283 _subscription = _httpParser.listen( |
| 1284 (incoming) { |
| 1285 // Only handle one incoming response at the time. Keep the |
| 1286 // stream paused until the response have been processed. |
| 1287 _subscription.pause(); |
| 1288 // We assume the response is not here, until we have send the request. |
| 1289 if (_nextResponseCompleter == null) { |
| 1290 throw new HttpException( |
| 1291 "Unexpected response (unsolicited response without request).", |
| 1292 uri: _currentUri); |
| 1293 } |
| 1294 |
| 1295 // Check for status code '100 Continue'. In that case just |
| 1296 // consume that response as the final response will follow |
| 1297 // it. There is currently no API for the client to wait for |
| 1298 // the '100 Continue' response. |
| 1299 if (incoming.statusCode == 100) { |
| 1300 incoming.drain().then((_) { |
| 1301 _subscription.resume(); |
| 1302 }).catchError((error, [StackTrace stackTrace]) { |
| 1303 _nextResponseCompleter.completeError( |
| 1304 new HttpException(error.message, uri: _currentUri), |
| 1305 stackTrace); |
| 1306 _nextResponseCompleter = null; |
| 1307 }); |
| 1308 } else { |
| 1309 _nextResponseCompleter.complete(incoming); |
| 1310 _nextResponseCompleter = null; |
| 1311 } |
| 1312 }, |
| 1313 onError: (error, [StackTrace stackTrace]) { |
| 1314 if (_nextResponseCompleter != null) { |
| 1315 _nextResponseCompleter.completeError( |
| 1316 new HttpException(error.message, uri: _currentUri), |
| 1317 stackTrace); |
| 1318 _nextResponseCompleter = null; |
| 1319 } |
| 1320 }, |
| 1321 onDone: () { |
| 1322 if (_nextResponseCompleter != null) { |
| 1323 _nextResponseCompleter.completeError(new HttpException( |
| 1324 "Connection closed before response was received", |
| 1325 uri: _currentUri)); |
| 1326 _nextResponseCompleter = null; |
| 1327 } |
| 1328 close(); |
| 1329 }); |
| 1330 } |
| 1331 |
| 1332 _HttpClientRequest send(Uri uri, int port, String method, _Proxy proxy) { |
| 1333 if (closed) { |
| 1334 throw new HttpException( |
| 1335 "Socket closed before request was sent", uri: uri); |
| 1336 } |
| 1337 _currentUri = uri; |
| 1338 // Start with pausing the parser. |
| 1339 _subscription.pause(); |
| 1340 _ProxyCredentials proxyCreds; // Credentials used to authorize proxy. |
| 1341 _SiteCredentials creds; // Credentials used to authorize this request. |
| 1342 var outgoing = new _HttpOutgoing(_socket); |
| 1343 // Create new request object, wrapping the outgoing connection. |
| 1344 var request = new _HttpClientRequest(outgoing, |
| 1345 uri, |
| 1346 method, |
| 1347 proxy, |
| 1348 _httpClient, |
| 1349 this); |
| 1350 // For the Host header an IPv6 address must be enclosed in []'s. |
| 1351 var host = uri.host; |
| 1352 if (host.contains(':')) host = "[$host]"; |
| 1353 request.headers |
| 1354 ..host = host |
| 1355 ..port = port |
| 1356 .._add(HttpHeaders.ACCEPT_ENCODING, "gzip"); |
| 1357 if (_httpClient.userAgent != null) { |
| 1358 request.headers._add('user-agent', _httpClient.userAgent); |
| 1359 } |
| 1360 if (proxy.isAuthenticated) { |
| 1361 // If the proxy configuration contains user information use that |
| 1362 // for proxy basic authorization. |
| 1363 String auth = _CryptoUtils.bytesToBase64( |
| 1364 UTF8.encode("${proxy.username}:${proxy.password}")); |
| 1365 request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, "Basic $auth"); |
| 1366 } else if (!proxy.isDirect && _httpClient._proxyCredentials.length > 0) { |
| 1367 proxyCreds = _httpClient._findProxyCredentials(proxy); |
| 1368 if (proxyCreds != null) { |
| 1369 proxyCreds.authorize(request); |
| 1370 } |
| 1371 } |
| 1372 if (uri.userInfo != null && !uri.userInfo.isEmpty) { |
| 1373 // If the URL contains user information use that for basic |
| 1374 // authorization. |
| 1375 String auth = |
| 1376 _CryptoUtils.bytesToBase64(UTF8.encode(uri.userInfo)); |
| 1377 request.headers.set(HttpHeaders.AUTHORIZATION, "Basic $auth"); |
| 1378 } else { |
| 1379 // Look for credentials. |
| 1380 creds = _httpClient._findCredentials(uri); |
| 1381 if (creds != null) { |
| 1382 creds.authorize(request); |
| 1383 } |
| 1384 } |
| 1385 // Start sending the request (lazy, delayed until the user provides |
| 1386 // data). |
| 1387 _httpParser.isHead = method == "HEAD"; |
| 1388 _streamFuture = outgoing.done |
| 1389 .then((s) { |
| 1390 // Request sent, set up response completer. |
| 1391 _nextResponseCompleter = new Completer(); |
| 1392 |
| 1393 // Listen for response. |
| 1394 _nextResponseCompleter.future |
| 1395 .then((incoming) { |
| 1396 _currentUri = null; |
| 1397 incoming.dataDone.then((closing) { |
| 1398 if (incoming.upgraded) { |
| 1399 _httpClient._connectionClosed(this); |
| 1400 startTimer(); |
| 1401 return; |
| 1402 } |
| 1403 if (closed) return; |
| 1404 if (!closing && |
| 1405 !_dispose && |
| 1406 incoming.headers.persistentConnection && |
| 1407 request.persistentConnection) { |
| 1408 // Return connection, now we are done. |
| 1409 _httpClient._returnConnection(this); |
| 1410 _subscription.resume(); |
| 1411 } else { |
| 1412 destroy(); |
| 1413 } |
| 1414 }); |
| 1415 // For digest authentication if proxy check if the proxy |
| 1416 // requests the client to start using a new nonce for proxy |
| 1417 // authentication. |
| 1418 if (proxyCreds != null && |
| 1419 proxyCreds.scheme == _AuthenticationScheme.DIGEST) { |
| 1420 var authInfo = incoming.headers["proxy-authentication-info"]; |
| 1421 if (authInfo != null && authInfo.length == 1) { |
| 1422 var header = |
| 1423 _HeaderValue.parse( |
| 1424 authInfo[0], parameterSeparator: ','); |
| 1425 var nextnonce = header.parameters["nextnonce"]; |
| 1426 if (nextnonce != null) proxyCreds.nonce = nextnonce; |
| 1427 } |
| 1428 } |
| 1429 // For digest authentication check if the server requests the |
| 1430 // client to start using a new nonce. |
| 1431 if (creds != null && |
| 1432 creds.scheme == _AuthenticationScheme.DIGEST) { |
| 1433 var authInfo = incoming.headers["authentication-info"]; |
| 1434 if (authInfo != null && authInfo.length == 1) { |
| 1435 var header = |
| 1436 _HeaderValue.parse( |
| 1437 authInfo[0], parameterSeparator: ','); |
| 1438 var nextnonce = header.parameters["nextnonce"]; |
| 1439 if (nextnonce != null) creds.nonce = nextnonce; |
| 1440 } |
| 1441 } |
| 1442 request._onIncoming(incoming); |
| 1443 }) |
| 1444 // If we see a state error, we failed to get the 'first' |
| 1445 // element. |
| 1446 .catchError((error) { |
| 1447 throw new HttpException( |
| 1448 "Connection closed before data was received", uri: uri); |
| 1449 }, test: (error) => error is StateError) |
| 1450 .catchError((error, stackTrace) { |
| 1451 // We are done with the socket. |
| 1452 destroy(); |
| 1453 request._onError(error, stackTrace); |
| 1454 }); |
| 1455 |
| 1456 // Resume the parser now we have a handler. |
| 1457 _subscription.resume(); |
| 1458 return s; |
| 1459 }, onError: (e) { |
| 1460 destroy(); |
| 1461 }); |
| 1462 return request; |
| 1463 } |
| 1464 |
| 1465 Future<Socket> detachSocket() { |
| 1466 return _streamFuture.then( |
| 1467 (_) => new _DetachedSocket(_socket, _httpParser.detachIncoming())); |
| 1468 } |
| 1469 |
| 1470 void destroy() { |
| 1471 closed = true; |
| 1472 _httpClient._connectionClosed(this); |
| 1473 _socket.destroy(); |
| 1474 } |
| 1475 |
| 1476 void close() { |
| 1477 closed = true; |
| 1478 _httpClient._connectionClosed(this); |
| 1479 _streamFuture |
| 1480 // TODO(ajohnsen): Add timeout. |
| 1481 .then((_) => _socket.destroy()); |
| 1482 } |
| 1483 |
| 1484 Future<_HttpClientConnection> createProxyTunnel(host, port, proxy, callback) { |
| 1485 _HttpClientRequest request = |
| 1486 send(new Uri(host: host, port: port), |
| 1487 port, |
| 1488 "CONNECT", |
| 1489 proxy); |
| 1490 if (proxy.isAuthenticated) { |
| 1491 // If the proxy configuration contains user information use that |
| 1492 // for proxy basic authorization. |
| 1493 String auth = _CryptoUtils.bytesToBase64( |
| 1494 UTF8.encode("${proxy.username}:${proxy.password}")); |
| 1495 request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, "Basic $auth"); |
| 1496 } |
| 1497 return request.close() |
| 1498 .then((response) { |
| 1499 if (response.statusCode != HttpStatus.OK) { |
| 1500 throw "Proxy failed to establish tunnel " |
| 1501 "(${response.statusCode} ${response.reasonPhrase})"; |
| 1502 } |
| 1503 var socket = (response as _HttpClientResponse)._httpRequest |
| 1504 ._httpClientConnection._socket; |
| 1505 return SecureSocket.secure( |
| 1506 socket, |
| 1507 host: host, |
| 1508 context: _context, |
| 1509 onBadCertificate: callback); |
| 1510 }) |
| 1511 .then((secureSocket) { |
| 1512 String key = _HttpClientConnection.makeKey(true, host, port); |
| 1513 return new _HttpClientConnection( |
| 1514 key, secureSocket, request._httpClient, true); |
| 1515 }); |
| 1516 } |
| 1517 |
| 1518 HttpConnectionInfo get connectionInfo => _HttpConnectionInfo.create(_socket); |
| 1519 |
| 1520 static makeKey(bool isSecure, String host, int port) { |
| 1521 return isSecure ? "ssh:$host:$port" : "$host:$port"; |
| 1522 } |
| 1523 |
| 1524 void stopTimer() { |
| 1525 if (_idleTimer != null) { |
| 1526 _idleTimer.cancel(); |
| 1527 _idleTimer = null; |
| 1528 } |
| 1529 } |
| 1530 |
| 1531 void startTimer() { |
| 1532 assert(_idleTimer == null); |
| 1533 _idleTimer = new Timer( |
| 1534 _httpClient.idleTimeout, |
| 1535 () { |
| 1536 _idleTimer = null; |
| 1537 close(); |
| 1538 }); |
| 1539 } |
| 1540 } |
| 1541 |
| 1542 class _ConnectionInfo { |
| 1543 final _HttpClientConnection connection; |
| 1544 final _Proxy proxy; |
| 1545 |
| 1546 _ConnectionInfo(this.connection, this.proxy); |
| 1547 } |
| 1548 |
| 1549 |
| 1550 class _ConnectionTarget { |
| 1551 // Unique key for this connection target. |
| 1552 final String key; |
| 1553 final String host; |
| 1554 final int port; |
| 1555 final bool isSecure; |
| 1556 final SecurityContext context; |
| 1557 final Set<_HttpClientConnection> _idle = new HashSet(); |
| 1558 final Set<_HttpClientConnection> _active = new HashSet(); |
| 1559 final Queue _pending = new ListQueue(); |
| 1560 int _connecting = 0; |
| 1561 |
| 1562 _ConnectionTarget(this.key, |
| 1563 this.host, |
| 1564 this.port, |
| 1565 this.isSecure, |
| 1566 this.context); |
| 1567 |
| 1568 bool get isEmpty => _idle.isEmpty && _active.isEmpty && _connecting == 0; |
| 1569 |
| 1570 bool get hasIdle => _idle.isNotEmpty; |
| 1571 |
| 1572 bool get hasActive => _active.isNotEmpty || _connecting > 0; |
| 1573 |
| 1574 _HttpClientConnection takeIdle() { |
| 1575 assert(hasIdle); |
| 1576 _HttpClientConnection connection = _idle.first; |
| 1577 _idle.remove(connection); |
| 1578 connection.stopTimer(); |
| 1579 _active.add(connection); |
| 1580 return connection; |
| 1581 } |
| 1582 |
| 1583 _checkPending() { |
| 1584 if (_pending.isNotEmpty) { |
| 1585 _pending.removeFirst()(); |
| 1586 } |
| 1587 } |
| 1588 |
| 1589 void addNewActive(_HttpClientConnection connection) { |
| 1590 _active.add(connection); |
| 1591 } |
| 1592 |
| 1593 void returnConnection(_HttpClientConnection connection) { |
| 1594 assert(_active.contains(connection)); |
| 1595 _active.remove(connection); |
| 1596 _idle.add(connection); |
| 1597 connection.startTimer(); |
| 1598 _checkPending(); |
| 1599 } |
| 1600 |
| 1601 void connectionClosed(_HttpClientConnection connection) { |
| 1602 assert(!_active.contains(connection) || !_idle.contains(connection)); |
| 1603 _active.remove(connection); |
| 1604 _idle.remove(connection); |
| 1605 _checkPending(); |
| 1606 } |
| 1607 |
| 1608 void close(bool force) { |
| 1609 for (var c in _idle.toList()) { |
| 1610 c.close(); |
| 1611 } |
| 1612 if (force) { |
| 1613 for (var c in _active.toList()) { |
| 1614 c.destroy(); |
| 1615 } |
| 1616 } |
| 1617 } |
| 1618 |
| 1619 Future<_ConnectionInfo> connect(String uriHost, |
| 1620 int uriPort, |
| 1621 _Proxy proxy, |
| 1622 _HttpClient client) { |
| 1623 if (hasIdle) { |
| 1624 var connection = takeIdle(); |
| 1625 client._connectionsChanged(); |
| 1626 return new Future.value(new _ConnectionInfo(connection, proxy)); |
| 1627 } |
| 1628 if (client.maxConnectionsPerHost != null && |
| 1629 _active.length + _connecting >= client.maxConnectionsPerHost) { |
| 1630 var completer = new Completer(); |
| 1631 _pending.add(() { |
| 1632 connect(uriHost, uriPort, proxy, client) |
| 1633 .then(completer.complete, onError: completer.completeError); |
| 1634 }); |
| 1635 return completer.future; |
| 1636 } |
| 1637 var currentBadCertificateCallback = client._badCertificateCallback; |
| 1638 |
| 1639 bool callback(X509Certificate certificate) { |
| 1640 if (currentBadCertificateCallback == null) return false; |
| 1641 return currentBadCertificateCallback(certificate, uriHost, uriPort); |
| 1642 } |
| 1643 |
| 1644 Future socketFuture = (isSecure && proxy.isDirect |
| 1645 ? SecureSocket.connect(host, |
| 1646 port, |
| 1647 context: context, |
| 1648 onBadCertificate: callback) |
| 1649 : Socket.connect(host, port)); |
| 1650 _connecting++; |
| 1651 return socketFuture.then((socket) { |
| 1652 _connecting--; |
| 1653 socket.setOption(SocketOption.TCP_NODELAY, true); |
| 1654 var connection = |
| 1655 new _HttpClientConnection(key, socket, client, false, context); |
| 1656 if (isSecure && !proxy.isDirect) { |
| 1657 connection._dispose = true; |
| 1658 return connection.createProxyTunnel(uriHost, uriPort, proxy, callback) |
| 1659 .then((tunnel) { |
| 1660 client._getConnectionTarget(uriHost, uriPort, true) |
| 1661 .addNewActive(tunnel); |
| 1662 return new _ConnectionInfo(tunnel, proxy); |
| 1663 }); |
| 1664 } else { |
| 1665 addNewActive(connection); |
| 1666 return new _ConnectionInfo(connection, proxy); |
| 1667 } |
| 1668 }, onError: (error) { |
| 1669 _connecting--; |
| 1670 _checkPending(); |
| 1671 throw error; |
| 1672 }); |
| 1673 } |
| 1674 } |
| 1675 |
| 1676 typedef bool BadCertificateCallback(X509Certificate cr, String host, int port); |
| 1677 |
| 1678 class _HttpClient implements HttpClient { |
| 1679 bool _closing = false; |
| 1680 bool _closingForcefully = false; |
| 1681 final Map<String, _ConnectionTarget> _connectionTargets |
| 1682 = new HashMap<String, _ConnectionTarget>(); |
| 1683 final List<_Credentials> _credentials = []; |
| 1684 final List<_ProxyCredentials> _proxyCredentials = []; |
| 1685 final SecurityContext _context; |
| 1686 Function _authenticate; |
| 1687 Function _authenticateProxy; |
| 1688 Function _findProxy = HttpClient.findProxyFromEnvironment; |
| 1689 Duration _idleTimeout = const Duration(seconds: 15); |
| 1690 BadCertificateCallback _badCertificateCallback; |
| 1691 |
| 1692 Duration get idleTimeout => _idleTimeout; |
| 1693 |
| 1694 int maxConnectionsPerHost; |
| 1695 |
| 1696 bool autoUncompress = true; |
| 1697 |
| 1698 String userAgent = _getHttpVersion(); |
| 1699 |
| 1700 _HttpClient(SecurityContext this._context); |
| 1701 |
| 1702 void set idleTimeout(Duration timeout) { |
| 1703 _idleTimeout = timeout; |
| 1704 for (var c in _connectionTargets.values) { |
| 1705 for (var idle in c._idle) { |
| 1706 // Reset timer. This is fine, as it's not happening often. |
| 1707 idle.stopTimer(); |
| 1708 idle.startTimer(); |
| 1709 } |
| 1710 } |
| 1711 } |
| 1712 |
| 1713 set badCertificateCallback(bool callback(X509Certificate cert, |
| 1714 String host, |
| 1715 int port)) { |
| 1716 _badCertificateCallback = callback; |
| 1717 } |
| 1718 |
| 1719 |
| 1720 Future<HttpClientRequest> open(String method, |
| 1721 String host, |
| 1722 int port, |
| 1723 String path) { |
| 1724 const int hashMark = 0x23; |
| 1725 const int questionMark = 0x3f; |
| 1726 int fragmentStart = path.length; |
| 1727 int queryStart = path.length; |
| 1728 for (int i = path.length - 1; i >= 0; i--) { |
| 1729 var char = path.codeUnitAt(i); |
| 1730 if (char == hashMark) { |
| 1731 fragmentStart = i; |
| 1732 queryStart = i; |
| 1733 } else if (char == questionMark) { |
| 1734 queryStart = i; |
| 1735 } |
| 1736 } |
| 1737 String query = null; |
| 1738 if (queryStart < fragmentStart) { |
| 1739 query = path.substring(queryStart + 1, fragmentStart); |
| 1740 path = path.substring(0, queryStart); |
| 1741 } |
| 1742 Uri uri = new Uri(scheme: "http", host: host, port: port, |
| 1743 path: path, query: query); |
| 1744 return _openUrl(method, uri); |
| 1745 } |
| 1746 |
| 1747 Future<HttpClientRequest> openUrl(String method, Uri url) |
| 1748 => _openUrl(method, url); |
| 1749 |
| 1750 Future<HttpClientRequest> get(String host, int port, String path) |
| 1751 => open("get", host, port, path); |
| 1752 |
| 1753 Future<HttpClientRequest> getUrl(Uri url) => _openUrl("get", url); |
| 1754 |
| 1755 Future<HttpClientRequest> post(String host, int port, String path) |
| 1756 => open("post", host, port, path); |
| 1757 |
| 1758 Future<HttpClientRequest> postUrl(Uri url) => _openUrl("post", url); |
| 1759 |
| 1760 Future<HttpClientRequest> put(String host, int port, String path) |
| 1761 => open("put", host, port, path); |
| 1762 |
| 1763 Future<HttpClientRequest> putUrl(Uri url) => _openUrl("put", url); |
| 1764 |
| 1765 Future<HttpClientRequest> delete(String host, int port, String path) |
| 1766 => open("delete", host, port, path); |
| 1767 |
| 1768 Future<HttpClientRequest> deleteUrl(Uri url) => _openUrl("delete", url); |
| 1769 |
| 1770 Future<HttpClientRequest> head(String host, int port, String path) |
| 1771 => open("head", host, port, path); |
| 1772 |
| 1773 Future<HttpClientRequest> headUrl(Uri url) => _openUrl("head", url); |
| 1774 |
| 1775 Future<HttpClientRequest> patch(String host, int port, String path) |
| 1776 => open("patch", host, port, path); |
| 1777 |
| 1778 Future<HttpClientRequest> patchUrl(Uri url) => _openUrl("patch", url); |
| 1779 |
| 1780 void close({bool force: false}) { |
| 1781 _closing = true; |
| 1782 _closingForcefully = force; |
| 1783 _closeConnections(_closingForcefully); |
| 1784 assert(!_connectionTargets.values.any((s) => s.hasIdle)); |
| 1785 assert(!force || |
| 1786 !_connectionTargets.values.any((s) => s._active.isNotEmpty)); |
| 1787 } |
| 1788 |
| 1789 set authenticate(Future<bool> f(Uri url, String scheme, String realm)) { |
| 1790 _authenticate = f; |
| 1791 } |
| 1792 |
| 1793 void addCredentials(Uri url, String realm, HttpClientCredentials cr) { |
| 1794 _credentials.add(new _SiteCredentials(url, realm, cr)); |
| 1795 } |
| 1796 |
| 1797 set authenticateProxy( |
| 1798 Future<bool> f(String host, int port, String scheme, String realm)) { |
| 1799 _authenticateProxy = f; |
| 1800 } |
| 1801 |
| 1802 void addProxyCredentials(String host, |
| 1803 int port, |
| 1804 String realm, |
| 1805 HttpClientCredentials cr) { |
| 1806 _proxyCredentials.add(new _ProxyCredentials(host, port, realm, cr)); |
| 1807 } |
| 1808 |
| 1809 set findProxy(String f(Uri uri)) => _findProxy = f; |
| 1810 |
| 1811 Future<_HttpClientRequest> _openUrl(String method, Uri uri) { |
| 1812 // Ignore any fragments on the request URI. |
| 1813 uri = uri.removeFragment(); |
| 1814 |
| 1815 if (method == null) { |
| 1816 throw new ArgumentError(method); |
| 1817 } |
| 1818 if (method != "CONNECT") { |
| 1819 if (uri.host.isEmpty) { |
| 1820 throw new ArgumentError("No host specified in URI $uri"); |
| 1821 } else if (uri.scheme != "http" && uri.scheme != "https") { |
| 1822 throw new ArgumentError( |
| 1823 "Unsupported scheme '${uri.scheme}' in URI $uri"); |
| 1824 } |
| 1825 } |
| 1826 |
| 1827 bool isSecure = (uri.scheme == "https"); |
| 1828 int port = uri.port; |
| 1829 if (port == 0) { |
| 1830 port = isSecure ? |
| 1831 HttpClient.DEFAULT_HTTPS_PORT : |
| 1832 HttpClient.DEFAULT_HTTP_PORT; |
| 1833 } |
| 1834 // Check to see if a proxy server should be used for this connection. |
| 1835 var proxyConf = const _ProxyConfiguration.direct(); |
| 1836 if (_findProxy != null) { |
| 1837 // TODO(sgjesse): Keep a map of these as normally only a few |
| 1838 // configuration strings will be used. |
| 1839 try { |
| 1840 proxyConf = new _ProxyConfiguration(_findProxy(uri)); |
| 1841 } catch (error, stackTrace) { |
| 1842 return new Future.error(error, stackTrace); |
| 1843 } |
| 1844 } |
| 1845 return _getConnection(uri.host, port, proxyConf, isSecure) |
| 1846 .then((_ConnectionInfo info) { |
| 1847 |
| 1848 _HttpClientRequest send(_ConnectionInfo info) { |
| 1849 return info.connection.send(uri, |
| 1850 port, |
| 1851 method.toUpperCase(), |
| 1852 info.proxy); |
| 1853 } |
| 1854 |
| 1855 // If the connection was closed before the request was sent, create |
| 1856 // and use another connection. |
| 1857 if (info.connection.closed) { |
| 1858 return _getConnection(uri.host, port, proxyConf, isSecure) |
| 1859 .then(send); |
| 1860 } |
| 1861 return send(info); |
| 1862 }); |
| 1863 } |
| 1864 |
| 1865 Future<_HttpClientRequest> _openUrlFromRequest(String method, |
| 1866 Uri uri, |
| 1867 _HttpClientRequest previous) { |
| 1868 // If the new URI is relative (to either '/' or some sub-path), |
| 1869 // construct a full URI from the previous one. |
| 1870 Uri resolved = previous.uri.resolveUri(uri); |
| 1871 return _openUrl(method, resolved).then((_HttpClientRequest request) { |
| 1872 |
| 1873 request |
| 1874 // Only follow redirects if initial request did. |
| 1875 ..followRedirects = previous.followRedirects |
| 1876 // Allow same number of redirects. |
| 1877 ..maxRedirects = previous.maxRedirects; |
| 1878 // Copy headers. |
| 1879 for (var header in previous.headers._headers.keys) { |
| 1880 if (request.headers[header] == null) { |
| 1881 request.headers.set(header, previous.headers[header]); |
| 1882 } |
| 1883 } |
| 1884 return request |
| 1885 ..headers.chunkedTransferEncoding = false |
| 1886 ..contentLength = 0; |
| 1887 }); |
| 1888 } |
| 1889 |
| 1890 // Return a live connection to the idle pool. |
| 1891 void _returnConnection(_HttpClientConnection connection) { |
| 1892 _connectionTargets[connection.key].returnConnection(connection); |
| 1893 _connectionsChanged(); |
| 1894 } |
| 1895 |
| 1896 // Remove a closed connnection from the active set. |
| 1897 void _connectionClosed(_HttpClientConnection connection) { |
| 1898 connection.stopTimer(); |
| 1899 var connectionTarget = _connectionTargets[connection.key]; |
| 1900 if (connectionTarget != null) { |
| 1901 connectionTarget.connectionClosed(connection); |
| 1902 if (connectionTarget.isEmpty) { |
| 1903 _connectionTargets.remove(connection.key); |
| 1904 } |
| 1905 _connectionsChanged(); |
| 1906 } |
| 1907 } |
| 1908 |
| 1909 void _connectionsChanged() { |
| 1910 if (_closing) { |
| 1911 _closeConnections(_closingForcefully); |
| 1912 } |
| 1913 } |
| 1914 |
| 1915 void _closeConnections(bool force) { |
| 1916 for (var connectionTarget in _connectionTargets.values.toList()) { |
| 1917 connectionTarget.close(force); |
| 1918 } |
| 1919 } |
| 1920 |
| 1921 _ConnectionTarget _getConnectionTarget(String host, int port, bool isSecure) { |
| 1922 String key = _HttpClientConnection.makeKey(isSecure, host, port); |
| 1923 return _connectionTargets.putIfAbsent(key, () { |
| 1924 return new _ConnectionTarget(key, host, port, isSecure, _context); |
| 1925 }); |
| 1926 } |
| 1927 |
| 1928 // Get a new _HttpClientConnection, from the matching _ConnectionTarget. |
| 1929 Future<_ConnectionInfo> _getConnection(String uriHost, |
| 1930 int uriPort, |
| 1931 _ProxyConfiguration proxyConf, |
| 1932 bool isSecure) { |
| 1933 Iterator<_Proxy> proxies = proxyConf.proxies.iterator; |
| 1934 |
| 1935 Future<_ConnectionInfo> connect(error) { |
| 1936 if (!proxies.moveNext()) return new Future.error(error); |
| 1937 _Proxy proxy = proxies.current; |
| 1938 String host = proxy.isDirect ? uriHost: proxy.host; |
| 1939 int port = proxy.isDirect ? uriPort: proxy.port; |
| 1940 return _getConnectionTarget(host, port, isSecure) |
| 1941 .connect(uriHost, uriPort, proxy, this) |
| 1942 // On error, continue with next proxy. |
| 1943 .catchError(connect); |
| 1944 } |
| 1945 // Make sure we go through the event loop before taking a |
| 1946 // connection from the pool. For long-running synchronous code the |
| 1947 // server might have closed the connection, so this lowers the |
| 1948 // probability of getting a connection that was already closed. |
| 1949 return new Future(() => connect(new HttpException("No proxies given"))); |
| 1950 } |
| 1951 |
| 1952 _SiteCredentials _findCredentials(Uri url, [_AuthenticationScheme scheme]) { |
| 1953 // Look for credentials. |
| 1954 _SiteCredentials cr = |
| 1955 _credentials.fold(null, (_SiteCredentials prev, value) { |
| 1956 var siteCredentials = value as _SiteCredentials; |
| 1957 if (siteCredentials.applies(url, scheme)) { |
| 1958 if (prev == null) return value; |
| 1959 return siteCredentials.uri.path.length > prev.uri.path.length |
| 1960 ? siteCredentials |
| 1961 : prev; |
| 1962 } else { |
| 1963 return prev; |
| 1964 } |
| 1965 }); |
| 1966 return cr; |
| 1967 } |
| 1968 |
| 1969 _ProxyCredentials _findProxyCredentials(_Proxy proxy, |
| 1970 [_AuthenticationScheme scheme]) { |
| 1971 // Look for credentials. |
| 1972 var it = _proxyCredentials.iterator; |
| 1973 while (it.moveNext()) { |
| 1974 if (it.current.applies(proxy, scheme)) { |
| 1975 return it.current; |
| 1976 } |
| 1977 } |
| 1978 return null; |
| 1979 } |
| 1980 |
| 1981 void _removeCredentials(_Credentials cr) { |
| 1982 int index = _credentials.indexOf(cr); |
| 1983 if (index != -1) { |
| 1984 _credentials.removeAt(index); |
| 1985 } |
| 1986 } |
| 1987 |
| 1988 void _removeProxyCredentials(_Credentials cr) { |
| 1989 int index = _proxyCredentials.indexOf(cr); |
| 1990 if (index != -1) { |
| 1991 _proxyCredentials.removeAt(index); |
| 1992 } |
| 1993 } |
| 1994 |
| 1995 static String _findProxyFromEnvironment(Uri url, |
| 1996 Map<String, String> environment) { |
| 1997 checkNoProxy(String option) { |
| 1998 if (option == null) return null; |
| 1999 Iterator<String> names = option.split(",").map((s) => s.trim()).iterator; |
| 2000 while (names.moveNext()) { |
| 2001 var name = names.current; |
| 2002 if ((name.startsWith("[") && |
| 2003 name.endsWith("]") && |
| 2004 "[${url.host}]" == name) || |
| 2005 (name.isNotEmpty && |
| 2006 url.host.endsWith(name))) { |
| 2007 return "DIRECT"; |
| 2008 } |
| 2009 } |
| 2010 return null; |
| 2011 } |
| 2012 |
| 2013 checkProxy(String option) { |
| 2014 if (option == null) return null; |
| 2015 option = option.trim(); |
| 2016 if (option.isEmpty) return null; |
| 2017 int pos = option.indexOf("://"); |
| 2018 if (pos >= 0) { |
| 2019 option = option.substring(pos + 3); |
| 2020 } |
| 2021 pos = option.indexOf("/"); |
| 2022 if (pos >= 0) { |
| 2023 option = option.substring(0, pos); |
| 2024 } |
| 2025 // Add default port if no port configured. |
| 2026 if (option.indexOf("[") == 0) { |
| 2027 var pos = option.lastIndexOf(":"); |
| 2028 if (option.indexOf("]") > pos) option = "$option:1080"; |
| 2029 } else { |
| 2030 if (option.indexOf(":") == -1) option = "$option:1080"; |
| 2031 } |
| 2032 return "PROXY $option"; |
| 2033 } |
| 2034 |
| 2035 // Default to using the process current environment. |
| 2036 if (environment == null) environment = _platformEnvironmentCache; |
| 2037 |
| 2038 String proxyCfg; |
| 2039 |
| 2040 String noProxy = environment["no_proxy"]; |
| 2041 if (noProxy == null) noProxy = environment["NO_PROXY"]; |
| 2042 if ((proxyCfg = checkNoProxy(noProxy)) != null) { |
| 2043 return proxyCfg; |
| 2044 } |
| 2045 |
| 2046 if (url.scheme == "http") { |
| 2047 String proxy = environment["http_proxy"]; |
| 2048 if (proxy == null) proxy = environment["HTTP_PROXY"]; |
| 2049 if ((proxyCfg = checkProxy(proxy)) != null) { |
| 2050 return proxyCfg; |
| 2051 } |
| 2052 } else if (url.scheme == "https") { |
| 2053 String proxy = environment["https_proxy"]; |
| 2054 if (proxy == null) proxy = environment["HTTPS_PROXY"]; |
| 2055 if ((proxyCfg = checkProxy(proxy)) != null) { |
| 2056 return proxyCfg; |
| 2057 } |
| 2058 } |
| 2059 return "DIRECT"; |
| 2060 } |
| 2061 |
| 2062 static Map<String, String> _platformEnvironmentCache = Platform.environment; |
| 2063 } |
| 2064 |
| 2065 |
| 2066 class _HttpConnection |
| 2067 extends LinkedListEntry<_HttpConnection> with _ServiceObject { |
| 2068 static const _ACTIVE = 0; |
| 2069 static const _IDLE = 1; |
| 2070 static const _CLOSING = 2; |
| 2071 static const _DETACHED = 3; |
| 2072 |
| 2073 // Use HashMap, as we don't need to keep order. |
| 2074 static Map<int, _HttpConnection> _connections = |
| 2075 new HashMap<int, _HttpConnection>(); |
| 2076 |
| 2077 final _socket; |
| 2078 final _HttpServer _httpServer; |
| 2079 final _HttpParser _httpParser; |
| 2080 int _state = _IDLE; |
| 2081 StreamSubscription _subscription; |
| 2082 bool _idleMark = false; |
| 2083 Future _streamFuture; |
| 2084 |
| 2085 _HttpConnection(this._socket, this._httpServer) |
| 2086 : _httpParser = new _HttpParser.requestParser() { |
| 2087 try { _socket._owner = this; } catch (_) { print(_); } |
| 2088 _connections[_serviceId] = this; |
| 2089 _httpParser.listenToStream(_socket); |
| 2090 _subscription = _httpParser.listen( |
| 2091 (incoming) { |
| 2092 _httpServer._markActive(this); |
| 2093 // If the incoming was closed, close the connection. |
| 2094 incoming.dataDone.then((closing) { |
| 2095 if (closing) destroy(); |
| 2096 }); |
| 2097 // Only handle one incoming request at the time. Keep the |
| 2098 // stream paused until the request has been send. |
| 2099 _subscription.pause(); |
| 2100 _state = _ACTIVE; |
| 2101 var outgoing = new _HttpOutgoing(_socket); |
| 2102 var response = new _HttpResponse(incoming.uri, |
| 2103 incoming.headers.protocolVersion, |
| 2104 outgoing, |
| 2105 _httpServer.defaultResponseHeaders, |
| 2106 _httpServer.serverHeader); |
| 2107 var request = new _HttpRequest(response, incoming, _httpServer, this); |
| 2108 _streamFuture = outgoing.done |
| 2109 .then((_) { |
| 2110 response.deadline = null; |
| 2111 if (_state == _DETACHED) return; |
| 2112 if (response.persistentConnection && |
| 2113 request.persistentConnection && |
| 2114 incoming.fullBodyRead && |
| 2115 !_httpParser.upgrade && |
| 2116 !_httpServer.closed) { |
| 2117 _state = _IDLE; |
| 2118 _idleMark = false; |
| 2119 _httpServer._markIdle(this); |
| 2120 // Resume the subscription for incoming requests as the |
| 2121 // request is now processed. |
| 2122 _subscription.resume(); |
| 2123 } else { |
| 2124 // Close socket, keep-alive not used or body sent before |
| 2125 // received data was handled. |
| 2126 destroy(); |
| 2127 } |
| 2128 }, onError: (_) { |
| 2129 destroy(); |
| 2130 }); |
| 2131 outgoing.ignoreBody = request.method == "HEAD"; |
| 2132 response._httpRequest = request; |
| 2133 _httpServer._handleRequest(request); |
| 2134 }, |
| 2135 onDone: () { |
| 2136 destroy(); |
| 2137 }, |
| 2138 onError: (error) { |
| 2139 // Ignore failed requests that was closed before headers was received. |
| 2140 destroy(); |
| 2141 }); |
| 2142 } |
| 2143 |
| 2144 void markIdle() { |
| 2145 _idleMark = true; |
| 2146 } |
| 2147 |
| 2148 bool get isMarkedIdle => _idleMark; |
| 2149 |
| 2150 void destroy() { |
| 2151 if (_state == _CLOSING || _state == _DETACHED) return; |
| 2152 _state = _CLOSING; |
| 2153 _socket.destroy(); |
| 2154 _httpServer._connectionClosed(this); |
| 2155 _connections.remove(_serviceId); |
| 2156 } |
| 2157 |
| 2158 Future<Socket> detachSocket() { |
| 2159 _state = _DETACHED; |
| 2160 // Remove connection from server. |
| 2161 _httpServer._connectionClosed(this); |
| 2162 |
| 2163 _HttpDetachedIncoming detachedIncoming = _httpParser.detachIncoming(); |
| 2164 |
| 2165 return _streamFuture.then((_) { |
| 2166 _connections.remove(_serviceId); |
| 2167 return new _DetachedSocket(_socket, detachedIncoming); |
| 2168 }); |
| 2169 } |
| 2170 |
| 2171 HttpConnectionInfo get connectionInfo => _HttpConnectionInfo.create(_socket); |
| 2172 |
| 2173 bool get _isActive => _state == _ACTIVE; |
| 2174 bool get _isIdle => _state == _IDLE; |
| 2175 bool get _isClosing => _state == _CLOSING; |
| 2176 bool get _isDetached => _state == _DETACHED; |
| 2177 |
| 2178 String get _serviceTypePath => 'io/http/serverconnections'; |
| 2179 String get _serviceTypeName => 'HttpServerConnection'; |
| 2180 |
| 2181 Map _toJSON(bool ref) { |
| 2182 var name = "${_socket.address.host}:${_socket.port} <-> " |
| 2183 "${_socket.remoteAddress.host}:${_socket.remotePort}"; |
| 2184 var r = <String, dynamic>{ |
| 2185 'id': _servicePath, |
| 2186 'type': _serviceType(ref), |
| 2187 'name': name, |
| 2188 'user_name': name, |
| 2189 }; |
| 2190 if (ref) { |
| 2191 return r; |
| 2192 } |
| 2193 r['server'] = _httpServer._toJSON(true); |
| 2194 try { |
| 2195 r['socket'] = _socket._toJSON(true); |
| 2196 } catch (_) { |
| 2197 r['socket'] = { |
| 2198 'id': _servicePath, |
| 2199 'type': '@Socket', |
| 2200 'name': 'UserSocket', |
| 2201 'user_name': 'UserSocket', |
| 2202 }; |
| 2203 } |
| 2204 switch (_state) { |
| 2205 case _ACTIVE: r['state'] = "Active"; break; |
| 2206 case _IDLE: r['state'] = "Idle"; break; |
| 2207 case _CLOSING: r['state'] = "Closing"; break; |
| 2208 case _DETACHED: r['state'] = "Detached"; break; |
| 2209 default: r['state'] = 'Unknown'; break; |
| 2210 } |
| 2211 return r; |
| 2212 } |
| 2213 } |
| 2214 |
| 2215 |
| 2216 // HTTP server waiting for socket connections. |
| 2217 class _HttpServer |
| 2218 extends Stream<HttpRequest> with _ServiceObject |
| 2219 implements HttpServer { |
| 2220 // Use default Map so we keep order. |
| 2221 static Map<int, _HttpServer> _servers = new Map<int, _HttpServer>(); |
| 2222 |
| 2223 String serverHeader; |
| 2224 final HttpHeaders defaultResponseHeaders = _initDefaultResponseHeaders(); |
| 2225 bool autoCompress = false; |
| 2226 |
| 2227 Duration _idleTimeout; |
| 2228 Timer _idleTimer; |
| 2229 |
| 2230 static Future<HttpServer> bind( |
| 2231 address, int port, int backlog, bool v6Only, bool shared) { |
| 2232 return ServerSocket.bind( |
| 2233 address, port, backlog: backlog, v6Only: v6Only, shared: shared) |
| 2234 .then((socket) { |
| 2235 return new _HttpServer._(socket, true); |
| 2236 }); |
| 2237 } |
| 2238 |
| 2239 static Future<HttpServer> bindSecure(address, |
| 2240 int port, |
| 2241 SecurityContext context, |
| 2242 int backlog, |
| 2243 bool v6Only, |
| 2244 bool requestClientCertificate, |
| 2245 bool shared) { |
| 2246 return SecureServerSocket.bind( |
| 2247 address, |
| 2248 port, |
| 2249 context, |
| 2250 backlog: backlog, |
| 2251 v6Only: v6Only, |
| 2252 requestClientCertificate: requestClientCertificate, |
| 2253 shared: shared) |
| 2254 .then((socket) { |
| 2255 return new _HttpServer._(socket, true); |
| 2256 }); |
| 2257 } |
| 2258 |
| 2259 _HttpServer._(this._serverSocket, this._closeServer) { |
| 2260 _controller = new StreamController<HttpRequest>(sync: true, |
| 2261 onCancel: close); |
| 2262 idleTimeout = const Duration(seconds: 120); |
| 2263 _servers[_serviceId] = this; |
| 2264 _serverSocket._owner = this; |
| 2265 } |
| 2266 |
| 2267 _HttpServer.listenOn(this._serverSocket) : _closeServer = false { |
| 2268 _controller = new StreamController<HttpRequest>(sync: true, |
| 2269 onCancel: close); |
| 2270 idleTimeout = const Duration(seconds: 120); |
| 2271 _servers[_serviceId] = this; |
| 2272 try { _serverSocket._owner = this; } catch (_) {} |
| 2273 } |
| 2274 |
| 2275 static HttpHeaders _initDefaultResponseHeaders() { |
| 2276 var defaultResponseHeaders = new _HttpHeaders('1.1'); |
| 2277 defaultResponseHeaders.contentType = ContentType.TEXT; |
| 2278 defaultResponseHeaders.set('X-Frame-Options', 'SAMEORIGIN'); |
| 2279 defaultResponseHeaders.set('X-Content-Type-Options', 'nosniff'); |
| 2280 defaultResponseHeaders.set('X-XSS-Protection', '1; mode=block'); |
| 2281 return defaultResponseHeaders; |
| 2282 } |
| 2283 |
| 2284 Duration get idleTimeout => _idleTimeout; |
| 2285 |
| 2286 void set idleTimeout(Duration duration) { |
| 2287 if (_idleTimer != null) { |
| 2288 _idleTimer.cancel(); |
| 2289 _idleTimer = null; |
| 2290 } |
| 2291 _idleTimeout = duration; |
| 2292 if (_idleTimeout != null) { |
| 2293 _idleTimer = new Timer.periodic(_idleTimeout, (_) { |
| 2294 for (var idle in _idleConnections.toList()) { |
| 2295 if (idle.isMarkedIdle) { |
| 2296 idle.destroy(); |
| 2297 } else { |
| 2298 idle.markIdle(); |
| 2299 } |
| 2300 } |
| 2301 }); |
| 2302 } |
| 2303 } |
| 2304 |
| 2305 StreamSubscription<HttpRequest> listen(void onData(HttpRequest event), |
| 2306 {Function onError, |
| 2307 void onDone(), |
| 2308 bool cancelOnError}) { |
| 2309 _serverSocket.listen( |
| 2310 (Socket socket) { |
| 2311 socket.setOption(SocketOption.TCP_NODELAY, true); |
| 2312 // Accept the client connection. |
| 2313 _HttpConnection connection = new _HttpConnection(socket, this); |
| 2314 _idleConnections.add(connection); |
| 2315 }, |
| 2316 onError: (error, stackTrace) { |
| 2317 // Ignore HandshakeExceptions as they are bound to a single request, |
| 2318 // and are not fatal for the server. |
| 2319 if (error is! HandshakeException) { |
| 2320 _controller.addError(error, stackTrace); |
| 2321 } |
| 2322 }, |
| 2323 onDone: _controller.close); |
| 2324 return _controller.stream.listen(onData, |
| 2325 onError: onError, |
| 2326 onDone: onDone, |
| 2327 cancelOnError: cancelOnError); |
| 2328 } |
| 2329 |
| 2330 Future close({bool force: false}) { |
| 2331 closed = true; |
| 2332 Future result; |
| 2333 if (_serverSocket != null && _closeServer) { |
| 2334 result = _serverSocket.close(); |
| 2335 } else { |
| 2336 result = new Future.value(); |
| 2337 } |
| 2338 idleTimeout = null; |
| 2339 if (force) { |
| 2340 for (var c in _activeConnections.toList()) { |
| 2341 c.destroy(); |
| 2342 } |
| 2343 assert(_activeConnections.isEmpty); |
| 2344 } |
| 2345 for (var c in _idleConnections.toList()) { |
| 2346 c.destroy(); |
| 2347 } |
| 2348 _maybePerformCleanup(); |
| 2349 return result; |
| 2350 } |
| 2351 |
| 2352 void _maybePerformCleanup() { |
| 2353 if (closed && |
| 2354 _idleConnections.isEmpty && |
| 2355 _activeConnections.isEmpty && |
| 2356 _sessionManagerInstance != null) { |
| 2357 _sessionManagerInstance.close(); |
| 2358 _sessionManagerInstance = null; |
| 2359 _servers.remove(_serviceId); |
| 2360 } |
| 2361 } |
| 2362 |
| 2363 int get port { |
| 2364 if (closed) throw new HttpException("HttpServer is not bound to a socket"); |
| 2365 return _serverSocket.port; |
| 2366 } |
| 2367 |
| 2368 InternetAddress get address { |
| 2369 if (closed) throw new HttpException("HttpServer is not bound to a socket"); |
| 2370 return _serverSocket.address; |
| 2371 } |
| 2372 |
| 2373 set sessionTimeout(int timeout) { |
| 2374 _sessionManager.sessionTimeout = timeout; |
| 2375 } |
| 2376 |
| 2377 void _handleRequest(_HttpRequest request) { |
| 2378 if (!closed) { |
| 2379 _controller.add(request); |
| 2380 } else { |
| 2381 request._httpConnection.destroy(); |
| 2382 } |
| 2383 } |
| 2384 |
| 2385 void _connectionClosed(_HttpConnection connection) { |
| 2386 // Remove itself from either idle or active connections. |
| 2387 connection.unlink(); |
| 2388 _maybePerformCleanup(); |
| 2389 } |
| 2390 |
| 2391 void _markIdle(_HttpConnection connection) { |
| 2392 _activeConnections.remove(connection); |
| 2393 _idleConnections.add(connection); |
| 2394 } |
| 2395 |
| 2396 void _markActive(_HttpConnection connection) { |
| 2397 _idleConnections.remove(connection); |
| 2398 _activeConnections.add(connection); |
| 2399 } |
| 2400 |
| 2401 _HttpSessionManager get _sessionManager { |
| 2402 // Lazy init. |
| 2403 if (_sessionManagerInstance == null) { |
| 2404 _sessionManagerInstance = new _HttpSessionManager(); |
| 2405 } |
| 2406 return _sessionManagerInstance; |
| 2407 } |
| 2408 |
| 2409 HttpConnectionsInfo connectionsInfo() { |
| 2410 HttpConnectionsInfo result = new HttpConnectionsInfo(); |
| 2411 result.total = _activeConnections.length + _idleConnections.length; |
| 2412 _activeConnections.forEach((_HttpConnection conn) { |
| 2413 if (conn._isActive) { |
| 2414 result.active++; |
| 2415 } else { |
| 2416 assert(conn._isClosing); |
| 2417 result.closing++; |
| 2418 } |
| 2419 }); |
| 2420 _idleConnections.forEach((_HttpConnection conn) { |
| 2421 result.idle++; |
| 2422 assert(conn._isIdle); |
| 2423 }); |
| 2424 return result; |
| 2425 } |
| 2426 |
| 2427 String get _serviceTypePath => 'io/http/servers'; |
| 2428 String get _serviceTypeName => 'HttpServer'; |
| 2429 |
| 2430 Map<String, dynamic> _toJSON(bool ref) { |
| 2431 var r = <String, dynamic>{ |
| 2432 'id': _servicePath, |
| 2433 'type': _serviceType(ref), |
| 2434 'name': '${address.host}:$port', |
| 2435 'user_name': '${address.host}:$port', |
| 2436 }; |
| 2437 if (ref) { |
| 2438 return r; |
| 2439 } |
| 2440 try { |
| 2441 r['socket'] = _serverSocket._toJSON(true); |
| 2442 } catch (_) { |
| 2443 r['socket'] = { |
| 2444 'id': _servicePath, |
| 2445 'type': '@Socket', |
| 2446 'name': 'UserSocket', |
| 2447 'user_name': 'UserSocket', |
| 2448 }; |
| 2449 } |
| 2450 r['port'] = port; |
| 2451 r['address'] = address.host; |
| 2452 r['active'] = _activeConnections.map((c) => c._toJSON(true)).toList(); |
| 2453 r['idle'] = _idleConnections.map((c) => c._toJSON(true)).toList(); |
| 2454 r['closed'] = closed; |
| 2455 return r; |
| 2456 } |
| 2457 |
| 2458 _HttpSessionManager _sessionManagerInstance; |
| 2459 |
| 2460 // Indicated if the http server has been closed. |
| 2461 bool closed = false; |
| 2462 |
| 2463 // The server listen socket. Untyped as it can be both ServerSocket and |
| 2464 // SecureServerSocket. |
| 2465 final _serverSocket; |
| 2466 final bool _closeServer; |
| 2467 |
| 2468 // Set of currently connected clients. |
| 2469 final LinkedList<_HttpConnection> _activeConnections |
| 2470 = new LinkedList<_HttpConnection>(); |
| 2471 final LinkedList<_HttpConnection> _idleConnections |
| 2472 = new LinkedList<_HttpConnection>(); |
| 2473 StreamController<HttpRequest> _controller; |
| 2474 } |
| 2475 |
| 2476 |
| 2477 class _ProxyConfiguration { |
| 2478 static const String PROXY_PREFIX = "PROXY "; |
| 2479 static const String DIRECT_PREFIX = "DIRECT"; |
| 2480 |
| 2481 _ProxyConfiguration(String configuration) : proxies = new List<_Proxy>() { |
| 2482 if (configuration == null) { |
| 2483 throw new HttpException("Invalid proxy configuration $configuration"); |
| 2484 } |
| 2485 List<String> list = configuration.split(";"); |
| 2486 list.forEach((String proxy) { |
| 2487 proxy = proxy.trim(); |
| 2488 if (!proxy.isEmpty) { |
| 2489 if (proxy.startsWith(PROXY_PREFIX)) { |
| 2490 String username; |
| 2491 String password; |
| 2492 // Skip the "PROXY " prefix. |
| 2493 proxy = proxy.substring(PROXY_PREFIX.length).trim(); |
| 2494 // Look for proxy authentication. |
| 2495 int at = proxy.indexOf("@"); |
| 2496 if (at != -1) { |
| 2497 String userinfo = proxy.substring(0, at).trim(); |
| 2498 proxy = proxy.substring(at + 1).trim(); |
| 2499 int colon = userinfo.indexOf(":"); |
| 2500 if (colon == -1 || colon == 0 || colon == proxy.length - 1) { |
| 2501 throw new HttpException( |
| 2502 "Invalid proxy configuration $configuration"); |
| 2503 } |
| 2504 username = userinfo.substring(0, colon).trim(); |
| 2505 password = userinfo.substring(colon + 1).trim(); |
| 2506 } |
| 2507 // Look for proxy host and port. |
| 2508 int colon = proxy.lastIndexOf(":"); |
| 2509 if (colon == -1 || colon == 0 || colon == proxy.length - 1) { |
| 2510 throw new HttpException( |
| 2511 "Invalid proxy configuration $configuration"); |
| 2512 } |
| 2513 String host = proxy.substring(0, colon).trim(); |
| 2514 if (host.startsWith("[") && host.endsWith("]")) { |
| 2515 host = host.substring(1, host.length - 1); |
| 2516 } |
| 2517 String portString = proxy.substring(colon + 1).trim(); |
| 2518 int port; |
| 2519 try { |
| 2520 port = int.parse(portString); |
| 2521 } on FormatException catch (e) { |
| 2522 throw new HttpException( |
| 2523 "Invalid proxy configuration $configuration, " |
| 2524 "invalid port '$portString'"); |
| 2525 } |
| 2526 proxies.add(new _Proxy(host, port, username, password)); |
| 2527 } else if (proxy.trim() == DIRECT_PREFIX) { |
| 2528 proxies.add(new _Proxy.direct()); |
| 2529 } else { |
| 2530 throw new HttpException("Invalid proxy configuration $configuration"); |
| 2531 } |
| 2532 } |
| 2533 }); |
| 2534 } |
| 2535 |
| 2536 const _ProxyConfiguration.direct() |
| 2537 : proxies = const [const _Proxy.direct()]; |
| 2538 |
| 2539 final List<_Proxy> proxies; |
| 2540 } |
| 2541 |
| 2542 |
| 2543 class _Proxy { |
| 2544 final String host; |
| 2545 final int port; |
| 2546 final String username; |
| 2547 final String password; |
| 2548 final bool isDirect; |
| 2549 |
| 2550 const _Proxy(this.host, this.port, this.username, this.password) |
| 2551 : isDirect = false; |
| 2552 const _Proxy.direct() : host = null, port = null, |
| 2553 username = null, password = null, isDirect = true; |
| 2554 |
| 2555 bool get isAuthenticated => username != null; |
| 2556 } |
| 2557 |
| 2558 |
| 2559 class _HttpConnectionInfo implements HttpConnectionInfo { |
| 2560 InternetAddress remoteAddress; |
| 2561 int remotePort; |
| 2562 int localPort; |
| 2563 |
| 2564 static _HttpConnectionInfo create(Socket socket) { |
| 2565 if (socket == null) return null; |
| 2566 try { |
| 2567 _HttpConnectionInfo info = new _HttpConnectionInfo(); |
| 2568 return info |
| 2569 ..remoteAddress = socket.remoteAddress |
| 2570 ..remotePort = socket.remotePort |
| 2571 ..localPort = socket.port; |
| 2572 } catch (e) { } |
| 2573 return null; |
| 2574 } |
| 2575 } |
| 2576 |
| 2577 |
| 2578 class _DetachedSocket extends Stream<List<int>> implements Socket { |
| 2579 final Stream<List<int>> _incoming; |
| 2580 final _socket; |
| 2581 |
| 2582 _DetachedSocket(this._socket, this._incoming); |
| 2583 |
| 2584 StreamSubscription<List<int>> listen(void onData(List<int> event), |
| 2585 {Function onError, |
| 2586 void onDone(), |
| 2587 bool cancelOnError}) { |
| 2588 return _incoming.listen(onData, |
| 2589 onError: onError, |
| 2590 onDone: onDone, |
| 2591 cancelOnError: cancelOnError); |
| 2592 } |
| 2593 |
| 2594 Encoding get encoding => _socket.encoding; |
| 2595 |
| 2596 void set encoding(Encoding value) { |
| 2597 _socket.encoding = value; |
| 2598 } |
| 2599 |
| 2600 void write(Object obj) { _socket.write(obj); } |
| 2601 |
| 2602 void writeln([Object obj = ""]) { _socket.writeln(obj); } |
| 2603 |
| 2604 void writeCharCode(int charCode) { _socket.writeCharCode(charCode); } |
| 2605 |
| 2606 void writeAll(Iterable objects, [String separator = ""]) { |
| 2607 _socket.writeAll(objects, separator); |
| 2608 } |
| 2609 |
| 2610 void add(List<int> bytes) { _socket.add(bytes); } |
| 2611 |
| 2612 void addError(error, [StackTrace stackTrace]) => |
| 2613 _socket.addError(error, stackTrace); |
| 2614 |
| 2615 Future<Socket> addStream(Stream<List<int>> stream) { |
| 2616 return _socket.addStream(stream); |
| 2617 } |
| 2618 |
| 2619 void destroy() { _socket.destroy(); } |
| 2620 |
| 2621 Future flush() => _socket.flush(); |
| 2622 |
| 2623 Future close() => _socket.close(); |
| 2624 |
| 2625 Future<Socket> get done => _socket.done; |
| 2626 |
| 2627 int get port => _socket.port; |
| 2628 |
| 2629 InternetAddress get address => _socket.address; |
| 2630 |
| 2631 InternetAddress get remoteAddress => _socket.remoteAddress; |
| 2632 |
| 2633 int get remotePort => _socket.remotePort; |
| 2634 |
| 2635 bool setOption(SocketOption option, bool enabled) { |
| 2636 return _socket.setOption(option, enabled); |
| 2637 } |
| 2638 |
| 2639 Map _toJSON(bool ref) => _socket._toJSON(ref); |
| 2640 void set _owner(owner) { _socket._owner = owner; } |
| 2641 } |
| 2642 |
| 2643 |
| 2644 class _AuthenticationScheme { |
| 2645 final int _scheme; |
| 2646 |
| 2647 static const UNKNOWN = const _AuthenticationScheme(-1); |
| 2648 static const BASIC = const _AuthenticationScheme(0); |
| 2649 static const DIGEST = const _AuthenticationScheme(1); |
| 2650 |
| 2651 const _AuthenticationScheme(this._scheme); |
| 2652 |
| 2653 factory _AuthenticationScheme.fromString(String scheme) { |
| 2654 if (scheme.toLowerCase() == "basic") return BASIC; |
| 2655 if (scheme.toLowerCase() == "digest") return DIGEST; |
| 2656 return UNKNOWN; |
| 2657 } |
| 2658 |
| 2659 String toString() { |
| 2660 if (this == BASIC) return "Basic"; |
| 2661 if (this == DIGEST) return "Digest"; |
| 2662 return "Unknown"; |
| 2663 } |
| 2664 } |
| 2665 |
| 2666 |
| 2667 abstract class _Credentials { |
| 2668 _HttpClientCredentials credentials; |
| 2669 String realm; |
| 2670 bool used = false; |
| 2671 |
| 2672 // Digest specific fields. |
| 2673 String ha1; |
| 2674 String nonce; |
| 2675 String algorithm; |
| 2676 String qop; |
| 2677 int nonceCount; |
| 2678 |
| 2679 _Credentials(this.credentials, this.realm) { |
| 2680 if (credentials.scheme == _AuthenticationScheme.DIGEST) { |
| 2681 // Calculate the H(A1) value once. There is no mentioning of |
| 2682 // username/password encoding in RFC 2617. However there is an |
| 2683 // open draft for adding an additional accept-charset parameter to |
| 2684 // the WWW-Authenticate and Proxy-Authenticate headers, see |
| 2685 // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For |
| 2686 // now always use UTF-8 encoding. |
| 2687 _HttpClientDigestCredentials creds = credentials; |
| 2688 var hasher = new _MD5() |
| 2689 ..add(UTF8.encode(creds.username)) |
| 2690 ..add([_CharCode.COLON]) |
| 2691 ..add(realm.codeUnits) |
| 2692 ..add([_CharCode.COLON]) |
| 2693 ..add(UTF8.encode(creds.password)); |
| 2694 ha1 = _CryptoUtils.bytesToHex(hasher.close()); |
| 2695 } |
| 2696 } |
| 2697 |
| 2698 _AuthenticationScheme get scheme => credentials.scheme; |
| 2699 |
| 2700 void authorize(HttpClientRequest request); |
| 2701 } |
| 2702 |
| 2703 class _SiteCredentials extends _Credentials { |
| 2704 Uri uri; |
| 2705 |
| 2706 _SiteCredentials(this.uri, realm, _HttpClientCredentials creds) |
| 2707 : super(creds, realm); |
| 2708 |
| 2709 bool applies(Uri uri, _AuthenticationScheme scheme) { |
| 2710 if (scheme != null && credentials.scheme != scheme) return false; |
| 2711 if (uri.host != this.uri.host) return false; |
| 2712 int thisPort = |
| 2713 this.uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : this.uri.port; |
| 2714 int otherPort = uri.port == 0 ? HttpClient.DEFAULT_HTTP_PORT : uri.port; |
| 2715 if (otherPort != thisPort) return false; |
| 2716 return uri.path.startsWith(this.uri.path); |
| 2717 } |
| 2718 |
| 2719 void authorize(HttpClientRequest request) { |
| 2720 // Digest credentials cannot be used without a nonce from the |
| 2721 // server. |
| 2722 if (credentials.scheme == _AuthenticationScheme.DIGEST && |
| 2723 nonce == null) { |
| 2724 return; |
| 2725 } |
| 2726 credentials.authorize(this, request); |
| 2727 used = true; |
| 2728 } |
| 2729 } |
| 2730 |
| 2731 |
| 2732 class _ProxyCredentials extends _Credentials { |
| 2733 String host; |
| 2734 int port; |
| 2735 |
| 2736 _ProxyCredentials(this.host, |
| 2737 this.port, |
| 2738 realm, |
| 2739 _HttpClientCredentials creds) |
| 2740 : super(creds, realm); |
| 2741 |
| 2742 bool applies(_Proxy proxy, _AuthenticationScheme scheme) { |
| 2743 if (scheme != null && credentials.scheme != scheme) return false; |
| 2744 return proxy.host == host && proxy.port == port; |
| 2745 } |
| 2746 |
| 2747 void authorize(HttpClientRequest request) { |
| 2748 // Digest credentials cannot be used without a nonce from the |
| 2749 // server. |
| 2750 if (credentials.scheme == _AuthenticationScheme.DIGEST && |
| 2751 nonce == null) { |
| 2752 return; |
| 2753 } |
| 2754 credentials.authorizeProxy(this, request); |
| 2755 } |
| 2756 } |
| 2757 |
| 2758 |
| 2759 abstract class _HttpClientCredentials implements HttpClientCredentials { |
| 2760 _AuthenticationScheme get scheme; |
| 2761 void authorize(_Credentials credentials, HttpClientRequest request); |
| 2762 void authorizeProxy(_ProxyCredentials credentials, HttpClientRequest request); |
| 2763 } |
| 2764 |
| 2765 |
| 2766 class _HttpClientBasicCredentials |
| 2767 extends _HttpClientCredentials |
| 2768 implements HttpClientBasicCredentials { |
| 2769 String username; |
| 2770 String password; |
| 2771 |
| 2772 _HttpClientBasicCredentials(this.username, this.password); |
| 2773 |
| 2774 _AuthenticationScheme get scheme => _AuthenticationScheme.BASIC; |
| 2775 |
| 2776 String authorization() { |
| 2777 // There is no mentioning of username/password encoding in RFC |
| 2778 // 2617. However there is an open draft for adding an additional |
| 2779 // accept-charset parameter to the WWW-Authenticate and |
| 2780 // Proxy-Authenticate headers, see |
| 2781 // http://tools.ietf.org/html/draft-reschke-basicauth-enc-06. For |
| 2782 // now always use UTF-8 encoding. |
| 2783 String auth = |
| 2784 _CryptoUtils.bytesToBase64(UTF8.encode("$username:$password")); |
| 2785 return "Basic $auth"; |
| 2786 } |
| 2787 |
| 2788 void authorize(_Credentials _, HttpClientRequest request) { |
| 2789 request.headers.set(HttpHeaders.AUTHORIZATION, authorization()); |
| 2790 } |
| 2791 |
| 2792 void authorizeProxy(_ProxyCredentials _, HttpClientRequest request) { |
| 2793 request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, authorization()); |
| 2794 } |
| 2795 } |
| 2796 |
| 2797 |
| 2798 class _HttpClientDigestCredentials |
| 2799 extends _HttpClientCredentials |
| 2800 implements HttpClientDigestCredentials { |
| 2801 String username; |
| 2802 String password; |
| 2803 |
| 2804 _HttpClientDigestCredentials(this.username, this.password); |
| 2805 |
| 2806 _AuthenticationScheme get scheme => _AuthenticationScheme.DIGEST; |
| 2807 |
| 2808 String authorization(_Credentials credentials, _HttpClientRequest request) { |
| 2809 String requestUri = request._requestUri(); |
| 2810 _MD5 hasher = new _MD5() |
| 2811 ..add(request.method.codeUnits) |
| 2812 ..add([_CharCode.COLON]) |
| 2813 ..add(requestUri.codeUnits); |
| 2814 var ha2 = _CryptoUtils.bytesToHex(hasher.close()); |
| 2815 |
| 2816 String qop; |
| 2817 String cnonce; |
| 2818 String nc; |
| 2819 var x; |
| 2820 hasher = new _MD5() |
| 2821 ..add(credentials.ha1.codeUnits) |
| 2822 ..add([_CharCode.COLON]); |
| 2823 if (credentials.qop == "auth") { |
| 2824 qop = credentials.qop; |
| 2825 cnonce = _CryptoUtils.bytesToHex(_IOCrypto.getRandomBytes(4)); |
| 2826 ++credentials.nonceCount; |
| 2827 nc = credentials.nonceCount.toRadixString(16); |
| 2828 nc = "00000000".substring(0, 8 - nc.length + 1) + nc; |
| 2829 hasher |
| 2830 ..add(credentials.nonce.codeUnits) |
| 2831 ..add([_CharCode.COLON]) |
| 2832 ..add(nc.codeUnits) |
| 2833 ..add([_CharCode.COLON]) |
| 2834 ..add(cnonce.codeUnits) |
| 2835 ..add([_CharCode.COLON]) |
| 2836 ..add(credentials.qop.codeUnits) |
| 2837 ..add([_CharCode.COLON]) |
| 2838 ..add(ha2.codeUnits); |
| 2839 } else { |
| 2840 hasher |
| 2841 ..add(credentials.nonce.codeUnits) |
| 2842 ..add([_CharCode.COLON]) |
| 2843 ..add(ha2.codeUnits); |
| 2844 } |
| 2845 var response = _CryptoUtils.bytesToHex(hasher.close()); |
| 2846 |
| 2847 StringBuffer buffer = new StringBuffer() |
| 2848 ..write('Digest ') |
| 2849 ..write('username="$username"') |
| 2850 ..write(', realm="${credentials.realm}"') |
| 2851 ..write(', nonce="${credentials.nonce}"') |
| 2852 ..write(', uri="$requestUri"') |
| 2853 ..write(', algorithm="${credentials.algorithm}"'); |
| 2854 if (qop == "auth") { |
| 2855 buffer |
| 2856 ..write(', qop="$qop"') |
| 2857 ..write(', cnonce="$cnonce"') |
| 2858 ..write(', nc="$nc"'); |
| 2859 } |
| 2860 buffer.write(', response="$response"'); |
| 2861 return buffer.toString(); |
| 2862 } |
| 2863 |
| 2864 void authorize(_Credentials credentials, HttpClientRequest request) { |
| 2865 request.headers.set(HttpHeaders.AUTHORIZATION, |
| 2866 authorization(credentials, request)); |
| 2867 } |
| 2868 |
| 2869 void authorizeProxy(_ProxyCredentials credentials, |
| 2870 HttpClientRequest request) { |
| 2871 request.headers.set(HttpHeaders.PROXY_AUTHORIZATION, |
| 2872 authorization(credentials, request)); |
| 2873 } |
| 2874 } |
| 2875 |
| 2876 |
| 2877 class _RedirectInfo implements RedirectInfo { |
| 2878 final int statusCode; |
| 2879 final String method; |
| 2880 final Uri location; |
| 2881 const _RedirectInfo(this.statusCode, this.method, this.location); |
| 2882 } |
| 2883 |
| 2884 String _getHttpVersion() { |
| 2885 var version = Platform.version; |
| 2886 // Only include major and minor version numbers. |
| 2887 int index = version.indexOf('.', version.indexOf('.') + 1); |
| 2888 version = version.substring(0, index); |
| 2889 return 'Dart/$version (dart:io)'; |
| 2890 } |
OLD | NEW |