// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. part of "dart:io"; /// A TCP socket using TLS and SSL. /// /// See [Socket] for more information. abstract interface class SecureSocket implements Socket { external factory SecureSocket._(RawSecureSocket rawSocket); /// Constructs a new secure client socket and connects it to the given /// [host] on port [port]. /// /// The returned Future will complete with a /// [SecureSocket] that is connected and ready for subscription. /// /// The certificate provided by the server is checked /// using the trusted certificates set in the SecurityContext object. /// The default SecurityContext object contains a built-in set of trusted /// root certificates for well-known certificate authorities. /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and /// decide (or let the user decide) whether to accept /// the connection or not. The handler should return true /// to continue the [SecureSocket] connection. /// /// [keyLog] is an optional callback that will be called when new TLS keys /// are exchanged with the server. [keyLog] will receive one line of text in /// [NSS Key Log Format](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) /// for each call. Writing these lines to a file will allow tools (such as /// [Wireshark](https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption)) /// to decrypt content sent through this socket. This is meant to allow /// network-level debugging of secure sockets and should not be used in /// production code. For example: /// ```dart /// final log = File('keylog.txt'); /// final socket = await SecureSocket.connect('www.example.com', 443, /// keyLog: (line) => log.writeAsStringSync(line, mode: FileMode.append)); /// ``` /// /// [supportedProtocols] is an optional list of protocols (in decreasing /// order of preference) to use during the ALPN protocol negotiation with the /// server. Example values are "http/1.1" or "h2". The selected protocol /// can be obtained via [SecureSocket.selectedProtocol]. /// /// The argument [timeout] is used to specify the maximum allowed time to wait /// for a connection to be established. If [timeout] is longer than the system /// level timeout duration, a timeout may occur sooner than specified in /// [timeout]. On timeout, a [SocketException] is thrown and all ongoing /// connection attempts to [host] are cancelled. static Future connect( host, int port, { SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, Duration? timeout, }) { return RawSecureSocket.connect( host, port, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, timeout: timeout, ).then((rawSocket) => SecureSocket._(rawSocket)); } /// Like [connect], but returns a [Future] that completes with a /// [ConnectionTask] that can be cancelled if the [SecureSocket] is no /// longer needed. static Future> startConnect( host, int port, { SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, }) { return RawSecureSocket.startConnect( host, port, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, ).then((rawState) { Future socket = rawState.socket.then( (rawSocket) => SecureSocket._(rawSocket), ); return ConnectionTask._(socket, rawState._onCancel); }); } /// Initiates TLS on an existing connection. /// /// Takes an already connected [socket] and starts client side TLS /// handshake to make the communication secure. When the returned /// future completes the [SecureSocket] has completed the TLS /// handshake. Using this function requires that the other end of the /// connection is prepared for TLS handshake. /// /// If the [socket] already has a subscription, this subscription /// will no longer receive and events. In most cases calling /// [StreamSubscription.pause] on this subscription before /// starting TLS handshake is the right thing to do. /// /// The given [socket] is closed and may not be used anymore. /// /// If the [host] argument is passed it will be used as the host name /// for the TLS handshake. If [host] is not passed the host name from /// the [socket] will be used. The [host] can be either a [String] or /// an [InternetAddress]. /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and /// decide (or let the user decide) whether to accept /// the connection or not. The handler should return true /// to continue the [SecureSocket] connection. /// /// [keyLog] is an optional callback that will be called when new TLS keys /// are exchanged with the server. [keyLog] will receive one line of text in /// [NSS Key Log Format](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) /// for each call. Writing these lines to a file will allow tools (such as /// [Wireshark](https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption)) /// to decrypt content sent through this socket. This is meant to allow /// network-level debugging of secure sockets and should not be used in /// production code. For example: /// ```dart /// final log = File('keylog.txt'); /// final socket = await SecureSocket.connect('www.example.com', 443, /// keyLog: (line) => log.writeAsStringSync(line, mode: FileMode.append)); /// ``` /// /// [supportedProtocols] is an optional list of protocols (in decreasing /// order of preference) to use during the ALPN protocol negotiation with the /// server. Example values are "http/1.1" or "h2". The selected protocol /// can be obtained via [SecureSocket.selectedProtocol]. /// /// Calling this function will _not_ cause a DNS host lookup. If the /// [host] passed is a [String], the [InternetAddress] for the /// resulting [SecureSocket] will have the passed in [host] as its /// host value and the internet address of the already connected /// socket as its address value. /// /// See [connect] for more information on the arguments. static Future secure( Socket socket, { host, SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, }) { return socket ._detachRaw() .then((detachedRaw) { return RawSecureSocket.secure( detachedRaw[0] as RawSocket, subscription: detachedRaw[1] as StreamSubscription?, host: host, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, ); }) .then((raw) => SecureSocket._(raw)); } /// Initiates TLS on an existing server connection. /// /// Takes an already connected [socket] and starts server side TLS /// handshake to make the communication secure. When the returned /// future completes the [SecureSocket] has completed the TLS /// handshake. Using this function requires that the other end of the /// connection is going to start the TLS handshake. /// /// If the [socket] already has a subscription, this subscription /// will no longer receive and events. In most cases calling /// [StreamSubscription.pause] on this subscription /// before starting TLS handshake is the right thing to do. /// /// If some of the data of the TLS handshake has already been read /// from the socket this data can be passed in the [bufferedData] /// parameter. This data will be processed before any other data /// available on the socket. /// /// See [SecureServerSocket.bind] for more information on the /// arguments. static Future secureServer( Socket socket, SecurityContext? context, { List? bufferedData, bool requestClientCertificate = false, bool requireClientCertificate = false, List? supportedProtocols, }) { return socket ._detachRaw() .then((detachedRaw) { return RawSecureSocket.secureServer( detachedRaw[0] as RawSocket, context, subscription: detachedRaw[1] as StreamSubscription?, bufferedData: bufferedData, requestClientCertificate: requestClientCertificate, requireClientCertificate: requireClientCertificate, supportedProtocols: supportedProtocols, ); }) .then((raw) => SecureSocket._(raw)); } /// The peer certificate for a connected SecureSocket. /// /// If this [SecureSocket] is the server end of a secure socket connection, /// [peerCertificate] will return the client certificate, or `null` if no /// client certificate was received. If this socket is the client end, /// [peerCertificate] will return the server's certificate. X509Certificate? get peerCertificate; /// The protocol which was selected during ALPN protocol negotiation. /// /// Returns `null` if one of the peers does not have support for ALPN, did not /// specify a list of supported ALPN protocols or there was no common /// protocol between client and server. String? get selectedProtocol; /// Does nothing. /// /// The original intent was to allow TLS renegotiation of existing secure /// connections. @Deprecated("Not implemented") void renegotiate({ bool useSessionCache = true, bool requestClientCertificate = false, bool requireClientCertificate = false, }); } /// `RawSecureSocket` provides a secure (SSL or TLS) network connection. /// /// Client connections to a server are provided by calling /// RawSecureSocket.connect. A secure server, created with /// [RawSecureServerSocket], also returns `RawSecureSocket` objects representing /// the server end of a secure connection. /// The certificate provided by the server is checked /// using the trusted certificates set in the [SecurityContext] object. /// The default [SecurityContext] object contains a built-in set of trusted /// root certificates for well-known certificate authorities. /// /// See [RawSocket] for more information. abstract interface class RawSecureSocket implements RawSocket { /// Constructs a new secure client socket and connect it to the given /// host on the given port. /// /// The returned [Future] is completed with the /// [RawSecureSocket] when it is connected and ready for subscription. /// /// The certificate provided by the server is checked using the trusted /// certificates set in the SecurityContext object If a certificate and key are /// set on the client, using [SecurityContext.useCertificateChain] and /// [SecurityContext.usePrivateKey], and the server asks for a client /// certificate, then that client certificate is sent to the server. /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and /// decide (or let the user decide) whether to accept /// the connection or not. The handler should return true /// to continue the [RawSecureSocket] connection. /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and /// decide (or let the user decide) whether to accept /// the connection or not. The handler should return true /// to continue the [SecureSocket] connection. /// /// [keyLog] is an optional callback that will be called when new TLS keys /// are exchanged with the server. [keyLog] will receive one line of text in /// [NSS Key Log Format](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) /// for each call. Writing these lines to a file will allow tools (such as /// [Wireshark](https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption)) /// to decrypt content sent through this socket. This is meant to allow /// network-level debugging of secure sockets and should not be used in /// production code. For example: /// ```dart /// final log = File('keylog.txt'); /// final socket = await SecureSocket.connect('www.example.com', 443, /// keyLog: (line) => log.writeAsStringSync(line, mode: FileMode.append)); /// ``` /// /// [supportedProtocols] is an optional list of protocols (in decreasing /// order of preference) to use during the ALPN protocol negotiation with the /// server. Example values are "http/1.1" or "h2". The selected protocol /// can be obtained via [RawSecureSocket.selectedProtocol]. static Future connect( host, int port, { SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, Duration? timeout, }) { _RawSecureSocket._verifyFields(host, port, false, false); return RawSocket.connect(host, port, timeout: timeout).then((socket) { return secure( socket, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, ); }); } /// Like [connect], but returns a [Future] that completes with a /// [ConnectionTask] that can be cancelled if the [RawSecureSocket] is no /// longer needed. static Future> startConnect( host, int port, { SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, }) { return RawSocket.startConnect(host, port).then(( ConnectionTask rawState, ) { Future socket = rawState.socket.then((rawSocket) { return secure( rawSocket, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, ); }); return ConnectionTask._(socket, rawState._onCancel); }); } /// Initiates TLS on an existing connection. /// /// Takes an already connected [socket] and starts client side TLS /// handshake to make the communication secure. When the returned /// future completes the [RawSecureSocket] has completed the TLS /// handshake. Using this function requires that the other end of the /// connection is prepared for TLS handshake. /// /// If the [socket] already has a subscription, pass the existing /// subscription in the [subscription] parameter. The [secure] /// operation will take over the subscription by replacing the /// handlers with it own secure processing. The caller must not touch /// this subscription anymore. Passing a paused subscription is an /// error. /// /// If the [host] argument is passed it will be used as the host name /// for the TLS handshake. If [host] is not passed the host name from /// the [socket] will be used. The [host] can be either a [String] or /// an [InternetAddress]. /// /// [onBadCertificate] is an optional handler for unverifiable certificates. /// The handler receives the [X509Certificate], and can inspect it and /// decide (or let the user decide) whether to accept /// the connection or not. The handler should return true /// to continue the [SecureSocket] connection. /// /// [keyLog] is an optional callback that will be called when new TLS keys /// are exchanged with the server. [keyLog] will receive one line of text in /// [NSS Key Log Format](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format) /// for each call. Writing these lines to a file will allow tools (such as /// [Wireshark](https://gitlab.com/wireshark/wireshark/-/wikis/TLS#tls-decryption)) /// to decrypt content sent through this socket. This is meant to allow /// network-level debugging of secure sockets and should not be used in /// production code. For example: /// ```dart /// final log = File('keylog.txt'); /// final socket = await SecureSocket.connect('www.example.com', 443, /// keyLog: (line) => log.writeAsStringSync(line, mode: FileMode.append)); /// ``` /// /// [supportedProtocols] is an optional list of protocols (in decreasing /// order of preference) to use during the ALPN protocol negotiation with the /// server. Example values are "http/1.1" or "h2". The selected protocol /// can be obtained via [SecureSocket.selectedProtocol]. /// /// Calling this function will _not_ cause a DNS host lookup. If the /// [host] passed is a [String] the [InternetAddress] for the /// resulting [SecureSocket] will have this passed in [host] as its /// host value and the internet address of the already connected /// socket as its address value. /// /// See [connect] for more information on the arguments. static Future secure( RawSocket socket, { StreamSubscription? subscription, host, SecurityContext? context, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, }) { socket.readEventsEnabled = false; socket.writeEventsEnabled = false; return _RawSecureSocket.connect( host != null ? host : socket.address.host, socket.port, false, socket, subscription: subscription, context: context, onBadCertificate: onBadCertificate, keyLog: keyLog, supportedProtocols: supportedProtocols, ); } /// Initiates TLS on an existing server connection. /// /// Takes an already connected [socket] and starts server side TLS /// handshake to make the communication secure. When the returned /// future completes the [RawSecureSocket] has completed the TLS /// handshake. Using this function requires that the other end of the /// connection is going to start the TLS handshake. /// /// If the [socket] already has a subscription, pass the existing /// subscription in the [subscription] parameter. The [secureServer] /// operation will take over the subscription by replacing the /// handlers with it own secure processing. The caller must not touch /// this subscription anymore. Passing a paused subscription is an /// error. /// /// If some of the data of the TLS handshake has already been read /// from the socket this data can be passed in the [bufferedData] /// parameter. This data will be processed before any other data /// available on the socket. /// /// See [RawSecureServerSocket.bind] for more information on the /// arguments. static Future secureServer( RawSocket socket, SecurityContext? context, { StreamSubscription? subscription, List? bufferedData, bool requestClientCertificate = false, bool requireClientCertificate = false, List? supportedProtocols, }) { socket.readEventsEnabled = false; socket.writeEventsEnabled = false; return _RawSecureSocket.connect( socket.address, socket.remotePort, true, socket, context: context, subscription: subscription, bufferedData: bufferedData, requestClientCertificate: requestClientCertificate, requireClientCertificate: requireClientCertificate, supportedProtocols: supportedProtocols, ); } /// Does nothing. /// /// The original intent was to allow TLS renegotiation of existing secure /// connections. @Deprecated("Not implemented") void renegotiate({ bool useSessionCache = true, bool requestClientCertificate = false, bool requireClientCertificate = false, }); /// Get the peer certificate for a connected RawSecureSocket. If this /// RawSecureSocket is the server end of a secure socket connection, /// [peerCertificate] will return the client certificate, or null, if no /// client certificate was received. If it is the client end, /// [peerCertificate] will return the server's certificate. X509Certificate? get peerCertificate; /// The protocol which was selected during protocol negotiation. /// /// Returns null if one of the peers does not have support for ALPN, did not /// specify a list of supported ALPN protocols or there was no common /// protocol between client and server. String? get selectedProtocol; } /// X509Certificate represents an SSL certificate, with accessors to /// get the fields of the certificate. @pragma("vm:entry-point") abstract interface class X509Certificate { @pragma("vm:entry-point") external factory X509Certificate._(); /// The DER encoded bytes of the certificate. Uint8List get der; /// The PEM encoded String of the certificate. String get pem; /// The SHA1 hash of the certificate. Uint8List get sha1; String get subject; String get issuer; DateTime get startValidity; DateTime get endValidity; } class _FilterStatus { bool progress = false; // The filter read or wrote data to the buffers. bool readEmpty = true; // The read buffers and decryption filter are empty. bool writeEmpty = true; // The write buffers and encryption filter are empty. // These are set if a buffer changes state from empty or full. bool readPlaintextNoLongerEmpty = false; bool writePlaintextNoLongerFull = false; bool readEncryptedNoLongerFull = false; bool writeEncryptedNoLongerEmpty = false; _FilterStatus(); } // Interface used by [RawSecureServerSocket] and [_RawSecureSocket] that exposes // members of [_NativeSocket]. abstract interface class _RawSocketBase { bool get _closedReadEventSent; void set _owner(owner); } class _RawSecureSocket extends Stream implements RawSecureSocket, _RawSocketBase { // Status states static const int handshakeStatus = 201; static const int connectedStatus = 202; static const int closedStatus = 203; // Buffer identifiers. // These must agree with those in the native C++ implementation. static const int readPlaintextId = 0; static const int writePlaintextId = 1; static const int readEncryptedId = 2; static const int writeEncryptedId = 3; static const int bufferCount = 4; // Is a buffer identifier for an encrypted buffer? static bool _isBufferEncrypted(int identifier) => identifier >= readEncryptedId; final RawSocket _socket; final Completer<_RawSecureSocket> _handshakeComplete = Completer<_RawSecureSocket>(); final _controller = StreamController(sync: true); late final StreamSubscription _socketSubscription; List? _bufferedData; int _bufferedDataIndex = 0; final InternetAddress address; final bool isServer; final SecurityContext context; final bool requestClientCertificate; final bool requireClientCertificate; final bool Function(X509Certificate certificate)? onBadCertificate; final void Function(String line)? keyLog; ReceivePort? keyLogPort; var _status = handshakeStatus; bool _writeEventsEnabled = true; bool _readEventsEnabled = true; int _pauseCount = 0; bool _pendingReadEvent = false; bool _socketClosedRead = false; // The network socket is closed for reading. bool _socketClosedWrite = false; // The network socket is closed for writing. bool _closedRead = false; // The secure socket has fired an onClosed event. bool _closedWrite = false; // The secure socket has been closed for writing. // The network socket is gone. Completer _closeCompleter = Completer(); _FilterStatus _filterStatus = _FilterStatus(); bool _connectPending = true; bool _filterPending = false; bool _filterActive = false; _SecureFilter? _secureFilter = _SecureFilter._(); String? _selectedProtocol; static Future<_RawSecureSocket> connect( dynamic /*String|InternetAddress*/ host, int requestedPort, bool isServer, RawSocket socket, { SecurityContext? context, StreamSubscription? subscription, List? bufferedData, bool requestClientCertificate = false, bool requireClientCertificate = false, bool onBadCertificate(X509Certificate certificate)?, void keyLog(String line)?, List? supportedProtocols, }) { _verifyFields( host, requestedPort, requestClientCertificate, requireClientCertificate, ); if (host is InternetAddress) host = host.host; InternetAddress address = socket.address; if (host != null) { address = InternetAddress._cloneWithNewHost(address, host); } return _RawSecureSocket( address, requestedPort, isServer, context ?? SecurityContext.defaultContext, socket, subscription, bufferedData, requestClientCertificate, requireClientCertificate, onBadCertificate, keyLog, supportedProtocols, )._handshakeComplete.future; } _RawSecureSocket( this.address, int requestedPort, this.isServer, this.context, this._socket, StreamSubscription? subscription, this._bufferedData, this.requestClientCertificate, this.requireClientCertificate, this.onBadCertificate, this.keyLog, List? supportedProtocols, ) { _controller ..onListen = _onSubscriptionStateChange ..onPause = _onPauseStateChange ..onResume = _onPauseStateChange ..onCancel = _onSubscriptionStateChange; // Throw an ArgumentError if any field is invalid. After this, all // errors will be reported through the future or the stream. final secureFilter = _secureFilter!; secureFilter.init(); secureFilter.registerHandshakeCompleteCallback( _secureHandshakeCompleteHandler, ); if (keyLog != null) { final port = ReceivePort(); port.listen((line) { try { keyLog!((line as String) + '\n'); } catch (e, s) { // There is no obvious place to surface exceptions from the keyLog // callback so write the details to stderr. stderr.writeln("Failure in keyLog callback:"); stderr.writeln(s); } }); secureFilter.registerKeyLogPort(port.sendPort); keyLogPort = port; } if (onBadCertificate != null) { secureFilter.registerBadCertificateCallback(_onBadCertificateWrapper); } _socket.readEventsEnabled = true; _socket.writeEventsEnabled = false; if (subscription == null) { // If a current subscription is provided use this otherwise // create a new one. _socketSubscription = _socket.listen( _eventDispatcher, onError: _reportError, onDone: _doneHandler, ); } else { _socketSubscription = subscription; if (_socketSubscription.isPaused) { _socket.close(); throw ArgumentError("Subscription passed to TLS upgrade is paused"); } // If we are upgrading a socket that is already closed for read, // report an error as if we received readClosed during the handshake. if (_closedReadEventSent) { _eventDispatcher(RawSocketEvent.readClosed); } _socketSubscription ..onData(_eventDispatcher) ..onError(_reportError) ..onDone(_doneHandler); } try { var encodedProtocols = SecurityContext._protocolsToLengthEncoding( supportedProtocols, ); secureFilter.connect( address.host, context, isServer, requestClientCertificate || requireClientCertificate, requireClientCertificate, encodedProtocols, ); _secureHandshake(); } catch (e, s) { _reportError(e, s); } } StreamSubscription listen( void onData(RawSocketEvent data)?, { Function? onError, void onDone()?, bool? cancelOnError, }) { _sendWriteEvent(); return _controller.stream.listen( onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError, ); } static void _verifyFields( host, int requestedPort, bool requestClientCertificate, bool requireClientCertificate, ) { if (host is! String && host is! InternetAddress) { throw ArgumentError("host is not a String or an InternetAddress"); } if (requestedPort < 0 || requestedPort > 65535) { throw ArgumentError("requestedPort is not in the range 0..65535"); } } int get port => _socket.port; InternetAddress get remoteAddress => _socket.remoteAddress; int get remotePort => _socket.remotePort; bool get _closedReadEventSent => (_socket as _RawSocketBase)._closedReadEventSent; void set _owner(owner) { (_socket as _RawSocketBase)._owner = owner; } int available() { return _status != connectedStatus ? 0 : _secureFilter!.buffers![readPlaintextId].length; } Future close() { shutdown(SocketDirection.both); return _closeCompleter.future; } void _completeCloseCompleter([RawSocket? dummy]) { if (!_closeCompleter.isCompleted) _closeCompleter.complete(this); } void _close() { _closedWrite = true; _closedRead = true; _socket.close().then(_completeCloseCompleter); _socketClosedWrite = true; _socketClosedRead = true; if (!_filterActive && _secureFilter != null) { _secureFilter!.destroy(); _secureFilter = null; } keyLogPort?.close(); if (_socketSubscription != null) { _socketSubscription.cancel(); } _controller.close(); _status = closedStatus; } void shutdown(SocketDirection direction) { if (direction == SocketDirection.send || direction == SocketDirection.both) { _closedWrite = true; if (_filterStatus.writeEmpty) { _socket.shutdown(SocketDirection.send); _socketClosedWrite = true; if (_closedRead) { _close(); } } } if (direction == SocketDirection.receive || direction == SocketDirection.both) { _closedRead = true; _socketClosedRead = true; _socket.shutdown(SocketDirection.receive); if (_socketClosedWrite) { _close(); } } } bool get writeEventsEnabled => _writeEventsEnabled; void set writeEventsEnabled(bool value) { _writeEventsEnabled = value; if (value) { Timer.run(() => _sendWriteEvent()); } } bool get readEventsEnabled => _readEventsEnabled; void set readEventsEnabled(bool value) { _readEventsEnabled = value; _scheduleReadEvent(); } Uint8List? read([int? length]) { if (length != null && length < 0) { throw ArgumentError( "Invalid length parameter in SecureSocket.read (length: $length)", ); } if (_closedRead) { throw SocketException("Reading from a closed socket"); } if (_status != connectedStatus) { return null; } var result = _secureFilter!.buffers![readPlaintextId].read(length); _scheduleFilter(); return result; } SocketMessage? readMessage([int? count]) { throw UnsupportedError("Message-passing not supported by secure sockets"); } // Write the data to the socket, and schedule the filter to encrypt it. int write(List data, [int offset = 0, int? bytes]) { if (bytes != null && bytes < 0) { throw ArgumentError( "Invalid bytes parameter in SecureSocket.read (bytes: $bytes)", ); } if (offset < 0) { throw ArgumentError( "Invalid offset parameter in SecureSocket.read (offset: $offset)", ); } if (_closedWrite) { _controller.addError(SocketException("Writing to a closed socket")); return 0; } if (_status != connectedStatus) return 0; bytes ??= data.length - offset; int written = _secureFilter!.buffers![writePlaintextId].write( data, offset, bytes, ); if (written > 0) { _filterStatus.writeEmpty = false; } _scheduleFilter(); return written; } int sendMessage( List controlMessages, List data, [ int offset = 0, int? count, ]) { throw UnsupportedError("Message-passing not supported by secure sockets"); } X509Certificate? get peerCertificate => _secureFilter!.peerCertificate; String? get selectedProtocol => _selectedProtocol; bool _onBadCertificateWrapper(X509Certificate certificate) { if (onBadCertificate == null) return false; return onBadCertificate!(certificate); } bool setOption(SocketOption option, bool enabled) { return _socket.setOption(option, enabled); } Uint8List getRawOption(RawSocketOption option) { return _socket.getRawOption(option); } void setRawOption(RawSocketOption option) { _socket.setRawOption(option); } void _eventDispatcher(RawSocketEvent event) { try { if (event == RawSocketEvent.read) { _readHandler(); } else if (event == RawSocketEvent.write) { _writeHandler(); } else if (event == RawSocketEvent.readClosed) { _closeHandler(); } } catch (e, stackTrace) { _reportError(e, stackTrace); } } void _readHandler() { _readSocket(); _scheduleFilter(); } void _writeHandler() { _writeSocket(); _scheduleFilter(); } void _doneHandler() { if (_filterStatus.readEmpty) { _close(); } } void _reportError(e, [StackTrace? stackTrace]) { if (_status == closedStatus) { return; } else if (_connectPending) { // _connectPending is true until the handshake has completed, and the // _handshakeComplete future returned from SecureSocket.connect has // completed. Before this point, we must complete it with an error. _handshakeComplete.completeError(e, stackTrace); } else { _controller.addError(e, stackTrace); } _close(); } void _closeHandler() async { if (_status == connectedStatus) { if (_closedRead) return; _socketClosedRead = true; if (_filterStatus.readEmpty) { _closedRead = true; _controller.add(RawSocketEvent.readClosed); if (_socketClosedWrite) { _close(); } } else { await _scheduleFilter(); } } else if (_status == handshakeStatus) { _socketClosedRead = true; // The other party might have disconnected, but if there still // bytes available we can continue handshake. if (_filterStatus.readEmpty) { _reportError( HandshakeException('Connection terminated during handshake'), null, ); } } } Future _secureHandshake() async { try { bool needRetryHandshake = await _secureFilter!.handshake(); if (needRetryHandshake) { // Some certificates have been evaluated, need to retry handshake. await _secureHandshake(); } else { _filterStatus.writeEmpty = false; _readSocket(); _writeSocket(); await _scheduleFilter(); } } catch (e, stackTrace) { _reportError(e, stackTrace); } } @Deprecated("Not implemented") void renegotiate({ bool useSessionCache = true, bool requestClientCertificate = false, bool requireClientCertificate = false, }) { if (_status != connectedStatus) { throw HandshakeException("Called renegotiate on a non-connected socket"); } _status = handshakeStatus; _filterStatus.writeEmpty = false; _scheduleFilter(); } void _secureHandshakeCompleteHandler() { _status = connectedStatus; if (_connectPending) { _connectPending = false; try { _selectedProtocol = _secureFilter!.selectedProtocol(); // We don't want user code to run synchronously in this callback. Timer.run(() => _handshakeComplete.complete(this)); } catch (error, stack) { _handshakeComplete.completeError(error, stack); } } } void _onPauseStateChange() { if (_controller.isPaused) { _pauseCount++; } else { _pauseCount--; if (_pauseCount == 0) { _scheduleReadEvent(); _sendWriteEvent(); // Can send event synchronously. } } if (!_socketClosedRead || !_socketClosedWrite) { if (_controller.isPaused) { _socketSubscription.pause(); } else { _socketSubscription.resume(); } } } void _onSubscriptionStateChange() { if (_controller.hasListener) { // TODO(ajohnsen): Do something here? } } Future _scheduleFilter() { _filterPending = true; return _tryFilter(); } Future _tryFilter() async { try { while (true) { if (_status == closedStatus) { return; } if (!_filterPending || _filterActive) { return; } _filterActive = true; _filterPending = false; _filterStatus = await _pushAllFilterStages(); _filterActive = false; if (_status == closedStatus) { _secureFilter!.destroy(); _secureFilter = null; return; } _socket.readEventsEnabled = true; if (_filterStatus.writeEmpty && _closedWrite && !_socketClosedWrite) { // Checks for and handles all cases of partially closed sockets. shutdown(SocketDirection.send); if (_status == closedStatus) { return; } } if (_filterStatus.readEmpty && _socketClosedRead && !_closedRead) { if (_status == handshakeStatus) { _secureFilter!.handshake(); if (_status == handshakeStatus) { throw HandshakeException( 'Connection terminated during handshake', ); } } _closeHandler(); } if (_status == closedStatus) { return; } if (_filterStatus.progress) { _filterPending = true; if (_filterStatus.writeEncryptedNoLongerEmpty) { _writeSocket(); } if (_filterStatus.writePlaintextNoLongerFull) { _sendWriteEvent(); } if (_filterStatus.readEncryptedNoLongerFull) { _readSocket(); } if (_filterStatus.readPlaintextNoLongerEmpty) { _scheduleReadEvent(); } if (_status == handshakeStatus) { await _secureHandshake(); } } } } catch (e, st) { _reportError(e, st); } } List? _readSocketOrBufferedData(int bytes) { final bufferedData = _bufferedData; if (bufferedData != null) { if (bytes > bufferedData.length - _bufferedDataIndex) { bytes = bufferedData.length - _bufferedDataIndex; } var result = bufferedData.sublist( _bufferedDataIndex, _bufferedDataIndex + bytes, ); _bufferedDataIndex += bytes; if (bufferedData.length == _bufferedDataIndex) { _bufferedData = null; } return result; } else if (!_socketClosedRead) { return _socket.read(bytes); } else { return null; } } void _readSocket() { if (_status == closedStatus) return; var buffer = _secureFilter!.buffers![readEncryptedId]; if (buffer.writeFromSource(_readSocketOrBufferedData) > 0) { _filterStatus.readEmpty = false; } else { _socket.readEventsEnabled = false; } } void _writeSocket() { if (_socketClosedWrite) return; var buffer = _secureFilter!.buffers![writeEncryptedId]; if (buffer.readToSocket(_socket)) { // Returns true if blocked _socket.writeEventsEnabled = true; } } // If a read event should be sent, add it to the controller. _scheduleReadEvent() { if (!_pendingReadEvent && _readEventsEnabled && _pauseCount == 0 && _secureFilter != null && !_secureFilter!.buffers![readPlaintextId].isEmpty) { _pendingReadEvent = true; Timer.run(_sendReadEvent); } } _sendReadEvent() { _pendingReadEvent = false; if (_status != closedStatus && _readEventsEnabled && _pauseCount == 0 && _secureFilter != null && !_secureFilter!.buffers![readPlaintextId].isEmpty) { _controller.add(RawSocketEvent.read); _scheduleReadEvent(); } } // If a write event should be sent, add it to the controller. _sendWriteEvent() { if (!_closedWrite && _writeEventsEnabled && _pauseCount == 0 && _secureFilter != null && _secureFilter!.buffers![writePlaintextId].free > 0) { _writeEventsEnabled = false; _controller.add(RawSocketEvent.write); } } Future<_FilterStatus> _pushAllFilterStages() async { bool wasInHandshake = _status != connectedStatus; List args = List.filled(2 + bufferCount * 2, null); args[0] = _secureFilter!._pointer(); args[1] = wasInHandshake; var bufs = _secureFilter!.buffers!; for (var i = 0; i < bufferCount; ++i) { args[2 * i + 2] = bufs[i].start; args[2 * i + 3] = bufs[i].end; } var response = (await _IOService._dispatch(_IOService.sslProcessFilter, args)) as List; if (response.length == 2) { if (wasInHandshake) { // If we're in handshake, throw a handshake error. _reportError( HandshakeException('${response[1]} error ${response[0]}'), null, ); } else { // If we're connected, throw a TLS error. _reportError(TlsException('${response[1]} error ${response[0]}'), null); } } int start(int index) => response[2 * index] as int; int end(int index) => response[2 * index + 1] as int; _FilterStatus status = _FilterStatus(); // Compute writeEmpty as "write plaintext buffer and write encrypted // buffer were empty when we started and are empty now". status.writeEmpty = bufs[writePlaintextId].isEmpty && start(writeEncryptedId) == end(writeEncryptedId); // If we were in handshake when this started, _writeEmpty may be false // because the handshake wrote data after we checked. if (wasInHandshake) status.writeEmpty = false; // Compute readEmpty as "both read buffers were empty when we started // and are empty now". status.readEmpty = bufs[readEncryptedId].isEmpty && start(readPlaintextId) == end(readPlaintextId); _ExternalBuffer buffer = bufs[writePlaintextId]; int new_start = start(writePlaintextId); if (new_start != buffer.start) { status.progress = true; if (buffer.free == 0) { status.writePlaintextNoLongerFull = true; } buffer.start = new_start; } buffer = bufs[readEncryptedId]; new_start = start(readEncryptedId); if (new_start != buffer.start) { status.progress = true; if (buffer.free == 0) { status.readEncryptedNoLongerFull = true; } buffer.start = new_start; } buffer = bufs[writeEncryptedId]; int new_end = end(writeEncryptedId); if (new_end != buffer.end) { status.progress = true; if (buffer.length == 0) { status.writeEncryptedNoLongerEmpty = true; } buffer.end = new_end; } buffer = bufs[readPlaintextId]; new_end = end(readPlaintextId); if (new_end != buffer.end) { status.progress = true; if (buffer.length == 0) { status.readPlaintextNoLongerEmpty = true; } buffer.end = new_end; } return status; } } /// A circular buffer backed by an external byte array. Accessed from /// both C++ and Dart code in an unsynchronized way, with one reading /// and one writing. All updates to start and end are done by Dart code. class _ExternalBuffer { // This will be an ExternalByteArray, backed by C allocated data. @pragma("vm:entry-point") List? data; @pragma("vm:entry-point") int start; @pragma("vm:entry-point") int end; final int size; _ExternalBuffer(int size) : size = size, start = size ~/ 2, end = size ~/ 2; void advanceStart(int bytes) { assert(start > end || start + bytes <= end); start += bytes; if (start >= size) { start -= size; assert(start <= end); assert(start < size); } } void advanceEnd(int bytes) { assert(start <= end || start > end + bytes); end += bytes; if (end >= size) { end -= size; assert(end < start); assert(end < size); } } bool get isEmpty => end == start; int get length => start > end ? size + end - start : end - start; int get linearLength => start > end ? size - start : end - start; int get free => start > end ? start - end - 1 : size + start - end - 1; int get linearFree { if (start > end) return start - end - 1; if (start == 0) return size - end - 1; return size - end; } Uint8List? read(int? bytes) { if (bytes == null) { bytes = length; } else { bytes = min(bytes, length); } if (bytes == 0) return null; Uint8List result = Uint8List(bytes); int bytesRead = 0; // Loop over zero, one, or two linear data ranges. while (bytesRead < bytes) { int toRead = min(bytes - bytesRead, linearLength); result.setRange(bytesRead, bytesRead + toRead, data!, start); advanceStart(toRead); bytesRead += toRead; } return result; } int write(List inputData, int offset, int bytes) { if (bytes > free) { bytes = free; } int written = 0; int toWrite = min(bytes, linearFree); // Loop over zero, one, or two linear data ranges. while (toWrite > 0) { data!.setRange(end, end + toWrite, inputData, offset); advanceEnd(toWrite); offset += toWrite; written += toWrite; toWrite = min(bytes - written, linearFree); } return written; } int writeFromSource(List? getData(int requested)) { int written = 0; int toWrite = linearFree; // Loop over zero, one, or two linear data ranges. while (toWrite > 0) { // Source returns at most toWrite bytes, and it returns null when empty. var inputData = getData(toWrite); if (inputData == null || inputData.length == 0) break; var len = inputData.length; data!.setRange(end, end + len, inputData); advanceEnd(len); written += len; toWrite = linearFree; } return written; } bool readToSocket(RawSocket socket) { // Loop over zero, one, or two linear data ranges. while (true) { var toWrite = linearLength; if (toWrite == 0) return false; int bytes = socket.write(data!, start, toWrite); advanceStart(bytes); if (bytes < toWrite) { // The socket has blocked while we have data to write. return true; } } } } abstract class _SecureFilter { external factory _SecureFilter._(); void connect( String hostName, SecurityContext context, bool isServer, bool requestClientCertificate, bool requireClientCertificate, Uint8List protocols, ); void destroy(); Future handshake(); String? selectedProtocol(); void rehandshake(); void init(); X509Certificate? get peerCertificate; int processBuffer(int bufferIndex); void registerBadCertificateCallback(bool Function(X509Certificate) callback); void registerHandshakeCompleteCallback(Function handshakeCompleteHandler); void registerKeyLogPort(SendPort port); // This call may cause a reference counted pointer in the native // implementation to be retained. It should only be called when the resulting // value is passed to the IO service through a call to dispatch(). int _pointer(); List<_ExternalBuffer>? get buffers; } /// A secure networking exception caused by a failure in the /// TLS/SSL protocol. @pragma("vm:entry-point") class TlsException implements IOException { final String type; final String message; final OSError? osError; @pragma("vm:entry-point") const TlsException([String message = "", OSError? osError]) : this._("TlsException", message, osError); const TlsException._(this.type, this.message, this.osError); String toString() { StringBuffer sb = StringBuffer(); sb.write(type); if (message.isNotEmpty) { sb.write(": $message"); if (osError != null) { sb.write(" ($osError)"); } } else if (osError != null) { sb.write(": $osError"); } return sb.toString(); } } /// An exception that happens in the handshake phase of establishing /// a secure network connection. @pragma("vm:entry-point") class HandshakeException extends TlsException { @pragma("vm:entry-point") const HandshakeException([String message = "", OSError? osError]) : super._("HandshakeException", message, osError); } /// An exception that happens in the handshake phase of establishing /// a secure network connection, when looking up or verifying a /// certificate. class CertificateException extends TlsException { @pragma("vm:entry-point") const CertificateException([String message = "", OSError? osError]) : super._("CertificateException", message, osError); }