Add proxy http adapter

This commit is contained in:
problematicconsumer
2024-02-11 16:45:22 +03:30
parent 6a81a2ab62
commit 7e1989d0bc
7 changed files with 142 additions and 95 deletions

View File

@@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart'; import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter_loggy_dio/flutter_loggy_dio.dart'; import 'package:flutter_loggy_dio/flutter_loggy_dio.dart';
import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/custom_loggers.dart';
@@ -38,6 +40,19 @@ class DioHttpClient with InfraLogger {
late final Dio _dio; 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<Response<T>> get<T>( Future<Response<T>> get<T>(
String url, { String url, {
CancelToken? cancelToken, CancelToken? cancelToken,

View File

@@ -1,15 +1,27 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart';
import 'package:hiddify/core/http_client/dio_http_client.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'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'http_client_provider.g.dart'; part 'http_client_provider.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
DioHttpClient httpClient(HttpClientRef ref) { DioHttpClient httpClient(HttpClientRef ref) {
return DioHttpClient( final client = DioHttpClient(
timeout: const Duration(seconds: 15), timeout: const Duration(seconds: 15),
userAgent: ref.watch(appInfoProvider).requireValue.userAgent, userAgent: ref.watch(appInfoProvider).requireValue.userAgent,
debug: kDebugMode, debug: kDebugMode,
); );
ref.listen(
configOptionNotifierProvider,
(_, next) {
if (next case AsyncData(value: final options)) {
client.setProxyPort(options.mixedPort);
}
},
fireImmediately: true,
);
return client;
} }

View File

@@ -11,6 +11,16 @@ ConfigOptionRepository configOptionRepository(
) { ) {
return ConfigOptionRepositoryImpl( return ConfigOptionRepositoryImpl(
preferences: ref.watch(sharedPreferencesProvider).requireValue, 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, geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue,
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
); );

View File

@@ -14,25 +14,115 @@ import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
abstract interface class ConfigOptionRepository { abstract interface class ConfigOptionRepository {
TaskEither<ConfigOptionFailure, SingboxConfigOption> Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption();
getFullSingboxConfigOption();
TaskEither<ConfigOptionFailure, ConfigOptionEntity> getConfigOption();
TaskEither<ConfigOptionFailure, Unit> updateConfigOption( TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
ConfigOptionPatch patch, ConfigOptionPatch patch,
); );
TaskEither<ConfigOptionFailure, Unit> resetConfigOption(); TaskEither<ConfigOptionFailure, Unit> resetConfigOption();
} }
abstract interface class SingBoxConfigOptionRepository {
TaskEither<ConfigOptionFailure, SingboxConfigOption>
getFullSingboxConfigOption();
}
class ConfigOptionRepositoryImpl class ConfigOptionRepositoryImpl
with ExceptionHandler, InfraLogger with ExceptionHandler, InfraLogger
implements ConfigOptionRepository { implements ConfigOptionRepository {
ConfigOptionRepositoryImpl({ ConfigOptionRepositoryImpl({required this.preferences});
final SharedPreferences preferences;
@override
Either<ConfigOptionFailure, ConfigOptionEntity> 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<ConfigOptionFailure, Unit> updateConfigOption(
ConfigOptionPatch patch,
) {
return exceptionHandler(
() async {
final map = patch.toJson();
await updateByJson(map);
return right(unit);
},
ConfigOptionUnexpectedFailure.new,
);
}
@override
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
return exceptionHandler(
() async {
final map = ConfigOptionEntity.initial.toJson();
await updateByJson(map);
return right(unit);
},
ConfigOptionUnexpectedFailure.new,
);
}
@visibleForTesting
Future<void> updateByJson(
Map<String, dynamic> 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.preferences,
required this.optionsRepository,
required this.geoAssetRepository, required this.geoAssetRepository,
required this.geoAssetPathResolver, required this.geoAssetPathResolver,
}); });
final SharedPreferences preferences; final SharedPreferences preferences;
final ConfigOptionRepository optionsRepository;
final GeoAssetRepository geoAssetRepository; final GeoAssetRepository geoAssetRepository;
final GeoAssetPathResolver geoAssetPathResolver; final GeoAssetPathResolver geoAssetPathResolver;
@@ -81,7 +171,7 @@ class ConfigOptionRepositoryImpl
.run(); .run();
final persisted = final persisted =
await getConfigOption().getOrElse((l) => throw l).run(); optionsRepository.getConfigOption().getOrElse((l) => throw l);
final singboxConfigOption = SingboxConfigOption( final singboxConfigOption = SingboxConfigOption(
executeConfigAsIs: false, executeConfigAsIs: false,
logLevel: persisted.logLevel, logLevel: persisted.logLevel,
@@ -138,82 +228,4 @@ class ConfigOptionRepositoryImpl
ConfigOptionUnexpectedFailure.new, ConfigOptionUnexpectedFailure.new,
); );
} }
@override
TaskEither<ConfigOptionFailure, ConfigOptionEntity> 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<ConfigOptionFailure, Unit> updateConfigOption(
ConfigOptionPatch patch,
) {
return exceptionHandler(
() async {
final map = patch.toJson();
await updateByJson(map);
return right(unit);
},
ConfigOptionUnexpectedFailure.new,
);
}
@override
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
return exceptionHandler(
() async {
final map = ConfigOptionEntity.initial.toJson();
await updateByJson(map);
return right(unit);
},
ConfigOptionUnexpectedFailure.new,
);
}
@visibleForTesting
Future<void> updateByJson(
Map<String, dynamic> 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");
}
}
}
}
} }

