Chromium Code Reviews| 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 /** | 7 /** |
| 8 * A high-level class for communicating securely over a TCP socket, using | 8 * A high-level class for communicating securely over a TCP socket, using |
| 9 * TLS and SSL. The [SecureSocket] exposes both a [Stream] and an | 9 * TLS and SSL. The [SecureSocket] exposes both a [Stream] and an |
| 10 * [IOSink] interface, making it ideal for using together with | 10 * [IOSink] interface, making it ideal for using together with |
| (...skipping 419 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 430 String certificateName, | 430 String certificateName, |
| 431 {bool is_server, | 431 {bool is_server, |
| 432 RawSocket socket, | 432 RawSocket socket, |
| 433 StreamSubscription subscription, | 433 StreamSubscription subscription, |
| 434 List<int> bufferedData, | 434 List<int> bufferedData, |
| 435 bool requestClientCertificate: false, | 435 bool requestClientCertificate: false, |
| 436 bool requireClientCertificate: false, | 436 bool requireClientCertificate: false, |
| 437 bool sendClientCertificate: false, | 437 bool sendClientCertificate: false, |
| 438 bool onBadCertificate(X509Certificate certificate)}) { | 438 bool onBadCertificate(X509Certificate certificate)}) { |
| 439 var future; | 439 var future; |
| 440 _verifyFields(host, requestedPort, certificateName, is_server, | |
| 441 requestClientCertificate, requireClientCertificate, | |
| 442 sendClientCertificate, onBadCertificate); | |
| 440 if (host is String) { | 443 if (host is String) { |
| 441 if (socket != null) { | 444 if (socket != null) { |
| 442 future = new Future.value( | 445 future = new Future.value( |
| 443 (socket.address as dynamic)._cloneWithNewHost(host)); | 446 (socket.address as dynamic)._cloneWithNewHost(host)); |
| 444 } else { | 447 } else { |
| 445 future = InternetAddress.lookup(host).then((addrs) => addrs.first); | 448 future = InternetAddress.lookup(host).then((addrs) => addrs.first); |
| 446 } | 449 } |
| 447 } else { | 450 } else { |
| 448 future = new Future.value(host); | 451 future = new Future.value(host); |
| 449 } | 452 } |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 477 bool this.onBadCertificate(X509Certificate certificate)) { | 480 bool this.onBadCertificate(X509Certificate certificate)) { |
| 478 _controller = new StreamController<RawSocketEvent>( | 481 _controller = new StreamController<RawSocketEvent>( |
| 479 sync: true, | 482 sync: true, |
| 480 onListen: _onSubscriptionStateChange, | 483 onListen: _onSubscriptionStateChange, |
| 481 onPause: _onPauseStateChange, | 484 onPause: _onPauseStateChange, |
| 482 onResume: _onPauseStateChange, | 485 onResume: _onPauseStateChange, |
| 483 onCancel: _onSubscriptionStateChange); | 486 onCancel: _onSubscriptionStateChange); |
| 484 _stream = _controller.stream; | 487 _stream = _controller.stream; |
| 485 // Throw an ArgumentError if any field is invalid. After this, all | 488 // Throw an ArgumentError if any field is invalid. After this, all |
| 486 // errors will be reported through the future or the stream. | 489 // errors will be reported through the future or the stream. |
| 487 _verifyFields(); | |
| 488 _secureFilter.init(); | 490 _secureFilter.init(); |
| 489 _filterPointer = _secureFilter._pointer(); | 491 _filterPointer = _secureFilter._pointer(); |
| 490 _secureFilter.registerHandshakeCompleteCallback( | 492 _secureFilter.registerHandshakeCompleteCallback( |
| 491 _secureHandshakeCompleteHandler); | 493 _secureHandshakeCompleteHandler); |
| 492 if (onBadCertificate != null) { | 494 if (onBadCertificate != null) { |
| 493 _secureFilter.registerBadCertificateCallback(onBadCertificate); | 495 _secureFilter.registerBadCertificateCallback(onBadCertificate); |
| 494 } | 496 } |
| 495 var futureSocket; | 497 var futureSocket; |
| 496 if (socket == null) { | 498 if (socket == null) { |
| 497 futureSocket = RawSocket.connect(address, requestedPort); | 499 futureSocket = RawSocket.connect(address, requestedPort); |
| 498 } else { | 500 } else { |
| 499 futureSocket = new Future.value(socket); | 501 futureSocket = new Future.value(socket); |
| 500 } | 502 } |
| 501 futureSocket.then((rawSocket) { | 503 futureSocket.then((rawSocket) { |
| 504 _connectPending = true; | |
| 502 _socket = rawSocket; | 505 _socket = rawSocket; |
| 503 _socket.readEventsEnabled = true; | 506 _socket.readEventsEnabled = true; |
| 504 _socket.writeEventsEnabled = false; | 507 _socket.writeEventsEnabled = false; |
| 505 if (_socketSubscription == null) { | 508 if (_socketSubscription == null) { |
| 506 // If a current subscription is provided use this otherwise | 509 // If a current subscription is provided use this otherwise |
| 507 // create a new one. | 510 // create a new one. |
| 508 _socketSubscription = _socket.listen(_eventDispatcher, | 511 _socketSubscription = _socket.listen(_eventDispatcher, |
| 509 onError: _errorHandler, | 512 onError: _reportError, |
| 510 onDone: _doneHandler); | 513 onDone: _doneHandler); |
| 511 } else { | 514 } else { |
| 512 _socketSubscription.onData(_eventDispatcher); | 515 _socketSubscription.onData(_eventDispatcher); |
| 513 _socketSubscription.onError(_errorHandler); | 516 _socketSubscription.onError(_reportError); |
| 514 _socketSubscription.onDone(_doneHandler); | 517 _socketSubscription.onDone(_doneHandler); |
| 515 } | 518 } |
| 516 _connectPending = true; | |
| 517 _secureFilter.connect(address.host, | 519 _secureFilter.connect(address.host, |
| 518 (address as dynamic)._sockaddr_storage, | 520 (address as dynamic)._sockaddr_storage, |
| 519 port, | 521 port, |
| 520 is_server, | 522 is_server, |
| 521 certificateName, | 523 certificateName, |
| 522 requestClientCertificate || | 524 requestClientCertificate || |
| 523 requireClientCertificate, | 525 requireClientCertificate, |
| 524 requireClientCertificate, | 526 requireClientCertificate, |
| 525 sendClientCertificate); | 527 sendClientCertificate); |
| 526 _secureHandshake(); | 528 _secureHandshake(); |
| 527 }) | 529 }) |
| 528 .catchError((error) { | 530 .catchError(_reportError); |
| 529 _handshakeComplete.completeError(error); | |
| 530 _close(); | |
| 531 }); | |
| 532 } | 531 } |
| 533 | 532 |
| 534 StreamSubscription listen(void onData(RawSocketEvent data), | 533 StreamSubscription listen(void onData(RawSocketEvent data), |
| 535 {void onError(error), | 534 {void onError(error), |
| 536 void onDone(), | 535 void onDone(), |
| 537 bool cancelOnError}) { | 536 bool cancelOnError}) { |
| 538 _sendWriteEvent(); | 537 _sendWriteEvent(); |
| 539 return _stream.listen(onData, | 538 return _stream.listen(onData, |
| 540 onError: onError, | 539 onError: onError, |
| 541 onDone: onDone, | 540 onDone: onDone, |
| 542 cancelOnError: cancelOnError); | 541 cancelOnError: cancelOnError); |
| 543 } | 542 } |
| 544 | 543 |
| 545 void _verifyFields() { | 544 static void _verifyFields(host, |
|
Søren Gjesse
2013/06/25 06:35:49
Indentation.
Bill Hesse
2013/06/25 12:41:14
Done.
| |
| 546 assert(is_server is bool); | 545 int requestedPort, |
| 547 assert(_socket == null || _socket is RawSocket); | 546 String certificateName, |
| 548 if (address is! InternetAddress) { | 547 bool is_server, |
| 549 throw new ArgumentError( | 548 bool requestClientCertificate, |
| 550 "RawSecureSocket constructor: host is not an InternetAddress"); | 549 bool requireClientCertificate, |
| 550 bool sendClientCertificate, | |
| 551 Function onBadCertificate) { | |
| 552 if (host is! String && host is! InternetAddress) { | |
| 553 throw new ArgumentError("host is not a String or an InternetAddress"); | |
| 554 } | |
| 555 if (requestedPort is! int) { | |
| 556 throw new ArgumentError("requestedPort is not an int"); | |
| 557 } | |
| 558 if (requestedPort < 0 || requestedPort > 65535) { | |
| 559 throw new ArgumentError("requestedPort is not in the range 0..65535"); | |
| 551 } | 560 } |
| 552 if (certificateName != null && certificateName is! String) { | 561 if (certificateName != null && certificateName is! String) { |
| 553 throw new ArgumentError("certificateName is not null or a String"); | 562 throw new ArgumentError("certificateName is not null or a String"); |
| 554 } | 563 } |
| 555 if (certificateName == null && is_server) { | 564 if (certificateName == null && is_server) { |
| 556 throw new ArgumentError("certificateName is null on a server"); | 565 throw new ArgumentError("certificateName is null on a server"); |
| 557 } | 566 } |
| 558 if (requestClientCertificate is! bool) { | 567 if (requestClientCertificate is! bool) { |
| 559 throw new ArgumentError("requestClientCertificate is not a bool"); | 568 throw new ArgumentError("requestClientCertificate is not a bool"); |
| 560 } | 569 } |
| (...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 686 } | 695 } |
| 687 | 696 |
| 688 X509Certificate get peerCertificate => _secureFilter.peerCertificate; | 697 X509Certificate get peerCertificate => _secureFilter.peerCertificate; |
| 689 | 698 |
| 690 bool setOption(SocketOption option, bool enabled) { | 699 bool setOption(SocketOption option, bool enabled) { |
| 691 if (_socket == null) return false; | 700 if (_socket == null) return false; |
| 692 return _socket.setOption(option, enabled); | 701 return _socket.setOption(option, enabled); |
| 693 } | 702 } |
| 694 | 703 |
| 695 void _eventDispatcher(RawSocketEvent event) { | 704 void _eventDispatcher(RawSocketEvent event) { |
| 696 if (event == RawSocketEvent.READ) { | 705 try { |
| 697 _readHandler(); | 706 if (event == RawSocketEvent.READ) { |
| 698 } else if (event == RawSocketEvent.WRITE) { | 707 _readHandler(); |
| 699 _writeHandler(); | 708 } else if (event == RawSocketEvent.WRITE) { |
| 700 } else if (event == RawSocketEvent.READ_CLOSED) { | 709 _writeHandler(); |
| 701 _closeHandler(); | 710 } else if (event == RawSocketEvent.READ_CLOSED) { |
| 711 _closeHandler(); | |
| 712 } | |
| 713 } catch (e) { | |
| 714 _reportError(e); | |
| 702 } | 715 } |
| 703 } | 716 } |
| 704 | 717 |
| 705 void _readHandler() { | 718 void _readHandler() { |
| 706 _readSocket(); | 719 _readSocket(); |
| 707 _scheduleFilter(); | 720 _scheduleFilter(); |
| 708 } | 721 } |
| 709 | 722 |
| 710 void _writeHandler() { | 723 void _writeHandler() { |
| 711 _writeSocket(); | 724 _writeSocket(); |
| 712 _scheduleFilter(); | 725 _scheduleFilter(); |
| 713 } | 726 } |
| 714 | 727 |
| 715 void _doneHandler() { | 728 void _doneHandler() { |
| 716 if (_filterStatus.readEmpty) { | 729 if (_filterStatus.readEmpty) { |
| 717 _close(); | 730 _close(); |
| 718 } | 731 } |
| 719 } | 732 } |
| 720 | 733 |
| 721 void _errorHandler(e) { | 734 void _reportError(e) { |
| 722 _reportError(e, 'Error on underlying RawSocket'); | |
| 723 } | |
| 724 | |
| 725 void _reportError(e, String message) { | |
| 726 // TODO(whesse): Call _reportError from all internal functions that throw. | |
| 727 if (e is SocketException) { | |
| 728 e = new SocketException('$message (${e.message})', e.osError); | |
| 729 } else if (e is OSError) { | |
| 730 e = new SocketException(message, e); | |
| 731 } else { | |
| 732 e = new SocketException('$message (${e.toString()})', null); | |
| 733 } | |
| 734 if (_connectPending) { | 735 if (_connectPending) { |
| 736 // _connectPending is true after the underlying connection has been | |
| 737 // made, but before the handshake has completed. | |
| 738 if (e is! TlsException) { | |
| 739 e = new HandshakeException("$e", null); | |
| 740 } | |
| 735 _handshakeComplete.completeError(e); | 741 _handshakeComplete.completeError(e); |
| 736 } else { | 742 } else { |
| 737 _controller.addError(e); | 743 _controller.addError(e); |
| 738 } | 744 } |
| 739 _close(); | 745 _close(); |
| 740 } | 746 } |
| 741 | 747 |
| 742 void _closeHandler() { | 748 void _closeHandler() { |
| 743 if (_status == CONNECTED) { | 749 if (_status == CONNECTED) { |
| 744 if (_closedRead) return; | 750 if (_closedRead) return; |
| 745 _socketClosedRead = true; | 751 _socketClosedRead = true; |
| 746 if (_filterStatus.readEmpty) { | 752 if (_filterStatus.readEmpty) { |
| 747 _closedRead = true; | 753 _closedRead = true; |
| 748 _controller.add(RawSocketEvent.READ_CLOSED); | 754 _controller.add(RawSocketEvent.READ_CLOSED); |
| 749 if (_socketClosedWrite) { | 755 if (_socketClosedWrite) { |
| 750 _close(); | 756 _close(); |
| 751 } | 757 } |
| 752 } else { | 758 } else { |
| 753 _scheduleFilter(); | 759 _scheduleFilter(); |
| 754 } | 760 } |
| 755 } else if (_status == HANDSHAKE) { | 761 } else if (_status == HANDSHAKE) { |
| 756 _socketClosedRead = true; | 762 _socketClosedRead = true; |
| 757 if (_filterStatus.readEmpty) { | 763 if (_filterStatus.readEmpty) { |
| 758 _reportError( | 764 _reportError( |
| 759 new SocketException('Connection terminated during handshake'), | 765 new HandshakeException('Connection terminated during handshake')); |
| 760 'RawSecureSocket error'); | |
| 761 } else { | 766 } else { |
| 762 _secureHandshake(); | 767 _secureHandshake(); |
| 763 } | 768 } |
| 764 } | 769 } |
| 765 } | 770 } |
| 766 | 771 |
| 767 void _secureHandshake() { | 772 void _secureHandshake() { |
| 768 try { | 773 try { |
| 769 _secureFilter.handshake(); | 774 _secureFilter.handshake(); |
| 770 _filterStatus.writeEmpty = false; | 775 _filterStatus.writeEmpty = false; |
| 771 _readSocket(); | 776 _readSocket(); |
| 772 _writeSocket(); | 777 _writeSocket(); |
| 773 _scheduleFilter(); | 778 _scheduleFilter(); |
| 774 } catch (e) { | 779 } catch (e) { |
| 775 _reportError(e, "RawSecureSocket error"); | 780 _reportError(e); |
| 776 } | 781 } |
| 777 } | 782 } |
| 778 | 783 |
| 779 void _secureHandshakeCompleteHandler() { | 784 void _secureHandshakeCompleteHandler() { |
| 780 _status = CONNECTED; | 785 _status = CONNECTED; |
| 781 if (_connectPending) { | 786 if (_connectPending) { |
| 782 _connectPending = false; | 787 _connectPending = false; |
| 783 // We don't want user code to run synchronously in this callback. | 788 // We don't want user code to run synchronously in this callback. |
| 784 Timer.run(() => _handshakeComplete.complete(this)); | 789 Timer.run(() => _handshakeComplete.complete(this)); |
| 785 } | 790 } |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 831 } | 836 } |
| 832 if (_filterStatus.writeEmpty && _closedWrite && !_socketClosedWrite) { | 837 if (_filterStatus.writeEmpty && _closedWrite && !_socketClosedWrite) { |
| 833 // Checks for and handles all cases of partially closed sockets. | 838 // Checks for and handles all cases of partially closed sockets. |
| 834 shutdown(SocketDirection.SEND); | 839 shutdown(SocketDirection.SEND); |
| 835 if (_status == CLOSED) return; | 840 if (_status == CLOSED) return; |
| 836 } | 841 } |
| 837 if (_filterStatus.readEmpty && _socketClosedRead && !_closedRead) { | 842 if (_filterStatus.readEmpty && _socketClosedRead && !_closedRead) { |
| 838 if (_status == HANDSHAKE) { | 843 if (_status == HANDSHAKE) { |
| 839 _secureFilter.handshake(); | 844 _secureFilter.handshake(); |
| 840 if (_status == HANDSHAKE) { | 845 if (_status == HANDSHAKE) { |
| 841 _reportError( | 846 throw new HandshakeException( |
| 842 new SocketException('Connection terminated during handshake'), | 847 'Connection terminated during handshake'); |
| 843 'RawSecureSocket error'); | |
| 844 } | 848 } |
| 845 } | 849 } |
| 846 _closeHandler(); | 850 _closeHandler(); |
| 847 } | 851 } |
| 848 if (_status == CLOSED) return; | 852 if (_status == CLOSED) return; |
| 849 if (_filterStatus.progress) { | 853 if (_filterStatus.progress) { |
| 850 _filterPending = true; | 854 _filterPending = true; |
| 851 if (_filterStatus.writePlaintextNoLongerFull) _sendWriteEvent(); | 855 if (_filterStatus.writePlaintextNoLongerFull) _sendWriteEvent(); |
| 852 if (_filterStatus.readEncryptedNoLongerFull) _readSocket(); | 856 if (_filterStatus.readEncryptedNoLongerFull) _readSocket(); |
| 853 if (_filterStatus.writeEncryptedNoLongerEmpty) _writeSocket(); | 857 if (_filterStatus.writeEncryptedNoLongerEmpty) _writeSocket(); |
| 854 if (_filterStatus.readPlaintextNoLongerEmpty) _scheduleReadEvent(); | 858 if (_filterStatus.readPlaintextNoLongerEmpty) _scheduleReadEvent(); |
| 855 if (_status == HANDSHAKE) _secureHandshake(); | 859 if (_status == HANDSHAKE) _secureHandshake(); |
| 856 } | 860 } |
| 857 _tryFilter(); | 861 _tryFilter(); |
| 858 }); | 862 }).catchError(_reportError); |
| 859 } | 863 } |
| 860 } | 864 } |
| 861 | 865 |
| 862 List<int> _readSocketOrBufferedData(int bytes) { | 866 List<int> _readSocketOrBufferedData(int bytes) { |
| 863 if (_bufferedData != null) { | 867 if (_bufferedData != null) { |
| 864 if (bytes > _bufferedData.length - _bufferedDataIndex) { | 868 if (bytes > _bufferedData.length - _bufferedDataIndex) { |
| 865 bytes = _bufferedData.length - _bufferedDataIndex; | 869 bytes = _bufferedData.length - _bufferedDataIndex; |
| 866 } | 870 } |
| 867 var result = _bufferedData.sublist(_bufferedDataIndex, | 871 var result = _bufferedData.sublist(_bufferedDataIndex, |
| 868 _bufferedDataIndex + bytes); | 872 _bufferedDataIndex + bytes); |
| 869 _bufferedDataIndex += bytes; | 873 _bufferedDataIndex += bytes; |
| 870 if (_bufferedData.length == _bufferedDataIndex) { | 874 if (_bufferedData.length == _bufferedDataIndex) { |
| 871 _bufferedData = null; | 875 _bufferedData = null; |
| 872 } | 876 } |
| 873 return result; | 877 return result; |
| 874 } else if (!_socketClosedRead) { | 878 } else if (!_socketClosedRead) { |
| 875 try { | 879 return _socket.read(bytes); |
| 876 return _socket.read(bytes); | |
| 877 } catch (e) { | |
| 878 _reportError(e, "RawSecureSocket error reading encrypted socket"); | |
| 879 return null; | |
| 880 } | |
| 881 } else { | 880 } else { |
| 882 return null; | 881 return null; |
| 883 } | 882 } |
| 884 } | 883 } |
| 885 | 884 |
| 886 void _readSocket() { | 885 void _readSocket() { |
| 887 if (_status == CLOSED) return; | 886 if (_status == CLOSED) return; |
| 888 var buffer = _secureFilter.buffers[READ_ENCRYPTED]; | 887 var buffer = _secureFilter.buffers[READ_ENCRYPTED]; |
| 889 if (buffer.writeFromSource(_readSocketOrBufferedData) > 0) { | 888 if (buffer.writeFromSource(_readSocketOrBufferedData) > 0) { |
| 890 _filterStatus.readEmpty = false; | 889 _filterStatus.readEmpty = false; |
| (...skipping 263 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1154 void handshake(); | 1153 void handshake(); |
| 1155 void init(); | 1154 void init(); |
| 1156 X509Certificate get peerCertificate; | 1155 X509Certificate get peerCertificate; |
| 1157 int processBuffer(int bufferIndex); | 1156 int processBuffer(int bufferIndex); |
| 1158 void registerBadCertificateCallback(Function callback); | 1157 void registerBadCertificateCallback(Function callback); |
| 1159 void registerHandshakeCompleteCallback(Function handshakeCompleteHandler); | 1158 void registerHandshakeCompleteCallback(Function handshakeCompleteHandler); |
| 1160 int _pointer(); | 1159 int _pointer(); |
| 1161 | 1160 |
| 1162 List<_ExternalBuffer> get buffers; | 1161 List<_ExternalBuffer> get buffers; |
| 1163 } | 1162 } |
| 1163 | |
| 1164 /** A secure networking exception caused by a failure in the | |
| 1165 * TLS/SSL protocol. | |
| 1166 */ | |
| 1167 class TlsException implements IOException { | |
| 1168 const TlsException([String message = "", | |
| 1169 OSError osError = null]) | |
| 1170 : this._("TlsException", message, osError); | |
| 1171 | |
| 1172 const TlsException._(String this.type, | |
| 1173 String this.message, | |
| 1174 OSError this.osError); | |
| 1175 | |
| 1176 String toString() { | |
| 1177 StringBuffer sb = new StringBuffer(); | |
| 1178 sb.write(type); | |
| 1179 if (!message.isEmpty) { | |
| 1180 sb.write(": $message"); | |
| 1181 if (osError != null) { | |
| 1182 sb.write(" ($osError)"); | |
| 1183 } | |
| 1184 } else if (osError != null) { | |
| 1185 sb.write(": $osError"); | |
| 1186 } | |
| 1187 return sb.toString(); | |
| 1188 } | |
| 1189 final String type; | |
|
Anders Johnsen
2013/06/25 05:55:03
Move to top.
Bill Hesse
2013/06/25 12:41:14
Done.
| |
| 1190 final String message; | |
| 1191 final OSError osError; | |
| 1192 } | |
| 1193 | |
| 1194 | |
| 1195 /** | |
| 1196 * An exception that happens in the handshake phase of establishing | |
| 1197 * a secure network connection. | |
| 1198 */ | |
|
Søren Gjesse
2013/06/25 06:35:49
As mentioned before maybe just drop HandshakeExcep
| |
| 1199 class HandshakeException extends TlsException { | |
| 1200 const HandshakeException([String message = "", | |
| 1201 OSError osError = null]) | |
| 1202 : super._("HandshakeException", message, osError); | |
| 1203 } | |
| 1204 | |
| 1205 | |
| 1206 /** | |
| 1207 * An exception that happens in the handshake phase of establishing | |
| 1208 * a secure network connection, when looking up or verifying a | |
| 1209 * certificate. | |
| 1210 */ | |
|
Søren Gjesse
2013/06/25 06:35:49
Ca we add some more information to this exception
Bill Hesse
2013/06/25 12:41:14
Done.
| |
| 1211 class CertificateException extends TlsException { | |
| 1212 const CertificateException([String message = "", | |
| 1213 OSError osError = null]) | |
| 1214 : super._("CertificateException", message, osError); | |
| 1215 } | |
| OLD | NEW |