CW-519 Enable built-in Tor (#1950)

* tor wip

* Enable tor on iOS

* Prevent app lag when node is exceptionally slow (usually over tor)

* fix: logic in daemonBlockchainHeight refresh
fix: storing tor state

* Pin ledger_flutter_plus dependency to fix builds

* bump arti version

* wip

* add single httpclient

* route everything I was able to catch trough the built-in tor node

* Enable proxy for http.Client [run tests]

* add tor proxy support to cw_evm, cw_tron and cw_polygon [run tests]

* remove log pollution, cleanup [skip slack]

* fix tests not working in latest main [skip slack] [run tests]

* remove cw_wownero import

* fix build issues

* migrate all remaining calls to use ProxyWrapper
add a CI action to enforce using ProxyWrapper instead of http/http.dart to prevent leaks

* fix tor background sync (will work on test builds after #2142 is merged and this PR is rebased on top)

* wip [skip ci]

* relicense to GPLv3 add socks5 license, build fixes

* use ProxyWrapper instead of http in robinhood

* Revert "relicense to GPLv3"

* feat(cw_bitcoin): support socks proxy and CakeTor

* fix(tor): migrate OCP and EVM over to ProxyWrapper()

* chore: cleanup
fix: show tor loading screen when app is starting

* fix: tor switch properly dismisses fullscreen loading dialog
fix: connectToNode after tor startup on app start

* fix(tor): status check for xmr/wow/zano

* fix(tor): onramper request fix

* fix(api): ServicesResponse is now being cached and doesn't fetch data everytime DashboardViewModel is being rebuilt
fix(tor): do not fallback to clearnet when tor failed.
fix(tor): do not leak connections during app startup
chore: refactor bootstrap() function to be separated into bootstrapOffline and bootstrapOnline
fix(cw_bitcoin): migrate payjoin to use ProxyWrapper

* [skip ci] remove print

* address comments from review

* fix: derusting tor implementation

Instead of rust-based Arti I've moved back to the
OG C++ tor implementation.
This fixed all issues we had with Tor.
- onion services now work
- all requests are going through without random errors
- we don't have to navigate a maze of multiple forks of multiple packages
- fully working `torrc` config file (probably will be needed for Tari).
- logging for Tor client
- and so on.

feat: network logging tab
feat: use built-in proxy on Tails - this should resolve all issues for Tails users (needs testing though)

* fix conflicts with main
bump https to fix build issue
relax store() call

* fix(cw_wownero): tor connection
fix(tor): connection issues

* fix(cw_evm): add missing chainId
fix(cw_core): solana rpc fix

* feat: mark tor as experimental
fix: drop anonpay onion authority
fix: drop fiatapi onion authority
fix: drop trocador onion authority
fix: disable networkimage when tor is enabled
fix: handle cakepay errors gracefully

* fix re-formatting [skip ci]

* changes from review

* Delete android/.kotlin/sessions/kotlin-compiler-2468481326039681181.salive

* fix missing imports

* Update pubspec_base.yaml

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan 2025-06-20 21:56:18 +02:00 committed by GitHub
parent 18c2ba9366
commit 5082dc20f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 2754 additions and 878 deletions

View file

@ -1,7 +1,7 @@
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:intl/intl.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
// FIXME: Hardcoded values; Works only for monero
@ -234,10 +234,14 @@ int getHavenHeightByDate({required DateTime date}) {
}
Future<int> getHavenCurrentHeight() async {
final response = await http.get(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo'));
final req = await ProxyWrapper().getHttpClient()
.getUrl(Uri.parse('https://explorer.havenprotocol.org/api/networkinfo'))
.timeout(Duration(seconds: 15));
final response = await req.close();
final stringResponse = await response.transform(utf8.decoder).join();
if (response.statusCode == 200) {
final info = jsonDecode(response.body);
final info = jsonDecode(stringResponse);
return info['data']['height'] as int;
} else {
throw Exception('Failed to load current blockchain height');
@ -269,13 +273,13 @@ const bitcoinDates = {
};
Future<int> getBitcoinHeightByDateAPI({required DateTime date}) async {
final response = await http.get(
Uri.parse(
"https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}",
),
);
final req = await ProxyWrapper().getHttpClient()
.getUrl(Uri.parse("https://mempool.cakewallet.com/api/v1/mining/blocks/timestamp/${(date.millisecondsSinceEpoch / 1000).round()}"))
.timeout(Duration(seconds: 15));
final response = await req.close();
final stringResponse = await response.transform(utf8.decoder).join();
return jsonDecode(response.body)['height'] as int;
return jsonDecode(stringResponse)['height'] as int;
}
int getBitcoinHeightByDate({required DateTime date}) {

View file

@ -1,12 +1,12 @@
import 'dart:io';
import 'package:cw_core/keyable.dart';
import 'package:cw_core/utils/proxy_socket/abstract.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:hive/hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/io_client.dart' as ioc;
import 'dart:math' as math;
import 'package:convert/convert.dart';
@ -184,23 +184,17 @@ class Node extends HiveObject with Keyable {
final body = {'jsonrpc': '2.0', 'id': '0', 'method': "getinfo"};
try {
final authenticatingClient = HttpClient();
authenticatingClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
final http.Client client = ioc.IOClient(authenticatingClient);
final jsonBody = json.encode(body);
final response = await client.post(
rpcUri,
final response = await ProxyWrapper().post(
clearnetUri: rpcUri,
headers: {'Content-Type': 'application/json'},
body: jsonBody,
);
printV("node check response: ${response.body}");
final resBody = json.decode(response.body) as Map<String, dynamic>;
return resBody['result']['height'] != null;
} catch (e) {
printV("error: $e");
@ -218,11 +212,7 @@ class Node extends HiveObject with Keyable {
final body = {'jsonrpc': '2.0', 'id': '0', 'method': methodName};
try {
final authenticatingClient = HttpClient();
authenticatingClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
final http.Client client = ioc.IOClient(authenticatingClient);
final client = ProxyWrapper().getHttpIOClient();
final jsonBody = json.encode(body);
@ -242,15 +232,15 @@ class Node extends HiveObject with Keyable {
return !(response['offline'] as bool);
}
printV("node check response: ${response.body}");
final responseString = await response.body;
if ((response.body.contains("400 Bad Request") // Some other generic error
if ((responseString.contains("400 Bad Request") // Some other generic error
||
response.body.contains("plain HTTP request was sent to HTTPS port") // Cloudflare
responseString.contains("plain HTTP request was sent to HTTPS port") // Cloudflare
||
response.headers["location"] != null // Generic reverse proxy
||
response.body
responseString
.contains("301 Moved Permanently") // Poorly configured generic reverse proxy
) &&
!(useSSL ?? false)) {
@ -277,15 +267,16 @@ class Node extends HiveObject with Keyable {
}
Future<bool> requestNodeWithProxy() async {
if (!isValidProxyAddress /* && !Tor.instance.enabled*/) {
if (!isValidProxyAddress && !CakeTor.instance.enabled) {
return false;
}
String? proxy = socksProxyAddress;
// if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) {
// proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}";
// }
if ((proxy?.isEmpty ?? true) && CakeTor.instance.enabled) {
proxy = "${InternetAddress.loopbackIPv4.address}:${CakeTor.instance.port}";
}
printV("proxy: $proxy");
if (proxy == null) {
return false;
}
@ -305,13 +296,9 @@ class Node extends HiveObject with Keyable {
// you try to communicate with it
Future<bool> requestElectrumServer() async {
try {
final Socket socket;
if (useSSL == true) {
socket = await SecureSocket.connect(uri.host, uri.port,
timeout: Duration(seconds: 5), onBadCertificate: (_) => true);
} else {
socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5));
}
final ProxySocket socket;
socket = await ProxyWrapper().getSocksSocket(useSSL ?? false, uri.host, uri.port);
socket.destroy();
return true;
@ -322,8 +309,8 @@ class Node extends HiveObject with Keyable {
Future<bool> requestNanoNode() async {
try {
final response = await http.post(
uri,
final response = await ProxyWrapper().post(
clearnetUri: uri,
headers: {"Content-Type": "application/json", "nano-app": "cake-wallet"},
body: jsonEncode(
{
@ -332,7 +319,8 @@ class Node extends HiveObject with Keyable {
},
),
);
final data = await jsonDecode(response.body);
final data = jsonDecode(response.body);
if (response.statusCode != 200 ||
data["error"] != null ||
data["balance"] == null ||
@ -348,13 +336,14 @@ class Node extends HiveObject with Keyable {
Future<bool> requestEthereumServer() async {
try {
final response = await http.get(
uri,
headers: {'Content-Type': 'application/json'},
);
final req = await ProxyWrapper().getHttpClient()
.getUrl(uri,)
.timeout(Duration(seconds: 15));
final response = await req.close();
return response.statusCode >= 200 && response.statusCode < 300;
} catch (_) {
} catch (err) {
printV("Failed to request ethereum server: $err");
return false;
}
}
@ -462,7 +451,7 @@ class DaemonRpc {
/// Perform a JSON-RPC call with Digest Authentication.
Future<Map<String, dynamic>> call(String method, Map<String, dynamic> params) async {
final http.Client client = http.Client();
final client = ProxyWrapper().getHttpIOClient();
final DigestAuth digestAuth = DigestAuth(username, password);
// Initial request to get the `WWW-Authenticate` header.

View file

@ -1,20 +1,19 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:on_chain/solana/solana.dart';
class SolanaRPCHTTPService implements SolanaJSONRPCService {
SolanaRPCHTTPService(
{required this.url, Client? client, this.defaultRequestTimeout = const Duration(seconds: 30)})
: client = client ?? Client();
{required this.url,
this.defaultRequestTimeout = const Duration(seconds: 30)});
@override
final String url;
final Client client;
final Duration defaultRequestTimeout;
@override
Future<Map<String, dynamic>> call(SolanaRequestDetails params, [Duration? timeout]) async {
final response = await client.post(
Uri.parse(url),
Future<Map<String, dynamic>> call(SolanaRequestDetails params,
[Duration? timeout]) async {
final response = await ProxyWrapper().post(
clearnetUri: Uri.parse(url),
body: params.toRequestBody(),
headers: {
'Content-Type': 'application/json',

View file

@ -0,0 +1,29 @@
import 'dart:typed_data';
import 'package:http/http.dart' as very_insecure_http_do_not_use;
enum RequestNetwork {
clearnet,
tor,
}
enum RequestMethod {
get,
post,
put,
delete,
newHttpClient,
newHttpIOClient,
newProxySocket,
}
abstract class ProxyLogger {
void log({
required Uri? uri,
required RequestMethod method,
required Uint8List body,
required very_insecure_http_do_not_use.Response? response,
required RequestNetwork network,
required String? error,
});
}

View file

@ -0,0 +1,63 @@
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_logger/abstract.dart';
import 'package:http/http.dart' as very_insecure_http_do_not_use;
class MemoryProxyLoggerEntry {
MemoryProxyLoggerEntry({
required this.trace,
required this.uri,
required this.body,
required this.network,
required this.method,
required this.response,
required this.error,
}) : time = DateTime.now();
final StackTrace trace;
final Uri? uri;
final Uint8List body;
final RequestNetwork network;
final very_insecure_http_do_not_use.Response? response;
final RequestMethod method;
final String? error;
final DateTime time;
@override
String toString() => """MemoryProxyLoggerEntry(
uri: $uri,
body: $body,
network: $network,
method: $method,
response:
code: ${response?.statusCode},
headers: ${response?.headers},
body: ${response?.body},
error: $error,
time: $time,
trace: ${trace}
);""";
}
class MemoryProxyLogger implements ProxyLogger {
static List<MemoryProxyLoggerEntry> logs = [];
@override
void log({
required Uri? uri,
required RequestMethod method,
required Uint8List body,
required very_insecure_http_do_not_use.Response? response,
required RequestNetwork network,
required String? error,
}) {
final trace = StackTrace.current;
logs.add(MemoryProxyLoggerEntry(
method: method,
trace: trace,
uri: uri,
body: body,
network: network,
response: response,
error: error,),
);
}
}

View file

@ -0,0 +1,17 @@
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_logger/abstract.dart';
import 'package:http/http.dart' as very_insecure_http_do_not_use;
// we are not doing anything
class SilentProxyLogger implements ProxyLogger {
@override
void log({
required Uri? uri,
required RequestMethod method,
required Uint8List body,
required very_insecure_http_do_not_use.Response? response,
required RequestNetwork network,
required String? error,
}) {}
}

View file

@ -0,0 +1,47 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_socket/insecure.dart';
import 'package:cw_core/utils/proxy_socket/secure.dart';
import 'package:cw_core/utils/proxy_socket/socks.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:socks_socket/socks_socket.dart';
class ProxyAddress {
final String host;
final int port;
ProxyAddress({required this.host, required this.port});
}
abstract class ProxySocket {
static Future<ProxySocket> connect(bool sslEnabled, ProxyAddress address, {Duration? connectionTimeout}) async {
if (CakeTor.instance.started) {
var socksSocket = await SOCKSSocket.create(
proxyHost: InternetAddress.loopbackIPv4.address,
proxyPort: CakeTor.instance.port,
sslEnabled: sslEnabled,
);
await socksSocket.connect();
await socksSocket.connectTo(address.host, address.port);
return ProxySocketSocks(socksSocket);
}
if (sslEnabled == false) {
return ProxySocketInsecure(await Socket.connect(address.host, address.port, timeout: connectionTimeout));
} else {
return ProxySocketSecure(await SecureSocket.connect(
address.host,
address.port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
));
}
}
Future<void> close();
Future<void> destroy();
Future<void> write(String data);
StreamSubscription<List<int>> listen(Function(Uint8List event) onData, {Function (Object error)? onError, Function ()? onDone, bool cancelOnError = true});
ProxyAddress get address;
}

View file

@ -0,0 +1,34 @@
import 'package:cw_core/utils/proxy_socket/abstract.dart';
import 'dart:async';
import 'dart:typed_data';
import 'dart:io';
class ProxySocketInsecure implements ProxySocket {
final Socket socket;
ProxySocketInsecure(this.socket);
ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort);
@override
Future<void> close() => socket.close();
@override
Future<void> destroy() async => socket.destroy();
@override
Future<void> write(String data) async => socket.write(data);
@override
StreamSubscription<List<int>> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) {
return socket.listen(
(data) {
onData(Uint8List.fromList(data));
},
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
}

View file

@ -0,0 +1,34 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_socket/abstract.dart';
class ProxySocketSecure implements ProxySocket {
final SecureSocket socket;
ProxySocketSecure(this.socket);
ProxyAddress get address => ProxyAddress(host: socket.remoteAddress.host, port: socket.remotePort);
@override
Future<void> close() => socket.close();
@override
Future<void> destroy() async => socket.destroy();
@override
Future<void> write(String data) async => socket.write(data);
@override
StreamSubscription<List<int>> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) {
return socket.listen(
(data) {
onData(Uint8List.fromList(data));
},
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
}

View file

@ -0,0 +1,36 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_socket/abstract.dart';
import 'package:socks_socket/socks_socket.dart';
class ProxySocketSocks implements ProxySocket {
final SOCKSSocket socket;
ProxySocketSocks(this.socket);
@override
ProxyAddress get address => ProxyAddress(host: socket.proxyHost, port: socket.proxyPort);
@override
Future<void> close() => socket.close();
@override
Future<void> destroy() => close();
@override
Future<void> write(String data) async => socket.write(data);
@override
StreamSubscription<List<int>> listen(Function(Uint8List event) onData, {Function(Object error)? onError, Function()? onDone, bool cancelOnError = true}) {
return socket.listen(
(data) {
onData(Uint8List.fromList(data));
},
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
}

View file

@ -0,0 +1,447 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cw_core/utils/proxy_logger/abstract.dart';
import 'package:cw_core/utils/proxy_socket/abstract.dart';
import 'package:cw_core/utils/tor/abstract.dart';
import 'package:cw_core/utils/tor/android.dart';
import 'package:cw_core/utils/tor/disabled.dart';
import 'package:http/http.dart';
import 'package:socks5_proxy/socks_client.dart';
import 'package:http/io_client.dart' as ioc;
class ProxyWrapper {
static final ProxyWrapper _proxyWrapper = ProxyWrapper._internal();
static ProxyLogger? logger;
factory ProxyWrapper() {
return _proxyWrapper;
}
ProxyWrapper._internal();
Future<ProxySocket> getSocksSocket(bool sslEnabled, String host, int port, {Duration? connectionTimeout}) async {
logger?.log(
uri: Uri(
scheme: sslEnabled ? "https" : "http",
host: host,
port: port,
),
method: RequestMethod.newProxySocket,
body: Uint8List(0),
response: null,
network: requestNetwork(),
error: null
);
return ProxySocket.connect(sslEnabled, ProxyAddress(host: host, port: port), connectionTimeout: connectionTimeout);
}
RequestNetwork requestNetwork() {
return CakeTor.instance.started ? RequestNetwork.tor : RequestNetwork.clearnet;
}
ioc.IOClient getHttpIOClient({int? portOverride, bool internal = false}) {
if (!internal) {
logger?.log(
uri: null,
method: RequestMethod.newHttpIOClient,
body: Uint8List(0),
response: null,
network: requestNetwork(),
error: null,
);
}
// ignore: deprecated_member_use_from_same_package
final httpClient = ProxyWrapper().getHttpClient(portOverride: portOverride, internal: true);
return ioc.IOClient(httpClient);
}
int getPort() => CakeTor.instance.port;
@Deprecated('Use ProxyWrapper().get/post/put methods instead, and provide proper clearnet and onion uri.')
HttpClient getHttpClient({int? portOverride, bool internal = false}) {
if (!internal) {
logger?.log(
uri: null,
method: RequestMethod.newProxySocket,
body: Uint8List(0),
response: null,
network: requestNetwork(),
error: null
);
}
if (CakeTor.instance.started) {
// Assign connection factory.
final client = HttpClient();
SocksTCPClient.assignToHttpClient(client, [
ProxySettings(
InternetAddress.loopbackIPv4,
CakeTor.instance.port,
password: null,
),
]);
return client;
} else {
return HttpClient();
}
}
Future<Response> _make({
required RequestMethod method,
required ioc.IOClient client,
required Uri uri,
required Map<String, String>? headers,
String? body,
}) async {
Object? error;
Response? resp;
try {
switch (method) {
case RequestMethod.get:
resp = await client. get(
uri,
headers: headers,
);
break;
case RequestMethod.delete:
resp = await client.delete(
uri,
headers: headers,
body: body,
);
break;
case RequestMethod.post:
resp = await client.post(
uri,
headers: headers,
body: body,
);
break;
case RequestMethod.put:
resp = await client.put(
uri,
headers: headers,
body: body,
);
break;
case RequestMethod.newHttpClient:
case RequestMethod.newHttpIOClient:
case RequestMethod.newProxySocket:
throw UnimplementedError();
}
return resp;
} catch (e) {
error = e;
rethrow;
} finally {
logger?.log(
uri: uri,
method: RequestMethod.get,
body: utf8.encode(body ?? ''),
response: resp,
network: requestNetwork(),
error: error?.toString(),
);
}
}
Future<Response> get({
Map<String, String>? headers,
int? portOverride,
Uri? clearnetUri,
Uri? onionUri,
}) async {
ioc.IOClient? torClient;
bool torEnabled = CakeTor.instance.started;
if (CakeTor.instance.started) {
torEnabled = true;
} else {
torEnabled = false;
}
// if tor is enabled, try to connect to the onion url first:
if (torEnabled) {
try {
// ignore: deprecated_member_use_from_same_package
torClient = await getHttpIOClient(portOverride: portOverride, internal: true);
} catch (_) {
rethrow;
}
if (onionUri != null) {
try {
return await _make(
method: RequestMethod.get,
client: torClient,
uri: onionUri,
headers: headers,
);
} catch (_) {
rethrow;
}
}
if (clearnetUri != null) {
try {
return await _make(
method: RequestMethod.get,
client: torClient,
uri: clearnetUri,
headers: headers,
);
} catch (_) {
rethrow;
}
}
}
if (clearnetUri != null) {
try {
return HttpOverrides.runZoned(
() async {
return await _make(
method: RequestMethod.get,
client: ioc.IOClient(),
uri: clearnetUri,
headers: headers,
);
},
);
} catch (_) {
// we weren't able to get a response:
rethrow;
}
}
throw Exception("Unable to connect to server");
}
Future<Response> post({
Map<String, String>? headers,
int? portOverride,
Uri? clearnetUri,
Uri? onionUri,
String? body,
bool allowMitmMoneroBypassSSLCheck = false,
}) async {
HttpClient? torHttpClient;
HttpClient cleatnetHttpClient = HttpClient();
if (allowMitmMoneroBypassSSLCheck) {
cleatnetHttpClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
}
ioc.IOClient clearnetClient = ioc.IOClient(cleatnetHttpClient);
bool torEnabled = CakeTor.instance.started;
if (torEnabled) {
try {
// ignore: deprecated_member_use_from_same_package
torHttpClient = await getHttpClient(portOverride: portOverride);
} catch (_) {
rethrow;
}
if (allowMitmMoneroBypassSSLCheck) {
torHttpClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
}
if (onionUri != null) {
try {
return await _make(
method: RequestMethod.post,
client: ioc.IOClient(torHttpClient),
uri: onionUri,
headers: headers,
body: body,
);
} catch (_) {
rethrow;
}
}
if (clearnetUri != null) {
try {
return await _make(
method: RequestMethod.post,
client: ioc.IOClient(torHttpClient),
uri: clearnetUri,
headers: headers,
body: body,
);
} catch (_) {
rethrow;
}
}
}
if (clearnetUri != null) {
try {
return HttpOverrides.runZoned(
() async {
return await _make(
method: RequestMethod.post,
client: clearnetClient,
uri: clearnetUri,
headers: headers,
body: body,
);
},
);
} catch (_) {
rethrow;
}
}
throw Exception("Unable to connect to server");
}
Future<Response> put({
Map<String, String>? headers,
int? portOverride,
Uri? clearnetUri,
Uri? onionUri,
String? body,
}) async {
ioc.IOClient? torClient;
bool torEnabled = CakeTor.instance.started;
if (torEnabled) {
try {
// ignore: deprecated_member_use_from_same_package
torClient = await getHttpIOClient(portOverride: portOverride, internal: true);
} catch (_) {}
if (onionUri != null) {
try {
return await _make(
method: RequestMethod.put,
client: torClient!,
uri: onionUri,
headers: headers,
body: body,
);
} catch (_) {
rethrow;
}
}
if (clearnetUri != null) {
try {
return await _make(
method: RequestMethod.put,
client: torClient!,
uri: clearnetUri,
headers: headers,
body: body,
);
} catch (_) {
rethrow;
}
}
}
if (clearnetUri != null) {
try {
return HttpOverrides.runZoned(
() async {
return await _make(
method: RequestMethod.put,
client: ioc.IOClient(),
uri: clearnetUri,
headers: headers,
body: body,
);
},
);
} catch (_) {
// we weren't able to get a response:
rethrow;
}
}
throw Exception("Unable to connect to server");
}
Future<Response> delete({
Map<String, String>? headers,
int? portOverride,
Uri? clearnetUri,
Uri? onionUri,
}) async {
ioc.IOClient? torClient;
bool torEnabled = CakeTor.instance.started;
if (CakeTor.instance.started) {
torEnabled = true;
} else {
torEnabled = false;
}
// if tor is enabled, try to connect to the onion url first:
if (torEnabled) {
try {
// ignore: deprecated_member_use_from_same_package
torClient = await getHttpIOClient(portOverride: portOverride, internal: true);
} catch (_) {
rethrow;
}
if (onionUri != null) {
try {
return await _make(
method: RequestMethod.delete,
client: torClient,
uri: onionUri,
headers: headers,
);
} catch (_) {
rethrow;
}
}
if (clearnetUri != null) {
try {
return await _make(
method: RequestMethod.delete,
client: torClient,
uri: clearnetUri,
headers: headers,
);
} catch (_) {
rethrow;
}
}
}
if (clearnetUri != null) {
try {
return HttpOverrides.runZoned(
() async {
return await _make(
method: RequestMethod.delete,
client: ioc.IOClient(),
uri: clearnetUri,
headers: headers,
);
},
);
} catch (_) {
// we weren't able to get a response:
rethrow;
}
}
throw Exception("Unable to connect to server");
}
}
class CakeTor {
static final CakeTorInstance instance = CakeTorInstance.getInstance();
}

View file

@ -0,0 +1,38 @@
import 'dart:io';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/tor/android.dart';
import 'package:cw_core/utils/tor/disabled.dart';
import 'package:cw_core/utils/tor/tails.dart';
abstract class CakeTorInstance {
bool get started;
int get port => -1;
bool get enabled => false;
bool get bootstrapped => false;
Future<void> start();
Future<void> stop();
static CakeTorInstance getInstance() {
if (Platform.isAndroid) {
return CakeTorAndroid();
}
if (Platform.isLinux) {
try {
final os = File("/etc/os-release").readAsLinesSync();
for (var line in os) {
if (!line.startsWith("ID=")) continue;
if (!line.contains("tails")) continue;
return CakeTorTails();
}
} catch (e) {
printV("Failed to identify linux version - /etc/os-release missing");
}
}
return CakeTorDisabled();
}
}

View file

@ -0,0 +1,73 @@
import 'dart:convert';
import 'dart:io';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/tor/abstract.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:tor_binary/tor_binary_platform_interface.dart';
class CakeTorAndroid implements CakeTorInstance {
@override
bool get bootstrapped => _proc != null;
@override
bool get enabled => _proc != null;
@override
int get port => 42142;
@override
Future<void> start() async {
await _runEmbeddedTor();
}
@override
bool get started => _proc != null;
@override
Future<void> stop() async {
_proc?.kill();
await _proc?.exitCode;
_proc = null;
}
static Process? _proc;
Future<void> _runEmbeddedTor() async {
final dir = await getApplicationCacheDirectory();
final torBinPath = p.join((await TorBinaryPlatform.instance.getBinaryPath())!, "libtor.so");
printV("torPath: $torBinPath");
if (started) {
printV("Proxy is running");
return;
}
printV("Starting embedded tor");
printV("app docs: $dir");
final torrc = """
SocksPort $port
Log notice file ${p.join(dir.path, "tor.log")}
RunAsDaemon 0
DataDirectory ${p.join(dir.path, "tor-data")}
""";
final torrcPath = p.join(dir.absolute.path, "torrc");
File(torrcPath).writeAsStringSync(torrc);
if (_proc != null) {
try {
_proc?.kill();
await _proc?.exitCode;
_proc = null;
} catch (e) {
printV(e);
}
}
printV("path: $torBinPath -f $torrcPath");
_proc = await Process.start(torBinPath, ["-f", torrcPath]);
_proc?.stdout.transform(utf8.decoder).forEach(printV);
_proc?.stderr.transform(utf8.decoder).forEach(printV);
}
}

View file

@ -0,0 +1,21 @@
import 'package:cw_core/utils/tor/abstract.dart';
class CakeTorDisabled implements CakeTorInstance {
@override
bool get bootstrapped => false;
@override
bool get enabled => false;
@override
int get port => -1;
@override
Future<void> start() => throw UnimplementedError();
@override
bool get started => false;
@override
Future<void> stop() => throw UnimplementedError();
}

View file

@ -0,0 +1,21 @@
import 'package:cw_core/utils/tor/abstract.dart';
class CakeTorTails implements CakeTorInstance {
@override
bool get bootstrapped => true;
@override
bool get enabled => true;
@override
int get port => 9150;
@override
Future<void> start() async {}
@override
bool get started => true;
@override
Future<void> stop() async {}
}

View file

@ -479,7 +479,7 @@ packages:
description:
path: "."
ref: cake-update-v2
resolved-ref: "01cbbacbb05d2113aafa8b7c4a2bb766f749d8d8"
resolved-ref: "096865a8c6b89c260beadfec04f7e184c40a3273"
url: "https://github.com/cake-tech/on_chain.git"
source: git
version: "3.7.0"
@ -635,11 +635,21 @@ packages:
socks5_proxy:
dependency: "direct main"
description:
name: socks5_proxy
sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
path: "."
ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
resolved-ref: "27ad7c2efae8d7460325c74b90f660085cbd0685"
url: "https://github.com/LacticWhale/socks_dart"
source: git
version: "2.1.0"
socks_socket:
dependency: "direct main"
description:
path: "."
ref: e6232c53c1595469931ababa878759a067c02e94
resolved-ref: e6232c53c1595469931ababa878759a067c02e94
url: "https://github.com/sneurlax/socks_socket"
source: git
version: "1.1.1"
source_gen:
dependency: transitive
description:
@ -716,10 +726,19 @@ packages:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.0.1"
tor_binary:
dependency: "direct main"
description:
path: "."
ref: cb811c610871a9517d47134b87c2f590c15c96c5
resolved-ref: cb811c610871a9517d47134b87c2f590c15c96c5
url: "https://github.com/MrCyjaneK/flutter-tor_binary"
source: git
version: "4.7.14"
tuple:
dependency: transitive
description:

View file

@ -25,16 +25,23 @@ dependencies:
url: https://github.com/cake-tech/cake_backup.git
ref: main
version: 1.0.0
socks5_proxy: ^1.0.4
socks5_proxy:
git:
url: https://github.com/LacticWhale/socks_dart
ref: 27ad7c2efae8d7460325c74b90f660085cbd0685
unorm_dart: ^0.3.0
on_chain:
git:
url: https://github.com/cake-tech/on_chain.git
ref: cake-update-v2
# tor:
# git:
# url: https://github.com/cake-tech/tor.git
# ref: main
socks_socket:
git:
url: https://github.com/sneurlax/socks_socket
ref: e6232c53c1595469931ababa878759a067c02e94
tor_binary:
git:
url: https://github.com/MrCyjaneK/flutter-tor_binary
ref: cb811c610871a9517d47134b87c2f590c15c96c5
dev_dependencies:
flutter_test: