Skip to content

Commit

Permalink
Merge pull request #563 from atsign-foundation/windows_support_2
Browse files Browse the repository at this point in the history
feat: client side Windows support
  • Loading branch information
XavierChanth authored Nov 14, 2023
2 parents ca754c2 + 06a828a commit d023d42
Show file tree
Hide file tree
Showing 17 changed files with 151 additions and 294 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/multibuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ jobs:
working-directory: ./packages/sshnoports
strategy:
matrix:
os: [ubuntu-latest, macOS-latest]
os: [ubuntu-latest, macOS-latest, windows-latest]
include:
- os: ubuntu-latest
output-name: sshnp-linux-x64
- os: macOS-latest
output-name: sshnp-macos-x64
- os: windows-latest
output-name: sshnp-windows-x64

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ class LocalSshKeyUtil implements AtSshKeyUtil {
files[1].writeAsString(keyPair.publicKeyContents),
]).catchError((e) => throw e);

chmod(files[0].path, '600');
chmod(files[1].path, '644');
if (!Platform.isWindows) {
chmod(files[0].path, '600');
chmod(files[1].path, '644');
}

return files;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/noports_core/lib/src/common/default_args.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class DefaultSshnpArgs {
static const List<String> localSshOptions = <String>[];
static const bool legacyDaemon = false;
static const bool listDevices = false;
static const SupportedSshClient sshClient = SupportedSshClient.exec;
static const SupportedSshClient sshClient = SupportedSshClient.openssh;
}

class DefaultSshnpdArgs {
static const SupportedSshClient sshClient = SupportedSshClient.exec;
static const SupportedSshClient sshClient = SupportedSshClient.openssh;
}
7 changes: 7 additions & 0 deletions packages/noports_core/lib/src/common/openssh_binary_path.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'dart:io';

const String _windowsOpensshPath = r'C:\Windows\System32\OpenSSH\ssh.exe';
const String _unixOpensshPath = '/usr/bin/ssh';

String get opensshBinaryPath =>
Platform.isWindows ? _windowsOpensshPath : _unixOpensshPath;
4 changes: 2 additions & 2 deletions packages/noports_core/lib/src/common/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import 'package:noports_core/sshrv.dart';
typedef SshrvGenerator<T> = Sshrv<T> Function(String, int, {int localSshdPort});

