Merge branch 'tor-presocks' into tor

This commit is contained in:
sneurlax 2023-08-09 12:15:25 -05:00
commit 0522d2a0ee
2 changed files with 434 additions and 24 deletions
lib
electrumx_rpc
networking

View file

@ -14,8 +14,6 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:mutex/mutex.dart';
import 'package:socks5_proxy/socks.dart';
import 'package:socks5_proxy/src/client/socks_client.dart'; // for SocksSocket
import 'package:stackwallet/networking/tor_service.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
@ -37,8 +35,7 @@ class JsonRPC {
final _requestMutex = Mutex();
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
Socket? _socket;
SocksSocket? _socksSocket;
Socket? _socket; // TODO make a SocksSocket extension/wrapper or similar
StreamSubscription<Uint8List>? _subscription;
void _dataHandler(List<int> data) {
@ -201,22 +198,62 @@ class JsonRPC {
level: LogLevel.Info);
}
_subscription = _socksSocket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
} else {
if (_socket != null) {
Logging.instance.log(
"JsonRPC.connect(): JsonRPC attempted to connect to an already existing socket!",
level: LogLevel.Error);
throw Exception(
"JsonRPC attempted to connect to an already existing socket!",
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.connect(): no tor proxy info, read $proxyInfo",
level: LogLevel.Warning);
}
// TODO connect to proxy socket...
// https://github.com/LacticWhale/socks_dart/blob/master/lib/src/client/socks_client.dart#L50C46-L50C56
// 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 {
_socket = await Socket.connect(
proxyInfo!.host,
proxyInfo!.port,
timeout: connectionTimeout,
);
// final _client = SocksSocket.protected(
// _socket!, SocksConnectionType.connect
// );
// final InternetAddress _host =
// await InternetAddress.lookup(host).then((value) => value.first);
// var _socket = await SocksSocket.initialize(
// [
// ProxySettings(
// InternetAddress.loopbackIPv4,
// proxyInfo!.port,
// )
// ],
// _host,
// port,
// SocksConnectionType.connect,
// );
if (_socket == null) {
Logging.instance.log(
"JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo",
level: LogLevel.Error);
throw Exception("JsonRPC.connect(): failed to connect to tor proxy");
} else {
Logging.instance.log(
"JsonRPC.connect(): connected to $host over tor proxy at $proxyInfo",
level: LogLevel.Info);
}
} else {
if (useSSL) {
_socket = await SecureSocket.connect(
host,
@ -231,13 +268,6 @@ class JsonRPC {
timeout: connectionTimeout,
);
}
_subscription = _socket!.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
}
}
}

380
lib/networking/socks5.dart Normal file
View file

