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