OLD | NEW |
---|---|
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 // Global constants. | 7 // Global constants. |
8 class _Const { | 8 class _Const { |
9 // Bytes for "HTTP". | 9 // Bytes for "HTTP". |
10 static const HTTP = const [72, 84, 84, 80]; | 10 static const HTTP = const [72, 84, 84, 80]; |
(...skipping 229 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
240 | 240 |
241 // The data that is currently being parsed. | 241 // The data that is currently being parsed. |
242 Uint8List _buffer; | 242 Uint8List _buffer; |
243 int _index; | 243 int _index; |
244 | 244 |
245 final bool _requestParser; | 245 final bool _requestParser; |
246 int _state; | 246 int _state; |
247 int _httpVersionIndex; | 247 int _httpVersionIndex; |
248 int _messageType; | 248 int _messageType; |
249 int _statusCode = 0; | 249 int _statusCode = 0; |
250 List _method_or_status_code; | 250 final List<int> _method = []; |
251 List _uri_or_reason_phrase; | 251 final List<int> _uri_or_reason_phrase = []; |
252 List _headerField; | 252 final List<int> _headerField = []; |
253 List _headerValue; | 253 final List<int> _headerValue = []; |
254 | 254 |
255 int _httpVersion; | 255 int _httpVersion; |
256 int _transferLength = -1; | 256 int _transferLength = -1; |
257 bool _persistentConnection; | 257 bool _persistentConnection; |
258 bool _connectionUpgrade; | 258 bool _connectionUpgrade; |
259 bool _chunked; | 259 bool _chunked; |
260 | 260 |
261 bool _noMessageBody; | 261 bool _noMessageBody = false; |
262 String _responseToMethod; // Indicates the method used for the request. | |
263 int _remainingContent = -1; | 262 int _remainingContent = -1; |
264 | 263 |
265 _HttpHeaders _headers; | 264 _HttpHeaders _headers; |
266 | 265 |
267 // The current incoming connection. | 266 // The current incoming connection. |
268 _HttpIncoming _incoming; | 267 _HttpIncoming _incoming; |
269 StreamSubscription _socketSubscription; | 268 StreamSubscription _socketSubscription; |
270 bool _paused = true; | 269 bool _paused = true; |
271 bool _bodyPaused = false; | 270 bool _bodyPaused = false; |
272 StreamController<_HttpIncoming> _controller; | 271 StreamController<_HttpIncoming> _controller; |
(...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
367 case _State.START: | 366 case _State.START: |
368 if (byte == _Const.HTTP[0]) { | 367 if (byte == _Const.HTTP[0]) { |
369 // Start parsing method or HTTP version. | 368 // Start parsing method or HTTP version. |
370 _httpVersionIndex = 1; | 369 _httpVersionIndex = 1; |
371 _state = _State.METHOD_OR_RESPONSE_HTTP_VERSION; | 370 _state = _State.METHOD_OR_RESPONSE_HTTP_VERSION; |
372 } else { | 371 } else { |
373 // Start parsing method. | 372 // Start parsing method. |
374 if (!_isTokenChar(byte)) { | 373 if (!_isTokenChar(byte)) { |
375 throw new HttpException("Invalid request method"); | 374 throw new HttpException("Invalid request method"); |
376 } | 375 } |
377 _method_or_status_code.add(byte); | 376 _method.add(byte); |
378 if (!_requestParser) { | 377 if (!_requestParser) { |
379 throw new HttpException("Invalid response line"); | 378 throw new HttpException("Invalid response line"); |
380 } | 379 } |
381 _state = _State.REQUEST_LINE_METHOD; | 380 _state = _State.REQUEST_LINE_METHOD; |
382 } | 381 } |
383 break; | 382 break; |
384 | 383 |
385 case _State.METHOD_OR_RESPONSE_HTTP_VERSION: | 384 case _State.METHOD_OR_RESPONSE_HTTP_VERSION: |
386 if (_httpVersionIndex < _Const.HTTP.length && | 385 if (_httpVersionIndex < _Const.HTTP.length && |
387 byte == _Const.HTTP[_httpVersionIndex]) { | 386 byte == _Const.HTTP[_httpVersionIndex]) { |
388 // Continue parsing HTTP version. | 387 // Continue parsing HTTP version. |
389 _httpVersionIndex++; | 388 _httpVersionIndex++; |
390 } else if (_httpVersionIndex == _Const.HTTP.length && | 389 } else if (_httpVersionIndex == _Const.HTTP.length && |
391 byte == _CharCode.SLASH) { | 390 byte == _CharCode.SLASH) { |
392 // HTTP/ parsed. As method is a token this cannot be a | 391 // HTTP/ parsed. As method is a token this cannot be a |
393 // method anymore. | 392 // method anymore. |
394 _httpVersionIndex++; | 393 _httpVersionIndex++; |
395 if (_requestParser) { | 394 if (_requestParser) { |
396 throw new HttpException("Invalid request line"); | 395 throw new HttpException("Invalid request line"); |
397 } | 396 } |
398 _state = _State.RESPONSE_HTTP_VERSION; | 397 _state = _State.RESPONSE_HTTP_VERSION; |
399 } else { | 398 } else { |
400 // Did not parse HTTP version. Expect method instead. | 399 // Did not parse HTTP version. Expect method instead. |
401 for (int i = 0; i < _httpVersionIndex; i++) { | 400 for (int i = 0; i < _httpVersionIndex; i++) { |
402 _method_or_status_code.add(_Const.HTTP[i]); | 401 _method.add(_Const.HTTP[i]); |
403 } | 402 } |
404 if (byte == _CharCode.SP) { | 403 if (byte == _CharCode.SP) { |
405 _state = _State.REQUEST_LINE_URI; | 404 _state = _State.REQUEST_LINE_URI; |
406 } else { | 405 } else { |
407 _method_or_status_code.add(byte); | 406 _method.add(byte); |
408 _httpVersion = _HttpVersion.UNDETERMINED; | 407 _httpVersion = _HttpVersion.UNDETERMINED; |
409 if (!_requestParser) { | 408 if (!_requestParser) { |
410 throw new HttpException("Invalid response line"); | 409 throw new HttpException("Invalid response line"); |
411 } | 410 } |
412 _state = _State.REQUEST_LINE_METHOD; | 411 _state = _State.REQUEST_LINE_METHOD; |
413 } | 412 } |
414 } | 413 } |
415 break; | 414 break; |
416 | 415 |
417 case _State.RESPONSE_HTTP_VERSION: | 416 case _State.RESPONSE_HTTP_VERSION: |
(...skipping 24 matching lines...) Expand all Loading... | |
442 | 441 |
443 case _State.REQUEST_LINE_METHOD: | 442 case _State.REQUEST_LINE_METHOD: |
444 if (byte == _CharCode.SP) { | 443 if (byte == _CharCode.SP) { |
445 _state = _State.REQUEST_LINE_URI; | 444 _state = _State.REQUEST_LINE_URI; |
446 } else { | 445 } else { |
447 if (_Const.SEPARATOR_MAP[byte] || | 446 if (_Const.SEPARATOR_MAP[byte] || |
448 byte == _CharCode.CR || | 447 byte == _CharCode.CR || |
449 byte == _CharCode.LF) { | 448 byte == _CharCode.LF) { |
450 throw new HttpException("Invalid request method"); | 449 throw new HttpException("Invalid request method"); |
451 } | 450 } |
452 _method_or_status_code.add(byte); | 451 _method.add(byte); |
453 } | 452 } |
454 break; | 453 break; |
455 | 454 |
456 case _State.REQUEST_LINE_URI: | 455 case _State.REQUEST_LINE_URI: |
457 if (byte == _CharCode.SP) { | 456 if (byte == _CharCode.SP) { |
458 if (_uri_or_reason_phrase.length == 0) { | 457 if (_uri_or_reason_phrase.length == 0) { |
459 throw new HttpException("Invalid request URI"); | 458 throw new HttpException("Invalid request URI"); |
460 } | 459 } |
461 _state = _State.REQUEST_LINE_HTTP_VERSION; | 460 _state = _State.REQUEST_LINE_HTTP_VERSION; |
462 _httpVersionIndex = 0; | 461 _httpVersionIndex = 0; |
(...skipping 30 matching lines...) Expand all Loading... | |
493 break; | 492 break; |
494 | 493 |
495 case _State.REQUEST_LINE_ENDING: | 494 case _State.REQUEST_LINE_ENDING: |
496 _expect(byte, _CharCode.LF); | 495 _expect(byte, _CharCode.LF); |
497 _messageType = _MessageType.REQUEST; | 496 _messageType = _MessageType.REQUEST; |
498 _state = _State.HEADER_START; | 497 _state = _State.HEADER_START; |
499 break; | 498 break; |
500 | 499 |
501 case _State.RESPONSE_LINE_STATUS_CODE: | 500 case _State.RESPONSE_LINE_STATUS_CODE: |
502 if (byte == _CharCode.SP) { | 501 if (byte == _CharCode.SP) { |
503 if (_method_or_status_code.length != 3) { | |
504 throw new HttpException("Invalid response status code"); | |
505 } | |
506 _state = _State.RESPONSE_LINE_REASON_PHRASE; | 502 _state = _State.RESPONSE_LINE_REASON_PHRASE; |
507 } else if (byte == _CharCode.CR) { | 503 } else if (byte == _CharCode.CR) { |
508 // Some HTTP servers does not follow the spec. and send | 504 // Some HTTP servers does not follow the spec. and send |
509 // \r\n right after the status code. | 505 // \r\n right after the status code. |
510 _state = _State.RESPONSE_LINE_ENDING; | 506 _state = _State.RESPONSE_LINE_ENDING; |
511 } else { | 507 } else { |
512 if (byte < 0x30 && 0x39 < byte) { | 508 if (byte < 0x30 && 0x39 < byte) { |
513 throw new HttpException("Invalid response status code"); | 509 throw new HttpException("Invalid response status code"); |
514 } else { | 510 } else { |
515 _method_or_status_code.add(byte); | 511 _statusCode = _statusCode * 10 + byte - 0x30; |
Søren Gjesse
2014/02/20 14:12:18
As discussed offline, we should keep track of the
Anders Johnsen
2014/02/20 14:46:53
Done.
| |
516 } | 512 } |
517 } | 513 } |
518 break; | 514 break; |
519 | 515 |
520 case _State.RESPONSE_LINE_REASON_PHRASE: | 516 case _State.RESPONSE_LINE_REASON_PHRASE: |
521 if (byte == _CharCode.CR) { | 517 if (byte == _CharCode.CR) { |
522 _state = _State.RESPONSE_LINE_ENDING; | 518 _state = _State.RESPONSE_LINE_ENDING; |
523 } else { | 519 } else { |
524 if (byte == _CharCode.CR || byte == _CharCode.LF) { | 520 if (byte == _CharCode.CR || byte == _CharCode.LF) { |
525 throw new HttpException("Invalid response reason phrase"); | 521 throw new HttpException("Invalid response reason phrase"); |
526 } | 522 } |
527 _uri_or_reason_phrase.add(byte); | 523 _uri_or_reason_phrase.add(byte); |
528 } | 524 } |
529 break; | 525 break; |
530 | 526 |
531 case _State.RESPONSE_LINE_ENDING: | 527 case _State.RESPONSE_LINE_ENDING: |
532 _expect(byte, _CharCode.LF); | 528 _expect(byte, _CharCode.LF); |
533 _messageType == _MessageType.RESPONSE; | 529 _messageType == _MessageType.RESPONSE; |
534 _statusCode = int.parse( | |
535 new String.fromCharCodes(_method_or_status_code)); | |
536 if (_statusCode < 100 || _statusCode > 599) { | 530 if (_statusCode < 100 || _statusCode > 599) { |
537 throw new HttpException("Invalid response status code"); | 531 throw new HttpException("Invalid response status code"); |
538 } else { | 532 } else { |
539 // Check whether this response will never have a body. | 533 // Check whether this response will never have a body. |
540 _noMessageBody = _statusCode <= 199 || _statusCode == 204 || | 534 if (_statusCode <= 199 || _statusCode == 204 || |
541 _statusCode == 304; | 535 _statusCode == 304) { |
536 _noMessageBody = true; | |
537 } | |
542 } | 538 } |
543 _state = _State.HEADER_START; | 539 _state = _State.HEADER_START; |
544 break; | 540 break; |
545 | 541 |
546 case _State.HEADER_START: | 542 case _State.HEADER_START: |
547 _headers = new _HttpHeaders(version); | 543 _headers = new _HttpHeaders(version); |
548 if (byte == _CharCode.CR) { | 544 if (byte == _CharCode.CR) { |
549 _state = _State.HEADER_ENDING; | 545 _state = _State.HEADER_ENDING; |
550 } else { | 546 } else { |
551 // Start of new header field. | 547 // Start of new header field. |
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
588 _state = _State.HEADER_VALUE_FOLD_OR_END; | 584 _state = _State.HEADER_VALUE_FOLD_OR_END; |
589 break; | 585 break; |
590 | 586 |
591 case _State.HEADER_VALUE_FOLD_OR_END: | 587 case _State.HEADER_VALUE_FOLD_OR_END: |
592 if (byte == _CharCode.SP || byte == _CharCode.HT) { | 588 if (byte == _CharCode.SP || byte == _CharCode.HT) { |
593 _state = _State.HEADER_VALUE_START; | 589 _state = _State.HEADER_VALUE_START; |
594 } else { | 590 } else { |
595 String headerField = new String.fromCharCodes(_headerField); | 591 String headerField = new String.fromCharCodes(_headerField); |
596 String headerValue = new String.fromCharCodes(_headerValue); | 592 String headerValue = new String.fromCharCodes(_headerValue); |
597 if (headerField == "transfer-encoding" && | 593 if (headerField == "transfer-encoding" && |
598 headerValue.toLowerCase() == "chunked") { | 594 _caseInsensitiveCompare("chunked".codeUnits, _headerValue)) { |
599 _chunked = true; | 595 _chunked = true; |
600 } | 596 } |
601 if (headerField == "connection") { | 597 if (headerField == "connection") { |
602 List<String> tokens = _tokenizeFieldValue(headerValue); | 598 List<String> tokens = _tokenizeFieldValue(headerValue); |
603 for (int i = 0; i < tokens.length; i++) { | 599 for (int i = 0; i < tokens.length; i++) { |
604 if (tokens[i].toLowerCase() == "upgrade") { | 600 if (_caseInsensitiveCompare("upgrade".codeUnits, |
601 tokens[i].codeUnits)) { | |
605 _connectionUpgrade = true; | 602 _connectionUpgrade = true; |
606 } | 603 } |
607 _headers._add(headerField, tokens[i]); | 604 _headers._add(headerField, tokens[i]); |
608 } | 605 } |
609 } else { | 606 } else { |
610 _headers._add(headerField, headerValue); | 607 _headers._add(headerField, headerValue); |
611 } | 608 } |
612 _headerField.clear(); | 609 _headerField.clear(); |
613 _headerValue.clear(); | 610 _headerValue.clear(); |
614 | 611 |
(...skipping 24 matching lines...) Expand all Loading... | |
639 _chunked == false) { | 636 _chunked == false) { |
640 _transferLength = 0; | 637 _transferLength = 0; |
641 } | 638 } |
642 if (_connectionUpgrade) { | 639 if (_connectionUpgrade) { |
643 _state = _State.UPGRADED; | 640 _state = _State.UPGRADED; |
644 _transferLength = 0; | 641 _transferLength = 0; |
645 } | 642 } |
646 _createIncoming(_transferLength); | 643 _createIncoming(_transferLength); |
647 if (_requestParser) { | 644 if (_requestParser) { |
648 _incoming.method = | 645 _incoming.method = |
649 new String.fromCharCodes(_method_or_status_code); | 646 new String.fromCharCodes(_method); |
650 _incoming.uri = | 647 _incoming.uri = |
651 Uri.parse( | 648 Uri.parse( |
652 new String.fromCharCodes(_uri_or_reason_phrase)); | 649 new String.fromCharCodes(_uri_or_reason_phrase)); |
653 } else { | 650 } else { |
654 _incoming.statusCode = _statusCode; | 651 _incoming.statusCode = _statusCode; |
655 _incoming.reasonPhrase = | 652 _incoming.reasonPhrase = |
656 new String.fromCharCodes(_uri_or_reason_phrase); | 653 new String.fromCharCodes(_uri_or_reason_phrase); |
657 } | 654 } |
658 _method_or_status_code.clear(); | 655 _method.clear(); |
659 _uri_or_reason_phrase.clear(); | 656 _uri_or_reason_phrase.clear(); |
660 if (_connectionUpgrade) { | 657 if (_connectionUpgrade) { |
661 _incoming.upgraded = true; | 658 _incoming.upgraded = true; |
662 _parserCalled = false; | 659 _parserCalled = false; |
663 var tmp = _incoming; | 660 var tmp = _incoming; |
664 _closeIncoming(); | 661 _closeIncoming(); |
665 _controller.add(tmp); | 662 _controller.add(tmp); |
666 return; | 663 return; |
667 } | 664 } |
668 if (_transferLength == 0 || | 665 if (_transferLength == 0 || |
669 (_messageType == _MessageType.RESPONSE && | 666 (_messageType == _MessageType.RESPONSE && _noMessageBody)) { |
670 (_noMessageBody || _responseToMethod == "HEAD"))) { | |
671 _reset(); | 667 _reset(); |
672 var tmp = _incoming; | 668 var tmp = _incoming; |
673 _closeIncoming(); | 669 _closeIncoming(); |
674 _controller.add(tmp); | 670 _controller.add(tmp); |
675 break; | 671 break; |
676 } else if (_chunked) { | 672 } else if (_chunked) { |
677 _state = _State.CHUNK_SIZE; | 673 _state = _State.CHUNK_SIZE; |
678 _remainingContent = 0; | 674 _remainingContent = 0; |
679 } else if (_transferLength > 0) { | 675 } else if (_transferLength > 0) { |
680 _remainingContent = _transferLength; | 676 _remainingContent = _transferLength; |
(...skipping 178 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
859 return "1.1"; | 855 return "1.1"; |
860 } | 856 } |
861 return null; | 857 return null; |
862 } | 858 } |
863 | 859 |
864 int get messageType => _messageType; | 860 int get messageType => _messageType; |
865 int get transferLength => _transferLength; | 861 int get transferLength => _transferLength; |
866 bool get upgrade => _connectionUpgrade && _state == _State.UPGRADED; | 862 bool get upgrade => _connectionUpgrade && _state == _State.UPGRADED; |
867 bool get persistentConnection => _persistentConnection; | 863 bool get persistentConnection => _persistentConnection; |
868 | 864 |
869 void set responseToMethod(String method) { _responseToMethod = method; } | 865 void set isHead(bool value) { |
866 if (value) _noMessageBody = true; | |
867 } | |
870 | 868 |
871 _HttpDetachedIncoming detachIncoming() { | 869 _HttpDetachedIncoming detachIncoming() { |
872 // Simulate detached by marking as upgraded. | 870 // Simulate detached by marking as upgraded. |
873 _state = _State.UPGRADED; | 871 _state = _State.UPGRADED; |
874 return new _HttpDetachedIncoming(_socketSubscription, | 872 return new _HttpDetachedIncoming(_socketSubscription, |
875 readUnparsedData()); | 873 readUnparsedData()); |
876 } | 874 } |
877 | 875 |
878 List<int> readUnparsedData() { | 876 List<int> readUnparsedData() { |
879 if (_buffer == null) return null; | 877 if (_buffer == null) return null; |
880 if (_index == _buffer.length) return null; | 878 if (_index == _buffer.length) return null; |
881 var result = _buffer.sublist(_index); | 879 var result = _buffer.sublist(_index); |
882 _releaseBuffer(); | 880 _releaseBuffer(); |
883 return result; | 881 return result; |
884 } | 882 } |
885 | 883 |
886 void _reset() { | 884 void _reset() { |
887 if (_state == _State.UPGRADED) return; | 885 if (_state == _State.UPGRADED) return; |
888 _state = _State.START; | 886 _state = _State.START; |
889 _messageType = _MessageType.UNDETERMINED; | 887 _messageType = _MessageType.UNDETERMINED; |
890 _headerField = new List(); | 888 _headerField.clear(); |
891 _headerValue = new List(); | 889 _headerValue.clear(); |
892 _method_or_status_code = new List(); | 890 _method.clear(); |
893 _uri_or_reason_phrase = new List(); | 891 _uri_or_reason_phrase.clear(); |
894 | 892 |
895 _statusCode = 0; | 893 _statusCode = 0; |
896 | 894 |
897 _httpVersion = _HttpVersion.UNDETERMINED; | 895 _httpVersion = _HttpVersion.UNDETERMINED; |
898 _transferLength = -1; | 896 _transferLength = -1; |
899 _persistentConnection = false; | 897 _persistentConnection = false; |
900 _connectionUpgrade = false; | 898 _connectionUpgrade = false; |
901 _chunked = false; | 899 _chunked = false; |
902 | 900 |
903 _noMessageBody = false; | 901 _noMessageBody = false; |
904 _responseToMethod = null; | |
905 _remainingContent = -1; | 902 _remainingContent = -1; |
906 | 903 |
907 _headers = null; | 904 _headers = null; |
908 } | 905 } |
909 | 906 |
910 void _releaseBuffer() { | 907 void _releaseBuffer() { |
911 _buffer = null; | 908 _buffer = null; |
912 _index = null; | 909 _index = null; |
913 } | 910 } |
914 | 911 |
(...skipping 18 matching lines...) Expand all Loading... | |
933 return tokens; | 930 return tokens; |
934 } | 931 } |
935 | 932 |
936 int _toLowerCase(int byte) { | 933 int _toLowerCase(int byte) { |
937 final int aCode = "A".codeUnitAt(0); | 934 final int aCode = "A".codeUnitAt(0); |
938 final int zCode = "Z".codeUnitAt(0); | 935 final int zCode = "Z".codeUnitAt(0); |
939 final int delta = "a".codeUnitAt(0) - aCode; | 936 final int delta = "a".codeUnitAt(0) - aCode; |
940 return (aCode <= byte && byte <= zCode) ? byte + delta : byte; | 937 return (aCode <= byte && byte <= zCode) ? byte + delta : byte; |
941 } | 938 } |
942 | 939 |
940 // expected should already be lowercase. | |
941 bool _caseInsensitiveCompare(List<int> expected, List<int> value) { | |
942 if (expected.length != value.length) return false; | |
943 for (int i = 0; i < expected.length; i++) { | |
944 if (expected[i] != _toLowerCase(value[i])) return false; | |
945 } | |
946 return true; | |
947 } | |
948 | |
943 int _expect(int val1, int val2) { | 949 int _expect(int val1, int val2) { |
944 if (val1 != val2) { | 950 if (val1 != val2) { |
945 throw new HttpException("Failed to parse HTTP"); | 951 throw new HttpException("Failed to parse HTTP"); |
946 } | 952 } |
947 } | 953 } |
948 | 954 |
949 int _expectHexDigit(int byte) { | 955 int _expectHexDigit(int byte) { |
950 if (0x30 <= byte && byte <= 0x39) { | 956 if (0x30 <= byte && byte <= 0x39) { |
951 return byte - 0x30; // 0 - 9 | 957 return byte - 0x30; // 0 - 9 |
952 } else if (0x41 <= byte && byte <= 0x46) { | 958 } else if (0x41 <= byte && byte <= 0x46) { |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
1023 } | 1029 } |
1024 } | 1030 } |
1025 | 1031 |
1026 void _reportError(error, [stackTrace]) { | 1032 void _reportError(error, [stackTrace]) { |
1027 if (_socketSubscription != null) _socketSubscription.cancel(); | 1033 if (_socketSubscription != null) _socketSubscription.cancel(); |
1028 _state = _State.FAILURE; | 1034 _state = _State.FAILURE; |
1029 _controller.addError(error, stackTrace); | 1035 _controller.addError(error, stackTrace); |
1030 _controller.close(); | 1036 _controller.close(); |
1031 } | 1037 } |
1032 } | 1038 } |
OLD | NEW |