Skip to content

Commit

Permalink
Merge pull request #604 from atsign-foundation/gkc/pure-dart-interact…
Browse files Browse the repository at this point in the history
…ive-ssh
  • Loading branch information
gkc authored Dec 4, 2023
2 parents 2b70c6f + 6e7c143 commit 043b4d3
Show file tree
Hide file tree
Showing 34 changed files with 527 additions and 288 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ abstract interface class AtSshKeyUtil {

FutureOr<dynamic> addKeyPair({
required AtSshKeyPair keyPair,
required String identifier,
String? identifier,
});

FutureOr<dynamic> deleteKeyPair({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ class LocalSshKeyUtil implements AtSshKeyUtil {
@override
Future<List<File>> 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),
Expand Down
3 changes: 1 addition & 2 deletions packages/noports_core/lib/src/common/io_types.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,7 +15,6 @@ typedef ProcessRunner = Future<ProcessResult> Function(
String? workingDirectory,
});


@internal
typedef ProcessStarter = Future<Process> Function(
String executable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import 'package:meta/meta.dart';

mixin class AsyncDisposal {
// * Private members
bool _dispoalStarted = false;
bool _disposalStarted = false;
final Completer<void> _disposedCompleter = Completer<void>();

// * 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<void> get disposed => _disposedCompleter.future;
Expand All @@ -24,8 +24,8 @@ mixin class AsyncDisposal {

@protected
Future<void> callDisposal() async {
if (!_dispoalStarted) {
_dispoalStarted = true;
if (!_disposalStarted) {
_disposalStarted = true;
unawaited(dispose());
}
return disposed;
Expand Down
66 changes: 54 additions & 12 deletions packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,14 +36,22 @@ class SshnpDartPureImpl extends SshnpCore
Future<void> initialize() async {
if (!isSafeToInitialize) return;
await super.initialize();
if (params.identityFile != null) {
identityKeyPair =
await keyUtil.getKeyPair(identifier: params.identityFile!);
}
completeInitialization();
}

SSHClient? tunnelSshClient;

@override
Future<SshnpResult> run() async {
/// Ensure that sshnp is initialized
await callInitialization();

logger.info('Sending request to sshnpd');

/// Send an ssh request to sshnpd
await notify(
AtKey()
Expand Down Expand Up @@ -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);
Expand All @@ -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<SshnpRemoteProcess> 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<void> get done => sshSession.done;

@override
StreamSink<List<int>> get stdin => sshSession.stdin;

@override
Stream<List<int>> get stdout => sshSession.stdout;

@override
Stream<List<int>> get stderr => sshSession.stderr;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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);
Expand All @@ -126,4 +125,12 @@ class SshnpOpensshLocalImpl extends SshnpCore
connectionBean: bean,
);
}

@override
bool get canRunShell => false;

@override
Future<SshnpRemoteProcess> runShell() {
throw UnimplementedError('$runtimeType does not implement runShell');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,12 @@ class SshnpUnsignedImpl extends SshnpCore with SshnpLocalSshKeyHandler {
connectionBean: bean,
);
}

@override
bool get canRunShell => false;

@override
Future<SshnpRemoteProcess> runShell() {
throw UnimplementedError('$runtimeType does not implement runShell');
}
}
30 changes: 21 additions & 9 deletions packages/noports_core/lib/src/sshnp/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> get done;
Stream<List<int>> get stderr;
StreamSink<List<int>> get stdin;
Stream<List<int>> get stdout;
}

abstract interface class Sshnp {
/// Legacy v3.x.x client
@Deprecated(
Expand All @@ -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;
}
Expand All @@ -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<SshnpResult> 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<SshnpRemoteProcess> runShell();

/// Send a ping out to all sshnpd and listen for heartbeats
/// Returns two Iterable<String> and a Map<String, dynamic>:
/// - Iterable<String> of atSigns of sshnpd that responded
Expand Down
Loading

0 comments on commit 043b4d3

Please sign in to comment.