mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
241
lib/core/utils/executable_manager.dart
Normal file
241
lib/core/utils/executable_manager.dart
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Exception thrown when executable management fails
|
||||||
|
class ExecutableException implements Exception {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
ExecutableException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ExecutableException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about an executable
|
||||||
|
class ExecutableInfo {
|
||||||
|
final String name;
|
||||||
|
final String? spokenName;
|
||||||
|
final String? version;
|
||||||
|
|
||||||
|
ExecutableInfo({required this.name, this.version, this.spokenName});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic executable manager for downloading and managing external tools
|
||||||
|
abstract final class ExecutableManager {
|
||||||
|
static const String _executablesDirName = 'executables';
|
||||||
|
static late final Directory _executablesDir;
|
||||||
|
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
final appDir = await getApplicationSupportDirectory();
|
||||||
|
_executablesDir = Directory(path.join(appDir.path, _executablesDirName));
|
||||||
|
if (!await _executablesDir.exists()) {
|
||||||
|
await _executablesDir.create(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Predefined executables
|
||||||
|
static final Map<String, ExecutableInfo> _predefinedExecutables = {
|
||||||
|
'cloudflared': ExecutableInfo(name: 'cloudflared'),
|
||||||
|
'ssh': ExecutableInfo(name: 'ssh'),
|
||||||
|
'nc': ExecutableInfo(name: 'nc'),
|
||||||
|
'socat': ExecutableInfo(name: 'socat'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Check if an executable exists in PATH or local directory
|
||||||
|
static Future<bool> isExecutableAvailable(String name) async {
|
||||||
|
// First check if it's in PATH
|
||||||
|
final pathExecutable = await _lookupExecutableInSystemPath(name);
|
||||||
|
if (pathExecutable != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check local executables directory
|
||||||
|
final localExecutable = _getLocalExecutablePath(name);
|
||||||
|
if (await localExecutable.exists()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path to an executable (either in PATH or local)
|
||||||
|
static Future<String> getExecutablePath(String name) async {
|
||||||
|
// First check if it's in PATH
|
||||||
|
final pathExecutable = await _lookupExecutableInSystemPath(name);
|
||||||
|
if (pathExecutable != null) {
|
||||||
|
return pathExecutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check local executables directory
|
||||||
|
final localExecutable = _getLocalExecutablePath(name);
|
||||||
|
if (await localExecutable.exists()) {
|
||||||
|
return localExecutable.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ExecutableException('Executable $name not found in PATH or local directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download an executable if it's not available
|
||||||
|
static Future<String> ensureExecutable(String name) async {
|
||||||
|
if (await isExecutableAvailable(name)) {
|
||||||
|
return await getExecutablePath(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getExecutablePath(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a local executable
|
||||||
|
static Future<void> removeExecutable(String name) async {
|
||||||
|
final localExecutable = _getLocalExecutablePath(name);
|
||||||
|
if (await localExecutable.exists()) {
|
||||||
|
await localExecutable.delete();
|
||||||
|
Loggers.app.info('Removed local executable: $name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all locally downloaded executables
|
||||||
|
static Future<List<String>> listLocalExecutables() async {
|
||||||
|
if (!await _executablesDir.exists()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final executables = <String>[];
|
||||||
|
await for (final entity in _executablesDir.list()) {
|
||||||
|
if (entity is File && _isExecutable(entity)) {
|
||||||
|
executables.add(path.basenameWithoutExtension(entity.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return executables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the size of a local executable
|
||||||
|
static Future<int> getExecutableSize(String name) async {
|
||||||
|
final localExecutable = _getLocalExecutablePath(name);
|
||||||
|
if (await localExecutable.exists()) {
|
||||||
|
return await localExecutable.length();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the version of an executable
|
||||||
|
static Future<String?> getExecutableVersion(String name) async {
|
||||||
|
try {
|
||||||
|
final executablePath = await getExecutablePath(name);
|
||||||
|
|
||||||
|
// Try common version flags
|
||||||
|
final versionFlags = ['--version', '-v', '-V', 'version'];
|
||||||
|
|
||||||
|
for (final flag in versionFlags) {
|
||||||
|
try {
|
||||||
|
final result = await Process.run(executablePath, [flag]);
|
||||||
|
if (result.exitCode == 0) {
|
||||||
|
final output = result.stdout.toString().trim();
|
||||||
|
if (output.isNotEmpty) {
|
||||||
|
return output.split('\n').first; // Return first line only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Try next flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('Error getting version for $name: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate an executable by trying to run it with a help flag
|
||||||
|
static Future<bool> validateExecutable(String name) async {
|
||||||
|
try {
|
||||||
|
final executablePath = await getExecutablePath(name);
|
||||||
|
|
||||||
|
// Try to run the executable with a help flag
|
||||||
|
final helpFlags = ['--help', '-h', '-help'];
|
||||||
|
|
||||||
|
for (final flag in helpFlags) {
|
||||||
|
try {
|
||||||
|
final result = await Process.run(executablePath, [flag]);
|
||||||
|
if (result.exitCode == 0 || result.exitCode == 1) {
|
||||||
|
// Help often returns 1
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Try next flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('Error validating $name: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String?> _lookupExecutableInSystemPath(String name) async {
|
||||||
|
final command = Platform.isWindows ? 'where' : 'which';
|
||||||
|
try {
|
||||||
|
final result = await Process.run(command, [name]);
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final stdoutString = result.stdout.toString().trim();
|
||||||
|
if (stdoutString.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final candidate = stdoutString
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.firstWhere((line) => line.isNotEmpty, orElse: () => '');
|
||||||
|
|
||||||
|
if (candidate.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('Error checking PATH for $name: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the local path for an executable
|
||||||
|
static File _getLocalExecutablePath(String name) {
|
||||||
|
final extension = Platform.isWindows ? '.exe' : '';
|
||||||
|
return File(path.join(_executablesDir.path, '$name$extension'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a file is executable
|
||||||
|
static bool _isExecutable(File file) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
return file.path.endsWith('.exe');
|
||||||
|
} else {
|
||||||
|
// Check file permissions
|
||||||
|
final stat = file.statSync();
|
||||||
|
return (stat.mode & 0x111) != 0; // Check execute bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get predefined executable info
|
||||||
|
static ExecutableInfo? getExecutableInfo(String name) {
|
||||||
|
return _predefinedExecutables[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom executable definition
|
||||||
|
static void addCustomExecutable(String name, ExecutableInfo info) {
|
||||||
|
// TODO: Implement persistent storage for custom executables
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
148
lib/core/utils/proxy_command_executor.dart
Normal file
148
lib/core/utils/proxy_command_executor.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
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';
|
||||||
|
|
||||||
|
/// Exception thrown when proxy command execution fails
|
||||||
|
class ProxyCommandException implements Exception {
|
||||||
|
final String message;
|
||||||
|
final int? exitCode;
|
||||||
|
final String? stdout;
|
||||||
|
final String? stderr;
|
||||||
|
|
||||||
|
ProxyCommandException({required this.message, this.exitCode, this.stdout, this.stderr});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProxyCommandException: $message'
|
||||||
|
'${exitCode != null ? ' (exit code: $exitCode)' : ''}'
|
||||||
|
'${stderr != null ? '\nStderr: $stderr' : ''}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generic proxy command executor that handles SSH ProxyCommand functionality
|
||||||
|
abstract final class ProxyCommandExecutor {
|
||||||
|
/// Execute a proxy command and return a socket connected through the proxy
|
||||||
|
static Future<SSHSocket> executeProxyCommand(
|
||||||
|
ProxyCommandConfig config, {
|
||||||
|
required String hostname,
|
||||||
|
required int port,
|
||||||
|
required String user,
|
||||||
|
}) async {
|
||||||
|
final finalCommand = config.getFinalCommand(hostname: hostname, port: port, user: user);
|
||||||
|
|
||||||
|
Loggers.app.info('Executing proxy command: $finalCommand');
|
||||||
|
|
||||||
|
// Ensure executable is available if required
|
||||||
|
String executablePath;
|
||||||
|
if (config.requiresExecutable && config.executableName != null) {
|
||||||
|
executablePath = await ExecutableManager.ensureExecutable(config.executableName!);
|
||||||
|
} else {
|
||||||
|
// Parse command to get executable
|
||||||
|
final parts = finalCommand.split(' ');
|
||||||
|
final executable = parts.first;
|
||||||
|
executablePath = await ExecutableManager.getExecutablePath(executable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command and arguments
|
||||||
|
final parts = finalCommand.split(' ');
|
||||||
|
final args = parts.skip(1).toList();
|
||||||
|
|
||||||
|
// Set up environment
|
||||||
|
final environment = {...Platform.environment, ...?config.environment};
|
||||||
|
|
||||||
|
// Start the process
|
||||||
|
Process process;
|
||||||
|
try {
|
||||||
|
process = await Process.start(
|
||||||
|
executablePath,
|
||||||
|
args,
|
||||||
|
workingDirectory: config.workingDirectory,
|
||||||
|
environment: environment,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw ProxyCommandException(message: 'Failed to start proxy command: $e', exitCode: -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up timeout handling
|
||||||
|
var timedOut = false;
|
||||||
|
final timeoutTimer = Timer(config.timeout, () {
|
||||||
|
timedOut = true;
|
||||||
|
process.kill();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For ProxyCommand, we create a ProxySocket that wraps the process
|
||||||
|
final socket = ProxySocket(process);
|
||||||
|
|
||||||
|
// Monitor the process for immediate failures
|
||||||
|
unawaited(
|
||||||
|
process.exitCode.then((code) {
|
||||||
|
if (code != 0 && !socket.closed && !timedOut) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
} catch (e) {
|
||||||
|
process.kill();
|
||||||
|
rethrow;
|
||||||
|
} finally {
|
||||||
|
timeoutTimer.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate proxy command configuration
|
||||||
|
static Future<String?> validateConfig(ProxyCommandConfig config) async {
|
||||||
|
final testCommand = config.getFinalCommand(hostname: 'test.example.com', port: 22, user: 'testuser');
|
||||||
|
|
||||||
|
// Check if required placeholders are present
|
||||||
|
if (!config.command.contains('%h')) {
|
||||||
|
return 'Proxy command must contain %h (hostname) placeholder';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If executable is required, check if it exists
|
||||||
|
if (config.requiresExecutable && config.executableName != null) {
|
||||||
|
try {
|
||||||
|
await ExecutableManager.ensureExecutable(config.executableName!);
|
||||||
|
} catch (e) {
|
||||||
|
return e.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to validate command syntax (dry run)
|
||||||
|
final parts = testCommand.split(' ');
|
||||||
|
final executable = parts.first;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Process.run(executable, ['--help'], runInShell: true);
|
||||||
|
} catch (e) {
|
||||||
|
return 'Command validation failed: $e';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // No error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get available proxy command presets
|
||||||
|
static Map<String, ProxyCommandConfig> getPresets() {
|
||||||
|
return proxyCommandPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a custom preset
|
||||||
|
static Future<void> addCustomPreset(String name, ProxyCommandConfig config) async {
|
||||||
|
// TODO: Implement custom preset storage
|
||||||
|
// This would involve storing custom presets in a persistent storage
|
||||||
|
Loggers.app.info('Adding custom proxy preset: $name');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a custom preset
|
||||||
|
static Future<void> removeCustomPreset(String name) async {
|
||||||
|
// TODO: Implement custom preset removal
|
||||||
|
Loggers.app.info('Removing custom proxy preset: $name');
|
||||||
|
}
|
||||||
|
}
|
||||||
125
lib/core/utils/proxy_socket.dart
Normal file
125
lib/core/utils/proxy_socket.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:dartssh2/dartssh2.dart';
|
||||||
|
import 'package:fl_lib/fl_lib.dart';
|
||||||
|
|
||||||
|
/// Socket implementation that communicates through a Process stdin/stdout
|
||||||
|
/// This is used for ProxyCommand functionality where the SSH connection
|
||||||
|
/// is proxied through an external command
|
||||||
|
class ProxySocket implements SSHSocket {
|
||||||
|
final Process _process;
|
||||||
|
final StreamController<Uint8List> _incomingController =
|
||||||
|
StreamController<Uint8List>();
|
||||||
|
final StreamController<List<int>> _outgoingController =
|
||||||
|
StreamController<List<int>>();
|
||||||
|
final Completer<void> _doneCompleter = Completer<void>();
|
||||||
|
|
||||||
|
bool _closed = false;
|
||||||
|
late StreamSubscription<Uint8List> _stdoutSubscription;
|
||||||
|
late StreamSubscription<Uint8List> _stderrSubscription;
|
||||||
|
|
||||||
|
ProxySocket(this._process) {
|
||||||
|
// Set up stdout reading
|
||||||
|
_stdoutSubscription = _process.stdout
|
||||||
|
.transform(Uint8ListStreamTransformer())
|
||||||
|
.listen(_onIncomingData,
|
||||||
|
onError: _onError,
|
||||||
|
onDone: _onProcessDone,
|
||||||
|
cancelOnError: true);
|
||||||
|
|
||||||
|
// Set up stderr reading (for logging)
|
||||||
|
_stderrSubscription = _process.stderr
|
||||||
|
.transform(Uint8ListStreamTransformer())
|
||||||
|
.listen((data) {
|
||||||
|
Loggers.app.warning('Proxy stderr: ${String.fromCharCodes(data)}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up outgoing data
|
||||||
|
_outgoingController.stream.listen(_onOutgoingData);
|
||||||
|
|
||||||
|
// Handle process exit
|
||||||
|
_process.exitCode.then((code) {
|
||||||
|
if (!_closed && code != 0) {
|
||||||
|
_onError('Proxy process exited with code: $code');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> get stream => _incomingController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
StreamSink<List<int>> get sink => _outgoingController.sink;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> get done => _doneCompleter.future;
|
||||||
|
|
||||||
|
/// Check if the socket is closed
|
||||||
|
bool get closed => _closed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
if (_closed) return;
|
||||||
|
_closed = true;
|
||||||
|
|
||||||
|
await _stdoutSubscription.cancel();
|
||||||
|
await _stderrSubscription.cancel();
|
||||||
|
await _outgoingController.close();
|
||||||
|
await _incomingController.close();
|
||||||
|
|
||||||
|
if (!_doneCompleter.isCompleted) {
|
||||||
|
_doneCompleter.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the process if it's still running
|
||||||
|
try {
|
||||||
|
_process.kill();
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('Error killing proxy process: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void destroy() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onIncomingData(Uint8List data) {
|
||||||
|
if (!_closed) {
|
||||||
|
_incomingController.add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOutgoingData(List<int> data) {
|
||||||
|
if (!_closed) {
|
||||||
|
_process.stdin.add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onError(dynamic error, [StackTrace? stackTrace]) {
|
||||||
|
if (!_closed) {
|
||||||
|
_incomingController.addError(error, stackTrace);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onProcessDone() {
|
||||||
|
if (!_closed) {
|
||||||
|
_incomingController.close();
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformer to convert `Stream<List<int>>` to `Stream<Uint8List>`
|
||||||
|
class Uint8ListStreamTransformer
|
||||||
|
extends StreamTransformerBase<List<int>, Uint8List> {
|
||||||
|
const Uint8ListStreamTransformer();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Uint8List> bind(Stream<List<int>> stream) {
|
||||||
|
return stream.map((data) => Uint8List.fromList(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:server_box/core/app_navigator.dart';
|
import 'package:server_box/core/app_navigator.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
|
import 'package:server_box/core/utils/proxy_command_executor.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/res/store.dart';
|
import 'package:server_box/data/res/store.dart';
|
||||||
@@ -68,8 +69,7 @@ Future<SSHClient> genClient(
|
|||||||
|
|
||||||
String? alterUser;
|
String? alterUser;
|
||||||
|
|
||||||
final socket = await () async {
|
// Check for Jump Server first - this needs special handling
|
||||||
// Proxy
|
|
||||||
final jumpSpi_ = () {
|
final jumpSpi_ = () {
|
||||||
// Multi-thread or key login
|
// Multi-thread or key login
|
||||||
if (jumpSpi != null) return jumpSpi;
|
if (jumpSpi != null) return jumpSpi;
|
||||||
@@ -77,6 +77,7 @@ Future<SSHClient> genClient(
|
|||||||
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
if (spi.jumpId != null) return Stores.server.box.get(spi.jumpId);
|
||||||
}();
|
}();
|
||||||
if (jumpSpi_ != null) {
|
if (jumpSpi_ != null) {
|
||||||
|
// For jump server, we establish connection through the jump client
|
||||||
final jumpClient = await genClient(
|
final jumpClient = await genClient(
|
||||||
jumpSpi_,
|
jumpSpi_,
|
||||||
privateKey: jumpPrivateKey,
|
privateKey: jumpPrivateKey,
|
||||||
@@ -86,25 +87,82 @@ Future<SSHClient> genClient(
|
|||||||
onHostKeyPrompt: onHostKeyPrompt,
|
onHostKeyPrompt: onHostKeyPrompt,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await jumpClient.forwardLocal(spi.ip, spi.port);
|
final forwardChannel = await jumpClient.forwardLocal(spi.ip, spi.port);
|
||||||
|
|
||||||
|
final hostKeyVerifier = _HostKeyVerifier(
|
||||||
|
spi: spi,
|
||||||
|
cache: hostKeyCache,
|
||||||
|
persistCallback: hostKeyPersist,
|
||||||
|
prompt: hostKeyPrompt,
|
||||||
|
);
|
||||||
|
|
||||||
|
final keyId = spi.keyId;
|
||||||
|
if (keyId == null) {
|
||||||
|
onStatus?.call(GenSSHClientStatus.pwd);
|
||||||
|
return SSHClient(
|
||||||
|
forwardChannel,
|
||||||
|
username: spi.user,
|
||||||
|
onPasswordRequest: () => spi.pwd,
|
||||||
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey ??= getPrivateKey(keyId);
|
||||||
|
onStatus?.call(GenSSHClientStatus.key);
|
||||||
|
return SSHClient(
|
||||||
|
forwardChannel,
|
||||||
|
username: spi.user,
|
||||||
|
identities: await compute(loadIndentity, privateKey),
|
||||||
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct
|
// For ProxyCommand and direct connections, get SSHSocket
|
||||||
|
SSHSocket? socket;
|
||||||
try {
|
try {
|
||||||
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
// ProxyCommand support - Check for ProxyCommand configuration first
|
||||||
} catch (e) {
|
if (spi.proxyCommand != null) {
|
||||||
Loggers.app.warning('genClient', e);
|
try {
|
||||||
if (spi.alterUrl == null) rethrow;
|
Loggers.app.info('Connecting via ProxyCommand: ${spi.proxyCommand!.command}');
|
||||||
try {
|
socket = await ProxyCommandExecutor.executeProxyCommand(
|
||||||
final res = spi.parseAlterUrl();
|
spi.proxyCommand!,
|
||||||
alterUser = res.$2;
|
hostname: spi.ip,
|
||||||
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
port: spi.port,
|
||||||
} catch (e) {
|
user: spi.user,
|
||||||
Loggers.app.warning('genClient alterUrl', e);
|
);
|
||||||
rethrow;
|
} catch (e) {
|
||||||
|
Loggers.app.warning('ProxyCommand failed', e);
|
||||||
|
if (!spi.proxyCommand!.retryOnFailure) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
// If retry is enabled, fall through to direct connection
|
||||||
|
Loggers.app.info('ProxyCommand failed, falling back to direct connection');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct connection (or fallback)
|
||||||
|
socket ??= await () async {
|
||||||
|
try {
|
||||||
|
return await SSHSocket.connect(spi.ip, spi.port, timeout: timeout);
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('genClient', e);
|
||||||
|
if (spi.alterUrl == null) rethrow;
|
||||||
|
try {
|
||||||
|
final res = spi.parseAlterUrl();
|
||||||
|
alterUser = res.$2;
|
||||||
|
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('genClient alterUrl', e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
} catch (e) {
|
||||||
|
Loggers.app.warning('Failed to establish connection', e);
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}();
|
|
||||||
|
|
||||||
final hostKeyVerifier = _HostKeyVerifier(
|
final hostKeyVerifier = _HostKeyVerifier(
|
||||||
spi: spi,
|
spi: spi,
|
||||||
@@ -122,8 +180,6 @@ Future<SSHClient> genClient(
|
|||||||
onPasswordRequest: () => spi.pwd,
|
onPasswordRequest: () => spi.pwd,
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
onVerifyHostKey: hostKeyVerifier.call,
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
// printDebug: debugPrint,
|
|
||||||
// printTrace: debugPrint,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
privateKey ??= getPrivateKey(keyId);
|
privateKey ??= getPrivateKey(keyId);
|
||||||
@@ -132,12 +188,9 @@ Future<SSHClient> genClient(
|
|||||||
return SSHClient(
|
return SSHClient(
|
||||||
socket,
|
socket,
|
||||||
username: spi.user,
|
username: spi.user,
|
||||||
// Must use [compute] here, instead of [Computer.shared.start]
|
|
||||||
identities: await compute(loadIndentity, privateKey),
|
identities: await compute(loadIndentity, privateKey),
|
||||||
onUserInfoRequest: onKeyboardInteractive,
|
onUserInfoRequest: onKeyboardInteractive,
|
||||||
onVerifyHostKey: hostKeyVerifier.call,
|
onVerifyHostKey: hostKeyVerifier.call,
|
||||||
// printDebug: debugPrint,
|
|
||||||
// printTrace: debugPrint,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,11 +149,28 @@ abstract final class SSHConfig {
|
|||||||
|
|
||||||
/// Extract jump host from ProxyJump or ProxyCommand
|
/// Extract jump host from ProxyJump or ProxyCommand
|
||||||
static String? _extractJumpHost(String value) {
|
static String? _extractJumpHost(String value) {
|
||||||
// For ProxyJump, the format is usually: user@host:port
|
// Normalize whitespace
|
||||||
// For ProxyCommand, it's more complex and might need custom parsing
|
final parts = value.trim().split(RegExp(r'\s+'));
|
||||||
if (value.contains('@')) {
|
|
||||||
return value.split(' ').first;
|
// Try to find a token that looks like a user@host[:port]
|
||||||
|
// This covers common patterns like:
|
||||||
|
// - ProxyJump user@host
|
||||||
|
// - ProxyCommand ssh -W %h:%p user@host
|
||||||
|
for (final token in parts) {
|
||||||
|
if (token.contains('@')) {
|
||||||
|
// Strip any surrounding quotes just in case
|
||||||
|
var cleaned = token;
|
||||||
|
if ((cleaned.startsWith("'") && cleaned.endsWith("'")) ||
|
||||||
|
(cleaned.startsWith('"') && cleaned.endsWith('"'))) {
|
||||||
|
cleaned = cleaned.substring(1, cleaned.length - 1);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyJump may also be provided as just a hostname (no user@)
|
||||||
|
// In that case we don't have enough information to build an oldId-style reference,
|
||||||
|
// so we ignore it here and let the user configure a jump server manually.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
lib/data/model/server/proxy_command_config.dart
Normal file
74
lib/data/model/server/proxy_command_config.dart
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'proxy_command_config.freezed.dart';
|
||||||
|
part 'proxy_command_config.g.dart';
|
||||||
|
|
||||||
|
/// ProxyCommand configuration for SSH connections
|
||||||
|
@freezed
|
||||||
|
abstract class ProxyCommandConfig with _$ProxyCommandConfig {
|
||||||
|
const factory ProxyCommandConfig({
|
||||||
|
/// Command template with placeholders
|
||||||
|
/// Available placeholders: %h (hostname), %p (port), %r (user)
|
||||||
|
required String command,
|
||||||
|
|
||||||
|
/// Command arguments (optional, can be included in command)
|
||||||
|
List<String>? args,
|
||||||
|
|
||||||
|
/// Working directory for the command
|
||||||
|
String? workingDirectory,
|
||||||
|
|
||||||
|
/// Environment variables for the command
|
||||||
|
Map<String, String>? environment,
|
||||||
|
|
||||||
|
/// Timeout for command execution
|
||||||
|
@Default(Duration(seconds: 30)) Duration timeout,
|
||||||
|
|
||||||
|
/// Whether to retry on connection failure
|
||||||
|
@Default(false) bool retryOnFailure,
|
||||||
|
|
||||||
|
/// Maximum retry attempts
|
||||||
|
@Default(3) int maxRetries,
|
||||||
|
|
||||||
|
/// Whether the proxy command requires executable download
|
||||||
|
@Default(false) bool requiresExecutable,
|
||||||
|
|
||||||
|
/// Executable name for download management
|
||||||
|
String? executableName,
|
||||||
|
|
||||||
|
/// Executable download URL
|
||||||
|
String? executableDownloadUrl,
|
||||||
|
}) = _ProxyCommandConfig;
|
||||||
|
|
||||||
|
factory ProxyCommandConfig.fromJson(Map<String, dynamic> json) => _$ProxyCommandConfigFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common proxy command presets
|
||||||
|
const Map<String, ProxyCommandConfig> proxyCommandPresets = {
|
||||||
|
'cloudflare_access': ProxyCommandConfig(
|
||||||
|
command: 'cloudflared access ssh --hostname %h',
|
||||||
|
requiresExecutable: true,
|
||||||
|
executableName: 'cloudflared',
|
||||||
|
timeout: Duration(seconds: 15),
|
||||||
|
),
|
||||||
|
'ssh_via_bastion': ProxyCommandConfig(
|
||||||
|
command: 'ssh -W %h:%p bastion.example.com',
|
||||||
|
timeout: Duration(seconds: 10),
|
||||||
|
),
|
||||||
|
'nc_netcat': ProxyCommandConfig(command: 'nc %h %p', timeout: Duration(seconds: 10)),
|
||||||
|
'socat': ProxyCommandConfig(
|
||||||
|
command: 'socat - PROXY:%h:%p,proxyport=8080',
|
||||||
|
timeout: Duration(seconds: 10),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Extension for ProxyCommandConfig to add utility methods
|
||||||
|
extension ProxyCommandConfigExtension on ProxyCommandConfig {
|
||||||
|
/// Get the final command with placeholders replaced
|
||||||
|
String getFinalCommand({required String hostname, required int port, required String user}) {
|
||||||
|
var finalCommand = command;
|
||||||
|
finalCommand = finalCommand.replaceAll('%h', hostname);
|
||||||
|
finalCommand = finalCommand.replaceAll('%p', port.toString());
|
||||||
|
finalCommand = finalCommand.replaceAll('%r', user);
|
||||||
|
return finalCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
344
lib/data/model/server/proxy_command_config.freezed.dart
Normal file
344
lib/data/model/server/proxy_command_config.freezed.dart
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'proxy_command_config.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ProxyCommandConfig {
|
||||||
|
|
||||||
|
/// Command template with placeholders
|
||||||
|
/// Available placeholders: %h (hostname), %p (port), %r (user)
|
||||||
|
String get command;/// Command arguments (optional, can be included in command)
|
||||||
|
List<String>? get args;/// Working directory for the command
|
||||||
|
String? get workingDirectory;/// Environment variables for the command
|
||||||
|
Map<String, String>? get environment;/// Timeout for command execution
|
||||||
|
Duration get timeout;/// Whether to retry on connection failure
|
||||||
|
bool get retryOnFailure;/// Maximum retry attempts
|
||||||
|
int get maxRetries;/// Whether the proxy command requires executable download
|
||||||
|
bool get requiresExecutable;/// Executable name for download management
|
||||||
|
String? get executableName;/// Executable download URL
|
||||||
|
String? get executableDownloadUrl;
|
||||||
|
/// Create a copy of ProxyCommandConfig
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ProxyCommandConfigCopyWith<ProxyCommandConfig> get copyWith => _$ProxyCommandConfigCopyWithImpl<ProxyCommandConfig>(this as ProxyCommandConfig, _$identity);
|
||||||
|
|
||||||
|
/// Serializes this ProxyCommandConfig to a JSON map.
|
||||||
|
Map<String, dynamic> toJson();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyCommandConfig&&(identical(other.command, command) || other.command == command)&&const DeepCollectionEquality().equals(other.args, args)&&(identical(other.workingDirectory, workingDirectory) || other.workingDirectory == workingDirectory)&&const DeepCollectionEquality().equals(other.environment, environment)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.retryOnFailure, retryOnFailure) || other.retryOnFailure == retryOnFailure)&&(identical(other.maxRetries, maxRetries) || other.maxRetries == maxRetries)&&(identical(other.requiresExecutable, requiresExecutable) || other.requiresExecutable == requiresExecutable)&&(identical(other.executableName, executableName) || other.executableName == executableName)&&(identical(other.executableDownloadUrl, executableDownloadUrl) || other.executableDownloadUrl == executableDownloadUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,command,const DeepCollectionEquality().hash(args),workingDirectory,const DeepCollectionEquality().hash(environment),timeout,retryOnFailure,maxRetries,requiresExecutable,executableName,executableDownloadUrl);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProxyCommandConfig(command: $command, args: $args, workingDirectory: $workingDirectory, environment: $environment, timeout: $timeout, retryOnFailure: $retryOnFailure, maxRetries: $maxRetries, requiresExecutable: $requiresExecutable, executableName: $executableName, executableDownloadUrl: $executableDownloadUrl)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ProxyCommandConfigCopyWith<$Res> {
|
||||||
|
factory $ProxyCommandConfigCopyWith(ProxyCommandConfig value, $Res Function(ProxyCommandConfig) _then) = _$ProxyCommandConfigCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ProxyCommandConfigCopyWithImpl<$Res>
|
||||||
|
implements $ProxyCommandConfigCopyWith<$Res> {
|
||||||
|
_$ProxyCommandConfigCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final ProxyCommandConfig _self;
|
||||||
|
final $Res Function(ProxyCommandConfig) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ProxyCommandConfig
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? command = null,Object? args = freezed,Object? workingDirectory = freezed,Object? environment = freezed,Object? timeout = null,Object? retryOnFailure = null,Object? maxRetries = null,Object? requiresExecutable = null,Object? executableName = freezed,Object? executableDownloadUrl = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,args: freezed == args ? _self.args : args // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,workingDirectory: freezed == workingDirectory ? _self.workingDirectory : workingDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,environment: freezed == environment ? _self.environment : environment // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, String>?,timeout: null == timeout ? _self.timeout : timeout // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,retryOnFailure: null == retryOnFailure ? _self.retryOnFailure : retryOnFailure // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,maxRetries: null == maxRetries ? _self.maxRetries : maxRetries // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,requiresExecutable: null == requiresExecutable ? _self.requiresExecutable : requiresExecutable // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,executableName: freezed == executableName ? _self.executableName : executableName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,executableDownloadUrl: freezed == executableDownloadUrl ? _self.executableDownloadUrl : executableDownloadUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [ProxyCommandConfig].
|
||||||
|
extension ProxyCommandConfigPatterns on ProxyCommandConfig {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProxyCommandConfig value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProxyCommandConfig value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig():
|
||||||
|
return $default(_that);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProxyCommandConfig value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig() when $default != null:
|
||||||
|
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig():
|
||||||
|
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProxyCommandConfig() when $default != null:
|
||||||
|
return $default(_that.command,_that.args,_that.workingDirectory,_that.environment,_that.timeout,_that.retryOnFailure,_that.maxRetries,_that.requiresExecutable,_that.executableName,_that.executableDownloadUrl);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
|
||||||
|
class _ProxyCommandConfig implements ProxyCommandConfig {
|
||||||
|
const _ProxyCommandConfig({required this.command, final List<String>? args, this.workingDirectory, final Map<String, String>? environment, this.timeout = const Duration(seconds: 30), this.retryOnFailure = false, this.maxRetries = 3, this.requiresExecutable = false, this.executableName, this.executableDownloadUrl}): _args = args,_environment = environment;
|
||||||
|
factory _ProxyCommandConfig.fromJson(Map<String, dynamic> json) => _$ProxyCommandConfigFromJson(json);
|
||||||
|
|
||||||
|
/// Command template with placeholders
|
||||||
|
/// Available placeholders: %h (hostname), %p (port), %r (user)
|
||||||
|
@override final String command;
|
||||||
|
/// Command arguments (optional, can be included in command)
|
||||||
|
final List<String>? _args;
|
||||||
|
/// Command arguments (optional, can be included in command)
|
||||||
|
@override List<String>? get args {
|
||||||
|
final value = _args;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_args is EqualUnmodifiableListView) return _args;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableListView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Working directory for the command
|
||||||
|
@override final String? workingDirectory;
|
||||||
|
/// Environment variables for the command
|
||||||
|
final Map<String, String>? _environment;
|
||||||
|
/// Environment variables for the command
|
||||||
|
@override Map<String, String>? get environment {
|
||||||
|
final value = _environment;
|
||||||
|
if (value == null) return null;
|
||||||
|
if (_environment is EqualUnmodifiableMapView) return _environment;
|
||||||
|
// ignore: implicit_dynamic_type
|
||||||
|
return EqualUnmodifiableMapView(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout for command execution
|
||||||
|
@override@JsonKey() final Duration timeout;
|
||||||
|
/// Whether to retry on connection failure
|
||||||
|
@override@JsonKey() final bool retryOnFailure;
|
||||||
|
/// Maximum retry attempts
|
||||||
|
@override@JsonKey() final int maxRetries;
|
||||||
|
/// Whether the proxy command requires executable download
|
||||||
|
@override@JsonKey() final bool requiresExecutable;
|
||||||
|
/// Executable name for download management
|
||||||
|
@override final String? executableName;
|
||||||
|
/// Executable download URL
|
||||||
|
@override final String? executableDownloadUrl;
|
||||||
|
|
||||||
|
/// Create a copy of ProxyCommandConfig
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ProxyCommandConfigCopyWith<_ProxyCommandConfig> get copyWith => __$ProxyCommandConfigCopyWithImpl<_ProxyCommandConfig>(this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$ProxyCommandConfigToJson(this, );
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyCommandConfig&&(identical(other.command, command) || other.command == command)&&const DeepCollectionEquality().equals(other._args, _args)&&(identical(other.workingDirectory, workingDirectory) || other.workingDirectory == workingDirectory)&&const DeepCollectionEquality().equals(other._environment, _environment)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.retryOnFailure, retryOnFailure) || other.retryOnFailure == retryOnFailure)&&(identical(other.maxRetries, maxRetries) || other.maxRetries == maxRetries)&&(identical(other.requiresExecutable, requiresExecutable) || other.requiresExecutable == requiresExecutable)&&(identical(other.executableName, executableName) || other.executableName == executableName)&&(identical(other.executableDownloadUrl, executableDownloadUrl) || other.executableDownloadUrl == executableDownloadUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,command,const DeepCollectionEquality().hash(_args),workingDirectory,const DeepCollectionEquality().hash(_environment),timeout,retryOnFailure,maxRetries,requiresExecutable,executableName,executableDownloadUrl);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProxyCommandConfig(command: $command, args: $args, workingDirectory: $workingDirectory, environment: $environment, timeout: $timeout, retryOnFailure: $retryOnFailure, maxRetries: $maxRetries, requiresExecutable: $requiresExecutable, executableName: $executableName, executableDownloadUrl: $executableDownloadUrl)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ProxyCommandConfigCopyWith<$Res> implements $ProxyCommandConfigCopyWith<$Res> {
|
||||||
|
factory _$ProxyCommandConfigCopyWith(_ProxyCommandConfig value, $Res Function(_ProxyCommandConfig) _then) = __$ProxyCommandConfigCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String command, List<String>? args, String? workingDirectory, Map<String, String>? environment, Duration timeout, bool retryOnFailure, int maxRetries, bool requiresExecutable, String? executableName, String? executableDownloadUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ProxyCommandConfigCopyWithImpl<$Res>
|
||||||
|
implements _$ProxyCommandConfigCopyWith<$Res> {
|
||||||
|
__$ProxyCommandConfigCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _ProxyCommandConfig _self;
|
||||||
|
final $Res Function(_ProxyCommandConfig) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ProxyCommandConfig
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? command = null,Object? args = freezed,Object? workingDirectory = freezed,Object? environment = freezed,Object? timeout = null,Object? retryOnFailure = null,Object? maxRetries = null,Object? requiresExecutable = null,Object? executableName = freezed,Object? executableDownloadUrl = freezed,}) {
|
||||||
|
return _then(_ProxyCommandConfig(
|
||||||
|
command: null == command ? _self.command : command // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,args: freezed == args ? _self._args : args // ignore: cast_nullable_to_non_nullable
|
||||||
|
as List<String>?,workingDirectory: freezed == workingDirectory ? _self.workingDirectory : workingDirectory // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,environment: freezed == environment ? _self._environment : environment // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Map<String, String>?,timeout: null == timeout ? _self.timeout : timeout // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Duration,retryOnFailure: null == retryOnFailure ? _self.retryOnFailure : retryOnFailure // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,maxRetries: null == maxRetries ? _self.maxRetries : maxRetries // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,requiresExecutable: null == requiresExecutable ? _self.requiresExecutable : requiresExecutable // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,executableName: freezed == executableName ? _self.executableName : executableName // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,executableDownloadUrl: freezed == executableDownloadUrl ? _self.executableDownloadUrl : executableDownloadUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
39
lib/data/model/server/proxy_command_config.g.dart
Normal file
39
lib/data/model/server/proxy_command_config.g.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'proxy_command_config.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
_ProxyCommandConfig _$ProxyCommandConfigFromJson(Map<String, dynamic> json) =>
|
||||||
|
_ProxyCommandConfig(
|
||||||
|
command: json['command'] as String,
|
||||||
|
args: (json['args'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||||
|
workingDirectory: json['workingDirectory'] as String?,
|
||||||
|
environment: (json['environment'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, e as String),
|
||||||
|
),
|
||||||
|
timeout: json['timeout'] == null
|
||||||
|
? const Duration(seconds: 30)
|
||||||
|
: Duration(microseconds: (json['timeout'] as num).toInt()),
|
||||||
|
retryOnFailure: json['retryOnFailure'] as bool? ?? false,
|
||||||
|
maxRetries: (json['maxRetries'] as num?)?.toInt() ?? 3,
|
||||||
|
requiresExecutable: json['requiresExecutable'] as bool? ?? false,
|
||||||
|
executableName: json['executableName'] as String?,
|
||||||
|
executableDownloadUrl: json['executableDownloadUrl'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ProxyCommandConfigToJson(_ProxyCommandConfig instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'command': instance.command,
|
||||||
|
'args': instance.args,
|
||||||
|
'workingDirectory': instance.workingDirectory,
|
||||||
|
'environment': instance.environment,
|
||||||
|
'timeout': instance.timeout.inMicroseconds,
|
||||||
|
'retryOnFailure': instance.retryOnFailure,
|
||||||
|
'maxRetries': instance.maxRetries,
|
||||||
|
'requiresExecutable': instance.requiresExecutable,
|
||||||
|
'executableName': instance.executableName,
|
||||||
|
'executableDownloadUrl': instance.executableDownloadUrl,
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import 'package:fl_lib/fl_lib.dart';
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:server_box/data/model/app/error.dart';
|
import 'package:server_box/data/model/app/error.dart';
|
||||||
import 'package:server_box/data/model/server/custom.dart';
|
import 'package:server_box/data/model/server/custom.dart';
|
||||||
|
import 'package:server_box/data/model/server/proxy_command_config.dart';
|
||||||
import 'package:server_box/data/model/server/system.dart';
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||||
import 'package:server_box/data/store/server.dart';
|
import 'package:server_box/data/store/server.dart';
|
||||||
@@ -45,14 +46,18 @@ abstract class Spi with _$Spi {
|
|||||||
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
|
@Default('') @JsonKey(fromJson: Spi.parseId) String id,
|
||||||
|
|
||||||
/// Custom system type (unix or windows). If set, skip auto-detection.
|
/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
@JsonKey(includeIfNull: false) SystemType? customSystemType,
|
SystemType? customSystemType,
|
||||||
|
|
||||||
/// Disabled command types for this server
|
/// Disabled command types for this server
|
||||||
@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes,
|
List<String>? disabledCmdTypes,
|
||||||
|
|
||||||
|
/// ProxyCommand configuration for SSH connections
|
||||||
|
ProxyCommandConfig? proxyCommand,
|
||||||
}) = _Spi;
|
}) = _Spi;
|
||||||
|
|
||||||
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
factory Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'Spi<$oldId>';
|
String toString() => 'Spi<$oldId>';
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ mixin _$Spi {
|
|||||||
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
@JsonKey(name: 'pubKeyId') String? get keyId; List<String>? get tags; String? get alterUrl; bool get autoConnect;/// [id] of the jump server
|
||||||
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
String? get jumpId; ServerCustom? get custom; WakeOnLanCfg? get wolCfg;/// It only applies to SSH terminal.
|
||||||
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
Map<String, String>? get envs;@JsonKey(fromJson: Spi.parseId) String get id;/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
@JsonKey(includeIfNull: false) SystemType? get customSystemType;/// Disabled command types for this server
|
SystemType? get customSystemType;/// Disabled command types for this server
|
||||||
@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes;
|
List<String>? get disabledCmdTypes;/// ProxyCommand configuration for SSH connections
|
||||||
|
ProxyCommandConfig? get proxyCommand;
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -33,12 +34,12 @@ $SpiCopyWith<Spi> get copyWith => _$SpiCopyWithImpl<Spi>(this as Spi, _$identity
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other.tags, tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other.envs, envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other.disabledCmdTypes, disabledCmdTypes)&&(identical(other.proxyCommand, proxyCommand) || other.proxyCommand == proxyCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(envs),id,customSystemType,const DeepCollectionEquality().hash(disabledCmdTypes),proxyCommand);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -49,11 +50,11 @@ abstract mixin class $SpiCopyWith<$Res> {
|
|||||||
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
factory $SpiCopyWith(Spi value, $Res Function(Spi) _then) = _$SpiCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -66,7 +67,7 @@ class _$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,Object? proxyCommand = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -84,10 +85,23 @@ as WakeOnLanCfg?,envs: freezed == envs ? _self.envs : envs // ignore: cast_nulla
|
|||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||||
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self.disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as List<String>?,proxyCommand: freezed == proxyCommand ? _self.proxyCommand : proxyCommand // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ProxyCommandConfig?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
/// Create a copy of Spi
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand {
|
||||||
|
if (_self.proxyCommand == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ProxyCommandConfigCopyWith<$Res>(_self.proxyCommand!, (value) {
|
||||||
|
return _then(_self.copyWith(proxyCommand: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -169,10 +183,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -190,10 +204,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi():
|
case _Spi():
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
|
||||||
throw StateError('Unexpected subclass');
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -210,10 +224,10 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, @JsonKey(includeIfNull: false) SystemType? customSystemType, @JsonKey(includeIfNull: false) List<String>? disabledCmdTypes)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String name, String ip, int port, String user, String? pwd, @JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _Spi() when $default != null:
|
case _Spi() when $default != null:
|
||||||
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes);case _:
|
return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,_that.tags,_that.alterUrl,_that.autoConnect,_that.jumpId,_that.custom,_that.wolCfg,_that.envs,_that.id,_that.customSystemType,_that.disabledCmdTypes,_that.proxyCommand);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -225,7 +239,7 @@ return $default(_that.name,_that.ip,_that.port,_that.user,_that.pwd,_that.keyId,
|
|||||||
|
|
||||||
@JsonSerializable(includeIfNull: false)
|
@JsonSerializable(includeIfNull: false)
|
||||||
class _Spi extends Spi {
|
class _Spi extends Spi {
|
||||||
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', @JsonKey(includeIfNull: false) this.customSystemType, @JsonKey(includeIfNull: false) final List<String>? disabledCmdTypes}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
const _Spi({required this.name, required this.ip, required this.port, required this.user, this.pwd, @JsonKey(name: 'pubKeyId') this.keyId, final List<String>? tags, this.alterUrl, this.autoConnect = true, this.jumpId, this.custom, this.wolCfg, final Map<String, String>? envs, @JsonKey(fromJson: Spi.parseId) this.id = '', this.customSystemType, final List<String>? disabledCmdTypes, this.proxyCommand}): _tags = tags,_envs = envs,_disabledCmdTypes = disabledCmdTypes,super._();
|
||||||
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
factory _Spi.fromJson(Map<String, dynamic> json) => _$SpiFromJson(json);
|
||||||
|
|
||||||
@override final String name;
|
@override final String name;
|
||||||
@@ -263,11 +277,11 @@ class _Spi extends Spi {
|
|||||||
|
|
||||||
@override@JsonKey(fromJson: Spi.parseId) final String id;
|
@override@JsonKey(fromJson: Spi.parseId) final String id;
|
||||||
/// Custom system type (unix or windows). If set, skip auto-detection.
|
/// Custom system type (unix or windows). If set, skip auto-detection.
|
||||||
@override@JsonKey(includeIfNull: false) final SystemType? customSystemType;
|
@override final SystemType? customSystemType;
|
||||||
/// Disabled command types for this server
|
/// Disabled command types for this server
|
||||||
final List<String>? _disabledCmdTypes;
|
final List<String>? _disabledCmdTypes;
|
||||||
/// Disabled command types for this server
|
/// Disabled command types for this server
|
||||||
@override@JsonKey(includeIfNull: false) List<String>? get disabledCmdTypes {
|
@override List<String>? get disabledCmdTypes {
|
||||||
final value = _disabledCmdTypes;
|
final value = _disabledCmdTypes;
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
|
if (_disabledCmdTypes is EqualUnmodifiableListView) return _disabledCmdTypes;
|
||||||
@@ -275,6 +289,8 @@ class _Spi extends Spi {
|
|||||||
return EqualUnmodifiableListView(value);
|
return EqualUnmodifiableListView(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ProxyCommand configuration for SSH connections
|
||||||
|
@override final ProxyCommandConfig? proxyCommand;
|
||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -289,12 +305,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Spi&&(identical(other.name, name) || other.name == name)&&(identical(other.ip, ip) || other.ip == ip)&&(identical(other.port, port) || other.port == port)&&(identical(other.user, user) || other.user == user)&&(identical(other.pwd, pwd) || other.pwd == pwd)&&(identical(other.keyId, keyId) || other.keyId == keyId)&&const DeepCollectionEquality().equals(other._tags, _tags)&&(identical(other.alterUrl, alterUrl) || other.alterUrl == alterUrl)&&(identical(other.autoConnect, autoConnect) || other.autoConnect == autoConnect)&&(identical(other.jumpId, jumpId) || other.jumpId == jumpId)&&(identical(other.custom, custom) || other.custom == custom)&&(identical(other.wolCfg, wolCfg) || other.wolCfg == wolCfg)&&const DeepCollectionEquality().equals(other._envs, _envs)&&(identical(other.id, id) || other.id == id)&&(identical(other.customSystemType, customSystemType) || other.customSystemType == customSystemType)&&const DeepCollectionEquality().equals(other._disabledCmdTypes, _disabledCmdTypes)&&(identical(other.proxyCommand, proxyCommand) || other.proxyCommand == proxyCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes));
|
int get hashCode => Object.hash(runtimeType,name,ip,port,user,pwd,keyId,const DeepCollectionEquality().hash(_tags),alterUrl,autoConnect,jumpId,custom,wolCfg,const DeepCollectionEquality().hash(_envs),id,customSystemType,const DeepCollectionEquality().hash(_disabledCmdTypes),proxyCommand);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -305,11 +321,11 @@ abstract mixin class _$SpiCopyWith<$Res> implements $SpiCopyWith<$Res> {
|
|||||||
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
factory _$SpiCopyWith(_Spi value, $Res Function(_Spi) _then) = __$SpiCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id,@JsonKey(includeIfNull: false) SystemType? customSystemType,@JsonKey(includeIfNull: false) List<String>? disabledCmdTypes
|
String name, String ip, int port, String user, String? pwd,@JsonKey(name: 'pubKeyId') String? keyId, List<String>? tags, String? alterUrl, bool autoConnect, String? jumpId, ServerCustom? custom, WakeOnLanCfg? wolCfg, Map<String, String>? envs,@JsonKey(fromJson: Spi.parseId) String id, SystemType? customSystemType, List<String>? disabledCmdTypes, ProxyCommandConfig? proxyCommand
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $ProxyCommandConfigCopyWith<$Res>? get proxyCommand;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -322,7 +338,7 @@ class __$SpiCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of Spi
|
/// Create a copy of Spi
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? ip = null,Object? port = null,Object? user = null,Object? pwd = freezed,Object? keyId = freezed,Object? tags = freezed,Object? alterUrl = freezed,Object? autoConnect = null,Object? jumpId = freezed,Object? custom = freezed,Object? wolCfg = freezed,Object? envs = freezed,Object? id = null,Object? customSystemType = freezed,Object? disabledCmdTypes = freezed,Object? proxyCommand = freezed,}) {
|
||||||
return _then(_Spi(
|
return _then(_Spi(
|
||||||
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
|
||||||
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
as String,ip: null == ip ? _self.ip : ip // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -340,11 +356,24 @@ as WakeOnLanCfg?,envs: freezed == envs ? _self._envs : envs // ignore: cast_null
|
|||||||
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, String>?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
as String,customSystemType: freezed == customSystemType ? _self.customSystemType : customSystemType // ignore: cast_nullable_to_non_nullable
|
||||||
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
as SystemType?,disabledCmdTypes: freezed == disabledCmdTypes ? _self._disabledCmdTypes : disabledCmdTypes // ignore: cast_nullable_to_non_nullable
|
||||||
as List<String>?,
|
as List<String>?,proxyCommand: freezed == proxyCommand ? _self.proxyCommand : proxyCommand // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ProxyCommandConfig?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of Spi
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ProxyCommandConfigCopyWith<$Res>? get proxyCommand {
|
||||||
|
if (_self.proxyCommand == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ProxyCommandConfigCopyWith<$Res>(_self.proxyCommand!, (value) {
|
||||||
|
return _then(_self.copyWith(proxyCommand: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dart format on
|
// dart format on
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ _Spi _$SpiFromJson(Map<String, dynamic> json) => _Spi(
|
|||||||
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
|
disabledCmdTypes: (json['disabledCmdTypes'] as List<dynamic>?)
|
||||||
?.map((e) => e as String)
|
?.map((e) => e as String)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
proxyCommand: json['proxyCommand'] == null
|
||||||
|
? null
|
||||||
|
: ProxyCommandConfig.fromJson(
|
||||||
|
json['proxyCommand'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
||||||
@@ -53,6 +58,7 @@ Map<String, dynamic> _$SpiToJson(_Spi instance) => <String, dynamic>{
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'customSystemType': ?_$SystemTypeEnumMap[instance.customSystemType],
|
'customSystemType': ?_$SystemTypeEnumMap[instance.customSystemType],
|
||||||
'disabledCmdTypes': ?instance.disabledCmdTypes,
|
'disabledCmdTypes': ?instance.disabledCmdTypes,
|
||||||
|
'proxyCommand': instance.proxyCommand?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$SystemTypeEnumMap = {
|
const _$SystemTypeEnumMap = {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ final class ContainerNotifierProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$containerNotifierHash() => r'fea65e66499234b0a59bffff8d69c4ab8c93b2fd';
|
String _$containerNotifierHash() => r'e6ced8a914631253daabe0de452e0338078cd1d9';
|
||||||
|
|
||||||
final class ContainerNotifierFamily extends $Family
|
final class ContainerNotifierFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -113,13 +113,14 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
id: fields[13] == null ? '' : fields[13] as String,
|
id: fields[13] == null ? '' : fields[13] as String,
|
||||||
customSystemType: fields[14] as SystemType?,
|
customSystemType: fields[14] as SystemType?,
|
||||||
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
|
disabledCmdTypes: (fields[15] as List?)?.cast<String>(),
|
||||||
|
proxyCommand: fields[16] as ProxyCommandConfig?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, Spi obj) {
|
void write(BinaryWriter writer, Spi obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(16)
|
..writeByte(17)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.name)
|
..write(obj.name)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -151,7 +152,9 @@ class SpiAdapter extends TypeAdapter<Spi> {
|
|||||||
..writeByte(14)
|
..writeByte(14)
|
||||||
..write(obj.customSystemType)
|
..write(obj.customSystemType)
|
||||||
..writeByte(15)
|
..writeByte(15)
|
||||||
..write(obj.disabledCmdTypes);
|
..write(obj.disabledCmdTypes)
|
||||||
|
..writeByte(16)
|
||||||
|
..write(obj.proxyCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ types:
|
|||||||
index: 4
|
index: 4
|
||||||
Spi:
|
Spi:
|
||||||
typeId: 3
|
typeId: 3
|
||||||
nextIndex: 16
|
nextIndex: 17
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
index: 0
|
index: 0
|
||||||
@@ -61,6 +61,8 @@ types:
|
|||||||
index: 14
|
index: 14
|
||||||
disabledCmdTypes:
|
disabledCmdTypes:
|
||||||
index: 15
|
index: 15
|
||||||
|
proxyCommand:
|
||||||
|
index: 16
|
||||||
VirtKey:
|
VirtKey:
|
||||||
typeId: 4
|
typeId: 4
|
||||||
nextIndex: 45
|
nextIndex: 45
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:server_box/app.dart';
|
import 'package:server_box/app.dart';
|
||||||
|
import 'package:server_box/core/utils/executable_manager.dart';
|
||||||
import 'package:server_box/data/model/app/menu/server_func.dart';
|
import 'package:server_box/data/model/app/menu/server_func.dart';
|
||||||
import 'package:server_box/data/model/app/server_detail_card.dart';
|
import 'package:server_box/data/model/app/server_detail_card.dart';
|
||||||
import 'package:server_box/data/res/build_data.dart';
|
import 'package:server_box/data/res/build_data.dart';
|
||||||
@@ -57,6 +58,9 @@ Future<void> _initData() async {
|
|||||||
await PrefStore.shared.init(); // Call this before accessing any store
|
await PrefStore.shared.init(); // Call this before accessing any store
|
||||||
await Stores.init();
|
await Stores.init();
|
||||||
|
|
||||||
|
// Initialize executable manager
|
||||||
|
await ExecutableManager.initialize();
|
||||||
|
|
||||||
// It may effect the following logic, so await it.
|
// It may effect the following logic, so await it.
|
||||||
// DO DB migration before load any provider.
|
// DO DB migration before load any provider.
|
||||||
await _doDbMigrate();
|
await _doDbMigrate();
|
||||||
|
|||||||
@@ -265,6 +265,35 @@ extension _Actions on _ServerEditPageState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProxyCommand configuration
|
||||||
|
ProxyCommandConfig? proxyCommand;
|
||||||
|
if (_proxyCommandEnabled.value) {
|
||||||
|
final command = _proxyCommandController.text.trim();
|
||||||
|
if (command.isEmpty) {
|
||||||
|
context.showSnackBar('ProxyCommand is enabled but command is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if command contains required placeholders
|
||||||
|
if (!command.contains('%h')) {
|
||||||
|
context.showSnackBar('ProxyCommand must contain %h (hostname) placeholder');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this requires an executable
|
||||||
|
final parts = command.split(' ');
|
||||||
|
final executable = parts.first;
|
||||||
|
final requiresExecutable = !['ssh', 'nc', 'socat'].contains(executable);
|
||||||
|
|
||||||
|
proxyCommand = ProxyCommandConfig(
|
||||||
|
command: command,
|
||||||
|
timeout: Duration(seconds: _proxyCommandTimeout.value),
|
||||||
|
retryOnFailure: true,
|
||||||
|
requiresExecutable: requiresExecutable,
|
||||||
|
executableName: requiresExecutable ? executable : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final spi = Spi(
|
final spi = Spi(
|
||||||
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
|
name: _nameController.text.isEmpty ? _ipController.text : _nameController.text,
|
||||||
ip: _ipController.text,
|
ip: _ipController.text,
|
||||||
@@ -284,6 +313,7 @@ extension _Actions on _ServerEditPageState {
|
|||||||
id: widget.args?.spi.id ?? ShortId.generate(),
|
id: widget.args?.spi.id ?? ShortId.generate(),
|
||||||
customSystemType: _systemType.value,
|
customSystemType: _systemType.value,
|
||||||
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
|
disabledCmdTypes: _disabledCmdTypes.value.isEmpty ? null : _disabledCmdTypes.value.toList(),
|
||||||
|
proxyCommand: proxyCommand,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.spi == null) {
|
if (this.spi == null) {
|
||||||
@@ -450,5 +480,27 @@ extension _Utils on _ServerEditPageState {
|
|||||||
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
final allAvailableCmdTypes = ShellCmdType.all.map((e) => e.displayName);
|
||||||
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
disabledCmdTypes.removeWhere((e) => !allAvailableCmdTypes.contains(e));
|
||||||
_disabledCmdTypes.value = disabledCmdTypes;
|
_disabledCmdTypes.value = disabledCmdTypes;
|
||||||
|
|
||||||
|
// Load ProxyCommand configuration
|
||||||
|
final proxyCommand = spi.proxyCommand;
|
||||||
|
if (proxyCommand != null) {
|
||||||
|
_proxyCommandEnabled.value = true;
|
||||||
|
_proxyCommandController.text = proxyCommand.command;
|
||||||
|
_proxyCommandTimeout.value = proxyCommand.timeout.inSeconds;
|
||||||
|
|
||||||
|
// Try to match with a preset
|
||||||
|
final presets = ProxyCommandExecutor.getPresets();
|
||||||
|
for (final entry in presets.entries) {
|
||||||
|
if (entry.value.command == proxyCommand.command) {
|
||||||
|
_proxyCommandPreset.value = entry.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_proxyCommandEnabled.value = false;
|
||||||
|
_proxyCommandController.text = '';
|
||||||
|
_proxyCommandTimeout.value = 30;
|
||||||
|
_proxyCommandPreset.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:icons_plus/icons_plus.dart';
|
import 'package:icons_plus/icons_plus.dart';
|
||||||
import 'package:server_box/core/extension/context/locale.dart';
|
import 'package:server_box/core/extension/context/locale.dart';
|
||||||
import 'package:server_box/core/route.dart';
|
import 'package:server_box/core/route.dart';
|
||||||
|
import 'package:server_box/core/utils/proxy_command_executor.dart';
|
||||||
import 'package:server_box/core/utils/server_dedup.dart';
|
import 'package:server_box/core/utils/server_dedup.dart';
|
||||||
import 'package:server_box/core/utils/ssh_config.dart';
|
import 'package:server_box/core/utils/ssh_config.dart';
|
||||||
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
import 'package:server_box/data/model/app/scripts/cmd_types.dart';
|
||||||
import 'package:server_box/data/model/server/custom.dart';
|
import 'package:server_box/data/model/server/custom.dart';
|
||||||
import 'package:server_box/data/model/server/discovery_result.dart';
|
import 'package:server_box/data/model/server/discovery_result.dart';
|
||||||
|
import 'package:server_box/data/model/server/proxy_command_config.dart';
|
||||||
import 'package:server_box/data/model/server/server_private_info.dart';
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
import 'package:server_box/data/model/server/system.dart';
|
import 'package:server_box/data/model/server/system.dart';
|
||||||
import 'package:server_box/data/model/server/wol_cfg.dart';
|
import 'package:server_box/data/model/server/wol_cfg.dart';
|
||||||
@@ -74,6 +76,12 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
final _systemType = ValueNotifier<SystemType?>(null);
|
final _systemType = ValueNotifier<SystemType?>(null);
|
||||||
final _disabledCmdTypes = <String>{}.vn;
|
final _disabledCmdTypes = <String>{}.vn;
|
||||||
|
|
||||||
|
// ProxyCommand fields
|
||||||
|
final _proxyCommandEnabled = ValueNotifier(false);
|
||||||
|
final _proxyCommandController = TextEditingController();
|
||||||
|
final _proxyCommandPreset = nvn<String>();
|
||||||
|
final _proxyCommandTimeout = ValueNotifier(30);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -107,6 +115,11 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
_tags.dispose();
|
_tags.dispose();
|
||||||
_systemType.dispose();
|
_systemType.dispose();
|
||||||
_disabledCmdTypes.dispose();
|
_disabledCmdTypes.dispose();
|
||||||
|
|
||||||
|
_proxyCommandEnabled.dispose();
|
||||||
|
_proxyCommandController.dispose();
|
||||||
|
_proxyCommandPreset.dispose();
|
||||||
|
_proxyCommandTimeout.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -200,6 +213,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
|
|||||||
_buildAuth(),
|
_buildAuth(),
|
||||||
_buildSystemType(),
|
_buildSystemType(),
|
||||||
_buildJumpServer(),
|
_buildJumpServer(),
|
||||||
|
_buildProxyCommand(),
|
||||||
_buildMore(),
|
_buildMore(),
|
||||||
];
|
];
|
||||||
return AutoMultiList(children: children);
|
return AutoMultiList(children: children);
|
||||||
|
|||||||
@@ -449,6 +449,115 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildProxyCommand() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
title: Text('ProxyCommand'),
|
||||||
|
subtitle: Text('Use a proxy command for SSH connection'),
|
||||||
|
trailing: _proxyCommandEnabled.listenVal(
|
||||||
|
(enabled) => Switch(
|
||||||
|
value: enabled,
|
||||||
|
onChanged: (value) {
|
||||||
|
_proxyCommandEnabled.value = value;
|
||||||
|
if (value && _proxyCommandController.text.isEmpty) {
|
||||||
|
// Set default preset when enabled
|
||||||
|
_proxyCommandPreset.value = 'cloudflare_access';
|
||||||
|
_proxyCommandController.text = 'cloudflared access ssh --hostname %h';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_proxyCommandEnabled.listenVal((enabled) {
|
||||||
|
if (!enabled) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Preset selection
|
||||||
|
Text('Presets:', style: Theme.of(context).textTheme.titleSmall),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_proxyCommandPreset.listenVal((preset) {
|
||||||
|
final presets = ProxyCommandExecutor.getPresets();
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: presets.entries.map((entry) {
|
||||||
|
final isSelected = preset == entry.key;
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(_getPresetDisplayName(entry.key)),
|
||||||
|
selected: isSelected,
|
||||||
|
onSelected: (selected) {
|
||||||
|
if (selected) {
|
||||||
|
_proxyCommandPreset.value = entry.key;
|
||||||
|
_proxyCommandController.text = entry.value.command;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Custom command input
|
||||||
|
Input(
|
||||||
|
controller: _proxyCommandController,
|
||||||
|
type: TextInputType.text,
|
||||||
|
label: 'Proxy Command',
|
||||||
|
icon: Icons.settings_ethernet,
|
||||||
|
hint: 'e.g., cloudflared access ssh --hostname %h',
|
||||||
|
suggestion: false,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Help text
|
||||||
|
Text(
|
||||||
|
'Available placeholders:\n'
|
||||||
|
'• %h - hostname\n'
|
||||||
|
'• %p - port\n'
|
||||||
|
'• %r - username',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Timeout setting
|
||||||
|
_proxyCommandTimeout.listenVal((timeout) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text('Connection Timeout'),
|
||||||
|
subtitle: Text('$timeout seconds'),
|
||||||
|
trailing: DropdownButton<int>(
|
||||||
|
value: timeout,
|
||||||
|
items: [10, 30, 60, 120].map((seconds) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: seconds,
|
||||||
|
child: Text('${seconds}s'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
_proxyCommandTimeout.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDelBtn() {
|
Widget _buildDelBtn() {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -472,4 +581,21 @@ extension _Widgets on _ServerEditPageState {
|
|||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _getPresetDisplayName(String presetKey) {
|
||||||
|
switch (presetKey) {
|
||||||
|
case 'cloudflare_access':
|
||||||
|
return 'Cloudflare Access';
|
||||||
|
case 'ssh_via_bastion':
|
||||||
|
return 'SSH via Bastion';
|
||||||
|
case 'nc_netcat':
|
||||||
|
return 'Netcat';
|
||||||
|
case 'socat':
|
||||||
|
return 'Socat';
|
||||||
|
default:
|
||||||
|
return presetKey.split('_').map((word) =>
|
||||||
|
word[0].toUpperCase() + word.substring(1)
|
||||||
|
).join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
test/proxy_command_test.dart
Normal file
137
test/proxy_command_test.dart
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:server_box/data/model/server/proxy_command_config.dart';
|
||||||
|
import 'package:server_box/data/model/server/server_private_info.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ProxyCommandConfig Tests', () {
|
||||||
|
test('should create ProxyCommandConfig with required fields', () {
|
||||||
|
const config = ProxyCommandConfig(
|
||||||
|
command: 'cloudflared access ssh --hostname %h',
|
||||||
|
timeout: Duration(seconds: 30),
|
||||||
|
requiresExecutable: true,
|
||||||
|
executableName: 'cloudflared',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(config.command, equals('cloudflared access ssh --hostname %h'));
|
||||||
|
expect(config.timeout.inSeconds, equals(30));
|
||||||
|
expect(config.requiresExecutable, isTrue);
|
||||||
|
expect(config.executableName, equals('cloudflared'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should get final command with placeholders replaced', () {
|
||||||
|
const config = ProxyCommandConfig(
|
||||||
|
command: 'cloudflared access ssh --hostname %h --port %p',
|
||||||
|
timeout: Duration(seconds: 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
final finalCommand = config.getFinalCommand(
|
||||||
|
hostname: 'example.com',
|
||||||
|
port: 22,
|
||||||
|
user: 'testuser',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(finalCommand, equals('cloudflared access ssh --hostname example.com --port 22'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle all placeholders correctly', () {
|
||||||
|
const config = ProxyCommandConfig(
|
||||||
|
command: 'ssh -W %h:%p -l %r bastion.example.com',
|
||||||
|
timeout: Duration(seconds: 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
final finalCommand = config.getFinalCommand(
|
||||||
|
hostname: 'target.example.com',
|
||||||
|
port: 2222,
|
||||||
|
user: 'admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(finalCommand, equals('ssh -W target.example.com:2222 -l admin bastion.example.com'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should validate presets from map', () {
|
||||||
|
final presets = proxyCommandPresets;
|
||||||
|
|
||||||
|
final cloudflareConfig = presets['cloudflare_access'];
|
||||||
|
expect(cloudflareConfig, isNotNull);
|
||||||
|
expect(cloudflareConfig!.command, equals('cloudflared access ssh --hostname %h'));
|
||||||
|
expect(cloudflareConfig.requiresExecutable, isTrue);
|
||||||
|
expect(cloudflareConfig.executableName, equals('cloudflared'));
|
||||||
|
|
||||||
|
final sshBastionConfig = presets['ssh_via_bastion'];
|
||||||
|
expect(sshBastionConfig, isNotNull);
|
||||||
|
expect(sshBastionConfig!.command, equals('ssh -W %h:%p bastion.example.com'));
|
||||||
|
expect(sshBastionConfig.requiresExecutable, isFalse);
|
||||||
|
|
||||||
|
final ncConfig = presets['nc_netcat'];
|
||||||
|
expect(ncConfig, isNotNull);
|
||||||
|
expect(ncConfig!.command, equals('nc %h %p'));
|
||||||
|
expect(ncConfig.requiresExecutable, isFalse);
|
||||||
|
|
||||||
|
final socatConfig = presets['socat'];
|
||||||
|
expect(socatConfig, isNotNull);
|
||||||
|
expect(socatConfig!.command, equals('socat - PROXY:%h:%p,proxyport=8080'));
|
||||||
|
expect(socatConfig.requiresExecutable, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Spi with ProxyCommand Tests', () {
|
||||||
|
test('should create Spi with ProxyCommand configuration', () {
|
||||||
|
final spi = Spi(
|
||||||
|
name: 'Test Server',
|
||||||
|
ip: 'example.com',
|
||||||
|
port: 22,
|
||||||
|
user: 'testuser',
|
||||||
|
pwd: 'testpass',
|
||||||
|
proxyCommand: const ProxyCommandConfig(
|
||||||
|
command: 'cloudflared access ssh --hostname %h',
|
||||||
|
timeout: Duration(seconds: 30),
|
||||||
|
requiresExecutable: true,
|
||||||
|
executableName: 'cloudflared',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spi.name, equals('Test Server'));
|
||||||
|
expect(spi.proxyCommand, isNotNull);
|
||||||
|
expect(spi.proxyCommand!.command, equals('cloudflared access ssh --hostname %h'));
|
||||||
|
expect(spi.proxyCommand!.requiresExecutable, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle Spi without ProxyCommand', () {
|
||||||
|
final spi = Spi(
|
||||||
|
name: 'Test Server',
|
||||||
|
ip: 'example.com',
|
||||||
|
port: 22,
|
||||||
|
user: 'testuser',
|
||||||
|
pwd: 'testpass',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spi.proxyCommand, isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should serialize and deserialize Spi with ProxyCommand', () {
|
||||||
|
final originalSpi = Spi(
|
||||||
|
name: 'Test Server',
|
||||||
|
ip: 'example.com',
|
||||||
|
port: 22,
|
||||||
|
user: 'testuser',
|
||||||
|
pwd: 'testpass',
|
||||||
|
proxyCommand: const ProxyCommandConfig(
|
||||||
|
command: 'cloudflared access ssh --hostname %h',
|
||||||
|
timeout: Duration(seconds: 30),
|
||||||
|
requiresExecutable: true,
|
||||||
|
executableName: 'cloudflared',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final json = originalSpi.toJson();
|
||||||
|
final deserializedSpi = Spi.fromJson(json);
|
||||||
|
|
||||||
|
expect(deserializedSpi.name, equals(originalSpi.name));
|
||||||
|
expect(deserializedSpi.ip, equals(originalSpi.ip));
|
||||||
|
expect(deserializedSpi.proxyCommand, isNotNull);
|
||||||
|
expect(deserializedSpi.proxyCommand!.command, equals(originalSpi.proxyCommand!.command));
|
||||||
|
expect(deserializedSpi.proxyCommand!.requiresExecutable, equals(originalSpi.proxyCommand!.requiresExecutable));
|
||||||
|
expect(deserializedSpi.proxyCommand!.executableName, equals(originalSpi.proxyCommand!.executableName));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -286,6 +286,25 @@ Host jumpserver
|
|||||||
// ProxyJump is ignored in current implementation
|
// ProxyJump is ignored in current implementation
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('parseConfig handles ProxyCommand with ssh -W jump host', () async {
|
||||||
|
await configFile.writeAsString('''
|
||||||
|
Host internal
|
||||||
|
HostName 172.16.0.50
|
||||||
|
User admin
|
||||||
|
ProxyCommand ssh -W %h:%p user@bastion.example.com
|
||||||
|
''');
|
||||||
|
|
||||||
|
final servers = await SSHConfig.parseConfig(configFile.path);
|
||||||
|
expect(servers, hasLength(1));
|
||||||
|
|
||||||
|
final server = servers.first;
|
||||||
|
expect(server.name, 'internal');
|
||||||
|
expect(server.ip, '172.16.0.50');
|
||||||
|
expect(server.user, 'admin');
|
||||||
|
// Jump host extracted from ProxyCommand token containing user@host
|
||||||
|
expect(server.jumpId, 'user@bastion.example.com');
|
||||||
|
});
|
||||||
|
|
||||||
test('parseConfig returns empty list for non-existent file', () async {
|
test('parseConfig returns empty list for non-existent file', () async {
|
||||||
final servers = await SSHConfig.parseConfig('/non/existent/path');
|
final servers = await SSHConfig.parseConfig('/non/existent/path');
|
||||||
expect(servers, isEmpty);
|
expect(servers, isEmpty);
|
||||||
@@ -352,4 +371,4 @@ Host internal-server
|
|||||||
expect(dev.keyId, isNull);
|
expect(dev.keyId, isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user