From 256db8547214e82ad17eb49311c798678a418215 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 15 Aug 2023 22:56:41 -0500 Subject: [PATCH] use socks socket if useTor in ElectrumX and JsonRPC, --- .gitignore | 4 + crypto_plugins/flutter_libtor | 2 +- lib/electrumx_rpc/electrumx.dart | 155 +++++++++++++++++++++++------ lib/electrumx_rpc/rpc.dart | 164 +++++++++++++++++++++++++------ lib/networking/socks_socket.dart | 112 +++++++++++++++++++++ lib/networking/tor_service.dart | 3 +- scripts/linux/build_all.sh | 2 +- test/json_rpc_test.dart | 5 +- 8 files changed, 382 insertions(+), 65 deletions(-) create mode 100644 lib/networking/socks_socket.dart diff --git a/.gitignore b/.gitignore index c38882323..e58a9412f 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,7 @@ libmobileliblelantus.dll libtor_ffi.dll /libisar.so libtor_ffi.so + +tor_logs.txt + +torrc diff --git a/crypto_plugins/flutter_libtor b/crypto_plugins/flutter_libtor index 6aca8f78a..fc91c3f42 160000 --- a/crypto_plugins/flutter_libtor +++ b/crypto_plugins/flutter_libtor @@ -1 +1 @@ -Subproject commit 6aca8f78a10972ce07ee1ededcce02e97cc46834 +Subproject commit fc91c3f421467545f198d95558848e94de2fa6d9 diff --git a/lib/electrumx_rpc/electrumx.dart b/lib/electrumx_rpc/electrumx.dart index c9417116d..7d3dd3d2b 100644 --- a/lib/electrumx_rpc/electrumx.dart +++ b/lib/electrumx_rpc/electrumx.dart @@ -15,6 +15,7 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/electrumx_rpc/rpc.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; +import 'package:stackwallet/networking/tor_service.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:uuid/uuid.dart'; @@ -71,6 +72,8 @@ class ElectrumX { final Duration connectionTimeoutForSpecialCaseJsonRPCClients; + ({String host, int port})? proxyInfo; + ElectrumX({ required String host, required int port, @@ -80,6 +83,7 @@ class ElectrumX { JsonRPC? client, this.connectionTimeoutForSpecialCaseJsonRPCClients = const Duration(seconds: 60), + ({String host, int port})? proxyInfo, }) { _prefs = prefs; _host = host; @@ -92,14 +96,38 @@ class ElectrumX { required ElectrumXNode node, required Prefs prefs, required List failovers, - }) => - ElectrumX( + ({String host, int port})? proxyInfo, + }) { + if (Prefs.instance.useTor) { + if (proxyInfo == null) { + // TODO await tor / make sure it's running + proxyInfo = ( + host: InternetAddress.loopbackIPv4.address, + port: TorService.sharedInstance.port + ); + Logging.instance.log( + "ElectrumX.from(): no tor proxy info, read $proxyInfo", + level: LogLevel.Warning); + } + return ElectrumX( host: node.address, port: node.port, useSSL: node.useSSL, prefs: prefs, failovers: failovers, + proxyInfo: proxyInfo, ); + } else { + return ElectrumX( + host: node.address, + port: node.port, + useSSL: node.useSSL, + prefs: prefs, + failovers: failovers, + proxyInfo: null, + ); + } + } Future _allow() async { if (_prefs.wifiOnly) { @@ -121,20 +149,52 @@ class ElectrumX { throw WifiOnlyException(); } - if (currentFailoverIndex == -1) { - _rpcClient ??= JsonRPC( - host: host, - port: port, - useSSL: useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - ); + if (Prefs.instance.useTor) { + if (proxyInfo == null) { + // TODO await tor / make sure Tor is running + proxyInfo = ( + host: InternetAddress.loopbackIPv4.address, + port: TorService.sharedInstance.port + ); + Logging.instance.log( + "ElectrumX.request(): no tor proxy info, read $proxyInfo", + level: LogLevel.Warning); + } + if (currentFailoverIndex == -1) { + _rpcClient ??= JsonRPC( + host: host, + port: port, + useSSL: useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: proxyInfo, + ); + } else { + _rpcClient ??= JsonRPC( + host: failovers![currentFailoverIndex].address, + port: failovers![currentFailoverIndex].port, + useSSL: failovers![currentFailoverIndex].useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: proxyInfo, + ); + } } else { - _rpcClient = JsonRPC( - host: failovers![currentFailoverIndex].address, - port: failovers![currentFailoverIndex].port, - useSSL: failovers![currentFailoverIndex].useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - ); + if (currentFailoverIndex == -1) { + _rpcClient ??= JsonRPC( + host: host, + port: port, + useSSL: useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: null, + ); + } else { + _rpcClient ??= JsonRPC( + host: failovers![currentFailoverIndex].address, + port: failovers![currentFailoverIndex].port, + useSSL: failovers![currentFailoverIndex].useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: null, + ); + } } try { @@ -153,7 +213,8 @@ class ElectrumX { ); if (response.exception != null) { - throw response.exception!; + throw response.exception! + as Object; // TODO properly check that .exception is an Object } if (response.data is Map && response.data["error"] != null) { @@ -221,20 +282,53 @@ class ElectrumX { throw WifiOnlyException(); } - if (currentFailoverIndex == -1) { - _rpcClient ??= JsonRPC( - host: host, - port: port, - useSSL: useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - ); + if (Prefs.instance.useTor) { + // TODO await tor / make sure Tor is initialized + if (proxyInfo == null) { + proxyInfo = ( + host: InternetAddress.loopbackIPv4.address, + port: TorService.sharedInstance.port + ); + Logging.instance.log( + "ElectrumX.batchRequest(): no tor proxy info, read $proxyInfo", + level: LogLevel.Warning); + } + + if (currentFailoverIndex == -1) { + _rpcClient ??= JsonRPC( + host: host, + port: port, + useSSL: useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: proxyInfo, + ); + } else { + _rpcClient = JsonRPC( + host: failovers![currentFailoverIndex].address, + port: failovers![currentFailoverIndex].port, + useSSL: failovers![currentFailoverIndex].useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: proxyInfo, + ); + } } else { - _rpcClient = JsonRPC( - host: failovers![currentFailoverIndex].address, - port: failovers![currentFailoverIndex].port, - useSSL: failovers![currentFailoverIndex].useSSL, - connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, - ); + if (currentFailoverIndex == -1) { + _rpcClient ??= JsonRPC( + host: host, + port: port, + useSSL: useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: null, + ); + } else { + _rpcClient = JsonRPC( + host: failovers![currentFailoverIndex].address, + port: failovers![currentFailoverIndex].port, + useSSL: failovers![currentFailoverIndex].useSSL, + connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, + proxyInfo: null, + ); + } } try { @@ -260,7 +354,8 @@ class ElectrumX { (await _rpcClient!.request(request, requestTimeout)); if (jsonRpcResponse.exception != null) { - throw jsonRpcResponse.exception!; + throw jsonRpcResponse.exception! + as Object; // TODO properly check that .exception is an Object } final response = jsonRpcResponse.data as List; diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart index 2dc0d3a71..1b5a42d42 100644 --- a/lib/electrumx_rpc/rpc.dart +++ b/lib/electrumx_rpc/rpc.dart @@ -14,7 +14,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:mutex/mutex.dart'; +import 'package:stackwallet/networking/socks_socket.dart'; +import 'package:stackwallet/networking/tor_service.dart'; import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; // Json RPC class to handle connecting to electrumx servers class JsonRPC { @@ -23,16 +26,20 @@ class JsonRPC { required this.port, this.useSSL = false, this.connectionTimeout = const Duration(seconds: 60), + required ({String host, int port})? proxyInfo, }); final bool useSSL; final String host; final int port; final Duration connectionTimeout; + ({String host, int port})? proxyInfo; final _requestMutex = Mutex(); final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue(); Socket? _socket; + SOCKSSocket? _socksSocket; StreamSubscription? _subscription; + StreamSubscription? get subscription => _subscription; void _dataHandler(List data) { _requestQueue.nextIncompleteReq.then((req) { @@ -75,7 +82,12 @@ class JsonRPC { _requestQueue.nextIncompleteReq.then((req) { if (req != null) { // \r\n required by electrumx server - _socket!.write('${req.jsonRequest}\r\n'); + if (_socket != null) { + _socket!.write('${req.jsonRequest}\r\n'); + } + if (_socksSocket != null) { + _socksSocket!.socket.writeln('${req.jsonRequest}\r\n'); + } // TODO different timeout length? req.initiateTimeout( @@ -92,12 +104,22 @@ class JsonRPC { Duration requestTimeout, ) async { await _requestMutex.protect(() async { - if (_socket == null) { - Logging.instance.log( - "JsonRPC request: opening socket $host:$port", - level: LogLevel.Info, - ); - await connect(); + if (!Prefs.instance.useTor) { + if (_socket == null) { + Logging.instance.log( + "JsonRPC request: opening socket $host:$port", + level: LogLevel.Info, + ); + await connect(); + } + } else { + if (_socksSocket == null) { + Logging.instance.log( + "JsonRPC request: opening SOCKS socket to $host:$port", + level: LogLevel.Info, + ); + await connect(); + } } }); @@ -137,6 +159,8 @@ class JsonRPC { _subscription = null; _socket?.destroy(); _socket = null; + await _socksSocket?.close(); + _socksSocket = null; // clean up remaining queue await _requestQueue.completeRemainingWithError( @@ -146,33 +170,100 @@ class JsonRPC { } Future connect() async { - if (_socket != null) { - throw Exception( - "JsonRPC attempted to connect to an already existing socket!", - ); - } + if (!Prefs.instance.useTor) { + if (useSSL) { + _socket = await SecureSocket.connect( + host, + port, + timeout: connectionTimeout, + onBadCertificate: (_) => true, + ); // TODO do not automatically trust bad certificates + } else { + _socket = await Socket.connect( + host, + port, + timeout: connectionTimeout, + ); + } - if (useSSL) { - _socket = await SecureSocket.connect( - host, - port, - timeout: connectionTimeout, - onBadCertificate: (_) => true, - ); // TODO do not automatically trust bad certificates + _subscription = _socket!.listen( + _dataHandler, + onError: _errorHandler, + onDone: _doneHandler, + cancelOnError: true, + ); } else { - _socket = await Socket.connect( - host, - port, - timeout: connectionTimeout, - ); - } + if (proxyInfo == null) { + // TODO await tor / make sure it's running + proxyInfo = ( + host: InternetAddress.loopbackIPv4.address, + port: TorService.sharedInstance.port + ); + Logging.instance.log( + "ElectrumX.connect(): no tor proxy info, read $proxyInfo", + level: LogLevel.Warning); + } + // TODO connect to proxy socket... - _subscription = _socket!.listen( - _dataHandler, - onError: _errorHandler, - onDone: _doneHandler, - cancelOnError: true, - ); + // TODO implement ssl over tor + // if (useSSL) { + // _socket = await SecureSocket.connect( + // host, + // port, + // timeout: connectionTimeout, + // onBadCertificate: (_) => true, + // ); // TODO do not automatically trust bad certificates + // final _client = SocksSocket.protected(_socket, type); + // } else { + // instantiate a socks socket at localhost and on the port selected by the tor service + _socksSocket = await SOCKSSocket.create( + proxyHost: InternetAddress.loopbackIPv4.address, + proxyPort: TorService.sharedInstance.port, + ); + + try { + Logging.instance.log( + "JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo...", + level: LogLevel.Info); + + await _socksSocket?.connect(); + + Logging.instance.log( + "JsonRPC.connect(): connected to SOCKS socket at $proxyInfo...", + level: LogLevel.Info); + } catch (e) { + Logging.instance.log( + "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e", + level: LogLevel.Error); + throw Exception( + "JsonRPC.connect(): failed to connect to SOCKS socket at $proxyInfo, $e"); + } + + try { + Logging.instance.log( + "JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...", + level: LogLevel.Info); + + await _socksSocket?.connectTo(host, port); + + Logging.instance.log( + "JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo", + level: LogLevel.Info); + } catch (e) { + Logging.instance.log( + "JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e", + level: LogLevel.Error); + throw Exception( + "JsonRPC.connect(): failed to connect to tor proxy, $e"); + } + + // _subscription = _socksSocket!.socket.listen( + // _dataHandler, + // onError: _errorHandler, + // onDone: _doneHandler, + // cancelOnError: true, + // ) as StreamSubscription?; + } } } @@ -295,3 +386,14 @@ class JsonRPCResponse { JsonRPCResponse({this.data, this.exception}); } + +bool isIpAddress(String host) { + try { + // if the string can be parsed into an InternetAddress, it's an IP. + InternetAddress(host); + return true; + } catch (e) { + // if parsing fails, it's not an IP. + return false; + } +} diff --git a/lib/networking/socks_socket.dart b/lib/networking/socks_socket.dart new file mode 100644 index 000000000..4d66e0357 --- /dev/null +++ b/lib/networking/socks_socket.dart @@ -0,0 +1,112 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +class SOCKSSocket { + final String proxyHost; + final int proxyPort; + + late final Socket _socksSocket; + Socket get socket => _socksSocket; + + final StreamController> _responseController = + StreamController.broadcast(); + + // Private constructor + SOCKSSocket._(this.proxyHost, this.proxyPort); + + static Future create( + {required String proxyHost, required int proxyPort}) async { + var instance = SOCKSSocket._(proxyHost, proxyPort); + await instance._init(); + return instance; + } + + SOCKSSocket({required this.proxyHost, required this.proxyPort}) { + _init(); + } + + /// Initializes the SOCKS socket. + + Future _init() async { + _socksSocket = await Socket.connect( + proxyHost, + proxyPort, + ); + + _socksSocket.listen( + (data) { + _responseController.add(data); + }, + onError: (dynamic e) { + if (e is Object) { + _responseController.addError(e); + } + _responseController.addError("$e"); + // TODO make sure sending error as string is acceptable + }, + onDone: () { + _responseController.close(); + }, + ); + } + + /// Connects to the SOCKS socket. + Future connect() async { + // Greeting and method selection + _socksSocket.add([0x05, 0x01, 0x00]); + + // Wait for server response + var response = await _responseController.stream.first; + if (response[1] != 0x00) { + throw Exception('Failed to connect to SOCKS5 socket.'); + } + } + + /// Connects to the specified [domain] and [port] through the SOCKS socket. + Future connectTo(String domain, int port) async { + var request = [ + 0x05, + 0x01, + 0x00, + 0x03, + domain.length, + ...domain.codeUnits, + (port >> 8) & 0xFF, + port & 0xFF + ]; + + _socksSocket.add(request); + + var response = await _responseController.stream.first; + if (response[1] != 0x00) { + throw Exception('Failed to connect to target through SOCKS5 proxy.'); + } + } + + /// Converts [object] to a String by invoking [Object.toString] and + /// sends the encoding of the result to the socket. + void write(Object? object) { + if (object == null) return; + + List data = utf8.encode(object.toString()); + _socksSocket.add(data); + } + + /// Sends the server.features command to the proxy server. + Future sendServerFeaturesCommand() async { + const String command = + '{"jsonrpc":"2.0","id":"0","method":"server.features","params":[]}'; + _socksSocket.writeln(command); + + var responseData = await _responseController.stream.first; + print("responseData: ${utf8.decode(responseData)}"); + } + + /// Closes the connection to the Tor proxy. + Future close() async { + await _socksSocket.flush(); // Ensure all data is sent before closing + await _responseController.close(); + return await _socksSocket.close(); + } +} diff --git a/lib/networking/tor_service.dart b/lib/networking/tor_service.dart index 9c27a35e0..7085f5401 100644 --- a/lib/networking/tor_service.dart +++ b/lib/networking/tor_service.dart @@ -12,7 +12,8 @@ class TorService { Future start() async { final dir = await StackFileSystem.applicationTorDirectory(); - return await _tor.start(torDir: dir); + await _tor.start(torDir: dir); + return; } Future stop() async { diff --git a/scripts/linux/build_all.sh b/scripts/linux/build_all.sh index 9a119d0d6..1ba99c0bd 100755 --- a/scripts/linux/build_all.sh +++ b/scripts/linux/build_all.sh @@ -9,7 +9,7 @@ mkdir -p build (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libepiccash/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_libmonero/scripts/linux && ./build_monero_all.sh && ./build_sharedfile.sh ) & -(cd ../../crypto_plugins/tor/scripts/linux && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libtor/scripts/linux && ./build_all.sh ) & wait echo "Done building" diff --git a/test/json_rpc_test.dart b/test/json_rpc_test.dart index 333c1bde6..b5df1d52f 100644 --- a/test/json_rpc_test.dart +++ b/test/json_rpc_test.dart @@ -11,6 +11,7 @@ void main() { port: DefaultNodes.bitcoin.port, useSSL: true, connectionTimeout: const Duration(seconds: 40), + proxyInfo: null, // TODO test for proxyInfo ); const jsonRequestString = @@ -27,7 +28,8 @@ void main() { final jsonRPC = JsonRPC( host: "some.bad.address.thingdsfsdfsdaf", port: 3000, - connectionTimeout: Duration(seconds: 10), + connectionTimeout: const Duration(seconds: 10), + proxyInfo: null, ); const jsonRequestString = @@ -47,6 +49,7 @@ void main() { port: 3000, useSSL: false, connectionTimeout: const Duration(seconds: 1), + proxyInfo: null, ); const jsonRequestString =