diff --git a/lib/core/utils/executable_manager.dart b/lib/core/utils/executable_manager.dart index 66c59505..51ea08a4 100644 --- a/lib/core/utils/executable_manager.dart +++ b/lib/core/utils/executable_manager.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:fl_lib/fl_lib.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:server_box/data/store/setting.dart'; /// Exception thrown when executable management fails class ExecutableException implements Exception { @@ -28,6 +30,8 @@ class ExecutableInfo { abstract final class ExecutableManager { static const String _executablesDirName = 'executables'; static late final Directory _executablesDir; + static final Map _customExecutables = {}; + static bool _customExecutablesLoaded = false; static Future initialize() async { final appDir = await getApplicationSupportDirectory(); @@ -35,6 +39,8 @@ abstract final class ExecutableManager { if (!await _executablesDir.exists()) { await _executablesDir.create(recursive: true); } + + _ensureCustomExecutablesLoaded(); } /// Predefined executables @@ -45,6 +51,52 @@ abstract final class ExecutableManager { 'socat': ExecutableInfo(name: 'socat'), }; + static void _ensureCustomExecutablesLoaded() { + if (_customExecutablesLoaded) return; + + final List stored = SettingStore.instance.proxyCmdCustomExecs.get(); + for (final raw in stored) { + final info = _parseExecutableInfo(raw); + if (info == null) continue; + _customExecutables[info.name] = info; + _predefinedExecutables[info.name] = info; + } + + _customExecutablesLoaded = true; + } + + static void _persistCustomExecutables() { + final values = _customExecutables.values + .map((info) => { + 'name': info.name, + if (info.spokenName != null) 'spokenName': info.spokenName, + if (info.version != null) 'version': info.version, + }) + .toList(); + SettingStore.instance.proxyCmdCustomExecs.set(values); + } + + static ExecutableInfo? _parseExecutableInfo(dynamic raw) { + if (raw is String) { + try { + return _parseExecutableInfo(jsonDecode(raw)); + } catch (e) { + Loggers.app.warning('Failed to decode custom executable entry: $e'); + return null; + } + } + if (raw is! Map) return null; + + final name = raw['name']?.toString(); + if (name == null || name.isEmpty) return null; + + return ExecutableInfo( + name: name, + spokenName: raw['spokenName']?.toString(), + version: raw['version']?.toString(), + ); + } + /// Check if an executable exists in PATH or local directory static Future isExecutableAvailable(String name) async { // First check if it's in PATH @@ -224,18 +276,27 @@ abstract final class ExecutableManager { /// Get predefined executable info static ExecutableInfo? getExecutableInfo(String name) { + _ensureCustomExecutablesLoaded(); return _predefinedExecutables[name]; } /// Add a custom executable definition static void addCustomExecutable(String name, ExecutableInfo info) { - // TODO: Implement persistent storage for custom executables + _ensureCustomExecutablesLoaded(); + _customExecutables[name] = info; + _predefinedExecutables[name] = info; + _persistCustomExecutables(); Loggers.app.info('Adding custom executable: $name'); } /// Remove a custom executable definition static void removeCustomExecutable(String name) { - // TODO: Implement persistent storage for custom executables - Loggers.app.info('Removing custom executable: $name'); + _ensureCustomExecutablesLoaded(); + final removed = _customExecutables.remove(name); + if (removed != null) { + _predefinedExecutables.remove(name); + _persistCustomExecutables(); + Loggers.app.info('Removing custom executable: $name'); + } } } diff --git a/lib/core/utils/proxy_command_executor.dart b/lib/core/utils/proxy_command_executor.dart index 62e51999..daa5a8ee 100644 --- a/lib/core/utils/proxy_command_executor.dart +++ b/lib/core/utils/proxy_command_executor.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:dartssh2/dartssh2.dart'; @@ -6,6 +7,7 @@ import 'package:fl_lib/fl_lib.dart'; import 'package:server_box/core/utils/executable_manager.dart'; import 'package:server_box/core/utils/proxy_socket.dart'; import 'package:server_box/data/model/server/proxy_command_config.dart'; +import 'package:server_box/data/store/setting.dart'; /// Exception thrown when proxy command execution fails class ProxyCommandException implements Exception { @@ -26,6 +28,58 @@ class ProxyCommandException implements Exception { /// Generic proxy command executor that handles SSH ProxyCommand functionality abstract final class ProxyCommandExecutor { + static final Map _customPresets = {}; + static bool _customPresetsLoaded = false; + + static void _ensureCustomPresetsLoaded() { + if (_customPresetsLoaded) return; + + final List stored = SettingStore.instance.proxyCmdCustomPresets.get(); + for (final raw in stored) { + final preset = _parsePreset(raw); + if (preset == null) continue; + _customPresets[preset.key] = preset.value; + } + + _customPresetsLoaded = true; + } + + static void _persistCustomPresets() { + final list = _customPresets.entries + .map((entry) => { + 'name': entry.key, + 'config': entry.value.toJson(), + }) + .toList(); + SettingStore.instance.proxyCmdCustomPresets.set(list); + } + + static MapEntry? _parsePreset(dynamic raw) { + dynamic payload = raw; + if (payload is String) { + try { + payload = jsonDecode(payload); + } catch (e) { + Loggers.app.warning('Failed to decode custom proxy preset entry: $e'); + return null; + } + } + + if (payload is! Map) return null; + + final name = payload['name']?.toString(); + final configRaw = payload['config']; + if (name == null || name.isEmpty || configRaw is! Map) return null; + + try { + final config = ProxyCommandConfig.fromJson(Map.from(configRaw)); + return MapEntry(name, config); + } catch (e) { + Loggers.app.warning('Failed to parse custom proxy preset "$name": $e'); + return null; + } + } + /// Execute a proxy command and return a socket connected through the proxy static Future executeProxyCommand( ProxyCommandConfig config, { @@ -124,10 +178,18 @@ abstract final class ProxyCommandExecutor { return 'Proxy command must contain %h (hostname) placeholder'; } - // If executable is required, check if it exists + String executablePath; + + // If executable is required, check if it exists and reuse resolved path if (config.requiresExecutable && config.executableName != null) { try { - await ExecutableManager.ensureExecutable(config.executableName!); + executablePath = await ExecutableManager.ensureExecutable(config.executableName!); + } catch (e) { + return e.toString(); + } + } else { + try { + executablePath = await ExecutableManager.getExecutablePath(tokens.first); } catch (e) { return e.toString(); } @@ -135,7 +197,7 @@ abstract final class ProxyCommandExecutor { // Try to validate command syntax (dry run) try { - await Process.run(tokens.first, ['--help']); + await Process.run(executablePath, ['--help']); } catch (e) { return 'Command validation failed: $e'; } @@ -145,20 +207,29 @@ abstract final class ProxyCommandExecutor { /// Get available proxy command presets static Map getPresets() { - return proxyCommandPresets; + _ensureCustomPresetsLoaded(); + return { + ...proxyCommandPresets, + ..._customPresets, + }; } /// Add a custom preset static Future addCustomPreset(String name, ProxyCommandConfig config) async { - // TODO: Implement custom preset storage - // This would involve storing custom presets in a persistent storage + _ensureCustomPresetsLoaded(); + _customPresets[name] = config; + _persistCustomPresets(); Loggers.app.info('Adding custom proxy preset: $name'); } /// Remove a custom preset static Future removeCustomPreset(String name) async { - // TODO: Implement custom preset removal - Loggers.app.info('Removing custom proxy preset: $name'); + _ensureCustomPresetsLoaded(); + final removed = _customPresets.remove(name); + if (removed != null) { + _persistCustomPresets(); + Loggers.app.info('Removing custom proxy preset: $name'); + } } static List tokenizeCommand(String command) => _tokenizeCommand(command); diff --git a/lib/data/model/server/proxy_command_config.dart b/lib/data/model/server/proxy_command_config.dart index 88adc195..c571b014 100644 --- a/lib/data/model/server/proxy_command_config.dart +++ b/lib/data/model/server/proxy_command_config.dart @@ -1,5 +1,4 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hive_ce_flutter/adapters.dart'; part 'proxy_command_config.freezed.dart'; part 'proxy_command_config.g.dart'; diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 0f127879..bfb07e12 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -228,6 +228,10 @@ class SettingStore extends HiveStore { late final betaTest = propertyDefault('betaTest', false); + late final proxyCmdCustomExecs = listProperty('proxyCmdCustomExecs'); + + late final proxyCmdCustomPresets = listProperty('proxyCmdCustomPresets'); + /// For desktop only. /// Record the position and size of the window. late final windowState = property(