mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-11-17 09:47:37 +00:00
clean up SOCKSSocket, still no joy
This commit is contained in:
parent
8492773438
commit
c7ea583a44
3 changed files with 208 additions and 564 deletions
|
@ -14,7 +14,7 @@ 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/socks5.dart';
|
import 'package:stackwallet/networking/socks_socket.dart';
|
||||||
import 'package:stackwallet/networking/tor_service.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';
|
||||||
|
@ -38,7 +38,6 @@ class JsonRPC {
|
||||||
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
|
final _JsonRPCRequestQueue _requestQueue = _JsonRPCRequestQueue();
|
||||||
Socket? _socket;
|
Socket? _socket;
|
||||||
SOCKSSocket? _socksSocket;
|
SOCKSSocket? _socksSocket;
|
||||||
SOCKS5Proxy? _socksProxy;
|
|
||||||
StreamSubscription<Uint8List>? _subscription;
|
StreamSubscription<Uint8List>? _subscription;
|
||||||
|
|
||||||
void _dataHandler(List<int> data) {
|
void _dataHandler(List<int> data) {
|
||||||
|
@ -81,22 +80,18 @@ class JsonRPC {
|
||||||
void _sendNextAvailableRequest() {
|
void _sendNextAvailableRequest() {
|
||||||
_requestQueue.nextIncompleteReq.then((req) {
|
_requestQueue.nextIncompleteReq.then((req) {
|
||||||
if (req != null) {
|
if (req != null) {
|
||||||
|
// TODO check if useTor to determine socket to which to write
|
||||||
// \r\n required by electrumx server
|
// \r\n required by electrumx server
|
||||||
if (_socket != null) {
|
if (_socket != null) {
|
||||||
_socket!.write('${req.jsonRequest}\r\n');
|
_socket!.write('${req.jsonRequest}\r\n');
|
||||||
}
|
}
|
||||||
if (_socksSocket != null) {
|
if (_socksSocket != null) {
|
||||||
print('writing to _socksSocket: ${req.jsonRequest}');
|
Logging.instance.log(
|
||||||
print(req.jsonRequest);
|
"writing to _socksSocket: ${req.jsonRequest}",
|
||||||
print(req);
|
level: LogLevel.Info,
|
||||||
|
);
|
||||||
_socksSocket!.write('${req.jsonRequest}\r\n');
|
_socksSocket!.write('${req.jsonRequest}\r\n');
|
||||||
}
|
}
|
||||||
if (_socksProxy != null) {
|
|
||||||
print('writing to _socksProxy: ${req.jsonRequest}');
|
|
||||||
print(req.jsonRequest);
|
|
||||||
print(req);
|
|
||||||
_socksProxy!.write('${req.jsonRequest}\r\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO different timeout length?
|
// TODO different timeout length?
|
||||||
req.initiateTimeout(
|
req.initiateTimeout(
|
||||||
|
@ -116,22 +111,15 @@ class JsonRPC {
|
||||||
if (!Prefs.instance.useTor) {
|
if (!Prefs.instance.useTor) {
|
||||||
if (_socket == null) {
|
if (_socket == null) {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"JsonRPC request: opening socket $host:$port",
|
"JsonRPC request: opening direct socket $host:$port",
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
);
|
);
|
||||||
// await connect();
|
await connect();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if (_socksSocket == null) {
|
if (_socksSocket == null) {
|
||||||
// Logging.instance.log(
|
|
||||||
// "JsonRPC request: opening SOCKS socket to $host:$port",
|
|
||||||
// level: LogLevel.Info,
|
|
||||||
// );
|
|
||||||
// await connect();
|
|
||||||
// }
|
|
||||||
if (_socksProxy == null) {
|
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"JsonRPC request: opening SOCKS proxy to $host:$port",
|
"JsonRPC request: opening SOCKS socket to $host:$port",
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
);
|
);
|
||||||
await connect();
|
await connect();
|
||||||
|
@ -175,8 +163,8 @@ class JsonRPC {
|
||||||
_subscription = null;
|
_subscription = null;
|
||||||
_socket?.destroy();
|
_socket?.destroy();
|
||||||
_socket = null;
|
_socket = null;
|
||||||
unawaited(_socksSocket?.close(keepOpen: false));
|
unawaited(_socksSocket?.close());
|
||||||
// TODO check that it's ok to not await this
|
// TODO check that we can safely unawaited this
|
||||||
_socksSocket = null;
|
_socksSocket = null;
|
||||||
|
|
||||||
// clean up remaining queue
|
// clean up remaining queue
|
||||||
|
@ -210,158 +198,98 @@ class JsonRPC {
|
||||||
cancelOnError: true,
|
cancelOnError: true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (_socksProxy == null) {
|
if (proxyInfo == null) {
|
||||||
print(1111111);
|
// TODO await tor / make sure it's running
|
||||||
_socksProxy = SOCKS5Proxy();
|
proxyInfo = (
|
||||||
// TODO check if null
|
host: InternetAddress.loopbackIPv4.address,
|
||||||
await _socksProxy!.connect();
|
port: TorService.sharedInstance.port
|
||||||
print(222222);
|
|
||||||
// TODO check if null
|
|
||||||
await _socksProxy!.connectTo('bitcoincash.stackwallet.com', 50002);
|
|
||||||
|
|
||||||
print(333333);
|
|
||||||
|
|
||||||
// TODO check if null
|
|
||||||
_subscription = _socksProxy!.socket.listen(
|
|
||||||
_dataHandler,
|
|
||||||
onError: _errorHandler,
|
|
||||||
onDone: _doneHandler,
|
|
||||||
cancelOnError: true,
|
|
||||||
);
|
);
|
||||||
} else {
|
Logging.instance.log(
|
||||||
print('0000000');
|
"ElectrumX.connect(): no tor proxy info, read $proxyInfo",
|
||||||
|
level: LogLevel.Warning);
|
||||||
}
|
}
|
||||||
// if (proxyInfo == null) {
|
// TODO connect to proxy socket...
|
||||||
// // TODO await tor / make sure it's running
|
|
||||||
// proxyInfo = (
|
// TODO implement ssl over tor
|
||||||
// host: InternetAddress.loopbackIPv4.address,
|
// if (useSSL) {
|
||||||
// port: TorService.sharedInstance.port
|
// _socket = await SecureSocket.connect(
|
||||||
// );
|
// host,
|
||||||
// Logging.instance.log(
|
// port,
|
||||||
// "ElectrumX.connect(): no tor proxy info, read $proxyInfo",
|
// timeout: connectionTimeout,
|
||||||
// level: LogLevel.Warning);
|
// onBadCertificate: (_) => true,
|
||||||
// }
|
// ); // TODO do not automatically trust bad certificates
|
||||||
// // TODO connect to proxy socket...
|
// final _client = SocksSocket.protected(_socket, type);
|
||||||
//
|
|
||||||
// // 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 {
|
|
||||||
// final sock = await RawSocket.connect(
|
|
||||||
// InternetAddress.loopbackIPv4, proxyInfo!.port);
|
|
||||||
//
|
|
||||||
// if (_socksSocket == null) {
|
|
||||||
// Logging.instance.log(
|
|
||||||
// "JsonRPC.connect(): creating SOCKS socket at $proxyInfo",
|
|
||||||
// level: LogLevel.Info);
|
|
||||||
// _socksSocket = SOCKSSocket(sock);
|
|
||||||
// if (_socksSocket == null) {
|
|
||||||
// Logging.instance.log(
|
|
||||||
// "JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo",
|
|
||||||
// level: LogLevel.Error);
|
|
||||||
// throw Exception(
|
|
||||||
// "JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo");
|
|
||||||
// } else {
|
|
||||||
// Logging.instance.log(
|
|
||||||
// "JsonRPC.connect(): created SOCKS socket at $proxyInfo",
|
|
||||||
// level: LogLevel.Info);
|
|
||||||
// }
|
|
||||||
// } else {
|
// } else {
|
||||||
// // TODO also check if sock == previous sock, eg. if RawSocket is different
|
// set up socks socket
|
||||||
// Logging.instance.log(
|
if (_socksSocket == null) {
|
||||||
// "JsonRPC.connect(): using pre-existing SOCKS socket at $proxyInfo",
|
Logging.instance.log(
|
||||||
// level: LogLevel.Info);
|
"JsonRPC.connect(): creating SOCKS socket at $proxyInfo",
|
||||||
// }
|
level: LogLevel.Info);
|
||||||
//
|
_socksSocket =
|
||||||
// try {
|
SOCKSSocket(host: proxyInfo!.host, port: proxyInfo!.port);
|
||||||
// Logging.instance.log(
|
if (_socksSocket == null) {
|
||||||
// "JsonRPC.connect(): connecting to $host:$port over SOCKS socket at $proxyInfo...",
|
Logging.instance.log(
|
||||||
// level: LogLevel.Info);
|
"JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo",
|
||||||
// if (!isIpAddress(host)) {
|
level: LogLevel.Error);
|
||||||
// await _socksSocket!.connect("$host:$port");
|
throw Exception(
|
||||||
// } else {
|
"JsonRPC.connect(): failed to create SOCKS socket at $proxyInfo");
|
||||||
// await _socksSocket!.connectIp(InternetAddress(host), port);
|
} else {
|
||||||
// }
|
Logging.instance.log(
|
||||||
// Logging.instance.log(
|
"JsonRPC.connect(): created SOCKS socket at $proxyInfo",
|
||||||
// "JsonRPC.connect(): connected to $host:$port over SOCKS socket at $proxyInfo",
|
level: LogLevel.Info);
|
||||||
// level: LogLevel.Info);
|
}
|
||||||
// } catch (e) {
|
} else {
|
||||||
// Logging.instance.log(
|
// TODO also check if sock == previous sock, eg. if RawSocket is different, and if _socksSocket is valid
|
||||||
// "JsonRPC.connect(): failed to connect to $host over tor proxy at $proxyInfo, $e",
|
Logging.instance.log(
|
||||||
// level: LogLevel.Error);
|
"JsonRPC.connect(): using pre-existing SOCKS socket at $proxyInfo",
|
||||||
// throw Exception(
|
level: LogLevel.Info);
|
||||||
// "JsonRPC.connect(): failed to connect to tor proxy, $e");
|
}
|
||||||
// }
|
|
||||||
|
// connect to socks socket
|
||||||
|
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, $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to node through socks socket
|
||||||
|
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 SOCKS socket at $proxyInfo, $e",
|
||||||
|
level: LogLevel.Error);
|
||||||
|
throw Exception(
|
||||||
|
"JsonRPC.connect(): failed to connect to $host over SOCKS socket, $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check if null
|
||||||
|
_subscription = _socksSocket!.socket.listen(
|
||||||
|
_dataHandler,
|
||||||
|
onError: _errorHandler,
|
||||||
|
onDone: _doneHandler,
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SOCKS5Proxy {
|
|
||||||
final String host;
|
|
||||||
final int port;
|
|
||||||
|
|
||||||
late Socket _socks5Socket;
|
|
||||||
Socket get socket => _socks5Socket;
|
|
||||||
|
|
||||||
SOCKS5Proxy({String? host, int? port})
|
|
||||||
: host = host ?? InternetAddress.loopbackIPv4.address,
|
|
||||||
port = port ?? TorService.sharedInstance.port;
|
|
||||||
|
|
||||||
Future<void> connect() async {
|
|
||||||
_socks5Socket = await Socket.connect(host, port);
|
|
||||||
|
|
||||||
// Greeting and method selection
|
|
||||||
_socks5Socket.add([0x05, 0x01, 0x00]);
|
|
||||||
|
|
||||||
// Wait for server response
|
|
||||||
var response = await _socks5Socket.first;
|
|
||||||
if (response[1] != 0x00) {
|
|
||||||
throw Exception('Failed to connect to SOCKS5 proxy.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just a basic example for domain-based addresses.
|
|
||||||
Future<void> connectTo(String domain, int port) async {
|
|
||||||
// Command, Reserved, Address Type, Address, Port
|
|
||||||
var request = [
|
|
||||||
0x05,
|
|
||||||
0x01,
|
|
||||||
0x00,
|
|
||||||
0x03,
|
|
||||||
domain.length,
|
|
||||||
...domain.codeUnits,
|
|
||||||
(port >> 8) & 0xFF,
|
|
||||||
port & 0xFF
|
|
||||||
];
|
|
||||||
|
|
||||||
_socks5Socket.add(request);
|
|
||||||
print(444444);
|
|
||||||
|
|
||||||
// Wait for server response
|
|
||||||
// var response = await _socks5Socket.first;
|
|
||||||
// if (response[1] != 0x00) {
|
|
||||||
// throw Exception('Failed to connect to target through SOCKS5 proxy.');
|
|
||||||
// }
|
|
||||||
// print(response);
|
|
||||||
print(55555);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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());
|
|
||||||
_socks5Socket.add(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _JsonRPCRequestQueue {
|
class _JsonRPCRequestQueue {
|
||||||
final _lock = Mutex();
|
final _lock = Mutex();
|
||||||
final List<_JsonRPCRequest> _rq = [];
|
final List<_JsonRPCRequest> _rq = [];
|
||||||
|
|
|
@ -1,395 +0,0 @@
|
||||||
// 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';
|
|
||||||
|
|
||||||
import 'package:stackwallet/utilities/logger.dart';
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
Logging.instance.log(
|
|
||||||
"SOCKSSocket._start(): unhandled event RawSocketEvent.readClosed",
|
|
||||||
level: LogLevel.Warning);
|
|
||||||
break;
|
|
||||||
case RawSocketEvent.write:
|
|
||||||
// TODO: Handle this case.
|
|
||||||
|
|
||||||
Logging.instance.log(
|
|
||||||
"SOCKSSocket._start(): unhandled event RawSocketEvent.write",
|
|
||||||
level: LogLevel.Warning);
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void write(Object? object) {
|
|
||||||
_sock.write(utf8.encode(object.toString()));
|
|
||||||
// TODO make sure the is correct; see _writeRequest above, may need to construct a SOCKSRequest from the data coming in
|
|
||||||
}
|
|
||||||
}
|
|
111
lib/networking/socks_socket.dart
Normal file
111
lib/networking/socks_socket.dart
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// A SOCKS5 proxy client
|
||||||
|
class SOCKSSocket {
|
||||||
|
final String host;
|
||||||
|
final int port;
|
||||||
|
|
||||||
|
late Socket _socksSocket;
|
||||||
|
Socket get socket => _socksSocket;
|
||||||
|
|
||||||
|
final StreamController<List<int>> _responseController = StreamController();
|
||||||
|
|
||||||
|
// TODO accept String host or InternetAddress host
|
||||||
|
SOCKSSocket({required this.host, required this.port}) {
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the SOCKS socket.
|
||||||
|
Future<void> _init() async {
|
||||||
|
_socksSocket = await Socket.connect(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
|
||||||
|
_socksSocket.listen(
|
||||||
|
(data) {
|
||||||
|
_responseController.add(data);
|
||||||
|
},
|
||||||
|
onError: (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 {
|
||||||
|
_socksSocket = await Socket.connect(host, port);
|
||||||
|
|
||||||
|
// Greeting and method selection
|
||||||
|
_socksSocket.add([0x05, 0x01, 0x00]);
|
||||||
|
|
||||||
|
// Wait for server response
|
||||||
|
var response = await _socksSocket.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 {
|
||||||
|
// Command, Reserved, Address Type, Address, Port
|
||||||
|
var request = [
|
||||||
|
0x05,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x03,
|
||||||
|
domain.length,
|
||||||
|
...domain.codeUnits,
|
||||||
|
(port >> 8) & 0xFF,
|
||||||
|
port & 0xFF
|
||||||
|
];
|
||||||
|
|
||||||
|
_socksSocket.add(request);
|
||||||
|
|
||||||
|
if (await _responseController.stream.isEmpty) {
|
||||||
|
throw Exception(
|
||||||
|
'Stream has no data: Failed to connect to target through SOCKS5 proxy.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// // Wait for server response
|
||||||
|
// var response;
|
||||||
|
// try {
|
||||||
|
// response = await _responseController.stream.first
|
||||||
|
// .timeout(const Duration(seconds: 10), onTimeout: () {
|
||||||
|
// throw TimeoutException(
|
||||||
|
// 'Failed to get response from the server within 10 seconds.');
|
||||||
|
// });
|
||||||
|
// } catch (e) {
|
||||||
|
// throw Exception('Failed to connect to target through SOCKS5 proxy: $e');
|
||||||
|
// }
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue