Add warp config generator

This commit is contained in:
problematicconsumer
2024-02-18 12:35:11 +03:30
parent ce6ab59bf7
commit 33dc21918d
15 changed files with 459 additions and 973 deletions

View File

@@ -7,6 +7,7 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.mobile.Mobile
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -30,6 +31,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
SelectOutbound("select_outbound"), SelectOutbound("select_outbound"),
UrlTest("url_test"), UrlTest("url_test"),
ClearLogs("clear_logs"), ClearLogs("clear_logs"),
GenerateWarpConfig("generate_warp_config"),
} }
} }
@@ -181,6 +183,20 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
} }
} }
Trigger.GenerateWarpConfig.method -> {
scope.launch(Dispatchers.IO) {
result.runCatching {
val args = call.arguments as Map<*, *>
val warpConfig = Mobile.generateWarpConfig(
args["license-key"] as String,
args["previous-account-id"] as String,
args["previous-access-token"] as String,
)
success(warpConfig)
}
}
}
else -> result.notImplemented() else -> result.notImplemented()
} }
} }

View File

@@ -237,6 +237,8 @@
"title": "Cloudflare WARP Consent", "title": "Cloudflare WARP Consent",
"description(rich)": "Cloudflare WARP is a free WireGuard VPN provider. By enabling this option you are agreeing to the Cloudflare WARP's ${tos(Terms of Service)} and ${privacy(Privacy Policy)}." "description(rich)": "Cloudflare WARP is a free WireGuard VPN provider. By enabling this option you are agreeing to the Cloudflare WARP's ${tos(Terms of Service)} and ${privacy(Privacy Policy)}."
}, },
"generateWarpConfig": "Generate WARP config",
"missingWarpConfig": "Missing WARP config",
"pageTitle": "Config Options", "pageTitle": "Config Options",
"logLevel": "Log Level", "logLevel": "Log Level",
"resolveDestination": "Resolve Destination", "resolveDestination": "Resolve Destination",

View File

@@ -1,6 +1,7 @@
import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'config_option_data_providers.g.dart'; part 'config_option_data_providers.g.dart';
@@ -11,6 +12,7 @@ ConfigOptionRepository configOptionRepository(
) { ) {
return ConfigOptionRepositoryImpl( return ConfigOptionRepositoryImpl(
preferences: ref.watch(sharedPreferencesProvider).requireValue, preferences: ref.watch(sharedPreferencesProvider).requireValue,
singbox: ref.watch(singboxServiceProvider),
); );
} }

View File

@@ -7,6 +7,7 @@ import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart'; import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_rule.dart'; import 'package:hiddify/singbox/model/singbox_rule.dart';
import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -17,6 +18,7 @@ abstract interface class ConfigOptionRepository {
ConfigOptionPatch patch, ConfigOptionPatch patch,
); );
TaskEither<ConfigOptionFailure, Unit> resetConfigOption(); TaskEither<ConfigOptionFailure, Unit> resetConfigOption();
TaskEither<ConfigOptionFailure, Unit> generateWarpConfig();
} }
abstract interface class SingBoxConfigOptionRepository { abstract interface class SingBoxConfigOptionRepository {
@@ -27,9 +29,13 @@ abstract interface class SingBoxConfigOptionRepository {
class ConfigOptionRepositoryImpl class ConfigOptionRepositoryImpl
with ExceptionHandler, InfraLogger with ExceptionHandler, InfraLogger
implements ConfigOptionRepository { implements ConfigOptionRepository {
ConfigOptionRepositoryImpl({required this.preferences}); ConfigOptionRepositoryImpl({
required this.preferences,
required this.singbox,
});
final SharedPreferences preferences; final SharedPreferences preferences;
final SingboxService singbox;
@override @override
Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption() { Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption() {
@@ -107,6 +113,35 @@ class ConfigOptionRepositoryImpl
} }
} }
} }
@override
TaskEither<ConfigOptionFailure, Unit> generateWarpConfig() {
return exceptionHandler(
() async {
final options = getConfigOption().getOrElse((l) => throw l);
return await singbox
.generateWarpConfig(
licenseKey: options.warpLicenseKey,
previousAccountId: options.warpAccountId,
previousAccessToken: options.warpAccessToken,
)
.mapLeft((l) => ConfigOptionFailure.unexpected(l))
.flatMap(
(warp) => updateConfigOption(
ConfigOptionPatch(
warpAccountId: warp.accountId,
warpAccessToken: warp.accessToken,
),
),
)
.run();
},
(error, stackTrace) {
loggy.error(error);
return ConfigOptionUnexpectedFailure(error, stackTrace);
},
);
}
} }
class SingBoxConfigOptionRepositoryImpl class SingBoxConfigOptionRepositoryImpl

