haveno-app/lib/services/tor_interface.dart
2024-12-08 06:38:57 +00:00

334 lines
No EOL
12 KiB
Dart

// Haveno App extends the features of Haveno, supporting mobile devices and more.
// Copyright (C) 2024 Kewbit (https://kewbit.org)
// Source Code: https://git.haveno.com/haveno/haveno-app.git
//
// Author: Kewbit
// Website: https://kewbit.org
// Contact Email: me@kewbit.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:cryptography/cryptography.dart';
import 'package:haveno_app/models/tor/tor_daemon_config.dart';
import 'package:haveno_app/models/tor/hsv3_onion_config.dart';
// import 'package:haveno_app/services/tor/tor_status_service.dart';
import 'package:http/io_client.dart';
//import 'package:tor/socks_socket.dart';
class TorService {
final StreamController<String> _statusController = StreamController<String>.broadcast();
final StreamController<String> _controlPortStatusController = StreamController<String>.broadcast();
late final TorDaemonConfig _torDaemonConfig;
Stream<String> get statusStream => _statusController.stream;
Stream<String> get controlPortStatusStream => _controlPortStatusController.stream;
TorService();
static Future<bool> isTorConnected() async {
//final torStatusService = TorStatusService();
int socksPort = 9066;
bool socks5Open = false;
bool i2pOpen = false;
//if (Platform.isAndroid || Platform.isIOS) {
// if (torStatusService.torPort == null) {
// return false;
// } else {
// socksPort = torStatusService.torPort!;
// }
//}
try {
if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
socksPort = 9066; // Example port, adjust as needed
socks5Open = await checkSocks5Proxy(testHost: '127.0.0.1', testPort: socksPort);
if (!socks5Open) {
print("SOCKS5 proxy port $socksPort is not open at 127.0.0.1 (It should be, start the daemon...)");
return false; // If the SOCKS5 proxy port is closed, return false
} else {
print("The socks5 port is open at $socksPort");
return await checkTorViaApiStandardBinary();
}
} else if (Platform.isAndroid) {
socks5Open = await checkSocks5Proxy(testHost: '127.0.0.1', testPort: 9050);
i2pOpen = await checkI2pTunnel();
if (!socks5Open && !i2pOpen) {
return false;
} else {
print("I2P: $i2pOpen TOR: $socks5Open");
if (i2pOpen) {
return true;
}
}
} else if (Platform.isIOS) {
// Skip the SOCKS5 proxy check since we don't know the port, proceed directly to checking Tor connection via .onion
// We can find the port using OrbotKit implementation later
return await checkTorOnionLink();
}
} catch (e) {
print("Error when Tor via both Onion and Clearnet links: $e");
return false;
}
// If the SOCKS5 port is open, proceed with the actual request
try {
return await checkTorViaApiStandardBinary();
} catch (e) {
print("Error during the Tor connection check: $e");
return false;
}
}
// Function to check Tor connection via .onion link on iOS
static Future<bool> checkTorOnionLink() async {
final httpClient = HttpClient();
try {
final request = await httpClient.headUrl(Uri.parse('http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion/robots.txt'));
final response = await request.close();
return response.statusCode == 200;
} catch (e) {
print("Error during .onion connection check: $e");
try {
print('Attempting to check Tor connection via non-onion URL');
final request = await httpClient.headUrl(Uri.parse('https://check.torproject.org/api/ip'));
final response = await request.close();
return response.statusCode == 200;
} catch (e) {
throw Exception('No connection to tor what soever, even through clearnet link');
}
}
}
// Function to check Tor connection via Tor Project API
// Make a proxied HTTP request using IOClient
static Future<bool> checkTorViaApiStandardBinary() async {
// Wrap the HttpClient in an IOClient for modern HTTP API
final ioClient = IOClient(createTorHttpClient());
try {
// Perform the HTTP GET request
final response = await ioClient.get(Uri.parse('https://check.torproject.org/api/ip'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print("Response from Tor website: $data");
return data['IsTor'] == true; // Return true if IsTor is true
} else {
return false; // Return false if the API didn't return 200 OK
}
} catch (e) {
print("Error during Tor Project API check: $e");
return false;
} finally {
ioClient.close(); // Always close the client
}
}
static Future<bool> checkI2pTunnel({String testHost = '127.0.0.1', int? testPort}) async {
try {
final socket = await Socket.connect(testHost, testPort ?? 3201, timeout: const Duration(seconds: 7));
socket.destroy(); // Close the socket as soon as the connection is established
return true; // If connected, the SOCKS5 proxy is available
} catch (e) {
print("Error connecting to an I2P tunnel: $e");
return false; // Connection failed, proxy is not available
}
}
/* static Future<bool> checkTorViaApiWithRustSocks5() async {
final torStatusService = TorStatusService();
if (torStatusService.torPort == null) {
print("Tor port is not available.");
return false;
}
try {
// Step 1: Create and connect to SOCKSSocket with SSL enabled
final socksSocket = await SOCKSSocket.create(
proxyHost: InternetAddress.loopbackIPv4.address,
proxyPort: torStatusService.torPort!,
sslEnabled: true, // Enable SSL for secure communication
);
// Connect to the Tor Project API through the SOCKS5 proxy
await socksSocket.connect();
await socksSocket.connectTo('check.torproject.org', 443); // HTTPS port
// Step 2: Send the HTTPS request manually
const request = 'GET /api/ip HTTP/1.1\r\n'
'Host: check.torproject.org\r\n'
'Connection: close\r\n\r\n';
socksSocket.write(request);
// Step 3: Collect the entire response
StringBuffer responseBuffer = StringBuffer();
await for (var data in socksSocket.responseController.stream) {
responseBuffer.write(utf8.decode(data));
}
// Step 4: Extract and debug the response
final responseString = responseBuffer.toString();
print('Full response from Tor Project API: $responseString');
// Step 5: Split the response into headers and body
final responseParts = responseString.split('\r\n\r\n');
if (responseParts.length < 2) {
print('Invalid response format');
return false;
}
final body = responseParts[1]; // Extract the body (JSON content)
print('Extracted body: $body');
// Step 6: Parse the JSON body and check the IsTor field
final data = jsonDecode(body);
print('Parsed JSON: $data');
// Step 7: Close the SOCKSSocket
if (data['IsTor'] == true) {
socksSocket.close();
print("Tor is 100% connected.");
return true;
} else {
socksSocket.close();
print("IsTor contained ${data['IsTor']} and failed!");
return false;
}
} catch (e) {
print("Error during Tor Project API check: $e");
return false;
}
} */
Future<HSV3OnionConfig?> createHiddenServiceV3KeyPair(String identifier) async {
try {
final algorithm = Ed25519();
final keyPair = await algorithm.newKeyPair();
final privateKeyBytes = await keyPair.extractPrivateKeyBytes();
final publicKey = await keyPair.extractPublicKey();
return HSV3OnionConfig(
privateKeyBytes: privateKeyBytes,
publicKey: publicKey,
internalPort: 3201,
externalPort: 45256);
} catch (e) {
print("Failed to create Hidden Service key pair: $e");
return null;
}
}
Future<bool> publishOnionViaControlPort(HSV3OnionConfig onionConfig) async {
try {
final socket = await Socket.connect(_torDaemonConfig.host, _torDaemonConfig.controlPort);
final controlPortStream = socket.transform(utf8.decoder as StreamTransformer<Uint8List, dynamic>).transform(const LineSplitter());
// Authenticate with the control port
final authenticateCommand = 'AUTHENTICATE "${_torDaemonConfig.hashedPassword}"\r\n';
socket.write(authenticateCommand);
await for (var line in controlPortStream) {
if (line.startsWith('250')) {
print('Authentication successful');
// Add the hidden service using the private key
final addOnionCommand = 'ADD_ONION ${onionConfig.privateKey} Port=${onionConfig.externalPort},127.0.0.1:${onionConfig.internalPort}\r\n';
socket.write(addOnionCommand);
await for (var response in controlPortStream) {
if (response.startsWith('250')) {
print('Hidden service created successfully');
socket.destroy();
return true;
} else if (response.startsWith('551')) {
print('Error creating hidden service: $response');
socket.destroy();
return false;
}
}
} else if (line.startsWith('515')) {
print('Authentication failed: $line');
socket.destroy();
return false;
}
}
} catch (e) {
print('Error: $e');
return false;
}
return false;
}
static Future<bool> checkControlPort({String? password}) async {
try {
final socket = await Socket.connect('127.0.0.1', 9051, timeout: Duration(seconds: 5));
if (password != null) {
socket.write('AUTHENTICATE "$password"\r\n');
await socket.flush();
final response = await socket.transform(utf8.decoder as StreamTransformer<Uint8List, dynamic>).join();
socket.destroy();
return response.contains('250 OK');
} else {
socket.destroy();
return true;
}
} catch (e) {
return false;
}
}
static Future<bool> checkSocks5Proxy({String testHost = '127.0.0.1', int? testPort}) async {
try {
final socket = await Socket.connect(testHost, testPort ?? 9050, timeout: const Duration(seconds: 5));
socket.destroy(); // Close the socket as soon as the connection is established
return true; // If connected, the SOCKS5 proxy is available
} catch (e) {
print("Error connecting to SOCKS5 proxy: $e");
return false; // Connection failed, proxy is not available
}
}
static HttpClient createTorHttpClient() {
final httpClient = HttpClient();
String proxy;
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
proxy = "PROXY 127.0.0.1:8888"; // Set your proxy for desktop platforms
} else if (Platform.isAndroid) {
proxy = 'PROXY 127.0.0.1:8118'; // Orbot's default HTTP proxy for Android
} else if (Platform.isIOS) {
proxy = ''; // iOS-specific handling can be implemented here
} else {
proxy = 'PROXY 127.0.0.1:8118'; // Fallback
}
if (proxy.isNotEmpty) {
httpClient.findProxy = (uri) {
return proxy; // This sets the proxy for HttpClient
};
}
return httpClient;
}
}