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); |
+} |