diff --git a/lib/core/http_client/dio_http_client.dart b/lib/core/http_client/dio_http_client.dart index a19c3f6b..533ec17b 100644 --- a/lib/core/http_client/dio_http_client.dart +++ b/lib/core/http_client/dio_http_client.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:flutter_loggy_dio/flutter_loggy_dio.dart'; import 'package:hiddify/utils/custom_loggers.dart'; @@ -38,6 +40,19 @@ class DioHttpClient with InfraLogger { late final Dio _dio; + void setProxyPort(int port) { + loggy.debug("setting proxy port: [$port]"); + _dio.httpClientAdapter = IOHttpClientAdapter( + createHttpClient: () { + final client = HttpClient(); + client.findProxy = (url) { + return "PROXY localhost:$port; DIRECT"; + }; + return client; + }, + ); + } + Future> get( String url, { CancelToken? cancelToken, diff --git a/lib/core/http_client/http_client_provider.dart b/lib/core/http_client/http_client_provider.dart index a70df92d..8335ec9b 100644 --- a/lib/core/http_client/http_client_provider.dart +++ b/lib/core/http_client/http_client_provider.dart @@ -1,15 +1,27 @@ import 'package:flutter/foundation.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/http_client/dio_http_client.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'http_client_provider.g.dart'; @Riverpod(keepAlive: true) DioHttpClient httpClient(HttpClientRef ref) { - return DioHttpClient( + final client = DioHttpClient( timeout: const Duration(seconds: 15), userAgent: ref.watch(appInfoProvider).requireValue.userAgent, debug: kDebugMode, ); + + ref.listen( + configOptionNotifierProvider, + (_, next) { + if (next case AsyncData(value: final options)) { + client.setProxyPort(options.mixedPort); + } + }, + fireImmediately: true, + ); + return client; } diff --git a/lib/features/config_option/data/config_option_data_providers.dart b/lib/features/config_option/data/config_option_data_providers.dart index c9b5f9b4..cf8fc061 100644 --- a/lib/features/config_option/data/config_option_data_providers.dart +++ b/lib/features/config_option/data/config_option_data_providers.dart @@ -11,6 +11,16 @@ ConfigOptionRepository configOptionRepository( ) { return ConfigOptionRepositoryImpl( preferences: ref.watch(sharedPreferencesProvider).requireValue, + ); +} + +@Riverpod(keepAlive: true) +SingBoxConfigOptionRepository singBoxConfigOptionRepository( + SingBoxConfigOptionRepositoryRef ref, +) { + return SingBoxConfigOptionRepositoryImpl( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + optionsRepository: ref.watch(configOptionRepositoryProvider), geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue, geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), ); diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index 82faa16f..dc5f05cf 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -14,25 +14,115 @@ import 'package:meta/meta.dart'; import 'package:shared_preferences/shared_preferences.dart'; abstract interface class ConfigOptionRepository { - TaskEither - getFullSingboxConfigOption(); - TaskEither getConfigOption(); + Either getConfigOption(); TaskEither updateConfigOption( ConfigOptionPatch patch, ); TaskEither resetConfigOption(); } +abstract interface class SingBoxConfigOptionRepository { + TaskEither + getFullSingboxConfigOption(); +} + class ConfigOptionRepositoryImpl with ExceptionHandler, InfraLogger implements ConfigOptionRepository { - ConfigOptionRepositoryImpl({ + ConfigOptionRepositoryImpl({required this.preferences}); + + final SharedPreferences preferences; + + @override + Either getConfigOption() { + try { + final map = ConfigOptionEntity.initial.toJson(); + for (final key in map.keys) { + final persisted = preferences.get(key); + if (persisted != null) { + final defaultValue = map[key]; + if (defaultValue != null && + persisted.runtimeType != defaultValue.runtimeType) { + loggy.warning( + "error getting preference[$key], expected type: [${defaultValue.runtimeType}] - received value: [$persisted](${persisted.runtimeType})", + ); + continue; + } + map[key] = persisted; + } + } + final options = ConfigOptionEntity.fromJson(map); + return right(options); + } catch (error, stackTrace) { + return left(ConfigOptionUnexpectedFailure(error, stackTrace)); + } + } + + @override + TaskEither updateConfigOption( + ConfigOptionPatch patch, + ) { + return exceptionHandler( + () async { + final map = patch.toJson(); + await updateByJson(map); + return right(unit); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @override + TaskEither resetConfigOption() { + return exceptionHandler( + () async { + final map = ConfigOptionEntity.initial.toJson(); + await updateByJson(map); + return right(unit); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @visibleForTesting + Future updateByJson( + Map options, + ) async { + final map = ConfigOptionEntity.initial.toJson(); + for (final key in map.keys) { + final value = options[key]; + if (value != null) { + loggy.debug("updating [$key] to [$value]"); + + switch (value) { + case bool _: + await preferences.setBool(key, value); + case String _: + await preferences.setString(key, value); + case int _: + await preferences.setInt(key, value); + case double _: + await preferences.setDouble(key, value); + default: + loggy.warning("unexpected type"); + } + } + } + } +} + +class SingBoxConfigOptionRepositoryImpl + with ExceptionHandler, InfraLogger + implements SingBoxConfigOptionRepository { + SingBoxConfigOptionRepositoryImpl({ required this.preferences, + required this.optionsRepository, required this.geoAssetRepository, required this.geoAssetPathResolver, }); final SharedPreferences preferences; + final ConfigOptionRepository optionsRepository; final GeoAssetRepository geoAssetRepository; final GeoAssetPathResolver geoAssetPathResolver; @@ -81,7 +171,7 @@ class ConfigOptionRepositoryImpl .run(); final persisted = - await getConfigOption().getOrElse((l) => throw l).run(); + optionsRepository.getConfigOption().getOrElse((l) => throw l); final singboxConfigOption = SingboxConfigOption( executeConfigAsIs: false, logLevel: persisted.logLevel, @@ -138,82 +228,4 @@ class ConfigOptionRepositoryImpl ConfigOptionUnexpectedFailure.new, ); } - - @override - TaskEither getConfigOption() { - return exceptionHandler( - () async { - final map = ConfigOptionEntity.initial.toJson(); - for (final key in map.keys) { - final persisted = preferences.get(key); - if (persisted != null) { - final defaultValue = map[key]; - if (defaultValue != null && - persisted.runtimeType != defaultValue.runtimeType) { - loggy.warning( - "error getting preference[$key], expected type: [${defaultValue.runtimeType}] - received value: [$persisted](${persisted.runtimeType})", - ); - continue; - } - map[key] = persisted; - } - } - final options = ConfigOptionEntity.fromJson(map); - return right(options); - }, - ConfigOptionUnexpectedFailure.new, - ); - } - - @override - TaskEither updateConfigOption( - ConfigOptionPatch patch, - ) { - return exceptionHandler( - () async { - final map = patch.toJson(); - await updateByJson(map); - return right(unit); - }, - ConfigOptionUnexpectedFailure.new, - ); - } - - @override - TaskEither resetConfigOption() { - return exceptionHandler( - () async { - final map = ConfigOptionEntity.initial.toJson(); - await updateByJson(map); - return right(unit); - }, - ConfigOptionUnexpectedFailure.new, - ); - } - - @visibleForTesting - Future updateByJson( - Map options, - ) async { - final map = ConfigOptionEntity.initial.toJson(); - for (final key in map.keys) { - final value = options[key]; - if (value != null) { - loggy.debug("updating [$key] to [$value]"); - - switch (value) { - case bool _: - await preferences.setBool(key, value); - case String _: - await preferences.setString(key, value); - case int _: - await preferences.setInt(key, value); - case double _: - await preferences.setDouble(key, value); - default: - loggy.warning("unexpected type"); - } - } - } - } } diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart index 40fb1490..77a67763 100644 --- a/lib/features/config_option/notifier/config_option_notifier.dart +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -9,14 +9,14 @@ part 'config_option_notifier.g.dart'; @Riverpod(keepAlive: true) class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { @override - Future build() { + Future build() async { return ref .watch(configOptionRepositoryProvider) .getConfigOption() .getOrElse((l) { loggy.error("error getting persisted options $l", l); throw l; - }).run(); + }); } Future updateOption(ConfigOptionPatch patch) async { diff --git a/lib/features/connection/data/connection_data_providers.dart b/lib/features/connection/data/connection_data_providers.dart index 96de56d9..6287964b 100644 --- a/lib/features/connection/data/connection_data_providers.dart +++ b/lib/features/connection/data/connection_data_providers.dart @@ -15,7 +15,8 @@ ConnectionRepository connectionRepository( ) { return ConnectionRepositoryImpl( directories: ref.watch(appDirectoriesProvider).requireValue, - configOptionRepository: ref.watch(configOptionRepositoryProvider), + singBoxConfigOptionRepository: + ref.watch(singBoxConfigOptionRepositoryProvider), singbox: ref.watch(singboxServiceProvider), platformSource: ConnectionPlatformSourceImpl(), profilePathResolver: ref.watch(profilePathResolverProvider), diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart index e4d55c9b..9e53b74c 100644 --- a/lib/features/connection/data/connection_repository.dart +++ b/lib/features/connection/data/connection_repository.dart @@ -38,7 +38,7 @@ class ConnectionRepositoryImpl required this.directories, required this.singbox, required this.platformSource, - required this.configOptionRepository, + required this.singBoxConfigOptionRepository, required this.profilePathResolver, required this.geoAssetPathResolver, }); @@ -46,7 +46,7 @@ class ConnectionRepositoryImpl final Directories directories; final SingboxService singbox; final ConnectionPlatformSource platformSource; - final ConfigOptionRepository configOptionRepository; + final SingBoxConfigOptionRepository singBoxConfigOptionRepository; final ProfilePathResolver profilePathResolver; final GeoAssetPathResolver geoAssetPathResolver; @@ -83,7 +83,7 @@ class ConnectionRepositoryImpl return TaskEither.Do( ($) async { final options = await $( - configOptionRepository + singBoxConfigOptionRepository .getFullSingboxConfigOption() .mapLeft((l) => const InvalidConfigOption()), ); @@ -189,7 +189,7 @@ class ConnectionRepositoryImpl ($) async { final options = await $(getConfigOption()); - await $( + await $( TaskEither(() async { if (options.enableTun) { // final hasPrivilege = await platformSource.checkPrivilege(); @@ -202,13 +202,10 @@ class ConnectionRepositoryImpl }), ); return await $( - singbox.stop() - .mapLeft(UnexpectedConnectionFailure.new), + singbox.stop().mapLeft(UnexpectedConnectionFailure.new), ); }, ).handleExceptions(UnexpectedConnectionFailure.new); - - } @override