new: add more warp modes, handle both ipv4 and ipv6 in wireguard, add customizable size and more

This commit is contained in:
hiddify-com
2024-07-18 01:03:15 +02:00
parent 9c424eac9a
commit 1cab9ef3a9
8 changed files with 101 additions and 48 deletions

View File

@@ -411,6 +411,8 @@
"warpCleanIp": "Clean IP", "warpCleanIp": "Clean IP",
"warpPort": "Port", "warpPort": "Port",
"warpNoise": "Noise Count", "warpNoise": "Noise Count",
"warpNoiseSize": "Noise Size",
"warpNoiseMode": "Noise Mode",
"warpNoiseDelay": "Noise Delay" "warpNoiseDelay": "Noise Delay"
} }
} }

View File

@@ -1 +1 @@
core.version=1.6.3 core.version=1.7.0

View File

@@ -271,6 +271,10 @@ abstract class ConfigOptions {
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true), mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
mapTo: const OptionalRangeJsonConverter().toJson, mapTo: const OptionalRangeJsonConverter().toJson,
); );
static final warpNoiseMode = PreferencesNotifier.create<String, String>(
"warp-noise-mode",
"m6",
);
static final warpNoiseDelay = PreferencesNotifier.create<OptionalRange, String>( static final warpNoiseDelay = PreferencesNotifier.create<OptionalRange, String>(
"warp-noise-delay", "warp-noise-delay",
@@ -278,6 +282,12 @@ abstract class ConfigOptions {
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true), mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
mapTo: const OptionalRangeJsonConverter().toJson, mapTo: const OptionalRangeJsonConverter().toJson,
); );
static final warpNoiseSize = PreferencesNotifier.create<OptionalRange, String>(
"warp-noise-size",
const OptionalRange(min: 10, max: 30),
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
mapTo: const OptionalRangeJsonConverter().toJson,
);
static final warpWireguardConfig = PreferencesNotifier.create<String, String>( static final warpWireguardConfig = PreferencesNotifier.create<String, String>(
"warp-wireguard-config", "warp-wireguard-config",
@@ -361,6 +371,8 @@ abstract class ConfigOptions {
"warp.clean-ip": warpCleanIp, "warp.clean-ip": warpCleanIp,
"warp.clean-port": warpPort, "warp.clean-port": warpPort,
"warp.noise": warpNoise, "warp.noise": warpNoise,
"warp.noise-size": warpNoiseSize,
"warp.noise-mode": warpNoiseMode,
"warp.noise-delay": warpNoiseDelay, "warp.noise-delay": warpNoiseDelay,
"warp.wireguard-config": warpWireguardConfig, "warp.wireguard-config": warpWireguardConfig,
"warp2.license-key": warp2LicenseKey, "warp2.license-key": warp2LicenseKey,
@@ -461,6 +473,8 @@ abstract class ConfigOptions {
cleanIp: ref.watch(warpCleanIp), cleanIp: ref.watch(warpCleanIp),
cleanPort: ref.watch(warpPort), cleanPort: ref.watch(warpPort),
noise: ref.watch(warpNoise), noise: ref.watch(warpNoise),
noiseMode: ref.watch(warpNoiseMode),
noiseSize: ref.watch(warpNoiseSize),
noiseDelay: ref.watch(warpNoiseDelay), noiseDelay: ref.watch(warpNoiseDelay),
), ),
warp2: SingboxWarpOption( warp2: SingboxWarpOption(
@@ -473,6 +487,8 @@ abstract class ConfigOptions {
cleanIp: ref.watch(warpCleanIp), cleanIp: ref.watch(warpCleanIp),
cleanPort: ref.watch(warpPort), cleanPort: ref.watch(warpPort),
noise: ref.watch(warpNoise), noise: ref.watch(warpNoise),
noiseMode: ref.watch(warpNoiseMode),
noiseSize: ref.watch(warpNoiseSize),
noiseDelay: ref.watch(warpNoiseDelay), noiseDelay: ref.watch(warpNoiseDelay),
), ),
// geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath( // geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath(

View File

@@ -113,6 +113,21 @@ class WarpOptionsTiles extends HookConsumerWidget {
presentValue: (value) => value.present(t), presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(), formatInputValue: (value) => value.format(),
), ),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.warpNoiseMode),
preferences: ref.watch(ConfigOptions.warpNoiseMode.notifier),
enabled: canChangeOptions,
title: t.config.warpNoiseMode,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.warpNoiseSize),
preferences: ref.watch(ConfigOptions.warpNoiseSize.notifier),
enabled: canChangeOptions,
title: t.config.warpNoiseSize,
inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true),
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
ValuePreferenceWidget( ValuePreferenceWidget(
value: ref.watch(ConfigOptions.warpNoiseDelay), value: ref.watch(ConfigOptions.warpNoiseDelay),
preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier), preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier),

View File

