blob: d7218bdf6617a74a0c0e56f843a2e8b9cd9a7397 [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 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.
*
* [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),
List<String> supportedProtocols,
Duration timeout}) {
return RawSecureSocket.connect(host, port,
context: context,
onBadCertificate: onBadCertificate,
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),
List<String> supportedProtocols}) {
return RawSecureSocket.startConnect(host, port,
context: context,
onBadCertificate: onBadCertificate,
supportedProtocols: supportedProtocols)
.then((rawState) {
Future<SecureSocket> socket =
rawState.socket.then((rawSocket) => new SecureSocket._(rawSocket));
return new ConnectionTask<SecureSocket>._(
socket: socket, onCancel: rawState._onCancel);
});
}
/**
* 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.
*
* 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].
*
* 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)}) {
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);
}).then<SecureSocket>((raw) => new SecureSocket._(raw));
}
/**
* 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, 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));
}
/**
* 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;
/**
* 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;
/**
* 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});
}
/**
* 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.
*
* [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),
List<String> supportedProtocols,
Duration timeout}) {
_RawSecureSocket._verifyFields(
host, port, false, false, false, onBadCertificate);
return RawSocket.connect(host, port, timeout: timeout).then((socket) {
return secure(socket,
context: context,
onBadCertificate: onBadCertificate,
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),
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,
supportedProtocols: supportedProtocols);
});
return new ConnectionTask<RawSecureSocket>._(
socket: socket, onCancel: rawState._onCancel);
});
}
/**
* 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].
*
* 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),
List<String> supportedProtocols}) {
socket.readEventsEnabled = false;
socket.writeEventsEnabled = false;
return _RawSecureSocket.connect(
host != null ? host : socket.address.host, socket.port,
is_server: false,
socket: socket,
subscription: subscription,
context: context,
onBadCertificate: onBadCertificate,
supportedProtocols: supportedProtocols);
}
/**
* 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,
context: context,
is_server: true,
socket: socket,
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.
*/
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;
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;
SecurityContext context;
final bool requestClientCertificate;
final bool requireClientCertificate;
final Function onBadCertificate;
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 is_server,
SecurityContext context,
RawSocket socket,
StreamSubscription<RawSocketEvent> subscription,
List<int> bufferedData,
bool requestClientCertificate: false,
bool requireClientCertificate: false,
bool onBadCertificate(X509Certificate certificate),
List<String> supportedProtocols}) {
_verifyFields(host, requestedPort, is_server, requestClientCertificate,
requireClientCertificate, onBadCertificate);
if (host is InternetAddress) host = host.host;
InternetAddress address = socket.address;
if (host != null) {
address = InternetAddress._cloneWithNewHost(address, host);
}
return new _RawSecureSocket(
address,
requestedPort,
is_server,
context,
socket,
subscription,
bufferedData,
requestClientCertificate,
requireClientCertificate,
onBadCertificate,
supportedProtocols)
._handshakeComplete
.future;
}
_RawSecureSocket(
this.address,
int requestedPort,
this.is_server,
this.context,
this._socket,
this._socketSubscription,
this._bufferedData,
this.requestClientCertificate,
this.requireClientCertificate,
this.onBadCertificate,
List<String> supportedProtocols) {
if (context == null) {
context = SecurityContext.defaultContext;
}
_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();
_secureFilter
.registerHandshakeCompleteCallback(_secureHandshakeCompleteHandler);
if (onBadCertificate != null) {
_secureFilter.registerBadCertificateCallback(_onBadCertificateWrapper);
}
_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 {
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,
is_server,
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 _stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
static void _verifyFields(
host,
int requestedPort,
bool is_server,
bool requestClientCertificate,
bool requireClientCertificate,
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 (requestClientCertificate is! bool) {
throw new ArgumentError("requestClientCertificate is not a bool");
}
if (requireClientCertificate is! bool) {
throw new ArgumentError("requireClientCertificate 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;
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;
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 = 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();
}
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 != connectedStatus) {
return null;
}
var result = _secureFilter.buffers[readPlaintextId].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 != connectedStatus) return 0;
if (offset == null) offset = 0;
if (bytes == null) bytes = data.length - offset;
int written =
_secureFilter.buffers[writePlaintextId].write(data, offset, bytes);
if (written > 0) {
_filterStatus.writeEmpty = false;
}
_scheduleFilter();
return written;
}
X509Certificate get peerCertificate => _secureFilter.peerCertificate;
String get selectedProtocol => _selectedProtocol;
bool _onBadCertificateWrapper(X509Certificate certificate) {
if (onBadCertificate == null) return false;
var result = onBadCertificate(certificate);
if (result is bool) return result;
throw new HandshakeException(
"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.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() {
if (_status == connectedStatus) {
if (_closedRead) return;
_socketClosedRead = true;
if (_filterStatus.readEmpty) {
_closedRead = true;
_controller.add(RawSocketEvent.readClosed);
if (_socketClosedWrite) {
_close();
}
} else {
_scheduleFilter();
}
} else if (_status == handshakeStatus) {
_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 != 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?
}
}
void _scheduleFilter() {
_filterPending = true;
_tryFilter();
}
void _tryFilter() {
if (_status == closedStatus) {
return;
}
if (_filterPending && !_filterActive) {
_filterActive = true;
_filterPending = false;
_pushAllFilterStages().then((status) {
_filterStatus = status;
_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) {
_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 == 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() {
bool wasInHandshake = _status != connectedStatus;
List args = new List(2 + bufferCount * 2);
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;
}
return _IOService._dispatch(_IOService.sslProcessFilter, args)
.then((response) {
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 size;
_ExternalBuffer(this.size) {
start = 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;
}
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 - 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 is_server,
bool requestClientCertificate,
bool requireClientCertificate,
Uint8List protocols);
void destroy();
void 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);
// 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 = null])
: this._("TlsException", message, osError);
const TlsException._(this.type, this.message, 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 {
@pragma("vm.entry-point")
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 {
@pragma("vm.entry-point")
const CertificateException([String message = "", OSError osError = null])
: super._("CertificateException", message, osError);
}