Skip to content

Commit d014507

Browse files
authored
Merge pull request #53 from sneurlax/onion-example
Add bitcoin and monero onion examples
2 parents caf52f3 + dc3eb35 commit d014507

File tree

4 files changed

+268
-21
lines changed

4 files changed

+268
-21
lines changed

example/lib/main.dart

Lines changed: 229 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
//
44
// SPDX-License-Identifier: MIT
55

6-
// Example app deps, not necessarily needed for tor usage.
6+
// Flutter dependencies not necessarily needed for tor usage:
77
import 'dart:async';
88
import 'dart:convert';
99
import 'dart:io';
1010

1111
import 'package:flutter/material.dart';
12-
// Imports needed for tor usage:
13-
import 'package:socks5_proxy/socks_client.dart'; // Just for example; can use any socks5 proxy package, pick your favorite.
14-
import 'package:tor/tor.dart';
15-
import 'package:tor/socks_socket.dart'; // For socket connections
12+
// Example application dependencies you can replace with any that works for you:
13+
import 'package:socks5_proxy/socks_client.dart';
14+
import 'package:tor/socks_socket.dart';
15+
// The only real import needed for basic usage:
16+
import 'package:tor/tor.dart'; // This would go at the top, but dart autoformatter doesn't like it there.
1617

