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 |