@ -0,0 +1,380 @@
// https://github.com/v0l/socks5 https://pub.dev/packages/socks5 for Dart 3
// library socks;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
/// https://tools.ietf.org/html/rfc1928
/// https://tools.ietf.org/html/rfc1929
const SOCKSVersion = 0x05;
const RFC1929Version = 0x01;
class AuthMethods {
static const NoAuth = AuthMethods._(0x00);
static const GSSApi = AuthMethods._(0x01);
static const UsernamePassword = AuthMethods._(0x02);
static const NoAcceptableMethods = AuthMethods._(0xFF);
final int _value;
const AuthMethods._(this._value);
@override
String toString() {
return const {
0x00: 'AuthMethods.NoAuth',
0x01: 'AuthMethods.GSSApi',
0x02: 'AuthMethods.UsernamePassword',
0xFF: 'AuthMethods.NoAcceptableMethods'
}[_value] ??
'Unknown AuthMethod';
}
}
class SOCKSState {
static const Starting = SOCKSState._(0x00);
static const Auth = SOCKSState._(0x01);
static const RequestReady = SOCKSState._(0x02);
static const Connected = SOCKSState._(0x03);
static const AuthStarted = SOCKSState._(0x04);
final int _value;
const SOCKSState._(this._value);
@override
String toString() {
return const [
'SOCKSState.Starting',
'SOCKSState.Auth',
'SOCKSState.RequestReady',
'SOCKSState.Connected',
'SOCKSState.AuthStarted'
][_value];
}
}
class SOCKSAddressType {
static const IPv4 = SOCKSAddressType._(0x01);
static const Domain = SOCKSAddressType._(0x03);
static const IPv6 = SOCKSAddressType._(0x04);
final int _value;
const SOCKSAddressType._(this._value);
@override
String toString() {
return const [
null,
'SOCKSAddressType.IPv4',
null,
'SOCKSAddressType.Domain',
'SOCKSAddressType.IPv6',
][_value] ??
'Unknown SOCKSAddressType';
}
}
class SOCKSCommand {
static const Connect = SOCKSCommand._(0x01);
static const Bind = SOCKSCommand._(0x02);
static const UDPAssociate = SOCKSCommand._(0x03);
final int _value;
const SOCKSCommand._(this._value);
@override
String toString() {
return const [
null,
'SOCKSCommand.Connect',
'SOCKSCommand.Bind',
'SOCKSCommand.UDPAssociate',
][_value] ??
'Unknown SOCKSCommand';
}
}
class SOCKSReply {
static const Success = SOCKSReply._(0x00);
static const GeneralFailure = SOCKSReply._(0x01);
static const ConnectionNotAllowedByRuleset = SOCKSReply._(0x02);
static const NetworkUnreachable = SOCKSReply._(0x03);
static const HostUnreachable = SOCKSReply._(0x04);
static const ConnectionRefused = SOCKSReply._(0x05);
static const TTLExpired = SOCKSReply._(0x06);
static const CommandNotSupported = SOCKSReply._(0x07);
static const AddressTypeNotSupported = SOCKSReply._(0x08);
final int _value;
const SOCKSReply._(this._value);
@override
String toString() {
return const [
'SOCKSReply.Success',
'SOCKSReply.GeneralFailure',
'SOCKSReply.ConnectionNotAllowedByRuleset',
'SOCKSReply.NetworkUnreachable',
'SOCKSReply.HostUnreachable',
'SOCKSReply.ConnectionRefused',
'SOCKSReply.TTLExpired',
'SOCKSReply.CommandNotSupported',
'SOCKSReply.AddressTypeNotSupported'
][_value];
}
}
class SOCKSRequest {
final int version = SOCKSVersion;
final SOCKSCommand command;
final SOCKSAddressType addressType;
final Uint8List address;
final int port;
String? getAddressString() {
if (addressType == SOCKSAddressType.Domain) {
return const AsciiDecoder().convert(address);
} else if (addressType == SOCKSAddressType.IPv4) {
return address.join(".");
} else if (addressType == SOCKSAddressType.IPv6) {
var ret = <String>[];
for (var x = 0; x < address.length; x += 2) {
ret.add(
"${address[x].toRadixString(16).padLeft(2, "0")}${address[x + 1].toRadixString(16).padLeft(2, "0")}");
}
return ret.join(":");
}
return null;
}
SOCKSRequest({
required this.command,
required this.addressType,
required this.address,
required this.port,
});
}
class SOCKSSocket {
late List<AuthMethods> _auth;
late RawSocket _sock;
late SOCKSRequest _request;
late StreamSubscription<RawSocketEvent> _sockSub;
StreamSubscription<RawSocketEvent> get subscription => _sockSub;
late SOCKSState _state;
final StreamController<SOCKSState> _stateStream =
StreamController<SOCKSState>();
SOCKSState get state => _state;
Stream<SOCKSState> get stateStream => _stateStream.stream;
/// For username:password auth
final String? username;
final String? password;
/// Waits for state to change to [SOCKSState.Connected]
/// If the connection request returns an error from the
/// socks server it will be thrown as an exception in the stream
///
///
Future<SOCKSState> get _waitForConnect =>
stateStream.firstWhere((a) => a == SOCKSState.Connected);
SOCKSSocket(
RawSocket socket, {
List<AuthMethods> auth = const [AuthMethods.NoAuth],
this.username,
this.password,
}) {
_sock = socket;
_auth = auth;
_setState(SOCKSState.Starting);
}
void _setState(SOCKSState ns) {
_state = ns;
_stateStream.add(ns);
}
/// Issue connect command to proxy
///
Future<void> connect(String domain) async {
final ds = domain.split(':');
assert(ds.length == 2, "Domain must contain port, example.com:80");
_request = SOCKSRequest(
command: SOCKSCommand.Connect,
addressType: SOCKSAddressType.Domain,
address: const AsciiEncoder().convert(ds[0]).sublist(0, ds[0].length),
port: int.tryParse(ds[1]) ?? 80,
);
await _start();
await _waitForConnect;
}
Future<void> connectIp(InternetAddress ip, int port) async {
_request = SOCKSRequest(
command: SOCKSCommand.Connect,
addressType: ip.type == InternetAddressType.IPv4
? SOCKSAddressType.IPv4
: SOCKSAddressType.IPv6,
address: ip.rawAddress,
port: port,
);
await _start();
await _waitForConnect;
}
Future<void> close({bool keepOpen = true}) async {
await _stateStream.close();
if (!keepOpen) {
await _sock.close();
}
}
Future<void> _start() async {
// send auth methods
_setState(SOCKSState.Auth);
//print(">> Version: 5, AuthMethods: $_auth");
_sock.write([
0x05,
_auth.length,
..._auth.map((v) => v._value),
]);
_sockSub = _sock.listen((RawSocketEvent ev) {
switch (ev) {
case RawSocketEvent.read:
{
final have = _sock.available();
final data = _sock.read(have);
if (data != null) _handleRead(data);
break;
}
case RawSocketEvent.closed:
{
_sockSub.cancel();
break;
}
case RawSocketEvent.readClosed:
// TODO: Handle this case.
break;
case RawSocketEvent.write:
// TODO: Handle this case.
break;
}
});
}
void _sendUsernamePassword(String uname, String password) {
if (uname.length > 255 || password.length > 255) {
throw "Username or Password is too long";
}
final data = [
RFC1929Version,
uname.length,
...const AsciiEncoder().convert(uname),
password.length,
...const AsciiEncoder().convert(password)
];
//print(">> Sending $username:$password");
_sock.write(data);
}
void _handleRead(Uint8List data) async {
if (state == SOCKSState.Auth) {
if (data.length == 2) {
// final version = data[0];
//print("<< Version: $version, Auth: $auth");
final auth = AuthMethods._(data[1]);
if (auth._value == AuthMethods.UsernamePassword._value) {
_setState(SOCKSState.AuthStarted);
_sendUsernamePassword(username ?? '', password ?? '');
// TODO check that passing an empty string is valid (vs. null previously)
} else if (auth._value == AuthMethods.NoAuth._value) {
_setState(SOCKSState.RequestReady);
_writeRequest(_request);
} else if (auth._value == AuthMethods.NoAcceptableMethods._value) {
throw "No auth methods acceptable";
}
} else {
throw "Expected 2 bytes";
}
} else if (_state == SOCKSState.AuthStarted) {
if (_auth.contains(AuthMethods.UsernamePassword)) {
final version = data[0];
final status = data[1];
if (version != RFC1929Version || status != 0x00) {
throw "Invalid username or password";
} else {
_setState(SOCKSState.RequestReady);
_writeRequest(_request);
}
}
} else if (_state == SOCKSState.RequestReady) {
if (data.length >= 10) {
final reply = SOCKSReply._(data[1]);
//data[2] reserved
// final version = data[0];
// final addrType = SOCKSAddressType._(data[3]);
// Uint8List addr;
// var port = 0;
// if (addrType == SOCKSAddressType.Domain) {
// final len = data[4];
// addr = data.sublist(5, 5 + len);
// port = data[5 + len] << 8 | data[6 + len];
// } else if (addrType == SOCKSAddressType.IPv4) {
// addr = data.sublist(5, 9);
// port = data[9] << 8 | data[10];
// } else if (addrType == SOCKSAddressType.IPv6) {
// addr = data.sublist(5, 21);
// port = data[21] << 8 | data[22];
// }
// print("<< Version: $version, Reply: $reply, AddrType: $addrType, Addr: $addr, Port: $port");
if (reply._value == SOCKSReply.Success._value) {
_setState(SOCKSState.Connected);
} else {
throw reply;
}
} else {
throw "Expected 10 bytes";
}
}
}
void _writeRequest(SOCKSRequest req) {
if (_state == SOCKSState.RequestReady) {
final data = [
req.version,
req.command._value,
0x00,
req.addressType._value,
if (req.addressType == SOCKSAddressType.Domain)
req.address.lengthInBytes,
...req.address,
req.port >> 8,
req.port & 0xF0,
];
//print(">> Version: ${req.version}, Command: ${req.command}, AddrType: ${req.addressType}, Addr: ${req.getAddressString()}, Port: ${req.port}");
_sock.write(data);
} else {
throw "Must be in RequestReady state, current state $_state";
}
}
}