| 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 |