diff --git a/example/lib/main.dart b/example/lib/main.dart index 77a70d0..5db00ef 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -11,8 +11,8 @@ import 'dart:io'; import 'package:flutter/material.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/tor.dart'; import 'package:tor/socks_socket.dart'; // For socket connections +import 'package:tor/tor.dart'; void main() { runApp(const MyApp()); @@ -44,6 +44,28 @@ class _MyAppState extends State { final hostController = TextEditingController(text: 'https://icanhazip.com/'); // https://check.torproject.org is another good option. + // Set the default text for the onion input field. + final onionController = TextEditingController( + text: + 'https://cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion'); + // See https://blog.cloudflare.com/cloudflare-onion-service/ for more options: + // cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion + // cflarenuttlfuyn7imozr4atzvfbiw3ezgbdjdldmdx7srterayaozid.onion + // cflares35lvdlczhy3r6qbza5jjxbcplzvdveabhf7bsp7y4nzmn67yd.onion + // cflareusni3s7vwhq2f7gc4opsik7aa4t2ajedhzr42ez6uajaywh3qd.onion + // cflareki4v3lh674hq55k3n7xd4ibkwx3pnw67rr3gkpsonjmxbktxyd.onion + // cflarejlah424meosswvaeqzb54rtdetr4xva6mq2bm2hfcx5isaglid.onion + // cflaresuje2rb7w2u3w43pn4luxdi6o7oatv6r2zrfb5xvsugj35d2qd.onion + // cflareer7qekzp3zeyqvcfktxfrmncse4ilc7trbf6bp6yzdabxuload.onion + // cflareub6dtu7nvs3kqmoigcjdwap2azrkx5zohb2yk7gqjkwoyotwqd.onion + // cflare2nge4h4yqr3574crrd7k66lil3torzbisz6uciyuzqc2h2ykyd.onion + + final bitcoinOnionController = TextEditingController( + text: + 'qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion:50001'); + // For more options, see https://bitnodes.io/nodes/addresses/?q=onion and + // https://sethforprivacy.com/about/ + Future startTor() async { await Tor.init(); @@ -235,6 +257,95 @@ class _MyAppState extends State { "Connect to bitcoin.stackwallet.com:50002 (SSL) via socks socket", ), ), + spacerSmall, + Row( + children: [ + // Bitcoin onion input field. + Expanded( + child: TextField( + controller: bitcoinOnionController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'Bitcoin onion address to test', + ), + ), + ), + spacerSmall, + TextButton( + onPressed: torStarted + ? () async { + // Validate the onion address.www + if (!onionController.text.contains(".onion")) { + print("Invalid onion address"); + return; + } else if (!onionController.text.contains(":")) { + print("Invalid onion address (needs port)"); + return; + } + + String domain = + bitcoinOnionController.text.split(":").first; + int port = int.parse( + bitcoinOnionController.text.split(":").last); + + // 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.instance.port, + sslEnabled: !domain + .endsWith(".onion"), // For SSL connections. + ); + + // Connect to the socks instantiated above. + await socksSocket.connect(); + + // Connect to onion node via socks socket. + // + // Note that this is an SSL example. + await socksSocket.connectTo(domain, port); + + // Send a server features command to the connected socket, see method for more specific usage example.. + await socksSocket.sendServerFeaturesCommand(); + + // You should see a server response printed to the console. + // + // Example response: + // `flutter: secure responseData: { + // "id": "0", + // "jsonrpc": "2.0", + // "result": { + // "cashtokens": true, + // "dsproof": true, + // "genesis_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + // "hash_function": "sha256", + // "hosts": { + // "bitcoin.stackwallet.com": { + // "ssl_port": 50002, + // "tcp_port": 50001, + // "ws_port": 50003, + // "wss_port": 50004 + // } + // }, + // "protocol_max": "1.5", + // "protocol_min": "1.4", + // "pruning": null, + // "server_version": "Fulcrum 1.9.1" + // } + // } + + // Close the socket. + await socksSocket.close(); + } + + // A mutex should be added to this example to prevent + // multiple connections from being made at once. TODO + : null, + child: const Text( + "Test Bitcoin onion node connection", + ), + ), + ], + ), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 3479fa2..a13f188 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" cupertino_icons: dependency: "direct main" description: @@ -87,18 +87,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -215,15 +215,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socks5_proxy: dependency: "direct main" description: name: socks5_proxy - sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a" + sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.3+dev.3" source_span: dependency: transitive description: @@ -252,10 +252,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -268,17 +268,17 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" tor: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.7" + version: "0.0.8" vector_math: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4e2700a..bd200ef 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -46,7 +46,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - socks5_proxy: ^1.0.3+dev.3 + socks5_proxy: 1.0.3+dev.3 dev_dependencies: flutter_test: diff --git a/lib/socks_socket.dart b/lib/socks_socket.dart index aab8617..23bd5f8 100644 --- a/lib/socks_socket.dart +++ b/lib/socks_socket.dart @@ -1,7 +1,3 @@ -// SPDX-FileCopyrightText: 2024 Foundation Devices Inc. -// -// SPDX-License-Identifier: MIT - import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -10,8 +6,7 @@ import 'package:flutter/foundation.dart'; /// A SOCKS5 socket. /// -/// This class is a wrapper around the Socket class that implements the -/// SOCKS5 protocol. It supports SSL and non-SSL connections. +/// A Dart 3 Socket wrapper that implements the SOCKS5 protocol. Now with SSL! /// /// Properties: /// - [proxyHost]: The host of the SOCKS5 proxy server. @@ -99,6 +94,26 @@ class SOCKSSocket { /// Private constructor. SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled); + /// Provides a stream of data as List. + Stream> get inputStream => sslEnabled + ? _secureResponseController.stream + : _responseController.stream; + + /// Provides a StreamSink compatible with List for sending data. + StreamSink> get outputStream { + // Create a simple StreamSink wrapper for _socksSocket and + // _secureSocksSocket that accepts List and forwards it to write method. + var sink = StreamController>(); + 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 @@ -163,7 +178,7 @@ class SOCKSSocket { }, onDone: () { // Close the response controller when the socket is closed. - _responseController.close(); + // _responseController.close(); }, ); } @@ -221,7 +236,7 @@ class SOCKSSocket { 'socks_socket.connectTo(): Failed to connect to target through SOCKS5 proxy.'); } - // Upgrade to SSL if needed + // Upgrade to SSL if needed. if (sslEnabled) { // Upgrade to SSL. _secureSocksSocket = await SecureSocket.secure( @@ -283,15 +298,19 @@ class SOCKSSocket { /// A Future that resolves to void. Future close() async { // Ensure all data is sent before closing. - // - // TODO test this. - if (sslEnabled) { + try { + if (sslEnabled) { + await _secureSocksSocket.flush(); + } await _socksSocket.flush(); - await _secureResponseController.close(); + } finally { + await _subscription?.cancel(); + await _socksSocket.close(); + _responseController.close(); + if (sslEnabled) { + _secureResponseController.close(); + } } - await _socksSocket.flush(); - await _responseController.close(); - return await _socksSocket.close(); } StreamSubscription> listen(