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