View File

@@ -9,14 +9,14 @@ part 'config_option_notifier.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
@override @override
Future<ConfigOptionEntity> build() { Future<ConfigOptionEntity> build() async {
return ref return ref
.watch(configOptionRepositoryProvider) .watch(configOptionRepositoryProvider)
.getConfigOption() .getConfigOption()
.getOrElse((l) { .getOrElse((l) {
loggy.error("error getting persisted options $l", l); loggy.error("error getting persisted options $l", l);
throw l; throw l;
}).run(); });
} }
Future<void> updateOption(ConfigOptionPatch patch) async { Future<void> updateOption(ConfigOptionPatch patch) async {

View File

@@ -15,7 +15,8 @@ ConnectionRepository connectionRepository(
) { ) {
return ConnectionRepositoryImpl( return ConnectionRepositoryImpl(
directories: ref.watch(appDirectoriesProvider).requireValue, directories: ref.watch(appDirectoriesProvider).requireValue,
configOptionRepository: ref.watch(configOptionRepositoryProvider), singBoxConfigOptionRepository:
ref.watch(singBoxConfigOptionRepositoryProvider),
singbox: ref.watch(singboxServiceProvider), singbox: ref.watch(singboxServiceProvider),
platformSource: ConnectionPlatformSourceImpl(), platformSource: ConnectionPlatformSourceImpl(),
profilePathResolver: ref.watch(profilePathResolverProvider), profilePathResolver: ref.watch(profilePathResolverProvider),

View File

@@ -38,7 +38,7 @@ class ConnectionRepositoryImpl
required this.directories, required this.directories,
required this.singbox, required this.singbox,
required this.platformSource, required this.platformSource,
required this.configOptionRepository, required this.singBoxConfigOptionRepository,
required this.profilePathResolver, required this.profilePathResolver,
required this.geoAssetPathResolver, required this.geoAssetPathResolver,
}); });
@@ -46,7 +46,7 @@ class ConnectionRepositoryImpl
final Directories directories; final Directories directories;
final SingboxService singbox; final SingboxService singbox;
final ConnectionPlatformSource platformSource; final ConnectionPlatformSource platformSource;
final ConfigOptionRepository configOptionRepository; final SingBoxConfigOptionRepository singBoxConfigOptionRepository;
final ProfilePathResolver profilePathResolver; final ProfilePathResolver profilePathResolver;
final GeoAssetPathResolver geoAssetPathResolver; final GeoAssetPathResolver geoAssetPathResolver;
@@ -83,7 +83,7 @@ class ConnectionRepositoryImpl
return TaskEither<ConnectionFailure, SingboxConfigOption>.Do( return TaskEither<ConnectionFailure, SingboxConfigOption>.Do(
($) async { ($) async {
final options = await $( final options = await $(
configOptionRepository singBoxConfigOptionRepository
.getFullSingboxConfigOption() .getFullSingboxConfigOption()
.mapLeft((l) => const InvalidConfigOption()), .mapLeft((l) => const InvalidConfigOption()),
); );
@@ -189,7 +189,7 @@ class ConnectionRepositoryImpl
($) async { ($) async {
final options = await $(getConfigOption()); final options = await $(getConfigOption());
await $( await $(
TaskEither(() async { TaskEither(() async {
if (options.enableTun) { if (options.enableTun) {
// final hasPrivilege = await platformSource.checkPrivilege(); // final hasPrivilege = await platformSource.checkPrivilege();
@@ -202,13 +202,10 @@ class ConnectionRepositoryImpl
}), }),
); );
return await $( return await $(
singbox.stop() singbox.stop().mapLeft(UnexpectedConnectionFailure.new),
.mapLeft(UnexpectedConnectionFailure.new),
); );
}, },
).handleExceptions(UnexpectedConnectionFailure.new); ).handleExceptions(UnexpectedConnectionFailure.new);
} }
@override @override