1718
void main() {
1819
runApp(const MyApp());
@@ -44,6 +45,32 @@ class _MyAppState extends State<Home> {
4445
final hostController = TextEditingController(text: 'https://icanhazip.com/');
4546
// https://check.torproject.org is another good option.
4647

48+
// Set the default text for the onion input field.
49+
final onionController = TextEditingController(
50+
text:
51+
'https://cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion');
52+
// See https://blog.cloudflare.com/cloudflare-onion-service/ for more options:
53+
// cflarexljc3rw355ysrkrzwapozws6nre6xsy3n4yrj7taye3uiby3ad.onion
54+
// cflarenuttlfuyn7imozr4atzvfbiw3ezgbdjdldmdx7srterayaozid.onion
55+
// cflares35lvdlczhy3r6qbza5jjxbcplzvdveabhf7bsp7y4nzmn67yd.onion
56+
// cflareusni3s7vwhq2f7gc4opsik7aa4t2ajedhzr42ez6uajaywh3qd.onion
57+
// cflareki4v3lh674hq55k3n7xd4ibkwx3pnw67rr3gkpsonjmxbktxyd.onion
58+
// cflarejlah424meosswvaeqzb54rtdetr4xva6mq2bm2hfcx5isaglid.onion
59+
// cflaresuje2rb7w2u3w43pn4luxdi6o7oatv6r2zrfb5xvsugj35d2qd.onion
60+
// cflareer7qekzp3zeyqvcfktxfrmncse4ilc7trbf6bp6yzdabxuload.onion
61+
// cflareub6dtu7nvs3kqmoigcjdwap2azrkx5zohb2yk7gqjkwoyotwqd.onion
62+
// cflare2nge4h4yqr3574crrd7k66lil3torzbisz6uciyuzqc2h2ykyd.onion
63+
64+
final bitcoinOnionController = TextEditingController(
65+
text:
66+
'qly7g5n5t3f3h23xvbp44vs6vpmayurno4basuu5rcvrupli7y2jmgid.onion:50001');
67+
// For more options, see https://bitnodes.io/nodes/addresses/?q=onion and
68+
// https://sethforprivacy.com/about/
69+
70+
final moneroOnionController = TextEditingController(
71+
text:
72+
'ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion:18081/json_rpc');
73+
4774
Future<void> startTor() async {
4875
await Tor.init();
4976

@@ -235,6 +262,203 @@ class _MyAppState extends State<Home> {
235262
"Connect to bitcoin.stackwallet.com:50002 (SSL) via socks socket",
236263
),
237264
),
265+
spacerSmall,
266+
Row(
267+
children: [
268+
// Bitcoin onion input field.
269+
Expanded(
270+
child: TextField(
271+
controller: bitcoinOnionController,
272+
decoration: const InputDecoration(
273+
border: OutlineInputBorder(),
274+
hintText: 'Bitcoin onion address to test',
275+
),
276+
),
277+
),
278+
spacerSmall,
279+
TextButton(
280+
onPressed: torStarted
281+
? () async {
282+
// Validate the onion address.
283+
if (!onionController.text.contains(".onion")) {
284+
print("Invalid onion address");
285+
return;
286+
} else if (!onionController.text.contains(":")) {
287+
print("Invalid onion address (needs port)");
288+
return;
289+
}
290+
291+
String domain =
292+
bitcoinOnionController.text.split(":").first;
293+
int port = int.parse(
294+
bitcoinOnionController.text.split(":").last);
295+
296+
// Instantiate a socks socket at localhost and on the port selected by the tor service.
297+
var socksSocket = await SOCKSSocket.create(
298+
proxyHost: InternetAddress.loopbackIPv4.address,
299+
proxyPort: Tor.instance.port,
300+
sslEnabled: !domain
301+
.endsWith(".onion"), // For SSL connections.
302+
);
303+
304+
// Connect to the socks instantiated above.
305+
await socksSocket.connect();
306+
307+
// Connect to onion node via socks socket.
308+
//
309+
// Note that this is an SSL example.
310+
await socksSocket.connectTo(domain, port);
311+
312+
// Send a server features command to the connected socket, see method for more specific usage example..
313+
await socksSocket.sendServerFeaturesCommand();
314+
315+
// You should see a server response printed to the console.
316+
//
317+
// Example response:
318+
// `flutter: secure responseData: {
319+
// "id": "0",
320+
// "jsonrpc": "2.0",
321+
// "result": {
322+
// "cashtokens": true,
323+
// "dsproof": true,
324+
// "genesis_hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
325+
// "hash_function": "sha256",
326+
// "hosts": {
327+
// "bitcoin.stackwallet.com": {
328+
// "ssl_port": 50002,
329+
// "tcp_port": 50001,
330+
// "ws_port": 50003,
331+
// "wss_port": 50004
332+
// }
333+
// },
334+
// "protocol_max": "1.5",
335+
// "protocol_min": "1.4",
336+
// "pruning": null,
337+
// "server_version": "Fulcrum 1.9.1"
338+
// }
339+
// }
340+
341+
// Close the socket.
342+
await socksSocket.close();
343+
}
344+
345+
// A mutex should be added to this example to prevent
346+
// multiple connections from being made at once. TODO
347+
: null,
348+
child: const Text(
349+
"Test Bitcoin onion node connection",
350+
),
351+
),
352+
],
353+
),
354+
spacerSmall,
355+
Row(
356+
children: [
357+
// Monero onion input field.
358+
Expanded(
359+
child: TextField(
360+
controller: moneroOnionController,
361+
decoration: const InputDecoration(
362+
border: OutlineInputBorder(),
363+
hintText: 'Monero onion address to test',
364+
),
365+
),
366+
),
367+
spacerSmall,
368+
TextButton(
369+
onPressed: torStarted
370+
? () async {
371+
// Validate the onion address.
372+
if (!moneroOnionController.text
373+
.contains(".onion")) {
374+
print("Invalid onion address");
375+
return;
376+
} else if (!moneroOnionController.text
377+
.contains(":")) {
378+
print("Invalid onion address (needs port)");
379+
return;
380+
}
381+
382+
final String host =
383+
moneroOnionController.text.split(":").first;
384+
final int port = int.parse(moneroOnionController
385+
.text
386+
.split(":")
387+
.last
388+
.split("/")
389+
.first);
390+
final String path = moneroOnionController.text
391+
.split(":")
392+
.last
393+
.split("/")
394+
.last; // Extract the path
395+
396+
var socksSocket = await SOCKSSocket.create(
397+
proxyHost: InternetAddress.loopbackIPv4.address,
398+
proxyPort: Tor.instance.port,
399+
sslEnabled: false,
400+
);
401+
402+
await socksSocket.connect();
403+
await socksSocket.connectTo(host, port);
404+
405+
final body = jsonEncode({
406+
"jsonrpc": "2.0",
407+
"id": "0",
408+
"method": "get_info",
409+
});
410+
411+
final request = 'POST /$path HTTP/1.1\r\n'
412+
'Host: $host\r\n'
413+
'Content-Type: application/json\r\n'
414+
'Content-Length: ${body.length}\r\n'
415+
'\r\n'
416+
'$body';
417+
418+
socksSocket.write(request);
419+
print("Request sent: $request");
420+
421+
await for (var response
422+
in socksSocket.inputStream) {
423+
final result = utf8.decode(response);
424+
print("Response received: $result");
425+
break;
426+
}
427+
428+
// You should see a server response printed to the console.
429+
//
430+
// Example response:
431+
// Host: ucdouiihzwvb5edg3ezeufcs4yp26gq4x64n6b4kuffb7s7jxynnk7qd.onion
432+
// Content-Type: application/json
433+
// Content-Length: 46
434+
//
435+
// {"jsonrpc":"2.0","id":"0","method":"get_info"}
436+
// flutter: Response received: HTTP/1.1 200 Ok
437+
// Server: Epee-based
438+
// Content-Length: 1434
439+
// Content-Type: application/json
440+
// Last-Modified: Thu, 03 Oct 2024 23:08:19 GMT
441+
// Accept-Ranges: bytes
442+
//
443+
// {
444+
// "id": "0",
445+
// "jsonrpc": "2.0",
446+
// "result": {
447+
// "adjusted_time": 1727996959,
448+
// ...
449+
450+
await socksSocket.close();
451+
}
452+
453+
// A mutex should be added to this example to prevent
454+
// multiple connections from being made at once. TODO
455+
: null,
456+
child: const Text(
457+
"Test Monero onion node connection",
458+
),
459+
),
460+
],
461+
),
238462
],
239463
),
240464
),

