reorganize tor package
This commit is contained in:
@@ -31,7 +31,7 @@ analyzer:
|
|||||||
- "**.swagger.dart"
|
- "**.swagger.dart"
|
||||||
- "**.freezed.dart"
|
- "**.freezed.dart"
|
||||||
- "**.chopper.dart"
|
- "**.chopper.dart"
|
||||||
- "lib/generated_bindings.dart"
|
- "lib/src/generated_bindings.dart"
|
||||||
# For more information about the core and recommended set of lints, see
|
# For more information about the core and recommended set of lints, see
|
||||||
# https://dart.dev/go/core-lints
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
# Run with `flutter pub run ffigen --config ffigen.yaml`.
|
|
||||||
name: NativeLibrary
|
|
||||||
description: |
|
|
||||||
Bindings for `tor.h`.
|
|
||||||
|
|
||||||
Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.
|
|
||||||
output: 'lib/tor_bindings_generated.dart'
|
|
||||||
headers:
|
|
||||||
entry-points:
|
|
||||||
- 'rust/target/tor.h'
|
|
||||||
include-directives:
|
|
||||||
- 'rust/target/tor.h'
|
|
||||||
preamble: |
|
|
||||||
// ignore_for_file: always_specify_types
|
|
||||||
// ignore_for_file: camel_case_types
|
|
||||||
// ignore_for_file: non_constant_identifier_names
|
|
||||||
comments:
|
|
||||||
style: any
|
|
||||||
length: full
|
|
||||||
@@ -1,376 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Cypher Stack LLC
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
/// A SOCKS5 socket.
|
|
||||||
///
|
|
||||||
/// A Dart 3 Socket wrapper that implements the SOCKS5 protocol. Now with SSL!
|
|
||||||
///
|
|
||||||
/// Properties:
|
|
||||||
/// - [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;
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
|
|
||||||
/// A StreamController that listens to the _secureSocksSocket and broadcasts.
|
|
||||||
final StreamController<List<int>> _secureResponseController =
|
|
||||||
StreamController.broadcast();
|
|
||||||
|
|
||||||
/// Getter for the StreamController that listens to the _socksSocket and
|
|
||||||
/// broadcasts, or the _secureSocksSocket and broadcasts if SSL is enabled.
|
|
||||||
StreamController<List<int>> get responseController =>
|
|
||||||
sslEnabled ? _secureResponseController : _responseController;
|
|
||||||
|
|
||||||
/// A StreamSubscription that listens to the _socksSocket or the
|
|
||||||
/// _secureSocksSocket if SSL is enabled.
|
|
||||||
StreamSubscription<List<int>>? _subscription;
|
|
||||||
|
|
||||||
/// Getter for the StreamSubscription that listens to the _socksSocket or the
|
|
||||||
/// _secureSocksSocket if SSL is enabled.
|
|
||||||
StreamSubscription<List<int>>? get subscription => _subscription;
|
|
||||||
|
|
||||||
/// Is SSL enabled?
|
|
||||||
final bool sslEnabled;
|
|
||||||
|
|
||||||
/// Constructor.
|
|
||||||
SOCKSSocket({
|
|
||||||
required this.proxyHost,
|
|
||||||
required this.proxyPort,
|
|
||||||
required this.sslEnabled,
|
|
||||||
}) {
|
|
||||||
unawaited(_init());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Private constructor.
|
|
||||||
SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled);
|
|
||||||
|
|
||||||
/// Provides a stream of data as List`<int>`.
|
|
||||||
Stream<List<int>> get inputStream => sslEnabled
|
|
||||||
? _secureResponseController.stream
|
|
||||||
: _responseController.stream;
|
|
||||||
|
|
||||||
/// Provides a StreamSink compatible with List`<int>` for sending data.
|
|
||||||
StreamSink<List<int>> get outputStream {
|
|
||||||
// Create a simple StreamSink wrapper for _socksSocket and
|
|
||||||
// _secureSocksSocket that accepts List<int> and forwards it to write method.
|
|
||||||
final sink = StreamController<List<int>>();
|
|
||||||
sink.stream.listen((data) {
|
|
||||||
if (sslEnabled) {
|
|
||||||
_secureSocksSocket.add(data);
|
|
||||||
} else {
|
|
||||||
_socksSocket.add(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return sink.sink;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
bool sslEnabled = false,
|
|
||||||
}) async {
|
|
||||||
// Create a SOCKS socket instance.
|
|
||||||
final instance = SOCKSSocket._(proxyHost, proxyPort, sslEnabled);
|
|
||||||
|
|
||||||
// Initialize the SOCKS socket.
|
|
||||||
await instance._init();
|
|
||||||
|
|
||||||
// Return the SOCKS socket instance.
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
_subscription = _socksSocket.listen(
|
|
||||||
(data) {
|
|
||||||
// Add the data to the response controller.
|
|
||||||
_responseController.add(data);
|
|
||||||
},
|
|
||||||
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.
|
|
||||||
},
|
|
||||||
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.
|
|
||||||
_socksSocket.add([0x05, 0x01, 0x00]);
|
|
||||||
|
|
||||||
// Wait for server response.
|
|
||||||
final response = await _responseController.stream.first;
|
|
||||||
|
|
||||||
// Check if the connection was successful.
|
|
||||||
if (response[1] != 0x00) {
|
|
||||||
throw Exception(
|
|
||||||
'socks_socket.connect(): Failed to connect to SOCKS5 proxy.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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.
|
|
||||||
final request = [
|
|
||||||
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.
|
|
||||||
final response = await _responseController.stream.first;
|
|
||||||
|
|
||||||
// Check if the connection was successful.
|
|
||||||
if (response[1] != 0x00) {
|
|
||||||
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.
|
|
||||||
_subscription = _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: () async {
|
|
||||||
// Close the response controller when the socket is closed.
|
|
||||||
await _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.
|
|
||||||
final List<int> data = utf8.encode(object.toString());
|
|
||||||
if (sslEnabled) {
|
|
||||||
_secureSocksSocket.add(data);
|
|
||||||
} else {
|
|
||||||
_socksSocket.add(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the connection to the Tor proxy.
|
|
||||||
///
|
|
||||||
/// Returns:
|
|
||||||
/// A Future that resolves to void.
|
|
||||||
Future<void> close() async {
|
|
||||||
// Ensure all data is sent before closing.
|
|
||||||
try {
|
|
||||||
if (sslEnabled) {
|
|
||||||
await _secureSocksSocket.flush();
|
|
||||||
}
|
|
||||||
await _socksSocket.flush();
|
|
||||||
} finally {
|
|
||||||
await _subscription?.cancel();
|
|
||||||
await _socksSocket.close();
|
|
||||||
await _responseController.close();
|
|
||||||
if (sslEnabled) {
|
|
||||||
await _secureResponseController.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends the server.features command to the proxy server.
|
|
||||||
///
|
|
||||||
/// This demos how to send the server.features command. Use as an example
|
|
||||||
/// for sending other commands.
|
|
||||||
///
|
|
||||||
/// 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":[]}';
|
|
||||||
|
|
||||||
if (!sslEnabled) {
|
|
||||||
// Send the command to the proxy server.
|
|
||||||
_socksSocket.writeln(command);
|
|
||||||
|
|
||||||
// Wait for the response from the proxy server.
|
|
||||||
final responseData = await _responseController.stream.first;
|
|
||||||
if (kDebugMode) {
|
|
||||||
print("responseData: ${utf8.decode(responseData)}");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Send the command to the proxy server.
|
|
||||||
_secureSocksSocket.writeln(command);
|
|
||||||
|
|
||||||
// Wait for the response from the proxy server.
|
|
||||||
final responseData = await _secureResponseController.stream.first;
|
|
||||||
if (kDebugMode) {
|
|
||||||
print("secure responseData: ${utf8.decode(responseData)}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2022 Foundation Devices Inc.
|
||||||
|
// SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ffi';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:ffi/ffi.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:tor/src/generated_bindings.dart' as rust;
|
||||||
|
|
||||||
|
DynamicLibrary load(String name) {
|
||||||
|
if (Platform.isAndroid || Platform.isLinux) {
|
||||||
|
return DynamicLibrary.open('lib$name.so');
|
||||||
|
} else if (Platform.isIOS || Platform.isMacOS) {
|
||||||
|
return DynamicLibrary.open('$name.framework/$name');
|
||||||
|
} else if (Platform.isWindows) {
|
||||||
|
return DynamicLibrary.open('$name.dll');
|
||||||
|
} else {
|
||||||
|
throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CouldntBootstrapDirectory implements Exception {
|
||||||
|
String? rustError;
|
||||||
|
|
||||||
|
CouldntBootstrapDirectory({this.rustError});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotSupportedPlatform implements Exception {
|
||||||
|
String reason;
|
||||||
|
|
||||||
|
NotSupportedPlatform(this.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClientNotActive implements Exception {}
|
||||||
|
|
||||||
|
class Tor {
|
||||||
|
static const String libName = "tor";
|
||||||
|
static late DynamicLibrary _lib;
|
||||||
|
|
||||||
|
Pointer<Void> _clientPtr = nullptr;
|
||||||
|
Pointer<Void> _proxyPtr = nullptr;
|
||||||
|
|
||||||
|
/// Flag to indicate that Tor client and proxy have started. Traffic is routed through the proxy only if it is also [enabled].
|
||||||
|
bool get started => _proxyPort > -1;
|
||||||
|
|
||||||
|
bool get hasClient => _clientPtr != nullptr;
|
||||||
|
|
||||||
|
/// Flag to indicate that traffic should flow through the proxy.
|
||||||
|
bool _enabled = false;
|
||||||
|
|
||||||
|
/// Getter for the enabled flag.
|
||||||
|
bool get enabled => _enabled;
|
||||||
|
|
||||||
|
/// Flag to indicate that a Tor circuit is thought to have been established
|
||||||
|
/// (true means that Tor has bootstrapped).
|
||||||
|
bool get bootstrapped => _bootstrapped;
|
||||||
|
|
||||||
|
/// Getter for the bootstrapped flag.
|
||||||
|
bool _bootstrapped = false;
|
||||||
|
|
||||||
|
/// A stream of Tor events.
|
||||||
|
///
|
||||||
|
/// This stream broadcast just the port for now (-1 if circuit not established or proxy not enabled)
|
||||||
|
final StreamController events = StreamController.broadcast();
|
||||||
|
|
||||||
|
/// Getter for the proxy port.
|
||||||
|
///
|
||||||
|
/// Returns -1 if Tor is not enabled or if the circuit is not established.
|
||||||
|
///
|
||||||
|
/// Returns the proxy port if Tor is enabled and the circuit is established.
|
||||||
|
///
|
||||||
|
/// This is the port that should be used for all requests.
|
||||||
|
int get port {
|
||||||
|
if (!_enabled) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return _proxyPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The proxy port.
|
||||||
|
int _proxyPort = -1;
|
||||||
|
|
||||||
|
/// Singleton instance of the Tor class.
|
||||||
|
static final Tor _instance = Tor._internal();
|
||||||
|
|
||||||
|
/// Getter for the singleton instance of the Tor class.
|
||||||
|
static Tor get instance => _instance;
|
||||||
|
|
||||||
|
/// Initialize the Tor ffi lib instance if it hasn't already been set. Nothing
|
||||||
|
/// changes if _tor is already been set.
|
||||||
|
///
|
||||||
|
/// Returns a Future that completes when the Tor service has started.
|
||||||
|
///
|
||||||
|
/// Throws an exception if the Tor service fails to start.
|
||||||
|
static Tor init({bool enabled = true}) {
|
||||||
|
final singleton = Tor._instance;
|
||||||
|
singleton._enabled = enabled;
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Private constructor for the Tor class.
|
||||||
|
Tor._internal() {
|
||||||
|
_lib = load(libName);
|
||||||
|
|
||||||
|
if (kDebugMode) {
|
||||||
|
print("Instance of Tor created!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void broadcastState() {
|
||||||
|
events.add(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _getRandomUnusedPort({List<int> excluded = const []}) async {
|
||||||
|
final random = Random.secure();
|
||||||
|
int potentialPort = 0;
|
||||||
|
|
||||||
|
retry:
|
||||||
|
while (potentialPort <= 0 || excluded.contains(potentialPort)) {
|
||||||
|
potentialPort = random.nextInt(65535);
|
||||||
|
try {
|
||||||
|
final socket = await ServerSocket.bind("0.0.0.0", potentialPort);
|
||||||
|
await socket.close();
|
||||||
|
return potentialPort;
|
||||||
|
} catch (_) {
|
||||||
|
continue retry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the Tor service.
|
||||||
|
///
|
||||||
|
/// This will start the Tor service and establish a Tor circuit.
|
||||||
|
///
|
||||||
|
/// Throws an exception if the Tor service fails to start.
|
||||||
|
///
|
||||||
|
/// Returns a Future that completes when the Tor service has started.
|
||||||
|
Future<void> start({
|
||||||
|
int? obfs4Port,
|
||||||
|
int? snowflakePort,
|
||||||
|
String? bridgeLines,
|
||||||
|
}) async {
|
||||||
|
broadcastState();
|
||||||
|
|
||||||
|
// Set the state and cache directories.
|
||||||
|
final Directory appSupportDir = await getApplicationSupportDirectory();
|
||||||
|
final stateDir = await Directory(
|
||||||
|
'${appSupportDir.path}/tor_state',
|
||||||
|
).create();
|
||||||
|
final cacheDir = await Directory(
|
||||||
|
'${appSupportDir.path}/tor_cache',
|
||||||
|
).create();
|
||||||
|
|
||||||
|
// Generate a random port.
|
||||||
|
final newPort = await _getRandomUnusedPort();
|
||||||
|
|
||||||
|
// Start the Tor service in an isolate.
|
||||||
|
final tor = await Isolate.run(() {
|
||||||
|
// Load the Tor library.
|
||||||
|
final lib = rust.NativeLibrary(load(libName));
|
||||||
|
|
||||||
|
// Start the Tor service.
|
||||||
|
final tor = lib.tor_start(
|
||||||
|
newPort,
|
||||||
|
stateDir.path.toNativeUtf8() as Pointer<Char>,
|
||||||
|
cacheDir.path.toNativeUtf8() as Pointer<Char>,
|
||||||
|
obfs4Port ?? -1,
|
||||||
|
snowflakePort ?? -1,
|
||||||
|
bridgeLines?.toNativeUtf8() as Pointer<Char>? ?? nullptr,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Throw an exception if the Tor service fails to start.
|
||||||
|
if (tor.client == nullptr) {
|
||||||
|
throwRustException(lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tor;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the client pointer and started flag.
|
||||||
|
_clientPtr = Pointer.fromAddress(tor.client.address);
|
||||||
|
_proxyPtr = Pointer.fromAddress(tor.proxy.address);
|
||||||
|
|
||||||
|
// Bootstrap the Tor service.
|
||||||
|
bootstrap();
|
||||||
|
|
||||||
|
// Set the proxy port.
|
||||||
|
_proxyPort = newPort;
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reconfigure({
|
||||||
|
int? obfs4Port,
|
||||||
|
int? snowflakePort,
|
||||||
|
String? bridgeLines,
|
||||||
|
}) async {
|
||||||
|
final lib = rust.NativeLibrary(_lib);
|
||||||
|
|
||||||
|
// Set the state and cache directories.
|
||||||
|
final Directory appSupportDir = await getApplicationSupportDirectory();
|
||||||
|
final stateDir = await Directory(
|
||||||
|
'${appSupportDir.path}/tor_state',
|
||||||
|
).create();
|
||||||
|
final cacheDir = await Directory(
|
||||||
|
'${appSupportDir.path}/tor_cache',
|
||||||
|
).create();
|
||||||
|
|
||||||
|
final reconfigured = lib.tor_reconfigure(
|
||||||
|
_clientPtr,
|
||||||
|
stateDir.path.toNativeUtf8() as Pointer<Char>,
|
||||||
|
cacheDir.path.toNativeUtf8() as Pointer<Char>,
|
||||||
|
obfs4Port ?? -1,
|
||||||
|
snowflakePort ?? -1,
|
||||||
|
bridgeLines?.toNativeUtf8() as Pointer<Char>? ?? nullptr,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reconfigured) {
|
||||||
|
throwRustException(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bootstrap the Tor service.
|
||||||
|
///
|
||||||
|
/// This will bootstrap the Tor service and establish a Tor circuit. This
|
||||||
|
/// function should only be called after the Tor service has been started.
|
||||||
|
///
|
||||||
|
/// This function will block until the Tor service has bootstrapped.
|
||||||
|
///
|
||||||
|
/// Throws an exception if the Tor service fails to bootstrap.
|
||||||
|
///
|
||||||
|
/// Returns void.
|
||||||
|
void bootstrap() {
|
||||||
|
// Load the Tor library.
|
||||||
|
final lib = rust.NativeLibrary(_lib);
|
||||||
|
|
||||||
|
// Bootstrap the Tor service.
|
||||||
|
_bootstrapped = lib.tor_client_bootstrap(_clientPtr);
|
||||||
|
|
||||||
|
// Throw an exception if the Tor service fails to bootstrap.
|
||||||
|
if (!bootstrapped) {
|
||||||
|
throwRustException(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prevent traffic flowing through the proxy
|
||||||
|
void disable() {
|
||||||
|
stop();
|
||||||
|
|
||||||
|
_enabled = false;
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the proxy
|
||||||
|
void stop() {
|
||||||
|
final lib = rust.NativeLibrary(_lib);
|
||||||
|
if (_proxyPtr != nullptr) {
|
||||||
|
lib.tor_proxy_stop(_proxyPtr);
|
||||||
|
_proxyPtr = nullptr;
|
||||||
|
|
||||||
|
_bootstrapped = false;
|
||||||
|
_proxyPort = -1;
|
||||||
|
|
||||||
|
broadcastState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setClientDormant(bool dormant) {
|
||||||
|
if (_clientPtr == nullptr || !started || !bootstrapped) {
|
||||||
|
throw ClientNotActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
final lib = rust.NativeLibrary(_lib);
|
||||||
|
lib.tor_client_set_dormant(_clientPtr, dormant);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> isReady() async {
|
||||||
|
return await Future.doWhile(
|
||||||
|
() => Future.delayed(const Duration(seconds: 1)).then((_) {
|
||||||
|
// We are waiting and making absolutely no request unless:
|
||||||
|
// Tor is disabled
|
||||||
|
if (!enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...or Tor circuit is established
|
||||||
|
if (bootstrapped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This way we avoid making clearnet req's while Tor is initialising
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void throwRustException(rust.NativeLibrary lib) {
|
||||||
|
final String rustError = lib
|
||||||
|
.tor_last_error_message()
|
||||||
|
.cast<Utf8>()
|
||||||
|
.toDartString();
|
||||||
|
|
||||||
|
throw _getRustException(rustError);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Exception _getRustException(String rustError) {
|
||||||
|
if (rustError.contains('Unable to bootstrap a working directory')) {
|
||||||
|
return CouldntBootstrapDirectory(rustError: rustError);
|
||||||
|
} else {
|
||||||
|
return Exception(rustError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hello() {
|
||||||
|
rust.NativeLibrary(_lib).tor_hello();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-325
@@ -1,325 +1 @@
|
|||||||
// SPDX-FileCopyrightText: 2022 Foundation Devices Inc.
|
export 'src/tor.dart';
|
||||||
// SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:tor/generated_bindings.dart' as rust;
|
|
||||||
|
|
||||||
DynamicLibrary load(String name) {
|
|
||||||
if (Platform.isAndroid || Platform.isLinux) {
|
|
||||||
return DynamicLibrary.open('lib$name.so');
|
|
||||||
} else if (Platform.isIOS || Platform.isMacOS) {
|
|
||||||
return DynamicLibrary.open('$name.framework/$name');
|
|
||||||
} else if (Platform.isWindows) {
|
|
||||||
return DynamicLibrary.open('$name.dll');
|
|
||||||
} else {
|
|
||||||
throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CouldntBootstrapDirectory implements Exception {
|
|
||||||
String? rustError;
|
|
||||||
|
|
||||||
CouldntBootstrapDirectory({this.rustError});
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotSupportedPlatform implements Exception {
|
|
||||||
String reason;
|
|
||||||
|
|
||||||
NotSupportedPlatform(this.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ClientNotActive implements Exception {}
|
|
||||||
|
|
||||||
class Tor {
|
|
||||||
static const String libName = "tor";
|
|
||||||
static late DynamicLibrary _lib;
|
|
||||||
|
|
||||||
Pointer<Void> _clientPtr = nullptr;
|
|
||||||
Pointer<Void> _proxyPtr = nullptr;
|
|
||||||
|
|
||||||
/// Flag to indicate that Tor client and proxy have started. Traffic is routed through the proxy only if it is also [enabled].
|
|
||||||
bool get started => _proxyPort > -1;
|
|
||||||
|
|
||||||
bool get hasClient => _clientPtr != nullptr;
|
|
||||||
|
|
||||||
/// Flag to indicate that traffic should flow through the proxy.
|
|
||||||
bool _enabled = false;
|
|
||||||
|
|
||||||
/// Getter for the enabled flag.
|
|
||||||
bool get enabled => _enabled;
|
|
||||||
|
|
||||||
/// Flag to indicate that a Tor circuit is thought to have been established
|
|
||||||
/// (true means that Tor has bootstrapped).
|
|
||||||
bool get bootstrapped => _bootstrapped;
|
|
||||||
|
|
||||||
/// Getter for the bootstrapped flag.
|
|
||||||
bool _bootstrapped = false;
|
|
||||||
|
|
||||||
/// A stream of Tor events.
|
|
||||||
///
|
|
||||||
/// This stream broadcast just the port for now (-1 if circuit not established or proxy not enabled)
|
|
||||||
final StreamController events = StreamController.broadcast();
|
|
||||||
|
|
||||||
/// Getter for the proxy port.
|
|
||||||
///
|
|
||||||
/// Returns -1 if Tor is not enabled or if the circuit is not established.
|
|
||||||
///
|
|
||||||
/// Returns the proxy port if Tor is enabled and the circuit is established.
|
|
||||||
///
|
|
||||||
/// This is the port that should be used for all requests.
|
|
||||||
int get port {
|
|
||||||
if (!_enabled) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return _proxyPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The proxy port.
|
|
||||||
int _proxyPort = -1;
|
|
||||||
|
|
||||||
/// Singleton instance of the Tor class.
|
|
||||||
static final Tor _instance = Tor._internal();
|
|
||||||
|
|
||||||
/// Getter for the singleton instance of the Tor class.
|
|
||||||
static Tor get instance => _instance;
|
|
||||||
|
|
||||||
/// Initialize the Tor ffi lib instance if it hasn't already been set. Nothing
|
|
||||||
/// changes if _tor is already been set.
|
|
||||||
///
|
|
||||||
/// Returns a Future that completes when the Tor service has started.
|
|
||||||
///
|
|
||||||
/// Throws an exception if the Tor service fails to start.
|
|
||||||
static Tor init({bool enabled = true}) {
|
|
||||||
final singleton = Tor._instance;
|
|
||||||
singleton._enabled = enabled;
|
|
||||||
return singleton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Private constructor for the Tor class.
|
|
||||||
Tor._internal() {
|
|
||||||
_lib = load(libName);
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
|
||||||
print("Instance of Tor created!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void broadcastState() {
|
|
||||||
events.add(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<int> _getRandomUnusedPort({List<int> excluded = const []}) async {
|
|
||||||
final random = Random.secure();
|
|
||||||
int potentialPort = 0;
|
|
||||||
|
|
||||||
retry:
|
|
||||||
while (potentialPort <= 0 || excluded.contains(potentialPort)) {
|
|
||||||
potentialPort = random.nextInt(65535);
|
|
||||||
try {
|
|
||||||
final socket = await ServerSocket.bind("0.0.0.0", potentialPort);
|
|
||||||
await socket.close();
|
|
||||||
return potentialPort;
|
|
||||||
} catch (_) {
|
|
||||||
continue retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the Tor service.
|
|
||||||
///
|
|
||||||
/// This will start the Tor service and establish a Tor circuit.
|
|
||||||
///
|
|
||||||
/// Throws an exception if the Tor service fails to start.
|
|
||||||
///
|
|
||||||
/// Returns a Future that completes when the Tor service has started.
|
|
||||||
Future<void> start({
|
|
||||||
int? obfs4Port,
|
|
||||||
int? snowflakePort,
|
|
||||||
String? bridgeLines,
|
|
||||||
}) async {
|
|
||||||
broadcastState();
|
|
||||||
|
|
||||||
// Set the state and cache directories.
|
|
||||||
final Directory appSupportDir = await getApplicationSupportDirectory();
|
|
||||||
final stateDir = await Directory(
|
|
||||||
'${appSupportDir.path}/tor_state',
|
|
||||||
).create();
|
|
||||||
final cacheDir = await Directory(
|
|
||||||
'${appSupportDir.path}/tor_cache',
|
|
||||||
).create();
|
|
||||||
|
|
||||||
// Generate a random port.
|
|
||||||
final newPort = await _getRandomUnusedPort();
|
|
||||||
|
|
||||||
// Start the Tor service in an isolate.
|
|
||||||
final tor = await Isolate.run(() {
|
|
||||||
// Load the Tor library.
|
|
||||||
final lib = rust.NativeLibrary(load(libName));
|
|
||||||
|
|
||||||
// Start the Tor service.
|
|
||||||
final tor = lib.tor_start(
|
|
||||||
newPort,
|
|
||||||
stateDir.path.toNativeUtf8() as Pointer<Char>,
|
|
||||||
cacheDir.path.toNativeUtf8() as Pointer<Char>,
|
|
||||||
obfs4Port ?? -1,
|
|
||||||
snowflakePort ?? -1,
|
|
||||||
bridgeLines?.toNativeUtf8() as Pointer<Char>? ?? nullptr,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Throw an exception if the Tor service fails to start.
|
|
||||||
if (tor.client == nullptr) {
|
|
||||||
throwRustException(lib);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tor;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set the client pointer and started flag.
|
|
||||||
_clientPtr = Pointer.fromAddress(tor.client.address);
|
|
||||||
_proxyPtr = Pointer.fromAddress(tor.proxy.address);
|
|
||||||
|
|
||||||
// Bootstrap the Tor service.
|
|
||||||
bootstrap();
|
|
||||||
|
|
||||||
// Set the proxy port.
|
|
||||||
_proxyPort = newPort;
|
|
||||||
broadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> reconfigure({
|
|
||||||
int? obfs4Port,
|
|
||||||
int? snowflakePort,
|
|
||||||
String? bridgeLines,
|
|
||||||
}) async {
|
|
||||||
final lib = rust.NativeLibrary(_lib);
|
|
||||||
|
|
||||||
// Set the state and cache directories.
|
|
||||||
final Directory appSupportDir = await getApplicationSupportDirectory();
|
|
||||||
final stateDir = await Directory(
|
|
||||||
'${appSupportDir.path}/tor_state',
|
|
||||||
).create();
|
|
||||||
final cacheDir = await Directory(
|
|
||||||
'${appSupportDir.path}/tor_cache',
|
|
||||||
).create();
|
|
||||||
|
|
||||||
final reconfigured = lib.tor_reconfigure(
|
|
||||||
_clientPtr,
|
|
||||||
stateDir.path.toNativeUtf8() as Pointer<Char>,
|
|
||||||
cacheDir.path.toNativeUtf8() as Pointer<Char>,
|
|
||||||
obfs4Port ?? -1,
|
|
||||||
snowflakePort ?? -1,
|
|
||||||
bridgeLines?.toNativeUtf8() as Pointer<Char>? ?? nullptr,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!reconfigured) {
|
|
||||||
throwRustException(lib);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bootstrap the Tor service.
|
|
||||||
///
|
|
||||||
/// This will bootstrap the Tor service and establish a Tor circuit. This
|
|
||||||
/// function should only be called after the Tor service has been started.
|
|
||||||
///
|
|
||||||
/// This function will block until the Tor service has bootstrapped.
|
|
||||||
///
|
|
||||||
/// Throws an exception if the Tor service fails to bootstrap.
|
|
||||||
///
|
|
||||||
/// Returns void.
|
|
||||||
void bootstrap() {
|
|
||||||
// Load the Tor library.
|
|
||||||
final lib = rust.NativeLibrary(_lib);
|
|
||||||
|
|
||||||
// Bootstrap the Tor service.
|
|
||||||
_bootstrapped = lib.tor_client_bootstrap(_clientPtr);
|
|
||||||
|
|
||||||
// Throw an exception if the Tor service fails to bootstrap.
|
|
||||||
if (!bootstrapped) {
|
|
||||||
throwRustException(lib);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prevent traffic flowing through the proxy
|
|
||||||
void disable() {
|
|
||||||
stop();
|
|
||||||
|
|
||||||
_enabled = false;
|
|
||||||
broadcastState();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the proxy
|
|
||||||
void stop() {
|
|
||||||
final lib = rust.NativeLibrary(_lib);
|
|
||||||
if (_proxyPtr != nullptr) {
|
|
||||||
lib.tor_proxy_stop(_proxyPtr);
|
|
||||||
_proxyPtr = nullptr;
|
|
||||||
|
|
||||||
_bootstrapped = false;
|
|
||||||
_proxyPort = -1;
|
|
||||||
|
|
||||||
broadcastState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setClientDormant(bool dormant) {
|
|
||||||
if (_clientPtr == nullptr || !started || !bootstrapped) {
|
|
||||||
throw ClientNotActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
final lib = rust.NativeLibrary(_lib);
|
|
||||||
lib.tor_client_set_dormant(_clientPtr, dormant);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> isReady() async {
|
|
||||||
return await Future.doWhile(
|
|
||||||
() => Future.delayed(const Duration(seconds: 1)).then((_) {
|
|
||||||
// We are waiting and making absolutely no request unless:
|
|
||||||
// Tor is disabled
|
|
||||||
if (!enabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...or Tor circuit is established
|
|
||||||
if (bootstrapped) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This way we avoid making clearnet req's while Tor is initialising
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void throwRustException(rust.NativeLibrary lib) {
|
|
||||||
final String rustError = lib
|
|
||||||
.tor_last_error_message()
|
|
||||||
.cast<Utf8>()
|
|
||||||
.toDartString();
|
|
||||||
|
|
||||||
throw _getRustException(rustError);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Exception _getRustException(String rustError) {
|
|
||||||
if (rustError.contains('Unable to bootstrap a working directory')) {
|
|
||||||
return CouldntBootstrapDirectory(rustError: rustError);
|
|
||||||
} else {
|
|
||||||
return Exception(rustError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void hello() {
|
|
||||||
rust.NativeLibrary(_lib).tor_hello();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import 'package:tor/generated_bindings.dart' as rust;
|
|
||||||
import 'package:tor/tor.dart';
|
|
||||||
|
|
||||||
int getNofileLimit() {
|
|
||||||
return rust.NativeLibrary(load(Tor.libName)).tor_get_nofile_limit();
|
|
||||||
}
|
|
||||||
|
|
||||||
int setNofileLimit(int limit) {
|
|
||||||
return rust.NativeLibrary(load(Tor.libName)).tor_set_nofile_limit(limit);
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,7 @@ dev_dependencies:
|
|||||||
lint: ^2.8.0
|
lint: ^2.8.0
|
||||||
|
|
||||||
ffigen:
|
ffigen:
|
||||||
output: 'lib/generated_bindings.dart'
|
output: 'lib/src/generated_bindings.dart'
|
||||||
llvm-path:
|
llvm-path:
|
||||||
- '/usr/lib/llvm-14/lib/libclang.so.1'
|
- '/usr/lib/llvm-14/lib/libclang.so.1'
|
||||||
functions:
|
functions:
|
||||||
|
|||||||
Reference in New Issue
Block a user