Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3)

Side by Side Diff: sdk/lib/io/http_impl.dart

Issue 18768002: Add 'uri' to HttpException, and include in toString. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« sdk/lib/io/http.dart ('K') | « sdk/lib/io/http.dart ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 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 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 part of dart.io; 5 part of dart.io;
6 6
7 class _HttpIncoming extends Stream<List<int>> { 7 class _HttpIncoming extends Stream<List<int>> {
8 final int _transferLength; 8 final int _transferLength;
9 final Completer _dataCompleter = new Completer(); 9 final Completer _dataCompleter = new Completer();
10 Stream<List<int>> _stream; 10 Stream<List<int>> _stream;
(...skipping 23 matching lines...) Expand all
34 _HttpIncoming(_HttpHeaders this.headers, 34 _HttpIncoming(_HttpHeaders this.headers,
35 int this._transferLength, 35 int this._transferLength,
36 Stream<List<int>> this._stream) { 36 Stream<List<int>> this._stream) {
37 } 37 }
38 38
39 StreamSubscription<List<int>> listen(void onData(List<int> event), 39 StreamSubscription<List<int>> listen(void onData(List<int> event),
40 {void onError(error), 40 {void onError(error),
41 void onDone(), 41 void onDone(),
42 bool cancelOnError}) { 42 bool cancelOnError}) {
43 hasSubscriber = true; 43 hasSubscriber = true;
44 return _stream.listen(onData, 44 return _stream
45 onError: onError, 45 .handleError((error) {
46 onDone: onDone, 46 throw new HttpException(error.message, uri: uri);
47 cancelOnError: cancelOnError); 47 })
48 .listen(onData,
49 onError: onError,
50 onDone: onDone,
51 cancelOnError: cancelOnError);
48 } 52 }
49 53
50 // Is completed once all data have been received. 54 // Is completed once all data have been received.
51 Future get dataDone => _dataCompleter.future; 55 Future get dataDone => _dataCompleter.future;
52 56
53 void close(bool closing) { 57 void close(bool closing) {
54 fullBodyRead = true; 58 fullBodyRead = true;
55 hasSubscriber = true; 59 hasSubscriber = true;
56 _dataCompleter.complete(closing); 60 _dataCompleter.complete(closing);
57 } 61 }
(...skipping 330 matching lines...) Expand 10 before | Expand all | Expand 10 after
388 // Used to mark when the body should be written. This is used for HEAD 392 // Used to mark when the body should be written. This is used for HEAD
389 // requests and in error handling. 393 // requests and in error handling.
390 bool _ignoreBody = false; 394 bool _ignoreBody = false;
391 bool _headersWritten = false; 395 bool _headersWritten = false;
392 bool _asGZip = false; 396 bool _asGZip = false;
393 397
394 IOSink _headersSink; 398 IOSink _headersSink;
395 IOSink _dataSink; 399 IOSink _dataSink;
396 400
397 final _HttpOutgoing _outgoing; 401 final _HttpOutgoing _outgoing;
402 final Uri _uri;
398 403
399 final _HttpHeaders headers; 404 final _HttpHeaders headers;
400 405
401 _HttpOutboundMessage(String protocolVersion, _HttpOutgoing outgoing) 406 _HttpOutboundMessage(Uri this._uri,
407 String protocolVersion,
408 _HttpOutgoing outgoing)
402 : _outgoing = outgoing, 409 : _outgoing = outgoing,
403 _headersSink = new IOSink(outgoing, encoding: Encoding.ASCII), 410 _headersSink = new IOSink(outgoing, encoding: Encoding.ASCII),
404 headers = new _HttpHeaders(protocolVersion) { 411 headers = new _HttpHeaders(protocolVersion) {
405 _dataSink = new IOSink(new _HttpOutboundConsumer(this)); 412 _dataSink = new IOSink(new _HttpOutboundConsumer(this));
406 } 413 }
407 414
408 int get contentLength => headers.contentLength; 415 int get contentLength => headers.contentLength;
409 void set contentLength(int contentLength) { 416 void set contentLength(int contentLength) {
410 headers.contentLength = contentLength; 417 headers.contentLength = contentLength;
411 } 418 }
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after
504 return _headersSink.close(); 511 return _headersSink.close();
505 } 512 }
506 stream = stream.transform(new _BufferTransformer()); 513 stream = stream.transform(new _BufferTransformer());
507 if (headers.chunkedTransferEncoding) { 514 if (headers.chunkedTransferEncoding) {
508 if (_asGZip) { 515 if (_asGZip) {
509 stream = stream.transform(new ZLibDeflater(gzip: true, level: 6)); 516 stream = stream.transform(new ZLibDeflater(gzip: true, level: 6));
510 } 517 }
511 stream = stream.transform(new _ChunkedTransformer()); 518 stream = stream.transform(new _ChunkedTransformer());
512 } else if (contentLength >= 0) { 519 } else if (contentLength >= 0) {
513 stream = stream.transform( 520 stream = stream.transform(
514 new _ContentLengthValidator(contentLength)); 521 new _ContentLengthValidator(contentLength, _uri));
515 } 522 }
516 return _headersSink.addStream(stream); 523 return _headersSink.addStream(stream);
517 }); 524 });
518 } 525 }
519 526
520 Future _close() { 527 Future _close() {
521 // TODO(ajohnsen): Currently, contentLength, chunkedTransferEncoding and 528 // TODO(ajohnsen): Currently, contentLength, chunkedTransferEncoding and
522 // persistentConnection is not guaranteed to be in sync. 529 // persistentConnection is not guaranteed to be in sync.
523 if (!_headersWritten) { 530 if (!_headersWritten) {
524 if (!_ignoreBody && headers.contentLength == -1) { 531 if (!_ignoreBody && headers.contentLength == -1) {
525 // If no body was written, _ignoreBody is false (it's not a HEAD 532 // If no body was written, _ignoreBody is false (it's not a HEAD
526 // request) and the content-length is unspecified, set contentLength to 533 // request) and the content-length is unspecified, set contentLength to
527 // 0. 534 // 0.
528 headers.chunkedTransferEncoding = false; 535 headers.chunkedTransferEncoding = false;
529 headers.contentLength = 0; 536 headers.contentLength = 0;
530 } else if (!_ignoreBody && headers.contentLength > 0) { 537 } else if (!_ignoreBody && headers.contentLength > 0) {
531 _headersSink.close().catchError((_) {}); 538 _headersSink.close().catchError((_) {});
532 return new Future.error(new HttpException( 539 return new Future.error(new HttpException(
533 "No content while contentLength was specified to be greater " 540 "No content while contentLength was specified to be greater "
534 " than 0: ${headers.contentLength}.")); 541 " than 0: ${headers.contentLength}.",
542 uri: _uri));
535 } 543 }
536 } 544 }
537 return _writeHeaders().then((_) => _headersSink.close()); 545 return _writeHeaders().then((_) => _headersSink.close());
538 } 546 }
539 547
540 void _writeHeader(); // TODO(ajohnsen): Better name. 548 void _writeHeader(); // TODO(ajohnsen): Better name.
541 } 549 }
542 550
543 551
544 class _HttpOutboundConsumer implements StreamConsumer { 552 class _HttpOutboundConsumer implements StreamConsumer {
(...skipping 124 matching lines...) Expand 10 before | Expand all | Expand 10 after
669 } 677 }
670 678
671 679
672 class _HttpResponse extends _HttpOutboundMessage<HttpResponse> 680 class _HttpResponse extends _HttpOutboundMessage<HttpResponse>
673 implements HttpResponse { 681 implements HttpResponse {
674 int statusCode = 200; 682 int statusCode = 200;
675 String _reasonPhrase; 683 String _reasonPhrase;
676 List<Cookie> _cookies; 684 List<Cookie> _cookies;
677 _HttpRequest _httpRequest; 685 _HttpRequest _httpRequest;
678 686
679 _HttpResponse(String protocolVersion, 687 _HttpResponse(Uri uri,
688 String protocolVersion,
680 _HttpOutgoing _outgoing, 689 _HttpOutgoing _outgoing,
681 String serverHeader) 690 String serverHeader)
682 : super(protocolVersion, _outgoing) { 691 : super(uri, protocolVersion, _outgoing) {
683 if (serverHeader != null) headers.set('Server', serverHeader); 692 if (serverHeader != null) headers.set('Server', serverHeader);
684 } 693 }
685 694
686 List<Cookie> get cookies { 695 List<Cookie> get cookies {
687 if (_cookies == null) _cookies = new List<Cookie>(); 696 if (_cookies == null) _cookies = new List<Cookie>();
688 return _cookies; 697 return _cookies;
689 } 698 }
690 699
691 String get reasonPhrase => _findReasonPhrase(statusCode); 700 String get reasonPhrase => _findReasonPhrase(statusCode);
692 void set reasonPhrase(String reasonPhrase) { 701 void set reasonPhrase(String reasonPhrase) {
(...skipping 144 matching lines...) Expand 10 before | Expand all | Expand 10 after
837 Future<HttpClientResponse> _response; 846 Future<HttpClientResponse> _response;
838 847
839 // TODO(ajohnsen): Get default value from client? 848 // TODO(ajohnsen): Get default value from client?
840 bool _followRedirects = true; 849 bool _followRedirects = true;
841 850
842 int _maxRedirects = 5; 851 int _maxRedirects = 5;
843 852
844 List<RedirectInfo> _responseRedirects = []; 853 List<RedirectInfo> _responseRedirects = [];
845 854
846 _HttpClientRequest(_HttpOutgoing outgoing, 855 _HttpClientRequest(_HttpOutgoing outgoing,
847 Uri this.uri, 856 Uri uri,
848 String this.method, 857 String this.method,
849 _Proxy this._proxy, 858 _Proxy this._proxy,
850 _HttpClient this._httpClient, 859 _HttpClient this._httpClient,
851 _HttpClientConnection this._httpClientConnection) 860 _HttpClientConnection this._httpClientConnection)
852 : super("1.1", outgoing) { 861 : super(uri, "1.1", outgoing),
862 uri = uri {
853 // GET and HEAD have 'content-length: 0' by default. 863 // GET and HEAD have 'content-length: 0' by default.
854 if (method == "GET" || method == "HEAD") { 864 if (method == "GET" || method == "HEAD") {
855 contentLength = 0; 865 contentLength = 0;
856 } 866 }
857 } 867 }
858 868
859 Future<HttpClientResponse> get done { 869 Future<HttpClientResponse> get done {
860 if (_response == null) { 870 if (_response == null) {
861 _response = Future.wait([_responseCompleter.future, 871 _response = Future.wait([_responseCompleter.future,
862 super.done]) 872 super.done])
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
1042 1052
1043 static List<int> get _chunk0Length => new Uint8List.fromList( 1053 static List<int> get _chunk0Length => new Uint8List.fromList(
1044 const [0x30, _CharCode.CR, _CharCode.LF, _CharCode.CR, _CharCode.LF]); 1054 const [0x30, _CharCode.CR, _CharCode.LF, _CharCode.CR, _CharCode.LF]);
1045 } 1055 }
1046 1056
1047 1057
1048 // Transformer that validates the content length. 1058 // Transformer that validates the content length.
1049 class _ContentLengthValidator 1059 class _ContentLengthValidator
1050 extends StreamEventTransformer<List<int>, List<int>> { 1060 extends StreamEventTransformer<List<int>, List<int>> {
1051 final int expectedContentLength; 1061 final int expectedContentLength;
1062 final Uri uri;
1052 int _bytesWritten = 0; 1063 int _bytesWritten = 0;
1053 1064
1054 _ContentLengthValidator(int this.expectedContentLength); 1065 _ContentLengthValidator(int this.expectedContentLength, Uri this.uri);
1055 1066
1056 void handleData(List<int> data, EventSink<List<int>> sink) { 1067 void handleData(List<int> data, EventSink<List<int>> sink) {
1057 _bytesWritten += data.length; 1068 _bytesWritten += data.length;
1058 if (_bytesWritten > expectedContentLength) { 1069 if (_bytesWritten > expectedContentLength) {
1059 sink.addError(new HttpException( 1070 sink.addError(new HttpException(
1060 "Content size exceeds specified contentLength. " 1071 "Content size exceeds specified contentLength. "
1061 "$_bytesWritten bytes written while expected " 1072 "$_bytesWritten bytes written while expected "
1062 "$expectedContentLength. " 1073 "$expectedContentLength. "
1063 "[${new String.fromCharCodes(data)}]")); 1074 "[${new String.fromCharCodes(data)}]",
1075 uri: uri));
1064 sink.close(); 1076 sink.close();
1065 } else { 1077 } else {
1066 sink.add(data); 1078 sink.add(data);
1067 } 1079 }
1068 } 1080 }
1069 1081
1070 void handleDone(EventSink<List<int>> sink) { 1082 void handleDone(EventSink<List<int>> sink) {
1071 if (_bytesWritten < expectedContentLength) { 1083 if (_bytesWritten < expectedContentLength) {
1072 sink.addError(new HttpException( 1084 sink.addError(new HttpException(
1073 "Content size below specified contentLength. " 1085 "Content size below specified contentLength. "
1074 " $_bytesWritten bytes written while expected " 1086 " $_bytesWritten bytes written while expected "
1075 "$expectedContentLength.")); 1087 "$expectedContentLength.",
1088 uri: uri));
1076 } 1089 }
1077 sink.close(); 1090 sink.close();
1078 } 1091 }
1079 } 1092 }
1080 1093
1081 1094
1082 // Extends StreamConsumer as this is an internal type, only used to pipe to. 1095 // Extends StreamConsumer as this is an internal type, only used to pipe to.
1083 class _HttpOutgoing implements StreamConsumer<List<int>> { 1096 class _HttpOutgoing implements StreamConsumer<List<int>> {
1084 final Completer _doneCompleter = new Completer(); 1097 final Completer _doneCompleter = new Completer();
1085 final StreamConsumer _consumer; 1098 final StreamConsumer _consumer;
(...skipping 19 matching lines...) Expand all
1105 class _HttpClientConnection { 1118 class _HttpClientConnection {
1106 final String key; 1119 final String key;
1107 final Socket _socket; 1120 final Socket _socket;
1108 final bool _proxyTunnel; 1121 final bool _proxyTunnel;
1109 final _HttpParser _httpParser; 1122 final _HttpParser _httpParser;
1110 StreamSubscription _subscription; 1123 StreamSubscription _subscription;
1111 final _HttpClient _httpClient; 1124 final _HttpClient _httpClient;
1112 bool _dispose = false; 1125 bool _dispose = false;
1113 Timer _idleTimer; 1126 Timer _idleTimer;
1114 bool closed = false; 1127 bool closed = false;
1128 Uri _currentUri;
1115 1129
1116 Completer<_HttpIncoming> _nextResponseCompleter; 1130 Completer<_HttpIncoming> _nextResponseCompleter;
1117 Future _streamFuture; 1131 Future _streamFuture;
1118 1132
1119 _HttpClientConnection(String this.key, 1133 _HttpClientConnection(String this.key,
1120 Socket this._socket, 1134 Socket this._socket,
1121 _HttpClient this._httpClient, 1135 _HttpClient this._httpClient,
1122 [this._proxyTunnel = false]) 1136 [this._proxyTunnel = false])
1123 : _httpParser = new _HttpParser.responseParser() { 1137 : _httpParser = new _HttpParser.responseParser() {
1124 _socket.pipe(_httpParser); 1138 _socket.pipe(_httpParser);
1125 1139
1126 // Set up handlers on the parser here, so we are sure to get 'onDone' from 1140 // Set up handlers on the parser here, so we are sure to get 'onDone' from
1127 // the parser. 1141 // the parser.
1128 _subscription = _httpParser.listen( 1142 _subscription = _httpParser.listen(
1129 (incoming) { 1143 (incoming) {
1130 // Only handle one incoming response at the time. Keep the 1144 // Only handle one incoming response at the time. Keep the
1131 // stream paused until the response have been processed. 1145 // stream paused until the response have been processed.
1132 _subscription.pause(); 1146 _subscription.pause();
1133 // We assume the response is not here, until we have send the request. 1147 // We assume the response is not here, until we have send the request.
1134 if (_nextResponseCompleter == null) { 1148 if (_nextResponseCompleter == null) {
1135 throw new HttpException("Unexpected response."); 1149 throw new HttpException("Unexpected response.", uri: _currentUri);
1136 } 1150 }
1137 _nextResponseCompleter.complete(incoming); 1151 _nextResponseCompleter.complete(incoming);
1138 _nextResponseCompleter = null; 1152 _nextResponseCompleter = null;
1139 }, 1153 },
1140 onError: (error) { 1154 onError: (error) {
1141 if (_nextResponseCompleter != null) { 1155 if (_nextResponseCompleter != null) {
1142 _nextResponseCompleter.completeError(error); 1156 _nextResponseCompleter.completeError(
1157 new HttpException(error.message, uri: _currentUri));
1143 _nextResponseCompleter = null; 1158 _nextResponseCompleter = null;
1144 } 1159 }
1145 }, 1160 },
1146 onDone: () { 1161 onDone: () {
1147 if (_nextResponseCompleter != null) { 1162 if (_nextResponseCompleter != null) {
1148 _nextResponseCompleter.completeError(new HttpException( 1163 _nextResponseCompleter.completeError(new HttpException(
1149 "Connection closed before response was received")); 1164 "Connection closed before response was received",
1165 uri: _currentUri));
1150 _nextResponseCompleter = null; 1166 _nextResponseCompleter = null;
1151 } 1167 }
1152 close(); 1168 close();
1153 }); 1169 });
1154 } 1170 }
1155 1171
1156 _HttpClientRequest send(Uri uri, int port, String method, _Proxy proxy) { 1172 _HttpClientRequest send(Uri uri, int port, String method, _Proxy proxy) {
1157 if (closed) { 1173 if (closed) {
1158 throw new HttpException("Socket closed before request was sent"); 1174 throw new HttpException(
1175 "Socket closed before request was sent", uri: uri);
1159 } 1176 }
1177 _currentUri = uri;
1160 // Start with pausing the parser. 1178 // Start with pausing the parser.
1161 _subscription.pause(); 1179 _subscription.pause();
1162 _ProxyCredentials proxyCreds; // Credentials used to authorize proxy. 1180 _ProxyCredentials proxyCreds; // Credentials used to authorize proxy.
1163 _SiteCredentials creds; // Credentials used to authorize this request. 1181 _SiteCredentials creds; // Credentials used to authorize this request.
1164 var outgoing = new _HttpOutgoing(_socket); 1182 var outgoing = new _HttpOutgoing(_socket);
1165 // Create new request object, wrapping the outgoing connection. 1183 // Create new request object, wrapping the outgoing connection.
1166 var request = new _HttpClientRequest(outgoing, 1184 var request = new _HttpClientRequest(outgoing,
1167 uri, 1185 uri,
1168 method, 1186 method,
1169 proxy, 1187 proxy,
(...skipping 78 matching lines...) Expand 10 before | Expand all | Expand 10 after
1248 var nextnonce = header.parameters["nextnonce"]; 1266 var nextnonce = header.parameters["nextnonce"];
1249 if (nextnonce != null) creds.nonce = nextnonce; 1267 if (nextnonce != null) creds.nonce = nextnonce;
1250 } 1268 }
1251 } 1269 }
1252 request._onIncoming(incoming); 1270 request._onIncoming(incoming);
1253 }) 1271 })
1254 // If we see a state error, we failed to get the 'first' 1272 // If we see a state error, we failed to get the 'first'
1255 // element. 1273 // element.
1256 .catchError((error) { 1274 .catchError((error) {
1257 throw new HttpException( 1275 throw new HttpException(
1258 "Connection closed before data was received"); 1276 "Connection closed before data was received", uri);
1259 }, test: (error) => error is StateError) 1277 }, test: (error) => error is StateError)
1260 .catchError((error) { 1278 .catchError((error) {
1261 // We are done with the socket. 1279 // We are done with the socket.
1262 destroy(); 1280 destroy();
1263 request._onError(error); 1281 request._onError(error);
1264 }); 1282 });
1265 1283
1266 // Resume the parser now we have a handler. 1284 // Resume the parser now we have a handler.
1267 _subscription.resume(); 1285 _subscription.resume();
1268 return s; 1286 return s;
1269 }, onError: (e) { 1287 }, onError: (e) {
1270 destroy(); 1288 destroy();
1271 }); 1289 });
Bill Hesse 2013/07/05 12:57:45 If _currentUri is used to pass the URI to other me
Anders Johnsen 2013/08/20 05:46:31 Done.
1272 return request; 1290 return request;
1273 } 1291 }
1274 1292
1275 Future<Socket> detachSocket() { 1293 Future<Socket> detachSocket() {
1276 return _streamFuture.then( 1294 return _streamFuture.then(
1277 (_) => new _DetachedSocket(_socket, _httpParser.detachIncoming())); 1295 (_) => new _DetachedSocket(_socket, _httpParser.detachIncoming()));
1278 } 1296 }
1279 1297
1280 void destroy() { 1298 void destroy() {
1281 closed = true; 1299 closed = true;
(...skipping 494 matching lines...) Expand 10 before | Expand all | Expand 10 after
1776 (incoming) { 1794 (incoming) {
1777 // If the incoming was closed, close the connection. 1795 // If the incoming was closed, close the connection.
1778 incoming.dataDone.then((closing) { 1796 incoming.dataDone.then((closing) {
1779 if (closing) destroy(); 1797 if (closing) destroy();
1780 }); 1798 });
1781 // Only handle one incoming request at the time. Keep the 1799 // Only handle one incoming request at the time. Keep the
1782 // stream paused until the request has been send. 1800 // stream paused until the request has been send.
1783 _subscription.pause(); 1801 _subscription.pause();
1784 _state = _ACTIVE; 1802 _state = _ACTIVE;
1785 var outgoing = new _HttpOutgoing(_socket); 1803 var outgoing = new _HttpOutgoing(_socket);
1786 var response = new _HttpResponse(incoming.headers.protocolVersion, 1804 var response = new _HttpResponse(incoming.uri,
1805 incoming.headers.protocolVersion,
1787 outgoing, 1806 outgoing,
1788 _httpServer.serverHeader); 1807 _httpServer.serverHeader);
1789 var request = new _HttpRequest(response, incoming, _httpServer, this); 1808 var request = new _HttpRequest(response, incoming, _httpServer, this);
1790 _streamFuture = outgoing.done 1809 _streamFuture = outgoing.done
1791 .then((_) { 1810 .then((_) {
1792 if (_state == _DETACHED) return; 1811 if (_state == _DETACHED) return;
1793 if (response.persistentConnection && 1812 if (response.persistentConnection &&
1794 request.persistentConnection && 1813 request.persistentConnection &&
1795 incoming.fullBodyRead) { 1814 incoming.fullBodyRead) {
1796 _state = _IDLE; 1815 _state = _IDLE;
(...skipping 584 matching lines...) Expand 10 before | Expand all | Expand 10 after
2381 final Uri location; 2400 final Uri location;
2382 } 2401 }
2383 2402
2384 String _getHttpVersion() { 2403 String _getHttpVersion() {
2385 var version = Platform.version; 2404 var version = Platform.version;
2386 // Only include major and minor version numbers. 2405 // Only include major and minor version numbers.
2387 int index = version.indexOf('.', version.indexOf('.') + 1); 2406 int index = version.indexOf('.', version.indexOf('.') + 1);
2388 version = version.substring(0, index); 2407 version = version.substring(0, index);
2389 return 'Dart/$version (dart:io)'; 2408 return 'Dart/$version (dart:io)';
2390 } 2409 }
OLDNEW
« sdk/lib/io/http.dart ('K') | « sdk/lib/io/http.dart ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698