Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bitcoin and monero onion examples #53

Merged
merged 2 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 229 additions & 5 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
//
// SPDX-License-Identifier: MIT

// Example app deps, not necessarily needed for tor usage.
// Flutter dependencies not necessarily needed for tor usage:
import 'dart:async';
import 'dart:convert';
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
// Example application dependencies you can replace with any that works for you:
import 'package:socks5_proxy/socks_client.dart';
import 'package:tor/socks_socket.dart';
// The only real import needed for basic usage:
import 'package:tor/tor.dart'; // This would go at the top, but dart autoformatter doesn't like it there.

void main() {
runApp(const MyApp());
Expand Down Expand Up @@ -44,6 +45,32 @@ class _MyAppState extends State<Home> {
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/

final moneroOnionController = TextEditingController(
text:
'ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion:18081/json_rpc');

Future<void> startTor() async {
await Tor.init();

Expand Down Expand Up @@ -235,6 +262,203 @@ class _MyAppState extends State<Home> {
"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.
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",
),
),
],
),
spacerSmall,
Row(
children: [
// Monero onion input field.
Expanded(
child: TextField(
controller: moneroOnionController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Monero onion address to test',
),
),
),
spacerSmall,
TextButton(
onPressed: torStarted
? () async {
// Validate the onion address.
if (!moneroOnionController.text
.contains(".onion")) {
print("Invalid onion address");
return;
} else if (!moneroOnionController.text
.contains(":")) {
print("Invalid onion address (needs port)");
return;
}

final String host =
moneroOnionController.text.split(":").first;
final int port = int.parse(moneroOnionController
.text
.split(":")
.last
.split("/")
.first);
final String path = moneroOnionController.text
.split(":")
.last
.split("/")
.last; // Extract the path

var socksSocket = await SOCKSSocket.create(
proxyHost: InternetAddress.loopbackIPv4.address,
proxyPort: Tor.instance.port,
sslEnabled: false,
);

await socksSocket.connect();
await socksSocket.connectTo(host, port);

final body = jsonEncode({
"jsonrpc": "2.0",
"id": "0",
"method": "get_info",
});

final request = 'POST /$path HTTP/1.1\r\n'
'Host: $host\r\n'
'Content-Type: application/json\r\n'
'Content-Length: ${body.length}\r\n'
'\r\n'
'$body';

socksSocket.write(request);
print("Request sent: $request");

await for (var response
in socksSocket.inputStream) {
final result = utf8.decode(response);
print("Response received: $result");
break;
}

// You should see a server response printed to the console.
//
// Example response:
// Host: ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion
// Content-Type: application/json
// Content-Length: 46
//
// {"jsonrpc":"2.0","id":"0","method":"get_info"}
// flutter: Response received: HTTP/1.1 200 Ok
// Server: Epee-based
// Content-Length: 1434
// Content-Type: application/json
// Last-Modified: Thu, 03 Oct 2024 23:08:19 GMT
// Accept-Ranges: bytes
//
// {
// "id": "0",
// "jsonrpc": "2.0",
// "result": {
// "adjusted_time": 1727996959,
// ...

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 Monero onion node connection",
),
),
],
),
],
),
),
Expand Down
6 changes: 3 additions & 3 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,10 @@ packages:
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:
Expand Down Expand Up @@ -278,7 +278,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.7"
version: "0.0.8"
vector_math:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
47 changes: 35 additions & 12 deletions lib/socks_socket.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
// SPDX-FileCopyrightText: 2024 Cypher Stack LLC
//
// SPDX-License-Identifier: MIT

Expand All @@ -10,8 +10,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.
Expand Down Expand Up @@ -99,6 +98,26 @@ class SOCKSSocket {
/// 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.
var 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
Expand Down Expand Up @@ -163,7 +182,7 @@ class SOCKSSocket {
},
onDone: () {
// Close the response controller when the socket is closed.
_responseController.close();
// _responseController.close();
},
);
}
Expand Down Expand Up @@ -221,7 +240,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(
Expand Down Expand Up @@ -283,15 +302,19 @@ class SOCKSSocket {
/// A Future that resolves to void.
Future<void> 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<List<int>> listen(
Expand Down
Loading