View File

@@ -61,6 +61,8 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
@Default(false) bool enableWarp, @Default(false) bool enableWarp,
@Default(WarpDetourMode.outbound) WarpDetourMode warpDetourMode, @Default(WarpDetourMode.outbound) WarpDetourMode warpDetourMode,
@Default("") String warpLicenseKey, @Default("") String warpLicenseKey,
@Default("") String warpAccountId,
@Default("") String warpAccessToken,
@Default("auto") String warpCleanIp, @Default("auto") String warpCleanIp,
@Default(0) int warpPort, @Default(0) int warpPort,
@OptionalRangeJsonConverter() @OptionalRangeJsonConverter()
@@ -133,6 +135,8 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
enableWarp: patch.enableWarp ?? enableWarp, enableWarp: patch.enableWarp ?? enableWarp,
warpDetourMode: patch.warpDetourMode ?? warpDetourMode, warpDetourMode: patch.warpDetourMode ?? warpDetourMode,
warpLicenseKey: patch.warpLicenseKey ?? warpLicenseKey, warpLicenseKey: patch.warpLicenseKey ?? warpLicenseKey,
warpAccountId: patch.warpAccountId ?? warpAccountId,
warpAccessToken: patch.warpAccessToken ?? warpAccessToken,
warpCleanIp: patch.warpCleanIp ?? warpCleanIp, warpCleanIp: patch.warpCleanIp ?? warpCleanIp,
warpPort: patch.warpPort ?? warpPort, warpPort: patch.warpPort ?? warpPort,
warpNoise: patch.warpNoise ?? warpNoise, warpNoise: patch.warpNoise ?? warpNoise,
@@ -183,6 +187,8 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
enableWarp: enableWarp, enableWarp: enableWarp,
warpDetourMode: warpDetourMode, warpDetourMode: warpDetourMode,
warpLicenseKey: warpLicenseKey, warpLicenseKey: warpLicenseKey,
warpAccountId: warpAccountId,
warpAccessToken: warpAccessToken,
warpCleanIp: warpCleanIp, warpCleanIp: warpCleanIp,
warpPort: warpPort, warpPort: warpPort,
warpNoise: warpNoise, warpNoise: warpNoise,
@@ -237,6 +243,8 @@ class ConfigOptionPatch with _$ConfigOptionPatch {
bool? enableWarp, bool? enableWarp,
WarpDetourMode? warpDetourMode, WarpDetourMode? warpDetourMode,
String? warpLicenseKey, String? warpLicenseKey,
String? warpAccountId,
String? warpAccessToken,
String? warpCleanIp, String? warpCleanIp,
int? warpPort, int? warpPort,
@OptionalRangeJsonConverter() OptionalRange? warpNoise, @OptionalRangeJsonConverter() OptionalRange? warpNoise,

View File

@@ -14,6 +14,9 @@ sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure {
StackTrace? stackTrace, StackTrace? stackTrace,
]) = ConfigOptionUnexpectedFailure; ]) = ConfigOptionUnexpectedFailure;
@With<ExpectedFailure>()
const factory ConfigOptionFailure.missingWarp() = MissingWarpConfigFailure;
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
@@ -21,6 +24,10 @@ sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure {
type: t.failure.unexpected, type: t.failure.unexpected,
message: null, message: null,
), ),
MissingWarpConfigFailure() => (
type: t.settings.config.missingWarpConfig,
message: null,
),
}; };
} }
} }

