blob: de267bbed6acaec5ac0595c06e18b0b51fc98310 [file] [log] [blame]
// 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.
///
/// A secure socket may be used as either a [Stream] or an [IOSink].
abstract 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<SecureSocket> connect(host, int port,
{SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? supportedProtocols,
Duration? timeout}) {
return RawSecureSocket.connect(host, port,
context: context,
onBadCertificate: onBadCertificate,
keyLog: keyLog,
supportedProtocols: supportedProtocols,
timeout: timeout)
.then((rawSocket) => new 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<ConnectionTask<SecureSocket>> startConnect(host, int port,
{SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? supportedProtocols}) {
return RawSecureSocket.startConnect(host, port,
context: context,
onBadCertificate: onBadCertificate,
keyLog: keyLog,
supportedProtocols: supportedProtocols)
.then((rawState) {
Future<SecureSocket> socket =
rawState.socket.then((rawSocket) => new SecureSocket._(rawSocket));
return new ConnectionTask<SecureSocket>._(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<SecureSocket> secure(Socket socket,
{host,
SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
@Since("2.6") List<String>? supportedProtocols}) {
return ((socket as dynamic /*_Socket*/)._detachRaw() as Future)
.then<RawSecureSocket>((detachedRaw) {
return RawSecureSocket.secure(detachedRaw[0] as RawSocket,
subscription: detachedRaw[1] as StreamSubscription<RawSocketEvent>?,
host: host,
context: context,
onBadCertificate: onBadCertificate,
keyLog: keyLog,
supportedProtocols: supportedProtocols);
}).then<SecureSocket>((raw) => new 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<SecureSocket> secureServer(
Socket socket, SecurityContext? context,
{List<int>? bufferedData,
bool requestClientCertificate = false,
bool requireClientCertificate = false,
List<String>? supportedProtocols}) {
return ((socket as dynamic /*_Socket*/)._detachRaw() as Future)
.then<RawSecureSocket>((detachedRaw) {
return RawSecureSocket.secureServer(detachedRaw[0] as RawSocket, context,
subscription: detachedRaw[1] as StreamSubscription<RawSocketEvent>?,
bufferedData: bufferedData,
requestClientCertificate: requestClientCertificate,
requireClientCertificate: requireClientCertificate,
supportedProtocols: supportedProtocols);
}).then<SecureSocket>((raw) => new 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;
/// Renegotiates an existing secure connection.
///
/// Renews the session keys and possibly changes the connection properties.
///
/// This repeats the SSL or TLS handshake, with options that allow clearing
/// the session cache and requesting a client certificate.
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.
abstract 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<RawSecureSocket> connect(host, int port,
{SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? 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<ConnectionTask<RawSecureSocket>> startConnect(host, int port,
{SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? supportedProtocols}) {
return RawSocket.startConnect(host, port)
.then((ConnectionTask<RawSocket> rawState) {
Future<RawSecureSocket> socket = rawState.socket.then((rawSocket) {
return secure(rawSocket,
context: context,
onBadCertificate: onBadCertificate,
keyLog: keyLog,
supportedProtocols: supportedProtocols);
});
return new ConnectionTask<RawSecureSocket>._(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<RawSecureSocket> secure(RawSocket socket,
{StreamSubscription<RawSocketEvent>? subscription,
host,
SecurityContext? context,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? 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<RawSecureSocket> secureServer(
RawSocket socket, SecurityContext? context,
{StreamSubscription<RawSocketEvent>? subscription,
List<int>? bufferedData,
bool requestClientCertificate = false,
bool requireClientCertificate = false,
List<String>? 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);
}
/// Renegotiate an existing secure connection, renewing the session keys
/// and possibly changing the connection properties.
///
/// This repeats the SSL or TLS handshake, with options that allow clearing
/// the session cache and requesting a client certificate.
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 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();
}
class _RawSecureSocket extends Stream<RawSocketEvent>
implements RawSecureSocket {
// 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 =
new Completer<_RawSecureSocket>();
final _controller = new StreamController<RawSocketEvent>(sync: true);
late final StreamSubscription<RawSocketEvent> _socketSubscription;
List<int>? _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<RawSecureSocket> _closeCompleter = new Completer<RawSecureSocket>();
_FilterStatus _filterStatus = new _FilterStatus();
bool _connectPending = true;
bool _filterPending = false;
bool _filterActive = false;
_SecureFilter? _secureFilter = new _SecureFilter._();
String? _selectedProtocol;
static Future<_RawSecureSocket> connect(
dynamic /*String|InternetAddress*/ host,
int requestedPort,
bool isServer,
RawSocket socket,
{SecurityContext? context,
StreamSubscription<RawSocketEvent>? subscription,
List<int>? bufferedData,
bool requestClientCertificate = false,
bool requireClientCertificate = false,
bool onBadCertificate(X509Certificate certificate)?,
void keyLog(String line)?,
List<String>? 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 new _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<RawSocketEvent>? subscription,
this._bufferedData,
this.requestClientCertificate,
this.requireClientCertificate,
this.onBadCertificate,
this.keyLog,
List<String>? 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 new 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.
dynamic s = _socket; // Cast to dynamic to avoid warning.
if (s._socket.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<RawSocketEvent> 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 new ArgumentError("host is not a String or an InternetAddress");
}
// TODO(40614): Remove once non-nullability is sound.
ArgumentError.checkNotNull(requestedPort, "requestedPort");
if (requestedPort < 0 || requestedPort > 65535) {
throw ArgumentError("requestedPort is not in the range 0..65535");
}
// TODO(40614): Remove once non-nullability is sound.
ArgumentError.checkNotNull(
requestClientCertificate, "requestClientCertificate");
ArgumentError.checkNotNull(
requireClientCertificate, "requireClientCertificate");
}
int get port => _socket.port;
InternetAddress get remoteAddress => _socket.remoteAddress;
int get remotePort => _socket.remotePort;
void set _owner(owner) {
(_socket as dynamic)._owner = owner;
}
int available() {
return _status != connectedStatus
? 0
: _secureFilter!.buffers![readPlaintextId].length;
}
Future<RawSecureSocket> 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 new ArgumentError(
"Invalid length parameter in SecureSocket.read (length: $length)");
}
if (_closedRead) {
throw new 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");
}
static int _fixOffset(int? offset) => offset ?? 0;
// Write the data to the socket, and schedule the filter to encrypt it.
int write(List<int> data, [int offset = 0, int? bytes]) {
if (bytes != null && bytes < 0) {
throw new ArgumentError(
"Invalid bytes parameter in SecureSocket.read (bytes: $bytes)");
}
// TODO(40614): Remove once non-nullability is sound.
offset = _fixOffset(offset);
if (offset < 0) {
throw new ArgumentError(
"Invalid offset parameter in SecureSocket.read (offset: $offset)");
}
if (_closedWrite) {
_controller.addError(new 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<SocketControlMessage> controlMessages, List<int> 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;
if (_filterStatus.readEmpty) {
_reportError(
new HandshakeException('Connection terminated during handshake'),
null);
} else {
await _secureHandshake();
}
}
}
Future<void> _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);
}
}
void renegotiate(
{bool useSessionCache = true,
bool requestClientCertificate = false,
bool requireClientCertificate = false}) {
if (_status != connectedStatus) {
throw new HandshakeException(
"Called renegotiate on a non-connected socket");
}
_secureFilter!.renegotiate(
useSessionCache, requestClientCertificate, requireClientCertificate);
_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<void> _scheduleFilter() {
_filterPending = true;
return _tryFilter();
}
Future<void> _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 new 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<int>? _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 = new List<dynamic>.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);
if (response.length == 2) {
if (wasInHandshake) {
// If we're in handshake, throw a handshake error.
_reportError(
new HandshakeException('${response[1]} error ${response[0]}'),
null);
} else {
// If we're connected, throw a TLS error.
_reportError(
new TlsException('${response[1]} error ${response[0]}'), null);
}
}
int start(int index) => response[2 * index];
int end(int index) => response[2 * index + 1];
_FilterStatus status = new _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", "set")
List<int>? 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 = new 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<int> 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<int>? 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<bool> handshake();
String? selectedProtocol();
void rehandshake();
void renegotiate(bool useSessionCache, bool requestClientCertificate,
bool requireClientCertificate);
void init();
X509Certificate? get peerCertificate;
int processBuffer(int bufferIndex);
void registerBadCertificateCallback(Function 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.
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 = new 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);
}