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: sshnp pure dart for direct ssh #443

Merged
merged 27 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c2485a4
feat: Interim commit
gkc Sep 10, 2023
e5a904b
feat: Use the ephemeral private key for the tunnel connection for 'di…
gkc Sep 10, 2023
3525d34
fix: fix file permissions on the tmp ephemeral private key
gkc Sep 10, 2023
d978123
fix: bug fixes to `directSshViaSSHClient` - (1) connect to `_sshrvdPo…
gkc Sep 10, 2023
36589fe
fix: fix control flow in SSHNPImpl.run()
gkc Sep 10, 2023
0167370
fix: omg newlines
gkc Sep 10, 2023
c4a0152
fix: tee stderr to console and to file so we can see sshnpd output in…
gkc Sep 10, 2023
10845f0
sigh
gkc Sep 10, 2023
73397ce
deep sigh
gkc Sep 10, 2023
8d63bad
diminishing returns due to tiredness now
gkc Sep 10, 2023
7fe77ec
wat
gkc Sep 10, 2023
3bd3dc6
fix: newlines were red herring
gkc Sep 10, 2023
3f5c627
Merge remote-tracking branch 'origin/trunk' into feat/sshnp-pure-dart…
gkc Sep 10, 2023
5acf678
test: revert changes to entrypoints and healthcheck
gkc Sep 10, 2023
df3b04e
build: add explicit dependency on archive package version 3.3.8 or hi…
gkc Sep 10, 2023
88c416f
fix: sshnpd: revert the behaviour in handlePublicKeyNotification when…
gkc Sep 11, 2023
d27434d
style: some format adjustments
gkc Sep 11, 2023
d1ff176
style: adjusted import in defaults.dart
gkc Sep 11, 2023
07b51e2
style: added a comma, ran dart format
gkc Sep 11, 2023
5cb1be4
Merge remote-tracking branch 'origin/trunk' into feat/sshnp-pure-dart…
gkc Sep 12, 2023
d764845
feat: add `addForwardsToTunnel` instance variable and command-line fl…
gkc Sep 12, 2023
5ad3a2b
feat: implement `addForwardsToTunnel` in `directSshViaExec`
gkc Sep 12, 2023
70bdaf8
feat: implement `addForwardsToTunnel` in `directSshViaExec`
gkc Sep 12, 2023
e1e82e8
refactor: re-ordered some code within SSHNPImpl.directSshViaSSHClient…
gkc Sep 12, 2023
cc31b14
style: format
gkc Sep 12, 2023
a64498f
fix: make directSshViaExec understand that localSshOptions is a list
gkc Sep 12, 2023
4d2c9a1
feat: implement `addForwardsToTunnel` in `directSshViaSSHClient` and …
gkc Sep 12, 2023
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
4 changes: 3 additions & 1 deletion packages/sshnoports/bin/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ void main(List<String> args) async {
await sshnp.init();
SSHNPResult res = await sshnp.run();
if (res is SSHNPFailed) {
stderr.write('$res\n');
exit(1);
}
if (res is SSHCommand) {
stdout.write('$res\n');
await sshnp.done;
exit(0);
}
exit(0);
}, (Object error, StackTrace stackTrace) async {
stderr.writeln(error.toString());

Expand Down
11 changes: 11 additions & 0 deletions packages/sshnoports/lib/common/defaults.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'package:sshnoports/sshrv/sshrv.dart';

const defaultVerbose = false;
const defaultRsa = false;
const defaultRootDomain = 'root.atsign.org';
const defaultSshrvGenerator = SSHRV.localBinary;
const defaultLocalSshdPort = 22;
const defaultRemoteSshdPort = 22;

/// value in seconds after which idle ssh tunnels will be closed
const defaultIdleTimeout = 15;
100 changes: 100 additions & 0 deletions packages/sshnoports/lib/common/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,106 @@ void assertValidValue(Map m, String k, Type t) {
}
}

Future<(String, String)> generateSshKeys(
{required bool rsa,
required String sessionId,
String? sshHomeDirectory}) async {
sshHomeDirectory ??=
getDefaultSshDirectory(getHomeDirectory(throwIfNull: true)!);
if (!Directory(sshHomeDirectory).existsSync()) {
Directory(sshHomeDirectory).createSync();
}

if (rsa) {
await Process.run('ssh-keygen',
['-t', 'rsa', '-b', '4096', '-f', '${sessionId}_sshnp', '-q', '-N', ''],
workingDirectory: sshHomeDirectory);
} else {
await Process.run(
'ssh-keygen',
[
'-t',
'ed25519',
'-a',
'100',
'-f',
'${sessionId}_sshnp',
'-q',
'-N',
''
],
workingDirectory: sshHomeDirectory);
}

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

return (sshPublicKey, sshPrivateKey);
}

Future<void> addEphemeralKeyToAuthorizedKeys(
{required String sshPublicKey,
required int localSshdPort,
String? homeDirectory,
String sessionId = '',
String permissions = ''}) async {
// Check to see if the ssh public key looks like one!
if (!sshPublicKey.startsWith('ssh-')) {
throw ('$sshPublicKey does not look like a public key');
}

homeDirectory ??= getHomeDirectory(throwIfNull: true)!;

var sshHomeDirectory =
'$homeDirectory/.ssh/'.replaceAll('/', Platform.pathSeparator);
if (!Directory(sshHomeDirectory).existsSync()) {
Directory(sshHomeDirectory).createSync();
}

// Check to see if the ssh Publickey is already in the authorized_keys file.
// If not, then append it.
var authKeys = File('${sshHomeDirectory}authorized_keys');

var authKeysContent = await authKeys.readAsString();
if (!authKeysContent.endsWith('\n')) {
await authKeys.writeAsString('\n', mode: FileMode.append);
}

if (!authKeysContent.contains(sshPublicKey)) {
if (permissions.isNotEmpty && !permissions.startsWith(',')) {
permissions = ',$permissions';
}
// Set up a safe authorized_keys file, for the ssh tunnel
await authKeys.writeAsString(
'command="echo \\"ssh session complete\\";sleep 20"'
',PermitOpen="localhost:$localSshdPort"'
'$permissions'
' '
'${sshPublicKey.trim()}'
' '
'$sessionId\n',
mode: FileMode.append);
}
}

Future<void> removeEphemeralKeyFromAuthorizedKeys(
String sshHomeDirectory, String sessionId, AtSignLogger logger) async {
try {
final File file = File('${sshHomeDirectory}authorized_keys');
// read into List of strings
final List<String> lines = await file.readAsLines();
// find the line we want to remove
lines.removeWhere((element) => element.contains(sessionId));
// Write back the file and add a \n
await file.writeAsString(lines.join('\n'));
await file.writeAsString('\n', mode: FileMode.writeOnlyAppend);
} catch (e) {
logger.severe('Unable to tidy up ${sshHomeDirectory}authorized_keys');
}
}

String signAndWrapAndJsonEncode(AtClient atClient, Map payload) {
Map envelope = {'payload': payload};

Expand Down
38 changes: 29 additions & 9 deletions packages/sshnoports/lib/sshnp/sshnp.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:at_client/at_client.dart' hide StringBuffer;
import 'package:at_commons/at_builders.dart';
import 'package:at_utils/at_logger.dart';
import 'package:at_utils/at_utils.dart';
import 'package:dartssh2/dartssh2.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
Expand All @@ -21,6 +22,8 @@ import 'package:sshnoports/sshrvd/sshrvd.dart';
import 'package:sshnoports/version.dart';
import 'package:uuid/uuid.dart';

import 'package:sshnoports/common/defaults.dart' as defaults;

part 'sshnp_impl.dart';
part 'sshnp_params.dart';
part 'sshnp_result.dart';
Expand Down Expand Up @@ -83,10 +86,13 @@ abstract class SSHNP {
abstract int localPort;

/// Port that local sshd is listening on localhost interface
/// Default set to 22

/// Default set to [defaultLocalSshdPort]
abstract int localSshdPort;

/// Port that the remote sshd is listening on localhost interface
/// Default set to [defaultRemoteSshdPort]
abstract int remoteSshdPort;

// ====================================================================
// Derived final instance variables, set during construction or init
// ====================================================================
Expand Down Expand Up @@ -146,19 +152,27 @@ abstract class SSHNP {

abstract final bool direct;

/// If ssh tunnel is unused (no active connections via port forwards) for
/// longer than this many seconds, then the connection will be closed.
/// Defaults to [defaults.defaultIdleTimeout]
abstract int idleTimeout;

/// The ssh client to use when doing outbound ssh within this program
abstract SupportedSshClient sshClient;

/// Completes when the SSHNP instance is no longer doing anything
/// e.g. controlling a direct ssh tunnel using the pure-dart SSHClient
Future<void> get done;

/// Default parameters for sshnp
static const defaultDevice = 'default';
static const defaultPort = 22;
static const defaultLocalPort = 0;
static const defaultSendSshPublicKey = '';
static const defaultLocalSshOptions = <String>[];
static const defaultVerbose = false;
static const defaultRsa = false;
static const defaultRootDomain = 'root.atsign.org';
static const defaultSshrvGenerator = SSHRV.localBinary;
static const defaultLocalSshdPort = 22;
static const defaultLegacyDaemon = true;
static const defaultListDevices = false;
static const defaultSshClient = SupportedSshClient.hostSsh;
XavierChanth marked this conversation as resolved.
Show resolved Hide resolved

factory SSHNP({
// final fields
Expand All @@ -177,9 +191,12 @@ abstract class SSHNP {
required int localPort,
String? remoteUsername,
bool verbose = false,
SSHRVGenerator sshrvGenerator = defaultSshrvGenerator,
int localSshdPort = defaultLocalSshdPort,
SSHRVGenerator sshrvGenerator = defaults.defaultSshrvGenerator,
int localSshdPort = defaults.defaultLocalSshdPort,
bool legacyDaemon = defaultLegacyDaemon,
int remoteSshdPort = defaults.defaultRemoteSshdPort,
int idleTimeout = defaults.defaultIdleTimeout,
required SupportedSshClient sshClient,
}) {
return SSHNPImpl(
atClient: atClient,
Expand All @@ -199,6 +216,9 @@ abstract class SSHNP {
sshrvGenerator: sshrvGenerator,
localSshdPort: localSshdPort,
legacyDaemon: legacyDaemon,
remoteSshdPort: remoteSshdPort,
idleTimeout: idleTimeout,
sshClient: sshClient,
);
}

Expand Down
54 changes: 41 additions & 13 deletions packages/sshnoports/lib/sshnp/sshnp_arg.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'package:sshnoports/common/supported_ssh_clients.dart';

import 'package:sshnoports/common/defaults.dart';
import 'sshnp.dart';

enum ArgFormat {
Expand All @@ -20,6 +23,7 @@ class SSHNPArg {
final bool mandatory;
final dynamic defaultsTo;
final ArgType type;
final Iterable<String>? allowed;

const SSHNPArg({
required this.name,
Expand All @@ -29,6 +33,7 @@ class SSHNPArg {
this.format = ArgFormat.option,
this.defaultsTo,
this.type = ArgType.string,
this.allowed,
});

String get bashName => name.replaceAll('-', '_').toUpperCase();
Expand Down Expand Up @@ -90,13 +95,12 @@ class SSHNPArg {
type: ArgType.integer,
),
SSHNPArg(
name: 'local-port',
abbr: 'l',
help:
'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port',
defaultsTo: SSHNP.defaultLocalPort,
type: ArgType.integer
),
name: 'local-port',
abbr: 'l',
help:
'Reverse ssh port to listen on, on your local machine, by sshnp default finds a spare port',
defaultsTo: SSHNP.defaultLocalPort,
type: ArgType.integer),
SSHNPArg(
name: 'ssh-public-key',
abbr: 's',
Expand All @@ -113,14 +117,14 @@ class SSHNPArg {
SSHNPArg(
name: 'verbose',
abbr: 'v',
defaultsTo: SSHNP.defaultVerbose,
defaultsTo: defaultVerbose,
help: 'More logging',
format: ArgFormat.flag,
),
SSHNPArg(
name: 'rsa',
abbr: 'r',
defaultsTo: SSHNP.defaultRsa,
defaultsTo: defaultRsa,
help: 'Use RSA 4096 keys rather than the default ED25519 keys',
format: ArgFormat.flag,
),
Expand All @@ -132,26 +136,50 @@ class SSHNPArg {
SSHNPArg(
name: 'root-domain',
help: 'atDirectory domain',
defaultsTo: SSHNP.defaultRootDomain,
defaultsTo: defaultRootDomain,
mandatory: false,
format: ArgFormat.option,
),
SSHNPArg(
name: 'local-sshd-port',
help: 'port sshd is listening locally on localhost',
defaultsTo: SSHNP.defaultLocalSshdPort,
help: 'port on which sshd is listening locally on the client host',
defaultsTo: defaultLocalSshdPort,
abbr: 'P',
mandatory: false,
format: ArgFormat.option,
type: ArgType.integer,
),

SSHNPArg(
name: 'legacy-daemon',
defaultsTo: SSHNP.defaultLegacyDaemon,
help: 'Request is to a legacy (< 3.5.0) noports daemon',
format: ArgFormat.flag,
),
SSHNPArg(
name: 'remote-sshd-port',
help: 'port on which sshd is listening locally on the device host',
defaultsTo: defaultRemoteSshdPort,
mandatory: false,
format: ArgFormat.option,
type: ArgType.integer,
),
SSHNPArg(
name: 'idle-timeout',
help:
'number of seconds after which inactive ssh connections will be closed',
defaultsTo: defaultIdleTimeout,
mandatory: false,
format: ArgFormat.option,
type: ArgType.integer,
),
SSHNPArg(
name: 'ssh-client',
help: 'What to use for outbound ssh connections',
defaultsTo: SupportedSshClient.hostSsh.cliArg,
mandatory: false,
format: ArgFormat.option,
type: ArgType.string,
allowed: SupportedSshClient.values.map((c) => c.cliArg).toList()),
];

@override
Expand Down
Loading