diff --git a/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart index 09e45e976..a376228eb 100644 --- a/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart @@ -21,7 +21,7 @@ abstract interface class AtSshKeyUtil { FutureOr addKeyPair({ required AtSshKeyPair keyPair, - required String identifier, + String? identifier, }); FutureOr deleteKeyPair({ diff --git a/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart index 657788903..79a9c63e4 100644 --- a/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart @@ -50,9 +50,9 @@ class DartSshKeyUtil implements AtSshKeyUtil { @override FutureOr addKeyPair({ required AtSshKeyPair keyPair, - required String identifier, + String? identifier, }) { - _keyPairCache[identifier] = keyPair; + _keyPairCache[identifier ?? keyPair.identifier] = keyPair; } @override diff --git a/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart index e17ba59aa..0b6ba6e09 100644 --- a/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart @@ -47,9 +47,10 @@ class LocalSshKeyUtil implements AtSshKeyUtil { @override Future> addKeyPair({ required AtSshKeyPair keyPair, - required String identifier, + String? identifier, }) async { - var files = _filesFromIdentifier(identifier: identifier); + var files = + _filesFromIdentifier(identifier: identifier ?? keyPair.identifier); await Future.wait([ files[0].writeAsString(keyPair.privateKeyContents), files[1].writeAsString(keyPair.publicKeyContents), diff --git a/packages/noports_core/lib/src/common/io_types.dart b/packages/noports_core/lib/src/common/io_types.dart index a0195e948..f5d01e756 100644 --- a/packages/noports_core/lib/src/common/io_types.dart +++ b/packages/noports_core/lib/src/common/io_types.dart @@ -1,5 +1,5 @@ /// This file contains all of the dart:io calls in noports_core -/// All io used should be wrapped for the sake of testing and compatibilty +/// All io used should be wrapped for the sake of testing and compatibility import 'dart:io' show Process, ProcessResult, ProcessStartMode; import 'package:meta/meta.dart'; @@ -15,7 +15,6 @@ typedef ProcessRunner = Future Function( String? workingDirectory, }); - @internal typedef ProcessStarter = Future Function( String executable, diff --git a/packages/noports_core/lib/src/common/mixins/async_completion.dart b/packages/noports_core/lib/src/common/mixins/async_completion.dart index 8c484d88e..2d779a902 100644 --- a/packages/noports_core/lib/src/common/mixins/async_completion.dart +++ b/packages/noports_core/lib/src/common/mixins/async_completion.dart @@ -4,13 +4,13 @@ import 'package:meta/meta.dart'; mixin class AsyncDisposal { // * Private members - bool _dispoalStarted = false; + bool _disposalStarted = false; final Completer _disposedCompleter = Completer(); // * Public members /// Used to check if disposal has started - bool get disposalStarted => _dispoalStarted; + bool get disposalStarted => _disposalStarted; /// Used to check if disposal has completed Future get disposed => _disposedCompleter.future; @@ -24,8 +24,8 @@ mixin class AsyncDisposal { @protected Future callDisposal() async { - if (!_dispoalStarted) { - _dispoalStarted = true; + if (!_disposalStarted) { + _disposalStarted = true; unawaited(dispose()); } return disposed; diff --git a/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart b/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart index 5c22528d2..1638a801b 100644 --- a/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart @@ -5,11 +5,12 @@ import 'package:dartssh2/dartssh2.dart'; import 'package:noports_core/sshnp_foundation.dart'; class SshnpDartPureImpl extends SshnpCore - with SshnpDartSshKeyHandler, SshnpDartInitialTunnelHandler { - SshnpDartPureImpl({ - required super.atClient, - required super.params, - }) { + with SshnpDartSshKeyHandler, DartSshSessionHandler { + SshnpDartPureImpl( + {required super.atClient, + required super.params, + required AtSshKeyPair? identityKeyPair}) { + this.identityKeyPair = identityKeyPair; _sshnpdChannel = SshnpdDefaultChannel( atClient: atClient, params: params, @@ -35,14 +36,22 @@ class SshnpDartPureImpl extends SshnpCore Future initialize() async { if (!isSafeToInitialize) return; await super.initialize(); + if (params.identityFile != null) { + identityKeyPair = + await keyUtil.getKeyPair(identifier: params.identityFile!); + } completeInitialization(); } + SSHClient? tunnelSshClient; + @override Future run() async { /// Ensure that sshnp is initialized await callInitialization(); + logger.info('Sending request to sshnpd'); + /// Send an ssh request to sshnpd await notify( AtKey() @@ -78,14 +87,11 @@ class SshnpDartPureImpl extends SshnpCore ); /// Add the key pair to the key utility - await keyUtil.addKeyPair( - keyPair: ephemeralKeyPair, - identifier: ephemeralKeyPair.identifier, - ); + await keyUtil.addKeyPair(keyPair: ephemeralKeyPair); /// Start the initial tunnel - SSHClient bean = - await startInitialTunnel(identifier: ephemeralKeyPair.identifier); + tunnelSshClient = await startInitialTunnelSession( + ephemeralKeyPairIdentifier: ephemeralKeyPair.identifier); /// Remove the key pair from the key utility await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); @@ -101,7 +107,43 @@ class SshnpDartPureImpl extends SshnpCore localSshOptions: (params.addForwardsToTunnel) ? null : params.localSshOptions, privateKeyFileName: identityKeyPair?.identifier, - connectionBean: bean, + connectionBean: tunnelSshClient, ); } + + @override + bool get canRunShell => true; + + @override + Future runShell() async { + if (tunnelSshClient == null) { + throw StateError( + 'Cannot execute runShell, tunnel has not yet been created'); + } + + SSHClient userSession = + await startUserSession(tunnelSession: tunnelSshClient!); + + SSHSession shell = await userSession.shell(); + + return SSHSessionAsSshnpRemoteProcess(shell); + } +} + +class SSHSessionAsSshnpRemoteProcess implements SshnpRemoteProcess { + SSHSession sshSession; + + SSHSessionAsSshnpRemoteProcess(this.sshSession); + + @override + Future get done => sshSession.done; + + @override + StreamSink> get stdin => sshSession.stdin; + + @override + Stream> get stdout => sshSession.stdout; + + @override + Stream> get stderr => sshSession.stderr; } diff --git a/packages/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart b/packages/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart index 4c76d60a4..59783f949 100644 --- a/packages/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart @@ -6,7 +6,7 @@ import 'package:noports_core/src/common/io_types.dart'; import 'package:noports_core/sshnp_foundation.dart'; class SshnpOpensshLocalImpl extends SshnpCore - with SshnpLocalSshKeyHandler, SshnpOpensshInitialTunnelHandler { + with SshnpLocalSshKeyHandler, OpensshSshSessionHandler { SshnpOpensshLocalImpl({ required super.atClient, required super.params, @@ -64,6 +64,8 @@ class SshnpOpensshLocalImpl extends SshnpCore /// Ensure that sshnp is initialized await callInitialization(); + logger.info('Sending request to sshnpd'); + /// Send an ssh request to sshnpd await notify( AtKey() @@ -100,14 +102,11 @@ class SshnpOpensshLocalImpl extends SshnpCore ); /// Add the key pair to the key utility - await keyUtil.addKeyPair( - keyPair: ephemeralKeyPair, - identifier: ephemeralKeyPair.identifier, - ); + await keyUtil.addKeyPair(keyPair: ephemeralKeyPair); /// Start the initial tunnel - Process? bean = - await startInitialTunnel(identifier: ephemeralKeyPair.identifier); + Process? bean = await startInitialTunnelSession( + ephemeralKeyPairIdentifier: ephemeralKeyPair.identifier); /// Remove the key pair from the key utility await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); @@ -126,4 +125,12 @@ class SshnpOpensshLocalImpl extends SshnpCore connectionBean: bean, ); } + + @override + bool get canRunShell => false; + + @override + Future runShell() { + throw UnimplementedError('$runtimeType does not implement runShell'); + } } diff --git a/packages/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart b/packages/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart index 8fa0ae53b..e1f3d7b92 100644 --- a/packages/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart @@ -108,4 +108,12 @@ class SshnpUnsignedImpl extends SshnpCore with SshnpLocalSshKeyHandler { connectionBean: bean, ); } + + @override + bool get canRunShell => false; + + @override + Future runShell() { + throw UnimplementedError('$runtimeType does not implement runShell'); + } } diff --git a/packages/noports_core/lib/src/sshnp/sshnp.dart b/packages/noports_core/lib/src/sshnp/sshnp.dart index 3dccf1178..4ee1f996e 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp.dart +++ b/packages/noports_core/lib/src/sshnp/sshnp.dart @@ -3,6 +3,13 @@ import 'dart:async'; import 'package:at_client/at_client.dart' hide StringBuffer; import 'package:noports_core/sshnp_foundation.dart'; +abstract interface class SshnpRemoteProcess { + Future get done; + Stream> get stderr; + StreamSink> get stdin; + Stream> get stdout; +} + abstract interface class Sshnp { /// Legacy v3.x.x client @Deprecated( @@ -29,14 +36,9 @@ abstract interface class Sshnp { required AtSshKeyPair? identityKeyPair, }) { var sshnp = SshnpDartPureImpl( - atClient: atClient, - params: params, - ); + atClient: atClient, params: params, identityKeyPair: identityKeyPair); if (identityKeyPair != null) { - sshnp.keyUtil.addKeyPair( - keyPair: identityKeyPair, - identifier: identityKeyPair.identifier, - ); + sshnp.keyUtil.addKeyPair(keyPair: identityKeyPair); } return sshnp; } @@ -48,12 +50,22 @@ abstract interface class Sshnp { SshnpParams get params; /// May only be run after [initialize] has been run. + /// - Sends request to sshrvd if required /// - Sends request to sshnpd; the response listener was started by [initialize] /// - Waits for success or error response, or time out after 10 secs - /// - If got a success response, print the ssh command to use to stdout - /// - Clean up temporary files + /// - Make ssh tunnel connection using ephemeral keys Future run(); + /// May only be run after [run] has been run. + /// When true, [runShell] will work. + /// When false, runShell will throw an + /// UnimplementedError + bool get canRunShell; + + /// Creates a user ssh session on top of the tunnel session, + /// and starts a shell. + Future runShell(); + /// Send a ping out to all sshnpd and listen for heartbeats /// Returns two Iterable and a Map: /// - Iterable of atSigns of sshnpd that responded diff --git a/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart new file mode 100644 index 000000000..e38164d38 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart @@ -0,0 +1,250 @@ +import 'dart:async'; + +// Current implementation uses ServerSocket, which will be replaced with an +// internal Dart stream at a later time +import 'dart:io' show ServerSocket; + +import 'package:at_utils/at_logger.dart'; +import 'package:dartssh2/dartssh2.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/sshnp_foundation.dart'; + +mixin DartSshSessionHandler on SshnpCore + implements SshSessionHandler { + /// Set up timer to check to see if all connections are down + @visibleForTesting + String get terminateMessage => + 'ssh session will terminate after ${params.idleTimeout} seconds' + ' if it is not being used'; + + @override + Future startInitialTunnelSession( + {required String ephemeralKeyPairIdentifier}) async { + // If we are starting an initial tunnel, it should be to sshrvd, + // so it is safe to assume that sshrvdChannel is not null here + + var username = tunnelUsername ?? getUserName(throwIfNull: true)!; + + logger.info('Starting tunnel ssh session as $username' + ' to ${sshrvdChannel.host} on port ${sshrvdChannel.sshrvdPort!}'); + + AtSshKeyPair keyPair = + await keyUtil.getKeyPair(identifier: ephemeralKeyPairIdentifier); + + SshClientHelper helper = SshClientHelper(logger); + SSHClient tunnelSshClient = await helper.createSshClient( + host: sshrvdChannel.host, + port: sshrvdChannel.sshrvdPort!, + username: username, + keyPair: keyPair, + ); + + logger.info('Starting port forwarding' + ' from localhost:$localPort on local side' + ' to localhost:${params.remoteSshdPort} on remote side'); + + // Start local forwarding to the remote sshd + localPort = await helper.startForwarding( + fLocalPort: localPort, + fRemoteHost: 'localhost', + fRemotePort: params.remoteSshdPort, + ); + + logger.info('Started port forwarding' + ' from localhost:$localPort on local side' + ' to localhost:${params.remoteSshdPort} on remote side'); + + if (params.addForwardsToTunnel) { + var optionsSplitBySpace = params.localSshOptions.join(' ').split(' '); + logger.finer('addForwardsToTunnel is true, adding them;' + ' localSshOptions split by space is $optionsSplitBySpace'); + await helper.addForwards(optionsSplitBySpace); + } + + logger.info(terminateMessage); + + Timer.periodic(Duration(seconds: params.idleTimeout), (timer) async { + if (helper.counter == 0 || tunnelSshClient.isClosed) { + timer.cancel(); + if (!tunnelSshClient.isClosed) tunnelSshClient.close(); + logger + .shout('$sessionId | no active connections - ssh session complete'); + } + }); + + return tunnelSshClient; + } + + @override + Future startUserSession({ + required SSHClient tunnelSession, + }) async { + if (identityKeyPair == null) { + throw SshnpError('Identity Key pair is mandatory with the dart client.'); + } + + var username = remoteUsername ?? getUserName(throwIfNull: true)!; + + logger + .info('Starting user ssh session as $username to localhost:$localPort'); + + SshClientHelper helper = SshClientHelper(logger); + SSHClient userSshClient = await helper.createSshClient( + host: 'localhost', + port: localPort, + username: username, + keyPair: identityKeyPair!, + ); + + if (!params.addForwardsToTunnel) { + var optionsSplitBySpace = params.localSshOptions.join(' ').split(' '); + logger.finer('addForwardsToTunnel was false,' + ' so adding them to user session instead;' + ' localSshOptions split by space is $optionsSplitBySpace'); + await helper.addForwards(optionsSplitBySpace); + } + + return userSshClient; + } +} + +@visibleForTesting +class SshClientHelper { + // TODO get rid of this + final AtSignLogger logger; + + int counter = 0; + + @visibleForTesting + late final SSHClient client; + + SshClientHelper(this.logger); + + Future createSshClient({ + required String host, + required int port, + required String username, + required AtSshKeyPair keyPair, + }) async { + try { + late final SSHSocket socket; + try { + socket = await SSHSocket.connect( + host, + port, + ).catchError((e) => throw e); + } catch (e, s) { + var error = SshnpError( + 'Failed to open socket to $host:$port : $e', + error: e, + stackTrace: s, + ); + throw error; + } + + try { + client = SSHClient( + socket, + username: username, + identities: [keyPair.keyPair], + keepAliveInterval: Duration(seconds: 15), + ); + } catch (e, s) { + throw SshnpError( + 'Failed to create SSHClient for $username@$host:$port : $e', + error: e, + stackTrace: s, + ); + } + + try { + await client.authenticated.catchError((e) => throw e); + } catch (e, s) { + throw SshnpError( + 'Failed to authenticate as $username@$host:$port : $e', + error: e, + stackTrace: s, + ); + } + + return client; + } on SshnpError catch (_) { + rethrow; + } catch (e, s) { + throw SshnpError( + 'SSH Client failure : $e', + error: e, + stackTrace: s, + ); + } + } + + Future startForwarding( + {required int fLocalPort, + required String fRemoteHost, + required int fRemotePort}) async { + // TODO remove local dependency on ServerSockets + /// Do the port forwarding for sshd + final serverSocket = await ServerSocket.bind('localhost', fLocalPort); + + serverSocket.listen((socket) async { + counter++; + final forward = await client.forwardLocal(fRemoteHost, fRemotePort); + unawaited( + forward.stream.cast>().pipe(socket).whenComplete( + () async { + counter--; + }, + ), + ); + unawaited(socket.pipe(forward.sink)); + }, onError: (Object error) { + counter = 0; + }, onDone: () { + counter = 0; + }); + + return serverSocket.port; + } + + Future addForwards(List optionsSplitBySpace) async { + // parse the localSshOptions, extract all of the local port forwarding + // directives and act on all of them + var lsoIter = optionsSplitBySpace.iterator; + while (lsoIter.moveNext()) { + if (lsoIter.current == '-L') { + // we expect the args next + bool hasArgs = lsoIter.moveNext(); + if (!hasArgs) { + logger.warning('localSshOptions has -L with no args'); + continue; + } + String argString = lsoIter.current; + // We expect args like $localPort:$remoteHost:$remotePort + List args = argString.split(':'); + if (args.length != 3) { + logger.warning('localSshOptions has -L with bad args $argString'); + continue; + } + int? fLocalPort = int.tryParse(args[0]); + String fRemoteHost = args[1]; + int? fRemotePort = int.tryParse(args[2]); + if (fLocalPort == null || fRemoteHost.isEmpty || fRemotePort == null) { + logger.warning('localSshOptions has -L with bad args $argString'); + continue; + } + + // Start the forwarding + logger.info('Starting port forwarding' + ' from localhost:$fLocalPort on local side' + ' to $fRemoteHost:$fRemotePort on remote side'); + + await startForwarding( + fLocalPort: fLocalPort, + fRemoteHost: fRemoteHost, + fRemotePort: fRemotePort, + ); + } + } + } +} diff --git a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler.dart b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart similarity index 89% rename from packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler.dart rename to packages/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart index f2768f967..3a6779172 100644 --- a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler.dart +++ b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart @@ -6,11 +6,11 @@ import 'package:noports_core/src/common/io_types.dart'; import 'package:noports_core/src/common/openssh_binary_path.dart'; import 'package:noports_core/sshnp_foundation.dart'; -mixin SshnpOpensshInitialTunnelHandler on SshnpCore - implements SshnpInitialTunnelHandler { +mixin OpensshSshSessionHandler on SshnpCore + implements SshSessionHandler { @override - Future startInitialTunnel({ - required String identifier, + Future startInitialTunnelSession({ + required String ephemeralKeyPairIdentifier, @visibleForTesting ProcessStarter startProcess = Process.start, }) async { Process? process; @@ -18,7 +18,7 @@ mixin SshnpOpensshInitialTunnelHandler on SshnpCore // so it is safe to assume that sshrvdChannel is not null here String argsString = '$tunnelUsername@${sshrvdChannel.host}' ' -p ${sshrvdChannel.sshrvdPort}' - ' -i $identifier' + ' -i $ephemeralKeyPairIdentifier' ' -L $localPort:localhost:${params.remoteSshdPort}' ' -o LogLevel=VERBOSE' ' -t -t' @@ -83,4 +83,11 @@ mixin SshnpOpensshInitialTunnelHandler on SshnpCore } return process; } + + @override + Future startUserSession({ + required Process? tunnelSession, + }) async { + throw UnimplementedError(); + } } diff --git a/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart new file mode 100644 index 000000000..5d7aa3d55 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart @@ -0,0 +1,15 @@ +import 'dart:async'; +import 'package:meta/meta.dart'; + +mixin SshSessionHandler { + @protected + @visibleForTesting + Future startInitialTunnelSession( + {required String ephemeralKeyPairIdentifier}); + + @protected + @visibleForTesting + Future startUserSession({ + required T tunnelSession, + }); +} diff --git a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_dart_initial_tunnel_handler.dart b/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_dart_initial_tunnel_handler.dart deleted file mode 100644 index ec5fe5eff..000000000 --- a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_dart_initial_tunnel_handler.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'dart:async'; -// Current implementation uses ServerSocket, which will be replaced with an -// internal Dart stream at a later time -import 'dart:io' show ServerSocket; - -import 'package:dartssh2/dartssh2.dart'; -import 'package:meta/meta.dart'; -import 'package:noports_core/sshnp_foundation.dart'; - -mixin SshnpDartInitialTunnelHandler on SshnpCore - implements SshnpInitialTunnelHandler { - /// Set up timer to check to see if all connections are down - @visibleForTesting - String get terminateMessage => - 'ssh session will terminate after ${params.idleTimeout} seconds' - ' if it is not being used'; - - @override - Future startInitialTunnel({required String identifier}) async { - // If we are starting an initial tunnel, it should be to sshrvd, - // so it is safe to assume that sshrvdChannel is not null here - logger.info( - 'Starting direct ssh session to ${sshrvdChannel.host} on port ${sshrvdChannel.sshrvdPort} with forwardLocal of $localPort'); - try { - late final SSHClient client; - - late final SSHSocket socket; - try { - socket = await SSHSocket.connect( - sshrvdChannel.host, - sshrvdChannel.sshrvdPort!, - ).catchError((e) => throw e); - } catch (e, s) { - var error = SshnpError( - 'Failed to open socket to ${sshrvdChannel.host}:${sshrvdChannel.sshrvdPort} : $e', - error: e, - stackTrace: s, - ); - throw error; - } - - var usernameForTunnel = tunnelUsername ?? getUserName(throwIfNull: true)!; - try { - AtSshKeyPair keyPair = await keyUtil.getKeyPair(identifier: identifier); - client = SSHClient( - socket, - username: usernameForTunnel, - identities: [keyPair.keyPair], - keepAliveInterval: Duration(seconds: 15), - ); - } catch (e, s) { - throw SshnpError( - 'Failed to create SSHClient for $usernameForTunnel@${sshrvdChannel.host}:${sshrvdChannel.sshrvdPort} : $e', - error: e, - stackTrace: s, - ); - } - - try { - await client.authenticated.catchError((e) => throw e); - } catch (e, s) { - throw SshnpError( - 'Failed to authenticate as $usernameForTunnel@${sshrvdChannel.host}:${sshrvdChannel.sshrvdPort} : $e', - error: e, - stackTrace: s, - ); - } - - int counter = 0; - - Future startForwarding( - {required int fLocalPort, - required String fRemoteHost, - required int fRemotePort}) async { - logger.info('Starting port forwarding' - ' from localhost:$fLocalPort on local side' - ' to $fRemoteHost:$fRemotePort on remote side'); - - // TODO remove local dependency on ServerSockets - /// Do the port forwarding for sshd - final serverSocket = await ServerSocket.bind('localhost', fLocalPort); - - serverSocket.listen((socket) async { - counter++; - final forward = await client.forwardLocal(fRemoteHost, fRemotePort); - unawaited( - forward.stream.cast>().pipe(socket).whenComplete( - () async { - counter--; - }, - ), - ); - unawaited(socket.pipe(forward.sink)); - }, onError: (Object error) { - counter = 0; - }, onDone: () { - counter = 0; - }); - } - - // Start local forwarding to the remote sshd - await startForwarding( - fLocalPort: localPort, - fRemoteHost: 'localhost', - fRemotePort: params.remoteSshdPort, - ); - - if (params.addForwardsToTunnel) { - var optionsSplitBySpace = params.localSshOptions.join(' ').split(' '); - logger.info('addForwardsToTunnel is true;' - ' localSshOptions split by space is $optionsSplitBySpace'); - // parse the localSshOptions, extract all of the local port forwarding - // directives and act on all of them - var lsoIter = optionsSplitBySpace.iterator; - while (lsoIter.moveNext()) { - if (lsoIter.current == '-L') { - // we expect the args next - bool hasArgs = lsoIter.moveNext(); - if (!hasArgs) { - logger.warning('localSshOptions has -L with no args'); - continue; - } - String argString = lsoIter.current; - // We expect args like $localPort:$remoteHost:$remotePort - List args = argString.split(':'); - if (args.length != 3) { - logger.warning('localSshOptions has -L with bad args $argString'); - continue; - } - int? fLocalPort = int.tryParse(args[0]); - String fRemoteHost = args[1]; - int? fRemotePort = int.tryParse(args[2]); - if (fLocalPort == null || - fRemoteHost.isEmpty || - fRemotePort == null) { - logger.warning('localSshOptions has -L with bad args $argString'); - continue; - } - - // Start the forwarding - await startForwarding( - fLocalPort: fLocalPort, - fRemoteHost: fRemoteHost, - fRemotePort: fRemotePort, - ); - } - } - } - - logger.info(terminateMessage); - Timer.periodic(Duration(seconds: params.idleTimeout), (timer) async { - if (counter == 0 || client.isClosed) { - timer.cancel(); - if (!client.isClosed) client.close(); - logger.shout( - '$sessionId | no active connections - ssh session complete'); - } - }); - return client; - } on SshnpError catch (_) { - rethrow; - } catch (e, s) { - throw SshnpError( - 'SSH Client failure : $e', - error: e, - stackTrace: s, - ); - } - } -} diff --git a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler.dart b/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler.dart deleted file mode 100644 index 95fbb68c7..000000000 --- a/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:async'; -import 'package:meta/meta.dart'; - -mixin SshnpInitialTunnelHandler { - @protected - @visibleForTesting - Future startInitialTunnel({required String identifier}); -} diff --git a/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart index f174514ed..00b36dc79 100644 --- a/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart +++ b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart @@ -47,7 +47,9 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { required this.params, required this.sessionId, required this.namespace, - }); + }) { + logger.level = params.verbose ? 'info' : 'shout'; + } /// Initialization starts the subscription to notifications from the daemon. @override @@ -88,11 +90,12 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { /// Returns true if the deamon acknowledged our request. /// Returns false if a timeout occurred. Future waitForDaemonResponse() async { - int counter = 0; // Timer to timeout after 10 Secs or after the Ack of connected/Errors - for (int i = 0; i < 100; i++) { - logger.info('Waiting for sshnpd response: $counter'); - logger.info('sshnpdAck: $sshnpdAck'); + for (int counter = 1; counter <= 100; counter++) { + if (counter % 20 == 0) { + logger.info('Still waiting for sshnpd response'); + logger.info('sshnpdAck: $sshnpdAck'); + } await Future.delayed(Duration(milliseconds: 100)); if (sshnpdAck != SshnpdAck.notAcknowledged) break; } @@ -225,6 +228,7 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { ..namespace = DefaultArgs.namespace ..metadata = metaData; + logger.info('Sending ping to sshnpd'); unawaited(notify(pingKey, 'ping')); } diff --git a/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart index c3ab88c0a..d2164083a 100644 --- a/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart +++ b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart @@ -39,6 +39,7 @@ abstract class SshrvdChannel with AsyncInitialization, AtClientBindings { int? _port; String get host => _host ?? params.host; + int get port => _port ?? params.port; // * Volatile fields set at runtime @@ -49,6 +50,7 @@ abstract class SshrvdChannel with AsyncInitialization, AtClientBindings { /// The port sshrvd is listening on int? _sshrvdPort; + int? get sshrvdPort => _sshrvdPort; SshrvdChannel({ @@ -56,7 +58,9 @@ abstract class SshrvdChannel with AsyncInitialization, AtClientBindings { required this.params, required this.sessionId, required this.sshrvGenerator, - }); + }) { + logger.level = params.verbose ? 'info' : 'shout'; + } @override Future initialize() async { @@ -110,12 +114,14 @@ abstract class SshrvdChannel with AsyncInitialization, AtClientBindings { logger.info('Sending notification to sshrvd: $ourSshrvdIdKey'); await notify(ourSshrvdIdKey, sessionId); - int counter = 0; + int counter = 1; while (sshrvdAck == SshrvdAck.notAcknowledged) { - logger.info('Waiting for sshrvd response: $counter'); + if (counter % 20 == 0) { + logger.info('Still waiting for sshrvd response'); + } await Future.delayed(Duration(milliseconds: 100)); counter++; - if (counter == 100) { + if (counter > 100) { logger.warning('Timed out waiting for sshrvd response'); throw ('Connection timeout to sshrvd $host service\nhint: make sure host is valid and online'); } diff --git a/packages/noports_core/lib/sshnp_foundation.dart b/packages/noports_core/lib/sshnp_foundation.dart index d26e5101f..59bedf3d3 100644 --- a/packages/noports_core/lib/sshnp_foundation.dart +++ b/packages/noports_core/lib/sshnp_foundation.dart @@ -24,9 +24,9 @@ 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/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/ssh_session_handler/ssh_session_handler.dart'; +export 'src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart'; +export 'src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart'; export 'src/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler.dart'; export 'src/sshnp/util/sshnp_ssh_key_handler/sshnp_local_ssh_key_handler.dart'; diff --git a/packages/noports_core/test/sshnp/sshnp_core_mocks.dart b/packages/noports_core/test/sshnp/sshnp_core_mocks.dart index 29c7f4ae5..3fdded77b 100644 --- a/packages/noports_core/test/sshnp/sshnp_core_mocks.dart +++ b/packages/noports_core/test/sshnp/sshnp_core_mocks.dart @@ -40,6 +40,14 @@ class StubbedSshnp extends SshnpCore with StubbedAsyncInitializationMixin { SshrvdChannel get sshrvdChannel => _sshrvdChannel ?? (throw UnimplementedError()); final SshrvdChannel? _sshrvdChannel; + + @override + bool get canRunShell => throw UnimplementedError(); + + @override + Future runShell() { + throw UnimplementedError(); + } } /// Stubbed mixin wrapper diff --git a/packages/noports_core/test/sshnp/sshnp_core_test.dart b/packages/noports_core/test/sshnp/sshnp_core_test.dart index 772052231..a7726a33b 100644 --- a/packages/noports_core/test/sshnp/sshnp_core_test.dart +++ b/packages/noports_core/test/sshnp/sshnp_core_test.dart @@ -67,8 +67,10 @@ void main() { test('verbose=false', () { whenConstructor(verbose: false); - final sshnpCore = - StubbedSshnp(atClient: mockAtClient, params: mockParams); + final sshnpCore = StubbedSshnp( + atClient: mockAtClient, + params: mockParams, + ); /// Expect that the namespace is set in the preferences verify(() => mockAtClient.getPreferences()).called(1); @@ -83,8 +85,10 @@ void main() { test('verbose=true', () { whenConstructor(verbose: true); - final sshnpCore = - StubbedSshnp(atClient: mockAtClient, params: mockParams); + final sshnpCore = StubbedSshnp( + atClient: mockAtClient, + params: mockParams, + ); /// Expect that the namespace is set in the preferences verify(() => mockAtClient.getPreferences()).called(1); diff --git a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_mocks.dart b/packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart similarity index 71% rename from packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_mocks.dart rename to packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart index bf52162b3..7fd24ec82 100644 --- a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_mocks.dart +++ b/packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart @@ -12,12 +12,11 @@ abstract class StartInitialTunnelCaller { class StartInitialTunnelStub extends Mock implements StartInitialTunnelCaller {} /// Stubbed Mixin that we are testing -mixin StubbedSshnpOpensshInitialTunnelHandler - on SshnpOpensshInitialTunnelHandler { +mixin StubbedSshnpOpensshSshSessionHandler on OpensshSshSessionHandler { late StartInitialTunnelStub _stubbedStartInitialTunnel; late StartProcessStub _stubbedStartProcess; - void stubSshnpOpensshInitialTunnelHandler({ + void stubSshnpOpensshSshSessionHandler({ required StartInitialTunnelStub stubbedStartInitialTunnel, required StartProcessStub stubbedStartProcess, }) { @@ -26,13 +25,13 @@ mixin StubbedSshnpOpensshInitialTunnelHandler } @override - Future startInitialTunnel({ - required String identifier, + Future startInitialTunnelSession({ + required String ephemeralKeyPairIdentifier, ProcessStarter startProcess = Process.start, }) { _stubbedStartInitialTunnel(); - return super.startInitialTunnel( - identifier: identifier, + return super.startInitialTunnelSession( + ephemeralKeyPairIdentifier: ephemeralKeyPairIdentifier, startProcess: _stubbedStartProcess.call, ); } @@ -40,9 +39,7 @@ mixin StubbedSshnpOpensshInitialTunnelHandler /// Stubbed Sshnp instance with the mixin class StubbedSshnp extends SshnpCore - with - SshnpOpensshInitialTunnelHandler, - StubbedSshnpOpensshInitialTunnelHandler { + with OpensshSshSessionHandler, StubbedSshnpOpensshSshSessionHandler { StubbedSshnp({ required super.atClient, required super.params, @@ -67,4 +64,17 @@ class StubbedSshnp extends SshnpCore @override SshrvdChannel get sshrvdChannel => _sshrvdChannel; final SshrvdChannel _sshrvdChannel; + + @override + Future startUserSession({required Process? tunnelSession}) { + throw UnimplementedError(); + } + + @override + bool get canRunShell => false; + + @override + Future runShell() { + throw UnimplementedError(); + } } diff --git a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_test.dart b/packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart similarity index 83% rename from packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_test.dart rename to packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart index 054e7c196..4734e9bdd 100644 --- a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_openssh_initial_tunnel_handler_test.dart +++ b/packages/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart @@ -6,10 +6,10 @@ import 'package:noports_core/sshnp_foundation.dart'; import 'package:test/test.dart'; import '../../sshnp_mocks.dart'; -import 'sshnp_openssh_initial_tunnel_handler_mocks.dart'; +import 'openssh_ssh_session_handler_mocks.dart'; void main() { - group('SshnpOpensshInitialTunnelHandler', () { + group('SshnpOpensshSshSessionHandler', () { late MockAtClient mockAtClient; late MockSshnpParams mockParams; late MockSshnpdChannel mockSshnpChannel; @@ -42,9 +42,9 @@ void main() { // TODO sshnpd channel mock calls }); - test('implements SshnpInitialTunnelHandler', () { - expect(stubbedSshnp, isA>()); + test('implements SshnpSshSessionHandler', () { + expect(stubbedSshnp, isA>()); }); // test public API test('startInitialTunnel', () {}); // test startInitialTunnel - }); // group SshnpOpensshInitialTunnelHandler + }); // group SshnpOpensshSshSessionHandler } diff --git a/packages/noports_core/test/sshnp/util/ssh_session_handler/ssh_session_handler_test.dart b/packages/noports_core/test/sshnp/util/ssh_session_handler/ssh_session_handler_test.dart new file mode 100644 index 000000000..1b87ce57c --- /dev/null +++ b/packages/noports_core/test/sshnp/util/ssh_session_handler/ssh_session_handler_test.dart @@ -0,0 +1,26 @@ +import 'package:noports_core/sshnp_foundation.dart'; +import 'package:test/test.dart'; +import 'package:mocktail/mocktail.dart'; + +class StubbedSshnpSshSessionHandler extends Mock + with SshSessionHandler {} + +void main() { + group('SshnpSshSessionHandler', () { + late final StubbedSshnpSshSessionHandler handler; + setUp(() { + handler = StubbedSshnpSshSessionHandler(); + }); + test('public API', () async { + when(() => handler.startInitialTunnelSession( + ephemeralKeyPairIdentifier: 'asdf')) + .thenAnswer((invocation) async => 'Called'); + + await expectLater( + await handler.startInitialTunnelSession( + ephemeralKeyPairIdentifier: 'asdf'), + 'Called', + ); + }); // test public API + }); // group SshnpSshSessionHandler +} diff --git a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler_test.dart b/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler_test.dart deleted file mode 100644 index c352299dd..000000000 --- a/packages/noports_core/test/sshnp/util/sshnp_initial_tunnel_handler/sshnp_initial_tunnel_handler_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:noports_core/sshnp_foundation.dart'; -import 'package:test/test.dart'; -import 'package:mocktail/mocktail.dart'; - -class StubbedSshnpInitialTunnelHandler extends Mock - with SshnpInitialTunnelHandler {} - -void main() { - group('SshnpInitialTunnelHandler', () { - late final StubbedSshnpInitialTunnelHandler handler; - setUp(() { - handler = StubbedSshnpInitialTunnelHandler(); - }); - test('public API', () async { - when(() => handler.startInitialTunnel(identifier: 'asdf')) - .thenAnswer((invocation) async => 'Called'); - - await expectLater( - await handler.startInitialTunnel(identifier: 'asdf'), - 'Called', - ); - }); // test public API - }); // group SshnpInitialTunnelHandler -} diff --git a/packages/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart b/packages/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart index 0a75a76f1..d95f72988 100644 --- a/packages/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart +++ b/packages/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart @@ -36,6 +36,14 @@ class StubbedSshnp extends SshnpCore with SshnpLocalSshKeyHandler { SshrvdChannel get sshrvdChannel => _sshrvdChannel ?? (throw UnimplementedError()); final SshrvdChannel? _sshrvdChannel; + + @override + bool get canRunShell => false; + + @override + Future runShell() { + throw UnimplementedError(); + } } class MockLocalSshKeyUtil extends Mock implements LocalSshKeyUtil {} diff --git a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart index 595e4a17b..b20a5ed76 100644 --- a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart @@ -37,6 +37,7 @@ void main() { setUp(() { mockAtClient = MockAtClient(); mockParams = MockSshnpParams(); + when(() => mockParams.verbose).thenReturn(false); sessionId = Uuid().v4(); notificationStreamController = StreamController(); notifyStub = NotifyStub(); diff --git a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart index bdc59dc81..d342d7932 100644 --- a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart @@ -41,6 +41,7 @@ void main() { notificationStreamController = StreamController(); subscribeStub = SubscribeStub(); + when(() => mockParams.verbose).thenReturn(false); when(() => mockParams.device).thenReturn(device); namespace = '$device.sshnp'; diff --git a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel_test.dart b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel_test.dart index ca2573c51..093272c91 100644 --- a/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel_test.dart @@ -34,6 +34,7 @@ void main() { notificationStreamController = StreamController(); subscribeStub = SubscribeStub(); + when(() => mockParams.verbose).thenReturn(false); when(() => mockParams.device).thenReturn(device); namespace = '$device.sshnp'; diff --git a/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart b/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart index 3fef43af8..9b460d318 100644 --- a/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart @@ -45,6 +45,7 @@ void main() { notifyStub = NotifyStub(); subscribeStub = SubscribeStub(); mockParams = MockSshnpParams(); + when(() => mockParams.verbose).thenReturn(false); sessionId = Uuid().v4(); mockSshrv = MockSshrv(); diff --git a/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart b/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart index 7ea836892..a8d558bc2 100644 --- a/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart @@ -1,3 +1,4 @@ +import 'package:mocktail/mocktail.dart'; import 'package:noports_core/sshnp_foundation.dart'; import 'package:socket_connector/socket_connector.dart'; import 'package:test/test.dart'; @@ -14,6 +15,7 @@ void main() { setUp(() { mockAtClient = MockAtClient(); mockSshnpParams = MockSshnpParams(); + when(() => mockSshnpParams.verbose).thenReturn(false); sessionId = Uuid().v4(); }); test('public API', () { diff --git a/packages/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart b/packages/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart index ba08ddae2..6fd0a1965 100644 --- a/packages/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart +++ b/packages/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart @@ -1,3 +1,4 @@ +import 'package:mocktail/mocktail.dart'; import 'package:noports_core/src/common/io_types.dart'; import 'package:noports_core/sshnp_foundation.dart'; import 'package:test/test.dart'; @@ -14,6 +15,8 @@ void main() { setUp(() { mockAtClient = MockAtClient(); mockSshnpParams = MockSshnpParams(); + when(() => mockSshnpParams.verbose).thenReturn(false); + sessionId = Uuid().v4(); }); test('public API', () { diff --git a/packages/sshnoports/bin/sshnp.dart b/packages/sshnoports/bin/sshnp.dart index 2d595bf89..1f6b0866d 100644 --- a/packages/sshnoports/bin/sshnp.dart +++ b/packages/sshnoports/bin/sshnp.dart @@ -1,14 +1,13 @@ // dart packages import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; // atPlatform packages import 'package:at_utils/at_logger.dart'; // local packages -import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshnp_params.dart' show ParserType, SshnpArg; -import 'package:noports_core/utils.dart'; +import 'package:noports_core/sshnp_foundation.dart'; import 'package:sshnoports/src/extended_arg_parser.dart'; import 'package:sshnoports/src/create_at_client_cli.dart'; import 'package:sshnoports/src/print_devices.dart'; @@ -64,7 +63,8 @@ void main(List args) async { rootDomain: params.rootDomain, ), legacyDaemon: argResults['legacy-daemon'] as bool, - sshClient: SupportedSshClient.fromString(argResults['ssh-client'] as String), + sshClient: + SupportedSshClient.fromString(argResults['ssh-client'] as String), ).catchError((e) { if (e.stackTrace != null) { Error.throwWithStackTrace(e, e.stackTrace!); @@ -87,10 +87,36 @@ void main(List args) async { } throw res; } - if (res is SshnpCommand || res is SshnpNoOpSuccess) { + if (res is SshnpNoOpSuccess) { stdout.write('$res\n'); exit(0); } + if (res is SshnpCommand) { + if (sshnp.canRunShell) { + // ignore: unused_local_variable + SshnpRemoteProcess shell = await sshnp.runShell(); + + shell.stdout.listen(stdout.add); + shell.stderr.listen(stderr.add); + + // don't wait for a newline before sending to remote stdin + stdin.lineMode = false; + // echo only what is sent back from the other side + stdin.echoMode = false; + stdin.listen(shell.stdin.add); + + // catch local ctrl-c's and forward to remote + ProcessSignal.sigint.watch().listen((signal) { + shell.stdin.add(Uint8List.fromList([3])); + }); + + await shell.done; + exit(0); + } else { + stdout.write('$res\n'); + exit(0); + } + } } on ArgumentError catch (error, stackTrace) { printUsage(error: error, stackTrace: stackTrace); exit(1); diff --git a/packages/sshnoports/lib/src/create_sshnp.dart b/packages/sshnoports/lib/src/create_sshnp.dart index 6dfdbe2ac..a90317316 100644 --- a/packages/sshnoports/lib/src/create_sshnp.dart +++ b/packages/sshnoports/lib/src/create_sshnp.dart @@ -36,7 +36,7 @@ Future createSshnp( ); case SupportedSshClient.dart: String identityFile = params.identityFile ?? - (throw SshnpError( + (throw ArgumentError( 'Identity file is mandatory when using the dart client.', )); String pemText = await File(identityFile).readAsString(); diff --git a/packages/sshnoports/lib/src/print_devices.dart b/packages/sshnoports/lib/src/print_devices.dart index 539c08abd..a6061d429 100644 --- a/packages/sshnoports/lib/src/print_devices.dart +++ b/packages/sshnoports/lib/src/print_devices.dart @@ -26,4 +26,4 @@ void printDeviceList(Iterable devices, Map info) { for (var device in devices) { stderr.writeln(' $device - v${info[device]?['version']}'); } -} \ No newline at end of file +} diff --git a/packages/sshnoports/pubspec.lock b/packages/sshnoports/pubspec.lock index dff1996b2..e2a4b97fe 100644 --- a/packages/sshnoports/pubspec.lock +++ b/packages/sshnoports/pubspec.lock @@ -564,10 +564,9 @@ packages: noports_core: dependency: "direct main" description: - name: noports_core - sha256: "6929f39f9d2cefc4dd06dbb5834a7632f6d8bc4bb3e378dd4b06f07ed98dc3fb" - url: "https://pub.dev" - source: hosted + path: "../noports_core" + relative: true + source: path version: "4.0.0-dev.4" openssh_ed25519: dependency: transitive