diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e5e66..beba3d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.0 + +- Add `onBadCertificate` to `SOCKSSocket.create()` and `SocksConnection.start()`, passed through to `RawSecureSocket.secure`. `allowBadCertificates: true` still accepts everything. + ## 2.0.0 - **Breaking:** Remove `SOCKSSocket.socket` so transport operations cannot bypass the wrapper's write queue and lifecycle tracking. Use `destroy()`, `closeOutput()`, `close()`, `write()`, `outputStream`, and `inputStream` instead. `closeOutput()` preserves the native socket's write-side half-close behavior. diff --git a/lib/socks_connection.dart b/lib/socks_connection.dart index c368025..6d39314 100644 --- a/lib/socks_connection.dart +++ b/lib/socks_connection.dart @@ -38,6 +38,7 @@ class SocksConnection { required int targetPort, String? tlsHost, SecurityContext? securityContext, + bool Function(X509Certificate)? onBadCertificate, SocksCredentials? credentials, List? supportedProtocols, Duration timeout = const Duration(seconds: 30), @@ -55,7 +56,7 @@ class SocksConnection { final request = encodeSocksDestination(targetHost, targetPort); final operation = _ConnectOperation(); operation.run(proxyHost, proxyPort, request, tlsHost, securityContext, - credentials, supportedProtocols, timeout); + onBadCertificate, credentials, supportedProtocols, timeout); return ConnectionTask.fromSocket(operation.result.future, operation.cancel); } } @@ -90,6 +91,7 @@ class _ConnectOperation { List request, String? tlsHost, SecurityContext? context, + bool Function(X509Certificate)? onBadCertificate, SocksCredentials? credentials, List? supportedProtocols, Duration timeout) async { @@ -127,6 +129,7 @@ class _ConnectOperation { subscription: channel.detach(), host: tlsHost, context: context, + onBadCertificate: onBadCertificate, supportedProtocols: supportedProtocols); _raw = secured; if (_aborted) { diff --git a/lib/socks_socket.dart b/lib/socks_socket.dart index 41ca9a1..05a0496 100644 --- a/lib/socks_socket.dart +++ b/lib/socks_socket.dart @@ -146,6 +146,9 @@ class SOCKSSocket { final SecurityContext? _securityContext; + /// Passed to [RawSecureSocket.secure]; ignored with [allowBadCertificates]. + final bool Function(X509Certificate)? _onBadCertificate; + final bool? _requireIsolation; final bool _closeOnPeerEof; @@ -199,6 +202,7 @@ class SOCKSSocket { this._operationTimeout, this._allowBadCertificates, this._securityContext, + this._onBadCertificate, this._requireIsolation, this._closeOnPeerEof); @@ -216,6 +220,7 @@ class SOCKSSocket { Duration operationTimeout = const Duration(seconds: 30), bool allowBadCertificates = false, SecurityContext? securityContext, + bool Function(X509Certificate)? onBadCertificate, bool? requireIsolation, bool closeOnPeerEof = true, }) async { @@ -245,6 +250,7 @@ class SOCKSSocket { operationTimeout, allowBadCertificates, securityContext, + onBadCertificate, requireIsolation, closeOnPeerEof); await instance._init(); @@ -408,7 +414,8 @@ class SOCKSSocket { subscription: channel.detach(), host: domain, context: _securityContext, - onBadCertificate: _allowBadCertificates ? (_) => true : null, + onBadCertificate: + _allowBadCertificates ? (_) => true : _onBadCertificate, ); try { _channel = RawChannel(await _handshake(() => upgrade)); diff --git a/pubspec.yaml b/pubspec.yaml index 35b4b9f..5f97eb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: socks_socket description: SOCKS version 5 sockets for Dart and Flutter, *eg.* ElectrumX and/or Fulcrum over Tor via socket(s). -version: 2.0.0 +version: 2.1.0 repository: https://github.com/cypherstack/socks_socket environment: diff --git a/test/socks_connection_test.dart b/test/socks_connection_test.dart index eb993de..4f4ed4d 100644 --- a/test/socks_connection_test.dart +++ b/test/socks_connection_test.dart @@ -10,6 +10,7 @@ ConnectionTask connect(TunnelProxy proxy, {String host = 'unresolvable.invalid', String? tlsHost, SecurityContext? context, + bool Function(X509Certificate)? onBadCertificate, SocksCredentials? credentials, List? supportedProtocols, Duration timeout = const Duration(seconds: 3)}) => @@ -20,6 +21,7 @@ ConnectionTask connect(TunnelProxy proxy, targetPort: 443, tlsHost: tlsHost, securityContext: context, + onBadCertificate: onBadCertificate, credentials: credentials, supportedProtocols: supportedProtocols, timeout: timeout); @@ -252,6 +254,31 @@ void main() { await proxy.disconnected.future.timeout(const Duration(seconds: 2)); }); } + test('onBadCertificate can accept an untrusted certificate', () async { + final server = await SecureServerSocket.bind( + InternetAddress.loopbackIPv4, 0, certificates.serverContext()); + final accepted = []; + server.listen(accepted.add, onError: (Object _) {}); + addTearDown(() async { + for (final socket in accepted) { + socket.destroy(); + } + await server.close(); + }); + final proxy = TunnelProxy(upstreamPort: server.port); + await proxy.start(); + addTearDown(proxy.close); + X509Certificate? offered; + final socket = await connect(proxy, + tlsHost: 'localhost', context: SecurityContext(withTrustedRoots: false), + onBadCertificate: (certificate) { + offered = certificate; + return true; + }).socket; + addTearDown(socket.destroy); + expect(socket, isA()); + expect(offered, isNotNull); + }); test('renegotiate is unsupported on the TLS socket', () async { final server = await SecureServerSocket.bind( InternetAddress.loopbackIPv4, 0, certificates.serverContext()); diff --git a/test/tls_configuration_test.dart b/test/tls_configuration_test.dart index a64ef1c..1d0db4e 100644 --- a/test/tls_configuration_test.dart +++ b/test/tls_configuration_test.dart @@ -28,4 +28,29 @@ void main() { await socket.write('custom TLS'); expect(utf8.decode(await reply), 'custom TLS'); }); + + test('SOCKSSocket passes onBadCertificate to the TLS upgrade', () async { + final certificates = TestCertificates.generate(); + final proxy = MockSocksServer() + ..sslEnabled = true + ..securityContext = certificates.serverContext(); + await proxy.start(); + addTearDown(proxy.stop); + X509Certificate? offered; + final socket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: proxy.port, + sslEnabled: true, + securityContext: SecurityContext(withTrustedRoots: false), + onBadCertificate: (certificate) { + offered = certificate; + return true; + }, + ); + addTearDown(socket.close); + await socket.connect(); + await socket.connectTo('localhost', 443); + expect(socket.state, ConnectionState.connected); + expect(offered, isNotNull); + }); }