View File

@@ -1,26 +1,71 @@
import 'package:fpdart/fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/features/config_option/data/config_option_data_providers.dart';
import 'package:hiddify/features/config_option/model/config_option_failure.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'warp_option_notifier.freezed.dart';
part 'warp_option_notifier.g.dart'; part 'warp_option_notifier.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class WarpOptionNotifier extends _$WarpOptionNotifier { class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
@override @override
bool build() { WarpOptions build() {
return ref final consent = _prefs.getBool(warpConsentGiven) ?? false;
.read(sharedPreferencesProvider) bool hasWarpConfig = false;
.requireValue try {
.getBool(warpConsentGiven) ?? final accountId = _prefs.getString("warp-account-id");
false; final accessToken = _prefs.getString("warp-access-token");
hasWarpConfig = accountId != null && accessToken != null;
} catch (e) {
loggy.warning(e);
} }
return WarpOptions(
consentGiven: consent,
configGeneration: hasWarpConfig
? const AsyncValue.data(unit)
: AsyncError(const MissingWarpConfigFailure(), StackTrace.current),
);
}
SharedPreferences get _prefs =>
ref.read(sharedPreferencesProvider).requireValue;
Future<void> agree() async { Future<void> agree() async {
await ref await ref
.read(sharedPreferencesProvider) .read(sharedPreferencesProvider)
.requireValue .requireValue
.setBool(warpConsentGiven, true); .setBool(warpConsentGiven, true);
state = true; state = state.copyWith(consentGiven: true);
await generateWarpConfig();
}
Future<void> generateWarpConfig() async {
if (state.configGeneration.isLoading) return;
state = state.copyWith(configGeneration: const AsyncLoading());
final result = await AsyncValue.guard(
() async => await ref
.read(configOptionRepositoryProvider)
.generateWarpConfig()
.getOrElse((l) {
loggy.warning("error generating warp config: $l", l);
throw l;
}).run(),
);
state = state.copyWith(configGeneration: result);
} }
static const warpConsentGiven = "warp_consent_given"; static const warpConsentGiven = "warp_consent_given";
} }
@freezed
class WarpOptions with _$WarpOptions {
const factory WarpOptions({
required bool consentGiven,
required AsyncValue<Unit> configGeneration,
}) = _WarpOptions;
}

View File

