diff --git a/example/lib/main.dart b/example/lib/main.dart index 608ef155..fae08536 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; // Imports needed for tor usage: import 'package:socks5_proxy/socks_client.dart'; // Just for example; can use any socks5 proxy package, pick your favorite. import 'package:tor_ffi_plugin/tor_ffi_plugin.dart'; @@ -33,7 +34,7 @@ class Home extends StatefulWidget { class _MyAppState extends State { // Flag to track if tor has started. - bool torStarted = false; + bool torIsRunning = false; // Set the default text for the host input field. final hostController = TextEditingController(text: 'https://icanhazip.com/'); @@ -41,11 +42,13 @@ class _MyAppState extends State { Future startTor() async { // Start the Tor daemon. - await Tor.instance.start(); + await Tor.instance.start( + torDataDirPath: (await getApplicationSupportDirectory()).path, + ); // Toggle started flag. setState(() { - torStarted = Tor.instance.started; // Update flag + torIsRunning = Tor.instance.status == TorStatus.on; // Update flag }); print('Done awaiting; tor should be running'); @@ -71,7 +74,7 @@ class _MyAppState extends State { child: Column( children: [ TextButton( - onPressed: torStarted + onPressed: torIsRunning ? null : () async { unawaited( @@ -117,7 +120,7 @@ class _MyAppState extends State { ), spacerSmall, TextButton( - onPressed: torStarted + onPressed: torIsRunning ? () async { // `socks5_proxy` package example, use another socks5 // connection of your choice. @@ -158,7 +161,7 @@ class _MyAppState extends State { ), spacerSmall, TextButton( - onPressed: torStarted + onPressed: torIsRunning ? () async { // Instantiate a socks socket at localhost and on the port selected by the tor service. var socksSocket = await SOCKSSocket.create( diff --git a/example/pubspec.lock b/example/pubspec.lock index 111154ef..02491ee8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -132,7 +132,7 @@ packages: source: hosted version: "1.8.3" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0e67a20e..acf87c70 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 socks5_proxy: ^1.0.3+dev.3 + path_provider: ^2.1.1 dev_dependencies: flutter_test: diff --git a/lib/tor_ffi_plugin.dart b/lib/tor_ffi_plugin.dart index 5696a544..10c32070 100644 --- a/lib/tor_ffi_plugin.dart +++ b/lib/tor_ffi_plugin.dart @@ -9,10 +9,10 @@ import 'dart:isolate'; import 'dart:math'; import 'package:ffi/ffi.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:flutter/foundation.dart'; import 'package:tor_ffi_plugin/tor_ffi_plugin_bindings_generated.dart'; -DynamicLibrary load(name) { +DynamicLibrary _load(name) { if (Platform.isAndroid || Platform.isLinux) { return DynamicLibrary.open('lib$name.so'); } else if (Platform.isIOS || Platform.isMacOS) { @@ -34,155 +34,113 @@ class NotSupportedPlatform implements Exception { NotSupportedPlatform(String s); } -class Tor { - static const String _libName = "tor_ffi_plugin"; - static late DynamicLibrary _lib; - - Pointer _clientPtr = nullptr; +enum TorStatus { + on, + starting, + off; +} - /// Flag to indicate that a Tor should be started. - bool get started => _started; +class Tor { + /// Private constructor for the Tor class. + Tor._() { + _lib = _load(_libName); + if (kDebugMode) { + print("Native tor library loaded!"); + } + } - /// Getter for the started flag. - bool _started = false; + /// Singleton instance of the Tor class. + static final Tor instance = Tor._(); - /// Flag to indicate that Tor has started. - bool _enabled = false; + static const String _libName = "tor_ffi_plugin"; + static late DynamicLibrary _lib; - /// Getter for the enabled flag. - bool get enabled => _enabled; + /// Status of the tor proxy service + TorStatus get status => _status; + TorStatus _status = TorStatus.off; /// 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) - final StreamController events = StreamController.broadcast(); - /// Getter for the proxy port. /// - /// Returns -1 if Tor is not enabled or if the circuit is not established. + /// Throws 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; + if (_proxyPort == null) { + throw Exception(""); } - return _proxyPort; + 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 Future init({enabled = true}) async { - var singleton = Tor._instance; - singleton._enabled = enabled; - return singleton; - } - - /// Private constructor for the Tor class. - Tor._internal() { - _lib = load(_libName); - print("Instance of Tor created!"); - } - - /// Start the Tor service. - Future enable() async { - _enabled = true; - if (!started) { - return await start(); - } - } - - Future _getRandomUnusedPort({List excluded = const []}) async { - var random = Random.secure(); - int potentialPort = 0; - - retry: - while (potentialPort <= 0 || excluded.contains(potentialPort)) { - potentialPort = random.nextInt(65535); - try { - var socket = await ServerSocket.bind("0.0.0.0", potentialPort); - socket.close(); - return potentialPort; - } catch (_) { - continue retry; - } - } - - return -1; - } + int? _proxyPort; /// Start the Tor service. /// - /// This will start the Tor service and establish a Tor circuit. + /// This will start the Tor service and establish a Tor circuit if there + /// already hasn't been one established. /// /// Throws an exception if the Tor service fails to start. /// /// Returns a Future that completes when the Tor service has started. - Future start() async { - // Send the port to the events stream. - events.add(port); - - // 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. - int newPort = await _getRandomUnusedPort(); - - // Start the Tor service in an isolate. - int ptr = await Isolate.run(() async { - // Load the Tor library. - var lib = NativeLibrary(load(_libName)); - - // Start the Tor service. - final ptr = lib.tor_start( - newPort, - stateDir.path.toNativeUtf8() as Pointer, - cacheDir.path.toNativeUtf8() as Pointer); - - // Throw an exception if the Tor service fails to start. - if (ptr == nullptr) { - throwRustException(lib); - } + Future start({required String torDataDirPath}) async { + if (_status != TorStatus.off) { + // already starting or running + return; + } - // Return the pointer. - return ptr.address; - }); + try { + _status = TorStatus.starting; - // Set the client pointer and started flag. - _clientPtr = Pointer.fromAddress(ptr); - _started = true; + // Set the state and cache directories. + final stateDir = await Directory('$torDataDirPath/tor_state').create(); + final cacheDir = await Directory('$torDataDirPath/tor_cache').create(); - // Bootstrap the Tor service. - bootstrap(); + // Generate a random port. + final int? newPort = await _getRandomUnusedPort(); - // Set the proxy port and enabled flag. - _proxyPort = newPort; - _enabled = true; + if (newPort == null) { + throw Exception("Failed to get random unused port!"); + } + + // Start the Tor service in an isolate. + final int ptr = await Isolate.run(() async { + // Load the Tor library. + var lib = NativeLibrary(_load(_libName)); + + // Start the Tor service. + final ptr = lib.tor_start( + newPort, + stateDir.path.toNativeUtf8() as Pointer, + cacheDir.path.toNativeUtf8() as Pointer); + + // Throw an exception if the Tor service fails to start. + if (ptr == nullptr) { + throwRustException(lib); + } + + // Return the pointer. + return ptr.address; + }); + + // Set the client pointer and started flag. + _clientPtr = Pointer.fromAddress(ptr); + + // Bootstrap the Tor service. + _bootstrap(); + + // Set the proxy port and change status. + _proxyPort = newPort; + _status = TorStatus.on; + } catch (_) { + _status = TorStatus.off; + rethrow; + } } /// Bootstrap the Tor service. @@ -195,7 +153,7 @@ class Tor { /// Throws an exception if the Tor service fails to bootstrap. /// /// Returns void. - void bootstrap() { + void _bootstrap() { // Load the Tor library. final lib = NativeLibrary(_lib); @@ -203,42 +161,44 @@ class Tor { _bootstrapped = lib.tor_bootstrap(_clientPtr); // Throw an exception if the Tor service fails to bootstrap. - if (!bootstrapped) { + if (!_bootstrapped) { throwRustException(lib); } } // TODO: this doesn't actually shut tor down - void disable() { - _enabled = false; - } + // void disable() { + // _status = false; + // } - void restart() { - // TODO: arti seems to recover by itself and there is no client restart fn - // TODO: but follow up with them if restart is truly unnecessary - // if (enabled && started && circuitEstablished) {} - } + Pointer _clientPtr = nullptr; + + Future _getRandomUnusedPort({List excluded = const []}) async { + var random = Random.secure(); + int potentialPort = 0; + + retry: + while (potentialPort <= 0 || excluded.contains(potentialPort)) { + potentialPort = random.nextInt(65535); + try { + var socket = await ServerSocket.bind("0.0.0.0", potentialPort); + socket.close(); + return potentialPort; + } catch (_) { + continue retry; + } + } - Future 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; - })); + return null; } - static throwRustException(NativeLibrary lib) { + // Future restart() async { + // // TODO: arti seems to recover by itself and there is no client restart fn + // // TODO: but follow up with them if restart is truly unnecessary + // // if (enabled && started && circuitEstablished) {} + // } + + static void throwRustException(NativeLibrary lib) { String rustError = lib.tor_last_error_message().cast().toDartString(); throw _getRustException(rustError); diff --git a/pubspec.yaml b/pubspec.yaml index 62535be1..034066aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,6 @@ environment: dependencies: flutter: sdk: flutter - path_provider: ^2.1.1 ffi: ^2.0.1 plugin_platform_interface: ^2.0.2