enum SupportedSshClient {
exec(cliArg: '/usr/bin/ssh'),
openssh(cliArg: 'openssh'),
dart(cliArg: 'dart');

final String _cliArg;
const SupportedSshClient({required String cliArg}) : _cliArg = cliArg;

factory SupportedSshClient.fromString(String cliArg) {
return SupportedSshClient.values.firstWhere(
(arg) => arg._cliArg == cliArg,
(arg) => arg._cliArg == cliArg.toLowerCase(),
orElse: () => throw ArgumentError('Unsupported SSH client: $cliArg'),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import 'dart:io';
import 'package:at_client/at_client.dart';
import 'package:noports_core/sshnp_foundation.dart';

class SshnpExecLocalImpl extends SshnpCore
with SshnpLocalSshKeyHandler, SshnpExecInitialTunnelHandler {
SshnpExecLocalImpl({
class SshnpOpensshLocalImpl extends SshnpCore
with SshnpLocalSshKeyHandler, SshnpOpensshInitialTunnelHandler {
SshnpOpensshLocalImpl({
required super.atClient,
required super.params,
}) {
Expand Down Expand Up @@ -79,7 +79,7 @@ class SshnpExecLocalImpl extends SshnpCore
);

/// Start the initial tunnel
Process bean =
Process? bean =
await startInitialTunnel(identifier: ephemeralKeyPair.identifier);

/// Remove the key pair from the key utility
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:at_client/at_client.dart';
import 'package:noports_core/sshnp_foundation.dart';
Expand All @@ -8,6 +9,11 @@ class SshnpUnsignedImpl extends SshnpCore with SshnpLocalSshKeyHandler {
required super.atClient,
required super.params,
}) {
if (Platform.isWindows) {
throw SshnpError(
'Windows is not supported by unsigned sshnp clients.',
);
}
_sshnpdChannel = SshnpdUnsignedChannel(
atClient: atClient,
params: params,
Expand Down
6 changes: 3 additions & 3 deletions packages/noports_core/lib/src/sshnp/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ abstract interface class Sshnp {
return SshnpUnsignedImpl(atClient: atClient, params: params);
}

/// Think of this as the "default" client - calls /usr/bin/ssh
factory Sshnp.execLocal({
/// Think of this as the "default" client - calls openssh
factory Sshnp.openssh({
required AtClient atClient,
required SshnpParams params,
}) {
return SshnpExecLocalImpl(atClient: atClient, params: params);
return SshnpOpensshLocalImpl(atClient: atClient, params: params);
}

/// Uses a dartssh2 ssh client - still expects local ssh keys
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:dartssh2/dartssh2.dart';
import 'package:meta/meta.dart';
import 'package:noports_core/src/sshnp/sshnp_core.dart';
import 'package:noports_core/sshnp.dart';
import 'package:noports_core/utils.dart';

mixin SshnpInitialTunnelHandler<T> {
@protected
Future<T> startInitialTunnel({required String identifier});
}

mixin SshnpExecInitialTunnelHandler on SshnpCore
implements SshnpInitialTunnelHandler<Process> {
@override
Future<Process> startInitialTunnel({required String identifier}) async {
Process? process;
// If we are starting an initial tunnel, it should be to sshrvd,
// so it is safe to assume that sshrvdChannel is not null here
String argsString = '$remoteUsername@${sshrvdChannel!.host}'
' -p ${sshrvdChannel!.sshrvdPort}'
' -i $identifier'
' -L $localPort:localhost:${params.remoteSshdPort}'
' -o LogLevel=VERBOSE'
' -t -t'
' -o StrictHostKeyChecking=accept-new'
' -o IdentitiesOnly=yes'
' -o BatchMode=yes'
' -o ExitOnForwardFailure=yes'
' -n'
' -f' // fork after authentication - this is important
;
if (params.addForwardsToTunnel) {
argsString += ' ${params.localSshOptions.join(' ')}';
}
argsString += ' sleep 15';

List<String> args = argsString.split(' ');

logger.info('$sessionId | Executing /usr/bin/ssh ${args.join(' ')}');

// Because of the options we are using, we can wait for this process
// to complete, because it will exit with exitCode 0 once it has connected
// successfully
final soutBuf = StringBuffer();
final serrBuf = StringBuffer();
try {
process = await Process.start('/usr/bin/ssh', args);
process.stdout.transform(Utf8Decoder()).listen((String s) {
soutBuf.write(s);
logger.info(' $sessionId | sshStdOut | $s');
}, onError: (e) {});
process.stderr.transform(Utf8Decoder()).listen((String s) {
serrBuf.write(s);
logger.info(' $sessionId | sshStdErr | $s');
}, onError: (e) {});
await process.exitCode.timeout(Duration(seconds: 10));
} on TimeoutException catch (e) {
throw SshnpError(
'ssh process timed out after 10 seconds',
error: e,
);
}
return process;
}
}
import 'package:noports_core/sshnp_foundation.dart';

mixin SshnpDartInitialTunnelHandler on SshnpCore
implements SshnpInitialTunnelHandler<SSHClient> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'dart:async';
import 'package:meta/meta.dart';

mixin SshnpInitialTunnelHandler<T> {
@protected
Future<T> startInitialTunnel({required String identifier});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:noports_core/src/common/openssh_binary_path.dart';
import 'package:noports_core/sshnp_foundation.dart';

mixin SshnpOpensshInitialTunnelHandler on SshnpCore
implements SshnpInitialTunnelHandler<Process?> {
@override
Future<Process?> startInitialTunnel({required String identifier}) async {
Process? process;
// If we are starting an initial tunnel, it should be to sshrvd,
// so it is safe to assume that sshrvdChannel is not null here
String argsString = '$remoteUsername@${sshrvdChannel!.host}'
' -p ${sshrvdChannel!.sshrvdPort}'
' -i $identifier'
' -L $localPort:localhost:${params.remoteSshdPort}'
' -o LogLevel=VERBOSE'
' -t -t'
' -o StrictHostKeyChecking=accept-new'
' -o IdentitiesOnly=yes'
' -o BatchMode=yes'
' -o ExitOnForwardFailure=yes'
' -n'
' -f' // fork after authentication - this is important
;
if (params.addForwardsToTunnel) {
argsString += ' ${params.localSshOptions.join(' ')}';
}
argsString += ' sleep 15';

List<String> args = argsString.split(' ');

logger.info('$sessionId | Executing $opensshBinaryPath ${args.join(' ')}');

// Because of the options we are using, we can wait for this process
// to complete, because it will exit with exitCode 0 once it has connected
// successfully
final soutBuf = StringBuffer();
final serrBuf = StringBuffer();
try {
if (Platform.isWindows) {
// We have to do special stuff on Windows because -f doesn't fork
// properly in the Windows OpenSSH client:
// This incantation opens the initial tunnel in a separate powershell
// window. It's not necessary (and currently not possible) to capture
// the process since there is a physical window the user can close to
// end the session
unawaited(Process.start(
'powershell.exe',
[
'-command',
opensshBinaryPath,
...args,
],
runInShell: true,
mode: ProcessStartMode.detachedWithStdio,
));
// Delay to allow the detached session to pick up the keys
await Future.delayed(Duration(seconds: 3));
} else {
process = await Process.start(opensshBinaryPath, args);
process.stdout.transform(Utf8Decoder()).listen((String s) {
soutBuf.write(s);
logger.info(' $sessionId | sshStdOut | $s');
}, onError: (e) {});
process.stderr.transform(Utf8Decoder()).listen((String s) {
serrBuf.write(s);
logger.info(' $sessionId | sshStdErr | $s');
}, onError: (e) {});
await process.exitCode.timeout(Duration(seconds: 10));
}
} on TimeoutException catch (e) {
throw SshnpError(
'SSHNP failed to start the initial tunnel',
error: e,
);
}
return process;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:at_client/at_client.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -55,7 +56,8 @@ mixin SshnpdDefaultPayloadHandler on SshnpdChannel {
params.sshnpdAtSign,
logger,
envelope,
useFileStorage: useLocalFileStorage,
useFileStorage: useLocalFileStorage &&
!Platform.isWindows, // disable publickey cache on windows
);
} catch (e) {
logger.shout(
Expand Down
11 changes: 6 additions & 5 deletions packages/noports_core/lib/src/sshnpd/sshnpd_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:at_utils/at_logger.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:noports_core/src/common/openssh_binary_path.dart';
import 'package:noports_core/src/sshrv/sshrv.dart';
import 'package:noports_core/sshnpd.dart';
import 'package:noports_core/utils.dart';
Expand Down Expand Up @@ -561,7 +562,7 @@ class SshnpdImpl implements Sshnpd {
String? errorMessage;

switch (sshClient) {
case SupportedSshClient.exec:
case SupportedSshClient.openssh:
(success, errorMessage) = await reverseSshViaExec(
host: host,
port: port,
Expand Down Expand Up @@ -782,7 +783,7 @@ class SshnpdImpl implements Sshnpd {
' -f' // fork after authentication
' sleep 15'
.split(' ');
logger.info('$sessionId | Executing /usr/bin/ssh ${args.join(' ')}');
logger.info('$sessionId | Executing $opensshBinaryPath ${args.join(' ')}');

// Because of the options we are using, we can wait for this process
// to complete, because it will exit with exitCode 0 once it has connected
Expand All @@ -791,7 +792,7 @@ class SshnpdImpl implements Sshnpd {
final soutBuf = StringBuffer();
final serrBuf = StringBuffer();
try {
Process process = await Process.start('/usr/bin/ssh', args);
Process process = await Process.start(opensshBinaryPath, args);
process.stdout.listen((List<int> l) {
var s = utf8.decode(l);
soutBuf.write(s);
Expand All @@ -813,11 +814,11 @@ class SshnpdImpl implements Sshnpd {
if (sshExitCode != 0) {
if (sshExitCode == 6464) {
logger.shout(
'$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}');
'$sessionId | Command timed out: $opensshBinaryPath ${args.join(' ')}');
errorMessage = 'Failed to establish connection - timed out';
} else {
logger.shout('$sessionId | Exit code $sshExitCode from'
' /usr/bin/ssh ${args.join(' ')}');
' $opensshBinaryPath ${args.join(' ')}');
errorMessage =
'Failed to establish connection - exit code $sshExitCode';
}
Expand Down
7 changes: 5 additions & 2 deletions packages/noports_core/lib/sshnp_foundation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ export 'src/sshnp/util/sshrvd_channel/sshrvd_channel.dart';
export 'src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart';
export 'src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart';

export 'src/sshnp/util/sshnp_initial_tunnel_handler.dart';
export 'src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler.dart';
export 'src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_dart_initial_tunnel_handler.dart';
export 'src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler.dart';

export 'src/sshnp/util/sshnp_ssh_key_handler.dart';

// Impl
export 'src/sshnp/impl/sshnp_dart_local_impl.dart';
export 'src/sshnp/impl/sshnp_dart_pure_impl.dart';
export 'src/sshnp/impl/sshnp_exec_local_impl.dart';
export 'src/sshnp/impl/sshnp_openssh_local_impl.dart';
export 'src/sshnp/impl/sshnp_unsigned_impl.dart';

// Common
Expand Down
4 changes: 2 additions & 2 deletions packages/sshnoports/lib/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Future<Sshnp> sshnpFromParamsWithFileBindings(
}

switch (params.sshClient) {
case SupportedSshClient.exec:
return Sshnp.execLocal(
case SupportedSshClient.openssh:
return Sshnp.openssh(
atClient: atClient,
params: params,
);
Expand Down
Loading

0 comments on commit d023d42

Please sign in to comment.