Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class RecoverBullRepository {
String salt,
String vaultKey,
) async {
final externalProxy = await torConfigPort.getExternalTorConfig();
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.store(
HEX.decode(identifier),
utf8.encode(password),
Expand All @@ -65,7 +65,7 @@ class RecoverBullRepository {
String password,
String salt,
) async {
final externalProxy = await torConfigPort.getExternalTorConfig();
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
final vaultKey = await remoteDatasource.fetch(
HEX.decode(identifier),
utf8.encode(password),
Expand All @@ -80,7 +80,7 @@ class RecoverBullRepository {
String password,
String salt,
) async {
final externalProxy = await torConfigPort.getExternalTorConfig();
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.trash(
HEX.decode(identifier),
utf8.encode(password),
Expand All @@ -90,7 +90,7 @@ class RecoverBullRepository {
}

Future<void> checkConnection() async {
final externalProxy = await torConfigPort.getExternalTorConfig();
final externalProxy = await torConfigPort.getAvailableExternalTorConfig();
await remoteDatasource.checkConnection(externalProxy: externalProxy);
}

Expand Down
6 changes: 5 additions & 1 deletion lib/core/recoverbull/recoverbull_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.
import 'package:bb_mobile/core/storage/sqlite_database.dart';
import 'package:bb_mobile/core/tor/data/datasources/tor_datasource.dart';
import 'package:bb_mobile/core/tor/domain/ports/tor_config_port.dart';
import 'package:bb_mobile/core/tor/infrastructure/services/tor_connectivity_service.dart';
import 'package:bb_mobile/core/tor/interface_adapters/adapters/tor_config_adapter.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:bb_mobile/core/wallet/domain/usecases/create_default_wallets_usecase.dart';
Expand Down Expand Up @@ -69,7 +70,10 @@ class RecoverbullLocator {
);

locator.registerLazySingleton<TorConfigPort>(
() => TorConfigAdapter(settingsRepository: locator<SettingsRepository>()),
() => TorConfigAdapter(
settingsRepository: locator<SettingsRepository>(),
torConnectivityService: locator<TorConnectivityService>(),
),
);

locator.registerSingletonWithDependencies<RecoverBullRepository>(
Expand Down
2 changes: 1 addition & 1 deletion lib/core/tor/domain/ports/tor_config_port.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ abstract class TorConfigPort {
/// Get the Tor proxy configuration
/// Returns null if internal Tor should be used
/// Returns TorProxyConfig if external Tor proxy is configured
Future<TorProxyConfig?> getExternalTorConfig();
Future<TorProxyConfig?> getAvailableExternalTorConfig();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:io';

import 'package:bb_mobile/features/tor_settings/domain/ports/socket_port.dart';
import 'package:bb_mobile/core/tor/domain/ports/socket_port.dart';

class SocketAdapter implements SocketPort {
@override
Expand Down
103 changes: 103 additions & 0 deletions lib/core/tor/infrastructure/services/tor_connectivity_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:io';

import 'package:bb_mobile/core/tor/domain/ports/socket_port.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:bb_mobile/core/utils/logger.dart';

class TorConnectivityService {
final SocketPort _socketPort;

// cache to avoid repeated connection checks
TorStatus? _cachedStatus;
int? _cachedPort;
DateTime? _cacheTime;
static const _cacheDuration = Duration(seconds: 30);

TorConnectivityService({required SocketPort socketPort})
: _socketPort = socketPort;

/// Checks if a Tor SOCKS5 proxy is running and accessible on the given port
///
/// This performs a SOCKS5 handshake to verify:
/// 1. The port is open and listening
/// 2. The service responds as a valid SOCKS5 proxy
/// 3. The app has permission to connect
///
/// Returns the appropriate [TorStatus] based on the connection check
Future<TorStatus> checkConnection(int port) async {
if (_cachedStatus != null &&
_cachedPort == port &&
_cacheTime != null &&
DateTime.now().difference(_cacheTime!) < _cacheDuration) {
log.config('Using cached Tor proxy status for port $port');
return _cachedStatus!;
}

final status = await _performCheck(port);

_cachedStatus = status;
_cachedPort = port;
_cacheTime = DateTime.now();

return status;
}

Future<TorStatus> _performCheck(int port) async {
try {
// Test SOCKS5 proxy functionality by attempting SOCKS5 handshake
// This verifies both that the port is open AND that our app can use it
final socket = await _socketPort.connect(
'127.0.0.1',
port,
// TODO: maybe we should give some more time for this?
timeout: const Duration(seconds: 3),
);

try {
// Send SOCKS5 handshake: version 5, 1 auth method (no auth)
socket.add([0x05, 0x01, 0x00]);

// Wait for SOCKS5 response with timeout
final response = await socket.first.timeout(
const Duration(seconds: 3),
onTimeout: () => throw TimeoutException('SOCKS5 handshake timeout'),
);

// Valid SOCKS5 response should be [0x05, 0x00] (version 5, no auth)
if (response.length >= 2 && response[0] == 0x05) {
// SOCKS5 proxy responded correctly - connection is working
await socket.close();
log.config(
'Tor SOCKS5 proxy is online and accessible at 127.0.0.1:$port',
);
return TorStatus.online;
} else {
// Unexpected response - port is open but not a valid SOCKS5 proxy
await socket.close();
log.warning('Port $port is open but not responding as SOCKS5 proxy');
return TorStatus.unknown;
}
} catch (e) {
// SOCKS5 handshake failed - likely app doesn't have permission
await socket.close();
log.warning(
'SOCKS5 handshake failed (app may not have permission): $e',
);
return TorStatus.offline;
}
} on SocketException catch (e) {
// Connection failed - Tor proxy is not running or not reachable
log.warning('Tor proxy connection failed: $e');
return TorStatus.offline;
} on TimeoutException catch (e) {
// Connection timeout - Tor proxy is not responding
log.warning('Tor proxy connection timeout: $e');
return TorStatus.offline;
} catch (e) {
// Other errors - mark as unknown
log.severe('Unexpected error checking Tor status: $e');
return TorStatus.unknown;
}
}
}
18 changes: 14 additions & 4 deletions lib/core/tor/interface_adapters/adapters/tor_config_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import 'package:bb_mobile/core/settings/domain/repositories/settings_repository.dart';
import 'package:bb_mobile/core/tor/domain/ports/tor_config_port.dart';
import 'package:bb_mobile/core/tor/domain/value_objects/tor_proxy_config.dart';
import 'package:bb_mobile/core/tor/infrastructure/services/tor_connectivity_service.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';

class TorConfigAdapter implements TorConfigPort {
final SettingsRepository _settingsRepository;
final TorConnectivityService _torConnectivityService;

TorConfigAdapter({required SettingsRepository settingsRepository})
: _settingsRepository = settingsRepository;
TorConfigAdapter({
required SettingsRepository settingsRepository,
required TorConnectivityService torConnectivityService,
}) : _settingsRepository = settingsRepository,
_torConnectivityService = torConnectivityService;

@override
Future<TorProxyConfig?> getExternalTorConfig() async {
Future<TorProxyConfig?> getAvailableExternalTorConfig() async {
final settings = await _settingsRepository.fetch();

if (settings.useTorProxy) {
final status = await _torConnectivityService.checkConnection(
settings.torProxyPort,
);

if (status == TorStatus.online) {
return TorProxyConfig(port: settings.torProxyPort);
}

Expand Down
8 changes: 8 additions & 0 deletions lib/core/tor/tor_locator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import 'package:bb_mobile/core/tor/data/repository/tor_repository.dart';
import 'package:bb_mobile/core/tor/data/usecases/init_tor_usecase.dart';
import 'package:bb_mobile/core/tor/data/usecases/is_tor_required_usecase.dart';
import 'package:bb_mobile/core/tor/data/usecases/tor_status_usecase.dart';
import 'package:bb_mobile/core/tor/domain/ports/socket_port.dart';
import 'package:bb_mobile/core/tor/infrastructure/adapters/socket_adapter.dart';
import 'package:bb_mobile/core/tor/infrastructure/services/tor_connectivity_service.dart';
import 'package:bb_mobile/core/wallet/data/repositories/wallet_repository.dart';
import 'package:get_it/get_it.dart';

Expand All @@ -15,6 +18,11 @@ class TorLocator {
});
}
await locator.isReady<TorDatasource>();

locator.registerLazySingleton<SocketPort>(() => SocketAdapter());
locator.registerLazySingleton<TorConnectivityService>(
() => TorConnectivityService(socketPort: locator<SocketPort>()),
);
}

static Future<void> registerRepositories(GetIt locator) async {
Expand Down
7 changes: 4 additions & 3 deletions lib/features/recoverbull/presentation/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class RecoverBullBloc extends Bloc<RecoverBullEvent, RecoverBullState> {
Emitter<RecoverBullState> emit,
) async {
try {
final externalTorConfig = await _torConfigPort.getExternalTorConfig();
final externalTorConfig = await _torConfigPort
.getAvailableExternalTorConfig();

if (externalTorConfig == null) {
await _initializeTorUsecase.execute();
Expand Down Expand Up @@ -253,8 +254,8 @@ class RecoverBullBloc extends Bloc<RecoverBullEvent, RecoverBullState> {
switch (event.provider) {
case VaultProvider.googleDrive:
await _connectToGoogleDriveUsecase.execute();
final encryptedVault =
await _fetchLatestGoogleDriveVaultUsecase.execute();
final encryptedVault = await _fetchLatestGoogleDriveVaultUsecase
.execute();
emit(state.copyWith(vault: encryptedVault));
return;
case VaultProvider.customLocation:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,79 +1,14 @@
import 'dart:async';
import 'dart:io';

import 'package:bb_mobile/core/tor/infrastructure/services/tor_connectivity_service.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:bb_mobile/core/utils/logger.dart';
import 'package:bb_mobile/features/tor_settings/domain/ports/socket_port.dart';

class CheckTorProxyConnectionUsecase {
final SocketPort _socketPort;
final TorConnectivityService _torConnectivityService;

const CheckTorProxyConnectionUsecase({required SocketPort socketPort})
: _socketPort = socketPort;
CheckTorProxyConnectionUsecase({
required TorConnectivityService torConnectivityService,
}) : _torConnectivityService = torConnectivityService;

/// Checks if a Tor SOCKS5 proxy is running and accessible on the given port
///
/// This performs a SOCKS5 handshake to verify:
/// 1. The port is open and listening
/// 2. The service responds as a valid SOCKS5 proxy
/// 3. The app has permission to connect
///
/// Returns the appropriate [TorStatus] based on the connection check
Future<TorStatus> execute(int port) async {
try {
// Test SOCKS5 proxy functionality by attempting SOCKS5 handshake
// This verifies both that the port is open AND that our app can use it
final socket = await _socketPort.connect(
'127.0.0.1',
port,
// TODO: maybe we should give some more time for this?
timeout: const Duration(seconds: 3),
);

try {
// Send SOCKS5 handshake: version 5, 1 auth method (no auth)
socket.add([0x05, 0x01, 0x00]);

// Wait for SOCKS5 response with timeout
final response = await socket.first.timeout(
const Duration(seconds: 3),
onTimeout: () => throw TimeoutException('SOCKS5 handshake timeout'),
);

// Valid SOCKS5 response should be [0x05, 0x00] (version 5, no auth)
if (response.length >= 2 && response[0] == 0x05) {
// SOCKS5 proxy responded correctly - connection is working
await socket.close();
log.config(
'Tor SOCKS5 proxy is online and accessible at 127.0.0.1:$port',
);
return TorStatus.online;
} else {
// Unexpected response - port is open but not a valid SOCKS5 proxy
await socket.close();
log.warning('Port $port is open but not responding as SOCKS5 proxy');
return TorStatus.unknown;
}
} catch (e) {
// SOCKS5 handshake failed - likely app doesn't have permission
await socket.close();
log.warning(
'SOCKS5 handshake failed (app may not have permission): $e',
);
return TorStatus.offline;
}
} on SocketException catch (e) {
// Connection failed - Tor proxy is not running or not reachable
log.warning('Tor proxy connection failed: $e');
return TorStatus.offline;
} on TimeoutException catch (e) {
// Connection timeout - Tor proxy is not responding
log.warning('Tor proxy connection timeout: $e');
return TorStatus.offline;
} catch (e) {
// Other errors - mark as unknown
log.severe('Unexpected error checking Tor status: $e');
return TorStatus.unknown;
}
return _torConnectivityService.checkConnection(port);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'dart:async';

import 'package:bb_mobile/core/settings/domain/get_settings_usecase.dart';
import 'package:bb_mobile/core/settings/domain/update_tor_settings_usecase.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:bb_mobile/features/tor_settings/domain/usecases/check_tor_proxy_connection_usecase.dart';
import 'package:bb_mobile/core/tor/tor_status.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

Expand Down
14 changes: 6 additions & 8 deletions lib/features/tor_settings/tor_settings_locator.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import 'package:bb_mobile/core/settings/domain/get_settings_usecase.dart';
import 'package:bb_mobile/core/settings/domain/update_tor_settings_usecase.dart';
import 'package:bb_mobile/features/tor_settings/domain/ports/socket_port.dart';
import 'package:bb_mobile/core/tor/infrastructure/services/tor_connectivity_service.dart';
import 'package:bb_mobile/features/tor_settings/domain/usecases/check_tor_proxy_connection_usecase.dart';
import 'package:bb_mobile/features/tor_settings/infrastructure/adapters/socket_adapter.dart';
import 'package:bb_mobile/features/tor_settings/presentation/bloc/tor_settings_cubit.dart';
import 'package:get_it/get_it.dart';

class TorSettingsLocator {
static void setup(GetIt locator) {
// Infrastructure
locator.registerLazySingleton<SocketPort>(() => SocketAdapter());

// Use cases
locator.registerLazySingleton<CheckTorProxyConnectionUsecase>(
() => CheckTorProxyConnectionUsecase(socketPort: locator<SocketPort>()),
// Usecases
locator.registerFactory<CheckTorProxyConnectionUsecase>(
() => CheckTorProxyConnectionUsecase(
torConnectivityService: locator<TorConnectivityService>(),
),
);

// Presentation
Expand Down