Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/lib/models/host.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:uuid/uuid.dart';

import 'proxy_settings.dart';

enum AuthType { password, privateKey, certificate, agent }

/// How SFTP sessions are started on this host. [normal] requests the
Expand Down Expand Up @@ -47,6 +49,13 @@ class Host {
bool shellIntegration;
bool agentForwarding;
bool osc52Clipboard;

// ── Connection proxy (per-host outbound proxy for the first TCP dial) ──
ProxyType proxyType;
String? proxyHost;
int? proxyPort;
String? proxyUsername;

SftpMode sftpMode;
String? sftpServerCommand;

Expand Down Expand Up @@ -85,6 +94,10 @@ class Host {
this.shellIntegration = true,
this.agentForwarding = false,
this.osc52Clipboard = false,
this.proxyType = ProxyType.none,
this.proxyHost,
this.proxyPort,
this.proxyUsername,
this.sftpMode = SftpMode.normal,
this.sftpServerCommand,
this.protocol = HostProtocol.ssh,
Expand Down Expand Up @@ -136,6 +149,10 @@ class Host {
'shellIntegration': shellIntegration,
'agentForwarding': agentForwarding,
'osc52Clipboard': osc52Clipboard,
'proxyType': proxyType.name,
'proxyHost': proxyHost,
'proxyPort': proxyPort,
'proxyUsername': proxyUsername,
'sftpMode': sftpMode.name,
'sftpServerCommand': sftpServerCommand,
'protocol': protocol.name,
Expand Down Expand Up @@ -190,6 +207,11 @@ class Host {
final name = json['rdpSecurity'] as String?;
return RdpSecurityMode.values.asNameMap()[name] ?? RdpSecurityMode.auto;
}
ProxyType parseProxyType() {
final name = json['proxyType'] as String?;
if (name == null) return ProxyType.none;
return ProxyType.values.asNameMap()[name] ?? ProxyType.none;
}
Map<String, String> parseEnvVars() {
final raw = json['envVars'];
// Malformed/forward-compat values degrade to empty rather than
Expand Down Expand Up @@ -227,6 +249,10 @@ class Host {
shellIntegration: (json['shellIntegration'] as bool?) ?? true,
agentForwarding: (json['agentForwarding'] as bool?) ?? false,
osc52Clipboard: (json['osc52Clipboard'] as bool?) ?? false,
proxyType: parseProxyType(),
proxyHost: json['proxyHost'] as String?,
proxyPort: (json['proxyPort'] as num?)?.toInt(),
proxyUsername: json['proxyUsername'] as String?,
sftpMode: parseSftpMode(),
sftpServerCommand: json['sftpServerCommand'] as String?,
protocol: parseProtocol(),
Expand Down Expand Up @@ -258,6 +284,10 @@ class Host {
bool? shellIntegration,
bool? agentForwarding,
bool? osc52Clipboard,
ProxyType? proxyType,
String? proxyHost,
int? proxyPort,
String? proxyUsername,
SftpMode? sftpMode,
Object? sftpServerCommand = const _Unset(),
HostProtocol? protocol,
Expand Down Expand Up @@ -290,6 +320,10 @@ class Host {
shellIntegration: shellIntegration ?? this.shellIntegration,
agentForwarding: agentForwarding ?? this.agentForwarding,
osc52Clipboard: osc52Clipboard ?? this.osc52Clipboard,
proxyType: proxyType ?? this.proxyType,
proxyHost: proxyHost ?? this.proxyHost,
proxyPort: proxyPort ?? this.proxyPort,
proxyUsername: proxyUsername ?? this.proxyUsername,
sftpMode: sftpMode ?? this.sftpMode,
sftpServerCommand: sftpServerCommand is _Unset
? this.sftpServerCommand
Expand Down
17 changes: 17 additions & 0 deletions app/lib/models/proxy_settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
enum ProxyType { none, http, socks5 }

/// Runtime proxy parameters resolved from a Host plus its stored password.
class ProxySettings {
final ProxyType type;
final String host;
final int port;
final String? username;
final String? password;
const ProxySettings({
required this.type,
required this.host,
required this.port,
this.username,
this.password,
});
}
85 changes: 85 additions & 0 deletions app/lib/services/connection_proxy.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:dartssh2/dartssh2.dart';

import '../models/proxy_settings.dart';
import 'proxy_handshake.dart';

class ConnectionProxy {
/// Connects to the proxy, runs the type-specific handshake to tunnel to
/// [targetHost]:[targetPort], and returns an [SSHSocket] whose stream
/// re-emits the handshake leftover before live data. When [timeout] is set
/// it bounds the whole connect-plus-handshake. Destroys the socket on any
/// failure.
static Future<SSHSocket> connect({
required ProxySettings settings,
required String targetHost,
required int targetPort,
Duration? timeout,
}) {
Future<SSHSocket> run() async {
final socket = await Socket.connect(settings.host, settings.port);
try {
final reader = ByteReader(socket);
final Uint8List leftover;
switch (settings.type) {
case ProxyType.http:
leftover = await httpConnectHandshake(reader, socket,
targetHost: targetHost,
targetPort: targetPort,
username: settings.username,
password: settings.password);
case ProxyType.socks5:
leftover = await socks5Handshake(reader, socket,
targetHost: targetHost,
targetPort: targetPort,
username: settings.username,
password: settings.password);
case ProxyType.none:
throw const ProxyException(
'ConnectionProxy.connect called with ProxyType.none');
}
return _ProxiedSocket(socket, leftover, reader.release());
} catch (_) {
socket.destroy();
rethrow;
}
}

final fut = run();
return timeout == null ? fut : fut.timeout(timeout);
}
}

class _ProxiedSocket implements SSHSocket {
_ProxiedSocket(this._socket, Uint8List leftover, Stream<Uint8List> live)
: _stream = _prepend(leftover, live);

final Socket _socket;
final Stream<Uint8List> _stream;

static Stream<Uint8List> _prepend(
Uint8List head, Stream<Uint8List> tail) async* {
if (head.isNotEmpty) yield head;
yield* tail;
}

@override
Stream<Uint8List> get stream => _stream;

@override
StreamSink<List<int>> get sink => _socket;

@override
Future<void> get done => _socket.done;

@override
Future<void> close() async {
await _socket.close();
}

@override
void destroy() => _socket.destroy();
}
Loading
Loading