Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implemented #202 #208

Merged
merged 7 commits into from
Jun 15, 2023
Merged
Changes from 3 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
107 changes: 60 additions & 47 deletions lib/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ class SSHNP {
/// Defaults to false
final bool rsa;



// ====================================================================
// Volatile instance variables, injected via constructor
// but possibly modified later on
Expand Down Expand Up @@ -85,9 +83,10 @@ class SSHNP {
@visibleForTesting
late final String clientAtSign;

/// The username to use on the remote host in the ssh session. Is fetched
/// from the sshnpd by [fetchRemoteUserName] during [init]
late final String remoteUsername;
/// The username to use on the remote host in the ssh session. Either passed
/// through class constructor or Is fetched from the sshnpd
purnimavenkatasubbu marked this conversation as resolved.
Show resolved Hide resolved
/// by [fetchRemoteUserName] during [init]
String? remoteUsername;
purnimavenkatasubbu marked this conversation as resolved.
Show resolved Hide resolved

/// Set by [generateSshKeys] during [init].
/// sshnp generates a new keypair for each ssh session, using ed25519 by
Expand Down Expand Up @@ -148,15 +147,18 @@ class SSHNP {
// volatile fields
required this.host,
required this.port,
required this.localPort
required this.localPort,
this.remoteUsername,
}) {
nameSpace = '$device.sshnp';
clientAtSign = atClient.getCurrentAtSign()!;
logger.hierarchicalLoggingEnabled = true;
logger.logger.level = Level.SHOUT;

// Setup ssh keys location
sshHomeDirectory = '$homeDirectory${Platform.pathSeparator}.ssh${Platform.pathSeparator}';
if (! Directory(sshHomeDirectory).existsSync()) {
sshHomeDirectory =
'$homeDirectory${Platform.pathSeparator}.ssh${Platform.pathSeparator}';
if (!Directory(sshHomeDirectory).existsSync()) {
Directory(sshHomeDirectory).createSync();
}
}
Expand Down Expand Up @@ -185,12 +187,14 @@ class SSHNP {

await generateSshKeys();

await fetchRemoteUserName();
if (remoteUsername == null) {
await fetchRemoteUserName();
}

// find a spare local port
if (localPort == '0') {
ServerSocket serverSocket =
await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
localPort = serverSocket.port.toString();
await serverSocket.close();
}
Expand Down Expand Up @@ -226,16 +230,18 @@ class SSHNP {
..namespace = nameSpace
..sharedBy = clientAtSign
..sharedWith = sshnpdAtSign
..metadata = (Metadata()..ttr=-1..ttl=10000);
..metadata = (Metadata()
..ttr = -1
..ttl = 10000);

try {
await atClient.notificationService
.notify(NotificationParams.forUpdate(keyForCommandToSend, value: sshString),
await atClient.notificationService.notify(
NotificationParams.forUpdate(keyForCommandToSend, value: sshString),
onSuccess: (notification) {
logger.info('SUCCESS:$notification $sshString');
}, onError: (notification) {
logger.info('ERROR:$notification $sshString');
});
logger.info('SUCCESS:$notification $sshString');
}, onError: (notification) {
logger.info('ERROR:$notification $sshString');
});
} catch (e) {
stderr.writeln(e.toString());
}
Expand Down Expand Up @@ -285,8 +291,8 @@ class SSHNP {
String notificationKey = notification.key
.replaceAll('${notification.to}:', '')
.replaceAll('.$device.sshnp${notification.from}', '')
// convert to lower case as the latest AtClient converts notification
// keys to lower case when received
// convert to lower case as the latest AtClient converts notification
// keys to lower case when received
.toLowerCase();
logger.info('Received $notificationKey notification');
if (notification.value == 'connected') {
Expand All @@ -299,13 +305,14 @@ class SSHNP {
}
}


/// Look up the user name ... we expect a key to have been shared with us by
/// sshnpd. Let's say we are @human running sshnp, and @daemon is running
/// sshnpd, then we expect a key to have been shared whose ID is
/// @human:username.device.sshnp@daemon
/// fetches remote user name if not provided through class constructor
purnimavenkatasubbu marked this conversation as resolved.
Show resolved Hide resolved
Future<void> fetchRemoteUserName() async {
purnimavenkatasubbu marked this conversation as resolved.
Show resolved Hide resolved
AtKey userNameRecordID = AtKey.fromString('$clientAtSign:username.$nameSpace$sshnpdAtSign');
AtKey userNameRecordID =
AtKey.fromString('$clientAtSign:username.$nameSpace$sshnpdAtSign');
try {
remoteUsername = (await atClient.get(userNameRecordID)).value as String;
} catch (e) {
Expand Down Expand Up @@ -356,13 +363,13 @@ class SSHNP {
..ttl = 10000);

try {
await atClient.notificationService
.notify(NotificationParams.forUpdate(sendOurPrivateKeyToSshnpd, value: sshPrivateKey),
onSuccess: (notification) {
logger.info('SUCCESS:$notification');
}, onError: (notification) {
logger.info('ERROR:$notification');
});
await atClient.notificationService.notify(
NotificationParams.forUpdate(sendOurPrivateKeyToSshnpd,
value: sshPrivateKey), onSuccess: (notification) {
logger.info('SUCCESS:$notification');
}, onError: (notification) {
logger.info('ERROR:$notification');
});
} catch (e) {
stderr.writeln(e.toString());
}
Expand All @@ -385,20 +392,20 @@ class SSHNP {
..sharedBy = clientAtSign // shared by us
..sharedWith = host // shared with the sshrvd host
..metadata = (Metadata()
// as we are sending a notification to the sshrvd namespace,
// we don't want to append our namespace
// as we are sending a notification to the sshrvd namespace,
// we don't want to append our namespace
..namespaceAware = false
..ttr = -1
..ttl = 10000);

try {
await atClient.notificationService
.notify(NotificationParams.forUpdate(ourSshrvdIdKey, value: sessionId),
await atClient.notificationService.notify(
NotificationParams.forUpdate(ourSshrvdIdKey, value: sessionId),
onSuccess: (notification) {
logger.info('SUCCESS:$notification $ourSshrvdIdKey');
}, onError: (notification) {
logger.info('ERROR:$notification $ourSshrvdIdKey');
});
logger.info('SUCCESS:$notification $ourSshrvdIdKey');
}, onError: (notification) {
logger.info('ERROR:$notification $ourSshrvdIdKey');
});
} catch (e) {
stderr.writeln(e.toString());
}
Expand Down Expand Up @@ -453,9 +460,9 @@ class SSHNP {
}

sshPublicKey =
await File('$sshHomeDirectory${sessionId}_sshnp.pub').readAsString();
await File('$sshHomeDirectory${sessionId}_sshnp.pub').readAsString();
sshPrivateKey =
await File('$sshHomeDirectory${sessionId}_sshnp').readAsString();
await File('$sshHomeDirectory${sessionId}_sshnp').readAsString();

// Set up a safe authorized_keys file, for the reverse ssh tunnel
File('${sshHomeDirectory}authorized_keys').writeAsStringSync(
Expand Down Expand Up @@ -545,6 +552,7 @@ class SSHNP {
localSshOptions: results['local-ssh-options'] ?? [],
rsa: results['rsa'],
sendSshPublicKey: sendSshPublicKey,
remoteUsername: results['remote-username'],
);
if (results['verbose']) {
sshnp.logger.logger.level = Level.INFO;
Expand All @@ -561,9 +569,9 @@ class SSHNP {

static Future<AtClient> createAtClient(
{required String clientAtSign,
required String device,
required String sessionId,
required String atKeysFilePath}) async {
required String device,
required String sessionId,
required String atKeysFilePath}) async {
// Now on to the atPlatform startup
//onboarding preference builder can be used to set onboardingService parameters
AtOnboardingPreference atOnboardingConfig = AtOnboardingPreference()
Expand All @@ -576,8 +584,9 @@ class SSHNP {
..atKeysFilePath = atKeysFilePath
..atProtocolEmitted = Version(2, 0, 0);

AtOnboardingService onboardingService =
AtOnboardingServiceImpl(clientAtSign, atOnboardingConfig, atServiceFactory: ServiceFactoryWithNoOpSyncService());
AtOnboardingService onboardingService = AtOnboardingServiceImpl(
clientAtSign, atOnboardingConfig,
atServiceFactory: ServiceFactoryWithNoOpSyncService());

await onboardingService.authenticate();

Expand Down Expand Up @@ -609,26 +618,30 @@ class SSHNP {
mandatory: false,
defaultsTo: '22',
help:
'TCP port to connect back to (only required if --host specified a FQDN/IP)');
'TCP port to connect back to (only required if --host specified a FQDN/IP)');
parser.addOption('local-port',
abbr: 'l',
defaultsTo: '0',
mandatory: false,
help:
'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port');
'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port');
parser.addOption('ssh-public-key',
abbr: 's',
defaultsTo: 'false',
mandatory: false,
help:
'Public key file from ~/.ssh to be appended to authorized_hosts on the remote device');
'Public key file from ~/.ssh to be appended to authorized_hosts on the remote device');
parser.addMultiOption('local-ssh-options',
abbr: 'o', help: 'Add these commands to the local ssh command');
parser.addFlag('verbose', abbr: 'v', help: 'More logging');
parser.addFlag('rsa',
abbr: 'r',
defaultsTo: false,
help: 'Use RSA 4096 keys rather than the default ED25519 keys');
parser.addOption('remote-user-name',
abbr: 'u',
mandatory: false,
help: 'user name to use in the ssh session on the remote host');
return parser;
}

Expand All @@ -641,7 +654,7 @@ class SSHNP {
late String sshnpDir;
if (Platform.executable.endsWith('${Platform.pathSeparator}sshnp')) {
List<String> pathList =
Platform.resolvedExecutable.split(Platform.pathSeparator);
Platform.resolvedExecutable.split(Platform.pathSeparator);
pathList.removeLast();
sshnpDir = pathList.join(Platform.pathSeparator) + Platform.pathSeparator;

Expand Down