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.MethodChannel
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.mobile.Mobile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -30,6 +31,7 @@ class MethodHandler(private val scope: CoroutineScope) : FlutterPlugin,
SelectOutbound("select_outbound"),
UrlTest("url_test"),
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()
}
}

View File

@@ -237,6 +237,8 @@
"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)}."
},
"generateWarpConfig": "Generate WARP config",
"missingWarpConfig": "Missing WARP config",
"pageTitle": "Config Options",
"logLevel": "Log Level",
"resolveDestination": "Resolve Destination",

View File

@@ -1,6 +1,7 @@
import 'package:hiddify/core/preferences/preferences_provider.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/singbox/service/singbox_service_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'config_option_data_providers.g.dart';
@@ -11,6 +12,7 @@ ConfigOptionRepository configOptionRepository(
) {
return ConfigOptionRepositoryImpl(
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/singbox/model/singbox_config_option.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:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -17,6 +18,7 @@ abstract interface class ConfigOptionRepository {
ConfigOptionPatch patch,
);
TaskEither<ConfigOptionFailure, Unit> resetConfigOption();
TaskEither<ConfigOptionFailure, Unit> generateWarpConfig();
}
abstract interface class SingBoxConfigOptionRepository {
@@ -27,9 +29,13 @@ abstract interface class SingBoxConfigOptionRepository {
class ConfigOptionRepositoryImpl
with ExceptionHandler, InfraLogger
implements ConfigOptionRepository {
ConfigOptionRepositoryImpl({required this.preferences});
ConfigOptionRepositoryImpl({
required this.preferences,
required this.singbox,
});
final SharedPreferences preferences;
final SingboxService singbox;
@override
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

View File

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

View File

@@ -14,6 +14,9 @@ sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure {
StackTrace? stackTrace,
]) = ConfigOptionUnexpectedFailure;
@With<ExpectedFailure>()
const factory ConfigOptionFailure.missingWarp() = MissingWarpConfigFailure;
@override
({String type, String? message}) present(TranslationsEn t) {
return switch (this) {
@@ -21,6 +24,10 @@ sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure {
type: t.failure.unexpected,
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/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:shared_preferences/shared_preferences.dart';
part 'warp_option_notifier.freezed.dart';
part 'warp_option_notifier.g.dart';
@Riverpod(keepAlive: true)
class WarpOptionNotifier extends _$WarpOptionNotifier {
class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
@override
bool build() {
return ref
.read(sharedPreferencesProvider)
.requireValue
.getBool(warpConsentGiven) ??
false;
WarpOptions build() {
final consent = _prefs.getBool(warpConsentGiven) ?? false;
bool hasWarpConfig = false;
try {
final accountId = _prefs.getString("warp-account-id");
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 {
await ref
.read(sharedPreferencesProvider)
.requireValue
.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";
}
@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) {
final t = ref.watch(translationsProvider);
final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider);
final warpOptions = ref.watch(warpOptionNotifierProvider);
final warpPrefaceCompleted = warpOptions.consentGiven;
final canChangeOptions = warpPrefaceCompleted && options.enableWarp;
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(
title: Text(t.settings.config.warpDetourMode),
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 WarpDetourMode warpDetourMode,
required String warpLicenseKey,
required String warpAccountId,
required String warpAccessToken,
required String warpCleanIp,
required int warpPort,
@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_stats.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/utils/utils.dart';
import 'package:loggy/loggy.dart';
@@ -454,4 +455,44 @@ class FFISingboxService with InfraLogger implements SingboxService {
}
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_stats.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/utils/custom_loggers.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_stats.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/platform_singbox_service.dart';
@@ -85,4 +86,10 @@ abstract interface class SingboxService {
Stream<List<String>> watchLogs(String path);
TaskEither<String, Unit> clearLogs();
TaskEither<String, WarpAccount> generateWarpConfig({
required String licenseKey,
required String previousAccountId,
required String previousAccessToken,
});
}

Submodule libcore updated: a006c94cdf...6672cd8104