@@ -62,9 +62,7 @@ abstract interface class ProfileRepository {
TaskEither<ProfileFailure, Unit> deleteById(String id); TaskEither<ProfileFailure, Unit> deleteById(String id);
} }
class ProfileRepositoryImpl class ProfileRepositoryImpl with ExceptionHandler, InfraLogger implements ProfileRepository {
with ExceptionHandler, InfraLogger
implements ProfileRepository {
ProfileRepositoryImpl({ ProfileRepositoryImpl({
required this.profileDataSource, required this.profileDataSource,
required this.profilePathResolver, required this.profilePathResolver,
@@ -105,10 +103,7 @@ class ProfileRepositoryImpl
@override @override
Stream<Either<ProfileFailure, ProfileEntity?>> watchActiveProfile() { Stream<Either<ProfileFailure, ProfileEntity?>> watchActiveProfile() {
return profileDataSource return profileDataSource.watchActiveProfile().map((event) => event?.toEntity()).handleExceptions(
.watchActiveProfile()
.map((event) => event?.toEntity())
.handleExceptions(
(error, stackTrace) { (error, stackTrace) {
loggy.error("error watching active profile", error, stackTrace); loggy.error("error watching active profile", error, stackTrace);
return ProfileUnexpectedFailure(error, stackTrace); return ProfileUnexpectedFailure(error, stackTrace);
@@ -118,10 +113,7 @@ class ProfileRepositoryImpl
@override @override
Stream<Either<ProfileFailure, bool>> watchHasAnyProfile() { Stream<Either<ProfileFailure, bool>> watchHasAnyProfile() {
return profileDataSource return profileDataSource.watchProfilesCount().map((event) => event != 0).handleExceptions(ProfileUnexpectedFailure.new);
.watchProfilesCount()
.map((event) => event != 0)
.handleExceptions(ProfileUnexpectedFailure.new);
} }
@override @override
@@ -129,10 +121,7 @@ class ProfileRepositoryImpl
ProfilesSort sort = ProfilesSort.lastUpdate, ProfilesSort sort = ProfilesSort.lastUpdate,
SortMode sortMode = SortMode.ascending, SortMode sortMode = SortMode.ascending,
}) { }) {
return profileDataSource return profileDataSource.watchAll(sort: sort, sortMode: sortMode).map((event) => event.map((e) => e.toEntity()).toList()).handleExceptions(ProfileUnexpectedFailure.new);
.watchAll(sort: sort, sortMode: sortMode)
.map((event) => event.map((e) => e.toEntity()).toList())
.handleExceptions(ProfileUnexpectedFailure.new);
} }
@override @override
@@ -143,14 +132,10 @@ class ProfileRepositoryImpl
}) { }) {
return exceptionHandler( return exceptionHandler(
() async { () async {
final existingProfile = await profileDataSource final existingProfile = await profileDataSource.getByUrl(url).then((value) => value?.toEntity());
.getByUrl(url)
.then((value) => value?.toEntity());
if (existingProfile case RemoteProfileEntity()) { if (existingProfile case RemoteProfileEntity()) {
loggy.info("profile with same url already exists, updating"); loggy.info("profile with same url already exists, updating");
final baseProfile = markAsActive final baseProfile = markAsActive ? existingProfile.copyWith(active: true) : existingProfile;
? existingProfile.copyWith(active: true)
: existingProfile;
return updateSubscription( return updateSubscription(
baseProfile, baseProfile,
cancelToken: cancelToken, cancelToken: cancelToken,
@@ -163,9 +148,7 @@ class ProfileRepositoryImpl
(profile) => TaskEither( (profile) => TaskEither(
() async { () async {
await profileDataSource.insert( await profileDataSource.insert(
profile profile.copyWith(id: profileId, active: markAsActive).toEntry(),
.copyWith(id: profileId, active: markAsActive)
.toEntry(),
); );
return right(unit); return right(unit);
}, },
@@ -188,10 +171,7 @@ class ProfileRepositoryImpl
) { ) {
return exceptionHandler( return exceptionHandler(
() { () {
return singbox return singbox.validateConfigByPath(path, tempPath, debug).mapLeft(ProfileFailure.invalidConfig).run();
.validateConfigByPath(path, tempPath, debug)
.mapLeft(ProfileFailure.invalidConfig)
.run();
}, },
ProfileUnexpectedFailure.new, ProfileUnexpectedFailure.new,
); );
@@ -273,9 +253,7 @@ class ProfileRepositoryImpl
final configFile = profilePathResolver.file(id); final configFile = profilePathResolver.file(id);
// TODO pass options // TODO pass options
return await $( return await $(
singbox singbox.generateFullConfigByPath(configFile.path).mapLeft(ProfileFailure.unexpected),
.generateFullConfigByPath(configFile.path)
.mapLeft(ProfileFailure.unexpected),
); );
}, },
).handleExceptions(ProfileFailure.unexpected); ).handleExceptions(ProfileFailure.unexpected);
@@ -296,9 +274,7 @@ class ProfileRepositoryImpl
.flatMap( .flatMap(
(remoteProfile) => TaskEither( (remoteProfile) => TaskEither(
() async { () async {
final profilePatch = remoteProfile final profilePatch = remoteProfile.subInfoPatch().copyWith(lastUpdate: Value(DateTime.now()));
.subInfoPatch()
.copyWith(lastUpdate: Value(DateTime.now()));
await profileDataSource.edit( await profileDataSource.edit(
baseProfile.id, baseProfile.id,
@@ -306,8 +282,7 @@ class ProfileRepositoryImpl
? profilePatch.copyWith( ? profilePatch.copyWith(
name: Value(baseProfile.name), name: Value(baseProfile.name),
url: Value(baseProfile.url), url: Value(baseProfile.url),
updateInterval: updateInterval: Value(baseProfile.options?.updateInterval),
Value(baseProfile.options?.updateInterval),
) )
: profilePatch, : profilePatch,
); );
@@ -393,8 +368,7 @@ class ProfileRepositoryImpl
tempFile.path, tempFile.path,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
final headers = final headers = await _populateHeaders(response.headers.map, tempFile.path);
await _populateHeaders(response.headers.map, tempFile.path);
return await validateConfig(file.path, tempFile.path, false) return await validateConfig(file.path, tempFile.path, false)
.andThen( .andThen(
() => TaskEither(() async { () => TaskEither(() async {
@@ -444,15 +418,9 @@ class ProfileRepositoryImpl
if (line.startsWith("#") || line.startsWith("//")) { if (line.startsWith("#") || line.startsWith("//")) {
final index = line.indexOf(':'); final index = line.indexOf(':');
if (index == -1) continue; if (index == -1) continue;
final key = line final key = line.substring(0, index).replaceFirst(RegExp("^#|//"), "").trim().toLowerCase();
.substring(0, index)
.replaceFirst(RegExp("^#|//"), "")
.trim()
.toLowerCase();
final value = line.substring(index + 1).trim(); final value = line.substring(index + 1).trim();
if (!headers.keys.contains(key) && if (!headers.keys.contains(key) && _subInfoHeaders.contains(key) && value.isNotEmpty) {
_subInfoHeaders.contains(key) &&
value.isNotEmpty) {
headers[key] = [value]; headers[key] = [value];
} }
} }

View File

@@ -1,10 +1,17 @@
import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/notification/in_app_notification_controller.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart';
import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart';
import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart';
import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart';
@@ -53,6 +60,7 @@ class AddProfile extends _$AddProfile with AppLogger {
Future<void> add(String rawInput) async { Future<void> add(String rawInput) async {
if (state.isLoading) return; if (state.isLoading) return;
state = const AsyncLoading(); state = const AsyncLoading();
// await check4Warp(rawInput);
state = await AsyncValue.guard( state = await AsyncValue.guard(
() async { () async {
final activeProfile = await ref.read(activeProfileProvider.future); final activeProfile = await ref.read(activeProfileProvider.future);
@@ -100,6 +108,48 @@ class AddProfile extends _$AddProfile with AppLogger {
}, },
); );
} }
Future<void> check4Warp(String rawInput) async {
for (final line in rawInput.split("\n")) {
if (line.toLowerCase().startsWith("warp://")) {
final _prefs = ref.read(sharedPreferencesProvider).requireValue;
final _warp = ref.read(warpOptionNotifierProvider.notifier);
final consent = false && (_prefs.getBool(WarpOptionNotifier.warpConsentGiven) ?? false);
final t = ref.read(translationsProvider);
final notification = ref.read(inAppNotificationControllerProvider);
if (!consent) {
final agreed = await showDialog<bool>(
context: RootScaffold.stateKey.currentContext!,
builder: (context) => const WarpLicenseAgreementModal(),
);
if (agreed ?? false) {
await _prefs.setBool(WarpOptionNotifier.warpConsentGiven, true);
final toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100));
toast?.pause();
await _warp.generateWarpConfig();
toast?.start();
} else {
return;
}
}
final accountId = _prefs.getString("warp2-account-id");
final accessToken = _prefs.getString("warp2-access-token");
final hasWarp2Config = accountId != null && accessToken != null;
if (!hasWarp2Config || true) {
final toast = notification.showInfoToast(t.profile.add.addingWarpMsg, duration: const Duration(milliseconds: 100));
toast?.pause();
await _warp.generateWarp2Config();
toast?.start();
}
}
}
}
} }
@riverpod @riverpod

View File

@@ -74,7 +74,9 @@ class SingboxWarpOption with _$SingboxWarpOption {
required String cleanIp, required String cleanIp,
required int cleanPort, required int cleanPort,
@OptionalRangeJsonConverter() required OptionalRange noise, @OptionalRangeJsonConverter() required OptionalRange noise,
@OptionalRangeJsonConverter() required OptionalRange noiseSize,
@OptionalRangeJsonConverter() required OptionalRange noiseDelay, @OptionalRangeJsonConverter() required OptionalRange noiseDelay,
@OptionalRangeJsonConverter() required String noiseMode,
}) = _SingboxWarpOption; }) = _SingboxWarpOption;
factory SingboxWarpOption.fromJson(Map<String, dynamic> json) => _$SingboxWarpOptionFromJson(json); factory SingboxWarpOption.fromJson(Map<String, dynamic> json) => _$SingboxWarpOptionFromJson(json);

Submodule libcore updated: 96fafad01d...e378a83c26