WIP update SOCKSSocket class

This commit is contained in:
sneurlax 2023-09-06 17:27:59 -05:00
parent e6e5c43f69
commit 7a01682bff
2 changed files with 234 additions and 41 deletions

View file

@ -86,7 +86,8 @@ class JsonRPC {
_socket!.write('${req.jsonRequest}\r\n');
}
if (_socksSocket != null) {
_socksSocket!.socket.writeln('${req.jsonRequest}\r\n');
_socksSocket!.write('${req.jsonRequest}\r\n\n');
// _socksSocket!.socket.writeln('${req.jsonRequest}\r\n');
}
// TODO different timeout length?
@ -203,27 +204,17 @@ class JsonRPC {
"ElectrumX.connect(): no tor proxy info, read $proxyInfo",
level: LogLevel.Warning);
}
// TODO connect to proxy socket...
// 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,
sslEnabled: useSSL,
);
try {
Logging.instance.log(
"JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo...",
"JsonRPC.connect(): connecting to SOCKS socket at $proxyInfo (SSL $useSSL)...",
level: LogLevel.Info);
await _socksSocket?.connect();
@ -257,12 +248,12 @@ class JsonRPC {
"JsonRPC.connect(): failed to connect to tor proxy, $e");
}
// _subscription = _socksSocket!.socket.listen(
// _dataHandler,
// onError: _errorHandler,
// onDone: _doneHandler,
// cancelOnError: true,
// ) as StreamSubscription<Uint8List>?;
_subscription = _socksSocket!.socket.listen(
_dataHandler,
onError: _errorHandler,
onDone: _doneHandler,
cancelOnError: true,
);
}
}
}

View file

