use socks socket if useTor in ElectrumX and JsonRPC,

This commit is contained in:
sneurlax 2023-08-15 22:56:41 -05:00
parent 80fcdef330
commit 256db85472
8 changed files with 382 additions and 65 deletions

4
.gitignore vendored
View file

@ -58,3 +58,7 @@ libmobileliblelantus.dll
libtor_ffi.dll libtor_ffi.dll
/libisar.so /libisar.so
libtor_ffi.so libtor_ffi.so
tor_logs.txt
torrc

@ -1 +1 @@
Subproject commit 6aca8f78a10972ce07ee1ededcce02e97cc46834 Subproject commit fc91c3f421467545f198d95558848e94de2fa6d9

View file

@ -15,6 +15,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:decimal/decimal.dart'; import 'package:decimal/decimal.dart';
import 'package:stackwallet/electrumx_rpc/rpc.dart'; import 'package:stackwallet/electrumx_rpc/rpc.dart';
import 'package:stackwallet/exceptions/electrumx/no_such_transaction.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/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -71,6 +72,8 @@ class ElectrumX {
final Duration connectionTimeoutForSpecialCaseJsonRPCClients; final Duration connectionTimeoutForSpecialCaseJsonRPCClients;
({String host, int port})? proxyInfo;
ElectrumX({ ElectrumX({
required String host, required String host,
required int port, required int port,
@ -80,6 +83,7 @@ class ElectrumX {
JsonRPC? client, JsonRPC? client,
this.connectionTimeoutForSpecialCaseJsonRPCClients = this.connectionTimeoutForSpecialCaseJsonRPCClients =
const Duration(seconds: 60), const Duration(seconds: 60),
({String host, int port})? proxyInfo,
}) { }) {
_prefs = prefs; _prefs = prefs;
_host = host; _host = host;
@ -92,14 +96,38 @@ class ElectrumX {
required ElectrumXNode node, required ElectrumXNode node,
required Prefs prefs, required Prefs prefs,
required List<ElectrumXNode> failovers, required List<ElectrumXNode> failovers,
}) => ({String host, int port})? proxyInfo,
ElectrumX( }) {
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, host: node.address,
port: node.port, port: node.port,
useSSL: node.useSSL, useSSL: node.useSSL,
prefs: prefs, prefs: prefs,
failovers: failovers, failovers: failovers,
proxyInfo: proxyInfo,
); );
} else {
return ElectrumX(
host: node.address,
port: node.port,
useSSL: node.useSSL,
prefs: prefs,
failovers: failovers,
proxyInfo: null,
);
}
}
Future<bool> _allow() async { Future<bool> _allow() async {
if (_prefs.wifiOnly) { if (_prefs.wifiOnly) {
@ -121,20 +149,52 @@ class ElectrumX {
throw WifiOnlyException(); throw WifiOnlyException();
} }
if (currentFailoverIndex == -1) { if (Prefs.instance.useTor) {
_rpcClient ??= JsonRPC( if (proxyInfo == null) {
host: host, // TODO await tor / make sure Tor is running
port: port, proxyInfo = (
useSSL: useSSL, host: InternetAddress.loopbackIPv4.address,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, 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 { } else {
_rpcClient = JsonRPC( if (currentFailoverIndex == -1) {
host: failovers![currentFailoverIndex].address, _rpcClient ??= JsonRPC(
port: failovers![currentFailoverIndex].port, host: host,
useSSL: failovers![currentFailoverIndex].useSSL, port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, 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 { try {
@ -153,7 +213,8 @@ class ElectrumX {
); );
if (response.exception != null) { 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) { if (response.data is Map && response.data["error"] != null) {
@ -221,20 +282,53 @@ class ElectrumX {
throw WifiOnlyException(); throw WifiOnlyException();
} }
if (currentFailoverIndex == -1) { if (Prefs.instance.useTor) {
_rpcClient ??= JsonRPC( // TODO await tor / make sure Tor is initialized
host: host, if (proxyInfo == null) {
port: port, proxyInfo = (
useSSL: useSSL, host: InternetAddress.loopbackIPv4.address,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, 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 { } else {
_rpcClient = JsonRPC( if (currentFailoverIndex == -1) {
host: failovers![currentFailoverIndex].address, _rpcClient ??= JsonRPC(
port: failovers![currentFailoverIndex].port, host: host,
useSSL: failovers![currentFailoverIndex].useSSL, port: port,
connectionTimeout: connectionTimeoutForSpecialCaseJsonRPCClients, 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 { try {
@ -260,7 +354,8 @@ class ElectrumX {
(await _rpcClient!.request(request, requestTimeout)); (await _rpcClient!.request(request, requestTimeout));
if (jsonRpcResponse.exception != null) { 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; final response = jsonRpcResponse.data as List;

View file

@ -14,7 +14,10 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.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/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
// Json RPC class to handle connecting to electrumx servers // Json RPC class to handle connecting to electrumx servers
class JsonRPC { class JsonRPC {
@ -23,16 +26,20 @@ class JsonRPC {
required this.port, required this.port,
this.useSSL = false, this.useSSL = false,
this.connectionTimeout = const Duration(seconds: 60), this.connectionTimeout = const Duration(seconds: 60),
required ({String host, int port})? proxyInfo,
}); });
final bool useSSL; final bool useSSL;
final String host; final String host;
final int port; final int port;
final Duration connectionTimeout; final Duration connectionTimeout;
({String host, int port})? proxyInfo;
final _requestMutex = Mutex(); final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue(); final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket; Socket? _socket;
SOCKSSocket? _socksSocket;
StreamSubscription<Uint8List>? _subscription; StreamSubscription<Uint8List>? _subscription;
StreamSubscription<Uint8List>? get subscription => _subscription;
void _dataHandler(List<int> data) { void _dataHandler(List<int> data) {
_requestQueue.nextIncompleteReq.then((req) { _requestQueue.nextIncompleteReq.then((req) {
@ -75,7 +82,12 @@ class JsonRPC {
_requestQueue.nextIncompleteReq.then((req) { _requestQueue.nextIncompleteReq.then((req) {
if (req != null) { if (req != null) {
// \r\n required by electrumx server // \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? // TODO different timeout length?
req.initiateTimeout( req.initiateTimeout(
@ -92,12 +104,22 @@ class JsonRPC {
Duration requestTimeout, Duration requestTimeout,
) async { ) async {
await _requestMutex.protect(() async { await _requestMutex.protect(() async {
if (_socket == null) { if (!Prefs.instance.useTor) {
Logging.instance.log( if (_socket == null) {
"JsonRPC request: opening socket $host:$port", Logging.instance.log(
level: LogLevel.Info, "JsonRPC request: opening socket $host:$port",
); level: LogLevel.Info,
await connect(); );
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; _subscription = null;
_socket?.destroy(); _socket?.destroy();
_socket = null; _socket = null;
await _socksSocket?.close();
_socksSocket = null;
// clean up remaining queue // clean up remaining queue
await _requestQueue.completeRemainingWithError( await _requestQueue.completeRemainingWithError(
@ -146,33 +170,100 @@ class JsonRPC {
} }
Future<void> connect() async { Future<void> connect() async {
if (_socket != null) { if (!Prefs.instance.useTor) {
throw Exception( if (useSSL) {
"JsonRPC attempted to connect to an already existing socket!", _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) { _subscription = _socket!.listen(
_socket = await SecureSocket.connect( _dataHandler,
host, onError: _errorHandler,
port, onDone: _doneHandler,
timeout: connectionTimeout, cancelOnError: true,
onBadCertificate: (_) => true, );
); // TODO do not automatically trust bad certificates
} else { } else {
_socket = await Socket.connect( if (proxyInfo == null) {
host, // TODO await tor / make sure it's running
port, proxyInfo = (
timeout: connectionTimeout, 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( // TODO implement ssl over tor
_dataHandler, // if (useSSL) {
onError: _errorHandler, // _socket = await SecureSocket.connect(
onDone: _doneHandler, // host,
cancelOnError: true, // 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<Uint8List>?;
}
} }
} }
@ -295,3 +386,14 @@ class JsonRPCResponse {
JsonRPCResponse({this.data, this.exception}); 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;
}
}

View file

@ -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<List<int>> _responseController =
StreamController.broadcast();
// Private constructor
SOCKSSocket._(this.proxyHost, this.proxyPort);
static Future<SOCKSSocket> 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<void> _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<void> 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<void> 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<int> data = utf8.encode(object.toString());
_socksSocket.add(data);
}
/// Sends the server.features command to the proxy server.
Future<void> 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<void> close() async {
await _socksSocket.flush(); // Ensure all data is sent before closing
await _responseController.close();
return await _socksSocket.close();
}
}

View file

@ -12,7 +12,8 @@ class TorService {
Future<void> start() async { Future<void> start() async {
final dir = await StackFileSystem.applicationTorDirectory(); final dir = await StackFileSystem.applicationTorDirectory();
return await _tor.start(torDir: dir); await _tor.start(torDir: dir);
return;
} }
Future<void> stop() async { Future<void> stop() async {

View file

@ -9,7 +9,7 @@ mkdir -p build
(cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) & (cd ../../crypto_plugins/flutter_liblelantus/scripts/linux && ./build_all.sh ) &
(cd ../../crypto_plugins/flutter_libepiccash/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/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 wait
echo "Done building" echo "Done building"

View file

@ -11,6 +11,7 @@ void main() {
port: DefaultNodes.bitcoin.port, port: DefaultNodes.bitcoin.port,
useSSL: true, useSSL: true,
connectionTimeout: const Duration(seconds: 40), connectionTimeout: const Duration(seconds: 40),
proxyInfo: null, // TODO test for proxyInfo
); );
const jsonRequestString = const jsonRequestString =
@ -27,7 +28,8 @@ void main() {
final jsonRPC = JsonRPC( final jsonRPC = JsonRPC(
host: "some.bad.address.thingdsfsdfsdaf", host: "some.bad.address.thingdsfsdfsdaf",
port: 3000, port: 3000,
connectionTimeout: Duration(seconds: 10), connectionTimeout: const Duration(seconds: 10),
proxyInfo: null,
); );
const jsonRequestString = const jsonRequestString =
@ -47,6 +49,7 @@ void main() {
port: 3000, port: 3000,
useSSL: false, useSSL: false,
connectionTimeout: const Duration(seconds: 1), connectionTimeout: const Duration(seconds: 1),
proxyInfo: null,
); );
const jsonRequestString = const jsonRequestString =