| Index: tool/input_sdk/lib/io/secure_socket.dart
|
| diff --git a/tool/input_sdk/lib/io/secure_socket.dart b/tool/input_sdk/lib/io/secure_socket.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..380a8c466abd282facda77184ed96728b4e0003b
|
| --- /dev/null
|
| +++ b/tool/input_sdk/lib/io/secure_socket.dart
|
| @@ -0,0 +1,1263 @@
|
| +// 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.
|
| + */
|
| + static Future<SecureSocket> connect(
|
| + host,
|
| + int port,
|
| + {SecurityContext context,
|
| + bool onBadCertificate(X509Certificate certificate),
|
| + List<String> supportedProtocols}) {
|
| + return RawSecureSocket.connect(host,
|
| + port,
|
| + context: context,
|
| + onBadCertificate: onBadCertificate,
|
| + supportedProtocols: supportedProtocols)
|
| + .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,
|
| + SecurityContext context,
|
| + bool onBadCertificate(X509Certificate certificate)}) {
|
| + var completer = new Completer();
|
| + (socket as dynamic)._detachRaw()
|
| + .then((detachedRaw) {
|
| + return RawSecureSocket.secure(
|
| + detachedRaw[0],
|
| + subscription: detachedRaw[1],
|
| + host: host,
|
| + context: context,
|
| + 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,
|
| + SecurityContext context,
|
| + {List<int> bufferedData,
|
| + bool requestClientCertificate: false,
|
| + bool requireClientCertificate: false,
|
| + List<String> supportedProtocols}) {
|
| + var completer = new Completer();
|
| + (socket as dynamic)._detachRaw()
|
| + .then((detachedRaw) {
|
| + return RawSecureSocket.secureServer(
|
| + detachedRaw[0],
|
| + context,
|
| + subscription: detachedRaw[1],
|
| + bufferedData: bufferedData,
|
| + requestClientCertificate: requestClientCertificate,
|
| + requireClientCertificate: requireClientCertificate,
|
| + supportedProtocols: supportedProtocols);
|
| + })
|
| + .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;
|
| +
|
| + /**
|
| + * Get the protocol which was selected during protocol negotiation.
|
| + */
|
| + 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.
|
| + */
|
| + static Future<RawSecureSocket> connect(
|
| + host,
|
| + int port,
|
| + {SecurityContext context,
|
| + bool onBadCertificate(X509Certificate certificate),
|
| + List<String> supportedProtocols}) {
|
| + _RawSecureSocket._verifyFields(
|
| + host,
|
| + port,
|
| + false,
|
| + false,
|
| + false,
|
| + onBadCertificate);
|
| + return RawSocket.connect(host, port)
|
| + .then((socket) {
|
| + return secure(socket,
|
| + context: context,
|
| + onBadCertificate: onBadCertificate,
|
| + supportedProtocols: supportedProtocols);
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * 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 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 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;
|
| +
|
| + /**
|
| + * Get the protocol which was selected during protocol negotiation.
|
| + */
|
| + String get selectedProtocol;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * X509Certificate represents an SSL certificate, with accessors to
|
| + * get the fields of the certificate.
|
| + */
|
| +abstract class X509Certificate {
|
| + external factory X509Certificate._();
|
| +
|
| + 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 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;
|
| + SecurityContext context;
|
| + final bool requestClientCertificate;
|
| + final bool requireClientCertificate;
|
| + 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();
|
| + String _selectedProtocol;
|
| +
|
| + static Future<_RawSecureSocket> connect(
|
| + dynamic/*String|InternetAddress*/ host,
|
| + int requestedPort,
|
| + {bool is_server,
|
| + SecurityContext context,
|
| + RawSocket socket,
|
| + StreamSubscription 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,
|
| + RawSocket this._socket,
|
| + this._socketSubscription,
|
| + this._bufferedData,
|
| + this.requestClientCertificate,
|
| + this.requireClientCertificate,
|
| + this.onBadCertificate(X509Certificate certificate),
|
| + 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 READ_CLOSED during the handshake.
|
| + dynamic s = _socket; // Cast to dynamic to avoid warning.
|
| + if (s._socket.closedReadEventSent) {
|
| + _eventDispatcher(RawSocketEvent.READ_CLOSED);
|
| + }
|
| + _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 != CONNECTED ? 0
|
| + : _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;
|
| +
|
| + 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.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;
|
| + 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 == CLOSED) {
|
| + return;
|
| + }
|
| + if (_filterPending && !_filterActive) {
|
| + _filterActive = true;
|
| + _filterPending = false;
|
| + _pushAllFilterStages().then((status) {
|
| + _filterStatus = status;
|
| + _filterActive = false;
|
| + if (_status == CLOSED) {
|
| + _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 == 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.writeEncryptedNoLongerEmpty) {
|
| + _writeSocket();
|
| + }
|
| + if (_filterStatus.writePlaintextNoLongerFull) {
|
| + _sendWriteEvent();
|
| + }
|
| + if (_filterStatus.readEncryptedNoLongerFull) {
|
| + _readSocket();
|
| + }
|
| + 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;
|
| + } else {
|
| + _socket.readEventsEnabled = 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 (_status != CLOSED &&
|
| + _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] = _secureFilter._pointer();
|
| + 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) {
|
| + 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[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 {
|
| + List data; // This will be a ExternalByteArray, backed by C allocated data.
|
| + int start;
|
| + 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;
|
| +
|
| + 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 {
|
| + 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);
|
| +}
|
|
|