@@ -28,7 +28,8 @@ class WarpOptionsTiles extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider); final warpOptions = ref.watch(warpOptionNotifierProvider);
final warpPrefaceCompleted = warpOptions.consentGiven;
final canChangeOptions = warpPrefaceCompleted && options.enableWarp; final canChangeOptions = warpPrefaceCompleted && options.enableWarp;
return Column( return Column(
@@ -51,6 +52,26 @@ class WarpOptionsTiles extends HookConsumerWidget {
} }
}, },
), ),
ListTile(
title: Text(t.settings.config.generateWarpConfig),
subtitle: canChangeOptions
? switch (warpOptions.configGeneration) {
AsyncLoading() => const LinearProgressIndicator(),
AsyncError() => Text(
t.settings.config.missingWarpConfig,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
),
_ => null,
}
: null,
enabled: canChangeOptions,
onTap: () async {
await ref
.read(warpOptionNotifierProvider.notifier)
.generateWarpConfig();
},
),
ListTile( ListTile(
title: Text(t.settings.config.warpDetourMode), title: Text(t.settings.config.warpDetourMode),
subtitle: Text(options.warpDetourMode.present(t)), subtitle: Text(options.warpDetourMode.present(t)),

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,8 @@ class SingboxConfigOption with _$SingboxConfigOption {
required bool enableWarp, required bool enableWarp,
required WarpDetourMode warpDetourMode, required WarpDetourMode warpDetourMode,
required String warpLicenseKey, required String warpLicenseKey,
required String warpAccountId,
required String warpAccessToken,
required String warpCleanIp, required String warpCleanIp,
required int warpPort, required int warpPort,
@OptionalRangeJsonConverter() required OptionalRange warpNoise, @OptionalRangeJsonConverter() required OptionalRange warpNoise,

View File

@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'warp_account.freezed.dart';
part 'warp_account.g.dart';
@freezed
class WarpAccount with _$WarpAccount {
const factory WarpAccount({
required String licenseKey,
required String accountId,
required String accessToken,
}) = _WarpAccount;
factory WarpAccount.fromJson(Map<String, dynamic> json) =>
_$WarpAccountFromJson(json);
}

View File

@@ -13,6 +13,7 @@ import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart'; import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart'; import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart'; import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:loggy/loggy.dart'; import 'package:loggy/loggy.dart';
@@ -454,4 +455,44 @@ class FFISingboxService with InfraLogger implements SingboxService {
} }
return _logBuffer; return _logBuffer;
} }
@override
TaskEither<String, WarpAccount> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
}) {
loggy.debug("generating warp config");
return TaskEither(
() => CombineWorker().execute(
() {
final response = _box
.generateWarpConfig(
licenseKey.toNativeUtf8().cast(),
previousAccountId.toNativeUtf8().cast(),
previousAccessToken.toNativeUtf8().cast(),
)
.cast<Utf8>()
.toDartString();
if (response.startsWith("error:")) {
return left(response.replaceFirst('error:', ""));
}
if (jsonDecode(response)
case {
"account-id": final String newAccountId,
"access-token": final String newAccessToken,
}) {
return right(
WarpAccount(
licenseKey: licenseKey,
accountId: newAccountId,
accessToken: newAccessToken,
),
);
}
return left("invalid response");
},
),
);
}
} }

View File

@@ -8,6 +8,7 @@ import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart'; import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart'; import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart'; import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/singbox/service/singbox_service.dart';
import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/custom_loggers.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@@ -263,4 +264,39 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
}, },
); );
} }
@override
TaskEither<String, WarpAccount> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
}) {
return TaskEither(
() async {
loggy.debug("generating warp config");
final warpConfig = await methodChannel.invokeMethod(
"generate_warp_config",
{
"license-key": licenseKey,
"previous-account-id": previousAccountId,
"previous-access-token": previousAccessToken,
},
);
if (jsonDecode(warpConfig as String)
case {
"account-id": final String newAccountId,
"access-token": final String newAccessToken,
}) {
return right(
WarpAccount(
licenseKey: licenseKey,
accountId: newAccountId,
accessToken: newAccessToken,
),
);
}
return left("invalid response");
},
);
}
} }

View File

@@ -6,6 +6,7 @@ import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_outbound.dart'; import 'package:hiddify/singbox/model/singbox_outbound.dart';
import 'package:hiddify/singbox/model/singbox_stats.dart'; import 'package:hiddify/singbox/model/singbox_stats.dart';
import 'package:hiddify/singbox/model/singbox_status.dart'; import 'package:hiddify/singbox/model/singbox_status.dart';
import 'package:hiddify/singbox/model/warp_account.dart';
import 'package:hiddify/singbox/service/ffi_singbox_service.dart'; import 'package:hiddify/singbox/service/ffi_singbox_service.dart';
import 'package:hiddify/singbox/service/platform_singbox_service.dart'; import 'package:hiddify/singbox/service/platform_singbox_service.dart';
@@ -85,4 +86,10 @@ abstract interface class SingboxService {
Stream<List<String>> watchLogs(String path); Stream<List<String>> watchLogs(String path);
TaskEither<String, Unit> clearLogs(); TaskEither<String, Unit> clearLogs();
TaskEither<String, WarpAccount> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
});
} }

Submodule libcore updated: a006c94cdf...6672cd8104