feat: Added Port Forwarding Functionality (#1083)
* feat: Added Port Forwarding Functionality Implemented port forwarding functionality, including the following major changes: - Added a port forwarding configuration model and related state management - Added a port forwarding page and interaction logic - Implemented forwarding connections between local and remote ports - Integrated into the server features menu - Added necessary Hive adapters and storage support - Updated plugin configurations across all platforms to support the new feature * feat (Port Forwarding): Added multilingual support and optimized implementation Added multilingual support for the port forwarding feature, including Chinese, English, and other languages Optimized the port forwarding implementation by adding connection management and error handling Fixed an issue with state persistence when updating port forwarding configurations Updated related dependencies and submodules * fix(port_forward): Fixed port forwarding error handling and redesigned the configuration dialog Handled uncaught errors when port forwarding is disabled or during connection attempts Extracted the configuration dialog into a standalone component and added port range validation * fix(port_forward): Fixed issues with port forwarding connection management and UI layout Fixed an issue where port forwarding connections were not closed properly; now uses `clientGetter` to delay the retrieval of `SSHClient` Added cleanup logic when connections are closed to prevent memory leaks Added a `mounted` check in `PortForwardPage` to prevent operations from executing after the component is unmounted Wrapped the configuration dialog content in a `SingleChildScrollView` to prevent content overflow * fix(port_forward): Fixed a concurrent modification exception that occurred when closing a port forwarding connection Fixed a concurrent modification exception that could occur when closing a local forwarding entry by copying the connection list to prevent modifications to the collection during iteration. Also improved the UI by using theme colors and added error handling for configuration saving. * fix(port_forward_provider): Fixed an issue where entries were not properly removed when port forwarding was stopped When port forwarding is stopped, ensure that the corresponding entries are removed from the _forwards map. Additionally, before adding a new forwarding rule, check for and close any existing forwarding rules with the same ID to prevent resource leaks. * refactor(l1n): Remove unused localization and remote host port translations * fix(port_forward_provider): Handle errors when closing port forwarding Add error handling to prevent the program from crashing due to exceptions when closing port forwarding * refactor(port_forward): Refactor port forwarding state management to use serverId Directly link port forwarding state management to the server ID to simplify parameter passing Remove direct dependencies on Spi and use serverId as the core identifier instead Update relevant providers and page logic to accommodate the new state structure * fix(port_forward): Fixed a race condition issue in port forwarding operations Added an _inFlight collection to prevent duplicate operations Added a _saving state when saving configurations to prevent duplicate submissions Automatically cleans up forwarding when changes in server connection status are detected * refactor(port_forward_provider): Remove unnecessary concurrency control logic Simplify the `toggleForward` method by removing concurrency control for the `_inFlight` collection, as it is not required in the current scenario
This commit is contained in:
197
lib/data/provider/port_forward_provider.dart
Normal file
197
lib/data/provider/port_forward_provider.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:server_box/data/model/server/port_forward.dart';
|
||||
import 'package:server_box/data/provider/server/single.dart';
|
||||
import 'package:server_box/data/res/store.dart';
|
||||
|
||||
part 'port_forward_provider.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class PortForwardNotifier extends _$PortForwardNotifier {
|
||||
final Map<String, _LocalForwardEntry> _forwards = {};
|
||||
final Set<String> _inFlight = {};
|
||||
|
||||
@override
|
||||
PortForwardState build(String serverId) {
|
||||
ref.onDispose(() => dispose());
|
||||
ref.listen(serverProvider(serverId), (prev, next) {
|
||||
if (next.client == null && prev?.client != null) {
|
||||
for (final entry in _forwards.values) {
|
||||
entry.close().catchError((_) {});
|
||||
}
|
||||
_forwards.clear();
|
||||
state = state.copyWith(activeForwards: {});
|
||||
}
|
||||
});
|
||||
final configs = Stores.portForward.fetch(serverId);
|
||||
return PortForwardState(serverId: serverId, configs: configs);
|
||||
}
|
||||
|
||||
String get _serverId => state.serverId;
|
||||
|
||||
SSHClient get _client {
|
||||
final serverState = ref.read(serverProvider(_serverId));
|
||||
final client = serverState.client;
|
||||
if (client == null) {
|
||||
throw StateError('SSH client is not connected');
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final entry in _forwards.values) {
|
||||
entry.close().catchError((_) {});
|
||||
}
|
||||
_forwards.clear();
|
||||
}
|
||||
|
||||
Future<void> addConfig(PortForwardConfig config) async {
|
||||
final configWithServerId = config.copyWith(serverId: _serverId);
|
||||
Stores.portForward.put(configWithServerId);
|
||||
final configs = [...state.configs, configWithServerId];
|
||||
state = state.copyWith(configs: configs);
|
||||
}
|
||||
|
||||
Future<void> updateConfig(PortForwardConfig oldConfig, PortForwardConfig newConfig) async {
|
||||
await stopForward(oldConfig.id);
|
||||
final configWithServerId = newConfig.copyWith(serverId: _serverId);
|
||||
Stores.portForward.update(oldConfig, configWithServerId);
|
||||
final configs = state.configs.map((c) => c.id == oldConfig.id ? configWithServerId : c).toList();
|
||||
state = state.copyWith(configs: configs);
|
||||
}
|
||||
|
||||
Future<void> removeConfig(String id) async {
|
||||
await stopForward(id);
|
||||
final config = state.configs.firstWhereOrNull((c) => c.id == id);
|
||||
if (config != null) {
|
||||
Stores.portForward.delete(config);
|
||||
}
|
||||
final configs = state.configs.where((c) => c.id != id).toList();
|
||||
final activeForwards = Map<String, PortForwardStatus>.from(state.activeForwards)..remove(id);
|
||||
state = state.copyWith(configs: configs, activeForwards: activeForwards);
|
||||
}
|
||||
|
||||
Future<void> startForward(String id) async {
|
||||
if (!_inFlight.add(id)) return;
|
||||
try {
|
||||
final config = state.configs.firstWhereOrNull((c) => c.id == id);
|
||||
if (config == null) {
|
||||
Loggers.app.warning('Port forward config not found: $id');
|
||||
return;
|
||||
}
|
||||
|
||||
final existing = _forwards[id];
|
||||
if (existing != null) {
|
||||
await existing.close().catchError((_) {});
|
||||
_forwards.remove(id);
|
||||
}
|
||||
|
||||
try {
|
||||
final serverSocket = await ServerSocket.bind(config.localHost, config.localPort);
|
||||
|
||||
Loggers.app.info('Port forward started: ${config.localHost}:${config.localPort} -> ${config.remoteHost}:${config.remotePort}');
|
||||
|
||||
final entry = _LocalForwardEntry(serverSocket: serverSocket);
|
||||
entry.start(config.remoteHost, config.remotePort, () => _client);
|
||||
_forwards[id] = entry;
|
||||
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: true));
|
||||
} catch (e) {
|
||||
Loggers.app.warning('Port forward failed to start: $e');
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: false, error: e.toString()));
|
||||
}
|
||||
} finally {
|
||||
_inFlight.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopForward(String id) async {
|
||||
if (!_inFlight.add(id)) return;
|
||||
try {
|
||||
final entry = _forwards[id];
|
||||
if (entry != null) {
|
||||
await entry.close().catchError((_) {});
|
||||
_forwards.remove(id);
|
||||
Loggers.app.info('Port forward stopped: $id');
|
||||
}
|
||||
_updateStatus(id, PortForwardStatus(id: id, isActive: false));
|
||||
} finally {
|
||||
_inFlight.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleForward(String id) async {
|
||||
final isActive = state.activeForwards[id]?.isActive ?? false;
|
||||
if (isActive) {
|
||||
await stopForward(id);
|
||||
} else {
|
||||
await startForward(id);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateStatus(String id, PortForwardStatus status) {
|
||||
final activeForwards = Map<String, PortForwardStatus>.from(state.activeForwards);
|
||||
activeForwards[id] = status;
|
||||
state = state.copyWith(activeForwards: activeForwards);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalForwardEntry {
|
||||
final ServerSocket serverSocket;
|
||||
final List<_ActiveConnection> _connections = [];
|
||||
StreamSubscription<Socket>? _subscription;
|
||||
|
||||
_LocalForwardEntry({required this.serverSocket});
|
||||
|
||||
void start(String remoteHost, int remotePort, SSHClient Function() clientGetter) {
|
||||
_subscription = serverSocket.listen((socket) async {
|
||||
try {
|
||||
final forward = await clientGetter().forwardLocal(remoteHost, remotePort);
|
||||
final conn = _ActiveConnection(socket: socket, forward: forward);
|
||||
_connections.add(conn);
|
||||
final pipe1 = forward.stream.cast<List<int>>().pipe(socket).catchError((_) {});
|
||||
final pipe2 = socket.cast<List<int>>().pipe(forward.sink).catchError((_) {});
|
||||
Future.wait([pipe1, pipe2]).whenComplete(() {
|
||||
_connections.remove(conn);
|
||||
conn.close();
|
||||
});
|
||||
} catch (e, s) {
|
||||
Loggers.app.warning('Port forward connection failed', e, s);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await _subscription?.cancel();
|
||||
await serverSocket.close();
|
||||
final connections = _connections.toList();
|
||||
for (final conn in connections) {
|
||||
await conn.close();
|
||||
}
|
||||
_connections.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _ActiveConnection {
|
||||
final Socket socket;
|
||||
final SSHForwardChannel forward;
|
||||
|
||||
_ActiveConnection({
|
||||
required this.socket,
|
||||
required this.forward,
|
||||
});
|
||||
|
||||
Future<void> close() async {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await forward.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
109
lib/data/provider/port_forward_provider.g.dart
Normal file
109
lib/data/provider/port_forward_provider.g.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'port_forward_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(PortForwardNotifier)
|
||||
const portForwardProvider = PortForwardNotifierFamily._();
|
||||
|
||||
final class PortForwardNotifierProvider
|
||||
extends $NotifierProvider<PortForwardNotifier, PortForwardState> {
|
||||
const PortForwardNotifierProvider._({
|
||||
required PortForwardNotifierFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'portForwardProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$portForwardNotifierHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'portForwardProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
PortForwardNotifier create() => PortForwardNotifier();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(PortForwardState value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<PortForwardState>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PortForwardNotifierProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$portForwardNotifierHash() =>
|
||||
r'e9a93e4e4ee526d334eaaba0e3e0093de7a337fd';
|
||||
|
||||
final class PortForwardNotifierFamily extends $Family
|
||||
with
|
||||
$ClassFamilyOverride<
|
||||
PortForwardNotifier,
|
||||
PortForwardState,
|
||||
PortForwardState,
|
||||
PortForwardState,
|
||||
String
|
||||
> {
|
||||
const PortForwardNotifierFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'portForwardProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
PortForwardNotifierProvider call(String serverId) =>
|
||||
PortForwardNotifierProvider._(argument: serverId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'portForwardProvider';
|
||||
}
|
||||
|
||||
abstract class _$PortForwardNotifier extends $Notifier<PortForwardState> {
|
||||
late final _$args = ref.$arg as String;
|
||||
String get serverId => _$args;
|
||||
|
||||
PortForwardState build(String serverId);
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build(_$args);
|
||||
final ref = this.ref as $Ref<PortForwardState, PortForwardState>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<PortForwardState, PortForwardState>,
|
||||
PortForwardState,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ final class PveNotifierProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$pveNotifierHash() => r'a66699f64eae680064a1904f475d0a241d6cb3f8';
|
||||
String _$pveNotifierHash() => r'1f80a27896013a275e5222f19e5ee3c3a68e2f84';
|
||||
|
||||
final class PveNotifierFamily extends $Family
|
||||
with $ClassFamilyOverride<PveNotifier, PveState, PveState, PveState, Spi> {
|
||||
|
||||
Reference in New Issue
Block a user