diff --git a/app/assets/preferences/builtin-bridges.json b/app/assets/preferences/builtin-bridges.json new file mode 100644 index 00000000..58661f35 --- /dev/null +++ b/app/assets/preferences/builtin-bridges.json @@ -0,0 +1 @@ +{"meek":["meek_lite 192.0.2.20:80 url=https://1603026938.rsc.cdn77.org front=www.phpmyadmin.net utls=HelloRandomizedALPN"],"meek-azure":["meek_lite 192.0.2.20:80 url=https://1603026938.rsc.cdn77.org front=www.phpmyadmin.net utls=HelloRandomizedALPN"],"obfs4":["obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0","obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0","obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0","obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1","obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0","obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0","obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0","obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0","obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0","obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0","obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0"],"snowflake":["snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn","snowflake 192.0.2.4:80 8838024498816A039FCBBAB14E6F40A0843051FA fingerprint=8838024498816A039FCBBAB14E6F40A0843051FA url=https://1098762253.rsc.cdn77.org/ fronts=www.cdn77.com,www.phpmyadmin.net ice=stun:stun.antisip.com:3478,stun:stun.epygi.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.mixvoip.com:3478,stun:stun.nextcloud.com:3478,stun:stun.bethesda.net:3478,stun:stun.nextcloud.com:443 utls-imitate=hellorandomizedalpn"]} diff --git a/app/lib/features/settings/presentation/screens/developer_settings.dart b/app/lib/features/settings/presentation/screens/developer_settings.dart index 7b70f55f..e437850a 100644 --- a/app/lib/features/settings/presentation/screens/developer_settings.dart +++ b/app/lib/features/settings/presentation/screens/developer_settings.dart @@ -17,7 +17,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'dart:developer'; + import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -163,6 +166,38 @@ class DeveloperSettingsScreen extends HookConsumerWidget { label: const Text('Copy'), ), ), + if (kDebugMode) + CustomListTile( + title: 'Dart VM', + subtitle: 'Copy Dart VM service URL', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.bug_report, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + final serviceProtocolInfo = await Service.getInfo(); + + await Clipboard.setData( + ClipboardData( + text: + serviceProtocolInfo.serverUri?.toString() ?? + 'Error', + ), + ); + + if (context.mounted) { + showInfoMessage(context, 'Service URL copied'); + } + }, + icon: const Icon(Icons.copy), + label: const Text('Copy'), + ), + ), ], ); }, diff --git a/app/lib/features/tor/data/services/builtin_bridges.dart b/app/lib/features/tor/data/services/builtin_bridges.dart new file mode 100644 index 00000000..0cc1d15b --- /dev/null +++ b/app/lib/features/tor/data/services/builtin_bridges.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart' show rootBundle; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:universal_io/io.dart'; + +part 'builtin_bridges.g.dart'; + +@Riverpod(keepAlive: true) +class BuiltinBridgesService extends _$BuiltinBridgesService { + late final _bridgeFileFuture = path_provider + .getApplicationSupportDirectory() + .then((dir) => File(p.join(dir.path, 'builtin-bridges.json'))); + + Future lastUpdate() async { + final bridgeFile = await _bridgeFileFuture; + if (!await bridgeFile.exists()) { + return null; + } + + return bridgeFile.lastModified(); + } + + Future updateStoredBuiltinBridges(BuiltInBridges bridges) async { + final bridgeFile = await _bridgeFileFuture; + await bridgeFile.writeAsString(jsonEncode(bridges.toJson()), flush: true); + } + + Future getStoredBuiltinBridges() async { + final bridgeFile = await _bridgeFileFuture; + if (!await bridgeFile.exists()) { + return null; + } + + final content = await bridgeFile.readAsString(); + return BuiltInBridges.fromJson(jsonDecode(content) as Map); + } + + Future getBundledBuiltinBridges() async { + final content = await rootBundle.loadString( + 'assets/preferences/builtin-bridges.json', + ); + return BuiltInBridges.fromJson(jsonDecode(content) as Map); + } + + @override + void build() { + return; + } +} diff --git a/app/lib/features/tor/data/services/builtin_bridges.g.dart b/app/lib/features/tor/data/services/builtin_bridges.g.dart new file mode 100644 index 00000000..40782e6b --- /dev/null +++ b/app/lib/features/tor/data/services/builtin_bridges.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'builtin_bridges.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$builtinBridgesServiceHash() => + r'beacb3c9d8c5a7179c7c9cad8024d128b09241f5'; + +/// See also [BuiltinBridgesService]. +@ProviderFor(BuiltinBridgesService) +final builtinBridgesServiceProvider = + NotifierProvider.internal( + BuiltinBridgesService.new, + name: r'builtinBridgesServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$builtinBridgesServiceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$BuiltinBridgesService = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/tor/domain/repositories/builtin_bridges.dart b/app/lib/features/tor/domain/repositories/builtin_bridges.dart new file mode 100644 index 00000000..349e195a --- /dev/null +++ b/app/lib/features/tor/domain/repositories/builtin_bridges.dart @@ -0,0 +1,49 @@ +import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/tor/data/services/builtin_bridges.dart'; + +part 'builtin_bridges.g.dart'; + +@Riverpod() +class BuiltinBridgesRepository extends _$BuiltinBridgesRepository { + Future updateIfNecessary() async { + final lastUpdate = await ref + .read(builtinBridgesServiceProvider.notifier) + .lastUpdate(); + + if (lastUpdate == null || + DateTime.now().difference(lastUpdate) > const Duration(days: 2)) { + BuiltInBridges? remoteBridges; + try { + remoteBridges = await service.getBuiltinBridges(); + } catch (e, s) { + logger.e('Failed fetching builtin bridges', error: e, stackTrace: s); + } + + if (remoteBridges != null) { + await ref + .read(builtinBridgesServiceProvider.notifier) + .updateStoredBuiltinBridges(remoteBridges); + } + } + } + + Future getBridges({bool tryUpdate = true}) async { + if (tryUpdate) { + await updateIfNecessary(); + } + + final storedBridges = await ref + .read(builtinBridgesServiceProvider.notifier) + .getStoredBuiltinBridges(); + + return storedBridges ?? + await ref + .read(builtinBridgesServiceProvider.notifier) + .getBundledBuiltinBridges(); + } + + @override + void build(MoatService service) {} +} diff --git a/app/lib/features/tor/domain/repositories/builtin_bridges.g.dart b/app/lib/features/tor/domain/repositories/builtin_bridges.g.dart new file mode 100644 index 00000000..1ee4a8c7 --- /dev/null +++ b/app/lib/features/tor/domain/repositories/builtin_bridges.g.dart @@ -0,0 +1,166 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'builtin_bridges.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$builtinBridgesRepositoryHash() => + r'05fc40a7742fc266aeb76b4e3f1d9bdfdb8d448f'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$BuiltinBridgesRepository + extends BuildlessAutoDisposeNotifier { + late final MoatService service; + + void build(MoatService service); +} + +/// See also [BuiltinBridgesRepository]. +@ProviderFor(BuiltinBridgesRepository) +const builtinBridgesRepositoryProvider = BuiltinBridgesRepositoryFamily(); + +/// See also [BuiltinBridgesRepository]. +class BuiltinBridgesRepositoryFamily extends Family { + /// See also [BuiltinBridgesRepository]. + const BuiltinBridgesRepositoryFamily(); + + /// See also [BuiltinBridgesRepository]. + BuiltinBridgesRepositoryProvider call(MoatService service) { + return BuiltinBridgesRepositoryProvider(service); + } + + @override + BuiltinBridgesRepositoryProvider getProviderOverride( + covariant BuiltinBridgesRepositoryProvider provider, + ) { + return call(provider.service); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'builtinBridgesRepositoryProvider'; +} + +/// See also [BuiltinBridgesRepository]. +class BuiltinBridgesRepositoryProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [BuiltinBridgesRepository]. + BuiltinBridgesRepositoryProvider(MoatService service) + : this._internal( + () => BuiltinBridgesRepository()..service = service, + from: builtinBridgesRepositoryProvider, + name: r'builtinBridgesRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$builtinBridgesRepositoryHash, + dependencies: BuiltinBridgesRepositoryFamily._dependencies, + allTransitiveDependencies: + BuiltinBridgesRepositoryFamily._allTransitiveDependencies, + service: service, + ); + + BuiltinBridgesRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.service, + }) : super.internal(); + + final MoatService service; + + @override + void runNotifierBuild(covariant BuiltinBridgesRepository notifier) { + return notifier.build(service); + } + + @override + Override overrideWith(BuiltinBridgesRepository Function() create) { + return ProviderOverride( + origin: this, + override: BuiltinBridgesRepositoryProvider._internal( + () => create()..service = service, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + service: service, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _BuiltinBridgesRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BuiltinBridgesRepositoryProvider && + other.service == service; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, service.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin BuiltinBridgesRepositoryRef on AutoDisposeNotifierProviderRef { + /// The parameter `service` of this provider. + MoatService get service; +} + +class _BuiltinBridgesRepositoryProviderElement + extends AutoDisposeNotifierProviderElement + with BuiltinBridgesRepositoryRef { + _BuiltinBridgesRepositoryProviderElement(super.provider); + + @override + MoatService get service => + (origin as BuiltinBridgesRepositoryProvider).service; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/tor/domain/services/tor_proxy.dart b/app/lib/features/tor/domain/services/tor_proxy.dart index ff0e06d5..d5543b15 100644 --- a/app/lib/features/tor/domain/services/tor_proxy.dart +++ b/app/lib/features/tor/domain/services/tor_proxy.dart @@ -20,13 +20,17 @@ import 'dart:async'; import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:nullability/nullability.dart'; import 'package:pluggable_transports_proxy/pluggable_transports_proxy.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers/lifecycle.dart'; +import 'package:weblibre/features/tor/domain/repositories/builtin_bridges.dart'; import 'package:weblibre/features/tor/utils/tor_entrypoint.dart'; +import 'package:weblibre/features/user/data/models/tor_settings.dart'; import 'package:weblibre/features/user/domain/repositories/tor_settings.dart'; part 'tor_proxy.g.dart'; @@ -38,9 +42,19 @@ class _TorService { ValueStream get portStream => _portSubject.stream; - Future start({ProxyType? proxyType, String? bridgeLines}) async { + Future startOrReconfigure({ + ProxyType? proxyType, + String? bridgeLines, + }) async { await service.startService(); - service.invoke('start', { + service.invoke('startOrReconfigure', { + if (proxyType != null) 'proxyType': proxyType.name, + if (bridgeLines != null) 'bridgeLines': bridgeLines, + }); + } + + Future reconfigure({ProxyType? proxyType, String? bridgeLines}) async { + service.invoke('reconfigure', { if (proxyType != null) 'proxyType': proxyType.name, if (bridgeLines != null) 'bridgeLines': bridgeLines, }); @@ -101,7 +115,7 @@ class TorProxyService extends _$TorProxyService { }); } - Future connect({bool forceReconnect = false}) async { + Future startOrReconfigure({bool forceReconnect = false}) async { final currentPort = await requestSync(); if (currentPort == null || forceReconnect) { @@ -111,14 +125,97 @@ class TorProxyService extends _$TorProxyService { .read(torSettingsRepositoryProvider.notifier) .fetchSettings(); - if (torSettings.autoConfig) { - final moat = MoatService(); - final config = await moat.autoConf(); + Setting? setting; + if (torSettings.config == TorConnectionConfig.auto) { + List? config; - todo + final moat = MoatService(); + try { + await moat.initialize(); + config = await moat.autoConf( + cannotConnectWithoutPt: torSettings.requireBridge, + ); + + if (config == null && torSettings.requireBridge) { + config = MoatService.convertBuiltinToSettings( + await ref + .read(builtinBridgesRepositoryProvider(moat).notifier) + .getBridges(), + ); + } + } catch (e, s) { + logger.e('Failed auto configure bridges', error: e, stackTrace: s); + } finally { + await moat.dispose(); + } + + setting = config.mapNotNull( + (config) => + config.firstWhereOrNull( + (setting) => setting.bridge.type == TransportType.obfs4, + ) ?? + config.firstWhereOrNull( + (setting) => setting.bridge.type == TransportType.snowflake, + ), + ); + } else if (torSettings.config != TorConnectionConfig.direct) { + List? config; + + final moat = MoatService(); + try { + await moat.initialize(); + if (torSettings.fetchRemoteBridges) { + config = await moat.getDefaultBridges(); + } + + if (config == null && torSettings.requireBridge) { + config = MoatService.convertBuiltinToSettings( + await ref + .read(builtinBridgesRepositoryProvider(moat).notifier) + .getBridges(tryUpdate: torSettings.fetchRemoteBridges), + ); + } + + setting = config.mapNotNull( + (config) => config.firstWhereOrNull( + (setting) => + setting.bridge.type == + switch (torSettings.config) { + TorConnectionConfig.auto => throw UnimplementedError(), + TorConnectionConfig.direct => throw UnimplementedError(), + TorConnectionConfig.obfs4 => TransportType.obfs4, + TorConnectionConfig.snowflake => TransportType.snowflake, + }, + ), + ); + } catch (e, s) { + logger.e('Failed auto configure bridges', error: e, stackTrace: s); + } finally { + await moat.dispose(); + } + } + + if (setting != null) { + switch (setting.bridge.type) { + case TransportType.obfs4: + await _tor.startOrReconfigure( + proxyType: ProxyType.obfs4, + bridgeLines: setting.bridge.bridges?.join('\n'), + ); + case TransportType.snowflake: + await _tor.startOrReconfigure( + proxyType: ProxyType.snowflake, + bridgeLines: setting.bridge.bridges?.join('\n'), + ); + case TransportType.meek: + case TransportType.meekAzure: + case TransportType.webtunnel: + throw UnimplementedError(); + } + } else { + await _tor.startOrReconfigure(); } - await _tor.start(); _enableHeartbeatTimer(); } } diff --git a/app/lib/features/tor/domain/services/tor_proxy.g.dart b/app/lib/features/tor/domain/services/tor_proxy.g.dart new file mode 100644 index 00000000..cd139a39 --- /dev/null +++ b/app/lib/features/tor/domain/services/tor_proxy.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tor_proxy.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$torProxyServiceHash() => r'82b4964ddf14d02893ebbea3b25d91216359ba10'; + +/// See also [TorProxyService]. +@ProviderFor(TorProxyService) +final torProxyServiceProvider = + AsyncNotifierProvider.internal( + TorProxyService.new, + name: r'torProxyServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$torProxyServiceHash, + dependencies: null, + allTransitiveDependencies: null, + ); + +typedef _$TorProxyService = AsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart b/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart index 6eeb641d..fbcbb6f3 100644 --- a/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart +++ b/app/lib/features/tor/presentation/controllers/start_tor_proxy.dart @@ -47,7 +47,7 @@ class StartProxyController extends _$StartProxyController { if (result == true) { final connection = ref .read(torProxyServiceProvider.notifier) - .connect(); + .startOrReconfigure(); ref .read(overlayControllerProvider.notifier) @@ -59,6 +59,9 @@ class StartProxyController extends _$StartProxyController { await connection; } } + } else { + //Reconfigure + await ref.read(torProxyServiceProvider.notifier).startOrReconfigure(); } } diff --git a/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart b/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart index dc377783..1f9caad7 100644 --- a/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart +++ b/app/lib/features/tor/presentation/controllers/start_tor_proxy.g.dart @@ -7,7 +7,7 @@ part of 'start_tor_proxy.dart'; // ************************************************************************** String _$startProxyControllerHash() => - r'3e8b409f069f32b601b1c60acbe0a72694fd1cc2'; + r'8840d5ffe9346c3f8b0aa0b3e034605b606c5e34'; /// See also [StartProxyController]. @ProviderFor(StartProxyController) diff --git a/app/lib/features/tor/presentation/screens/tor_proxy.dart b/app/lib/features/tor/presentation/screens/tor_proxy.dart index 23164da4..faaabd1c 100644 --- a/app/lib/features/tor/presentation/screens/tor_proxy.dart +++ b/app/lib/features/tor/presentation/screens/tor_proxy.dart @@ -39,6 +39,14 @@ class TorProxyScreen extends HookConsumerWidget { await ref.read(torProxyServiceProvider.notifier).requestSync(); }); + ref.listen(torSettingsRepositoryProvider, (previous, next) async { + final torService = ref.read(torProxyServiceProvider.notifier); + + if (await torService.requestSync() != null) { + await torService.startOrReconfigure(); + } + }); + return Scaffold( appBar: AppBar(title: const Text('Torâ„¢ Proxy')), body: SafeArea( @@ -46,9 +54,8 @@ class TorProxyScreen extends HookConsumerWidget { color: const Color(0xFF7D4698), child: Column( children: [ - Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, + Expanded( + child: ListView( children: [ const Icon(TorIcons.onionAlt, size: 84), const SizedBox(height: 16), @@ -92,7 +99,7 @@ class TorProxyScreen extends HookConsumerWidget { if (value) { await ref .read(torProxyServiceProvider.notifier) - .connect(); + .startOrReconfigure(); } else { await ref .read(torProxyServiceProvider.notifier) @@ -137,7 +144,7 @@ class TorProxyScreen extends HookConsumerWidget { subtitle: const Text( 'When enabled, all Private Tabs will be tunneled through Tor', ), - secondary: const Icon(MdiIcons.arrowDecision), + secondary: const Icon(MdiIcons.incognito), onChanged: (value) async { await ref .read(saveTorSettingsControllerProvider.notifier) @@ -179,27 +186,173 @@ class TorProxyScreen extends HookConsumerWidget { } return null; // Use the default color. }), - value: torSettings.autoConfig, - title: const Text('Configure Transport Automatically'), + value: torSettings.config == TorConnectionConfig.auto, + title: const Text('Auto Configure Transport'), subtitle: const Text( - 'From some locations, it is necessary to use a pluggable transport to connect to the Tor network that is otherwise blocked. Turn off automatic configuration to manually configure how Tor connects.', + 'From some locations, it is necessary to use a pluggable transport to connect to Tor', ), - secondary: const Icon(MdiIcons.arrowDecision), - onChanged: (value) async { - await ref - .read(saveTorSettingsControllerProvider.notifier) - .save( - (currentSettings) => - currentSettings.copyWith.autoConfig(value), - ); - }, + secondary: const Icon(MdiIcons.arrowDecisionAuto), + onChanged: torProxyPort.isLoading + ? null + : (value) async { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.config( + value + ? TorConnectionConfig.auto + : TorConnectionConfig.direct, + ), + ); + }, ), ), + if (torSettings.config == TorConnectionConfig.auto) ...[ + SwitchListTile.adaptive( + value: torSettings.requireBridge, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + onChanged: torProxyPort.isLoading + ? null + : (value) async { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings + .copyWith + .requireBridge(value), + ); + }, + title: const Text( + "I'm sure I cannot connect without a bridge", + ), + ), + ] else ...[ + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.config(value), + ); + } + }, + child: RadioListTile.adaptive( + value: TorConnectionConfig.direct, + enabled: !torProxyPort.isLoading, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + title: Text('Direct Connection'), + subtitle: Text( + 'The best way to connect to Tor if Tor is not blocked', + ), + ), + ), + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.config(value), + ); + } + }, + child: RadioListTile.adaptive( + value: TorConnectionConfig.obfs4, + enabled: !torProxyPort.isLoading, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + title: Text('obfs4'), + subtitle: Text( + 'Suitable for light censorship and high bandwidth needs', + ), + ), + ), + RadioGroup( + groupValue: torSettings.config, + onChanged: (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => + currentSettings.copyWith.config(value), + ); + } + }, + child: RadioListTile.adaptive( + value: TorConnectionConfig.snowflake, + enabled: !torProxyPort.isLoading, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + title: Text('Snowflake'), + subtitle: Text('Suitable for heavy censorship'), + ), + ), + CheckboxListTile.adaptive( + value: torSettings.fetchRemoteBridges, + controlAffinity: ListTileControlAffinity.leading, + enabled: + torSettings.config != TorConnectionConfig.direct, + contentPadding: const EdgeInsets.only( + left: 56, + right: 24, + ), + onChanged: torProxyPort.isLoading + ? null + : (value) async { + if (value != null) { + await ref + .read( + saveTorSettingsControllerProvider + .notifier, + ) + .save( + (currentSettings) => currentSettings + .copyWith + .fetchRemoteBridges(value), + ); + } + }, + title: const Text( + "Fetch fresh Bridges before connecting", + ), + ), + ], ], ), ), - Flexible( + Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), child: Column( + mainAxisSize: MainAxisSize.min, children: [ if (torProxyPort.isLoading) const Column( diff --git a/app/lib/features/tor/utils/tor_entrypoint.dart b/app/lib/features/tor/utils/tor_entrypoint.dart index d22d282c..b9b019f2 100644 --- a/app/lib/features/tor/utils/tor_entrypoint.dart +++ b/app/lib/features/tor/utils/tor_entrypoint.dart @@ -35,16 +35,19 @@ Future onStart(ServiceInstance service) async { service.invoke('portUpdate', {'port': port}); }); + Future stopProxies() async { + for (final proxyType in startedProxyType) { + await IPtProxyController().stop(proxyType); + } + startedProxyType.clear(); + } + Future stopService() async { timeout?.cancel(); timeout = null; Tor.instance.stop(); - - for (final proxyType in startedProxyType) { - await IPtProxyController().stop(proxyType); - } - startedProxyType.clear(); + await stopProxies(); service.invoke('portUpdate', {'port': -1}); @@ -84,12 +87,57 @@ Future onStart(ServiceInstance service) async { } } - service.on("start").listen((event) async { + Future reconfigureService( + ProxyType? proxyType, + String? bridgeLines, + ) async { + if (Tor.instance.hasClient) { + await stopProxies(); + + if (proxyType != null) { + final port = await IPtProxyController().start(proxyType, ""); + startedProxyType.add(proxyType); + + switch (proxyType) { + case ProxyType.obfs4: + await Tor.instance.reconfigure( + obfs4Port: port, + bridgeLines: bridgeLines, + ); + case ProxyType.meekLite: + throw UnimplementedError(); + case ProxyType.webtunnel: + throw UnimplementedError(); + case ProxyType.snowflake: + await Tor.instance.reconfigure( + snowflakePort: port, + bridgeLines: bridgeLines, + ); + } + } else { + await Tor.instance.reconfigure(); + } + } + } + + service.on("startOrReconfigure").listen((event) async { final proxyType = ProxyType.values.firstWhereOrNull( (x) => x.name == event?['proxyType'], ); - await startService(proxyType, event?['bridgeLines'] as String?); + await reconfigureService(proxyType, event?['bridgeLines'] as String?); + + if (!Tor.instance.hasClient) { + await startService(proxyType, event?['bridgeLines'] as String?); + } + }); + + service.on("reconfigure").listen((event) async { + final proxyType = ProxyType.values.firstWhereOrNull( + (x) => x.name == event?['proxyType'], + ); + + await reconfigureService(proxyType, event?['bridgeLines'] as String?); }); service.on("heartbeat").listen((event) { diff --git a/app/lib/features/user/data/database/daos/setting.dart b/app/lib/features/user/data/database/daos/setting.dart index b8a9b864..a2df40ae 100644 --- a/app/lib/features/user/data/database/daos/setting.dart +++ b/app/lib/features/user/data/database/daos/setting.dart @@ -42,7 +42,12 @@ class SettingDao extends DatabaseAccessor with _$SettingDaoMixin { partitionKey: Value(partitionKey), value: Value(driftvalue), ), - onConflict: DoUpdate((old) => SettingCompanion(value: Value(driftvalue))), + onConflict: DoUpdate( + (old) => SettingCompanion( + partitionKey: Value(partitionKey), + value: Value(driftvalue), + ), + ), ); } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index 05fd4484..8e30456e 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -266,7 +266,8 @@ Map _$GeneralSettingsToJson( 'showExtensionShortcut': instance.showExtensionShortcut, 'enableLocalAiFeatures': instance.enableLocalAiFeatures, 'defaultCreateTabType': _$TabTypeEnumMap[instance.defaultCreateTabType]!, - 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, + 'tabIntentOpenSetting': + _$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!, 'autoHideTabBar': instance.autoHideTabBar, 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, }; diff --git a/app/lib/features/user/data/models/tor_settings.dart b/app/lib/features/user/data/models/tor_settings.dart index fb15df08..01065749 100644 --- a/app/lib/features/user/data/models/tor_settings.dart +++ b/app/lib/features/user/data/models/tor_settings.dart @@ -4,17 +4,32 @@ import 'package:json_annotation/json_annotation.dart'; part 'tor_settings.g.dart'; +enum TorConnectionConfig { auto, direct, obfs4, snowflake } + @CopyWith() @JsonSerializable(includeIfNull: true, constructor: 'withDefaults') class TorSettings with FastEquatable { final bool proxyPrivateTabsTor; - final bool autoConfig; + final TorConnectionConfig config; + final bool requireBridge; + final bool fetchRemoteBridges; - TorSettings({required this.proxyPrivateTabsTor, required this.autoConfig}); + TorSettings({ + required this.proxyPrivateTabsTor, + required this.config, + required this.requireBridge, + required this.fetchRemoteBridges, + }); - TorSettings.withDefaults({bool? proxyPrivateTabsTor, bool? autoConfig}) - : proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, - autoConfig = autoConfig ?? true; + TorSettings.withDefaults({ + bool? proxyPrivateTabsTor, + TorConnectionConfig? config, + bool? requireBridge, + bool? fetchRemoteBridges, + }) : proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, + config = config ?? TorConnectionConfig.auto, + requireBridge = requireBridge ?? false, + fetchRemoteBridges = fetchRemoteBridges ?? true; factory TorSettings.fromJson(Map json) => _$TorSettingsFromJson(json); @@ -22,5 +37,10 @@ class TorSettings with FastEquatable { Map toJson() => _$TorSettingsToJson(this); @override - List get hashParameters => [proxyPrivateTabsTor, autoConfig]; + List get hashParameters => [ + proxyPrivateTabsTor, + config, + requireBridge, + fetchRemoteBridges, + ]; } diff --git a/app/lib/features/user/data/models/tor_settings.g.dart b/app/lib/features/user/data/models/tor_settings.g.dart index 298363ee..cd6b7159 100644 --- a/app/lib/features/user/data/models/tor_settings.g.dart +++ b/app/lib/features/user/data/models/tor_settings.g.dart @@ -9,7 +9,11 @@ part of 'tor_settings.dart'; abstract class _$TorSettingsCWProxy { TorSettings proxyPrivateTabsTor(bool proxyPrivateTabsTor); - TorSettings autoConfig(bool autoConfig); + TorSettings config(TorConnectionConfig config); + + TorSettings requireBridge(bool requireBridge); + + TorSettings fetchRemoteBridges(bool fetchRemoteBridges); /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TorSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -17,7 +21,12 @@ abstract class _$TorSettingsCWProxy { /// ```dart /// TorSettings(...).copyWith(id: 12, name: "My name") /// ```` - TorSettings call({bool proxyPrivateTabsTor, bool autoConfig}); + TorSettings call({ + bool proxyPrivateTabsTor, + TorConnectionConfig config, + bool requireBridge, + bool fetchRemoteBridges, + }); } /// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTorSettings.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTorSettings.copyWith.fieldName(...)` @@ -31,7 +40,15 @@ class _$TorSettingsCWProxyImpl implements _$TorSettingsCWProxy { this(proxyPrivateTabsTor: proxyPrivateTabsTor); @override - TorSettings autoConfig(bool autoConfig) => this(autoConfig: autoConfig); + TorSettings config(TorConnectionConfig config) => this(config: config); + + @override + TorSettings requireBridge(bool requireBridge) => + this(requireBridge: requireBridge); + + @override + TorSettings fetchRemoteBridges(bool fetchRemoteBridges) => + this(fetchRemoteBridges: fetchRemoteBridges); @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TorSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. @@ -42,17 +59,27 @@ class _$TorSettingsCWProxyImpl implements _$TorSettingsCWProxy { /// ```` TorSettings call({ Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), - Object? autoConfig = const $CopyWithPlaceholder(), + Object? config = const $CopyWithPlaceholder(), + Object? requireBridge = const $CopyWithPlaceholder(), + Object? fetchRemoteBridges = const $CopyWithPlaceholder(), }) { return TorSettings( proxyPrivateTabsTor: proxyPrivateTabsTor == const $CopyWithPlaceholder() ? _value.proxyPrivateTabsTor // ignore: cast_nullable_to_non_nullable : proxyPrivateTabsTor as bool, - autoConfig: autoConfig == const $CopyWithPlaceholder() - ? _value.autoConfig + config: config == const $CopyWithPlaceholder() + ? _value.config // ignore: cast_nullable_to_non_nullable - : autoConfig as bool, + : config as TorConnectionConfig, + requireBridge: requireBridge == const $CopyWithPlaceholder() + ? _value.requireBridge + // ignore: cast_nullable_to_non_nullable + : requireBridge as bool, + fetchRemoteBridges: fetchRemoteBridges == const $CopyWithPlaceholder() + ? _value.fetchRemoteBridges + // ignore: cast_nullable_to_non_nullable + : fetchRemoteBridges as bool, ); } } @@ -70,11 +97,22 @@ extension $TorSettingsCopyWith on TorSettings { TorSettings _$TorSettingsFromJson(Map json) => TorSettings.withDefaults( proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, - autoConfig: json['autoConfig'] as bool?, + config: $enumDecodeNullable(_$TorConnectionConfigEnumMap, json['config']), + requireBridge: json['requireBridge'] as bool?, + fetchRemoteBridges: json['fetchRemoteBridges'] as bool?, ); Map _$TorSettingsToJson(TorSettings instance) => { 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, - 'autoConfig': instance.autoConfig, + 'config': _$TorConnectionConfigEnumMap[instance.config]!, + 'requireBridge': instance.requireBridge, + 'fetchRemoteBridges': instance.fetchRemoteBridges, }; + +const _$TorConnectionConfigEnumMap = { + TorConnectionConfig.auto: 'auto', + TorConnectionConfig.direct: 'direct', + TorConnectionConfig.obfs4: 'obfs4', + TorConnectionConfig.snowflake: 'snowflake', +}; diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 0fc6484e..db5a7d59 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider = typedef GeneralSettingsWithDefaultsRef = AutoDisposeProviderRef; String _$generalSettingsRepositoryHash() => - r'07eeaf811fac4b9c787cce7c29379a7329910ae5'; + r'0d207ae4ae9fd94b1257eb8aa4cfd73d4a47caf2'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository) diff --git a/app/lib/features/user/domain/repositories/tor_settings.dart b/app/lib/features/user/domain/repositories/tor_settings.dart index 3b17764c..01388df0 100644 --- a/app/lib/features/user/domain/repositories/tor_settings.dart +++ b/app/lib/features/user/domain/repositories/tor_settings.dart @@ -23,7 +23,12 @@ class TorSettingsRepository extends _$TorSettingsRepository { DriftSqlType.bool, db.typeMapping, ), - 'autoConfig': settings['autoConfig']?.readAs( + 'config': settings['config']?.readAs(DriftSqlType.string, db.typeMapping), + 'requireBridge': settings['requireBridge']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), + 'fetchRemoteBridges': settings['fetchRemoteBridges']?.readAs( DriftSqlType.bool, db.typeMapping, ), diff --git a/app/lib/features/user/domain/repositories/tor_settings.g.dart b/app/lib/features/user/domain/repositories/tor_settings.g.dart index 843caba1..492f951f 100644 --- a/app/lib/features/user/domain/repositories/tor_settings.g.dart +++ b/app/lib/features/user/domain/repositories/tor_settings.g.dart @@ -26,7 +26,7 @@ final torSettingsWithDefaultsProvider = // ignore: unused_element typedef TorSettingsWithDefaultsRef = AutoDisposeProviderRef; String _$torSettingsRepositoryHash() => - r'686e8576453f33a0d2d0eb2869e29ca88e00bc37'; + r'ef80d1cc6b9b72cce371c03c656b10a0924d49d0'; /// See also [TorSettingsRepository]. @ProviderFor(TorSettingsRepository) diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 00000000..f592d85a --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,4 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: + - drift: true \ No newline at end of file diff --git a/packages/flutter_mozilla_components/javascript/readability/dist/report.html b/packages/flutter_mozilla_components/javascript/readability/dist/report.html index b76c3dbf..c05dde8b 100644 --- a/packages/flutter_mozilla_components/javascript/readability/dist/report.html +++ b/packages/flutter_mozilla_components/javascript/readability/dist/report.html @@ -3,7 +3,7 @@ - readability [24 Aug 2025 at 12:45] + readability [12 Sep 2025 at 07:08]