blob: 8ad69d370c7136cb962cea6b6d1d175bb1a5d114 [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 high-level class for communicating securely over a TCP socket, using
* TLS and SSL. The [SecureSocket] exposes both a [Stream] and an
* [IOSink] interface, making it ideal for using together with
* other [Stream]s.
*/
abstract class SecureSocket implements Socket {
external factory SecureSocket._(RawSecureSocket rawSocket);
/**
* Constructs a new secure client socket and connect it to the given
* [host] on port [port]. The returned Future will complete with a
* [SecureSocket] that is connected and ready for subscription.
*
* If [sendClientCertificate] is set to true, the socket will send a client
* certificate if one is requested by the server.
*
* If [certificateName] is the nickname of a certificate in the certificate
* database, that certificate will be sent.
*
* If [certificateName] is null, which is the usual use case, an
* appropriate certificate will be searched for in the database and
* sent automatically, based on what the server says it will accept.
*
* [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.
*/
static Future<SecureSocket> connect(
host,
int port,
{bool sendClientCertificate: false,
String certificateName,
bool onBadCertificate(X509Certificate certificate)}) {
return RawSecureSocket.connect(host,
port,
sendClientCertificate: sendClientCertificate,
certificateName: certificateName,
onBadCertificate: onBadCertificate)
.then((rawSocket) => new SecureSocket._(rawSocket));
}
/**
* 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
* `pause` on this subscription before starting TLS handshake is
* the right thing to do.
*
* 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].
*
* 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,
bool sendClientCertificate: false,
String certificateName,
bool onBadCertificate(X509Certificate certificate)}) {
var completer = new Completer();
(socket as dynamic)._detachRaw()
.then((detachedRaw) {
return RawSecureSocket.secure(
detachedRaw[0],
subscription: detachedRaw[1],
host: host,
sendClientCertificate: sendClientCertificate,
onBadCertificate: onBadCertificate);
})
.then((raw) {
completer.complete(new SecureSocket._(raw));
});
return completer.future;
}
/**
* 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
* [: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,
String certificateName,
{List<int> bufferedData,
bool requestClientCertificate: false,
bool requireClientCertificate: false}) {
var completer = new Completer();
(socket as dynamic)._detachRaw()
.then((detachedRaw) {
return RawSecureSocket.secureServer(
detachedRaw[0],
certificateName,
subscription: detachedRaw[1],
bufferedData: bufferedData,
requestClientCertificate: requestClientCertificate,
requireClientCertificate: requireClientCertificate);
})
.then((raw) {
completer.complete(new SecureSocket._(raw));
});
return completer.future;
}
/**
* Get 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 it is the client end,
* [peerCertificate] will return the server's certificate.
*/
X509Certificate get peerCertificate;
/**
* 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});
/**
* Initializes the NSS library. If [initialize] is not called, the library
* is automatically initialized as if [initialize] were called with no
* arguments. If [initialize] is called more than once, or called after
* automatic initialization has happened (when a secure connection is made),
* then a TlsException is thrown.
*
* The optional argument [database] is the path to a certificate database
* directory containing root certificates for verifying certificate paths on
* client connections, and server certificates to provide on server
* connections. The argument [password] should be used when creating
* secure server sockets, to allow the private key of the server
* certificate to be fetched. If [useBuiltinRoots] is true (the default),
* then a built-in set of root certificates for trusted certificate
* authorities is merged with the certificates in the database.
* The list of built-in root certificates, and documentation about this
* default database, is available at
* http://www.mozilla.org/projects/security/certs/included/ .
*
* If the [database] argument is omitted, then only the
* builtin root certificates are used. If [useBuiltinRoots] is also false,
* then no certificates are available.
*
* Examples:
* 1) Use only the builtin root certificates:
* SecureSocket.initialize(); or
*
* 2) Use a specified database directory and the builtin roots:
* SecureSocket.initialize(database: 'path/to/my/database',
* password: 'my_password');
*
* 3) Use a specified database directory, without builtin roots:
* SecureSocket.initialize(database: 'path/to/my/database',
* password: 'my_password'.
* useBuiltinRoots: false);
*
* The database should be an NSS certificate database directory
* containing a cert9.db file, not a cert8.db file. This version of
* the database can be created using the NSS certutil tool with "sql:" in
* front of the absolute path of the database directory, or setting the
* environment variable [[NSS_DEFAULT_DB_TYPE]] to "sql".
*/
external static void initialize({String database,
String password,
bool useBuiltinRoots: true});
}
/**
* 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 certificate database provided in SecureSocket.initialize, and/or
* the default built-in root certificates.
*/
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 certificate
* database provided in [SecureSocket.initialize], and/or the default built-in
* root certificates. If [sendClientCertificate] is
* set to true, the socket will send a client certificate if one is
* requested by the server. If [certificateName] is the nickname of
* a certificate in the certificate database, that certificate will be sent.
* If [certificateName] is null, which is the usual use case, an
* appropriate certificate will be searched for in the database and
* sent automatically, based on what the server says it will accept.
*
* [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.
*/
static Future<RawSecureSocket> connect(
host,
int port,
{bool sendClientCertificate: false,
String certificateName,
bool onBadCertificate(X509Certificate certificate)}) {
return _RawSecureSocket.connect(
host,
port,
certificateName,
is_server: false,
sendClientCertificate: sendClientCertificate,
onBadCertificate: onBadCertificate);
}
/**
* 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 socket
* will take over the subscription and process any subsequent
* events. In most cases calling `pause` on this subscription before
* starting TLS handshake is the right thing to do.
*
* 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].
*
* 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 subscription,
host,
bool sendClientCertificate: false,
String certificateName,
bool onBadCertificate(X509Certificate certificate)}) {
socket.readEventsEnabled = false;
socket.writeEventsEnabled = false;
return _RawSecureSocket.connect(
host != null ? host : socket.address,
socket.port,
certificateName,
is_server: false,
socket: socket,
subscription: subscription,
sendClientCertificate: sendClientCertificate,
onBadCertificate: onBadCertificate);
}
/**
* 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 secure socket
* will take over the subscription and process any subsequent
* events.
*
* 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,
String certificateName,
{StreamSubscription subscription,
List<int> bufferedData,
bool requestClientCertificate: false,
bool requireClientCertificate: false}) {
socket.readEventsEnabled = false;
socket.writeEventsEnabled = false;
return _RawSecureSocket.connect(
socket.address,
socket.remotePort,
certificateName,
is_server: true,
socket: socket,
subscription: subscription,
bufferedData: bufferedData,
requestClientCertificate: requestClientCertificate,
requireClientCertificate: requireClientCertificate);
}
/**
* 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;
}
/**
* X509Certificate represents an SSL certificate, with accessors to
* get the fields of the certificate.
*/
class X509Certificate {
X509Certificate(this.subject,
this.issuer,
this.startValidity,
this.endValidity);
final String subject;
final String issuer;
final DateTime startValidity;
final DateTime 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 final int HANDSHAKE = 201;
static final int CONNECTED = 202;
static final int CLOSED = 203;
// Buffer identifiers.
// These must agree with those in the native C++ implementation.
static final int READ_PLAINTEXT = 0;
static final int WRITE_PLAINTEXT = 1;
static final int READ_ENCRYPTED = 2;
static final int WRITE_ENCRYPTED = 3;
static final int NUM_BUFFERS = 4;
// Is a buffer identifier for an encrypted buffer?
static bool _isBufferEncrypted(int identifier) => identifier >= READ_ENCRYPTED;
RawSocket _socket;
final Completer<_RawSecureSocket> _handshakeComplete =
new Completer<_RawSecureSocket>();
StreamController<RawSocketEvent> _controller;
Stream<RawSocketEvent> _stream;
StreamSubscription<RawSocketEvent> _socketSubscription;
List<int> _bufferedData;
int _bufferedDataIndex = 0;
final InternetAddress address;
final bool is_server;
final String certificateName;
final bool requestClientCertificate;
final bool requireClientCertificate;
final bool sendClientCertificate;
final Function onBadCertificate;
var _status = HANDSHAKE;
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.
Completer _closeCompleter = new Completer(); // The network socket is gone.
_FilterStatus _filterStatus = new _FilterStatus();
bool _connectPending = true;
bool _filterPending = false;
bool _filterActive = false;
_SecureFilter _secureFilter = new _SecureFilter();
int _filterPointer;
static Future<_RawSecureSocket> connect(
host,
int requestedPort,
String certificateName,
{bool is_server,
RawSocket socket,
StreamSubscription subscription,
List<int> bufferedData,
bool requestClientCertificate: false,
bool requireClientCertificate: false,
bool sendClientCertificate: false,
bool onBadCertificate(X509Certificate certificate)}) {
var future;
_verifyFields(host, requestedPort, certificateName, is_server,
requestClientCertificate, requireClientCertificate,
sendClientCertificate, onBadCertificate);
if (host is String) {
if (socket != null) {
future = new Future.value(
(socket.address as dynamic)._cloneWithNewHost(host));
} else {
future = InternetAddress.lookup(host).then((addrs) => addrs.first);
}
} else {
future = new Future.value(host);
}
return future.then((addr) {
return new _RawSecureSocket(addr,
requestedPort,
certificateName,
is_server,
socket,
subscription,
bufferedData,
requestClientCertificate,
requireClientCertificate,
sendClientCertificate,
onBadCertificate)
._handshakeComplete.future;
});
}
_RawSecureSocket(
InternetAddress this.address,
int requestedPort,
String this.certificateName,
bool this.is_server,
RawSocket socket,
StreamSubscription this._socketSubscription,
List<int> this._bufferedData,
bool this.requestClientCertificate,
bool this.requireClientCertificate,
bool this.sendClientCertificate,
bool this.onBadCertificate(X509Certificate certificate)) {
_controller = new StreamController<RawSocketEvent>(
sync: true,
onListen: _onSubscriptionStateChange,
onPause: _onPauseStateChange,
onResume: _onPauseStateChange,
onCancel: _onSubscriptionStateChange);
_stream = _controller.stream;
// Throw an ArgumentError if any field is invalid. After this, all
// errors will be reported through the future or the stream.
_secureFilter.init();
_filterPointer = _secureFilter._pointer();
_secureFilter.registerHandshakeCompleteCallback(
_secureHandshakeCompleteHandler);
if (onBadCertificate != null) {
_secureFilter.registerBadCertificateCallback(_onBadCertificateWrapper);
}
var futureSocket;
if (socket == null) {
futureSocket = RawSocket.connect(address, requestedPort);
} else {
futureSocket = new Future.value(socket);
}
futureSocket.then((rawSocket) {
_socket = rawSocket;
_socket.readEventsEnabled = true;
_socket.writeEventsEnabled = false;
if (_socketSubscription == null) {
// If a current subscription is provided use this otherwise
// create a new one.
_socketSubscription = _socket.listen(_eventDispatcher,
onError: _reportError,
onDone: _doneHandler);
} else {
_socketSubscription.onData(_eventDispatcher);
_socketSubscription.onError(_reportError);
_socketSubscription.onDone(_doneHandler);
}
_secureFilter.connect(address.host,
(address as dynamic)._sockaddr_storage,
port,
is_server,
certificateName,
requestClientCertificate ||
requireClientCertificate,
requireClientCertificate,
sendClientCertificate);
_secureHandshake();
})
.catchError(_reportError);
}
StreamSubscription listen(void onData(RawSocketEvent data),
{Function onError,
void onDone(),
bool cancelOnError}) {
_sendWriteEvent();
return _stream.listen(onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError);
}
static void _verifyFields(host,
int requestedPort,
String certificateName,
bool is_server,
bool requestClientCertificate,
bool requireClientCertificate,
bool sendClientCertificate,
Function onBadCertificate) {
if (host is! String && host is! InternetAddress) {
throw new ArgumentError("host is not a String or an InternetAddress");
}
if (requestedPort is! int) {
throw new ArgumentError("requestedPort is not an int");
}
if (requestedPort < 0 || requestedPort > 65535) {
throw new ArgumentError("requestedPort is not in the range 0..65535");
}
if (certificateName != null && certificateName is! String) {
throw new ArgumentError("certificateName is not null or a String");
}
if (certificateName == null && is_server) {
throw new ArgumentError("certificateName is null on a server");
}
if (requestClientCertificate is! bool) {
throw new ArgumentError("requestClientCertificate is not a bool");
}
if (requireClientCertificate is! bool) {
throw new ArgumentError("requireClientCertificate is not a bool");
}
if (sendClientCertificate is! bool) {
throw new ArgumentError("sendClientCertificate is not a bool");
}
if (onBadCertificate != null && onBadCertificate is! Function) {
throw new ArgumentError("onBadCertificate is not null or a Function");
}
}
int get port => _socket.port;
String get remoteHost => _socket.remoteHost;
int get remotePort => _socket.remotePort;
int available() {
if (_status != CONNECTED) return 0;
return _secureFilter.buffers[READ_PLAINTEXT].length;
}
Future<RawSecureSocket> close() {
shutdown(SocketDirection.BOTH);
return _closeCompleter.future;
}
void _completeCloseCompleter([dummy]) {
if (!_closeCompleter.isCompleted) _closeCompleter.complete(this);
}
void _close() {
_closedWrite = true;
_closedRead = true;
if (_socket != null) {
_socket.close().then(_completeCloseCompleter);
} else {
_completeCloseCompleter();
}
_socketClosedWrite = true;
_socketClosedRead = true;
if (!_filterActive && _secureFilter != null) {
_secureFilter.destroy();
_secureFilter = null;
}
if (_socketSubscription != null) {
_socketSubscription.cancel();
}
_controller.close();
_status = CLOSED;
}
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();
}
List<int> read([int length]) {
if (length != null && (length is! int || 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 != CONNECTED) {
return null;
}
var result = _secureFilter.buffers[READ_PLAINTEXT].read(length);
_scheduleFilter();
return result;
}
// Write the data to the socket, and schedule the filter to encrypt it.
int write(List<int> data, [int offset, int bytes]) {
if (bytes != null && (bytes is! int || bytes < 0)) {
throw new ArgumentError(
"Invalid bytes parameter in SecureSocket.read (bytes: $bytes)");
}
if (offset != null && (offset is! int || 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 != CONNECTED) return 0;
if (offset == null) offset = 0;
if (bytes == null) bytes = data.length - offset;
int written =
_secureFilter.buffers[WRITE_PLAINTEXT].write(data, offset, bytes);
if (written > 0) {
_filterStatus.writeEmpty = false;
}
_scheduleFilter();
return written;
}
X509Certificate get peerCertificate => _secureFilter.peerCertificate;
bool _onBadCertificateWrapper(X509Certificate certificate) {
if (onBadCertificate == null) return false;
var result = onBadCertificate(certificate);
if (result is bool) return result;
throw new ArgumentError(
"onBadCertificate callback returned non-boolean $result");
}
bool setOption(SocketOption option, bool enabled) {
if (_socket == null) return false;
return _socket.setOption(option, enabled);
}
void _eventDispatcher(RawSocketEvent event) {
try {
if (event == RawSocketEvent.READ) {
_readHandler();
} else if (event == RawSocketEvent.WRITE) {
_writeHandler();
} else if (event == RawSocketEvent.READ_CLOSED) {
_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 == CLOSED) {
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() {
if (_status == CONNECTED) {
if (_closedRead) return;
_socketClosedRead = true;
if (_filterStatus.readEmpty) {
_closedRead = true;
_controller.add(RawSocketEvent.READ_CLOSED);
if (_socketClosedWrite) {
_close();
}
} else {
_scheduleFilter();
}
} else if (_status == HANDSHAKE) {
_socketClosedRead = true;
if (_filterStatus.readEmpty) {
_reportError(
new HandshakeException('Connection terminated during handshake'),
null);
} else {
_secureHandshake();
}
}
}
void _secureHandshake() {
try {
_secureFilter.handshake();
_filterStatus.writeEmpty = false;
_readSocket();
_writeSocket();
_scheduleFilter();
} catch (e, stackTrace) {
_reportError(e, stackTrace);
}
}
void renegotiate({bool useSessionCache: true,
bool requestClientCertificate: false,
bool requireClientCertificate: false}) {
if (_status != CONNECTED) {
throw new HandshakeException(
"Called renegotiate on a non-connected socket");
}
_secureFilter.renegotiate(useSessionCache,
requestClientCertificate,
requireClientCertificate);
_status = HANDSHAKE;
_filterStatus.writeEmpty = false;
_scheduleFilter();
}
void _secureHandshakeCompleteHandler() {
_status = CONNECTED;
if (_connectPending) {
_connectPending = false;
// We don't want user code to run synchronously in this callback.
Timer.run(() => _handshakeComplete.complete(this));
}
}
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?
}
}
void _scheduleFilter() {
_filterPending = true;
_tryFilter();
}
void _tryFilter() {
if (_status == CLOSED) return;
if (_filterPending && !_filterActive) {
_filterActive = true;
_filterPending = false;
_pushAllFilterStages().then((status) {
_filterStatus = status;
_filterActive = false;
if (_status == CLOSED) {
_secureFilter.destroy();
_secureFilter = null;
return;
}
if (_filterStatus.writeEmpty && _closedWrite && !_socketClosedWrite) {
// Checks for and handles all cases of partially closed sockets.
shutdown(SocketDirection.SEND);
if (_status == CLOSED) return;
}
if (_filterStatus.readEmpty && _socketClosedRead && !_closedRead) {
if (_status == HANDSHAKE) {
_secureFilter.handshake();
if (_status == HANDSHAKE) {
throw new HandshakeException(
'Connection terminated during handshake');
}
}
_closeHandler();
}
if (_status == CLOSED) return;
if (_filterStatus.progress) {
_filterPending = true;
if (_filterStatus.writePlaintextNoLongerFull) _sendWriteEvent();
if (_filterStatus.readEncryptedNoLongerFull) _readSocket();
if (_filterStatus.writeEncryptedNoLongerEmpty) _writeSocket();
if (_filterStatus.readPlaintextNoLongerEmpty) _scheduleReadEvent();
if (_status == HANDSHAKE) _secureHandshake();
}
_tryFilter();
}).catchError(_reportError);
}
}
List<int> _readSocketOrBufferedData(int bytes) {
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 == CLOSED) return;
var buffer = _secureFilter.buffers[READ_ENCRYPTED];
if (buffer.writeFromSource(_readSocketOrBufferedData) > 0) {
_filterStatus.readEmpty = false;
}
}
void _writeSocket() {
if (_socketClosedWrite) return;
var buffer = _secureFilter.buffers[WRITE_ENCRYPTED];
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[READ_PLAINTEXT].isEmpty) {
_pendingReadEvent = true;
Timer.run(_sendReadEvent);
}
}
_sendReadEvent() {
_pendingReadEvent = false;
if (_readEventsEnabled &&
_pauseCount == 0 &&
_secureFilter != null &&
!_secureFilter.buffers[READ_PLAINTEXT].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[WRITE_PLAINTEXT].free > 0) {
_writeEventsEnabled = false;
_controller.add(RawSocketEvent.WRITE);
}
}
Future<_FilterStatus> _pushAllFilterStages() {
bool wasInHandshake = _status != CONNECTED;
List args = new List(2 + NUM_BUFFERS * 2);
args[0] = _filterPointer;
args[1] = wasInHandshake;
var bufs = _secureFilter.buffers;
for (var i = 0; i < NUM_BUFFERS; ++i) {
args[2 * i + 2] = bufs[i].start;
args[2 * i + 3] = bufs[i].end;
}
return _IOService.dispatch(_SSL_PROCESS_FILTER, args).then((response) {
if (response.length == 2) {
_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[WRITE_PLAINTEXT].isEmpty &&
start(WRITE_ENCRYPTED) == end(WRITE_ENCRYPTED);
// 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[READ_ENCRYPTED].isEmpty &&
start(READ_PLAINTEXT) == end(READ_PLAINTEXT);
_ExternalBuffer buffer = bufs[WRITE_PLAINTEXT];
int new_start = start(WRITE_PLAINTEXT);
if (new_start != buffer.start) {
status.progress = true;
if (buffer.free == 0) {
status.writePlaintextNoLongerFull = true;
}
buffer.start = new_start;
}
buffer = bufs[READ_ENCRYPTED];
new_start = start(READ_ENCRYPTED);
if (new_start != buffer.start) {
status.progress = true;
if (buffer.free == 0) {
status.readEncryptedNoLongerFull = true;
}
buffer.start = new_start;
}
buffer = bufs[WRITE_ENCRYPTED];
int new_end = end(WRITE_ENCRYPTED);
if (new_end != buffer.end) {
status.progress = true;
if (buffer.length == 0) {
status.writeEncryptedNoLongerEmpty = true;
}
buffer.end = new_end;
}
buffer = bufs[READ_PLAINTEXT];
new_end = end(READ_PLAINTEXT);
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 {
_ExternalBuffer(this.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 {
if (start > end) return size + end - start;
return end - start;
}
int get linearLength {
if (start > end) return size - start;
return end - start;
}
int get free {
if (start > end) return start - end - 1;
return size + start - end - 1;
}
int get linearFree {
if (start > end) return start - end - 1;
if (start == 0) return size - end - 1;
return size - end;
}
List<int> read(int bytes) {
if (bytes == null) {
bytes = length;
} else {
bytes = min(bytes, length);
}
if (bytes == 0) return null;
List<int> result = new Uint8List(bytes);
int bytesRead = 0;
// Loop over zero, one, or two linear data ranges.
while (bytesRead < bytes) {
int toRead = min(bytes, 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) 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;
}
}
}
List data; // This will be a ExternalByteArray, backed by C allocated data.
int start;
int end;
final size;
}
abstract class _SecureFilter {
external factory _SecureFilter();
void connect(String hostName,
Uint8List addr,
int port,
bool is_server,
String certificateName,
bool requestClientCertificate,
bool requireClientCertificate,
bool sendClientCertificate);
void destroy();
void handshake();
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);
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;
const TlsException([String message = "",
OSError osError = null])
: this._("TlsException", message, osError);
const TlsException._(String this.type,
String this.message,
OSError this.osError);
String toString() {
StringBuffer sb = new StringBuffer();
sb.write(type);
if (!message.isEmpty) {
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.
*/
class HandshakeException extends TlsException {
const HandshakeException([String message = "",
OSError osError = null])
: 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 {
const CertificateException([String message = "",
OSError osError = null])
: super._("CertificateException", message, osError);
}