| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of dart.io; |
| |
| /** |
| * A high-level class for communicating securely over a TCP socket, using |
| * TLS and SSL. The [SecureSocket] exposes both a [Stream] and an |
| * [IOSink] interface, making it ideal for using together with |
| * other [Stream]s. |
| */ |
| abstract class SecureSocket implements Socket { |
| external factory SecureSocket._(RawSecureSocket rawSocket); |
| |
| /** |
| * Constructs a new secure client socket and connect it to the given |
| * [host] on port [port]. The returned Future will complete with a |
| * [SecureSocket] that is connected and ready for subscription. |
| * |
| * If [sendClientCertificate] is set to true, the socket will send a client |
| * certificate if one is requested by the server. |
| * |
| * If [certificateName] is the nickname of a certificate in the certificate |
| * database, that certificate will be sent. |
| * |
| * If [certificateName] is null, which is the usual use case, an |
| * appropriate certificate will be searched for in the database and |
| * sent automatically, based on what the server says it will accept. |
| * |
| * [onBadCertificate] is an optional handler for unverifiable certificates. |
| * The handler receives the [X509Certificate], and can inspect it and |
| * decide (or let the user decide) whether to accept |
| * the connection or not. The handler should return true |
| * to continue the [SecureSocket] connection. |
| */ |
| static Future<SecureSocket> connect( |
| host, |
| int port, |
| {bool sendClientCertificate: false, |
| String certificateName, |
| bool onBadCertificate(X509Certificate certificate)}) { |
| return RawSecureSocket.connect(host, |
| port, |
| sendClientCertificate: sendClientCertificate, |
| certificateName: certificateName, |
| onBadCertificate: onBadCertificate) |
| .then((rawSocket) => new SecureSocket._(rawSocket)); |
| } |
| |
| /** |
| * Takes an already connected [socket] and starts client side TLS |
| * handshake to make the communication secure. When the returned |
| * future completes the [SecureSocket] has completed the TLS |
| * handshake. Using this function requires that the other end of the |
| * connection is prepared for TLS handshake. |
| * |
| * If the [socket] already has a subscription, this subscription |
| * will no longer receive and events. In most cases calling |
| * `pause` on this subscription before starting TLS handshake is |
| * the right thing to do. |
| * |
| * If the [host] argument is passed it will be used as the host name |
| * for the TLS handshake. If [host] is not passed the host name from |
| * the [socket] will be used. The [host] can be either a [String] or |
| * an [InternetAddress]. |
| * |
| * Calling this function will _not_ cause a DNS host lookup. If the |
| * [host] passed is a [String] the [InternetAddress] for the |
| * resulting [SecureSocket] will have the passed in [host] as its |
| * host value and the internet address of the already connected |
| * socket as its address value. |
| * |
| * See [connect] for more information on the arguments. |
| * |
| */ |
| static Future<SecureSocket> secure( |
| Socket socket, |
| {host, |
| bool sendClientCertificate: false, |
| String certificateName, |
| bool onBadCertificate(X509Certificate certificate)}) { |
| var completer = new Completer(); |
| (socket as dynamic)._detachRaw() |
| .then((detachedRaw) { |
| return RawSecureSocket.secure( |
| detachedRaw[0], |
| subscription: detachedRaw[1], |
| host: host, |
| sendClientCertificate: sendClientCertificate, |
| onBadCertificate: onBadCertificate); |
| }) |
| .then((raw) { |
| completer.complete(new SecureSocket._(raw)); |
| }); |
| return completer.future; |
| } |
| |
| /** |
| * Takes an already connected [socket] and starts server side TLS |
| * handshake to make the communication secure. When the returned |
| * future completes the [SecureSocket] has completed the TLS |
| * handshake. Using this function requires that the other end of the |
| * connection is going to start the TLS handshake. |
| * |
| * If the [socket] already has a subscription, this subscription |
| * will no longer receive and events. In most cases calling |
| * [:pause:] on this subscription before starting TLS handshake is |
| * the right thing to do. |
| * |
| * If some of the data of the TLS handshake has already been read |
| * from the socket this data can be passed in the [bufferedData] |
| * parameter. This data will be processed before any other data |
| * available on the socket. |
| * |
| * See [SecureServerSocket.bind] for more information on the |
| * arguments. |
| * |
| */ |
| static Future<SecureSocket> secureServer( |
| Socket socket, |
| String certificateName, |
| {List<int> bufferedData, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false}) { |
| var completer = new Completer(); |
| (socket as dynamic)._detachRaw() |
| .then((detachedRaw) { |
| return RawSecureSocket.secureServer( |
| detachedRaw[0], |
| certificateName, |
| subscription: detachedRaw[1], |
| bufferedData: bufferedData, |
| requestClientCertificate: requestClientCertificate, |
| requireClientCertificate: requireClientCertificate); |
| }) |
| .then((raw) { |
| completer.complete(new SecureSocket._(raw)); |
| }); |
| return completer.future; |
| } |
| |
| /** |
| * Get the peer certificate for a connected SecureSocket. If this |
| * SecureSocket is the server end of a secure socket connection, |
| * [peerCertificate] will return the client certificate, or null, if no |
| * client certificate was received. If it is the client end, |
| * [peerCertificate] will return the server's certificate. |
| */ |
| X509Certificate get peerCertificate; |
| |
| /** |
| * Renegotiate an existing secure connection, renewing the session keys |
| * and possibly changing the connection properties. |
| * |
| * This repeats the SSL or TLS handshake, with options that allow clearing |
| * the session cache and requesting a client certificate. |
| */ |
| void renegotiate({bool useSessionCache: true, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false}); |
| |
| /** |
| * Initializes the NSS library. If [initialize] is not called, the library |
| * is automatically initialized as if [initialize] were called with no |
| * arguments. If [initialize] is called more than once, or called after |
| * automatic initialization has happened (when a secure connection is made), |
| * then a TlsException is thrown. |
| * |
| * The optional argument [database] is the path to a certificate database |
| * directory containing root certificates for verifying certificate paths on |
| * client connections, and server certificates to provide on server |
| * connections. The argument [password] should be used when creating |
| * secure server sockets, to allow the private key of the server |
| * certificate to be fetched. If [useBuiltinRoots] is true (the default), |
| * then a built-in set of root certificates for trusted certificate |
| * authorities is merged with the certificates in the database. |
| * The list of built-in root certificates, and documentation about this |
| * default database, is available at |
| * http://www.mozilla.org/projects/security/certs/included/ . |
| * |
| * If the [database] argument is omitted, then only the |
| * builtin root certificates are used. If [useBuiltinRoots] is also false, |
| * then no certificates are available. |
| * |
| * Examples: |
| * 1) Use only the builtin root certificates: |
| * SecureSocket.initialize(); or |
| * |
| * 2) Use a specified database directory and the builtin roots: |
| * SecureSocket.initialize(database: 'path/to/my/database', |
| * password: 'my_password'); |
| * |
| * 3) Use a specified database directory, without builtin roots: |
| * SecureSocket.initialize(database: 'path/to/my/database', |
| * password: 'my_password'. |
| * useBuiltinRoots: false); |
| * |
| * The database should be an NSS certificate database directory |
| * containing a cert9.db file, not a cert8.db file. This version of |
| * the database can be created using the NSS certutil tool with "sql:" in |
| * front of the absolute path of the database directory, or setting the |
| * environment variable [[NSS_DEFAULT_DB_TYPE]] to "sql". |
| */ |
| external static void initialize({String database, |
| String password, |
| bool useBuiltinRoots: true}); |
| } |
| |
| |
| /** |
| * RawSecureSocket provides a secure (SSL or TLS) network connection. |
| * Client connections to a server are provided by calling |
| * RawSecureSocket.connect. A secure server, created with |
| * RawSecureServerSocket, also returns RawSecureSocket objects representing |
| * the server end of a secure connection. |
| * The certificate provided by the server is checked |
| * using the certificate database provided in SecureSocket.initialize, and/or |
| * the default built-in root certificates. |
| */ |
| abstract class RawSecureSocket implements RawSocket { |
| /** |
| * Constructs a new secure client socket and connect it to the given |
| * host on the given port. The returned Future is completed with the |
| * RawSecureSocket when it is connected and ready for subscription. |
| * |
| * The certificate provided by the server is checked using the certificate |
| * database provided in [SecureSocket.initialize], and/or the default built-in |
| * root certificates. If [sendClientCertificate] is |
| * set to true, the socket will send a client certificate if one is |
| * requested by the server. If [certificateName] is the nickname of |
| * a certificate in the certificate database, that certificate will be sent. |
| * If [certificateName] is null, which is the usual use case, an |
| * appropriate certificate will be searched for in the database and |
| * sent automatically, based on what the server says it will accept. |
| * |
| * [onBadCertificate] is an optional handler for unverifiable certificates. |
| * The handler receives the [X509Certificate], and can inspect it and |
| * decide (or let the user decide) whether to accept |
| * the connection or not. The handler should return true |
| * to continue the [RawSecureSocket] connection. |
| */ |
| static Future<RawSecureSocket> connect( |
| host, |
| int port, |
| {bool sendClientCertificate: false, |
| String certificateName, |
| bool onBadCertificate(X509Certificate certificate)}) { |
| return _RawSecureSocket.connect( |
| host, |
| port, |
| certificateName, |
| is_server: false, |
| sendClientCertificate: sendClientCertificate, |
| onBadCertificate: onBadCertificate); |
| } |
| |
| /** |
| * Takes an already connected [socket] and starts client side TLS |
| * handshake to make the communication secure. When the returned |
| * future completes the [RawSecureSocket] has completed the TLS |
| * handshake. Using this function requires that the other end of the |
| * connection is prepared for TLS handshake. |
| * |
| * If the [socket] already has a subscription, pass the existing |
| * subscription in the [subscription] parameter. The secure socket |
| * will take over the subscription and process any subsequent |
| * events. In most cases calling `pause` on this subscription before |
| * starting TLS handshake is the right thing to do. |
| * |
| * If the [host] argument is passed it will be used as the host name |
| * for the TLS handshake. If [host] is not passed the host name from |
| * the [socket] will be used. The [host] can be either a [String] or |
| * an [InternetAddress]. |
| * |
| * Calling this function will _not_ cause a DNS host lookup. If the |
| * [host] passed is a [String] the [InternetAddress] for the |
| * resulting [SecureSocket] will have this passed in [host] as its |
| * host value and the internet address of the already connected |
| * socket as its address value. |
| * |
| * See [connect] for more information on the arguments. |
| * |
| */ |
| static Future<RawSecureSocket> secure( |
| RawSocket socket, |
| {StreamSubscription subscription, |
| host, |
| bool sendClientCertificate: false, |
| String certificateName, |
| bool onBadCertificate(X509Certificate certificate)}) { |
| socket.readEventsEnabled = false; |
| socket.writeEventsEnabled = false; |
| return _RawSecureSocket.connect( |
| host != null ? host : socket.address, |
| socket.port, |
| certificateName, |
| is_server: false, |
| socket: socket, |
| subscription: subscription, |
| sendClientCertificate: sendClientCertificate, |
| onBadCertificate: onBadCertificate); |
| } |
| |
| /** |
| * Takes an already connected [socket] and starts server side TLS |
| * handshake to make the communication secure. When the returned |
| * future completes the [RawSecureSocket] has completed the TLS |
| * handshake. Using this function requires that the other end of the |
| * connection is going to start the TLS handshake. |
| * |
| * If the [socket] already has a subscription, pass the existing |
| * subscription in the [subscription] parameter. The secure socket |
| * will take over the subscription and process any subsequent |
| * events. |
| * |
| * If some of the data of the TLS handshake has already been read |
| * from the socket this data can be passed in the [bufferedData] |
| * parameter. This data will be processed before any other data |
| * available on the socket. |
| * |
| * See [RawSecureServerSocket.bind] for more information on the |
| * arguments. |
| * |
| */ |
| static Future<RawSecureSocket> secureServer( |
| RawSocket socket, |
| String certificateName, |
| {StreamSubscription subscription, |
| List<int> bufferedData, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false}) { |
| socket.readEventsEnabled = false; |
| socket.writeEventsEnabled = false; |
| return _RawSecureSocket.connect( |
| socket.address, |
| socket.remotePort, |
| certificateName, |
| is_server: true, |
| socket: socket, |
| subscription: subscription, |
| bufferedData: bufferedData, |
| requestClientCertificate: requestClientCertificate, |
| requireClientCertificate: requireClientCertificate); |
| } |
| |
| /** |
| * Renegotiate an existing secure connection, renewing the session keys |
| * and possibly changing the connection properties. |
| * |
| * This repeats the SSL or TLS handshake, with options that allow clearing |
| * the session cache and requesting a client certificate. |
| */ |
| void renegotiate({bool useSessionCache: true, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false}); |
| |
| /** |
| * Get the peer certificate for a connected RawSecureSocket. If this |
| * RawSecureSocket is the server end of a secure socket connection, |
| * [peerCertificate] will return the client certificate, or null, if no |
| * client certificate was received. If it is the client end, |
| * [peerCertificate] will return the server's certificate. |
| */ |
| X509Certificate get peerCertificate; |
| } |
| |
| |
| /** |
| * X509Certificate represents an SSL certificate, with accessors to |
| * get the fields of the certificate. |
| */ |
| class X509Certificate { |
| X509Certificate(this.subject, |
| this.issuer, |
| this.startValidity, |
| this.endValidity); |
| final String subject; |
| final String issuer; |
| final DateTime startValidity; |
| final DateTime endValidity; |
| } |
| |
| |
| class _FilterStatus { |
| bool progress = false; // The filter read or wrote data to the buffers. |
| bool readEmpty = true; // The read buffers and decryption filter are empty. |
| bool writeEmpty = true; // The write buffers and encryption filter are empty. |
| // These are set if a buffer changes state from empty or full. |
| bool readPlaintextNoLongerEmpty = false; |
| bool writePlaintextNoLongerFull = false; |
| bool readEncryptedNoLongerFull = false; |
| bool writeEncryptedNoLongerEmpty = false; |
| |
| _FilterStatus(); |
| } |
| |
| |
| class _RawSecureSocket extends Stream<RawSocketEvent> |
| implements RawSecureSocket { |
| // Status states |
| static final int HANDSHAKE = 201; |
| static final int CONNECTED = 202; |
| static final int CLOSED = 203; |
| |
| // Buffer identifiers. |
| // These must agree with those in the native C++ implementation. |
| static final int READ_PLAINTEXT = 0; |
| static final int WRITE_PLAINTEXT = 1; |
| static final int READ_ENCRYPTED = 2; |
| static final int WRITE_ENCRYPTED = 3; |
| static final int NUM_BUFFERS = 4; |
| |
| // Is a buffer identifier for an encrypted buffer? |
| static bool _isBufferEncrypted(int identifier) => identifier >= READ_ENCRYPTED; |
| |
| RawSocket _socket; |
| final Completer<_RawSecureSocket> _handshakeComplete = |
| new Completer<_RawSecureSocket>(); |
| StreamController<RawSocketEvent> _controller; |
| Stream<RawSocketEvent> _stream; |
| StreamSubscription<RawSocketEvent> _socketSubscription; |
| List<int> _bufferedData; |
| int _bufferedDataIndex = 0; |
| final InternetAddress address; |
| final bool is_server; |
| final String certificateName; |
| final bool requestClientCertificate; |
| final bool requireClientCertificate; |
| final bool sendClientCertificate; |
| final Function onBadCertificate; |
| |
| var _status = HANDSHAKE; |
| bool _writeEventsEnabled = true; |
| bool _readEventsEnabled = true; |
| int _pauseCount = 0; |
| bool _pendingReadEvent = false; |
| bool _socketClosedRead = false; // The network socket is closed for reading. |
| bool _socketClosedWrite = false; // The network socket is closed for writing. |
| bool _closedRead = false; // The secure socket has fired an onClosed event. |
| bool _closedWrite = false; // The secure socket has been closed for writing. |
| Completer _closeCompleter = new Completer(); // The network socket is gone. |
| _FilterStatus _filterStatus = new _FilterStatus(); |
| bool _connectPending = true; |
| bool _filterPending = false; |
| bool _filterActive = false; |
| |
| _SecureFilter _secureFilter = new _SecureFilter(); |
| int _filterPointer; |
| |
| static Future<_RawSecureSocket> connect( |
| host, |
| int requestedPort, |
| String certificateName, |
| {bool is_server, |
| RawSocket socket, |
| StreamSubscription subscription, |
| List<int> bufferedData, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false, |
| bool sendClientCertificate: false, |
| bool onBadCertificate(X509Certificate certificate)}) { |
| var future; |
| _verifyFields(host, requestedPort, certificateName, is_server, |
| requestClientCertificate, requireClientCertificate, |
| sendClientCertificate, onBadCertificate); |
| if (host is String) { |
| if (socket != null) { |
| future = new Future.value( |
| (socket.address as dynamic)._cloneWithNewHost(host)); |
| } else { |
| future = InternetAddress.lookup(host).then((addrs) => addrs.first); |
| } |
| } else { |
| future = new Future.value(host); |
| } |
| return future.then((addr) { |
| return new _RawSecureSocket(addr, |
| requestedPort, |
| certificateName, |
| is_server, |
| socket, |
| subscription, |
| bufferedData, |
| requestClientCertificate, |
| requireClientCertificate, |
| sendClientCertificate, |
| onBadCertificate) |
| ._handshakeComplete.future; |
| }); |
| } |
| |
| _RawSecureSocket( |
| InternetAddress this.address, |
| int requestedPort, |
| String this.certificateName, |
| bool this.is_server, |
| RawSocket socket, |
| StreamSubscription this._socketSubscription, |
| List<int> this._bufferedData, |
| bool this.requestClientCertificate, |
| bool this.requireClientCertificate, |
| bool this.sendClientCertificate, |
| bool this.onBadCertificate(X509Certificate certificate)) { |
| _controller = new StreamController<RawSocketEvent>( |
| sync: true, |
| onListen: _onSubscriptionStateChange, |
| onPause: _onPauseStateChange, |
| onResume: _onPauseStateChange, |
| onCancel: _onSubscriptionStateChange); |
| _stream = _controller.stream; |
| // Throw an ArgumentError if any field is invalid. After this, all |
| // errors will be reported through the future or the stream. |
| _secureFilter.init(); |
| _filterPointer = _secureFilter._pointer(); |
| _secureFilter.registerHandshakeCompleteCallback( |
| _secureHandshakeCompleteHandler); |
| if (onBadCertificate != null) { |
| _secureFilter.registerBadCertificateCallback(_onBadCertificateWrapper); |
| } |
| var futureSocket; |
| if (socket == null) { |
| futureSocket = RawSocket.connect(address, requestedPort); |
| } else { |
| futureSocket = new Future.value(socket); |
| } |
| futureSocket.then((rawSocket) { |
| _socket = rawSocket; |
| _socket.readEventsEnabled = true; |
| _socket.writeEventsEnabled = false; |
| if (_socketSubscription == null) { |
| // If a current subscription is provided use this otherwise |
| // create a new one. |
| _socketSubscription = _socket.listen(_eventDispatcher, |
| onError: _reportError, |
| onDone: _doneHandler); |
| } else { |
| _socketSubscription.onData(_eventDispatcher); |
| _socketSubscription.onError(_reportError); |
| _socketSubscription.onDone(_doneHandler); |
| } |
| _secureFilter.connect(address.host, |
| (address as dynamic)._sockaddr_storage, |
| port, |
| is_server, |
| certificateName, |
| requestClientCertificate || |
| requireClientCertificate, |
| requireClientCertificate, |
| sendClientCertificate); |
| _secureHandshake(); |
| }) |
| .catchError(_reportError); |
| } |
| |
| StreamSubscription listen(void onData(RawSocketEvent data), |
| {void onError(error), |
| void onDone(), |
| bool cancelOnError}) { |
| _sendWriteEvent(); |
| return _stream.listen(onData, |
| onError: onError, |
| onDone: onDone, |
| cancelOnError: cancelOnError); |
| } |
| |
| static void _verifyFields(host, |
| int requestedPort, |
| String certificateName, |
| bool is_server, |
| bool requestClientCertificate, |
| bool requireClientCertificate, |
| bool sendClientCertificate, |
| Function onBadCertificate) { |
| if (host is! String && host is! InternetAddress) { |
| throw new ArgumentError("host is not a String or an InternetAddress"); |
| } |
| if (requestedPort is! int) { |
| throw new ArgumentError("requestedPort is not an int"); |
| } |
| if (requestedPort < 0 || requestedPort > 65535) { |
| throw new ArgumentError("requestedPort is not in the range 0..65535"); |
| } |
| if (certificateName != null && certificateName is! String) { |
| throw new ArgumentError("certificateName is not null or a String"); |
| } |
| if (certificateName == null && is_server) { |
| throw new ArgumentError("certificateName is null on a server"); |
| } |
| if (requestClientCertificate is! bool) { |
| throw new ArgumentError("requestClientCertificate is not a bool"); |
| } |
| if (requireClientCertificate is! bool) { |
| throw new ArgumentError("requireClientCertificate is not a bool"); |
| } |
| if (sendClientCertificate is! bool) { |
| throw new ArgumentError("sendClientCertificate is not a bool"); |
| } |
| if (onBadCertificate != null && onBadCertificate is! Function) { |
| throw new ArgumentError("onBadCertificate is not null or a Function"); |
| } |
| } |
| |
| int get port => _socket.port; |
| |
| String get remoteHost => _socket.remoteHost; |
| |
| int get remotePort => _socket.remotePort; |
| |
| int available() { |
| if (_status != CONNECTED) return 0; |
| return _secureFilter.buffers[READ_PLAINTEXT].length; |
| } |
| |
| Future<RawSecureSocket> close() { |
| shutdown(SocketDirection.BOTH); |
| return _closeCompleter.future; |
| } |
| |
| void _completeCloseCompleter([dummy]) { |
| if (!_closeCompleter.isCompleted) _closeCompleter.complete(this); |
| } |
| |
| void _close() { |
| _closedWrite = true; |
| _closedRead = true; |
| if (_socket != null) { |
| _socket.close().then(_completeCloseCompleter); |
| } else { |
| _completeCloseCompleter(); |
| } |
| _socketClosedWrite = true; |
| _socketClosedRead = true; |
| if (!_filterActive && _secureFilter != null) { |
| _secureFilter.destroy(); |
| _secureFilter = null; |
| } |
| if (_socketSubscription != null) { |
| _socketSubscription.cancel(); |
| } |
| _controller.close(); |
| _status = CLOSED; |
| } |
| |
| void shutdown(SocketDirection direction) { |
| if (direction == SocketDirection.SEND || |
| direction == SocketDirection.BOTH) { |
| _closedWrite = true; |
| if (_filterStatus.writeEmpty) { |
| _socket.shutdown(SocketDirection.SEND); |
| _socketClosedWrite = true; |
| if (_closedRead) { |
| _close(); |
| } |
| } |
| } |
| if (direction == SocketDirection.RECEIVE || |
| direction == SocketDirection.BOTH) { |
| _closedRead = true; |
| _socketClosedRead = true; |
| _socket.shutdown(SocketDirection.RECEIVE); |
| if (_socketClosedWrite) { |
| _close(); |
| } |
| } |
| } |
| |
| bool get writeEventsEnabled => _writeEventsEnabled; |
| |
| void set writeEventsEnabled(bool value) { |
| _writeEventsEnabled = value; |
| if (value) { |
| Timer.run(() => _sendWriteEvent()); |
| } |
| } |
| |
| bool get readEventsEnabled => _readEventsEnabled; |
| |
| void set readEventsEnabled(bool value) { |
| _readEventsEnabled = value; |
| _scheduleReadEvent(); |
| } |
| |
| List<int> read([int length]) { |
| if (length != null && (length is! int || length < 0)) { |
| throw new ArgumentError( |
| "Invalid length parameter in SecureSocket.read (length: $length)"); |
| } |
| if (_closedRead) { |
| throw new SocketException("Reading from a closed socket"); |
| } |
| if (_status != CONNECTED) { |
| return null; |
| } |
| var result = _secureFilter.buffers[READ_PLAINTEXT].read(length); |
| _scheduleFilter(); |
| return result; |
| } |
| |
| // Write the data to the socket, and schedule the filter to encrypt it. |
| int write(List<int> data, [int offset, int bytes]) { |
| if (bytes != null && (bytes is! int || bytes < 0)) { |
| throw new ArgumentError( |
| "Invalid bytes parameter in SecureSocket.read (bytes: $bytes)"); |
| } |
| if (offset != null && (offset is! int || offset < 0)) { |
| throw new ArgumentError( |
| "Invalid offset parameter in SecureSocket.read (offset: $offset)"); |
| } |
| if (_closedWrite) { |
| _controller.addError(new SocketException("Writing to a closed socket")); |
| return 0; |
| } |
| if (_status != CONNECTED) return 0; |
| if (offset == null) offset = 0; |
| if (bytes == null) bytes = data.length - offset; |
| |
| int written = |
| _secureFilter.buffers[WRITE_PLAINTEXT].write(data, offset, bytes); |
| if (written > 0) { |
| _filterStatus.writeEmpty = false; |
| } |
| _scheduleFilter(); |
| return written; |
| } |
| |
| X509Certificate get peerCertificate => _secureFilter.peerCertificate; |
| |
| bool _onBadCertificateWrapper(X509Certificate certificate) { |
| if (onBadCertificate == null) return false; |
| var result = onBadCertificate(certificate); |
| if (result is bool) return result; |
| throw new ArgumentError( |
| "onBadCertificate callback returned non-boolean $result"); |
| } |
| |
| bool setOption(SocketOption option, bool enabled) { |
| if (_socket == null) return false; |
| return _socket.setOption(option, enabled); |
| } |
| |
| void _eventDispatcher(RawSocketEvent event) { |
| try { |
| if (event == RawSocketEvent.READ) { |
| _readHandler(); |
| } else if (event == RawSocketEvent.WRITE) { |
| _writeHandler(); |
| } else if (event == RawSocketEvent.READ_CLOSED) { |
| _closeHandler(); |
| } |
| } catch (e) { |
| _reportError(e); |
| } |
| } |
| |
| void _readHandler() { |
| _readSocket(); |
| _scheduleFilter(); |
| } |
| |
| void _writeHandler() { |
| _writeSocket(); |
| _scheduleFilter(); |
| } |
| |
| void _doneHandler() { |
| if (_filterStatus.readEmpty) { |
| _close(); |
| } |
| } |
| |
| void _reportError(e) { |
| 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); |
| } else { |
| _controller.addError(e); |
| } |
| _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')); |
| } else { |
| _secureHandshake(); |
| } |
| } |
| } |
| |
| void _secureHandshake() { |
| try { |
| _secureFilter.handshake(); |
| _filterStatus.writeEmpty = false; |
| _readSocket(); |
| _writeSocket(); |
| _scheduleFilter(); |
| } catch (e) { |
| _reportError(e); |
| } |
| } |
| |
| void renegotiate({bool useSessionCache: true, |
| bool requestClientCertificate: false, |
| bool requireClientCertificate: false}) { |
| if (_status != CONNECTED) { |
| throw new HandshakeException( |
| "Called renegotiate on a non-connected socket"); |
| } |
| _secureFilter.renegotiate(useSessionCache, |
| requestClientCertificate, |
| requireClientCertificate); |
| _status = HANDSHAKE; |
| _filterStatus.writeEmpty = false; |
| _scheduleFilter(); |
| } |
| |
| void _secureHandshakeCompleteHandler() { |
| _status = CONNECTED; |
| if (_connectPending) { |
| _connectPending = false; |
| // We don't want user code to run synchronously in this callback. |
| Timer.run(() => _handshakeComplete.complete(this)); |
| } |
| } |
| |
| void _onPauseStateChange() { |
| if (_controller.isPaused) { |
| _pauseCount++; |
| } else { |
| _pauseCount--; |
| if (_pauseCount == 0) { |
| _scheduleReadEvent(); |
| _sendWriteEvent(); // Can send event synchronously. |
| } |
| } |
| |
| if (!_socketClosedRead || !_socketClosedWrite) { |
| if (_controller.isPaused) { |
| _socketSubscription.pause(); |
| } else { |
| _socketSubscription.resume(); |
| } |
| } |
| } |
| |
| void _onSubscriptionStateChange() { |
| if (_controller.hasListener) { |
| // TODO(ajohnsen): Do something here? |
| } |
| } |
| |
| void _scheduleFilter() { |
| _filterPending = true; |
| _tryFilter(); |
| } |
| |
| void _tryFilter() { |
| if (_status == CLOSED) return; |
| if (_filterPending && !_filterActive) { |
| _filterActive = true; |
| _filterPending = false; |
| _pushAllFilterStages().then((status) { |
| _filterStatus = status; |
| _filterActive = false; |
| if (_status == CLOSED) { |
| _secureFilter.destroy(); |
| _secureFilter = null; |
| return; |
| } |
| if (_filterStatus.writeEmpty && _closedWrite && !_socketClosedWrite) { |
| // Checks for and handles all cases of partially closed sockets. |
| shutdown(SocketDirection.SEND); |
| if (_status == CLOSED) return; |
| } |
| if (_filterStatus.readEmpty && _socketClosedRead && !_closedRead) { |
| if (_status == HANDSHAKE) { |
| _secureFilter.handshake(); |
| if (_status == HANDSHAKE) { |
| throw new HandshakeException( |
| 'Connection terminated during handshake'); |
| } |
| } |
| _closeHandler(); |
| } |
| if (_status == CLOSED) return; |
| if (_filterStatus.progress) { |
| _filterPending = true; |
| if (_filterStatus.writePlaintextNoLongerFull) _sendWriteEvent(); |
| if (_filterStatus.readEncryptedNoLongerFull) _readSocket(); |
| if (_filterStatus.writeEncryptedNoLongerEmpty) _writeSocket(); |
| if (_filterStatus.readPlaintextNoLongerEmpty) _scheduleReadEvent(); |
| if (_status == HANDSHAKE) _secureHandshake(); |
| } |
| _tryFilter(); |
| }).catchError(_reportError); |
| } |
| } |
| |
| List<int> _readSocketOrBufferedData(int bytes) { |
| if (_bufferedData != null) { |
| if (bytes > _bufferedData.length - _bufferedDataIndex) { |
| bytes = _bufferedData.length - _bufferedDataIndex; |
| } |
| var result = _bufferedData.sublist(_bufferedDataIndex, |
| _bufferedDataIndex + bytes); |
| _bufferedDataIndex += bytes; |
| if (_bufferedData.length == _bufferedDataIndex) { |
| _bufferedData = null; |
| } |
| return result; |
| } else if (!_socketClosedRead) { |
| return _socket.read(bytes); |
| } else { |
| return null; |
| } |
| } |
| |
| void _readSocket() { |
| if (_status == CLOSED) return; |
| var buffer = _secureFilter.buffers[READ_ENCRYPTED]; |
| if (buffer.writeFromSource(_readSocketOrBufferedData) > 0) { |
| _filterStatus.readEmpty = false; |
| } |
| } |
| |
| void _writeSocket() { |
| if (_socketClosedWrite) return; |
| var buffer = _secureFilter.buffers[WRITE_ENCRYPTED]; |
| if (buffer.readToSocket(_socket)) { // Returns true if blocked |
| _socket.writeEventsEnabled = true; |
| } |
| } |
| |
| // If a read event should be sent, add it to the controller. |
| _scheduleReadEvent() { |
| if (!_pendingReadEvent && |
| _readEventsEnabled && |
| _pauseCount == 0 && |
| _secureFilter != null && |
| !_secureFilter.buffers[READ_PLAINTEXT].isEmpty) { |
| _pendingReadEvent = true; |
| Timer.run(_sendReadEvent); |
| } |
| } |
| |
| _sendReadEvent() { |
| _pendingReadEvent = false; |
| if (_readEventsEnabled && |
| _pauseCount == 0 && |
| _secureFilter != null && |
| !_secureFilter.buffers[READ_PLAINTEXT].isEmpty) { |
| _controller.add(RawSocketEvent.READ); |
| _scheduleReadEvent(); |
| } |
| } |
| |
| // If a write event should be sent, add it to the controller. |
| _sendWriteEvent() { |
| if (!_closedWrite && |
| _writeEventsEnabled && |
| _pauseCount == 0 && |
| _secureFilter != null && |
| _secureFilter.buffers[WRITE_PLAINTEXT].free > 0) { |
| _writeEventsEnabled = false; |
| _controller.add(RawSocketEvent.WRITE); |
| } |
| } |
| |
| Future<_FilterStatus> _pushAllFilterStages() { |
| bool wasInHandshake = _status != CONNECTED; |
| List args = new List(2 + NUM_BUFFERS * 2); |
| args[0] = _filterPointer; |
| args[1] = wasInHandshake; |
| var bufs = _secureFilter.buffers; |
| for (var i = 0; i < NUM_BUFFERS; ++i) { |
| args[2 * i + 2] = bufs[i].start; |
| args[2 * i + 3] = bufs[i].end; |
| } |
| |
| return _IOService.dispatch(_SSL_PROCESS_FILTER, args).then((response) { |
| if (response.length == 2) { |
| _reportError(new TlsException('${response[1]} error ${response[0]}')); |
| } |
| int start(int index) => response[2 * index]; |
| int end(int index) => response[2 * index + 1]; |
| |
| _FilterStatus status = new _FilterStatus(); |
| // Compute writeEmpty as "write plaintext buffer and write encrypted |
| // buffer were empty when we started and are empty now". |
| status.writeEmpty = bufs[WRITE_PLAINTEXT].isEmpty && |
| start(WRITE_ENCRYPTED) == end(WRITE_ENCRYPTED); |
| // If we were in handshake when this started, _writeEmpty may be false |
| // because the handshake wrote data after we checked. |
| if (wasInHandshake) status.writeEmpty = false; |
| |
| // Compute readEmpty as "both read buffers were empty when we started |
| // and are empty now". |
| status.readEmpty = bufs[READ_ENCRYPTED].isEmpty && |
| start(READ_PLAINTEXT) == end(READ_PLAINTEXT); |
| |
| _ExternalBuffer buffer = bufs[WRITE_PLAINTEXT]; |
| int new_start = start(WRITE_PLAINTEXT); |
| if (new_start != buffer.start) { |
| status.progress = true; |
| if (buffer.free == 0) { |
| status.writePlaintextNoLongerFull = true; |
| } |
| buffer.start = new_start; |
| } |
| buffer = bufs[READ_ENCRYPTED]; |
| new_start = start(READ_ENCRYPTED); |
| if (new_start != buffer.start) { |
| status.progress = true; |
| if (buffer.free == 0) { |
| status.readEncryptedNoLongerFull = true; |
| } |
| buffer.start = new_start; |
| } |
| buffer = bufs[WRITE_ENCRYPTED]; |
| int new_end = end(WRITE_ENCRYPTED); |
| if (new_end != buffer.end) { |
| status.progress = true; |
| if (buffer.length == 0) { |
| status.writeEncryptedNoLongerEmpty = true; |
| } |
| buffer.end = new_end; |
| } |
| buffer = bufs[READ_PLAINTEXT]; |
| new_end = end(READ_PLAINTEXT); |
| if (new_end != buffer.end) { |
| status.progress = true; |
| if (buffer.length == 0) { |
| status.readPlaintextNoLongerEmpty = true; |
| } |
| buffer.end = new_end; |
| } |
| return status; |
| }); |
| } |
| } |
| |
| |
| /** |
| * A circular buffer backed by an external byte array. Accessed from |
| * both C++ and Dart code in an unsynchronized way, with one reading |
| * and one writing. All updates to start and end are done by Dart code. |
| */ |
| class _ExternalBuffer { |
| _ExternalBuffer(this.size) { |
| start = size~/2; |
| end = size~/2; |
| } |
| |
| void advanceStart(int bytes) { |
| assert(start > end || start + bytes <= end); |
| start += bytes; |
| if (start >= size) { |
| start -= size; |
| assert(start <= end); |
| assert(start < size); |
| } |
| } |
| |
| void advanceEnd(int bytes) { |
| assert(start <= end || start > end + bytes); |
| end += bytes; |
| if (end >= size) { |
| end -= size; |
| assert(end < start); |
| assert(end < size); |
| } |
| } |
| |
| bool get isEmpty => end == start; |
| |
| int get length { |
| if (start > end) return size + end - start; |
| return end - start; |
| } |
| |
| int get linearLength { |
| if (start > end) return size - start; |
| return end - start; |
| } |
| |
| int get free { |
| if (start > end) return start - end - 1; |
| return size + start - end - 1; |
| } |
| |
| int get linearFree { |
| if (start > end) return start - end - 1; |
| if (start == 0) return size - end - 1; |
| return size - end; |
| } |
| |
| List<int> read(int bytes) { |
| if (bytes == null) { |
| bytes = length; |
| } else { |
| bytes = min(bytes, length); |
| } |
| if (bytes == 0) return null; |
| List<int> result = new Uint8List(bytes); |
| int bytesRead = 0; |
| // Loop over zero, one, or two linear data ranges. |
| while (bytesRead < bytes) { |
| int toRead = min(bytes, linearLength); |
| result.setRange(bytesRead, |
| bytesRead + toRead, |
| data, |
| start); |
| advanceStart(toRead); |
| bytesRead += toRead; |
| } |
| return result; |
| } |
| |
| int write(List<int> inputData, int offset, int bytes) { |
| if (bytes > free) { |
| bytes = free; |
| } |
| int written = 0; |
| int toWrite = min(bytes, linearFree); |
| // Loop over zero, one, or two linear data ranges. |
| while (toWrite > 0) { |
| data.setRange(end, end + toWrite, inputData, offset); |
| advanceEnd(toWrite); |
| offset += toWrite; |
| written += toWrite; |
| toWrite = min(bytes - written, linearFree); |
| } |
| return written; |
| } |
| |
| int writeFromSource(List<int> getData(int requested)) { |
| int written = 0; |
| int toWrite = linearFree; |
| // Loop over zero, one, or two linear data ranges. |
| while (toWrite > 0) { |
| // Source returns at most toWrite bytes, and it returns null when empty. |
| var inputData = getData(toWrite); |
| if (inputData == null) break; |
| var len = inputData.length; |
| data.setRange(end, end + len, inputData); |
| advanceEnd(len); |
| written += len; |
| toWrite = linearFree; |
| } |
| return written; |
| } |
| |
| bool readToSocket(RawSocket socket) { |
| // Loop over zero, one, or two linear data ranges. |
| while (true) { |
| var toWrite = linearLength; |
| if (toWrite == 0) return false; |
| int bytes = socket.write(data, start, toWrite); |
| advanceStart(bytes); |
| if (bytes < toWrite) { |
| // The socket has blocked while we have data to write. |
| return true; |
| } |
| } |
| } |
| |
| List data; // This will be a ExternalByteArray, backed by C allocated data. |
| int start; |
| int end; |
| final size; |
| } |
| |
| |
| abstract class _SecureFilter { |
| external factory _SecureFilter(); |
| |
| void connect(String hostName, |
| Uint8List addr, |
| int port, |
| bool is_server, |
| String certificateName, |
| bool requestClientCertificate, |
| bool requireClientCertificate, |
| bool sendClientCertificate); |
| void destroy(); |
| void handshake(); |
| void rehandshake(); |
| void renegotiate(bool useSessionCache, |
| bool requestClientCertificate, |
| bool requireClientCertificate); |
| void init(); |
| X509Certificate get peerCertificate; |
| int processBuffer(int bufferIndex); |
| void registerBadCertificateCallback(Function callback); |
| void registerHandshakeCompleteCallback(Function handshakeCompleteHandler); |
| int _pointer(); |
| |
| List<_ExternalBuffer> get buffers; |
| } |
| |
| /** A secure networking exception caused by a failure in the |
| * TLS/SSL protocol. |
| */ |
| class TlsException implements IOException { |
| final String type; |
| final String message; |
| final OSError osError; |
| |
| const TlsException([String message = "", |
| OSError osError = null]) |
| : this._("TlsException", message, osError); |
| |
| const TlsException._(String this.type, |
| String this.message, |
| OSError this.osError); |
| |
| String toString() { |
| StringBuffer sb = new StringBuffer(); |
| sb.write(type); |
| if (!message.isEmpty) { |
| sb.write(": $message"); |
| if (osError != null) { |
| sb.write(" ($osError)"); |
| } |
| } else if (osError != null) { |
| sb.write(": $osError"); |
| } |
| return sb.toString(); |
| } |
| } |
| |
| |
| /** |
| * An exception that happens in the handshake phase of establishing |
| * a secure network connection. |
| */ |
| class HandshakeException extends TlsException { |
| const HandshakeException([String message = "", |
| OSError osError = null]) |
| : super._("HandshakeException", message, osError); |
| } |
| |
| |
| /** |
| * An exception that happens in the handshake phase of establishing |
| * a secure network connection, when looking up or verifying a |
| * certificate. |
| */ |
| class CertificateException extends TlsException { |
| const CertificateException([String message = "", |
| OSError osError = null]) |
| : super._("CertificateException", message, osError); |
| } |