example/pubspec.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,10 @@ packages:
220220
dependency: "direct main"
221221
description:
222222
name: socks5_proxy
223-
sha256: "1d21b5606169654bbf4cfb904e8e6ed897e9f763358709f87310c757096d909a"
223+
sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75
224224
url: "https://pub.dev"
225225
source: hosted
226-
version: "1.0.4"
226+
version: "1.0.3+dev.3"
227227
source_span:
228228
dependency: transitive
229229
description:
@@ -278,7 +278,7 @@ packages:
278278
path: ".."
279279
relative: true
280280
source: path
281-
version: "0.0.7"
281+
version: "0.0.8"
282282
vector_math:
283283
dependency: transitive
284284
description:

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ dependencies:
4646
# The following adds the Cupertino Icons font to your application.
4747
# Use with the CupertinoIcons class for iOS style icons.
4848
cupertino_icons: ^1.0.2
49-
socks5_proxy: ^1.0.3+dev.3
49+
socks5_proxy: 1.0.3+dev.3
5050

5151
dev_dependencies:
5252
flutter_test:

lib/socks_socket.dart

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2024 Foundation Devices Inc.
1+
// SPDX-FileCopyrightText: 2024 Cypher Stack LLC
22
//
33
// SPDX-License-Identifier: MIT
44

@@ -10,8 +10,7 @@ import 'package:flutter/foundation.dart';
1010

1111
/// A SOCKS5 socket.
1212
///
13-
/// This class is a wrapper around the Socket class that implements the
14-
/// SOCKS5 protocol. It supports SSL and non-SSL connections.
13+
/// A Dart 3 Socket wrapper that implements the SOCKS5 protocol. Now with SSL!
1514
///
1615
/// Properties:
1716
/// - [proxyHost]: The host of the SOCKS5 proxy server.
@@ -99,6 +98,26 @@ class SOCKSSocket {
9998
/// Private constructor.
10099
SOCKSSocket._(this.proxyHost, this.proxyPort, this.sslEnabled);
101100

101+
/// Provides a stream of data as List<int>.
102+
Stream<List<int>> get inputStream => sslEnabled
103+
? _secureResponseController.stream
104+
: _responseController.stream;
105+
106+
/// Provides a StreamSink compatible with List<int> for sending data.
107+
StreamSink<List<int>> get outputStream {
108+
// Create a simple StreamSink wrapper for _socksSocket and
109+
// _secureSocksSocket that accepts List<int> and forwards it to write method.
110+
var sink = StreamController<List<int>>();
111+
sink.stream.listen((data) {
112+
if (sslEnabled) {
113+
_secureSocksSocket.add(data);
114+
} else {
115+
_socksSocket.add(data);
116+
}
117+
});
118+
return sink.sink;
119+
}
120+
102121
/// Creates a SOCKS5 socket to the specified [proxyHost] and [proxyPort].
103122
///
104123
/// This method is a factory constructor that returns a Future that resolves
@@ -163,7 +182,7 @@ class SOCKSSocket {
163182
},
164183
onDone: () {
165184
// Close the response controller when the socket is closed.
166-
_responseController.close();
185+
// _responseController.close();
167186
},
168187
);
169188
}
@@ -221,7 +240,7 @@ class SOCKSSocket {
221240
'socks_socket.connectTo(): Failed to connect to target through SOCKS5 proxy.');
222241
}
223242

224-
// Upgrade to SSL if needed
243+
// Upgrade to SSL if needed.
225244
if (sslEnabled) {
226245
// Upgrade to SSL.
227246
_secureSocksSocket = await SecureSocket.secure(
@@ -283,15 +302,19 @@ class SOCKSSocket {
283302
/// A Future that resolves to void.
284303
Future<void> close() async {
285304
// Ensure all data is sent before closing.
286-
//
287-
// TODO test this.
288-
if (sslEnabled) {
305+
try {
306+
if (sslEnabled) {
307+
await _secureSocksSocket.flush();
308+
}
289309
await _socksSocket.flush();
290-
await _secureResponseController.close();
310+
} finally {
311+
await _subscription?.cancel();
312+
await _socksSocket.close();
313+
_responseController.close();
314+
if (sslEnabled) {
315+
_secureResponseController.close();
316+
}
291317
}
292-
await _socksSocket.flush();
293-
await _responseController.close();
294-
return await _socksSocket.close();
295318
}
296319

297320
StreamSubscription<List<int>> listen(

0 commit comments

Comments
 (0)