@ -2,110 +2,312 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
/// A SOCKS5 socket.
///
/// This class is a wrapper around a Socket that connects to a SOCKS5 proxy
/// server and sends all data through the proxy.
///
/// This class is used to connect to the Tor proxy server.
///
/// Attributes:
/// - [proxyHost]: The host of the SOCKS5 proxy server.
/// - [proxyPort]: The port of the SOCKS5 proxy server.
/// - [_socksSocket]: The underlying [Socket] that connects to the SOCKS5 proxy
/// server.
/// - [_responseController]: A [StreamController] that listens to the
/// [_socksSocket] and broadcasts the response.
///
/// Methods:
/// - connect: Connects to the SOCKS5 proxy server.
/// - connectTo: Connects to the specified [domain] and [port] through the
/// SOCKS5 proxy server.
/// - write: Converts [object] to a String by invoking [Object.toString] and
/// sends the encoding of the result to the socket.
/// - sendServerFeaturesCommand: Sends the server.features command to the
/// proxy server.
/// - close: Closes the connection to the Tor proxy.
///
/// Usage:
/// ```dart
/// // Instantiate a socks socket at localhost and on the port selected by the
/// // tor service.
/// var socksSocket = await SOCKSSocket.create(
/// proxyHost: InternetAddress.loopbackIPv4.address,
/// proxyPort: tor.port,
/// // sslEnabled: true, // For SSL connections.
/// );
///
/// // Connect to the socks instantiated above.
/// await socksSocket.connect();
///
/// // Connect to bitcoincash.stackwallet.com on port 50001 via socks socket.
/// await socksSocket.connectTo(
/// 'bitcoincash.stackwallet.com', 50001);
///
/// // Send a server features command to the connected socket, see method for
/// // more specific usage example..
/// await socksSocket.sendServerFeaturesCommand();
/// await socksSocket.close();
/// ```
///
/// See also:
/// - SOCKS5 protocol(https://www.ietf.org/rfc/rfc1928.txt)
class SOCKSSocket {
/// The host of the SOCKS5 proxy server.
final String proxyHost;
/// The port of the SOCKS5 proxy server.
final int proxyPort;
/// The underlying Socket that connects to the SOCKS5 proxy server.
late final Socket _socksSocket;
Socket get socket => _socksSocket;
/// Getter for the underlying Socket that connects to the SOCKS5 proxy server.
Socket get socket => sslEnabled ? _secureSocksSocket : _socksSocket;
/// A wrapper around the _socksSocket that enables SSL connections.
late final Socket _secureSocksSocket;
/// A StreamController that listens to the _socksSocket and broadcasts.
final StreamController<List<int>> _responseController =
StreamController.broadcast();
// Private constructor
SOCKSSocket._(this.proxyHost, this.proxyPort);
/// Getter for the StreamController that listens to the _socksSocket and
/// broadcasts.
StreamController<List<int>> get responseController =>
sslEnabled ? _secureResponseController : _responseController;
/// A StreamController that listens to the _secureSocksSocket and broadcasts.
final StreamController<List<int>> _secureResponseController =
StreamController.broadcast();
/// Is SSL enabled?
final bool sslEnabled;
/// Private constructor.
SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled);
/// Creates a SOCKS5 socket to the specified [proxyHost] and [proxyPort].
///
/// This method is a factory constructor that returns a Future that resolves
/// to a SOCKSSocket instance.
///
/// Parameters:
/// - [proxyHost]: The host of the SOCKS5 proxy server.
/// - [proxyPort]: The port of the SOCKS5 proxy server.
///
/// Returns:
/// A Future that resolves to a SOCKSSocket instance.
static Future<SOCKSSocket> create(
{required String proxyHost, required int proxyPort}) async {
var instance = SOCKSSocket._(proxyHost, proxyPort);
{required String proxyHost,
required int proxyPort,
bool sslEnabled = false}) async {
// Create a SOCKS socket instance.
var instance = SOCKSSocket._(proxyHost, proxyPort, sslEnabled);
// Initialize the SOCKS socket.
await instance._init();
// Return the SOCKS socket instance.
return instance;
}
SOCKSSocket({required this.proxyHost, required this.proxyPort}) {
/// Constructor.
SOCKSSocket(
{required this.proxyHost,
required this.proxyPort,
required this.sslEnabled}) {
_init();
}
/// Initializes the SOCKS socket.
///
/// This method is a private method that is called by the constructor.
///
/// Returns:
/// A Future that resolves to void.
Future<void> _init() async {
// Connect to the SOCKS proxy server.
_socksSocket = await Socket.connect(
proxyHost,
proxyPort,
);
// Listen to the socket.
_socksSocket.listen(
(data) {
// Add the data to the response controller.
_responseController.add(data);
},
onError: (dynamic e) {
onError: (e) {
// Handle errors.
if (e is Object) {
_responseController.addError(e);
}
// If the error is not an object, send the error as a string.
_responseController.addError("$e");
// TODO make sure sending error as string is acceptable
// TODO make sure sending error as string is acceptable.
},
onDone: () {
// Close the response controller when the socket is closed.
_responseController.close();
},
);
}
/// Connects to the SOCKS socket.
///
/// Returns:
/// A Future that resolves to void.
Future<void> connect() async {
// Greeting and method selection
// Greeting and method selection.
_socksSocket.add([0x05, 0x01, 0x00]);
// Wait for server response
// Wait for server response.
var response = await _responseController.stream.first;
// Check if the connection was successful.
if (response[1] != 0x00) {
throw Exception('Failed to connect to SOCKS5 socket.');
throw Exception(
'socks_socket.connect(): Failed to connect to SOCKS5 proxy.');
}
}
/// Connects to the specified [domain] and [port] through the SOCKS socket.
///
/// Parameters:
/// - [domain]: The domain to connect to.
/// - [port]: The port to connect to.
///
/// Returns:
/// A Future that resolves to void.
Future<void> connectTo(String domain, int port) async {
// Connect command.
var request = [
0x05,
0x01,
0x00,
0x03,
0x05, // SOCKS version.
0x01, // Connect command.
0x00, // Reserved.
0x03, // Domain name.
domain.length,
...domain.codeUnits,
(port >> 8) & 0xFF,
port & 0xFF
];
// Send the connect command to the SOCKS proxy server.
_socksSocket.add(request);
// Wait for server response.
var response = await _responseController.stream.first;
// Check if the connection was successful.
if (response[1] != 0x00) {
throw Exception('Failed to connect to target through SOCKS5 proxy.');
throw Exception(
'socks_socket.connectTo(): Failed to connect to target through SOCKS5 proxy.');
}
// Upgrade to SSL if needed
if (sslEnabled) {
// Upgrade to SSL.
_secureSocksSocket = await SecureSocket.secure(
_socksSocket,
host: domain,
// onBadCertificate: (_) => true, // Uncomment this to bypass certificate validation (NOT recommended for production).
);
// Listen to the secure socket.
_secureSocksSocket.listen(
(data) {
// Add the data to the response controller.
_secureResponseController.add(data);
},
onError: (e) {
// Handle errors.
if (e is Object) {
_secureResponseController.addError(e);
}
// If the error is not an object, send the error as a string.
_secureResponseController.addError("$e");
// TODO make sure sending error as string is acceptable.
},
onDone: () {
// Close the response controller when the socket is closed.
_secureResponseController.close();
},
);
}
return;
}
/// Converts [object] to a String by invoking [Object.toString] and
/// sends the encoding of the result to the socket.
///
/// Parameters:
/// - [object]: The object to write to the socket.
///
/// Returns:
/// A Future that resolves to void.
void write(Object? object) {
// Don't write null.
if (object == null) return;
// Write the data to the socket.
List<int> data = utf8.encode(object.toString());
_socksSocket.add(data);
if (sslEnabled) {
_secureSocksSocket.add(data);
} else {
_socksSocket.add(data);
}
}
/// Sends the server.features command to the proxy server.
///
/// This method is used to send the server.features command to the proxy
/// server. This command is used to request the features of the proxy server.
/// It serves as a demonstration of how to send commands to the proxy server.
///
/// Returns:
/// A Future that resolves to void.
Future<void> sendServerFeaturesCommand() async {
// The server.features command.
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)}");
if (!sslEnabled) {
// Send the command to the proxy server.
_socksSocket.writeln(command);
// Wait for the response from the proxy server.
var responseData = await _responseController.stream.first;
print("responseData: ${utf8.decode(responseData)}");
} else {
// Send the command to the proxy server.
_secureSocksSocket.writeln(command);
// Wait for the response from the proxy server.
var responseData = await _secureResponseController.stream.first;
print("secure responseData: ${utf8.decode(responseData)}");
}
return;
}
/// Closes the connection to the Tor proxy.
///
/// Returns:
/// A Future that resolves to void.
Future<void> close() async {
await _socksSocket.flush(); // Ensure all data is sent before closing
// Ensure all data is sent before closing.
//
// TODO test this.
if (sslEnabled) {
await _socksSocket.flush();
await _secureResponseController.close();
}
await _socksSocket.flush();
await _responseController.close();
return await _socksSocket.close();
}