2023-08-16 03:56:41 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'dart:io';
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// 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)
|
2023-08-16 03:56:41 +00:00
|
|
|
class SOCKSSocket {
|
2023-09-06 22:27:59 +00:00
|
|
|
/// The host of the SOCKS5 proxy server.
|
2023-08-16 03:56:41 +00:00
|
|
|
final String proxyHost;
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
/// The port of the SOCKS5 proxy server.
|
2023-08-16 03:56:41 +00:00
|
|
|
final int proxyPort;
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// The underlying Socket that connects to the SOCKS5 proxy server.
|
2023-08-16 03:56:41 +00:00
|
|
|
late final Socket _socksSocket;
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// 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.
|
2023-08-16 03:56:41 +00:00
|
|
|
final StreamController<List<int>> _responseController =
|
|
|
|
StreamController.broadcast();
|
|
|
|
|
2023-09-06 23:17:28 +00:00
|
|
|
/// A StreamController that listens to the _secureSocksSocket and broadcasts.
|
|
|
|
final StreamController<List<int>> _secureResponseController =
|
|
|
|
StreamController.broadcast();
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// Getter for the StreamController that listens to the _socksSocket and
|
2023-09-06 22:48:50 +00:00
|
|
|
/// broadcasts, or the _secureSocksSocket and broadcasts if SSL is enabled.
|
2023-09-06 22:27:59 +00:00
|
|
|
StreamController<List<int>> get responseController =>
|
|
|
|
sslEnabled ? _secureResponseController : _responseController;
|
|
|
|
|
2023-09-06 23:17:28 +00:00
|
|
|
/// A StreamSubscription that listens to the _socksSocket or the
|
|
|
|
/// _secureSocksSocket if SSL is enabled.
|
|
|
|
StreamSubscription<List<int>>? _subscription;
|
2023-09-06 22:48:50 +00:00
|
|
|
|
2023-09-06 23:17:28 +00:00
|
|
|
/// Getter for the StreamSubscription that listens to the _socksSocket or the
|
|
|
|
/// _secureSocksSocket if SSL is enabled.
|
|
|
|
StreamSubscription<List<int>>? get subscription => _subscription;
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
/// Is SSL enabled?
|
|
|
|
final bool sslEnabled;
|
2023-08-16 03:56:41 +00:00
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// 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.
|
2023-08-16 03:56:41 +00:00
|
|
|
static Future<SOCKSSocket> create(
|
2023-09-06 22:27:59 +00:00
|
|
|
{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.
|
2023-08-16 03:56:41 +00:00
|
|
|
await instance._init();
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
// Return the SOCKS socket instance.
|
2023-08-16 03:56:41 +00:00
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
/// Constructor.
|
|
|
|
SOCKSSocket(
|
|
|
|
{required this.proxyHost,
|
|
|
|
required this.proxyPort,
|
|
|
|
required this.sslEnabled}) {
|
2023-08-16 03:56:41 +00:00
|
|
|
_init();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Initializes the SOCKS socket.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// This method is a private method that is called by the constructor.
|
|
|
|
///
|
|
|
|
/// Returns:
|
|
|
|
/// A Future that resolves to void.
|
2023-08-16 03:56:41 +00:00
|
|
|
Future<void> _init() async {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Connect to the SOCKS proxy server.
|
2023-08-16 03:56:41 +00:00
|
|
|
_socksSocket = await Socket.connect(
|
|
|
|
proxyHost,
|
|
|
|
proxyPort,
|
|
|
|
);
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
// Listen to the socket.
|
2023-09-06 22:48:50 +00:00
|
|
|
_subscription = _socksSocket.listen(
|
2023-08-16 03:56:41 +00:00
|
|
|
(data) {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Add the data to the response controller.
|
2023-08-16 03:56:41 +00:00
|
|
|
_responseController.add(data);
|
|
|
|
},
|
2023-09-06 22:27:59 +00:00
|
|
|
onError: (e) {
|
|
|
|
// Handle errors.
|
2023-08-16 03:56:41 +00:00
|
|
|
if (e is Object) {
|
|
|
|
_responseController.addError(e);
|
|
|
|
}
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
// If the error is not an object, send the error as a string.
|
2023-08-16 03:56:41 +00:00
|
|
|
_responseController.addError("$e");
|
2023-09-06 22:27:59 +00:00
|
|
|
// TODO make sure sending error as string is acceptable.
|
2023-08-16 03:56:41 +00:00
|
|
|
},
|
|
|
|
onDone: () {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Close the response controller when the socket is closed.
|
2023-08-16 03:56:41 +00:00
|
|
|
_responseController.close();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Connects to the SOCKS socket.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// Returns:
|
|
|
|
/// A Future that resolves to void.
|
2023-08-16 03:56:41 +00:00
|
|
|
Future<void> connect() async {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Greeting and method selection.
|
2023-08-16 03:56:41 +00:00
|
|
|
_socksSocket.add([0x05, 0x01, 0x00]);
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
// Wait for server response.
|
2023-08-16 03:56:41 +00:00
|
|
|
var response = await _responseController.stream.first;
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
// Check if the connection was successful.
|
2023-08-16 03:56:41 +00:00
|
|
|
if (response[1] != 0x00) {
|
2023-09-06 22:27:59 +00:00
|
|
|
throw Exception(
|
|
|
|
'socks_socket.connect(): Failed to connect to SOCKS5 proxy.');
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Connects to the specified [domain] and [port] through the SOCKS socket.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// Parameters:
|
|
|
|
/// - [domain]: The domain to connect to.
|
|
|
|
/// - [port]: The port to connect to.
|
|
|
|
///
|
|
|
|
/// Returns:
|
|
|
|
/// A Future that resolves to void.
|
2023-08-16 03:56:41 +00:00
|
|
|
Future<void> connectTo(String domain, int port) async {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Connect command.
|
2023-08-16 03:56:41 +00:00
|
|
|
var request = [
|
2023-09-06 22:27:59 +00:00
|
|
|
0x05, // SOCKS version.
|
|
|
|
0x01, // Connect command.
|
|
|
|
0x00, // Reserved.
|
|
|
|
0x03, // Domain name.
|
2023-08-16 03:56:41 +00:00
|
|
|
domain.length,
|
|
|
|
...domain.codeUnits,
|
|
|
|
(port >> 8) & 0xFF,
|
|
|
|
port & 0xFF
|
|
|
|
];
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
// Send the connect command to the SOCKS proxy server.
|
2023-08-16 03:56:41 +00:00
|
|
|
_socksSocket.add(request);
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
// Wait for server response.
|
2023-08-16 03:56:41 +00:00
|
|
|
var response = await _responseController.stream.first;
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
// Check if the connection was successful.
|
2023-08-16 03:56:41 +00:00
|
|
|
if (response[1] != 0x00) {
|
2023-09-06 22:27:59 +00:00
|
|
|
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.
|
2023-09-06 22:48:50 +00:00
|
|
|
_subscription = _secureSocksSocket.listen(
|
2023-09-06 22:27:59 +00:00
|
|
|
(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();
|
|
|
|
},
|
|
|
|
);
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|
2023-09-06 22:27:59 +00:00
|
|
|
|
|
|
|
return;
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts [object] to a String by invoking [Object.toString] and
|
|
|
|
/// sends the encoding of the result to the socket.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// Parameters:
|
|
|
|
/// - [object]: The object to write to the socket.
|
|
|
|
///
|
|
|
|
/// Returns:
|
|
|
|
/// A Future that resolves to void.
|
2023-08-16 03:56:41 +00:00
|
|
|
void write(Object? object) {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Don't write null.
|
2023-08-16 03:56:41 +00:00
|
|
|
if (object == null) return;
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
// Write the data to the socket.
|
2023-08-16 03:56:41 +00:00
|
|
|
List<int> data = utf8.encode(object.toString());
|
2023-09-06 22:27:59 +00:00
|
|
|
if (sslEnabled) {
|
|
|
|
_secureSocksSocket.add(data);
|
|
|
|
} else {
|
|
|
|
_socksSocket.add(data);
|
|
|
|
}
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Sends the server.features command to the proxy server.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// 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.
|
2023-08-16 03:56:41 +00:00
|
|
|
Future<void> sendServerFeaturesCommand() async {
|
2023-09-06 22:27:59 +00:00
|
|
|
// The server.features command.
|
2023-08-16 03:56:41 +00:00
|
|
|
const String command =
|
|
|
|
'{"jsonrpc":"2.0","id":"0","method":"server.features","params":[]}';
|
|
|
|
|
2023-09-06 22:27:59 +00:00
|
|
|
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;
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Closes the connection to the Tor proxy.
|
2023-09-06 22:27:59 +00:00
|
|
|
///
|
|
|
|
/// Returns:
|
|
|
|
/// A Future that resolves to void.
|
2023-08-16 03:56:41 +00:00
|
|
|
Future<void> close() async {
|
2023-09-06 22:27:59 +00:00
|
|
|
// Ensure all data is sent before closing.
|
|
|
|
//
|
|
|
|
// TODO test this.
|
|
|
|
if (sslEnabled) {
|
|
|
|
await _socksSocket.flush();
|
|
|
|
await _secureResponseController.close();
|
|
|
|
}
|
|
|
|
await _socksSocket.flush();
|
2023-08-16 03:56:41 +00:00
|
|
|
await _responseController.close();
|
|
|
|
return await _socksSocket.close();
|
|
|
|
}
|
2023-09-06 22:48:50 +00:00
|
|
|
|
|
|
|
StreamSubscription<List<int>> listen(
|
|
|
|
void Function(List<int> data)? onData, {
|
|
|
|
Function? onError,
|
|
|
|
void Function()? onDone,
|
|
|
|
bool? cancelOnError,
|
|
|
|
}) {
|
|
|
|
return sslEnabled
|
|
|
|
? _secureResponseController.stream.listen(
|
|
|
|
onData,
|
|
|
|
onError: onError,
|
|
|
|
onDone: onDone,
|
|
|
|
cancelOnError: cancelOnError,
|
|
|
|
)
|
|
|
|
: _responseController.stream.listen(
|
|
|
|
onData,
|
|
|
|
onError: onError,
|
|
|
|
onDone: onDone,
|
|
|
|
cancelOnError: cancelOnError,
|
|
|
|
);
|
|
|
|
}
|
2023-08-16 03:56:41 +00:00
|
|
|
}
|