diff --git a/.github/workflows/end2end_tests.yaml b/.github/workflows/end2end_tests.yaml index 1f3e12284..6ee09e4ca 100644 --- a/.github/workflows/end2end_tests.yaml +++ b/.github/workflows/end2end_tests.yaml @@ -192,172 +192,6 @@ jobs: docker compose down # Test suite 2 - # Installer tests (local vs installer) - e2e_installer_test: - # Don't run on forks (cause no secrets), don't run if dependebot (cause no secrets) - if: ${{ github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'}} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - np: [local, installer] - npd: [local, installer] - exclude: - # Don't run these against themselves, pointless to test - - np: local - npd: local - steps: - - name: Show Matrix Values - run: | - echo "job index: ${{ strategy.job-index }}" - echo "np: ${{ matrix.np }}" - echo "npd: ${{ matrix.npd }}" - - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - - name: Setup Devicename - # First two guarantee a unique # per workflow call - # Last two guarantee a unique # per job per strategy in matrix - run: | - echo "DEVICENAME=${{ github.run_id }}${{ github.run_attempt }}2${{ strategy.job-index }}" >> $GITHUB_ENV - - - name: Setup NP/NPD key env - run: | - SSHNP_ATKEYS="$(tr '[:lower:]' '[:upper:]' <<< '${{ env.SSHNP_ATSIGN }}')" - echo "SSHNP_ATKEYS=ATKEYS_${SSHNP_ATKEYS:1}" >> $GITHUB_ENV - - SSHNPD_ATKEYS="$(tr '[:lower:]' '[:upper:]' <<< '${{ env.SSHNPD_ATSIGN }}')" - echo "SSHNPD_ATKEYS=ATKEYS_${SSHNPD_ATKEYS:1}" >> $GITHUB_ENV - - - name: Setup NP/NPD keys - working-directory: tests/end2end_tests/contexts - run: | - echo "${{ secrets[env.SSHNP_ATKEYS] }}" > sshnp/.atsign/keys/${{ env.SSHNP_ATSIGN }}_key.atKeys - echo "${{ secrets[env.SSHNPD_ATKEYS] }}" > sshnpd/.atsign/keys/${{ env.SSHNPD_ATSIGN }}_key.atKeys - - - name: Set up entrypoints - uses: ./.github/composite/setup_entrypoints - with: - sshnp: ${{ matrix.np }} - sshnp_atsign: ${{ env.SSHNP_ATSIGN }} - sshnpd: ${{ matrix.npd }} - sshnpd_atsign: ${{ env.SSHNPD_ATSIGN }} - sshrvd_atsign: ${{ env[env.PROD_RVD_ATSIGN] }} - devicename: ${{ env.DEVICENAME }} - - - name: Ensure entrypoints exist - working-directory: tests/end2end_tests/contexts - run: | - cat sshnp/entrypoint.sh - cat sshnpd/entrypoint.sh - cat sshrvd/entrypoint.sh - - - name: Create docker-compose.yaml - working-directory: tests/end2end_tests/tests - run: | - cat docker-compose-base.yaml > docker-compose.yaml - - - name: Add runtime-sshnp-installer image to docker-compose.yaml - working-directory: tests/end2end_tests/tests - if: ${{ matrix.np == 'installer' }} - run: | - cat service-image-runtime-sshnp-installer.yaml >> docker-compose.yaml - echo ' - client_atsign="${{ env.SSHNP_ATSIGN }}"' >> docker-compose.yaml - echo ' - device_atsign="${{ env.SSHNPD_ATSIGN }}"' >> docker-compose.yaml - echo ' - host_atsign="${{ env[env.PROD_RVD_ATSIGN] }}"' >> docker-compose.yaml - echo ' image: atsigncompany/sshnp-e2e-runtime:sshnp-installer' >> docker-compose.yaml - - - name: Add runtime-sshnpd-installer image to docker-compose.yaml - working-directory: tests/end2end_tests/tests - if: ${{ matrix.npd == 'installer' }} - run: | - cat service-image-runtime-sshnpd-installer.yaml >> docker-compose.yaml - echo ' - client_atsign="${{ env.SSHNP_ATSIGN }}"' >> docker-compose.yaml - echo ' - device_atsign="${{ env.SSHNPD_ATSIGN }}"' >> docker-compose.yaml - echo ' - device_name=${{ env.DEVICENAME }}' >> docker-compose.yaml - echo ' image: atsigncompany/sshnp-e2e-runtime:sshnpd-installer' >> docker-compose.yaml - - - name: Add container-sshnp to docker-compose.yaml - working-directory: tests/end2end_tests/tests - run: | - # Add the base service - cat service-container-sshnp.yaml >> docker-compose.yaml - # Add the runtime - if [ "${{ matrix.np }}" = 'installer' ]; then - echo ' image: atsigncompany/sshnp-e2e-runtime:sshnp-installer' >> docker-compose.yaml - else - echo ' image: atsigncompany/sshnp-e2e-runtime:${{ matrix.np }}' >> docker-compose.yaml - fi - - # Add the dependencies - echo ' depends_on:' >> docker-compose.yaml - echo ' container-sshnpd:' >> docker-compose.yaml - echo ' condition: service_healthy' >> docker-compose.yaml - if [ "${{ matrix.np }}" = 'installer' ]; then - echo ' image-runtime-sshnp-installer:' >> docker-compose.yaml - else - echo ' image-runtime-local:' >> docker-compose.yaml - fi - echo ' condition: service_started' >> docker-compose.yaml - - - name: Add container-sshnpd to docker-compose.yaml - working-directory: tests/end2end_tests/tests - run: | - # Add the base service - cat service-container-sshnpd.yaml >> docker-compose.yaml - # Add the runtime - if [ "${{ matrix.npd }}" = 'installer' ]; then - echo ' image: atsigncompany/sshnp-e2e-runtime:sshnpd-installer' >> docker-compose.yaml - else - echo ' image: atsigncompany/sshnp-e2e-runtime:${{ matrix.npd }}' >> docker-compose.yaml - fi - - # Add the dependencies - echo ' depends_on:' >> docker-compose.yaml - if [ "${{ matrix.npd }}" = 'installer' ]; then - echo ' - image-runtime-sshnpd-installer' >> docker-compose.yaml - else - echo ' - image-runtime-local' >> docker-compose.yaml - fi - - - name: docker-compose.yaml - if: always() - working-directory: tests/end2end_tests/tests - run: | - cat docker-compose.yaml - - - name: Build - working-directory: tests/end2end_tests/tests - run: | - docker compose build - - - name: Test - working-directory: tests/end2end_tests/tests - run: | - ${{ env.DOCKER_COMPOSE_UP_CMD }} - - - name: Logs - if: always() - working-directory: tests/end2end_tests/tests - run: | - docker compose ps -a - docker compose logs --timestamps - - - name: Found "Test Passed" in Logs - if: always() - working-directory: tests/end2end_tests/tests - run: | - docker compose logs --timestamps | grep -q "Test Passed$" - - - name: Tear down - # Always tear down outside of the act environment - # but don't tear down on failure in the act environment - if: ${{ !env.ACT }} || success() - working-directory: tests/end2end_tests/tests - run: | - docker compose down - - # Test suite 3 # Backward compatibility tests e2e_release_test: # Don't run on push and on pull, meant to be ran manually (workflow dispatch) diff --git a/.github/workflows/unit_tests.yaml b/.github/workflows/unit_tests.yaml index 61edc8405..192a71d27 100644 --- a/.github/workflows/unit_tests.yaml +++ b/.github/workflows/unit_tests.yaml @@ -19,3 +19,17 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: ./.github/composite/verify_cli_tags + noports_core-unit_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d # v1.6.0 + - name: dart pub get + working-directory: packages/noports_core + run: dart pub get + - name: dart analyze + working-directory: packages/noports_core + run: dart analyze + - name: dart test + working-directory: packages/noports_core + run: dart test diff --git a/packages/noports_core/lib/src/common/ssh_key_utils.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart similarity index 69% rename from packages/noports_core/lib/src/common/ssh_key_utils.dart rename to packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart index 789cd1b5f..09e45e976 100644 --- a/packages/noports_core/lib/src/common/ssh_key_utils.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/at_ssh_key_util.dart @@ -2,20 +2,38 @@ import 'dart:async'; import 'dart:convert'; import 'package:dartssh2/dartssh2.dart'; -import 'package:meta/meta.dart'; import 'package:noports_core/sshnp.dart'; import 'package:noports_core/utils.dart'; import 'package:path/path.dart' as path; -export 'ssh_key_utils/dart_ssh_key_util.dart'; -export 'ssh_key_utils/local_ssh_key_util.dart'; +export 'dart_ssh_key_util.dart'; +export 'local_ssh_key_util.dart'; -class AtSSHKeyPair { - @protected +abstract interface class AtSshKeyUtil { + FutureOr generateKeyPair({ + required String identifier, + SupportedSshAlgorithm algorithm, + }); + + FutureOr getKeyPair({ + required String identifier, + }); + + FutureOr addKeyPair({ + required AtSshKeyPair keyPair, + required String identifier, + }); + + FutureOr deleteKeyPair({ + required String identifier, + }); +} + +class AtSshKeyPair { final SSHKeyPair keyPair; final String identifier; - AtSSHKeyPair.fromPem( + AtSshKeyPair.fromPem( String pemText, { required String identifier, String? directory, @@ -34,20 +52,4 @@ class AtSSHKeyPair { String get privateKeyFileName => identifier; String get publicKeyFileName => '$privateKeyFileName.pub'; - - // TODO consider adding this function - // void destroy() { - // throw UnimplementedError(); - // } -} - -abstract interface class AtSSHKeyUtil { - FutureOr generateKeyPair({ - required String identifier, - SupportedSSHAlgorithm algorithm, - }); - - FutureOr getKeyPair({ - required String identifier, - }); } diff --git a/packages/noports_core/lib/src/common/ssh_key_utils/dart_ssh_key_util.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart similarity index 50% rename from packages/noports_core/lib/src/common/ssh_key_utils/dart_ssh_key_util.dart rename to packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart index b7f8b84f4..657788903 100644 --- a/packages/noports_core/lib/src/common/ssh_key_utils/dart_ssh_key_util.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/dart_ssh_key_util.dart @@ -5,19 +5,19 @@ import 'package:cryptography/cryptography.dart'; import 'package:noports_core/utils.dart'; import 'package:openssh_ed25519/openssh_ed25519.dart'; -class DartSSHKeyUtil implements AtSSHKeyUtil { - static final Map _keyPairCache = {}; +class DartSshKeyUtil implements AtSshKeyUtil { + static final Map _keyPairCache = {}; @override - Future generateKeyPair({ + Future generateKeyPair({ required String identifier, - SupportedSSHAlgorithm algorithm = DefaultArgs.sshAlgorithm, + SupportedSshAlgorithm algorithm = DefaultArgs.sshAlgorithm, }) async { - AtSSHKeyPair keyPair; + AtSshKeyPair keyPair; switch (algorithm) { - case SupportedSSHAlgorithm.rsa: + case SupportedSshAlgorithm.rsa: keyPair = _generateRSAKeyPair(identifier); - case SupportedSSHAlgorithm.ed25519: + case SupportedSshAlgorithm.ed25519: keyPair = await _generateEd25519KeyPair(identifier); } _keyPairCache[identifier] = keyPair; @@ -25,24 +25,38 @@ class DartSSHKeyUtil implements AtSSHKeyUtil { } @override - Future getKeyPair({required String identifier}) async { - return _keyPairCache[identifier] ?? await generateKeyPair(identifier: identifier); + Future getKeyPair({required String identifier}) async { + return _keyPairCache[identifier] ?? + await generateKeyPair(identifier: identifier); } - AtSSHKeyPair _generateRSAKeyPair(String identifier) => AtSSHKeyPair.fromPem( + AtSshKeyPair _generateRSAKeyPair(String identifier) => AtSshKeyPair.fromPem( AtChopsUtil.generateRSAKeyPair(keySize: 4096).privateKey.toPEM(), identifier: identifier, ); - Future _generateEd25519KeyPair(String identifier) async { + Future _generateEd25519KeyPair(String identifier) async { var keyPair2 = await Ed25519().newKeyPair(); var pemText = encodeEd25519Private( privateBytes: await keyPair2.extractPrivateKeyBytes(), publicBytes: (await keyPair2.extractPublicKey()).bytes, ); - return AtSSHKeyPair.fromPem( + return AtSshKeyPair.fromPem( pemText, identifier: identifier, ); } + + @override + FutureOr addKeyPair({ + required AtSshKeyPair keyPair, + required String identifier, + }) { + _keyPairCache[identifier] = keyPair; + } + + @override + FutureOr deleteKeyPair({required String identifier}) { + _keyPairCache.remove(identifier); + } } diff --git a/packages/noports_core/lib/src/common/ssh_key_utils/local_ssh_key_util.dart b/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart similarity index 87% rename from packages/noports_core/lib/src/common/ssh_key_utils/local_ssh_key_util.dart rename to packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart index bfddb2a28..04562b4ee 100644 --- a/packages/noports_core/lib/src/common/ssh_key_utils/local_ssh_key_util.dart +++ b/packages/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart @@ -6,17 +6,17 @@ import 'package:noports_core/utils.dart'; import 'package:path/path.dart' as path; import 'package:posix/posix.dart' show chmod; -class LocalSSHKeyUtil implements AtSSHKeyUtil { +class LocalSshKeyUtil implements AtSshKeyUtil { static const _sshKeygenArgMap = { - SupportedSSHAlgorithm.rsa: ['-t', 'rsa', '-b', '4096'], - SupportedSSHAlgorithm.ed25519: ['-t', 'ed25519', '-a', '100'], + SupportedSshAlgorithm.rsa: ['-t', 'rsa', '-b', '4096'], + SupportedSshAlgorithm.ed25519: ['-t', 'ed25519', '-a', '100'], }; - static final Map _keyPairCache = {}; + static final Map _keyPairCache = {}; final String homeDirectory; bool cacheKeys; - LocalSSHKeyUtil({String? homeDirectory, this.cacheKeys = true}) + LocalSshKeyUtil({String? homeDirectory, this.cacheKeys = true}) : homeDirectory = homeDirectory ?? getHomeDirectory(throwIfNull: true)!; bool get isValidPlatform => @@ -27,6 +27,8 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { String get _defaultDirectory => sshnpHomeDirectory; + String get username => getUserName(throwIfNull: true)!; + List _filesFromIdentifier({required String identifier}) { return [ File(path.normalize(identifier)), @@ -34,8 +36,9 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { ]; } + @override Future> addKeyPair({ - required AtSSHKeyPair keyPair, + required AtSshKeyPair keyPair, required String identifier, }) async { var files = _filesFromIdentifier(identifier: identifier); @@ -51,13 +54,13 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { } @override - Future getKeyPair( + Future getKeyPair( {required String identifier, String? passphrase}) async { if (_keyPairCache.containsKey((identifier))) { return _keyPairCache[(identifier)]!; } var files = _filesFromIdentifier(identifier: identifier); - var keyPair = AtSSHKeyPair.fromPem( + var keyPair = AtSshKeyPair.fromPem( await files[0].readAsString(), identifier: identifier, passphrase: passphrase, @@ -69,6 +72,7 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { return keyPair; } + @override Future> deleteKeyPair( {required String identifier}) async { var files = _filesFromIdentifier(identifier: identifier); @@ -79,9 +83,9 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { } @override - Future generateKeyPair({ + Future generateKeyPair({ required String identifier, - SupportedSSHAlgorithm algorithm = DefaultArgs.sshAlgorithm, + SupportedSshAlgorithm algorithm = DefaultArgs.sshAlgorithm, String? directory, String? passphrase, }) async { @@ -96,7 +100,7 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { String pemText = await File(path.join(workingDirectory, identifier)).readAsString(); - return AtSSHKeyPair.fromPem( + return AtSshKeyPair.fromPem( pemText, passphrase: passphrase, directory: directory, @@ -160,7 +164,7 @@ class LocalSSHKeyUtil implements AtSSHKeyUtil { await file.writeAsString(lines.join('\n')); await file.writeAsString('\n', mode: FileMode.writeOnlyAppend); } catch (e) { - throw SSHNPError( + throw SshnpError( 'Failed to remove ephemeral key from authorized_keys', error: e, ); diff --git a/packages/noports_core/lib/src/common/default_args.dart b/packages/noports_core/lib/src/common/default_args.dart index 3ed4b50e4..4665a1ade 100644 --- a/packages/noports_core/lib/src/common/default_args.dart +++ b/packages/noports_core/lib/src/common/default_args.dart @@ -5,15 +5,14 @@ import 'package:noports_core/sshrv.dart'; class DefaultArgs { static const String namespace = 'sshnp'; - static const SupportedSSHAlgorithm sshAlgorithm = - SupportedSSHAlgorithm.ed25519; + static const SupportedSshAlgorithm sshAlgorithm = + SupportedSshAlgorithm.ed25519; static const bool verbose = false; - static const bool algorithm = false; + static const SupportedSshAlgorithm algorithm = SupportedSshAlgorithm.ed25519; static const String rootDomain = 'root.atsign.org'; - static const SSHRVGenerator sshrvGenerator = SSHRV.exec; + static const SshrvGenerator sshrvGenerator = Sshrv.exec; static const int localSshdPort = 22; static const int remoteSshdPort = 22; - /// value in seconds after which idle ssh tunnels will be closed static const int idleTimeout = 15; static const bool help = false; @@ -22,7 +21,7 @@ class DefaultArgs { Platform.isLinux || Platform.isMacOS || Platform.isWindows; } -class DefaultSSHNPArgs { +class DefaultSshnpArgs { static const String device = 'default'; static const int port = 22; static const int localPort = 0; @@ -33,6 +32,6 @@ class DefaultSSHNPArgs { static const SupportedSshClient sshClient = SupportedSshClient.exec; } -class DefaultSSHNPDArgs { +class DefaultSshnpdArgs { static const SupportedSshClient sshClient = SupportedSshClient.exec; } diff --git a/packages/noports_core/lib/src/common/mixins/async_completion.dart b/packages/noports_core/lib/src/common/mixins/async_completion.dart new file mode 100644 index 000000000..8c484d88e --- /dev/null +++ b/packages/noports_core/lib/src/common/mixins/async_completion.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +mixin class AsyncDisposal { + // * Private members + bool _dispoalStarted = false; + final Completer _disposedCompleter = Completer(); + + // * Public members + + /// Used to check if disposal has started + bool get disposalStarted => _dispoalStarted; + + /// Used to check if disposal has completed + Future get disposed => _disposedCompleter.future; + + // * Protected members + + /// Used to check if it is safe to do a disposal step + /// (i.e. if disposal has not yet completed) + @protected + bool get isSafeToDispose => !_disposedCompleter.isCompleted; + + @protected + Future callDisposal() async { + if (!_dispoalStarted) { + _dispoalStarted = true; + unawaited(dispose()); + } + return disposed; + } + + /// To be overridden by the class that implements this mixin + /// to perform disposal steps. Do not call this method directly. + /// Instead, call [callDisposal] to ensure that disposal + /// is only done once. + /// + /// hint: call [completeDisposal] at the end of this method + /// to signal completion of the disposal process + /// + /// hint: call [isSafeToDispose] at the beginning of this method + /// to ensure that disposal is not done more than once + @visibleForOverriding + @protected + Future dispose() async {} + + /// To be called by the class that implements this mixin + /// to signal completion of the disposal process + /// hint: call this in the last line of [dispose] + @protected + void completeDisposal() { + if (isSafeToDispose) _disposedCompleter.complete(); + } +} diff --git a/packages/noports_core/lib/src/common/mixins/async_initialization.dart b/packages/noports_core/lib/src/common/mixins/async_initialization.dart new file mode 100644 index 000000000..e1108dad9 --- /dev/null +++ b/packages/noports_core/lib/src/common/mixins/async_initialization.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +mixin class AsyncInitialization { + // * Private members + bool _initializeStarted = false; + final Completer _initializedCompleter = Completer(); + + // * Public members + + /// Used to check if initialization has started + bool get initalizeStarted => _initializeStarted; + + /// Used to check if initialization has completed + Future get initialized => _initializedCompleter.future; + + // * Protected members + + /// Used to check if it is safe to do an initialization step + /// (i.e. if initialization has not yet completed) + @protected + bool get isSafeToInitialize => !_initializedCompleter.isCompleted; + + /// To be called by the class that implements this mixin + /// to ensure that [initialize] is only called once + @protected + Future callInitialization() async { + if (!_initializeStarted) { + _initializeStarted = true; + unawaited(initialize()); + } + return initialized; + } + + /// To be overridden by the class that implements this mixin + /// to perform initialization steps. Do not call this method directly. + /// Instead, call [callInitialization] to ensure that initialization + /// is only done once. + /// + /// hint: call [completeInitialization] at the end of this method + /// to signal completion of the initialization process + /// + /// hint: call [isSafeToInitialize] at the beginning of this method + /// to ensure that initialization is not done more than once + @visibleForOverriding + @protected + Future initialize() async {} + + /// To be called by the class that implements this mixin + /// to signal completion of the initialization process + /// hint: call this in the last line of [initialize] + @protected + void completeInitialization() { + if (isSafeToInitialize) _initializedCompleter.complete(); + } +} diff --git a/packages/noports_core/lib/src/common/mixins/at_client_bindings.dart b/packages/noports_core/lib/src/common/mixins/at_client_bindings.dart new file mode 100644 index 000000000..9422be2e1 --- /dev/null +++ b/packages/noports_core/lib/src/common/mixins/at_client_bindings.dart @@ -0,0 +1,20 @@ +import 'package:at_client/at_client.dart'; +import 'package:at_utils/at_utils.dart'; + +mixin AtClientBindings { + AtClient get atClient; + AtSignLogger get logger; + + Future notify( + AtKey atKey, + String value, + ) async { + await atClient.notificationService + .notify(NotificationParams.forUpdate(atKey, value: value), + onSuccess: (NotificationResult notification) { + logger.info('SUCCESS:$notification with key: ${atKey.toString()}'); + }, onError: (notification) { + logger.info('ERROR:$notification'); + }); + } +} diff --git a/packages/noports_core/lib/src/common/types.dart b/packages/noports_core/lib/src/common/types.dart index 28933f6a7..49f770e7c 100644 --- a/packages/noports_core/lib/src/common/types.dart +++ b/packages/noports_core/lib/src/common/types.dart @@ -1,6 +1,6 @@ import 'package:noports_core/sshrv.dart'; -typedef SSHRVGenerator = SSHRV Function(String, int, {int localSshdPort}); +typedef SshrvGenerator = Sshrv Function(String, int, {int localSshdPort}); enum SupportedSshClient { exec(cliArg: '/usr/bin/ssh'), @@ -20,15 +20,15 @@ enum SupportedSshClient { String toString() => _cliArg; } -enum SupportedSSHAlgorithm { +enum SupportedSshAlgorithm { ed25519(cliArg: 'ssh-ed25519'), rsa(cliArg: 'ssh-rsa'); final String _cliArg; - const SupportedSSHAlgorithm({required String cliArg}) : _cliArg = cliArg; + const SupportedSshAlgorithm({required String cliArg}) : _cliArg = cliArg; - factory SupportedSSHAlgorithm.fromString(String cliArg) { - return SupportedSSHAlgorithm.values.firstWhere( + factory SupportedSshAlgorithm.fromString(String cliArg) { + return SupportedSshAlgorithm.values.firstWhere( (arg) => arg._cliArg == cliArg, orElse: () => throw ArgumentError('Unsupported SSH algorithm: $cliArg'), ); @@ -37,4 +37,3 @@ enum SupportedSSHAlgorithm { @override String toString() => _cliArg; } - diff --git a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward.dart b/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward.dart deleted file mode 100644 index 32c961bc6..000000000 --- a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'dart:async'; - -import 'package:at_client/at_client.dart'; -import 'package:noports_core/src/sshnp/sshnp_core.dart'; -import 'package:noports_core/src/sshnp/sshnp_result.dart'; -import 'package:noports_core/utils.dart'; - -abstract class SSHNPForward extends SSHNPCore { - SSHNPForward({ - required super.atClient, - required super.params, - super.shouldInitialize, - }); - - // Direct ssh is only ever done with a sshrvd host - // So we should expect that sshrvdPort is never null - // Hence overriding the getter and setter to make it non-nullable - late int _sshrvdPort; - - @override - int get sshrvdPort => _sshrvdPort; - - @override - set sshrvdPort(int? port) => _sshrvdPort = port!; - - Future requestSocketTunnelFromDaemon() async { - logger.info( - 'Requesting daemon to set up socket tunnel for direct ssh session'); -// send request to the daemon via notification - await notify( - AtKey() - ..key = 'ssh_request' - ..namespace = this.namespace - ..sharedBy = clientAtSign - ..sharedWith = sshnpdAtSign - ..metadata = (Metadata()..ttl = 10000), - signAndWrapAndJsonEncode( - atClient, - { - 'direct': true, - 'sessionId': sessionId, - 'host': host, - 'port': port, - }, - ), - ); - - bool acked = await waitForDaemonResponse(); - if (!acked) { - var error = SSHNPError( - 'sshnp timed out: waiting for daemon response\nhint: make sure the device is online', - stackTrace: StackTrace.current); - doneCompleter.completeError(error); - return error; - } - - if (sshnpdAckErrors) { - var error = SSHNPError('sshnp failed: with sshnpd acknowledgement errors', - stackTrace: StackTrace.current); - doneCompleter.completeError(error); - return error; - } - - return null; - } -} diff --git a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_local_impl.dart b/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_local_impl.dart deleted file mode 100644 index 0676eb74b..000000000 --- a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_local_impl.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'dart:async'; - -import 'package:dartssh2/dartssh2.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnp_ssh_key_handler.dart'; -import 'package:noports_core/src/sshnp/sshnp_result.dart'; -import 'package:noports_core/sshnp_core.dart'; - -class SSHNPForwardDartLocalImpl extends SSHNPForwardDart - with SSHNPLocalSSHKeyHandler { - SSHNPForwardDartLocalImpl({ - required super.atClient, - required super.params, - super.shouldInitialize, - }); - - @override - Future init() async { - logger.info('Initializing SSHNPForwardDartLocalImpl'); - await super.init(); - completeInitialization(); - } - - @override - Future run() async { - // TODO consider starting the tunnel in a separate isolate - SSHClient client = await startInitialTunnel(); - - return SSHNPCommand( - localPort: localPort, - remoteUsername: remoteUsername, - host: 'localhost', - privateKeyFileName: identityKeyPair?.privateKeyFileName, - localSshOptions: - (params.addForwardsToTunnel) ? null : params.localSshOptions, - connectionBean: client, - ); - } -} diff --git a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_pure_impl.dart b/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_pure_impl.dart deleted file mode 100644 index f51023f5b..000000000 --- a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart_pure_impl.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:async'; - -import 'package:dartssh2/dartssh2.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnp_ssh_key_handler.dart'; -import 'package:noports_core/src/sshnp/sshnp_result.dart'; -import 'package:noports_core/sshnp_core.dart'; -import 'package:noports_core/utils.dart'; - -class SSHNPForwardDartPureImpl extends SSHNPForwardDart - with SSHNPDartSSHKeyHandler { - final AtSSHKeyPair _identityKeyPair; - - @override - AtSSHKeyPair get identityKeyPair => _identityKeyPair; - - SSHNPForwardDartPureImpl({ - required super.atClient, - required super.params, - required AtSSHKeyPair identityKeyPair, - super.shouldInitialize, - }) : _identityKeyPair = identityKeyPair; - - @override - Future init() async { - logger.info('Initializing SSHNPForwardDartPureImpl'); - await super.init(); - completeInitialization(); - } - - @override - Future run() async { - SSHClient client = await startInitialTunnel(); - // Todo: consider returning a SSHNPCommand instead of a SSHNPNoOpSuccess - return SSHNPNoOpSuccess( - message: 'Connection established:\n$terminateMessage', - connectionBean: client, - ); - } -} diff --git a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_exec_impl.dart b/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_exec_impl.dart deleted file mode 100644 index b3833d892..000000000 --- a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_exec_impl.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:at_client/at_client.dart' hide StringBuffer; - -import 'package:noports_core/src/sshnp/forward_direction/sshnp_forward.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnpd_payload_handler.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnp_ssh_key_handler.dart'; -import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshnp_params.dart'; -import 'package:noports_core/utils.dart'; - -class SSHNPForwardExecImpl extends SSHNPForward - with SSHNPLocalSSHKeyHandler, DefaultSSHNPDPayloadHandler { - late AtSSHKeyPair ephemeralKeyPair; - - SSHNPForwardExecImpl({ - required AtClient atClient, - required SSHNPParams params, - bool? shouldInitialize, - }) : super( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); - - @override - Future init() async { - logger.info('Initializing SSHNPForwardExecImpl'); - logger.info('params: ${params.toJson(parserType: ParserType.commandLine)}'); - await super.init(); - completeInitialization(); - } - - @override - Future run() async { - await startAndWaitForInit(); - - var error = await requestSocketTunnelFromDaemon(); - if (error != null) { - return error; - } - - ephemeralKeyPair = AtSSHKeyPair.fromPem( - ephemeralPrivateKey, - identifier: 'ephemeral_$sessionId', - directory: keyUtil.sshnpHomeDirectory, - ); - - logger.info( - 'Starting direct ssh session to $host on port $sshrvdPort with forwardLocal of $localPort'); - - try { - String? errorMessage; - Process? process; - - await keyUtil.addKeyPair( - keyPair: ephemeralKeyPair, - identifier: ephemeralKeyPair.identifier, - ); - - String argsString = '$remoteUsername@$host' - ' -p $sshrvdPort' - ' -i ${ephemeralKeyPair.privateKeyFileName}' - ' -L $localPort:localhost:${params.remoteSshdPort}' - ' -o LogLevel=VERBOSE' - ' -t -t' - ' -o StrictHostKeyChecking=accept-new' - ' -o IdentitiesOnly=yes' - ' -o BatchMode=yes' - ' -o ExitOnForwardFailure=yes' - ' -f' // fork after authentication - this is important - ; - if (params.addForwardsToTunnel) { - argsString += ' ${params.localSshOptions.join(' ')}'; - } - argsString += ' sleep 15'; - - List 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 - late int sshExitCode; - 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) {}); - sshExitCode = await process.exitCode.timeout(Duration(seconds: 10)); - // ignore: unused_catch_clause - } on TimeoutException catch (e) { - sshExitCode = 6464; - } - - await keyUtil.deleteKeyPair( - identifier: ephemeralKeyPair.identifier, - ); - - if (sshExitCode != 0) { - if (sshExitCode == 6464) { - logger.shout( - '$sessionId | Command timed out: /usr/bin/ssh ${args.join(' ')}'); - errorMessage = 'Failed to establish connection - timed out'; - } else { - logger.shout('$sessionId | Exit code $sshExitCode from' - ' /usr/bin/ssh ${args.join(' ')}'); - errorMessage = - 'Failed to establish connection - exit code $sshExitCode'; - } - throw SSHNPError(errorMessage); - } - - doneCompleter.complete(); - return SSHNPCommand( - localPort: localPort, - remoteUsername: remoteUsername, - host: 'localhost', - privateKeyFileName: identityKeyPair?.privateKeyFileName, - localSshOptions: - (params.addForwardsToTunnel) ? null : params.localSshOptions, - connectionBean: process, - ); - } on SSHNPError catch (e) { - doneCompleter.completeError(e, e.stackTrace); - return e; - } catch (e, s) { - doneCompleter.completeError(e, s); - return SSHNPError( - 'SSH Client failure : $e', - error: e, - stackTrace: s, - ); - } - } -} diff --git a/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_local_impl.dart b/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_local_impl.dart new file mode 100644 index 000000000..66c15e2fd --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_local_impl.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +import 'package:dartssh2/dartssh2.dart'; +import 'package:noports_core/sshnp_foundation.dart'; + +class SshnpDartLocalImpl extends SshnpCore + with SshnpLocalSshKeyHandler, SshnpDartInitialTunnelHandler { + SshnpDartLocalImpl({ + required super.atClient, + required super.params, + }) { + _sshnpdChannel = SshnpdDefaultChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + namespace: this.namespace, + ); + _sshrvdChannel = SshrvdDartChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + ); + } + + @override + SshnpdDefaultChannel get sshnpdChannel => _sshnpdChannel; + late final SshnpdDefaultChannel _sshnpdChannel; + + @override + SshrvdDartChannel get sshrvdChannel => _sshrvdChannel; + late final SshrvdDartChannel _sshrvdChannel; + + @override + Future initialize() async { + if (!isSafeToInitialize) return; + await super.initialize(); + completeInitialization(); + } + + @override + Future run() async { + /// Ensure that sshnp is initialized + await callInitialization(); + + /// Send an ssh request to sshnpd + await notify( + AtKey() + ..key = 'ssh_request' + ..namespace = this.namespace + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..metadata = (Metadata()..ttl = 10000), + signAndWrapAndJsonEncode(atClient, { + 'direct': true, + 'sessionId': sessionId, + 'host': sshrvdChannel.host, + 'port': sshrvdChannel.port, + }), + ); + + /// Wait for a response from sshnpd + var acked = await sshnpdChannel.waitForDaemonResponse(); + if (acked != SshnpdAck.acknowledged) { + throw SshnpError('sshnpd did not acknowledge the request'); + } + + /// Load the ephemeral private key into a key pair + AtSshKeyPair ephemeralKeyPair = AtSshKeyPair.fromPem( + sshnpdChannel.ephemeralPrivateKey, + identifier: 'ephemeral_$sessionId', + directory: keyUtil.sshnpHomeDirectory, + ); + + /// Add the key pair to the key utility + await keyUtil.addKeyPair( + keyPair: ephemeralKeyPair, + identifier: ephemeralKeyPair.identifier, + ); + + /// Start the initial tunnel + SSHClient bean = + await startInitialTunnel(identifier: ephemeralKeyPair.identifier); + + /// Remove the key pair from the key utility + await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); + + /// Ensure that we clean up after ourselves + await callDisposal(); + + /// Return the command to be executed externally + return SshnpCommand( + localPort: localPort, + host: 'localhost', + remoteUsername: remoteUsername, + localSshOptions: + (params.addForwardsToTunnel) ? null : params.localSshOptions, + privateKeyFileName: identityKeyPair?.identifier, + connectionBean: bean, + ); + } +} 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 new file mode 100644 index 000000000..18abca850 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +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, + }) { + _sshnpdChannel = SshnpdDefaultChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + namespace: this.namespace, + ); + _sshrvdChannel = SshrvdDartChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + ); + } + + @override + SshnpdDefaultChannel get sshnpdChannel => _sshnpdChannel; + late final SshnpdDefaultChannel _sshnpdChannel; + + @override + SshrvdDartChannel get sshrvdChannel => _sshrvdChannel; + late final SshrvdDartChannel _sshrvdChannel; + + @override + Future initialize() async { + if (!isSafeToInitialize) return; + await super.initialize(); + completeInitialization(); + } + + @override + Future run() async { + /// Ensure that sshnp is initialized + await callInitialization(); + + /// Send an ssh request to sshnpd + await notify( + AtKey() + ..key = 'ssh_request' + ..namespace = this.namespace + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..metadata = (Metadata()..ttl = 10000), + signAndWrapAndJsonEncode(atClient, { + 'direct': true, + 'sessionId': sessionId, + 'host': sshrvdChannel.host, + 'port': sshrvdChannel.port, + }), + ); + + /// Wait for a response from sshnpd + var acked = await sshnpdChannel.waitForDaemonResponse(); + if (acked != SshnpdAck.acknowledged) { + throw SshnpError('sshnpd did not acknowledge the request'); + } + + /// Load the ephemeral private key into a key pair + AtSshKeyPair ephemeralKeyPair = AtSshKeyPair.fromPem( + sshnpdChannel.ephemeralPrivateKey, + identifier: 'ephemeral_$sessionId', + ); + + /// Add the key pair to the key utility + await keyUtil.addKeyPair( + keyPair: ephemeralKeyPair, + identifier: ephemeralKeyPair.identifier, + ); + + /// Start the initial tunnel + SSHClient bean = + await startInitialTunnel(identifier: ephemeralKeyPair.identifier); + + /// Remove the key pair from the key utility + await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); + + /// Ensure that we clean up after ourselves + await callDisposal(); + + /// Return the command to be executed externally + return SshnpCommand( + localPort: localPort, + host: 'localhost', + remoteUsername: remoteUsername, + localSshOptions: + (params.addForwardsToTunnel) ? null : params.localSshOptions, + privateKeyFileName: identityKeyPair?.identifier, + connectionBean: bean, + ); + } +} diff --git a/packages/noports_core/lib/src/sshnp/impl/sshnp_exec_local_impl.dart b/packages/noports_core/lib/src/sshnp/impl/sshnp_exec_local_impl.dart new file mode 100644 index 000000000..68b96440a --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_exec_local_impl.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +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({ + required super.atClient, + required super.params, + }) { + _sshnpdChannel = SshnpdDefaultChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + namespace: this.namespace, + ); + _sshrvdChannel = SshrvdExecChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + ); + } + + @override + SshnpdDefaultChannel get sshnpdChannel => _sshnpdChannel; + late final SshnpdDefaultChannel _sshnpdChannel; + + @override + SshrvdExecChannel get sshrvdChannel => _sshrvdChannel; + late final SshrvdExecChannel _sshrvdChannel; + + @override + Future initialize() async { + if (!isSafeToInitialize) return; + await super.initialize(); + completeInitialization(); + } + + @override + Future run() async { + /// Ensure that sshnp is initialized + await callInitialization(); + + /// Send an ssh request to sshnpd + await notify( + AtKey() + ..key = 'ssh_request' + ..namespace = this.namespace + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..metadata = (Metadata()..ttl = 10000), + signAndWrapAndJsonEncode(atClient, { + 'direct': true, + 'sessionId': sessionId, + 'host': sshrvdChannel.host, + 'port': sshrvdChannel.port, + }), + ); + + /// Wait for a response from sshnpd + var acked = await sshnpdChannel.waitForDaemonResponse(); + if (acked != SshnpdAck.acknowledged) { + throw SshnpError('sshnpd did not acknowledge the request'); + } + + /// Load the ephemeral private key into a key pair + AtSshKeyPair ephemeralKeyPair = AtSshKeyPair.fromPem( + sshnpdChannel.ephemeralPrivateKey, + identifier: 'ephemeral_$sessionId', + directory: keyUtil.sshnpHomeDirectory, + ); + + /// Add the key pair to the key utility + await keyUtil.addKeyPair( + keyPair: ephemeralKeyPair, + identifier: ephemeralKeyPair.identifier, + ); + + /// Start the initial tunnel + Process bean = + await startInitialTunnel(identifier: ephemeralKeyPair.identifier); + + /// Remove the key pair from the key utility + await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); + + /// Ensure that we clean up after ourselves + await callDisposal(); + + /// Return the command to be executed externally + return SshnpCommand( + localPort: localPort, + host: 'localhost', + remoteUsername: remoteUsername, + localSshOptions: + (params.addForwardsToTunnel) ? null : params.localSshOptions, + privateKeyFileName: identityKeyPair?.identifier, + connectionBean: bean, + ); + } +} 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 new file mode 100644 index 000000000..a47f56839 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +import 'package:noports_core/sshnp_foundation.dart'; + +class SshnpUnsignedImpl extends SshnpCore with SshnpLocalSshKeyHandler { + SshnpUnsignedImpl({ + required super.atClient, + required super.params, + }) { + _sshnpdChannel = SshnpdUnsignedChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + namespace: this.namespace, + ); + _sshrvdChannel = SshrvdExecChannel( + atClient: atClient, + params: params, + sessionId: sessionId, + ); + } + + @override + SshnpdUnsignedChannel get sshnpdChannel => _sshnpdChannel; + late final SshnpdUnsignedChannel _sshnpdChannel; + + @override + SshrvdExecChannel get sshrvdChannel => _sshrvdChannel; + late final SshrvdExecChannel _sshrvdChannel; + + @override + Future initialize() async { + if (!isSafeToInitialize) return; + await super.initialize(); + + /// Generate an ephemeral key pair for this session + AtSshKeyPair ephemeralKeyPair = await keyUtil.generateKeyPair( + identifier: 'ephemeral_$sessionId', + directory: keyUtil.sshnpHomeDirectory, + ); + + /// Authorize the public key so sshnpd can connect to us + await keyUtil.authorizePublicKey( + sshPublicKey: ephemeralKeyPair.publicKeyContents, + localSshdPort: params.localSshdPort, + sessionId: sessionId, + ); + + /// Share our private key with sshnpd so it can connect to us + AtKey sendOurPrivateKeyToSshnpd = AtKey() + ..key = 'privatekey' + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..namespace = this.namespace + ..metadata = (Metadata()..ttl = 10000); + await notify( + sendOurPrivateKeyToSshnpd, + ephemeralKeyPair.privateKeyContents, + ); + + completeInitialization(); + } + + @override + Future run() async { + /// Ensure that sshnp is initialized + await callInitialization(); + + /// Start sshrv + var bean = await sshrvdChannel.runSshrv(); + + /// Send an sshd request to sshnpd + /// This will notify it that it can now connect to us + await notify( + AtKey() + ..key = 'sshd' + ..namespace = this.namespace + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..metadata = (Metadata()..ttl = 10000), + '$localPort ${sshrvdChannel.port} ${keyUtil.username} ${sshrvdChannel.host} $sessionId', + ); + + /// Wait for a response from sshnpd + var acked = await sshnpdChannel.waitForDaemonResponse(); + if (acked != SshnpdAck.acknowledged) { + throw SshnpError('sshnpd did not acknowledge the request'); + } + + /// Ensure that we clean up after ourselves + await callDisposal(); + + /// Return the command to be executed externally + return SshnpCommand( + localPort: localPort, + host: 'localhost', + remoteUsername: remoteUsername, + localSshOptions: + (params.addForwardsToTunnel) ? null : params.localSshOptions, + privateKeyFileName: identityKeyPair?.identifier, + connectionBean: bean, + ); + } +} diff --git a/packages/noports_core/lib/src/sshnp/mixins/sshnp_ssh_key_handler.dart b/packages/noports_core/lib/src/sshnp/mixins/sshnp_ssh_key_handler.dart deleted file mode 100644 index 5db53c608..000000000 --- a/packages/noports_core/lib/src/sshnp/mixins/sshnp_ssh_key_handler.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:noports_core/src/sshnp/sshnp_core.dart'; -import 'package:noports_core/src/sshnp/sshnp_result.dart'; -import 'package:noports_core/utils.dart'; - -mixin SSHNPLocalSSHKeyHandler on SSHNPCore { - final LocalSSHKeyUtil _sshKeyUtil = LocalSSHKeyUtil(); - @override - LocalSSHKeyUtil get keyUtil => _sshKeyUtil; - - AtSSHKeyPair? _identityKeyPair; - - @override - AtSSHKeyPair? get identityKeyPair => _identityKeyPair; - - @override - Future init() async { - logger.info('Initializing SSHNPLocalSSHKeyHandler'); - if (!keyUtil.isValidPlatform) { - throw SSHNPError( - 'The current platform is not supported: ${Platform.operatingSystem}'); - } - - if (params.identityFile != null) { - logger.info('Loading identity key pair from ${params.identityFile}'); - _identityKeyPair = await keyUtil.getKeyPair( - identifier: params.identityFile!, - passphrase: params.identityPassphrase, - ); - } - - /// Make sure we set the keyPair before calling [super.init()] - /// so that the keyPair is available in [SSHNPCore] to share to the daemon - await super.init(); - } -} - -mixin SSHNPDartSSHKeyHandler on SSHNPForwardDart { - final DartSSHKeyUtil _sshKeyUtil = DartSSHKeyUtil(); - @override - DartSSHKeyUtil get keyUtil => _sshKeyUtil; -} diff --git a/packages/noports_core/lib/src/sshnp/sshnp_params/config_file_repository.dart b/packages/noports_core/lib/src/sshnp/models/config_file_repository.dart similarity index 92% rename from packages/noports_core/lib/src/sshnp/sshnp_params/config_file_repository.dart rename to packages/noports_core/lib/src/sshnp/models/config_file_repository.dart index f963d4d94..09b3fda41 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_params/config_file_repository.dart +++ b/packages/noports_core/lib/src/sshnp/models/config_file_repository.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:noports_core/src/common/file_system_utils.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/sshnp_params.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/sshnp_arg.dart'; +import 'package:noports_core/src/sshnp/models/sshnp_params.dart'; +import 'package:noports_core/src/sshnp/models/sshnp_arg.dart'; import 'package:path/path.dart' as path; class ConfigFileRepository { @@ -56,13 +56,13 @@ class ConfigFileRepository { return profileNames; } - static Future getParams(String profileName, + static Future getParams(String profileName, {String? directory}) async { var fileName = await fromProfileName(profileName, directory: directory); - return SSHNPParams.fromFile(fileName); + return SshnpParams.fromFile(fileName); } - static Future putParams(SSHNPParams params, + static Future putParams(SshnpParams params, {String? directory, bool overwrite = false}) async { if (params.profileName == null || params.profileName!.isEmpty) { throw Exception('profileName is null or empty'); @@ -87,7 +87,7 @@ class ConfigFileRepository { ); } - static Future deleteParams(SSHNPParams params, + static Future deleteParams(SshnpParams params, {String? directory}) async { if (params.profileName == null || params.profileName!.isEmpty) { throw Exception('profileName is null or empty'); @@ -133,7 +133,7 @@ class ConfigFileRepository { var key = parts[0].trim(); var value = parts[1].trim(); - SSHNPArg arg = SSHNPArg.fromBashName(key); + SshnpArg arg = SshnpArg.fromBashName(key); if (arg.name.isEmpty) continue; if (!ParserType.configFile.shouldParse(arg.parseWhen)) continue; switch (arg.format) { diff --git a/packages/noports_core/lib/src/sshnp/sshnp_params/config_key_repository.dart b/packages/noports_core/lib/src/sshnp/models/config_key_repository.dart similarity index 61% rename from packages/noports_core/lib/src/sshnp/sshnp_params/config_key_repository.dart rename to packages/noports_core/lib/src/sshnp/models/config_key_repository.dart index 087a34d3f..26f0c1067 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_params/config_key_repository.dart +++ b/packages/noports_core/lib/src/sshnp/models/config_key_repository.dart @@ -1,42 +1,48 @@ import 'package:at_client/at_client.dart'; +import 'package:meta/meta.dart'; import 'package:noports_core/src/common/default_args.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/sshnp_params.dart'; +import 'package:noports_core/src/sshnp/models/sshnp_params.dart'; class ConfigKeyRepository { - static const String _keyPrefix = 'profile_'; - static const String _configNamespace = 'profiles.${DefaultArgs.namespace}'; + @visibleForTesting + static const String keyPrefix = 'profile_'; + + @visibleForTesting + static const String configNamespace = 'profiles.${DefaultArgs.namespace}'; static String toProfileName(AtKey atKey, {bool replaceSpaces = true}) { var profileName = atKey.key!.split('.').first; - profileName = profileName.replaceFirst(_keyPrefix, ''); + profileName = profileName.replaceFirst(keyPrefix, ''); if (replaceSpaces) profileName = profileName.replaceAll('_', ' '); return profileName; } - static AtKey fromProfileName(String profileName, {String sharedBy = '', bool replaceSpaces = true}) { + static AtKey fromProfileName(String profileName, + {String sharedBy = '', bool replaceSpaces = true}) { if (replaceSpaces) profileName = profileName.replaceAll(' ', '_'); return AtKey.self( - '$_keyPrefix$profileName', - namespace: _configNamespace, + '$keyPrefix$profileName', + namespace: configNamespace, sharedBy: sharedBy, ).build(); } static Future> listProfiles(AtClient atClient) async { - var keys = await atClient.getAtKeys(regex: _configNamespace); + var keys = await atClient.getAtKeys(regex: configNamespace); return keys.map((e) => toProfileName(e)); } - static Future getParams(String profileName, + static Future getParams(String profileName, {required AtClient atClient, GetRequestOptions? options}) async { AtKey key = fromProfileName(profileName); key.sharedBy = atClient.getCurrentAtSign()!; AtValue value = await atClient.get(key, getRequestOptions: options); - if (value.value == null) return SSHNPParams.empty(); - return SSHNPParams.fromJson(value.value!); + if (value.value == null) return SshnpParams.empty(); + return SshnpParams.fromJson(value.value!); } - static Future putParams(SSHNPParams params, {required AtClient atClient, PutRequestOptions? options}) async { + static Future putParams(SshnpParams params, + {required AtClient atClient, PutRequestOptions? options}) async { AtKey key = fromProfileName(params.profileName!); key.sharedBy = atClient.getCurrentAtSign()!; await atClient.put(key, params.toJson(), putRequestOptions: options); diff --git a/packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_arg.dart b/packages/noports_core/lib/src/sshnp/models/sshnp_arg.dart similarity index 80% rename from packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_arg.dart rename to packages/noports_core/lib/src/sshnp/models/sshnp_arg.dart index 4e4feeb11..84c62cbfe 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_arg.dart +++ b/packages/noports_core/lib/src/sshnp/models/sshnp_arg.dart @@ -46,7 +46,7 @@ enum ParserType { } } -class SSHNPArg { +class SshnpArg { final ArgFormat format; final String name; @@ -61,7 +61,7 @@ class SSHNPArg { final bool negatable; final bool hide; - const SSHNPArg({ + const SshnpArg({ required this.name, this.abbr, this.help, @@ -81,25 +81,25 @@ class SSHNPArg { List get aliasList => ['--$name', ...aliases?.map((e) => '--$e') ?? [], '-$abbr']; - factory SSHNPArg.noArg() { - return SSHNPArg(name: ''); + factory SshnpArg.noArg() { + return SshnpArg(name: ''); } - factory SSHNPArg.fromName(String name) { + factory SshnpArg.fromName(String name) { return args.firstWhere( (arg) => arg.name == name, - orElse: () => SSHNPArg.noArg(), + orElse: () => SshnpArg.noArg(), ); } - factory SSHNPArg.fromBashName(String bashName) { + factory SshnpArg.fromBashName(String bashName) { return args.firstWhere( (arg) => arg.bashName == bashName, - orElse: () => SSHNPArg.noArg(), + orElse: () => SshnpArg.noArg(), ); } - static final List args = [ + static final List args = [ profileNameArg, helpArg, keyFileArg, @@ -121,7 +121,7 @@ class SSHNPArg { remoteSshdPortArg, idleTimeoutArg, sshClientArg, - ssHAlgorithmArg, + sshAlgorithmArg, addForwardsToTunnelArg, configFileArg, listDevicesArg, @@ -129,7 +129,7 @@ class SSHNPArg { @override String toString() { - return 'SSHNPArg{format: $format, name: $name, abbr: $abbr, help: $help, mandatory: $mandatory, defaultsTo: $defaultsTo, type: $type}'; + return 'SshnpArg{format: $format, name: $name, abbr: $abbr, help: $help, mandatory: $mandatory, defaultsTo: $defaultsTo, type: $type}'; } static ArgParser createArgParser({ @@ -140,7 +140,7 @@ class SSHNPArg { }) { var parser = ArgParser(); // Basic arguments - for (SSHNPArg arg in SSHNPArg.args) { + for (SshnpArg arg in SshnpArg.args) { if (!parserType.shouldParse(arg.parseWhen)) { continue; } @@ -182,110 +182,110 @@ class SSHNPArg { return parser; } - static const profileNameArg = SSHNPArg( + static const profileNameArg = SshnpArg( name: 'profile-name', help: 'Name of the profile to use', parseWhen: ParseWhen.configFile, ); - static const helpArg = SSHNPArg( + static const helpArg = SshnpArg( name: 'help', help: 'Print this usage information', defaultsTo: DefaultArgs.help, format: ArgFormat.flag, parseWhen: ParseWhen.commandLine, ); - static const keyFileArg = SSHNPArg( + static const keyFileArg = SshnpArg( name: 'key-file', abbr: 'k', help: 'Sending atSign\'s atKeys file if not in ~/.atsign/keys/', parseWhen: ParseWhen.commandLine, ); - static const fromArg = SSHNPArg( + static const fromArg = SshnpArg( name: 'from', abbr: 'f', help: 'Sending (a.k.a. client) atSign', mandatory: true, ); - static const toArg = SSHNPArg( + static const toArg = SshnpArg( name: 'to', abbr: 't', help: 'Receiving device atSign', mandatory: true, ); - static const deviceArg = SSHNPArg( + static const deviceArg = SshnpArg( name: 'device', abbr: 'd', help: 'Receiving device name', - defaultsTo: DefaultSSHNPArgs.device, + defaultsTo: DefaultSshnpArgs.device, ); - static const hostArg = SSHNPArg( + static const hostArg = SshnpArg( name: 'host', abbr: 'h', help: 'atSign of sshrvd daemon or FQDN/IP address to connect back to', mandatory: true, ); - static const portArg = SSHNPArg( + static const portArg = SshnpArg( name: 'port', abbr: 'p', help: 'TCP port to connect back to (only required if --host specified a FQDN/IP)', - defaultsTo: DefaultSSHNPArgs.port, + defaultsTo: DefaultSshnpArgs.port, type: ArgType.integer, ); - static const localPortArg = SSHNPArg( + static const localPortArg = 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: DefaultSSHNPArgs.localPort, + defaultsTo: DefaultSshnpArgs.localPort, type: ArgType.integer, ); - static const identityFileArg = SSHNPArg( + static const identityFileArg = SshnpArg( name: 'identity-file', abbr: 'i', help: 'Identity file to use for ssh connection', parseWhen: ParseWhen.commandLine, ); - static const identityPassphraseArg = SSHNPArg( + static const identityPassphraseArg = SshnpArg( name: 'identity-passphrase', help: 'Passphrase for identity file', parseWhen: ParseWhen.commandLine, ); - static const sendSshPublicKeyArg = SSHNPArg( + static const sendSshPublicKeyArg = SshnpArg( name: 'send-ssh-public-key', abbr: 's', help: 'When true, the ssh public key will be sent to the remote host for use in the ssh session', - defaultsTo: DefaultSSHNPArgs.sendSshPublicKey, + defaultsTo: DefaultSshnpArgs.sendSshPublicKey, format: ArgFormat.flag, ); - static const localSshOptionsArg = SSHNPArg( + static const localSshOptionsArg = SshnpArg( name: 'local-ssh-options', abbr: 'o', - defaultsTo: DefaultSSHNPArgs.localSshOptions, + defaultsTo: DefaultSshnpArgs.localSshOptions, help: 'Add these commands to the local ssh command', format: ArgFormat.multiOption, ); - static const verboseArg = SSHNPArg( + static const verboseArg = SshnpArg( name: 'verbose', abbr: 'v', defaultsTo: DefaultArgs.verbose, help: 'More logging', format: ArgFormat.flag, ); - static const remoteUserNameArg = SSHNPArg( + static const remoteUserNameArg = SshnpArg( name: 'remote-user-name', abbr: 'u', help: 'username to use in the ssh session on the remote host', ); - static const rootDomainArg = SSHNPArg( + static const rootDomainArg = SshnpArg( name: 'root-domain', help: 'atDirectory domain', defaultsTo: DefaultArgs.rootDomain, mandatory: false, format: ArgFormat.option, ); - static const localSshdPortArg = SSHNPArg( + static const localSshdPortArg = SshnpArg( name: 'local-sshd-port', help: 'port on which sshd is listening locally on the client host', defaultsTo: DefaultArgs.localSshdPort, @@ -294,13 +294,13 @@ class SSHNPArg { format: ArgFormat.option, type: ArgType.integer, ); - static const legacyDaemonArg = SSHNPArg( + static const legacyDaemonArg = SshnpArg( name: 'legacy-daemon', help: 'Request is to a legacy (< 4.0.0) noports daemon', - defaultsTo: DefaultSSHNPArgs.legacyDaemon, + defaultsTo: DefaultSshnpArgs.legacyDaemon, format: ArgFormat.flag, ); - static const remoteSshdPortArg = SSHNPArg( + static const remoteSshdPortArg = SshnpArg( name: 'remote-sshd-port', help: 'port on which sshd is listening locally on the device host', defaultsTo: DefaultArgs.remoteSshdPort, @@ -308,7 +308,7 @@ class SSHNPArg { format: ArgFormat.option, type: ArgType.integer, ); - static const idleTimeoutArg = SSHNPArg( + static const idleTimeoutArg = SshnpArg( name: 'idle-timeout', help: 'number of seconds after which inactive ssh connections will be closed', @@ -318,21 +318,21 @@ class SSHNPArg { type: ArgType.integer, parseWhen: ParseWhen.commandLine, ); - static final sshClientArg = SSHNPArg( + static final sshClientArg = SshnpArg( name: 'ssh-client', help: 'What to use for outbound ssh connections', - defaultsTo: DefaultSSHNPArgs.sshClient.toString(), + defaultsTo: DefaultSshnpArgs.sshClient.toString(), allowed: SupportedSshClient.values.map((c) => c.toString()).toList(), parseWhen: ParseWhen.commandLine, ); - static final ssHAlgorithmArg = SSHNPArg( + static final sshAlgorithmArg = SshnpArg( name: 'ssh-algorithm', help: 'SSH algorithm to use', defaultsTo: DefaultArgs.sshAlgorithm.toString(), - allowed: SupportedSSHAlgorithm.values.map((c) => c.toString()).toList(), + allowed: SupportedSshAlgorithm.values.map((c) => c.toString()).toList(), parseWhen: ParseWhen.commandLine, ); - static const addForwardsToTunnelArg = SSHNPArg( + static const addForwardsToTunnelArg = SshnpArg( name: 'add-forwards-to-tunnel', help: 'When true, any local forwarding directives provided in' '--local-ssh-options will be added to the initial tunnel ssh request', @@ -340,16 +340,16 @@ class SSHNPArg { format: ArgFormat.flag, parseWhen: ParseWhen.commandLine, ); - static const configFileArg = SSHNPArg( + static const configFileArg = SshnpArg( name: 'config-file', help: 'Read args from a config file\nMandatory args are not required if already supplied in the config file', parseWhen: ParseWhen.commandLine, ); - static const listDevicesArg = SSHNPArg( + static const listDevicesArg = SshnpArg( name: 'list-devices', help: 'List available devices', - defaultsTo: DefaultSSHNPArgs.listDevices, + defaultsTo: DefaultSshnpArgs.listDevices, aliases: ['ls'], negatable: false, parseWhen: ParseWhen.commandLine, diff --git a/packages/noports_core/lib/src/sshnp/models/sshnp_device_list.dart b/packages/noports_core/lib/src/sshnp/models/sshnp_device_list.dart new file mode 100644 index 000000000..b07a00ae4 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/models/sshnp_device_list.dart @@ -0,0 +1,15 @@ +class SshnpDeviceList { + final Map info = {}; + final Set activeDevices = {}; + + SshnpDeviceList(); + + void setActive(String device) { + if (info.containsKey(device)) { + activeDevices.add(device); + } + } + + Set get inactiveDevices => + info.keys.toSet().difference(activeDevices); +} diff --git a/packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_params.dart b/packages/noports_core/lib/src/sshnp/models/sshnp_params.dart similarity index 60% rename from packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_params.dart rename to packages/noports_core/lib/src/sshnp/models/sshnp_params.dart index da5eed730..43006dc67 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_params/sshnp_params.dart +++ b/packages/noports_core/lib/src/sshnp/models/sshnp_params.dart @@ -1,12 +1,12 @@ import 'dart:convert'; import 'package:noports_core/src/common/types.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/config_file_repository.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/sshnp_arg.dart'; +import 'package:noports_core/src/sshnp/models/config_file_repository.dart'; +import 'package:noports_core/src/sshnp/models/sshnp_arg.dart'; import 'package:noports_core/src/common/default_args.dart'; import 'package:noports_core/sshnp.dart'; -class SSHNPParams { +class SshnpParams { /// Required Arguments /// These arguments do not have fallback values and must be provided. /// Since there are multiple sources for these values, we cannot validate @@ -34,7 +34,7 @@ class SSHNPParams { final bool addForwardsToTunnel; final String? atKeysFilePath; final SupportedSshClient sshClient; - final SupportedSSHAlgorithm sshAlgorithm; + final SupportedSshAlgorithm sshAlgorithm; /// Special Arguments final String? @@ -43,34 +43,34 @@ class SSHNPParams { /// Operation flags final bool listDevices; - SSHNPParams({ + SshnpParams({ required this.clientAtSign, required this.sshnpdAtSign, required this.host, this.profileName, - this.device = DefaultSSHNPArgs.device, - this.port = DefaultSSHNPArgs.port, - this.localPort = DefaultSSHNPArgs.localPort, + this.device = DefaultSshnpArgs.device, + this.port = DefaultSshnpArgs.port, + this.localPort = DefaultSshnpArgs.localPort, this.identityFile, this.identityPassphrase, - this.sendSshPublicKey = DefaultSSHNPArgs.sendSshPublicKey, - this.localSshOptions = DefaultSSHNPArgs.localSshOptions, + this.sendSshPublicKey = DefaultSshnpArgs.sendSshPublicKey, + this.localSshOptions = DefaultSshnpArgs.localSshOptions, this.verbose = DefaultArgs.verbose, this.remoteUsername, this.atKeysFilePath, this.rootDomain = DefaultArgs.rootDomain, this.localSshdPort = DefaultArgs.localSshdPort, - this.legacyDaemon = DefaultSSHNPArgs.legacyDaemon, - this.listDevices = DefaultSSHNPArgs.listDevices, + this.legacyDaemon = DefaultSshnpArgs.legacyDaemon, + this.listDevices = DefaultSshnpArgs.listDevices, this.remoteSshdPort = DefaultArgs.remoteSshdPort, this.idleTimeout = DefaultArgs.idleTimeout, this.addForwardsToTunnel = DefaultArgs.addForwardsToTunnel, - this.sshClient = DefaultSSHNPArgs.sshClient, + this.sshClient = DefaultSshnpArgs.sshClient, this.sshAlgorithm = DefaultArgs.sshAlgorithm, }); - factory SSHNPParams.empty() { - return SSHNPParams( + factory SshnpParams.empty() { + return SshnpParams( profileName: '', clientAtSign: '', sshnpdAtSign: '', @@ -78,12 +78,12 @@ class SSHNPParams { ); } - /// Merge an SSHNPPartialParams objects into an SSHNPParams + /// Merge an SshnpPartialParams objects into an SshnpParams /// Params in params2 take precedence over params1 - factory SSHNPParams.merge(SSHNPParams params1, - [SSHNPPartialParams? params2]) { - params2 ??= SSHNPPartialParams.empty(); - return SSHNPParams( + factory SshnpParams.merge(SshnpParams params1, + [SshnpPartialParams? params2]) { + params2 ??= SshnpPartialParams.empty(); + return SshnpParams( profileName: params2.profileName ?? params1.profileName, clientAtSign: params2.clientAtSign ?? params1.clientAtSign, sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, @@ -112,56 +112,56 @@ class SSHNPParams { ); } - factory SSHNPParams.fromFile(String fileName) { - return SSHNPParams.fromPartial(SSHNPPartialParams.fromFile(fileName)); + factory SshnpParams.fromFile(String fileName) { + return SshnpParams.fromPartial(SshnpPartialParams.fromFile(fileName)); } - factory SSHNPParams.fromJson(String json) => - SSHNPParams.fromPartial(SSHNPPartialParams.fromJson(json)); + factory SshnpParams.fromJson(String json) => + SshnpParams.fromPartial(SshnpPartialParams.fromJson(json)); - factory SSHNPParams.fromPartial(SSHNPPartialParams partial) { + factory SshnpParams.fromPartial(SshnpPartialParams partial) { partial.clientAtSign ?? (throw ArgumentError('from is mandatory')); partial.sshnpdAtSign ?? (throw ArgumentError('to is mandatory')); partial.host ?? (throw ArgumentError('host is mandatory')); - return SSHNPParams( + return SshnpParams( profileName: partial.profileName, clientAtSign: partial.clientAtSign!, sshnpdAtSign: partial.sshnpdAtSign!, host: partial.host!, - device: partial.device ?? DefaultSSHNPArgs.device, - port: partial.port ?? DefaultSSHNPArgs.port, - localPort: partial.localPort ?? DefaultSSHNPArgs.localPort, + device: partial.device ?? DefaultSshnpArgs.device, + port: partial.port ?? DefaultSshnpArgs.port, + localPort: partial.localPort ?? DefaultSshnpArgs.localPort, identityFile: partial.identityFile, identityPassphrase: partial.identityPassphrase, sendSshPublicKey: - partial.sendSshPublicKey ?? DefaultSSHNPArgs.sendSshPublicKey, + partial.sendSshPublicKey ?? DefaultSshnpArgs.sendSshPublicKey, localSshOptions: - partial.localSshOptions ?? DefaultSSHNPArgs.localSshOptions, + partial.localSshOptions ?? DefaultSshnpArgs.localSshOptions, verbose: partial.verbose ?? DefaultArgs.verbose, remoteUsername: partial.remoteUsername, atKeysFilePath: partial.atKeysFilePath, rootDomain: partial.rootDomain ?? DefaultArgs.rootDomain, localSshdPort: partial.localSshdPort ?? DefaultArgs.localSshdPort, - listDevices: partial.listDevices ?? DefaultSSHNPArgs.listDevices, - legacyDaemon: partial.legacyDaemon ?? DefaultSSHNPArgs.legacyDaemon, + listDevices: partial.listDevices ?? DefaultSshnpArgs.listDevices, + legacyDaemon: partial.legacyDaemon ?? DefaultSshnpArgs.legacyDaemon, remoteSshdPort: partial.remoteSshdPort ?? DefaultArgs.remoteSshdPort, idleTimeout: partial.idleTimeout ?? DefaultArgs.idleTimeout, addForwardsToTunnel: partial.addForwardsToTunnel ?? DefaultArgs.addForwardsToTunnel, - sshClient: partial.sshClient ?? DefaultSSHNPArgs.sshClient, + sshClient: partial.sshClient ?? DefaultSshnpArgs.sshClient, sshAlgorithm: partial.sshAlgorithm ?? DefaultArgs.sshAlgorithm, ); } - factory SSHNPParams.fromConfigLines(String profileName, List lines) { - return SSHNPParams.fromPartial( - SSHNPPartialParams.fromConfigLines(profileName, lines)); + factory SshnpParams.fromConfigLines(String profileName, List lines) { + return SshnpParams.fromPartial( + SshnpPartialParams.fromConfigLines(profileName, lines)); } List toConfigLines({ParserType parserType = ParserType.configFile}) { var lines = []; for (var entry in toArgMap().entries) { - var arg = SSHNPArg.fromName(entry.key); + var arg = SshnpArg.fromName(entry.key); if (!parserType.shouldParse(arg.parseWhen)) continue; var key = arg.bashName; if (key.isEmpty) continue; @@ -177,30 +177,30 @@ class SSHNPParams { Map toArgMap({ParserType parserType = ParserType.all}) { var args = { - SSHNPArg.profileNameArg.name: profileName, - SSHNPArg.fromArg.name: clientAtSign, - SSHNPArg.toArg.name: sshnpdAtSign, - SSHNPArg.hostArg.name: host, - SSHNPArg.deviceArg.name: device, - SSHNPArg.portArg.name: port, - SSHNPArg.localPortArg.name: localPort, - SSHNPArg.keyFileArg.name: atKeysFilePath, - SSHNPArg.identityFileArg.name: identityFile, - SSHNPArg.identityPassphraseArg.name: identityPassphrase, - SSHNPArg.sendSshPublicKeyArg.name: sendSshPublicKey, - SSHNPArg.localSshOptionsArg.name: localSshOptions, - SSHNPArg.remoteUserNameArg.name: remoteUsername, - SSHNPArg.verboseArg.name: verbose, - SSHNPArg.rootDomainArg.name: rootDomain, - SSHNPArg.localSshdPortArg.name: localSshdPort, - SSHNPArg.remoteSshdPortArg.name: remoteSshdPort, - SSHNPArg.idleTimeoutArg.name: idleTimeout, - SSHNPArg.addForwardsToTunnelArg.name: addForwardsToTunnel, - SSHNPArg.sshClientArg.name: sshClient.toString(), - SSHNPArg.ssHAlgorithmArg.name: sshAlgorithm.toString(), + SshnpArg.profileNameArg.name: profileName, + SshnpArg.fromArg.name: clientAtSign, + SshnpArg.toArg.name: sshnpdAtSign, + SshnpArg.hostArg.name: host, + SshnpArg.deviceArg.name: device, + SshnpArg.portArg.name: port, + SshnpArg.localPortArg.name: localPort, + SshnpArg.keyFileArg.name: atKeysFilePath, + SshnpArg.identityFileArg.name: identityFile, + SshnpArg.identityPassphraseArg.name: identityPassphrase, + SshnpArg.sendSshPublicKeyArg.name: sendSshPublicKey, + SshnpArg.localSshOptionsArg.name: localSshOptions, + SshnpArg.remoteUserNameArg.name: remoteUsername, + SshnpArg.verboseArg.name: verbose, + SshnpArg.rootDomainArg.name: rootDomain, + SshnpArg.localSshdPortArg.name: localSshdPort, + SshnpArg.remoteSshdPortArg.name: remoteSshdPort, + SshnpArg.idleTimeoutArg.name: idleTimeout, + SshnpArg.addForwardsToTunnelArg.name: addForwardsToTunnel, + SshnpArg.sshClientArg.name: sshClient.toString(), + SshnpArg.sshAlgorithmArg.name: sshAlgorithm.toString(), }; args.removeWhere( - (key, value) => !parserType.shouldParse(SSHNPArg.fromName(key).parseWhen), + (key, value) => !parserType.shouldParse(SshnpArg.fromName(key).parseWhen), ); return args; } @@ -210,10 +210,10 @@ class SSHNPParams { } } -/// A class which contains a subset of the SSHNPParams +/// A class which contains a subset of the SshnpParams /// This may be used when part of the params come from separate sources /// e.g. default values from a config file and the rest from the command line -class SSHNPPartialParams { +class SshnpPartialParams { /// Main Params final String? profileName; final String? clientAtSign; @@ -236,12 +236,12 @@ class SSHNPPartialParams { final int? idleTimeout; final bool? addForwardsToTunnel; final SupportedSshClient? sshClient; - final SupportedSSHAlgorithm? sshAlgorithm; + final SupportedSshAlgorithm? sshAlgorithm; /// Operation flags final bool? listDevices; - SSHNPPartialParams({ + SshnpPartialParams({ this.profileName, this.clientAtSign, this.sshnpdAtSign, @@ -267,16 +267,16 @@ class SSHNPPartialParams { this.sshAlgorithm, }); - factory SSHNPPartialParams.empty() { - return SSHNPPartialParams(); + factory SshnpPartialParams.empty() { + return SshnpPartialParams(); } - /// Merge two SSHNPPartialParams objects together + /// Merge two SshnpPartialParams objects together /// Params in params2 take precedence over params1 - factory SSHNPPartialParams.merge(SSHNPPartialParams params1, - [SSHNPPartialParams? params2]) { - params2 ??= SSHNPPartialParams.empty(); - return SSHNPPartialParams( + factory SshnpPartialParams.merge(SshnpPartialParams params1, + [SshnpPartialParams? params2]) { + params2 ??= SshnpPartialParams.empty(); + return SshnpPartialParams( profileName: params2.profileName ?? params1.profileName, clientAtSign: params2.clientAtSign ?? params1.clientAtSign, sshnpdAtSign: params2.sshnpdAtSign ?? params1.sshnpdAtSign, @@ -305,89 +305,89 @@ class SSHNPPartialParams { ); } - factory SSHNPPartialParams.fromFile(String fileName) { + factory SshnpPartialParams.fromFile(String fileName) { var args = ConfigFileRepository.parseConfigFile(fileName); - args[SSHNPArg.profileNameArg.name] = + args[SshnpArg.profileNameArg.name] = ConfigFileRepository.toProfileName(fileName); - return SSHNPPartialParams.fromArgMap(args); + return SshnpPartialParams.fromArgMap(args); } - factory SSHNPPartialParams.fromConfigLines( + factory SshnpPartialParams.fromConfigLines( String profileName, List lines) { var args = ConfigFileRepository.parseConfigFileContents(lines); - args[SSHNPArg.profileNameArg.name] = profileName; - return SSHNPPartialParams.fromArgMap(args); + args[SshnpArg.profileNameArg.name] = profileName; + return SshnpPartialParams.fromArgMap(args); } - factory SSHNPPartialParams.fromJson(String json) => - SSHNPPartialParams.fromArgMap(jsonDecode(json)); + factory SshnpPartialParams.fromJson(String json) => + SshnpPartialParams.fromArgMap(jsonDecode(json)); - factory SSHNPPartialParams.fromArgMap(Map args) { - return SSHNPPartialParams( - profileName: args[SSHNPArg.profileNameArg.name], - clientAtSign: args[SSHNPArg.fromArg.name], - sshnpdAtSign: args[SSHNPArg.toArg.name], - host: args[SSHNPArg.hostArg.name], - device: args[SSHNPArg.deviceArg.name], - port: args[SSHNPArg.portArg.name], - localPort: args[SSHNPArg.localPortArg.name], - atKeysFilePath: args[SSHNPArg.keyFileArg.name], - identityFile: args[SSHNPArg.identityFileArg.name], - identityPassphrase: args[SSHNPArg.identityPassphraseArg.name], - sendSshPublicKey: args[SSHNPArg.sendSshPublicKeyArg.name], - localSshOptions: args[SSHNPArg.localSshOptionsArg.name] == null + factory SshnpPartialParams.fromArgMap(Map args) { + return SshnpPartialParams( + profileName: args[SshnpArg.profileNameArg.name], + clientAtSign: args[SshnpArg.fromArg.name], + sshnpdAtSign: args[SshnpArg.toArg.name], + host: args[SshnpArg.hostArg.name], + device: args[SshnpArg.deviceArg.name], + port: args[SshnpArg.portArg.name], + localPort: args[SshnpArg.localPortArg.name], + atKeysFilePath: args[SshnpArg.keyFileArg.name], + identityFile: args[SshnpArg.identityFileArg.name], + identityPassphrase: args[SshnpArg.identityPassphraseArg.name], + sendSshPublicKey: args[SshnpArg.sendSshPublicKeyArg.name], + localSshOptions: args[SshnpArg.localSshOptionsArg.name] == null ? null - : List.from(args[SSHNPArg.localSshOptionsArg.name]), - remoteUsername: args[SSHNPArg.remoteUserNameArg.name], - verbose: args[SSHNPArg.verboseArg.name], - rootDomain: args[SSHNPArg.rootDomainArg.name], - localSshdPort: args[SSHNPArg.localSshdPortArg.name], - listDevices: args[SSHNPArg.listDevicesArg.name], - legacyDaemon: args[SSHNPArg.legacyDaemonArg.name], - remoteSshdPort: args[SSHNPArg.remoteSshdPortArg.name], - idleTimeout: args[SSHNPArg.idleTimeoutArg.name], - addForwardsToTunnel: args[SSHNPArg.addForwardsToTunnelArg.name], - sshClient: args[SSHNPArg.sshClientArg.name] == null + : List.from(args[SshnpArg.localSshOptionsArg.name]), + remoteUsername: args[SshnpArg.remoteUserNameArg.name], + verbose: args[SshnpArg.verboseArg.name], + rootDomain: args[SshnpArg.rootDomainArg.name], + localSshdPort: args[SshnpArg.localSshdPortArg.name], + listDevices: args[SshnpArg.listDevicesArg.name], + legacyDaemon: args[SshnpArg.legacyDaemonArg.name], + remoteSshdPort: args[SshnpArg.remoteSshdPortArg.name], + idleTimeout: args[SshnpArg.idleTimeoutArg.name], + addForwardsToTunnel: args[SshnpArg.addForwardsToTunnelArg.name], + sshClient: args[SshnpArg.sshClientArg.name] == null ? null - : SupportedSshClient.fromString(args[SSHNPArg.sshClientArg.name]), - sshAlgorithm: args[SSHNPArg.ssHAlgorithmArg.name] == null + : SupportedSshClient.fromString(args[SshnpArg.sshClientArg.name]), + sshAlgorithm: args[SshnpArg.sshAlgorithmArg.name] == null ? null - : SupportedSSHAlgorithm.fromString( - args[SSHNPArg.ssHAlgorithmArg.name]), + : SupportedSshAlgorithm.fromString( + args[SshnpArg.sshAlgorithmArg.name]), ); } /// Parses args from command line /// first merges from a config file if provided via --config-file - factory SSHNPPartialParams.fromArgList(List args, + factory SshnpPartialParams.fromArgList(List args, {ParserType parserType = ParserType.all}) { - var params = SSHNPPartialParams.empty(); - var parser = SSHNPArg.createArgParser( + var params = SshnpPartialParams.empty(); + var parser = SshnpArg.createArgParser( withDefaults: false, parserType: parserType, ); var parsedArgs = parser.parse(args); - if (parser.options.keys.contains(SSHNPArg.configFileArg.name) && - parsedArgs.wasParsed(SSHNPArg.configFileArg.name)) { - var configFileName = parsedArgs[SSHNPArg.configFileArg.name] as String; - params = SSHNPPartialParams.merge( + if (parser.options.keys.contains(SshnpArg.configFileArg.name) && + parsedArgs.wasParsed(SshnpArg.configFileArg.name)) { + var configFileName = parsedArgs[SshnpArg.configFileArg.name] as String; + params = SshnpPartialParams.merge( params, - SSHNPPartialParams.fromFile(configFileName), + SshnpPartialParams.fromFile(configFileName), ); } - // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SSHNPPartialParams.fromArgMap + // THIS IS A WORKAROUND IN ORDER TO BE TYPE SAFE IN SshnpPartialParams.fromArgMap Map parsedArgsMap = { for (var e in parsedArgs.options) - e: SSHNPArg.fromName(e).type == ArgType.integer + e: SshnpArg.fromName(e).type == ArgType.integer ? int.tryParse(parsedArgs[e]) : parsedArgs[e] }; - return SSHNPPartialParams.merge( + return SshnpPartialParams.merge( params, - SSHNPPartialParams.fromArgMap(parsedArgsMap), + SshnpPartialParams.fromArgMap(parsedArgsMap), ); } } diff --git a/packages/noports_core/lib/src/sshnp/sshnp_result.dart b/packages/noports_core/lib/src/sshnp/models/sshnp_result.dart similarity index 66% rename from packages/noports_core/lib/src/sshnp/sshnp_result.dart rename to packages/noports_core/lib/src/sshnp/models/sshnp_result.dart index eca264afe..c07a6d87f 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_result.dart +++ b/packages/noports_core/lib/src/sshnp/models/sshnp_result.dart @@ -3,16 +3,18 @@ import 'dart:io'; import 'package:meta/meta.dart'; import 'package:socket_connector/socket_connector.dart'; -abstract class SSHNPResult {} +abstract class SshnpResult {} -class SSHNPSuccess implements SSHNPResult {} +class SshnpSuccess implements SshnpResult {} -class SSHNPFailure implements SSHNPResult {} +class SshnpFailure implements SshnpResult {} -mixin SSHNPConnectionBean on SSHNPResult { +// This is a mixin class instead of a mixin on SshnpResult so that it can be tested independently +mixin class SshnpConnectionBean { Bean? _connectionBean; @protected + @visibleForTesting set connectionBean(Bean? connectionBean) { _connectionBean = connectionBean; } @@ -29,29 +31,31 @@ mixin SSHNPConnectionBean on SSHNPResult { } if (_connectionBean is Future) { - await (_connectionBean as Future).then((value) { - if (value is Process) { - value.kill(); - } - if (value is SocketConnector) { - value.close(); - } - }); + final value = await (_connectionBean as Future); + + if (value is Process) { + value.kill(); + } + + if (value is SocketConnector) { + value.close(); + } } } } -const _optionsWithPrivateKey = [ +@visibleForTesting +const optionsWithPrivateKey = [ '-o StrictHostKeyChecking=accept-new', '-o IdentitiesOnly=yes' ]; -class SSHNPError implements SSHNPFailure, Exception { +class SshnpError implements SshnpFailure, Exception { final Object message; final Object? error; final StackTrace? stackTrace; - SSHNPError(this.message, {this.error, this.stackTrace}); + SshnpError(this.message, {this.error, this.stackTrace}); @override String toString() { @@ -73,7 +77,7 @@ class SSHNPError implements SSHNPFailure, Exception { } } -class SSHNPCommand extends SSHNPSuccess with SSHNPConnectionBean { +class SshnpCommand extends SshnpSuccess with SshnpConnectionBean { final String command; final int localPort; final String? remoteUsername; @@ -82,17 +86,17 @@ class SSHNPCommand extends SSHNPSuccess with SSHNPConnectionBean { final List sshOptions; - SSHNPCommand( - {required this.localPort, - required this.remoteUsername, - required this.host, - this.command = 'ssh', - List? localSshOptions, - this.privateKeyFileName, - Bean? connectionBean}) - : sshOptions = [ + SshnpCommand({ + required this.localPort, + required this.host, + this.remoteUsername, + this.command = 'ssh', + List? localSshOptions, + this.privateKeyFileName, + Bean? connectionBean, + }) : sshOptions = [ if (shouldIncludePrivateKey(privateKeyFileName)) - ..._optionsWithPrivateKey, + ...optionsWithPrivateKey, ...(localSshOptions ?? []) ] { this.connectionBean = connectionBean; @@ -122,10 +126,10 @@ class SSHNPCommand extends SSHNPSuccess with SSHNPConnectionBean { } } -class SSHNPNoOpSuccess extends SSHNPSuccess - with SSHNPConnectionBean { +class SshnpNoOpSuccess extends SshnpSuccess + with SshnpConnectionBean { String? message; - SSHNPNoOpSuccess({this.message, Bean? connectionBean}) { + SshnpNoOpSuccess({this.message, Bean? connectionBean}) { this.connectionBean = connectionBean; } diff --git a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_legacy_impl.dart b/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_legacy_impl.dart deleted file mode 100644 index c588f13d4..000000000 --- a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_legacy_impl.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; - -import 'package:at_client/at_client.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnpd_payload_handler.dart'; -import 'package:noports_core/src/sshnp/reverse_direction/sshnp_reverse.dart'; -import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshrv.dart'; - -class SSHNPLegacyImpl extends SSHNPReverse with LegacySSHNPDPayloadHandler { - SSHNPLegacyImpl({ - required AtClient atClient, - required SSHNPParams params, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize, - }) : super( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); - - @override - Future init() async { - logger.info('Initializing SSHNPLegacyImpl'); - await super.init(); - if (initializedCompleter.isCompleted) return; - - // Share our private key with sshnpd - AtKey sendOurPrivateKeyToSshnpd = AtKey() - ..key = 'privatekey' - ..sharedBy = clientAtSign - ..sharedWith = sshnpdAtSign - ..namespace = this.namespace - ..metadata = (Metadata()..ttl = 10000); - await notify( - sendOurPrivateKeyToSshnpd, ephemeralKeyPair.privateKeyContents); - - completeInitialization(); - } - - @override - Future run() async { - await startAndWaitForInit(); - - logger.info('Requesting legacy daemon to start reverse ssh session'); - - Future? sshrvResult; - if (usingSshrv) { - // Connect to rendezvous point using background process. - // sshnp (this program) can then exit without issue. - SSHRV sshrv = sshrvGenerator(host, sshrvdPort!, - localSshdPort: params.localSshdPort); - sshrvResult = sshrv.run(); - } - - // send request to the daemon via notification - await notify( - AtKey() - ..key = 'sshd' - ..namespace = this.namespace - ..sharedBy = clientAtSign - ..sharedWith = sshnpdAtSign - ..metadata = (Metadata()..ttl = 10000), - '$localPort $port $localUsername $host $sessionId', - ); - - bool acked = await waitForDaemonResponse(); - if (!acked) { - var error = SSHNPError( - 'sshnp timed out: waiting for daemon response\nhint: make sure the device is online', - stackTrace: StackTrace.current, - ); - doneCompleter.completeError(error); - return error; - } - - if (sshnpdAckErrors) { - var error = SSHNPError( - 'sshnp failed: with sshnpd acknowledgement errors', - stackTrace: StackTrace.current, - ); - doneCompleter.completeError(error); - return error; - } - - doneCompleter.complete(); - return SSHNPCommand( - localPort: localPort, - remoteUsername: remoteUsername, - host: 'localhost', - privateKeyFileName: identityKeyPair?.privateKeyFileName, - localSshOptions: - (params.addForwardsToTunnel) ? null : params.localSshOptions, - connectionBean: sshrvResult, - ); - } -} diff --git a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse.dart b/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse.dart deleted file mode 100644 index 5916cbdde..000000000 --- a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:async'; - -import 'package:noports_core/src/sshnp/sshnp_core.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnp_ssh_key_handler.dart'; -import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshrv.dart'; -import 'package:noports_core/utils.dart'; - -abstract class SSHNPReverse extends SSHNPCore with SSHNPLocalSSHKeyHandler { - SSHNPReverse({ - required super.atClient, - required super.params, - SSHRVGenerator? sshrvGenerator, - super.shouldInitialize, - }) : sshrvGenerator = sshrvGenerator ?? DefaultArgs.sshrvGenerator; - - /// Function used to generate a [SSHRV] instance ([SSHRV.localbinary] by default) - final SSHRVGenerator sshrvGenerator; - - /// Set by [generateEphemeralSshKeys] during [init], if we're not doing direct ssh. - /// sshnp generates a new keypair for each ssh session, using the algorithm specified - /// in [params.sshAlgorithm]. - /// sshnp will write [ephemeralKeyPair] to ~/.ssh/ephemeral_$sessionId - /// sshnp will write [ephemeralKeyPair.publicKey] to ~/.ssh/authorized_keys - /// sshnp will send the [ephemeralKeyPair.privateKey] to sshnpd - late final AtSSHKeyPair ephemeralKeyPair; - - /// Local username, set by [init] - late final String localUsername; - - @override - Future init() async { - logger.info('Initializing SSHNPReverse'); - await super.init(); - if (initializedCompleter.isCompleted) return; - - localUsername = getUserName(throwIfNull: true)!; - - logger.info('Generating ephemeral keypair'); - try { - ephemeralKeyPair = await keyUtil.generateKeyPair( - algorithm: params.sshAlgorithm, - identifier: 'ephemeral_$sessionId', - directory: keyUtil.sshnpHomeDirectory, - ); - } catch (e, s) { - logger.info('Failed to generate ephemeral keypair'); - throw SSHNPError( - 'Failed to generate ephemeral keypair', - error: e, - stackTrace: s, - ); - } - - try { - logger.info('Adding ephemeral key to authorized_keys'); - await keyUtil.authorizePublicKey( - sshPublicKey: ephemeralKeyPair.publicKeyContents, - localSshdPort: params.localSshdPort, - sessionId: sessionId, - ); - } catch (e, s) { - throw SSHNPError( - 'Failed to add ephemeral key to authorized_keys', - error: e, - stackTrace: s, - ); - } - } - - @override - Future cleanUp() async { - logger.info('Tidying up files'); -// Delete the generated RSA keys and remove the entry from ~/.ssh/authorized_keys - await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); - await keyUtil.deauthorizePublicKey(sessionId); - await super.cleanUp(); - } - - bool get usingSshrv => sshrvdPort != null; -} diff --git a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse_impl.dart b/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse_impl.dart deleted file mode 100644 index d45700980..000000000 --- a/packages/noports_core/lib/src/sshnp/reverse_direction/sshnp_reverse_impl.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:async'; - -import 'package:at_client/at_client.dart'; -import 'package:noports_core/src/common/validation_utils.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnpd_payload_handler.dart'; -import 'package:noports_core/src/sshnp/reverse_direction/sshnp_reverse.dart'; -import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshrv.dart'; - -class SSHNPReverseImpl extends SSHNPReverse with DefaultSSHNPDPayloadHandler { - SSHNPReverseImpl({ - required AtClient atClient, - required SSHNPParams params, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize, - }) : super( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); - - @override - Future init() async { - logger.info('Initializing SSHNPReverseImpl'); - await super.init(); - completeInitialization(); - } - - @override - Future run() async { - await startAndWaitForInit(); - - logger.info('Requesting daemon to start reverse ssh session'); - - Future? sshrvResult; - if (usingSshrv) { - // Connect to rendezvous point using background process. - // sshnp (this program) can then exit without issue. - SSHRV sshrv = sshrvGenerator(host, sshrvdPort!, - localSshdPort: params.localSshdPort); - sshrvResult = sshrv.run(); - } - // send request to the daemon via notification - await notify( - AtKey() - ..key = 'ssh_request' - ..namespace = this.namespace - ..sharedBy = clientAtSign - ..sharedWith = sshnpdAtSign - ..metadata = (Metadata() - ..ttl = 10000), - signAndWrapAndJsonEncode( - atClient, - { - 'direct': false, - 'sessionId': sessionId, - 'host': host, - 'port': port, - 'username': localUsername, - 'remoteForwardPort': localPort, - 'privateKey': ephemeralKeyPair.privateKeyContents, - }, - ), - ); - - bool acked = await waitForDaemonResponse(); - if (!acked) { - var error = - SSHNPError('sshnp connection timeout: waiting for daemon response'); - doneCompleter.completeError(error); - return error; - } - - if (sshnpdAckErrors) { - var error = - SSHNPError('sshnp failed: with sshnpd acknowledgement errors'); - doneCompleter.completeError(error); - return error; - } - - doneCompleter.complete(); - return SSHNPCommand( - localPort: localPort, - remoteUsername: remoteUsername, - host: 'localhost', - privateKeyFileName: identityKeyPair?.privateKeyFileName, - localSshOptions: - (params.addForwardsToTunnel) ? null : params.localSshOptions, - connectionBean: sshrvResult, - ); - } -} diff --git a/packages/noports_core/lib/src/sshnp/sshnp.dart b/packages/noports_core/lib/src/sshnp/sshnp.dart index 0ad166353..304b80358 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp.dart +++ b/packages/noports_core/lib/src/sshnp/sshnp.dart @@ -1,173 +1,69 @@ import 'dart:async'; import 'package:at_client/at_client.dart' hide StringBuffer; -import 'package:noports_core/src/sshnp/forward_direction/sshnp_forward_dart_local_impl.dart'; -import 'package:noports_core/src/sshnp/forward_direction/sshnp_forward_dart_pure_impl.dart'; -import 'package:noports_core/src/sshnp/sshnp_core.dart'; -import 'package:noports_core/src/sshnp/sshnp_params/sshnp_params.dart'; -import 'package:noports_core/src/sshnp/sshnp_result.dart'; -import 'package:noports_core/utils.dart'; +import 'package:noports_core/sshnp_foundation.dart'; -typedef AtClientGenerator = FutureOr Function( - SSHNPParams params, String namespace); - -typedef UsageCallback = void Function(Object error, StackTrace stackTrace); - -abstract interface class SSHNP { - static Future fromParamsWithFileBindings( - SSHNPParams params, { - AtClient? atClient, - AtClientGenerator? atClientGenerator, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize, - }) async { - atClient ??= await atClientGenerator?.call( - params, SSHNPCore.getNamespace(params.device)); - - if (atClient == null) { - throw ArgumentError( - 'atClient must be provided or atClientGenerator must be provided'); - } - - if (params.legacyDaemon) { - return SSHNP.legacy( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); - } - - if (!params.host.startsWith('@')) { - return SSHNP.reverse( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); - } - - switch (params.sshClient) { - case SupportedSshClient.exec: - return SSHNP.forwardExec( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); - case SupportedSshClient.dart: - return SSHNP.forwardDart( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); - } - } - - /// Creates an SSHNP instance that is configured to communicate with legacy >= 3.0.0 <4.0.0 daemons - factory SSHNP.legacy({ +abstract interface class Sshnp { + /// Legacy v3.x.x client + factory Sshnp.unsigned({ required AtClient atClient, - required SSHNPParams params, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize, - }) => - SSHNPLegacyImpl( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); - - /// Creates an SSHNP instance that is configured to use reverse ssh tunneling - factory SSHNP.reverse({ - required AtClient atClient, - required SSHNPParams params, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize, - }) => - SSHNPReverseImpl( - atClient: atClient, - params: params, - sshrvGenerator: sshrvGenerator, - shouldInitialize: shouldInitialize, - ); + required SshnpParams params, + }) { + return SshnpUnsignedImpl(atClient: atClient, params: params); + } - /// Creates an SSHNP instance that is configured to use direct ssh tunneling by executing the ssh command - factory SSHNP.forwardExec({ + /// Think of this as the "default" client - calls /usr/bin/ssh + factory Sshnp.execLocal({ required AtClient atClient, - required SSHNPParams params, - bool? shouldInitialize, - }) => - SSHNPForwardExecImpl( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); + required SshnpParams params, + }) { + return SshnpExecLocalImpl(atClient: atClient, params: params); + } - /// Creates an SSHNP instance that is configured to use direct ssh tunneling using a dart SSHClient - factory SSHNP.forwardDart({ + /// Uses a dartssh2 ssh client - still expects local ssh keys + factory Sshnp.dartLocal({ required AtClient atClient, - required SSHNPParams params, - bool? shouldInitialize, - }) => - SSHNPForwardDartLocalImpl( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); + required SshnpParams params, + }) { + return SshnpDartLocalImpl(atClient: atClient, params: params); + } - /// Creates an SSHNP instance that is configured to use direct ssh tunneling using a pure-dart SSHClient - /// This class has absolutely zero dependencies on the local file system - factory SSHNP.forwardPureDart({ + /// Uses a dartssh2 ssh client - requires that you pass in the identity keypair + factory Sshnp.dartPure({ required AtClient atClient, - required SSHNPParams params, - required AtSSHKeyPair identityKeyPair, - bool? shouldInitialize, - }) => - SSHNPForwardDartPureImpl( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - identityKeyPair: identityKeyPair, + required SshnpParams params, + required AtSshKeyPair? identityKeyPair, + }) { + var sshnp = SshnpDartPureImpl( + atClient: atClient, + params: params, + ); + if (identityKeyPair != null) { + sshnp.keyUtil.addKeyPair( + keyPair: identityKeyPair, + identifier: identityKeyPair.identifier, ); + } + return sshnp; + } /// The atClient to use for communicating with the atsign's secondary server AtClient get atClient; - /// The parameters used to configure this SSHNP instance - SSHNPParams get params; - - /// Completes when the SSHNP instance is no longer doing anything - /// e.g. controlling a direct ssh tunnel using the pure-dart SSHClient - Future get done; - - /// Completes after asynchronous initialization has completed - Future get initialized; - - /// Must be run after construction, to complete initialization - /// - Starts notification subscription to listen for responses from sshnpd - /// - calls [generateSshKeys] which generates the ssh keypair to use - /// ( [sshPublicKey] and [sshPrivateKey] ) - /// - calls [fetchRemoteUserName] to fetch the username to use on the remote - /// host in the ssh session - /// - If not supplied via constructor, finds a spare port for [localPort] - /// - If using sshrv, calls [getHostAndPortFromSshrvd] to fetch host and port - /// from sshrvd - /// - calls [sharePrivateKeyWithSshnpd] - /// - calls [sharePublicKeyWithSshnpdIfRequired] - FutureOr init(); + /// The parameters used to configure this Sshnp instance + SshnpParams get params; - /// May only be run after [init] has been run. - /// - Sends request to sshnpd; the response listener was started by [init] + /// May only be run after [initialize] has been run. + /// - 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 - FutureOr run(); + Future run(); /// Send a ping out to all sshnpd and listen for heartbeats /// Returns two Iterable and a Map: /// - Iterable of atSigns of sshnpd that responded /// - Iterable of atSigns of sshnpd that did not respond /// - Map where the keys are all atSigns included in the maps, and the values being their device info - FutureOr<(Iterable, Iterable, Map)> - listDevices(); + Future listDevices(); } diff --git a/packages/noports_core/lib/src/sshnp/sshnp_core.dart b/packages/noports_core/lib/src/sshnp/sshnp_core.dart index 1c108cc31..cd8206208 100644 --- a/packages/noports_core/lib/src/sshnp/sshnp_core.dart +++ b/packages/noports_core/lib/src/sshnp/sshnp_core.dart @@ -1,141 +1,70 @@ import 'dart:async'; -import 'dart:convert'; + import 'dart:io'; 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:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/mixins/async_completion.dart'; +import 'package:noports_core/src/common/mixins/async_initialization.dart'; +import 'package:noports_core/src/common/mixins/at_client_bindings.dart'; +import 'package:noports_core/src/sshnp/util/sshnp_ssh_key_handler.dart'; +import 'package:noports_core/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart'; +import 'package:noports_core/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart'; import 'package:noports_core/sshnp.dart'; -import 'package:noports_core/sshrvd.dart'; -import 'package:noports_core/utils.dart'; import 'package:uuid/uuid.dart'; -export 'forward_direction/sshnp_forward.dart'; -export 'forward_direction/sshnp_forward_dart.dart'; -export 'forward_direction/sshnp_forward_exec_impl.dart'; - -export 'reverse_direction/sshnp_reverse.dart'; -export 'reverse_direction/sshnp_reverse_impl.dart'; -export 'reverse_direction/sshnp_legacy_impl.dart'; - // If you've never seen an abstract implementation before, here it is :P @protected -abstract class SSHNPCore implements SSHNP { - final AtSignLogger logger = AtSignLogger(' sshnp '); - - // ==================================================================== - // Final instance variables, injected via constructor - // ==================================================================== +abstract class SshnpCore + with AsyncInitialization, AsyncDisposal, AtClientBindings, SshnpKeyHandler + implements Sshnp { + // * AtClientBindings members + /// The logger for this class + @override + final AtSignLogger logger = AtSignLogger(' SshnpCore '); + /// The [AtClient] to use for this instance @override final AtClient atClient; - @override - final SSHNPParams params; - final String sessionId; + // * Main Parameters - // ==================================================================== - // Final instance variables, derived during initialization - // ==================================================================== - - late final String remoteUsername; - - // ==================================================================== - // Volatile instance variables, injected via constructor - // but possibly modified later on - // ==================================================================== + /// The parameters supplied for this instance + @override + final SshnpParams params; - /// Host that we will send to sshnpd for it to connect to, - /// or the atSign of the sshrvd. - /// If using sshrvd then we will fetch the _actual_ host to use from sshrvd. - String host; + /// The session ID for this instance (UUID v4) + final String sessionId; - /// Port that we will send to sshnpd for it to connect to. - /// Required if we are not using sshrvd. - /// If using sshrvd then initial port value will be ignored and instead we - /// will fetch the port from sshrvd. - int port; + /// The namespace for this instance ('[params.device].sshnp') + final String namespace; - /// Port to which sshnpd will forwardRemote its [SSHClient]. If localPort - /// is set to '0' then + // * Volatile State + /// The local port to use for the initial tunnel's sshd forwarding + /// If this is 0, then a spare port will be found and set int localPort; - /// When using sshrvd, this is fetched from sshrvd during [init] - /// This is only set when using sshrvd - /// (i.e. after [getHostAndPortFromSshrvd] has been called) - int? sshrvdPort; + /// The remote username to use for the ssh session + String? remoteUsername; - // ==================================================================== - // Status indicators (Available in the public API) - // ==================================================================== + // * Communication Channels + /// The channel to communicate with the sshrvd (host) @protected - final Completer doneCompleter = Completer(); - - @override - Future get done => doneCompleter.future; - - bool _initializeStarted = false; + SshrvdChannel? get sshrvdChannel; + /// The channel to communicate with the sshnpd (daemon) @protected - bool get initializeStarted => _initializeStarted; - @protected - final Completer initializedCompleter = Completer(); - - @override - Future get initialized => initializedCompleter.future; - - // ==================================================================== - // Internal state variables - // ==================================================================== - - /// true once we have received any response (success or error) from sshnpd - @protected - bool sshnpdAck = false; - - /// true once we have received an error response from sshnpd - @protected - bool sshnpdAckErrors = false; - - /// true once we have received a response from sshrvd - @visibleForTesting - bool sshrvdAck = false; - - // ==================================================================== - // Getters for derived values - // ==================================================================== - - String get clientAtSign => atClient.getCurrentAtSign()!; - - String get sshnpdAtSign => params.sshnpdAtSign; - - static String getNamespace(String device) => '$device.sshnp'; - - String get namespace => getNamespace(params.device); + SshnpdChannel get sshnpdChannel; - FutureOr identityKeyPair; - - // ==================================================================== - // Auxiliary - // ==================================================================== - - @protected - AtSSHKeyUtil get keyUtil; - - // ==================================================================== - // Constructor and Initialization - // ==================================================================== - - SSHNPCore({ + SshnpCore({ required this.atClient, required this.params, - SSHRVGenerator? sshrvGenerator, - bool? shouldInitialize = true, }) : sessionId = Uuid().v4(), - host = params.host, - port = params.port, + namespace = '${params.device}.sshnp', localPort = params.localPort { /// Set the logger level to shout logger.hierarchicalLoggingEnabled = true; @@ -143,52 +72,44 @@ abstract class SSHNPCore implements SSHNP { if (params.verbose) { logger.logger.level = Level.INFO; + AtSignLogger.root_level = 'info'; } /// Set the namespace to the device's namespace AtClientPreference preference = atClient.getPreferences() ?? AtClientPreference(); - preference.namespace = '${params.device}.sshnp'; + preference.namespace = namespace; atClient.setPreferences(preference); - - /// Also call init - if (shouldInitialize ?? true) init(); } @override @mustCallSuper - Future init() async { - logger.info('Initializing SSHNPCore'); - if (_initializeStarted) { - logger.warning('Cancelling initialization: Already started'); - return; - } else { - _initializeStarted = true; - } + Future initialize() async { + if (!isSafeToInitialize) return; + logger.info('Initializing SshnpCore'); - // Schedule a cleanup on exit - unawaited(doneCompleter.future.then((_) async { - logger.info('SSHNPCore done'); - await cleanUp(); - })); + /// Start the sshnpd payload handler + await sshnpdChannel.callInitialization(); - try { - if (!(await atSignIsActivated(atClient, sshnpdAtSign))) { - logger.severe('Device address $sshnpdAtSign is not activated.'); - throw ('Device address $sshnpdAtSign is not activated.'); - } - } catch (e, s) { - throw SSHNPError(e, stackTrace: s); - } + /// Set the remote username to use for the ssh session + remoteUsername = await sshnpdChannel.resolveRemoteUsername(); - // Start listening for response notifications from sshnpd - logger.info('Subscribing to notifications on $sessionId.$namespace@'); - atClient.notificationService - .subscribe(regex: '$sessionId.$namespace@', shouldDecrypt: true) - .listen(handleSshnpdResponses); + /// Find a spare local port if required + await _findLocalPortIfRequired(); - remoteUsername = params.remoteUsername ?? await fetchRemoteUserName(); + /// Shares the public key if required + await sshnpdChannel.sharePublicKeyIfRequired(identityKeyPair); + + /// Retrieve the sshrvd host and port pair + await sshrvdChannel?.callInitialization(); + } + + @override + Future dispose() async { + completeDisposal(); + } + Future _findLocalPortIfRequired() async { // TODO investigate if this is a problem on mobile // find a spare local port if (localPort == 0) { @@ -201,322 +122,12 @@ abstract class SSHNPCore implements SSHNP { await serverSocket.close().catchError((e) => throw e); } catch (e, s) { logger.info('Unable to find a spare local port'); - throw SSHNPError('Unable to find a spare local port', + throw SshnpError('Unable to find a spare local port', error: e, stackTrace: s); } } - - await sharePublicKeyWithSshnpdIfRequired().catchError((e, s) { - throw SSHNPError( - 'Unable to share ssh public key with sshnpd', - error: e, - stackTrace: s, - ); - }); - - // If host has an @ then contact the sshrvd service for some ports - if (host.startsWith('@')) { - logger.info('Host is an atSign, fetching host and port from sshrvd'); - await getHostAndPortFromSshrvd().catchError((e, s) { - throw SSHNPError( - 'Unable to get host and port from sshrvd', - error: e, - stackTrace: s, - ); - }); - } - - logger.finer('Base initialization complete'); - // N.B. Don't complete initialization here, subclasses will do that - // This is in case they need to implement further initialization steps - } - - @protected - void completeInitialization() { - if (initializedCompleter.isCompleted) return; - logger.info('Completing initialization'); - initializedCompleter.complete(); - } - - @visibleForTesting - Future handleSshnpdResponses(AtNotification notification) async { - String notificationKey = notification.key - .replaceAll('${notification.to}:', '') - .replaceAll('.$namespace${notification.from}', '') - // convert to lower case as the latest AtClient converts notification - // keys to lower case when received - .toLowerCase(); - logger.info('Received $notificationKey notification'); - - bool connected = await handleSshnpdPayload(notification); - - if (connected) { - logger.info('Session $sessionId connected successfully'); - sshnpdAck = true; - } else { - sshnpdAck = true; - sshnpdAckErrors = true; - } - } - - @protected - FutureOr handleSshnpdPayload(AtNotification notification); - - // ==================================================================== - // Internal methods - // ==================================================================== - - @protected - Future startAndWaitForInit() async { - if (!initializedCompleter.isCompleted) { - // Call init in case it hasn't been called yet - unawaited(init()); - } - // Wait for init to complete - // N.B. must be called this way in case the init call above is not the first init call - return await initialized; - } - - @protected - Future notify( - AtKey atKey, - String value, - ) async { - await atClient.notificationService - .notify(NotificationParams.forUpdate(atKey, value: value), - onSuccess: (NotificationResult notification) { - logger.info( - 'SUCCESS:$notification for: $sessionId with key: ${atKey.toString()}'); - }, onError: (notification) { - logger.info('ERROR:$notification'); - }); - } - - /// 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 - /// Is not called if remoteUserName was set via constructor - @protected - Future fetchRemoteUserName() async { - logger.info('Fetching remote username from sshnpd'); - AtKey userNameRecordID = - AtKey.fromString('$clientAtSign:username.$namespace$sshnpdAtSign'); - try { - return (await atClient.get(userNameRecordID)).value as String; - } catch (e, s) { - throw SSHNPError( - "Device unknown, or username not shared\n" - "hint: make sure the device shares username or set remote username manually", - error: e, - stackTrace: s, - ); - } - } - - @protected - Future getHostAndPortFromSshrvd() async { - atClient.notificationService - .subscribe( - regex: '$sessionId.${SSHRVD.namespace}@', shouldDecrypt: true) - .listen((notification) async { - String ipPorts = notification.value.toString(); - List results = ipPorts.split(','); - host = results[0]; - port = int.parse(results[1]); - sshrvdPort = int.parse(results[2]); - logger.info('Received host and port from sshrvd: $host:$port'); - logger.info('Set sshrvdPort to: $sshrvdPort'); - sshrvdAck = true; - }); - logger.info('Started listening for sshrvd response'); - AtKey ourSshrvdIdKey = AtKey() - ..key = '${params.device}.${SSHRVD.namespace}' - ..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 - ..namespaceAware = false - ..ttl = 10000); - logger.info('Sending notification to sshrvd: $ourSshrvdIdKey'); - await notify(ourSshrvdIdKey, sessionId); - - logger.info('Waiting for sshrvd response'); - int counter = 0; - while (!sshrvdAck) { - logger.info('Waiting for sshrvd response: $counter'); - await Future.delayed(Duration(milliseconds: 100)); - counter++; - 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'); - } - } - } - - @protected - Future sharePublicKeyWithSshnpdIfRequired() async { - if (!params.sendSshPublicKey) { - logger.info( - 'Skipped sharing public key with sshnpd: sendSshPublicKey=false'); - return; - } - - if (identityKeyPair == null) { - logger.info( - 'Skipped sharing public key with sshnpd: no identity key pair set'); - return; - } - - var publicKeyContents = (await identityKeyPair)!.publicKeyContents; - - logger.info('Sharing public key with sshnpd'); - try { - logger.info('sharing ssh public key: $publicKeyContents'); - if (!publicKeyContents.startsWith('ssh-')) { - logger.severe('SSH Public Key does not look like a public key file'); - throw ('SSH Public Key does not look like a public key file'); - } - AtKey sendOurPublicKeyToSshnpd = AtKey() - ..key = 'sshpublickey' - ..sharedBy = clientAtSign - ..sharedWith = sshnpdAtSign - ..metadata = (Metadata()..ttl = 10000); - await notify(sendOurPublicKeyToSshnpd, publicKeyContents); - } catch (e, s) { - throw SSHNPError( - 'Error opening or validating public key file or sending to remote atSign', - error: e, - stackTrace: s, - ); - } } - @protected - Future waitForDaemonResponse() async { - logger.finer('Waiting for daemon response'); - int counter = 0; - // Timer to timeout after 10 Secs or after the Ack of connected/Errors - while (!sshnpdAck) { - await Future.delayed(Duration(milliseconds: 100)); - counter++; - if (counter == 100) { - return false; - } - } - return true; - } - - @protected - @mustCallSuper - FutureOr cleanUp() { - logger.info('Cleaning up SSHNPImpl'); - // This is an intentional no-op to allow overrides to safely call super.cleanUp() - } - - Future> _getAtKeysRemote( - {String? regex, - String? sharedBy, - String? sharedWith, - bool showHiddenKeys = false}) async { - var builder = ScanVerbBuilder() - ..sharedWith = sharedWith - ..sharedBy = sharedBy - ..regex = regex - ..showHiddenKeys = showHiddenKeys - ..auth = true; - var scanResult = await atClient.getRemoteSecondary()?.executeVerb(builder); - scanResult = scanResult?.replaceFirst('data:', '') ?? ''; - var result = []; - if (scanResult.isNotEmpty) { - result = List.from(jsonDecode(scanResult)).map((key) { - try { - return AtKey.fromString(key); - } on InvalidSyntaxException { - logger.severe('$key is not a well-formed key'); - } on Exception catch (e) { - logger.severe( - 'Exception occurred: ${e.toString()}. Unable to form key $key'); - } - }).toList(); - } - result.removeWhere((element) => element == null); - return result.cast(); - } - - // ==================================================================== - // Public API - // ==================================================================== - @override - Future<(Iterable, Iterable, Map)> - listDevices() async { - // get all the keys device_info.*.sshnpd - var scanRegex = - 'device_info\\.$sshnpDeviceNameRegex\\.${DefaultArgs.namespace}'; - - var atKeys = - await _getAtKeysRemote(regex: scanRegex, sharedBy: sshnpdAtSign); - - var devices = {}; - var heartbeats = {}; - var info = {}; - - // Listen for heartbeat notifications - atClient.notificationService - .subscribe( - regex: 'heartbeat\\.$sshnpDeviceNameRegex', shouldDecrypt: true) - .listen((notification) { - var deviceInfo = jsonDecode(notification.value ?? '{}'); - var devicename = deviceInfo['devicename']; - if (devicename != null) { - heartbeats.add(devicename); - } - }); - - // for each key, get the value - for (var entryKey in atKeys) { - var atValue = await atClient.get( - entryKey, - getRequestOptions: GetRequestOptions()..bypassCache = true, - ); - var deviceInfo = jsonDecode(atValue.value) ?? {}; - - if (deviceInfo['devicename'] == null) { - continue; - } - - var devicename = deviceInfo['devicename'] as String; - info[devicename] = deviceInfo; - - var metaData = Metadata() - ..isPublic = false - ..isEncrypted = true - ..namespaceAware = true; - - var pingKey = AtKey() - ..key = "ping.$devicename" - ..sharedBy = clientAtSign - ..sharedWith = entryKey.sharedBy - ..namespace = DefaultArgs.namespace - ..metadata = metaData; - - unawaited(notify(pingKey, 'ping')); - - // Add the device to the base list - devices.add(devicename); - } - - // wait for 10 seconds in case any are being slow - await Future.delayed(const Duration(seconds: 10)); - - // The intersection is in place on the off chance that some random device - // sends a heartbeat notification, but is not on the list of devices - return ( - devices.intersection(heartbeats), - devices.difference(heartbeats), - info, - ); - } + Future listDevices() => sshnpdChannel.listDevices(); } diff --git a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart.dart b/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler.dart similarity index 53% rename from packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart.dart rename to packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler.dart index 177c0b577..a1cd7e4ec 100644 --- a/packages/noports_core/lib/src/sshnp/forward_direction/sshnp_forward_dart.dart +++ b/packages/noports_core/lib/src/sshnp/util/sshnp_initial_tunnel_handler.dart @@ -1,89 +1,129 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; -import 'package:at_client/at_client.dart'; import 'package:dartssh2/dartssh2.dart'; import 'package:meta/meta.dart'; -import 'package:noports_core/src/sshnp/forward_direction/sshnp_forward.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnpd_payload_handler.dart'; +import 'package:noports_core/src/sshnp/sshnp_core.dart'; import 'package:noports_core/sshnp.dart'; +import 'package:noports_core/utils.dart'; -abstract class SSHNPForwardDart extends SSHNPForward - with DefaultSSHNPDPayloadHandler { - - SSHNPForwardDart({ - required AtClient atClient, - required SSHNPParams params, - bool? shouldInitialize, - }) : super( - atClient: atClient, - params: params, - shouldInitialize: shouldInitialize, - ); +mixin SshnpInitialTunnelHandler { + @protected + Future startInitialTunnel({required String identifier}); +} + +mixin SshnpExecInitialTunnelHandler on SshnpCore + implements SshnpInitialTunnelHandler { + @override + Future 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 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; + } +} +mixin SshnpDartInitialTunnelHandler on SshnpCore + implements SshnpInitialTunnelHandler { /// Set up timer to check to see if all connections are down - @protected + @visibleForTesting String get terminateMessage => 'ssh session will terminate after ${params.idleTimeout} seconds' ' if it is not being used'; - @protected - Future startInitialTunnel() async { - await startAndWaitForInit(); - - var error = await requestSocketTunnelFromDaemon(); - if (error != null) { - throw error; - } - + @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 $host on port $sshrvdPort with forwardLocal of $localPort'); + '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(host, sshrvdPort); + socket = await SSHSocket.connect( + sshrvdChannel!.host, + sshrvdChannel!.sshrvdPort!, + ).catchError((e) => throw e); } catch (e, s) { - var error = SSHNPError( - 'Failed to open socket to $host:$port : $e', + var error = SshnpError( + 'Failed to open socket to ${sshrvdChannel!.host}:${sshrvdChannel!.sshrvdPort} : $e', error: e, stackTrace: s, ); - doneCompleter.completeError(error); throw error; } try { + AtSshKeyPair keyPair = await keyUtil.getKeyPair(identifier: identifier); client = SSHClient( socket, - username: remoteUsername, - identities: [ - // A single private key file may contain multiple keys. - ...SSHKeyPair.fromPem(ephemeralPrivateKey) - ], + username: remoteUsername ?? getUserName(throwIfNull: true)!, + identities: [keyPair.keyPair], keepAliveInterval: Duration(seconds: 15), ); } catch (e, s) { - var error = SSHNPError( - 'Failed to create SSHClient for ${params.remoteUsername}@$host:$port : $e', + throw SshnpError( + 'Failed to create SSHClient for ${params.remoteUsername}@${sshrvdChannel!.host}:${sshrvdChannel!.sshrvdPort} : $e', error: e, stackTrace: s, ); - doneCompleter.completeError(error); - throw error; } try { - await client.authenticated; + await client.authenticated.catchError((e) => throw e); } catch (e, s) { - var error = SSHNPError( - 'Failed to authenticate as ${params.remoteUsername}@$host:$port : $e', + throw SshnpError( + 'Failed to authenticate as ${params.remoteUsername}@${sshrvdChannel!.host}:${sshrvdChannel!.sshrvdPort} : $e', error: e, stackTrace: s, ); - doneCompleter.completeError(error); - throw error; } int counter = 0; @@ -119,9 +159,10 @@ abstract class SSHNPForwardDart extends SSHNPForward // Start local forwarding to the remote sshd await startForwarding( - fLocalPort: localPort, - fRemoteHost: 'localhost', - fRemotePort: params.remoteSshdPort); + fLocalPort: localPort, + fRemoteHost: 'localhost', + fRemotePort: params.remoteSshdPort, + ); if (params.addForwardsToTunnel) { var optionsSplitBySpace = params.localSshOptions.join(' ').split(' '); @@ -170,18 +211,15 @@ abstract class SSHNPForwardDart extends SSHNPForward if (counter == 0 || client.isClosed) { timer.cancel(); if (!client.isClosed) client.close(); - doneCompleter.complete(); logger.shout( '$sessionId | no active connections - ssh session complete'); } }); return client; - } on SSHNPError catch (e, s) { - doneCompleter.completeError(e, s); + } on SshnpError catch (_) { rethrow; } catch (e, s) { - doneCompleter.completeError(e, s); - throw SSHNPError( + throw SshnpError( 'SSH Client failure : $e', error: e, stackTrace: s, diff --git a/packages/noports_core/lib/src/sshnp/util/sshnp_ssh_key_handler.dart b/packages/noports_core/lib/src/sshnp/util/sshnp_ssh_key_handler.dart new file mode 100644 index 000000000..8c91f3679 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshnp_ssh_key_handler.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:meta/meta.dart'; +import 'package:noports_core/src/sshnp/sshnp_core.dart'; +import 'package:noports_core/src/sshnp/models/sshnp_result.dart'; +import 'package:noports_core/utils.dart'; + +mixin SshnpKeyHandler { + @protected + AtSshKeyUtil get keyUtil; + + @protected + AtSshKeyPair? get identityKeyPair; +} + +mixin SshnpLocalSshKeyHandler on SshnpCore implements SshnpKeyHandler { + @override + LocalSshKeyUtil get keyUtil => _sshKeyUtil; + final LocalSshKeyUtil _sshKeyUtil = LocalSshKeyUtil(); + + @override + AtSshKeyPair? get identityKeyPair => _identityKeyPair; + AtSshKeyPair? _identityKeyPair; + + @override + Future initialize() async { + if (!isSafeToInitialize) return; + logger.info('Initializing SshnpLocalSshKeyHandler'); + + if (!keyUtil.isValidPlatform) { + throw SshnpError( + 'The current platform is not supported with the local SSH key handler: ${Platform.operatingSystem}'); + } + + if (params.identityFile != null) { + logger.info('Loading identity key pair from ${params.identityFile}'); + _identityKeyPair = await keyUtil.getKeyPair( + identifier: params.identityFile!, + passphrase: params.identityPassphrase, + ); + } + + await super.initialize(); + } +} + +mixin SshnpDartSshKeyHandler on SshnpCore implements SshnpKeyHandler { + @override + DartSshKeyUtil get keyUtil => _sshKeyUtil; + final DartSshKeyUtil _sshKeyUtil = DartSshKeyUtil(); + + @override + AtSshKeyPair? get identityKeyPair => _identityKeyPair; + AtSshKeyPair? _identityKeyPair; +} 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 new file mode 100644 index 000000000..16c2dcea1 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart @@ -0,0 +1,258 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:at_client/at_client.dart'; +import 'package:at_commons/at_builders.dart'; +import 'package:at_utils/at_logger.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/mixins/async_initialization.dart'; +import 'package:noports_core/src/common/mixins/at_client_bindings.dart'; +import 'package:noports_core/sshnp.dart'; +import 'package:noports_core/utils.dart'; + +/// enum for sshnpd acknowledgement state +enum SshnpdAck { + /// sshnpd acknowledged our request + acknowledged, + + /// sshnpd acknowledged our request and had errors + acknowledgedWithErrors, + + /// sshnpd did not acknowledge our request + notAcknowledged, +} + +/// This is the generic class which represents the channel between the client +/// and the daemon. It is responsible for sending the request to the daemon and +/// receiving the response from the daemon. +abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { + @override + final logger = AtSignLogger(' SshnpdChannel '); + @override + final AtClient atClient; + + final SshnpParams params; + final String sessionId; + final String namespace; + + // * Volatile fields set at runtime + + /// State of sshnpd acknowledgement + @visibleForTesting + @protected + SshnpdAck sshnpdAck = SshnpdAck.notAcknowledged; + + SshnpdChannel({ + required this.atClient, + required this.params, + required this.sessionId, + required this.namespace, + }); + + /// Initialization starts the subscription to notifications from the daemon. + @override + Future initialize() async { + String regex = '$sessionId.$namespace${params.sshnpdAtSign}'; + logger.info('Starting monitor for notifications with regex: "$regex"'); + atClient.notificationService + .subscribe( + regex: regex, + shouldDecrypt: true, + ) + .listen(_handleSshnpdResponses); + } + + /// Main reponse handler for the daemon's notifications. + Future _handleSshnpdResponses(AtNotification notification) async { + String notificationKey = notification.key + .replaceAll('${notification.to}:', '') + .replaceAll('.$namespace@${notification.from}', '') + // convert to lower case as the latest AtClient converts notification + // keys to lower case when received + .toLowerCase(); + logger.info('Received $notificationKey notification'); + + sshnpdAck = await handleSshnpdPayload(notification); + + if (sshnpdAck == SshnpdAck.acknowledged) { + logger.info('Session $sessionId connected successfully'); + } + } + + /// This method is responsible for handling and validating the payload + /// received from the daemon and setting the [ephemeralPrivateKey] field. + /// Returns acknowledgement state. + @protected + Future handleSshnpdPayload(AtNotification notification); + + /// Wait until we've received an acknowledgement from the daemon. + /// 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'); + await Future.delayed(Duration(milliseconds: 100)); + if (sshnpdAck != SshnpdAck.notAcknowledged) break; + } + return sshnpdAck; + } + + /// Send a notification to the daemon with our shared public key. + /// Does nothing if [params.sendSshPublicKey] is false or if [identityKeyPair] + /// is null. + Future sharePublicKeyIfRequired(AtSshKeyPair? identityKeyPair) async { + if (!params.sendSshPublicKey) { + logger.info( + 'Skipped sharing public key with sshnpd: sendSshPublicKey=false'); + return; + } + if (identityKeyPair == null) { + logger.info( + 'Skipped sharing public key with sshnpd: no identity key pair set'); + return; + } + + var publicKeyContents = identityKeyPair.publicKeyContents; + + logger.info('Sharing public key with sshnpd'); + try { + logger.info('sharing ssh public key: $publicKeyContents'); + if (!publicKeyContents.startsWith('ssh-')) { + logger.severe('SSH Public Key does not look like a public key file'); + throw ('SSH Public Key does not look like a public key file'); + } + AtKey sendOurPublicKeyToSshnpd = AtKey() + ..key = 'sshpublickey' + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..metadata = (Metadata()..ttl = 10000); + await notify(sendOurPublicKeyToSshnpd, publicKeyContents); + } catch (e, s) { + throw SshnpError( + 'Error opening or validating public key file or sending to remote atSign', + error: e, + stackTrace: s, + ); + } + } + + /// Resolve the remote username to use in the ssh session. + /// If [params.remoteUsername] is set, it will be used. + /// Otherwise, the username will be fetched from the remote atSign. + /// Returns null if the username could not be resolved. + Future resolveRemoteUsername() async { + if (params.remoteUsername != null) { + return params.remoteUsername!; + } + AtKey userNameRecordID = AtKey.fromString( + '${params.clientAtSign}:username.$namespace${params.sshnpdAtSign}'); + + try { + return (await atClient.get(userNameRecordID).catchError( + (_) { + throw SshnpError('Remote username record not shared with the client'); + }, + )) + .value; + } catch (e) { + logger.info(e.toString()); + return null; + } + } + + /// List all available devices from the daemon. + /// Returns a [SSHPNPDeviceList] object which contains a map of device names + /// and corresponding info, and a list of active devices (devices which also + /// responded to our real-time ping). + Future listDevices() async { + // get all the keys device_info.*.sshnpd + var scanRegex = + 'device_info\\.$sshnpDeviceNameRegex\\.${DefaultArgs.namespace}'; + + var atKeys = + await _getAtKeysRemote(regex: scanRegex, sharedBy: params.sshnpdAtSign); + + SshnpDeviceList deviceList = SshnpDeviceList(); + + // Listen for heartbeat notifications + atClient.notificationService + .subscribe( + regex: 'heartbeat\\.$sshnpDeviceNameRegex', shouldDecrypt: true) + .listen((notification) { + var deviceInfo = jsonDecode(notification.value ?? '{}'); + var devicename = deviceInfo['devicename']; + if (devicename != null) { + deviceList.setActive(devicename); + } + }); + + // for each key, get the value + for (var entryKey in atKeys) { + var atValue = await atClient.get( + entryKey, + getRequestOptions: GetRequestOptions()..bypassCache = true, + ); + var deviceInfo = jsonDecode(atValue.value) ?? {}; + + if (deviceInfo['devicename'] == null) { + continue; + } + + var devicename = deviceInfo['devicename'] as String; + deviceList.info[devicename] = deviceInfo; + + var metaData = Metadata() + ..isPublic = false + ..isEncrypted = true + ..namespaceAware = true; + + var pingKey = AtKey() + ..key = "ping.$devicename" + ..sharedBy = params.clientAtSign + ..sharedWith = entryKey.sharedBy + ..namespace = DefaultArgs.namespace + ..metadata = metaData; + + unawaited(notify(pingKey, 'ping')); + } + + // wait for 10 seconds in case any are being slow + await Future.delayed(const Duration(seconds: 10)); + + return deviceList; + } + + /// A custom implementation of AtClient.getAtKeys which bypasses the cache + Future> _getAtKeysRemote( + {String? regex, + String? sharedBy, + String? sharedWith, + bool showHiddenKeys = false}) async { + var builder = ScanVerbBuilder() + ..sharedWith = sharedWith + ..sharedBy = sharedBy + ..regex = regex + ..showHiddenKeys = showHiddenKeys + ..auth = true; + var scanResult = await atClient.getRemoteSecondary()?.executeVerb(builder); + scanResult = scanResult?.replaceFirst('data:', '') ?? ''; + var result = []; + if (scanResult.isNotEmpty) { + result = List.from(jsonDecode(scanResult)).map((key) { + try { + return AtKey.fromString(key); + } on InvalidSyntaxException { + logger.severe('$key is not a well-formed key'); + } on Exception catch (e) { + logger.severe( + 'Exception occurred: ${e.toString()}. Unable to form key $key'); + } + }).toList(); + } + result.removeWhere((element) => element == null); + return result.cast(); + } +} diff --git a/packages/noports_core/lib/src/sshnp/mixins/sshnpd_payload_handler.dart b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart similarity index 50% rename from packages/noports_core/lib/src/sshnp/mixins/sshnpd_payload_handler.dart rename to packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart index a9e71429f..be7ef3dca 100644 --- a/packages/noports_core/lib/src/sshnp/mixins/sshnpd_payload_handler.dart +++ b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart @@ -3,19 +3,34 @@ import 'dart:convert'; import 'package:at_client/at_client.dart'; import 'package:meta/meta.dart'; -import 'package:noports_core/src/sshnp/mixins/sshnp_ssh_key_handler.dart'; -import 'package:noports_core/sshnp_core.dart'; +import 'package:noports_core/src/sshnp/util/sshnp_ssh_key_handler.dart'; +import 'package:noports_core/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart'; import 'package:noports_core/utils.dart'; -mixin DefaultSSHNPDPayloadHandler on SSHNPCore { - @protected +class SshnpdDefaultChannel extends SshnpdChannel + with SshnpdDefaultPayloadHandler { + SshnpdDefaultChannel({ + required super.atClient, + required super.params, + required super.sessionId, + required super.namespace, + }); +} + +mixin SshnpdDefaultPayloadHandler on SshnpdChannel { late final String ephemeralPrivateKey; @protected - bool get useLocalFileStorage => (this is SSHNPLocalSSHKeyHandler); + bool get useLocalFileStorage => (this is SshnpLocalSshKeyHandler); + + @override + Future initialize() async { + await super.initialize(); + completeInitialization(); + } @override - FutureOr handleSshnpdPayload(AtNotification notification) async { + Future handleSshnpdPayload(AtNotification notification) async { if (notification.value?.startsWith('{') ?? false) { late final Map envelope; late final Map daemonResponse; @@ -31,35 +46,30 @@ mixin DefaultSSHNPDPayloadHandler on SSHNPCore { } catch (e) { logger.warning( 'Failed to extract parameters from notification value "${notification.value}" with error : $e'); - sshnpdAck = true; - sshnpdAckErrors = true; - return false; + return SshnpdAck.acknowledgedWithErrors; } try { - await verifyEnvelopeSignature(atClient, sshnpdAtSign, logger, envelope, - useFileStorage: useLocalFileStorage); + await verifyEnvelopeSignature( + atClient, + params.sshnpdAtSign, + logger, + envelope, + useFileStorage: useLocalFileStorage, + ); } catch (e) { - logger.shout('Failed to verify signature of msg from $sshnpdAtSign'); + logger.shout( + 'Failed to verify signature of msg from ${params.sshnpdAtSign}'); logger.shout('Exception: $e'); logger.shout('Notification value: ${notification.value}'); - sshnpdAck = true; - sshnpdAckErrors = true; - return false; + return SshnpdAck.acknowledgedWithErrors; } - logger.info('Verified signature of msg from $sshnpdAtSign'); + logger.info('Verified signature of msg from ${params.sshnpdAtSign}'); logger.info('Setting ephemeralPrivateKey'); ephemeralPrivateKey = daemonResponse['ephemeralPrivateKey']; - return true; + return SshnpdAck.acknowledged; } - return false; - } -} - -mixin LegacySSHNPDPayloadHandler on SSHNPCore { - @override - bool handleSshnpdPayload(AtNotification notification) { - return (notification.value == 'connected'); + return SshnpdAck.acknowledgedWithErrors; } } diff --git a/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel.dart b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel.dart new file mode 100644 index 000000000..01fde7982 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +import 'package:noports_core/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart'; + +class SshnpdUnsignedChannel extends SshnpdChannel + with SshnpdUnsignedPayloadHandler { + SshnpdUnsignedChannel({ + required super.atClient, + required super.params, + required super.sessionId, + required super.namespace, + }); +} + +mixin SshnpdUnsignedPayloadHandler on SshnpdChannel { + @override + Future initialize() async { + await super.initialize(); + completeInitialization(); + } + + @override + Future handleSshnpdPayload(AtNotification notification) async { + return (notification.value == 'connected') + ? SshnpdAck.acknowledged + : SshnpdAck.acknowledgedWithErrors; + } +} 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 new file mode 100644 index 000000000..3fa50e5e8 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart @@ -0,0 +1,126 @@ +import 'dart:async'; + +import 'package:at_client/at_client.dart'; +import 'package:at_utils/at_utils.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/mixins/async_initialization.dart'; +import 'package:noports_core/src/common/mixins/at_client_bindings.dart'; +import 'package:noports_core/sshnp.dart'; +import 'package:noports_core/sshrv.dart'; +import 'package:noports_core/sshrvd.dart'; + +@visibleForTesting +enum SshrvdAck { + /// sshrvd acknowledged our request + acknowledged, + + /// sshrvd acknowledged our request and had errors + acknowledgedWithErrors, + + /// sshrvd did not acknowledge our request + notAcknowledged, +} + +abstract class SshrvdChannel with AsyncInitialization, AtClientBindings { + @override + final logger = AtSignLogger(' SshrvdChannel '); + + @override + final AtClient atClient; + + final SshrvGenerator sshrvGenerator; + final SshnpParams params; + final String sessionId; + + // * Volatile fields which are set in [params] but may be overridden with + // * values provided by sshrvd + + String? _host; + int? _port; + + String get host => _host ?? params.host; + int get port => _port ?? params.port; + + // * Volatile fields set at runtime + + /// Whether sshrvd acknowledged our request + @visibleForTesting + SshrvdAck sshrvdAck = SshrvdAck.notAcknowledged; + + /// The port sshrvd is listening on + int? _sshrvdPort; + int? get sshrvdPort => _sshrvdPort; + + SshrvdChannel({ + required this.atClient, + required this.params, + required this.sessionId, + required this.sshrvGenerator, + }); + + @override + Future initialize() async { + if (params.host.startsWith('@')) { + await getHostAndPortFromSshrvd(); + } else { + _host = params.host; + _port = params.port; + } + completeInitialization(); + } + + Future runSshrv() async { + await callInitialization(); + if (_sshrvdPort == null) throw Exception('sshrvdPort is null'); + + // Connect to rendezvous point using background process. + // sshnp (this program) can then exit without issue. + Sshrv sshrv = sshrvGenerator( + params.host, + _sshrvdPort!, + localSshdPort: params.localSshdPort, + ); + return sshrv.run(); + } + + @protected + Future getHostAndPortFromSshrvd() async { + sshrvdAck = SshrvdAck.notAcknowledged; + atClient.notificationService + .subscribe( + regex: '$sessionId.${Sshrvd.namespace}@', shouldDecrypt: true) + .listen((notification) async { + String ipPorts = notification.value.toString(); + List results = ipPorts.split(','); + _host = results[0]; + _port = int.parse(results[1]); + _sshrvdPort = int.parse(results[2]); + logger.info('Received host and port from sshrvd: $host:$port'); + logger.info('Set sshrvdPort to: $_sshrvdPort'); + sshrvdAck = SshrvdAck.acknowledged; + }); + logger.info('Started listening for sshrvd response'); + AtKey ourSshrvdIdKey = AtKey() + ..key = '${params.device}.${Sshrvd.namespace}' + ..sharedBy = params.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 + ..namespaceAware = false + ..ttl = 10000); + logger.info('Sending notification to sshrvd: $ourSshrvdIdKey'); + await notify(ourSshrvdIdKey, sessionId); + + int counter = 0; + while (sshrvdAck == SshrvdAck.notAcknowledged) { + logger.info('Waiting for sshrvd response: $counter'); + await Future.delayed(Duration(milliseconds: 100)); + counter++; + 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/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart new file mode 100644 index 000000000..d05b4e6f8 --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart @@ -0,0 +1,10 @@ +import 'package:noports_core/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart'; +import 'package:noports_core/sshrv.dart'; + +class SshrvdDartChannel extends SshrvdChannel { + SshrvdDartChannel({ + required super.atClient, + required super.params, + required super.sessionId, + }) : super(sshrvGenerator: Sshrv.dart); +} diff --git a/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart new file mode 100644 index 000000000..59534554a --- /dev/null +++ b/packages/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart @@ -0,0 +1,10 @@ +import 'package:noports_core/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart'; +import 'package:noports_core/sshrv.dart'; + +class SshrvdExecChannel extends SshrvdChannel { + SshrvdExecChannel({ + required super.atClient, + required super.params, + required super.sessionId, + }) : super(sshrvGenerator: Sshrv.exec); +} diff --git a/packages/noports_core/lib/src/sshnpd/sshnpd.dart b/packages/noports_core/lib/src/sshnpd/sshnpd.dart index 40247cad1..a6ff10859 100644 --- a/packages/noports_core/lib/src/sshnpd/sshnpd.dart +++ b/packages/noports_core/lib/src/sshnpd/sshnpd.dart @@ -7,7 +7,7 @@ import 'package:noports_core/src/common/types.dart'; import 'package:noports_core/src/sshnpd/sshnpd_impl.dart'; import 'package:noports_core/src/sshnpd/sshnpd_params.dart'; -abstract class SSHNPD { +abstract class Sshnpd { abstract final AtSignLogger logger; /// The [AtClient] used to communicate with sshnpd and sshrvd @@ -65,15 +65,15 @@ abstract class SSHNPD { /// The algorithm to use for ssh encryption /// Can be one of [SupportedSSHAlgorithm.values]: - /// - [SupportedSSHAlgorithm.ed25519] - /// - [SupportedSSHAlgorithm.rsa] - abstract final SupportedSSHAlgorithm sshAlgorithm; + /// - [SupportedSshAlgorithm.ed25519] + /// - [SupportedSshAlgorithm.rsa] + abstract final SupportedSshAlgorithm sshAlgorithm; - static Future fromCommandLineArgs(List args, + static Future fromCommandLineArgs(List args, {AtClient? atClient, - FutureOr Function(SSHNPDParams)? atClientGenerator, + FutureOr Function(SshnpdParams)? atClientGenerator, void Function(Object, StackTrace)? usageCallback}) async { - return SSHNPDImpl.fromCommandLineArgs( + return SshnpdImpl.fromCommandLineArgs( args, atClient: atClient, atClientGenerator: atClientGenerator, diff --git a/packages/noports_core/lib/src/sshnpd/sshnpd_impl.dart b/packages/noports_core/lib/src/sshnpd/sshnpd_impl.dart index 3b0b5305d..d2faf3fcc 100644 --- a/packages/noports_core/lib/src/sshnpd/sshnpd_impl.dart +++ b/packages/noports_core/lib/src/sshnpd/sshnpd_impl.dart @@ -14,7 +14,7 @@ import 'package:noports_core/src/version.dart'; import 'package:uuid/uuid.dart'; @protected -class SSHNPDImpl implements SSHNPD { +class SshnpdImpl implements Sshnpd { @override final AtSignLogger logger = AtSignLogger(' sshnpd '); @@ -52,7 +52,7 @@ class SSHNPDImpl implements SSHNPD { final String ephemeralPermissions; @override - final SupportedSSHAlgorithm sshAlgorithm; + final SupportedSshAlgorithm sshAlgorithm; @override @visibleForTesting @@ -63,7 +63,7 @@ class SSHNPDImpl implements SSHNPD { static const String commandToSend = 'sshd'; - SSHNPDImpl({ + SshnpdImpl({ // final fields required this.atClient, required this.username, @@ -81,12 +81,12 @@ class SSHNPDImpl implements SSHNPD { logger.logger.level = Level.SHOUT; } - static Future fromCommandLineArgs(List args, + static Future fromCommandLineArgs(List args, {AtClient? atClient, - FutureOr Function(SSHNPDParams)? atClientGenerator, + FutureOr Function(SshnpdParams)? atClientGenerator, void Function(Object, StackTrace)? usageCallback}) async { try { - var p = await SSHNPDParams.fromArgs(args); + var p = await SshnpdParams.fromArgs(args); // Check atKeyFile selected exists if (!await File(p.atKeysFilePath).exists()) { @@ -104,7 +104,7 @@ class SSHNPDImpl implements SSHNPD { atClient ??= await atClientGenerator!(p); - var sshnpd = SSHNPDImpl( + var sshnpd = SshnpdImpl( atClient: atClient, username: p.username, homeDirectory: p.homeDirectory, @@ -498,12 +498,12 @@ class SSHNPDImpl implements SSHNPD { // Connect to rendezvous point using background process. // This program can then exit without causing an issue. Process rv = - await SSHRV.exec(host, port, localSshdPort: localSshdPort).run(); + await Sshrv.exec(host, port, localSshdPort: localSshdPort).run(); logger.info('Started rv - pid is ${rv.pid}'); - LocalSSHKeyUtil keyUtil = LocalSSHKeyUtil(); + LocalSshKeyUtil keyUtil = LocalSshKeyUtil(); - AtSSHKeyPair keyPair = await keyUtil.generateKeyPair( + AtSshKeyPair keyPair = await keyUtil.generateKeyPair( algorithm: sshAlgorithm, identifier: 'ephemeral_$sessionId'); await keyUtil.authorizePublicKey( @@ -757,7 +757,7 @@ class SSHNPDImpl implements SSHNPD { // // We don't want keyboard interactive: we add -o BatchMode=yes // - // For convenience of this SSHNPD, we would like to know as quickly + // For convenience of this Sshnpd, we would like to know as quickly // as possible if the ssh connection has succeeded or not. // So we will add options 'ForkAfterAuthentication=yes' and also // 'ExitOnForwardFailure=yes' so that it won't fork until after diff --git a/packages/noports_core/lib/src/sshnpd/sshnpd_params.dart b/packages/noports_core/lib/src/sshnpd/sshnpd_params.dart index 896fe24a9..79365c6f9 100644 --- a/packages/noports_core/lib/src/sshnpd/sshnpd_params.dart +++ b/packages/noports_core/lib/src/sshnpd/sshnpd_params.dart @@ -4,7 +4,7 @@ import 'package:noports_core/src/common/file_system_utils.dart'; import 'package:noports_core/src/common/types.dart'; import 'package:noports_core/src/common/validation_utils.dart'; -class SSHNPDParams { +class SshnpdParams { final String device; final String username; final String homeDirectory; @@ -18,11 +18,11 @@ class SSHNPDParams { final String rootDomain; final int localSshdPort; final String ephemeralPermissions; - final SupportedSSHAlgorithm sshAlgorithm; + final SupportedSshAlgorithm sshAlgorithm; // Non param variables static final ArgParser parser = _createArgParser(); - SSHNPDParams({ + SshnpdParams({ required this.device, required this.username, required this.homeDirectory, @@ -39,7 +39,7 @@ class SSHNPDParams { required this.sshAlgorithm, }); - static Future fromArgs(List args) async { + static Future fromArgs(List args) async { // Arg check ArgResults r = parser.parse(args); @@ -52,14 +52,14 @@ class SSHNPDParams { SupportedSshClient sshClient = SupportedSshClient.values.firstWhere( (c) => c.toString() == r['ssh-client'], - orElse: () => DefaultSSHNPDArgs.sshClient); + orElse: () => DefaultSshnpdArgs.sshClient); // Do we have an ASCII ? if (checkNonAscii(device)) { throw ('\nDevice name can only contain alphanumeric characters with a max length of 15'); } - return SSHNPDParams( + return SshnpdParams( device: r['device'], username: getUserName(throwIfNull: true)!, homeDirectory: homeDirectory, @@ -75,7 +75,7 @@ class SSHNPDParams { localSshdPort: int.tryParse(r['local-sshd-port']) ?? DefaultArgs.localSshdPort, ephemeralPermissions: r['ephemeral-permissions'], - sshAlgorithm: SupportedSSHAlgorithm.fromString(r['ssh-algorithm']), + sshAlgorithm: SupportedSshAlgorithm.fromString(r['ssh-algorithm']), ); } @@ -134,7 +134,7 @@ class SSHNPDParams { parser.addOption('ssh-client', mandatory: false, - defaultsTo: DefaultSSHNPDArgs.sshClient.toString(), + defaultsTo: DefaultSshnpdArgs.sshClient.toString(), allowed: SupportedSshClient.values .map( (c) => c.toString(), @@ -168,7 +168,7 @@ class SSHNPDParams { 'ssh-algorithm', defaultsTo: DefaultArgs.sshAlgorithm.toString(), help: 'Use RSA 4096 keys rather than the default ED25519 keys', - allowed: SupportedSSHAlgorithm.values.map((c) => c.toString()).toList(), + allowed: SupportedSshAlgorithm.values.map((c) => c.toString()).toList(), ); return parser; diff --git a/packages/noports_core/lib/src/sshrv/sshrv.dart b/packages/noports_core/lib/src/sshrv/sshrv.dart index 421acab4f..ffbc36ffc 100644 --- a/packages/noports_core/lib/src/sshrv/sshrv.dart +++ b/packages/noports_core/lib/src/sshrv/sshrv.dart @@ -4,7 +4,7 @@ import 'package:noports_core/src/sshrv/sshrv_impl.dart'; import 'package:socket_connector/socket_connector.dart'; import 'package:noports_core/src/common/default_args.dart'; -abstract class SSHRV { +abstract class Sshrv { /// The internet address of the host to connect to. abstract final String host; @@ -18,24 +18,24 @@ abstract class SSHRV { Future run(); // Can't use factory functions since SSHRV contains a generic type - static SSHRV exec( + static Sshrv exec( String host, int streamingPort, { int localSshdPort = DefaultArgs.localSshdPort, }) { - return SSHRVImplExec( + return SshrvImplExec( host, streamingPort, localSshdPort: localSshdPort, ); } - static SSHRV dart( + static Sshrv dart( String host, int streamingPort, { int localSshdPort = 22, }) { - return SSHRVImplDart( + return SshrvImplDart( host, streamingPort, localSshdPort: localSshdPort, diff --git a/packages/noports_core/lib/src/sshrv/sshrv_impl.dart b/packages/noports_core/lib/src/sshrv/sshrv_impl.dart index 72d219f75..f708708b4 100644 --- a/packages/noports_core/lib/src/sshrv/sshrv_impl.dart +++ b/packages/noports_core/lib/src/sshrv/sshrv_impl.dart @@ -8,7 +8,7 @@ import 'package:socket_connector/socket_connector.dart'; import 'package:noports_core/src/common/default_args.dart'; @visibleForTesting -class SSHRVImplExec implements SSHRV { +class SshrvImplExec implements Sshrv { @override final String host; @@ -18,7 +18,7 @@ class SSHRVImplExec implements SSHRV { @override final int localSshdPort; - const SSHRVImplExec( + const SshrvImplExec( this.host, this.streamingPort, { this.localSshdPort = DefaultArgs.localSshdPort, @@ -26,7 +26,7 @@ class SSHRVImplExec implements SSHRV { @override Future run() async { - String? command = await SSHRV.getLocalBinaryPath(); + String? command = await Sshrv.getLocalBinaryPath(); String postfix = Platform.isWindows ? '.exe' : ''; if (command == null) { throw Exception( @@ -43,7 +43,7 @@ class SSHRVImplExec implements SSHRV { } @visibleForTesting -class SSHRVImplDart implements SSHRV { +class SshrvImplDart implements Sshrv { @override final String host; @@ -53,7 +53,7 @@ class SSHRVImplDart implements SSHRV { @override final int localSshdPort; - const SSHRVImplDart( + const SshrvImplDart( this.host, this.streamingPort, { this.localSshdPort = 22, diff --git a/packages/noports_core/lib/src/sshrvd/sshrvd.dart b/packages/noports_core/lib/src/sshrvd/sshrvd.dart index aabc46ac9..33f204861 100644 --- a/packages/noports_core/lib/src/sshrvd/sshrvd.dart +++ b/packages/noports_core/lib/src/sshrvd/sshrvd.dart @@ -6,7 +6,7 @@ import 'package:meta/meta.dart'; import 'package:noports_core/src/sshrvd/sshrvd_impl.dart'; import 'package:noports_core/src/sshrvd/sshrvd_params.dart'; -abstract class SSHRVD { +abstract class Sshrvd { static const String namespace = 'sshrvd'; abstract final AtSignLogger logger; @@ -22,16 +22,14 @@ abstract class SSHRVD { @visibleForTesting bool initialized = false; - static Future fromCommandLineArgs(List args, + static Future fromCommandLineArgs(List args, {AtClient? atClient, - FutureOr Function(SSHRVDParams)? atClientGenerator, + FutureOr Function(SshrvdParams)? atClientGenerator, void Function(Object, StackTrace)? usageCallback}) async { - return SSHRVDImpl.fromCommandLineArgs( - args, - atClient: atClient, - atClientGenerator: atClientGenerator, - usageCallback: usageCallback - ); + return SshrvdImpl.fromCommandLineArgs(args, + atClient: atClient, + atClientGenerator: atClientGenerator, + usageCallback: usageCallback); } Future init(); diff --git a/packages/noports_core/lib/src/sshrvd/sshrvd_impl.dart b/packages/noports_core/lib/src/sshrvd/sshrvd_impl.dart index 883f255c4..e14465995 100644 --- a/packages/noports_core/lib/src/sshrvd/sshrvd_impl.dart +++ b/packages/noports_core/lib/src/sshrvd/sshrvd_impl.dart @@ -11,7 +11,7 @@ import 'package:noports_core/src/sshrvd/sshrvd.dart'; import 'package:noports_core/src/sshrvd/sshrvd_params.dart'; @protected -class SSHRVDImpl implements SSHRVD { +class SshrvdImpl implements Sshrvd { @override final AtSignLogger logger = AtSignLogger(' sshrvd '); @override @@ -33,7 +33,7 @@ class SSHRVDImpl implements SSHRVD { @visibleForTesting bool initialized = false; - SSHRVDImpl({ + SshrvdImpl({ required this.atClient, required this.atSign, required this.homeDirectory, @@ -46,12 +46,12 @@ class SSHRVDImpl implements SSHRVD { logger.logger.level = Level.SHOUT; } - static Future fromCommandLineArgs(List args, + static Future fromCommandLineArgs(List args, {AtClient? atClient, - FutureOr Function(SSHRVDParams)? atClientGenerator, + FutureOr Function(SshrvdParams)? atClientGenerator, void Function(Object, StackTrace)? usageCallback}) async { try { - var p = await SSHRVDParams.fromArgs(args); + var p = await SshrvdParams.fromArgs(args); if (!await File(p.atKeysFilePath).exists()) { throw ('\n Unable to find .atKeys file : ${p.atKeysFilePath}'); @@ -68,7 +68,7 @@ class SSHRVDImpl implements SSHRVD { atClient ??= await atClientGenerator!(p); - var sshrvd = SSHRVDImpl( + var sshrvd = SshrvdImpl( atClient: atClient, atSign: p.atSign, homeDirectory: p.homeDirectory, @@ -105,12 +105,12 @@ class SSHRVDImpl implements SSHRVD { NotificationService notificationService = atClient.notificationService; notificationService - .subscribe(regex: '${SSHRVD.namespace}@', shouldDecrypt: true) + .subscribe(regex: '${Sshrvd.namespace}@', shouldDecrypt: true) .listen(_notificationHandler); } void _notificationHandler(AtNotification notification) async { - if (!notification.key.contains(SSHRVD.namespace)) { + if (!notification.key.contains(Sshrvd.namespace)) { // ignore notifications not for this namespace return; } @@ -139,7 +139,7 @@ class SSHRVDImpl implements SSHRVD { ..key = notification.value ..sharedBy = atSign ..sharedWith = notification.from - ..namespace = SSHRVD.namespace + ..namespace = Sshrvd.namespace ..metadata = metaData; String data = '$ipAddress,$portA,$portB'; diff --git a/packages/noports_core/lib/src/sshrvd/sshrvd_params.dart b/packages/noports_core/lib/src/sshrvd/sshrvd_params.dart index 80200f479..97c3ad5fa 100644 --- a/packages/noports_core/lib/src/sshrvd/sshrvd_params.dart +++ b/packages/noports_core/lib/src/sshrvd/sshrvd_params.dart @@ -1,7 +1,7 @@ import 'package:args/args.dart'; import 'package:noports_core/src/common/file_system_utils.dart'; -class SSHRVDParams { +class SshrvdParams { final String username; final String atSign; final String homeDirectory; @@ -15,7 +15,7 @@ class SSHRVDParams { // Non param variables static final ArgParser parser = _createArgParser(); - SSHRVDParams({ + SshrvdParams({ required this.username, required this.atSign, required this.homeDirectory, @@ -27,14 +27,14 @@ class SSHRVDParams { required this.rootDomain, }); - static Future fromArgs(List args) async { + static Future fromArgs(List args) async { // Arg check ArgResults r = parser.parse(args); String atSign = r['atsign']; String homeDirectory = getHomeDirectory()!; - return SSHRVDParams( + return SshrvdParams( username: getUserName(throwIfNull: true)!, atSign: atSign, homeDirectory: homeDirectory, diff --git a/packages/noports_core/lib/sshnp.dart b/packages/noports_core/lib/sshnp.dart index dd18bc06e..7fe11dec1 100644 --- a/packages/noports_core/lib/sshnp.dart +++ b/packages/noports_core/lib/sshnp.dart @@ -1,6 +1,7 @@ library noports_core_sshnp; export 'src/sshnp/sshnp.dart'; -export 'src/sshnp/sshnp_result.dart'; -export 'src/sshnp/sshnp_params/sshnp_params.dart'; +export 'src/sshnp/models/sshnp_result.dart'; +export 'src/sshnp/models/sshnp_params.dart'; +export 'src/sshnp/models/sshnp_device_list.dart'; export 'src/common/types.dart'; diff --git a/packages/noports_core/lib/sshnp_core.dart b/packages/noports_core/lib/sshnp_core.dart deleted file mode 100644 index 68a3d2d9a..000000000 --- a/packages/noports_core/lib/sshnp_core.dart +++ /dev/null @@ -1,3 +0,0 @@ -library noports_core_sshnp_core; - -export 'src/sshnp/sshnp_core.dart'; diff --git a/packages/noports_core/lib/sshnp_foundation.dart b/packages/noports_core/lib/sshnp_foundation.dart new file mode 100644 index 000000000..c76b516ce --- /dev/null +++ b/packages/noports_core/lib/sshnp_foundation.dart @@ -0,0 +1,48 @@ +library noports_core_sshnp_foundation; + +/// Sshnp Foundation Library +/// This library is used to build custom Sshnp implementations +/// It is not intended to be used directly by end users +/// All classes and methods are exported here for convenience + +// Core +export 'src/sshnp/sshnp.dart'; +export 'src/sshnp/sshnp_core.dart'; + +// Models +export 'src/sshnp/models/sshnp_arg.dart'; +export 'src/sshnp/models/sshnp_params.dart'; +export 'src/sshnp/models/sshnp_result.dart'; +export 'src/sshnp/models/sshnp_device_list.dart'; + +// Sshnp Utils +export 'src/sshnp/util/sshnpd_channel/sshnpd_channel.dart'; +export 'src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart'; +export 'src/sshnp/util/sshnpd_channel/sshnpd_unsigned_channel.dart'; + +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_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_unsigned_impl.dart'; + +// Common +export 'src/common/at_ssh_key_util/at_ssh_key_util.dart'; +export 'src/common/at_ssh_key_util/dart_ssh_key_util.dart'; +export 'src/common/at_ssh_key_util/local_ssh_key_util.dart'; + +export 'src/common/mixins/async_completion.dart'; +export 'src/common/mixins/async_initialization.dart'; +export 'src/common/mixins/at_client_bindings.dart'; + +export 'src/common/default_args.dart'; +export 'src/common/file_system_utils.dart'; +export 'src/common/types.dart'; +export 'src/common/validation_utils.dart'; diff --git a/packages/noports_core/lib/sshnp_params.dart b/packages/noports_core/lib/sshnp_params.dart index ff17300ef..3191f8118 100644 --- a/packages/noports_core/lib/sshnp_params.dart +++ b/packages/noports_core/lib/sshnp_params.dart @@ -1,7 +1,7 @@ library noports_core_sshnp_params; -export 'src/sshnp/sshnp_params/config_file_repository.dart'; -export 'src/sshnp/sshnp_params/config_key_repository.dart'; -export 'src/sshnp/sshnp_params/sshnp_params.dart'; -export 'src/sshnp/sshnp_params/sshnp_arg.dart'; +export 'src/sshnp/models/config_file_repository.dart'; +export 'src/sshnp/models/config_key_repository.dart'; +export 'src/sshnp/models/sshnp_params.dart'; +export 'src/sshnp/models/sshnp_arg.dart'; export 'src/common/types.dart'; diff --git a/packages/noports_core/lib/utils.dart b/packages/noports_core/lib/utils.dart index bd2286ba0..7d406166d 100644 --- a/packages/noports_core/lib/utils.dart +++ b/packages/noports_core/lib/utils.dart @@ -1,7 +1,7 @@ library noports_core_utils; +export 'src/common/at_ssh_key_util/at_ssh_key_util.dart'; +export 'src/common/default_args.dart'; +export 'src/common/file_system_utils.dart'; export 'src/common/types.dart'; export 'src/common/validation_utils.dart'; -export 'src/common/file_system_utils.dart'; -export 'src/common/ssh_key_utils.dart'; -export 'src/common/default_args.dart'; diff --git a/packages/noports_core/test/sshnp/sshnp_core_test.dart b/packages/noports_core/test/sshnp/sshnp_core_test.dart new file mode 100644 index 000000000..53584b581 --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_core_test.dart @@ -0,0 +1,38 @@ +import 'package:at_client/at_client.dart'; +import 'package:noports_core/sshnp_params.dart'; +import 'package:test/test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAtClient extends Mock implements AtClient {} + +class MockSshnpParams extends Mock implements SshnpParams {} + +void main() { + group('Sshnp Core', () { + late AtClient atClient; + late SshnpParams params; + + setUp(() { + atClient = MockAtClient(); + params = MockSshnpParams(); + registerFallbackValue(AtClientPreference()); + }); + + test('Constructor - expect that the namespace is set based on params', () { + verifyNever(() => atClient.getPreferences()); + verifyNever(() => params.device); + verifyNever(() => atClient.setPreferences(any())); + + when(() => atClient.getPreferences()).thenReturn(null); + when(() => params.device).thenReturn('mydevice'); + when(() => atClient.setPreferences(any())).thenReturn(null); + +// TODO write a new MYSshnpCore class + // final sshnpCore = MySshnpCore(atClient: atClient, params: params); + + // verify(() => atClient.getPreferences()).called(1); + // verify(() => params.device).called(1); + // verify(() => atClient.setPreferences(any())).called(1); + }); + }); +} diff --git a/packages/noports_core/test/sshnp/sshnp_params/config_file_repository_test.dart b/packages/noports_core/test/sshnp/sshnp_params/config_file_repository_test.dart new file mode 100644 index 000000000..29a6bb237 --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_params/config_file_repository_test.dart @@ -0,0 +1,26 @@ +import 'package:noports_core/sshnp_params.dart'; +import 'package:noports_core/utils.dart'; +import 'package:test/test.dart'; +import 'package:path/path.dart' as path; + +void main() { + group('', () { + test('ConfigFileRepository.atKeyFromProfileName test', () async { + String profileName = 'myProfileName'; + + expect(ConfigFileRepository.getDefaultSshnpConfigDirectory(getHomeDirectory()!), isA()); + expect(ConfigFileRepository.fromProfileName(profileName), isA>()); + expect(ConfigFileRepository.fromProfileName(profileName), completes); + expect( + await ConfigFileRepository.fromProfileName(profileName, basenameOnly: false), + equals(path.join(getHomeDirectory()!, '.sshnp', 'config', '$profileName.env')), + ); + expect(await ConfigFileRepository.fromProfileName(profileName, basenameOnly: true), equals('$profileName.env')); + }); + + group('[depends on ConfigFileRepository.atKeyFromProfileName]', () { + // TODO implement these tests with mock file system + // not a priority, so skipping for now + }); + }); +} diff --git a/packages/noports_core/test/sshnp/sshnp_params/config_key_repository_test.dart b/packages/noports_core/test/sshnp/sshnp_params/config_key_repository_test.dart new file mode 100644 index 000000000..137cd0dd6 --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_params/config_key_repository_test.dart @@ -0,0 +1,177 @@ +import 'package:at_client/at_client.dart'; +import 'package:noports_core/sshnp_params.dart'; +import 'package:test/test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAtClient extends Mock implements AtClient {} + +void main() { + group('ConfigKeyRepository', () { + /// NB: other tests depend on [ConfigKeyRepository.atKeyFromProfileName] + /// other tests may fail if this test fails + test('ConfigKeyRepository.atKeyFromProfileName test', () { + String profileName = 'myProfileName'; + String sharedBy = '@owner'; + + expect(ConfigKeyRepository.fromProfileName(profileName), isA()); + expect( + ConfigKeyRepository.fromProfileName(profileName, sharedBy: sharedBy) + .sharedBy, + equals(sharedBy)); + + expect(ConfigKeyRepository.fromProfileName(profileName).key, + equals('${ConfigKeyRepository.keyPrefix}$profileName')); + }); + + group('[depends on ConfigKeyRepository.atKeyFromProfileName]', () { + late MockAtClient atClient; + + setUpAll(() { + atClient = MockAtClient(); + + registerFallbackValue(AtKey()); + + /// Called by [ConfigKeyRepository.listProfiles] + when(() => + atClient.getAtKeys(regex: ConfigKeyRepository.configNamespace)) + .thenAnswer( + (_) => Future.value([ + ConfigKeyRepository.fromProfileName('profileName1'), + ConfigKeyRepository.fromProfileName('profileName2'), + ConfigKeyRepository.fromProfileName('profileName3'), + ]), + ); + + /// Called by [ConfigKeyRepository.getParams] + when(() => atClient.getCurrentAtSign()).thenReturn('@owner'); + + /// Called by [ConfigKeyRepository.getParams] + when(() => atClient.get( + ConfigKeyRepository.fromProfileName('profileName1', + sharedBy: '@owner'), + getRequestOptions: any(named: 'getRequestOptions'), + )).thenAnswer( + (_) => Future.value( + AtValue() + ..value = SshnpParams( + clientAtSign: '@owner', + sshnpdAtSign: '@device', + host: '@host') + .toJson(), + ), + ); + + /// Called by [ConfigKeyRepository.putParams] + when(() => atClient.put(any(), any(), + putRequestOptions: any(named: 'putRequestOptions'))) + .thenAnswer((_) => Future.value(true)); + + /// Called by [ConfigKeyRepository.deleteParams] + when(() => atClient.delete(any(), + deleteRequestOptions: any(named: 'deleteRequestOptions'))) + .thenAnswer((_) => Future.value(true)); + }); + + test('ConfigKeyRepository.atKeyToProfileName test', () { + String profileName = 'my_profile_name'; + AtKey atKey = ConfigKeyRepository.fromProfileName(profileName); + + expect(ConfigKeyRepository.toProfileName(atKey), + equals(profileName.replaceAll('_', ' '))); + expect(ConfigKeyRepository.toProfileName(atKey, replaceSpaces: false), + equals(profileName)); + expect(ConfigKeyRepository.toProfileName(atKey, replaceSpaces: true), + equals(profileName.replaceAll('_', ' '))); + }); + + test('ConfigKeyRepository.listProfiles test', () async { + expect(await ConfigKeyRepository.listProfiles(atClient), + isA>()); + expect(await ConfigKeyRepository.listProfiles(atClient), + equals(['profileName1', 'profileName2', 'profileName3'])); + }); + + test('ConfigKeyRepository.getParams test', () async { + var params = await ConfigKeyRepository.getParams('profileName1', + atClient: atClient); + expect(params, isA()); + expect(params.clientAtSign, equals('@owner')); + expect(params.sshnpdAtSign, equals('@device')); + expect(params.host, equals('@host')); + }); + + test('ConfigKeyRepository.putParams test', () async { + when( + () => atClient.put( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + any(), + putRequestOptions: any(named: 'putRequestOptions'), + ), + ).thenAnswer((_) => Future.value(true)); + + verifyNever( + () => atClient.put( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + any(), + putRequestOptions: any(named: 'putRequestOptions'), + ), + ); + + expect( + ConfigKeyRepository.putParams( + SshnpParams( + clientAtSign: '@owner', + sshnpdAtSign: '@device', + host: '@host', + profileName: 'profileName2'), + atClient: atClient, + ), + completes); + + verify( + () => atClient.put( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + any(), + putRequestOptions: any(named: 'putRequestOptions'), + ), + ).called(1); + }); + + test('ConfigKeyRepository.deleteParams test', () async { + when( + () => atClient.delete( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + deleteRequestOptions: any(named: 'deleteRequestOptions'), + ), + ).thenAnswer((_) => Future.value(true)); + + verifyNever( + () => atClient.delete( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + deleteRequestOptions: any(named: 'deleteRequestOptions'), + ), + ); + + expect( + ConfigKeyRepository.deleteParams( + 'profileName2', + atClient: atClient, + ), + completes); + + verify( + () => atClient.delete( + ConfigKeyRepository.fromProfileName('profileName2', + sharedBy: '@owner'), + deleteRequestOptions: any(named: 'deleteRequestOptions'), + ), + ).called(1); + }); + }); + }); +} diff --git a/packages/noports_core/test/sshnp/sshnp_params/sshnp_arg_test.dart b/packages/noports_core/test/sshnp/sshnp_params/sshnp_arg_test.dart new file mode 100644 index 000000000..3a520952b --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_params/sshnp_arg_test.dart @@ -0,0 +1,222 @@ +import 'package:args/args.dart'; +import 'package:noports_core/sshnp_params.dart'; +import 'package:test/test.dart'; + +void main() { + group('ParserType', () { + test('public API test', () { + // abitrary values + ParserType parserType = ParserType.all; + ParseWhen parseWhen = ParseWhen.always; + + expect(parserType.allowList, isA>()); + expect(parserType.denyList, isA>()); + expect(parserType.shouldParse(parseWhen), isA()); + }); + + group('ParserType.all', () { + test('ParserType.all allowList test', () { + expect(ParserType.all.allowList, contains(ParseWhen.always)); + expect(ParserType.all.allowList, contains(ParseWhen.commandLine)); + expect(ParserType.all.allowList, contains(ParseWhen.configFile)); + }); + + test('ParserType.all denyList test', () { + expect(ParserType.all.denyList, contains(ParseWhen.never)); + }); + + test('ParserType.all shouldParse test', () { + expect(ParserType.all.shouldParse(ParseWhen.always), isTrue); + expect(ParserType.all.shouldParse(ParseWhen.commandLine), isTrue); + expect(ParserType.all.shouldParse(ParseWhen.configFile), isTrue); + expect(ParserType.all.shouldParse(ParseWhen.never), isFalse); + }); + }); + + group('ParserType.commandLine', () { + test('ParserType.commandLine allowList test', () { + expect(ParserType.commandLine.allowList, contains(ParseWhen.always)); + expect( + ParserType.commandLine.allowList, contains(ParseWhen.commandLine)); + expect(ParserType.commandLine.allowList, + isNot(contains(ParseWhen.configFile))); + }); + + test('ParserType.commandLine denyList test', () { + expect( + ParserType.commandLine.denyList, isNot(contains(ParseWhen.always))); + expect(ParserType.commandLine.denyList, + isNot(contains(ParseWhen.commandLine))); + expect(ParserType.commandLine.denyList, contains(ParseWhen.configFile)); + }); + + test('ParserType.commandLine shouldParse test', () { + expect(ParserType.commandLine.shouldParse(ParseWhen.always), isTrue); + expect( + ParserType.commandLine.shouldParse(ParseWhen.commandLine), isTrue); + expect( + ParserType.commandLine.shouldParse(ParseWhen.configFile), isFalse); + expect(ParserType.commandLine.shouldParse(ParseWhen.never), isFalse); + }); + }); + + group('ParserType.configFile', () { + test('ParserType.configFile allowList test', () { + expect(ParserType.configFile.allowList, contains(ParseWhen.always)); + expect(ParserType.configFile.allowList, contains(ParseWhen.configFile)); + expect(ParserType.configFile.allowList, + isNot(contains(ParseWhen.commandLine))); + }); + + test('ParserType.configFile denyList test', () { + expect( + ParserType.configFile.denyList, isNot(contains(ParseWhen.always))); + expect(ParserType.configFile.denyList, + isNot(contains(ParseWhen.configFile))); + expect(ParserType.configFile.denyList, contains(ParseWhen.commandLine)); + }); + + test('ParserType.configFile shouldParse test', () { + expect(ParserType.configFile.shouldParse(ParseWhen.always), isTrue); + expect( + ParserType.configFile.shouldParse(ParseWhen.commandLine), isFalse); + expect(ParserType.configFile.shouldParse(ParseWhen.configFile), isTrue); + expect(ParserType.configFile.shouldParse(ParseWhen.never), isFalse); + }); + }); + }); + + group('SshnpArg', () { + test('public API test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name'); + + expect(sshnpArg.format, isA()); + expect(sshnpArg.name, isA()); + expect(sshnpArg.abbr, isA()); + expect(sshnpArg.help, isA()); + expect(sshnpArg.mandatory, isA()); + expect(sshnpArg.defaultsTo, isA()); + expect(sshnpArg.type, isA()); + expect(sshnpArg.allowed, isA?>()); + expect(sshnpArg.parseWhen, isA()); + expect(sshnpArg.aliases, isA?>()); + + expect(sshnpArg.bashName, isA()); + + expect(SshnpArg.args, isA>()); + expect(SshnpArg.createArgParser(), isA()); + }); + + group('SshnpArg final variables', () { + test('SshnpArg.name test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name'); + expect(sshnpArg.name, equals('name')); + }); + + test('SshnpArg.abbr test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', abbr: 'n'); + expect(sshnpArg.abbr, equals('n')); + }); + + test('SshnpArg.help test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', help: 'help'); + expect(sshnpArg.help, equals('help')); + }); + + test('SshnpArg.mandatory test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', mandatory: true); + expect(sshnpArg.mandatory, isTrue); + }); + + test('SshnpArg.defaultsTo test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', defaultsTo: 'default'); + expect(sshnpArg.defaultsTo, equals('default')); + }); + + test('SshnpArg.type test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', type: ArgType.string); + expect(sshnpArg.type, equals(ArgType.string)); + }); + + test('SshnpArg.allowed test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', allowed: ['allowed']); + expect(sshnpArg.allowed, equals(['allowed'])); + }); + + test('SshnpArg.parseWhen test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', parseWhen: ParseWhen.always); + expect(sshnpArg.parseWhen, equals(ParseWhen.always)); + }); + + test('SshnpArg.aliases test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', aliases: ['alias']); + expect(sshnpArg.aliases, equals(['alias'])); + }); + + test('SshnpArg.negatable test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', negatable: false); + expect(sshnpArg.negatable, isFalse); + }); + + test('SshnpArg.hide test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name', hide: true); + expect(sshnpArg.hide, isTrue); + }); + + test('SshnpArg default values test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name'); + expect(sshnpArg.abbr, isNull); + expect(sshnpArg.help, isNull); + expect(sshnpArg.mandatory, isFalse); + expect(sshnpArg.format, equals(ArgFormat.option)); + expect(sshnpArg.defaultsTo, isNull); + expect(sshnpArg.type, equals(ArgType.string)); + expect(sshnpArg.allowed, isNull); + expect(sshnpArg.parseWhen, equals(ParseWhen.always)); + expect(sshnpArg.aliases, isNull); + expect(sshnpArg.negatable, isTrue); + expect(sshnpArg.hide, isFalse); + }); + }); + + group('SshnpArg getters', () { + test('SshnpArg.bashName test', () { + SshnpArg sshnpArg = SshnpArg(name: 'name'); + expect(sshnpArg.bashName, equals('NAME')); + }); + + test('SshnpArg.alistList test', () { + SshnpArg sshnpArg = + SshnpArg(name: 'name', aliases: ['alias'], abbr: 'a'); + expect(sshnpArg.aliasList, equals(['--name', '--alias', '-a'])); + }); + }); + + group('SshnpArg factory', () { + test('SshnpArg.noArg test', () { + SshnpArg sshnpArg = SshnpArg.noArg(); + expect(sshnpArg.name, equals('')); + }); + + test('SshnpArg.fromName test', () { + SshnpArg sshnpArg = SshnpArg.fromName(SshnpArg.fromArg.name); + expect(sshnpArg.name, equals(SshnpArg.fromArg.name)); + }); + + test('SshnpArg.fromBashName test', () { + SshnpArg sshnpArg = SshnpArg.fromBashName(SshnpArg.fromArg.bashName); + expect(sshnpArg.name, equals(SshnpArg.fromArg.name)); + }); + + test('SshnpArg.fromName no match test', () { + SshnpArg sshnpArg = SshnpArg.fromName('no match'); + expect(sshnpArg.name, equals('')); + }); + + test('SshnpArg.fromBashName no match test', () { + SshnpArg sshnpArg = SshnpArg.fromBashName('no match'); + expect(sshnpArg.name, equals('')); + }); + }); + }); +} diff --git a/packages/noports_core/test/sshnp/sshnp_params/sshnp_params_test.dart b/packages/noports_core/test/sshnp/sshnp_params/sshnp_params_test.dart new file mode 100644 index 000000000..14d2023e4 --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_params/sshnp_params_test.dart @@ -0,0 +1,1033 @@ +import 'package:noports_core/sshnp_params.dart'; +import 'package:noports_core/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('SshnpParams', () { + test('public API test', () { + final params = SshnpParams(clientAtSign: '', sshnpdAtSign: '', host: ''); + expect(params, isNotNull); + expect(params.clientAtSign, isA()); + expect(params.sshnpdAtSign, isA()); + expect(params.host, isA()); + expect(params.device, isA()); + expect(params.port, isA()); + expect(params.localPort, isA()); + expect(params.identityFile, isA()); + expect(params.identityPassphrase, isA()); + expect(params.sendSshPublicKey, isA()); + expect(params.localSshOptions, isA>()); + expect(params.remoteUsername, isA()); + expect(params.verbose, isA()); + expect(params.rootDomain, isA()); + expect(params.localSshdPort, isA()); + expect(params.legacyDaemon, isA()); + expect(params.remoteSshdPort, isA()); + expect(params.idleTimeout, isA()); + expect(params.addForwardsToTunnel, isA()); + expect(params.atKeysFilePath, isA()); + expect(params.sshClient, isA()); + expect(params.sshAlgorithm, isA()); + expect(params.profileName, isA()); + expect(params.listDevices, isA()); + expect(params.toConfigLines(), isA>()); + expect(params.toArgMap(), isA>()); + expect(params.toJson(), isA()); + }); + + group('SshnpParams final variables', () { + test('SshnpParams.clientAtSign test', () { + final params = SshnpParams( + clientAtSign: '@myClientAtSign', sshnpdAtSign: '', host: ''); + expect(params.clientAtSign, equals('@myClientAtSign')); + }); + test('SshnpParams.sshnpdAtSign test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '@mySshnpdAtSign', host: ''); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + }); + test('SshnpParams.host test', () { + final params = + SshnpParams(clientAtSign: '', sshnpdAtSign: '', host: '@myHost'); + expect(params.host, equals('@myHost')); + }); + test('SshnpParams.device test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + device: 'myDeviceName'); + expect(params.device, equals('myDeviceName')); + }); + test('SshnpParams.port test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', port: 1234); + expect(params.port, equals(1234)); + }); + test('SshnpParams.localPort test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', localPort: 2345); + expect(params.localPort, equals(2345)); + }); + test('SshnpParams.identityFile test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + identityFile: '.ssh/id_ed25519'); + expect(params.identityFile, equals('.ssh/id_ed25519')); + }); + test('SshnpParams.identityPassphrase test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + identityPassphrase: 'myPassphrase'); + expect(params.identityPassphrase, equals('myPassphrase')); + }); + test('SshnpParams.sendSshPublicKey test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + sendSshPublicKey: true); + expect(params.sendSshPublicKey, equals(true)); + }); + test('SshnpParams.localSshOptions test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80']); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + }); + test('SshnpParams.remoteUsername test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + remoteUsername: 'myUsername'); + expect(params.remoteUsername, equals('myUsername')); + }); + test('SshnpParams.verbose test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', verbose: true); + expect(params.verbose, equals(true)); + }); + test('SshnpParams.rootDomain test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + rootDomain: 'root.atsign.wtf'); + expect(params.rootDomain, equals('root.atsign.wtf')); + }); + test('SshnpParams.localSshdPort test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', localSshdPort: 4567); + expect(params.localSshdPort, equals(4567)); + }); + test('SshnpParams.legacyDaemon test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', legacyDaemon: true); + expect(params.legacyDaemon, equals(true)); + }); + test('SshnpParams.remoteSshdPort test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', remoteSshdPort: 2222); + expect(params.remoteSshdPort, equals(2222)); + }); + test('SshnpParams.idleTimeout test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', idleTimeout: 120); + expect(params.idleTimeout, equals(120)); + }); + test('SshnpParams.addForwardsToTunnel test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + addForwardsToTunnel: true); + expect(params.addForwardsToTunnel, equals(true)); + }); + test('SshnpParams.atKeysFilePath test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys'); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + }); + test('SshnpParams.sshClient test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + sshClient: SupportedSshClient.dart); + expect(params.sshClient, equals(SupportedSshClient.dart)); + }); + test('SshnpParams.sshAlgorithm test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + sshAlgorithm: SupportedSshAlgorithm.rsa); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpParams.profileName test', () { + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + host: '', + profileName: 'myProfile'); + expect(params.profileName, equals('myProfile')); + }); + test('SshnpParams.listDevices test', () { + final params = SshnpParams( + clientAtSign: '', sshnpdAtSign: '', host: '', listDevices: true); + expect(params.listDevices, equals(true)); + }); + }); // group('SshnpParams final variables') + + group('SshnpParams factories', () { + test('SshnpParams.empty() test', () { + final params = SshnpParams.empty(); + expect(params.profileName, equals('')); + expect(params.clientAtSign, equals('')); + expect(params.sshnpdAtSign, equals('')); + expect(params.host, equals('')); + expect(params.device, equals(DefaultSshnpArgs.device)); + expect(params.port, equals(DefaultSshnpArgs.port)); + expect(params.localPort, equals(DefaultSshnpArgs.localPort)); + expect(params.identityFile, isNull); + expect(params.identityPassphrase, isNull); + expect( + params.sendSshPublicKey, equals(DefaultSshnpArgs.sendSshPublicKey)); + expect( + params.localSshOptions, equals(DefaultSshnpArgs.localSshOptions)); + expect(params.verbose, equals(DefaultArgs.verbose)); + expect(params.remoteUsername, isNull); + expect(params.atKeysFilePath, isNull); + expect(params.rootDomain, equals(DefaultArgs.rootDomain)); + expect(params.localSshdPort, equals(DefaultArgs.localSshdPort)); + expect(params.legacyDaemon, equals(DefaultSshnpArgs.legacyDaemon)); + expect(params.listDevices, equals(DefaultSshnpArgs.listDevices)); + expect(params.remoteSshdPort, equals(DefaultArgs.remoteSshdPort)); + expect(params.idleTimeout, equals(DefaultArgs.idleTimeout)); + expect(params.addForwardsToTunnel, + equals(DefaultArgs.addForwardsToTunnel)); + expect(params.sshClient, equals(DefaultSshnpArgs.sshClient)); + expect(params.sshAlgorithm, equals(DefaultArgs.sshAlgorithm)); + }); + test('SshnpParams.merge() test (overrides take priority)', () { + final params = SshnpParams.merge( + SshnpParams.empty(), + SshnpPartialParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + legacyDaemon: true, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ), + ); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpParams.merge() test (null coalesce values)', () { + final params = + SshnpParams.merge(SshnpParams.empty(), SshnpPartialParams()); + expect(params.profileName, equals('')); + expect(params.clientAtSign, equals('')); + expect(params.sshnpdAtSign, equals('')); + expect(params.host, equals('')); + expect(params.device, equals(DefaultSshnpArgs.device)); + expect(params.port, equals(DefaultSshnpArgs.port)); + expect(params.localPort, equals(DefaultSshnpArgs.localPort)); + expect(params.identityFile, isNull); + expect(params.identityPassphrase, isNull); + expect( + params.sendSshPublicKey, equals(DefaultSshnpArgs.sendSshPublicKey)); + expect( + params.localSshOptions, equals(DefaultSshnpArgs.localSshOptions)); + expect(params.verbose, equals(DefaultArgs.verbose)); + expect(params.remoteUsername, isNull); + expect(params.atKeysFilePath, isNull); + expect(params.rootDomain, equals(DefaultArgs.rootDomain)); + expect(params.localSshdPort, equals(DefaultArgs.localSshdPort)); + expect(params.legacyDaemon, equals(DefaultSshnpArgs.legacyDaemon)); + expect(params.listDevices, equals(DefaultSshnpArgs.listDevices)); + expect(params.remoteSshdPort, equals(DefaultArgs.remoteSshdPort)); + expect(params.idleTimeout, equals(DefaultArgs.idleTimeout)); + expect(params.addForwardsToTunnel, + equals(DefaultArgs.addForwardsToTunnel)); + expect(params.sshClient, equals(DefaultSshnpArgs.sshClient)); + expect(params.sshAlgorithm, equals(DefaultArgs.sshAlgorithm)); + }); + test('SshnpParams.fromJson() test', () { + String json = '{' + '"${SshnpArg.profileNameArg.name}": "myProfile",' + '"${SshnpArg.fromArg.name}": "@myClientAtSign",' + '"${SshnpArg.toArg.name}": "@mySshnpdAtSign",' + '"${SshnpArg.hostArg.name}": "@myHost",' + '"${SshnpArg.deviceArg.name}": "myDeviceName",' + '"${SshnpArg.portArg.name}": 1234,' + '"${SshnpArg.localPortArg.name}": 2345,' + '"${SshnpArg.identityFileArg.name}": ".ssh/id_ed25519",' + '"${SshnpArg.identityPassphraseArg.name}": "myPassphrase",' + '"${SshnpArg.sendSshPublicKeyArg.name}": true,' + '"${SshnpArg.localSshOptionsArg.name}": ["-L 127.0.01:8080:127.0.0.1:80"],' + '"${SshnpArg.remoteUserNameArg.name}": "myUsername",' + '"${SshnpArg.verboseArg.name}": true,' + '"${SshnpArg.rootDomainArg.name}": "root.atsign.wtf",' + '"${SshnpArg.localSshdPortArg.name}": 4567,' + '"${SshnpArg.legacyDaemonArg.name}": true,' + '"${SshnpArg.remoteSshdPortArg.name}": 2222,' + '"${SshnpArg.idleTimeoutArg.name}": 120,' + '"${SshnpArg.addForwardsToTunnelArg.name}": true,' + '"${SshnpArg.keyFileArg.name}": "~/.atsign/@myAtsign_keys.atKeys",' + '"${SshnpArg.sshClientArg.name}": "${SupportedSshClient.dart.toString()}",' + '"${SshnpArg.sshAlgorithmArg.name}": "${SupportedSshAlgorithm.rsa.toString()}"' + '}'; + + final params = SshnpParams.fromJson(json); + expect(params.profileName, equals('myProfile')); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpParams.fromPartial() test', () { + final partial = SshnpPartialParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + ); + final params = SshnpParams.fromPartial(partial); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + }); + test('SshnpParams.fromConfigLines() test', () { + final configLines = [ + '${SshnpArg.fromArg.bashName} = @myClientAtSign', + '${SshnpArg.toArg.bashName} = @mySshnpdAtSign', + '${SshnpArg.hostArg.bashName} = @myHost', + '${SshnpArg.deviceArg.bashName} = myDeviceName', + '${SshnpArg.portArg.bashName} = 1234', + '${SshnpArg.localPortArg.bashName} = 2345', + '${SshnpArg.identityFileArg.bashName} = .ssh/id_ed25519', + '${SshnpArg.identityPassphraseArg.bashName} = myPassphrase', + '${SshnpArg.sendSshPublicKeyArg.bashName} = true', + '${SshnpArg.localSshOptionsArg.bashName} = -L 127.0.01:8080:127.0.0.1:80', + '${SshnpArg.remoteUserNameArg.bashName} = myUsername', + '${SshnpArg.verboseArg.bashName} = true', + '${SshnpArg.rootDomainArg.bashName} = root.atsign.wtf', + '${SshnpArg.localSshdPortArg.bashName} = 4567', + '${SshnpArg.legacyDaemonArg.bashName} = true', + '${SshnpArg.remoteSshdPortArg.bashName} = 2222', + '${SshnpArg.idleTimeoutArg.bashName} = 120', + '${SshnpArg.addForwardsToTunnelArg.bashName} = true', + '${SshnpArg.keyFileArg.bashName} = ~/.atsign/@myAtsign_keys.atKeys', + '${SshnpArg.sshClientArg.bashName} = ${SupportedSshClient.dart.toString()}', + '${SshnpArg.sshAlgorithmArg.bashName} = ${SupportedSshAlgorithm.rsa.toString()}', + ]; + final params = SshnpParams.fromConfigLines('myProfile', configLines); + expect(params.profileName, equals('myProfile')); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + }); + }); // group('SshnpParams factories') + group('SshnpParams functions', () { + test('SshnpParams.toConfigLines', () { + final params = SshnpParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ); + final configLines = params.toConfigLines(); + // Since exact formatting is in question, + // it is safer to trust that the parser works as expected + // and just check that the lines are present + final parsedParams = + SshnpParams.fromConfigLines('myProfile', configLines); + expect(parsedParams.profileName, equals('myProfile')); + expect(parsedParams.clientAtSign, equals('@myClientAtSign')); + expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(parsedParams.host, equals('@myHost')); + expect(parsedParams.device, equals('myDeviceName')); + expect(parsedParams.port, equals(1234)); + expect(parsedParams.localPort, equals(2345)); + expect(parsedParams.sendSshPublicKey, equals(true)); + expect(parsedParams.localSshOptions, + equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(parsedParams.remoteUsername, equals('myUsername')); + expect(parsedParams.verbose, equals(true)); + expect(parsedParams.rootDomain, equals('root.atsign.wtf')); + expect(parsedParams.localSshdPort, equals(4567)); + expect(parsedParams.remoteSshdPort, equals(2222)); + }); + test('SshnpParams.toArgMap', () { + final params = SshnpParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ); + final argMap = params.toArgMap(); + expect(argMap[SshnpArg.fromArg.name], equals('@myClientAtSign')); + expect(argMap[SshnpArg.toArg.name], equals('@mySshnpdAtSign')); + expect(argMap[SshnpArg.hostArg.name], equals('@myHost')); + expect(argMap[SshnpArg.deviceArg.name], equals('myDeviceName')); + expect(argMap[SshnpArg.portArg.name], equals(1234)); + expect(argMap[SshnpArg.localPortArg.name], equals(2345)); + expect( + argMap[SshnpArg.identityFileArg.name], equals('.ssh/id_ed25519')); + expect(argMap[SshnpArg.identityPassphraseArg.name], + equals('myPassphrase')); + expect(argMap[SshnpArg.sendSshPublicKeyArg.name], equals(true)); + expect(argMap[SshnpArg.localSshOptionsArg.name], + equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(argMap[SshnpArg.remoteUserNameArg.name], equals('myUsername')); + expect(argMap[SshnpArg.verboseArg.name], equals(true)); + expect(argMap[SshnpArg.rootDomainArg.name], equals('root.atsign.wtf')); + expect(argMap[SshnpArg.localSshdPortArg.name], equals(4567)); + expect(argMap[SshnpArg.remoteSshdPortArg.name], equals(2222)); + expect(argMap[SshnpArg.idleTimeoutArg.name], equals(120)); + expect(argMap[SshnpArg.addForwardsToTunnelArg.name], equals(true)); + expect(argMap[SshnpArg.keyFileArg.name], + equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(argMap[SshnpArg.sshClientArg.name], + equals(SupportedSshClient.dart.toString())); + expect(argMap[SshnpArg.sshAlgorithmArg.name], + equals(SupportedSshAlgorithm.rsa.toString())); + }); + test('SshnpParams.toJson', () { + final params = SshnpParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ); + final json = params.toJson(); + final parsedParams = SshnpParams.fromJson(json); + expect(parsedParams.clientAtSign, equals('@myClientAtSign')); + expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(parsedParams.host, equals('@myHost')); + expect(parsedParams.device, equals('myDeviceName')); + expect(parsedParams.port, equals(1234)); + expect(parsedParams.localPort, equals(2345)); + expect(parsedParams.identityFile, equals('.ssh/id_ed25519')); + expect(parsedParams.identityPassphrase, equals('myPassphrase')); + expect(parsedParams.sendSshPublicKey, equals(true)); + expect(parsedParams.localSshOptions, + equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(parsedParams.remoteUsername, equals('myUsername')); + expect(parsedParams.verbose, equals(true)); + expect(parsedParams.rootDomain, equals('root.atsign.wtf')); + expect(parsedParams.localSshdPort, equals(4567)); + expect(parsedParams.remoteSshdPort, equals(2222)); + expect(parsedParams.idleTimeout, equals(120)); + expect(parsedParams.addForwardsToTunnel, equals(true)); + expect(parsedParams.atKeysFilePath, + equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(parsedParams.sshClient, equals(SupportedSshClient.dart)); + expect(parsedParams.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + }); // group('SshnpParams functions') + }); // group('SshnpParams') + + group('SshnpPartialParams', () { + test('public API test', () { + final partialParams = SshnpPartialParams(); + expect(partialParams, isNotNull); + expect(partialParams.clientAtSign, isA()); + expect(partialParams.sshnpdAtSign, isA()); + expect(partialParams.host, isA()); + expect(partialParams.device, isA()); + expect(partialParams.port, isA()); + expect(partialParams.localPort, isA()); + expect(partialParams.identityFile, isA()); + expect(partialParams.identityPassphrase, isA()); + expect(partialParams.sendSshPublicKey, isA()); + expect(partialParams.localSshOptions, isA?>()); + expect(partialParams.remoteUsername, isA()); + expect(partialParams.verbose, isA()); + expect(partialParams.rootDomain, isA()); + expect(partialParams.localSshdPort, isA()); + expect(partialParams.legacyDaemon, isA()); + expect(partialParams.remoteSshdPort, isA()); + expect(partialParams.idleTimeout, isA()); + expect(partialParams.addForwardsToTunnel, isA()); + expect(partialParams.atKeysFilePath, isA()); + expect(partialParams.sshClient, isA()); + expect(partialParams.sshAlgorithm, isA()); + expect(partialParams.profileName, isA()); + expect(partialParams.listDevices, isA()); + }); + + group('SshnpPartialParams final variables', () { + test('SshnpPartialParams.clientAtSign test', () { + final params = SshnpPartialParams(clientAtSign: '@myClientAtSign'); + expect(params.clientAtSign, equals('@myClientAtSign')); + }); + test('SshnpPartialParams.sshnpdAtSign test', () { + final params = SshnpPartialParams(sshnpdAtSign: '@mySshnpdAtSign'); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + }); + test('SshnpPartialParams.host test', () { + final params = SshnpPartialParams(host: '@myHost'); + expect(params.host, equals('@myHost')); + }); + test('SshnpPartialParams.device test', () { + final params = SshnpPartialParams(device: 'myDeviceName'); + expect(params.device, equals('myDeviceName')); + }); + test('SshnpPartialParams.port test', () { + final params = SshnpPartialParams(port: 1234); + expect(params.port, equals(1234)); + }); + test('SshnpPartialParams.localPort test', () { + final params = SshnpPartialParams(localPort: 2345); + expect(params.localPort, equals(2345)); + }); + test('SshnpPartialParams.identityFile test', () { + final params = SshnpPartialParams(identityFile: '.ssh/id_ed25519'); + expect(params.identityFile, equals('.ssh/id_ed25519')); + }); + test('SshnpPartialParams.identityPassphrase test', () { + final params = SshnpPartialParams(identityPassphrase: 'myPassphrase'); + expect(params.identityPassphrase, equals('myPassphrase')); + }); + test('SshnpPartialParams.sendSshPublicKey test', () { + final params = SshnpPartialParams(sendSshPublicKey: true); + expect(params.sendSshPublicKey, equals(true)); + }); + test('SshnpPartialParams.localSshOptions test', () { + final params = SshnpPartialParams( + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80']); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + }); + test('SshnpPartialParams.remoteUsername test', () { + final params = SshnpPartialParams(remoteUsername: 'myUsername'); + expect(params.remoteUsername, equals('myUsername')); + }); + test('SshnpPartialParams.verbose test', () { + final params = SshnpPartialParams(verbose: true); + expect(params.verbose, equals(true)); + }); + test('SshnpPartialParams.rootDomain test', () { + final params = SshnpPartialParams(rootDomain: 'root.atsign.wtf'); + expect(params.rootDomain, equals('root.atsign.wtf')); + }); + test('SshnpPartialParams.localSshdPort test', () { + final params = SshnpPartialParams(localSshdPort: 4567); + expect(params.localSshdPort, equals(4567)); + }); + test('SshnpPartialParams.legacyDaemon test', () { + final params = SshnpPartialParams(legacyDaemon: true); + expect(params.legacyDaemon, equals(true)); + }); + test('SshnpPartialParams.remoteSshdPort test', () { + final params = SshnpPartialParams(remoteSshdPort: 2222); + expect(params.remoteSshdPort, equals(2222)); + }); + test('SshnpPartialParams.idleTimeout test', () { + final params = SshnpPartialParams(idleTimeout: 120); + expect(params.idleTimeout, equals(120)); + }); + test('SshnpPartialParams.addForwardsToTunnel test', () { + final params = SshnpPartialParams(addForwardsToTunnel: true); + expect(params.addForwardsToTunnel, equals(true)); + }); + test('SshnpPartialParams.atKeysFilePath test', () { + final params = SshnpPartialParams( + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys'); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + }); + test('SshnpPartialParams.sshClient test', () { + final params = SshnpPartialParams(sshClient: SupportedSshClient.dart); + expect(params.sshClient, equals(SupportedSshClient.dart)); + }); + test('SshnpPartialParams.sshAlgorithm test', () { + final params = + SshnpPartialParams(sshAlgorithm: SupportedSshAlgorithm.rsa); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpPartialParams.profileName test', () { + final params = SshnpPartialParams(profileName: 'myProfile'); + expect(params.profileName, equals('myProfile')); + }); + test('SshnpPartialParams.listDevices test', () { + final params = SshnpPartialParams(listDevices: true); + expect(params.listDevices, equals(true)); + }); + }); // group('SshnpPartialParams final variables') + group('SshnpPartialParams factories', () { + test('SshnpPartialParams.empty() test', () { + final params = SshnpPartialParams.empty(); + expect(params.profileName, isNull); + expect(params.clientAtSign, isNull); + expect(params.sshnpdAtSign, isNull); + expect(params.host, isNull); + expect(params.device, isNull); + expect(params.port, isNull); + expect(params.localPort, isNull); + expect(params.identityFile, isNull); + expect(params.identityPassphrase, isNull); + expect(params.sendSshPublicKey, isNull); + expect(params.localSshOptions, isNull); + expect(params.verbose, isNull); + expect(params.remoteUsername, isNull); + expect(params.rootDomain, isNull); + expect(params.localSshdPort, isNull); + expect(params.legacyDaemon, isNull); + expect(params.remoteSshdPort, isNull); + expect(params.idleTimeout, isNull); + expect(params.addForwardsToTunnel, isNull); + expect(params.atKeysFilePath, isNull); + expect(params.sshClient, isNull); + expect(params.sshAlgorithm, isNull); + expect(params.listDevices, isNull); + }); + test('SshnpPartialParams.merge() test (overrides take priority)', () { + final params = SshnpPartialParams.merge( + SshnpPartialParams.empty(), + SshnpPartialParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ), + ); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpPartialParams.merge() test (null coalesce values)', () { + final params = SshnpPartialParams.merge( + SshnpPartialParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ), + SshnpPartialParams.empty(), + ); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + // TODO write tests for SshnpPartialParams.fromFile() + test('SshnpPartial.fromConfigLines() test', () { + final params = SshnpParams( + clientAtSign: '@myClientAtSign', + sshnpdAtSign: '@mySshnpdAtSign', + host: '@myHost', + device: 'myDeviceName', + port: 1234, + localPort: 2345, + identityFile: '.ssh/id_ed25519', + identityPassphrase: 'myPassphrase', + sendSshPublicKey: true, + localSshOptions: ['-L 127.0.01:8080:127.0.0.1:80'], + remoteUsername: 'myUsername', + verbose: true, + rootDomain: 'root.atsign.wtf', + localSshdPort: 4567, + remoteSshdPort: 2222, + idleTimeout: 120, + addForwardsToTunnel: true, + atKeysFilePath: '~/.atsign/@myAtsign_keys.atKeys', + sshClient: SupportedSshClient.dart, + sshAlgorithm: SupportedSshAlgorithm.rsa, + ); + final configLines = params.toConfigLines(); + // Since exact formatting is in question, + // it is safer to trust that the parser works as expected + // and just check that the lines are present + final parsedParams = + SshnpPartialParams.fromConfigLines('myProfile', configLines); + expect(parsedParams.profileName, equals('myProfile')); + expect(parsedParams.clientAtSign, equals('@myClientAtSign')); + expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(parsedParams.host, equals('@myHost')); + expect(parsedParams.device, equals('myDeviceName')); + expect(parsedParams.port, equals(1234)); + expect(parsedParams.localPort, equals(2345)); + expect(parsedParams.sendSshPublicKey, equals(true)); + expect(parsedParams.localSshOptions, + equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(parsedParams.remoteUsername, equals('myUsername')); + expect(parsedParams.verbose, equals(true)); + expect(parsedParams.rootDomain, equals('root.atsign.wtf')); + expect(parsedParams.localSshdPort, equals(4567)); + expect(parsedParams.remoteSshdPort, equals(2222)); + }); + test('SshnpPartialParams.fromJson() test', () { + String json = '{' + '"${SshnpArg.profileNameArg.name}": "myProfile",' + '"${SshnpArg.fromArg.name}": "@myClientAtSign",' + '"${SshnpArg.toArg.name}": "@mySshnpdAtSign",' + '"${SshnpArg.hostArg.name}": "@myHost",' + '"${SshnpArg.deviceArg.name}": "myDeviceName",' + '"${SshnpArg.portArg.name}": 1234,' + '"${SshnpArg.localPortArg.name}": 2345,' + '"${SshnpArg.identityFileArg.name}": ".ssh/id_ed25519",' + '"${SshnpArg.identityPassphraseArg.name}": "myPassphrase",' + '"${SshnpArg.sendSshPublicKeyArg.name}": true,' + '"${SshnpArg.localSshOptionsArg.name}": ["-L 127.0.01:8080:127.0.0.1:80"],' + '"${SshnpArg.remoteUserNameArg.name}": "myUsername",' + '"${SshnpArg.verboseArg.name}": true,' + '"${SshnpArg.rootDomainArg.name}": "root.atsign.wtf",' + '"${SshnpArg.localSshdPortArg.name}": 4567,' + '"${SshnpArg.legacyDaemonArg.name}": true,' + '"${SshnpArg.remoteSshdPortArg.name}": 2222,' + '"${SshnpArg.idleTimeoutArg.name}": 120,' + '"${SshnpArg.addForwardsToTunnelArg.name}": true,' + '"${SshnpArg.keyFileArg.name}": "~/.atsign/@myAtsign_keys.atKeys",' + '"${SshnpArg.sshClientArg.name}": "${SupportedSshClient.dart.toString()}",' + '"${SshnpArg.sshAlgorithmArg.name}": "${SupportedSshAlgorithm.rsa.toString()}"' + '}'; + + final params = SshnpPartialParams.fromJson(json); + expect(params.profileName, equals('myProfile')); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpPartialParams.fromArgMap() test', () { + final params = SshnpPartialParams.fromArgMap({ + SshnpArg.profileNameArg.name: 'myProfile', + SshnpArg.fromArg.name: '@myClientAtSign', + SshnpArg.toArg.name: '@mySshnpdAtSign', + SshnpArg.hostArg.name: '@myHost', + SshnpArg.deviceArg.name: 'myDeviceName', + SshnpArg.portArg.name: 1234, + SshnpArg.localPortArg.name: 2345, + SshnpArg.identityFileArg.name: '.ssh/id_ed25519', + SshnpArg.identityPassphraseArg.name: 'myPassphrase', + SshnpArg.sendSshPublicKeyArg.name: true, + SshnpArg.localSshOptionsArg.name: ['-L 127.0.01:8080:127.0.0.1:80'], + SshnpArg.remoteUserNameArg.name: 'myUsername', + SshnpArg.verboseArg.name: true, + SshnpArg.rootDomainArg.name: 'root.atsign.wtf', + SshnpArg.localSshdPortArg.name: 4567, + SshnpArg.legacyDaemonArg.name: true, + SshnpArg.remoteSshdPortArg.name: 2222, + SshnpArg.idleTimeoutArg.name: 120, + SshnpArg.addForwardsToTunnelArg.name: true, + SshnpArg.keyFileArg.name: '~/.atsign/@myAtsign_keys.atKeys', + SshnpArg.sshClientArg.name: SupportedSshClient.dart.toString(), + SshnpArg.sshAlgorithmArg.name: SupportedSshAlgorithm.rsa.toString(), + }); + expect(params.profileName, equals('myProfile')); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect( + params.atKeysFilePath, equals('~/.atsign/@myAtsign_keys.atKeys')); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + test('SshnpPartialParams.fromArgList() test', () { + final argList = [ + '--${SshnpArg.profileNameArg.name}', + 'myProfile', + '--${SshnpArg.fromArg.name}', + '@myClientAtSign', + '--${SshnpArg.toArg.name}', + '@mySshnpdAtSign', + '--${SshnpArg.hostArg.name}', + '@myHost', + '--${SshnpArg.deviceArg.name}', + 'myDeviceName', + '--${SshnpArg.portArg.name}', + '1234', + '--${SshnpArg.localPortArg.name}', + '2345', + '--${SshnpArg.identityFileArg.name}', + '.ssh/id_ed25519', + '--${SshnpArg.identityPassphraseArg.name}', + 'myPassphrase', + '--${SshnpArg.sendSshPublicKeyArg.name}', + 'true', + '--${SshnpArg.localSshOptionsArg.name}', + '-L 127.0.01:8080:127.0.0.1:80', + '--${SshnpArg.remoteUserNameArg.name}', + 'myUsername', + '--${SshnpArg.verboseArg.name}', + 'true', + '--${SshnpArg.rootDomainArg.name}', + 'root.atsign.wtf', + '--${SshnpArg.localSshdPortArg.name}', + '4567', + '--${SshnpArg.legacyDaemonArg.name}', + 'true', + '--${SshnpArg.remoteSshdPortArg.name}', + '2222', + '--${SshnpArg.idleTimeoutArg.name}', + '120', + '--${SshnpArg.addForwardsToTunnelArg.name}', + 'true', + '--${SshnpArg.keyFileArg.name}', + '~/.atsign/@myAtsign_keys.atKeys', + '--${SshnpArg.sshClientArg.name}', + SupportedSshClient.dart.toString(), + '--${SshnpArg.sshAlgorithmArg.name}', + SupportedSshAlgorithm.rsa.toString(), + ]; + final params = SshnpPartialParams.fromArgList(argList); + expect(params.profileName, equals('myProfile')); + expect(params.clientAtSign, equals('@myClientAtSign')); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.host, equals('@myHost')); + expect(params.device, equals('myDeviceName')); + expect(params.port, equals(1234)); + expect(params.localPort, equals(2345)); + expect(params.identityFile, equals('.ssh/id_ed25519')); + expect(params.identityPassphrase, equals('myPassphrase')); + expect(params.sendSshPublicKey, equals(true)); + expect( + params.localSshOptions, equals(['-L 127.0.01:8080:127.0.0.1:80'])); + expect(params.remoteUsername, equals('myUsername')); + expect(params.verbose, equals(true)); + expect(params.rootDomain, equals('root.atsign.wtf')); + expect(params.localSshdPort, equals(4567)); + expect(params.legacyDaemon, equals(true)); + expect(params.remoteSshdPort, equals(2222)); + expect(params.idleTimeout, equals(120)); + expect(params.addForwardsToTunnel, equals(true)); + expect(params.sshClient, equals(SupportedSshClient.dart)); + expect(params.sshAlgorithm, equals(SupportedSshAlgorithm.rsa)); + }); + }); // group('SshnpPartialParams factories') + }); // group('SshnpPartialParams') +} diff --git a/packages/noports_core/test/sshnp/sshnp_result_test.dart b/packages/noports_core/test/sshnp/sshnp_result_test.dart new file mode 100644 index 000000000..599e30a48 --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_result_test.dart @@ -0,0 +1,169 @@ +import 'dart:io'; + +import 'package:mocktail/mocktail.dart'; +import 'package:noports_core/sshnp.dart'; +import 'package:socket_connector/socket_connector.dart'; +import 'package:test/test.dart'; + +class MockProcess extends Mock implements Process {} + +class MockSocketConnector extends Mock implements SocketConnector {} + +void main() { + group('SshnpResult', () { + group('Subclass Confirmation', () { + test('SshnpSuccess test', () { + expect(SshnpSuccess(), isA()); + }); + test('SshnpCommand test', () { + final res = SshnpCommand(host: 'localhost', localPort: 22); + expect(res, isA()); + }); + test('SshnpNoOpSuccess test', () { + final res = SshnpNoOpSuccess(); + expect(res, isA()); + expect(res, isA()); + }); + test('SshnpFailure test', () { + expect(SshnpFailure(), isA()); + }); + test('SshnpError test', () { + final res = SshnpError('error message'); + expect(res, isA()); + expect(res, isA()); + }); + }); // group('Subclass Confirmation') + group('SshnpError', () { + late StackTrace stackTrace; + late SshnpError error; + setUp(() { + stackTrace = StackTrace.current; + error = + SshnpError('myMessage', error: 'myError', stackTrace: stackTrace); + }); + test('SshnpError.toString() test', () { + expect(error.toString(), equals('myMessage')); + }); + test('SshnpError.error test', () { + expect(error.error, equals('myError')); + }); + test('SshnpError.stackTrace test', () { + expect(error.stackTrace, equals(stackTrace)); + }); + }); // group('SshnpError') + group('SshnpCommand', () { + test('SshnpCommand.toString() test', () { + final command = SshnpCommand( + localPort: 22, + host: 'localhost', + remoteUsername: 'myUsername', + localSshOptions: ['-L 127.0.0.1:8080:127.0.0.1:80'], + privateKeyFileName: '~/.ssh/myPrivateKeyFile', + ); + expect( + command.toString(), + equals( + 'ssh -p 22 ${optionsWithPrivateKey.join(' ')} ' + '-L 127.0.0.1:8080:127.0.0.1:80 ' + 'myUsername@localhost ' + '-i ~/.ssh/myPrivateKeyFile', + ), + ); + }); + test('SshnpCommand.connectionBean test', () { + SshnpCommand command = SshnpCommand( + host: 'localhost', + localPort: 22, + connectionBean: 'myBean', + ); + expect(command.connectionBean, equals('myBean')); + }); + test('static SshnpCommand.shouldIncludePrivateKey test', () { + expect(SshnpCommand.shouldIncludePrivateKey(null), isFalse); + expect(SshnpCommand.shouldIncludePrivateKey(''), isFalse); + // it is not the responsibility of this class to validate whether the private key file name is valid + // it purely wants to know whether there is a value or not + expect(SshnpCommand.shouldIncludePrivateKey('asdfkjsdflkjd'), isTrue); + }); + test('SshnpCommand.args test', () { + final command = SshnpCommand( + localPort: 22, + host: 'localhost', + remoteUsername: 'myUsername', + localSshOptions: ['-L 127.0.0.1:8080:127.0.0.1:80'], + privateKeyFileName: '~/.ssh/myPrivateKeyFile', + ); + expect( + command.args, + equals([ + '-p 22', + ...optionsWithPrivateKey, + '-L 127.0.0.1:8080:127.0.0.1:80', + 'myUsername@localhost', + '-i', + '~/.ssh/myPrivateKeyFile', + ]), + ); + }); + }); // group('SshnpCommand') + group('SshnpNoOpSuccess', () { + test('SshnpNoOpSuccess.toString() test', () { + expect(SshnpNoOpSuccess().toString(), equals('Connection Established')); + }); + test('SshnpNoOpSuccess.connectionBean test', () { + SshnpNoOpSuccess success = + SshnpNoOpSuccess(connectionBean: 'myBean'); + expect(success.connectionBean, equals('myBean')); + }); + }); // group('SshnpNoOpSuccess') + }); + group('SshnpConnectionBean', () { + test('SshnpConnectionBean.killConnectionBean() test', () { + final bean = SshnpConnectionBean(); + final process = MockProcess(); + when(() => process.kill()).thenReturn(true); + bean.connectionBean = process; + + verifyNever(() => process.kill()); + expect(bean.killConnectionBean(), completes); + verify(() => process.kill()).called(1); + }); + + test('SshnpConnectionBean>.killConnectionBean() test', + () async { + final bean = SshnpConnectionBean>(); + final process = MockProcess(); + when(() => process.kill()).thenReturn(true); + final fProcess = Future.value(process); + bean.connectionBean = fProcess; + + verifyNever(() => process.kill()); + await expectLater(bean.killConnectionBean(), completes); + verify(() => process.kill()).called(1); + }); + test('SshnpConnectionBean.killConnectionBean() test', () { + final bean = SshnpConnectionBean(); + final socketConnector = MockSocketConnector(); + when(() => socketConnector.close()).thenReturn(null); + bean.connectionBean = socketConnector; + + verifyNever(() => socketConnector.close()); + expect(bean.killConnectionBean(), completes); + verify(() => socketConnector.close()).called(1); + }); + test( + 'SshnpConnectionBean>.killConnectionBean() test', + () async { + final bean = SshnpConnectionBean>(); + final socketConnector = MockSocketConnector(); + final fSocketConnector = Future.value(socketConnector); + when(() => socketConnector.close()).thenReturn(null); + bean.connectionBean = fSocketConnector; + + verifyNever(() => socketConnector.close()); + expect(bean.connectionBean, completes); + await expectLater(bean.killConnectionBean(), completes); + verify(() => socketConnector.close()).called(1); + }); + }); // group('SshnpConnectionBean') +} diff --git a/packages/noports_core/test/sshnp/sshnp_test.dart b/packages/noports_core/test/sshnp/sshnp_test.dart new file mode 100644 index 000000000..97622615a --- /dev/null +++ b/packages/noports_core/test/sshnp/sshnp_test.dart @@ -0,0 +1,7 @@ +import 'package:test/test.dart'; + +void main() { + group('Sshnp', () { + test('public API test', () {}); + }); +} diff --git a/packages/noports_core/test/sshnp_test.dart b/packages/noports_core/test/sshnp_test.dart deleted file mode 100644 index af7195ade..000000000 --- a/packages/noports_core/test/sshnp_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:args/args.dart'; -import 'package:noports_core/sshnp_params.dart'; -import 'package:test/test.dart'; - -void main() { - group('args parser tests', () { - test('test mandatory args', () { - ArgParser parser = - SSHNPArg.createArgParser(parserType: ParserType.commandLine); - // As of version 2.4.2 of the args package, exceptions regarding - // mandatory options are not thrown when the args are parsed, - // but when trying to retrieve a mandatory option. - // See https://pub.dev/packages/args/changelog - - List args = []; - expect(() => parser.parse(args)['from'], throwsA(isA())); - - args.addAll(['-f', '@alice']); - expect(parser.parse(args)['from'], '@alice'); - expect(() => parser.parse(args)['to'], throwsA(isA())); - - args.addAll(['-t', '@bob']); - expect(parser.parse(args)['from'], '@alice'); - expect(parser.parse(args)['to'], '@bob'); - expect(() => parser.parse(args)['host'], throwsA(isA())); - - args.addAll(['-h', 'host.subdomain.test']); - expect(parser.parse(args)['from'], '@alice'); - expect(parser.parse(args)['to'], '@bob'); - expect(parser.parse(args)['host'], 'host.subdomain.test'); - }); - - test('test parsed args with only mandatory provided', () { - // TODO fix these params with new public API - - List args = []; - args.addAll(['-f', '@alice']); - args.addAll(['-t', '@bob']); - args.addAll(['-h', 'host.subdomain.test']); - var p = SSHNPParams.fromPartial(SSHNPPartialParams.fromArgList(args)); - expect(p.clientAtSign, '@alice'); - expect(p.sshnpdAtSign, '@bob'); - expect(p.host, 'host.subdomain.test'); - expect(p.device, 'default'); - expect(p.port, 22); - expect(p.localPort, 0); - expect(p.sendSshPublicKey, ''); - expect(p.localSshOptions, []); - expect(p.sshAlgorithm, SupportedSSHAlgorithm.ed25519); - expect(p.verbose, false); - expect(p.remoteUsername, null); - }); - - test('test parsed args with non-mandatory args provided', () { - List args = []; - args.addAll(['-f', '@alice']); - args.addAll(['-t', '@bob']); - args.addAll(['-h', 'host.subdomain.test']); - - // TODO fix these params with new public API - args.addAll([ - '--device', - 'ancient_pc', - '--port', - '56789', - '--local-port', - '98765', - '--key-file', - '/tmp/temp_keys.json', - '--ssh-public-key', - 'sekrit.pub', - '--local-ssh-options', - '--arg 2 --arg 4 foo bar -x', - '--remote-user-name', - 'gary', - '-v', - '--ssh-algorithm', - 'ssh-rsa' - ]); - var p = SSHNPParams.fromPartial(SSHNPPartialParams.fromArgList(args)); - expect(p.clientAtSign, '@alice'); - expect(p.sshnpdAtSign, '@bob'); - expect(p.host, 'host.subdomain.test'); - - expect(p.device, 'ancient_pc'); - expect(p.port, 56789); - expect(p.localPort, 98765); - expect(p.atKeysFilePath, '/tmp/temp_keys.json'); - expect(p.sendSshPublicKey, 'sekrit.pub'); - expect(p.localSshOptions, ['--arg 2 --arg 4 foo bar -x']); - expect(p.sshAlgorithm, SupportedSSHAlgorithm.rsa); - expect(p.verbose, true); - expect(p.remoteUsername, 'gary'); - }); - }); -} diff --git a/packages/noports_core/test/sshnpd_test.dart b/packages/noports_core/test/sshnpd_test.dart deleted file mode 100644 index ddd21d381..000000000 --- a/packages/noports_core/test/sshnpd_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:noports_core/src/common/types.dart'; -import 'package:noports_core/sshnpd.dart'; -import 'package:noports_core/utils.dart'; -import 'package:test/test.dart'; -import 'package:args/args.dart'; - -void main() { - group('args parser test', () { - // TODO fix these params with new public API - - test('test mandatory args', () { - ArgParser parser = SSHNPDParams.parser; - - List args = []; - expect(() => parser.parse(args)['atsign'], throwsA(isA())); - - args.addAll(['-a', '@bob']); - expect(parser.parse(args)['atsign'], '@bob'); - expect( - () => parser.parse(args)['manager'], throwsA(isA())); - - args.addAll(['-m', '@alice']); - expect(parser.parse(args)['atsign'], '@bob'); - expect(parser.parse(args)['manager'], '@alice'); - }); - - test('test parsed args with only mandatory provided', () async { - List args = '-a @bob -m @alice'.split(' '); - - var p = await SSHNPDParams.fromArgs(args); - - expect(p.deviceAtsign, '@bob'); - expect(p.managerAtsign, '@alice'); - - expect(p.device, 'default'); - expect(p.username, getUserName(throwIfNull: true)); - expect(p.verbose, false); - expect(p.atKeysFilePath, - getDefaultAtKeysFilePath(p.homeDirectory, p.deviceAtsign)); - }); - - test('test --ssh-client arg', () async { - expect( - (await SSHNPDParams.fromArgs('-a @bob -m @alice'.split(' '))) - .sshClient, - SupportedSshClient.exec); - - expect( - (await SSHNPDParams.fromArgs( - '-a @bob -m @alice --ssh-client pure-dart'.split(' '))) - .sshClient, - SupportedSshClient.dart); - - expect( - (await SSHNPDParams.fromArgs( - '-a @bob -m @alice --ssh-client /usr/bin/ssh'.split(' '))) - .sshClient, - SupportedSshClient.exec); - - expect( - () => SSHNPDParams.fromArgs( - '-a @bob -m @alice --ssh-client something-we-do-not-support' - .split(' ')), - throwsA(isA())); - }); - - test('test parsed args with non-mandatory args provided', () async { - List args = '-a @bob -m @alice -d device -u -v -s -u'.split(' '); - - var p = await SSHNPDParams.fromArgs(args); - - expect(p.deviceAtsign, '@bob'); - expect(p.managerAtsign, '@alice'); - - expect(p.device, 'device'); - expect(p.username, getUserName(throwIfNull: true)); - expect(p.verbose, true); - expect(p.atKeysFilePath, - getDefaultAtKeysFilePath(p.homeDirectory, p.deviceAtsign)); - }); - }); -} diff --git a/packages/noports_core/test/version_test.dart b/packages/noports_core/test/version_test.dart new file mode 100644 index 000000000..dbca6f38c --- /dev/null +++ b/packages/noports_core/test/version_test.dart @@ -0,0 +1,8 @@ +import 'package:noports_core/src/version.dart'; +import 'package:test/test.dart'; + +void main() { + test('version exists', () { + expect(packageVersion, isA()); + }); +} diff --git a/packages/sshnoports/bin/sshnp.dart b/packages/sshnoports/bin/sshnp.dart index c01db56ed..eb148a415 100644 --- a/packages/sshnoports/bin/sshnp.dart +++ b/packages/sshnoports/bin/sshnp.dart @@ -7,57 +7,54 @@ 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/sshnp_params.dart' show ParserType, SshnpArg; import 'package:noports_core/utils.dart'; import 'package:sshnoports/create_at_client_cli.dart'; import 'package:sshnoports/print_version.dart'; +import 'package:sshnoports/sshnp.dart'; void main(List args) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; - late final SSHNPParams params; - SSHNP? sshnp; + late final SshnpParams params; + Sshnp? sshnp; // Manually check if the verbose flag is set - Set verboseSet = SSHNPArg.fromName('verbose').aliasList.toSet(); + Set verboseSet = SshnpArg.fromName('verbose').aliasList.toSet(); final bool verbose = args.toSet().intersection(verboseSet).isNotEmpty; // Manually check if the help flag is set - Set helpSet = SSHNPArg.fromName('help').aliasList.toSet(); + Set helpSet = SshnpArg.fromName('help').aliasList.toSet(); final bool help = args.toSet().intersection(helpSet).isNotEmpty; if (help) { printVersion(); stderr.writeln( - SSHNPArg.createArgParser(parserType: ParserType.commandLine).usage); + SshnpArg.createArgParser(parserType: ParserType.commandLine).usage); exit(0); } await runZonedGuarded(() async { try { - params = SSHNPParams.fromPartial( - SSHNPPartialParams.fromArgList( + params = SshnpParams.fromPartial( + SshnpPartialParams.fromArgList( args, parserType: ParserType.commandLine, ), ); String homeDirectory = getHomeDirectory()!; - sshnp = await SSHNP - .fromParamsWithFileBindings( + sshnp = await sshnpFromParamsWithFileBindings( params, - atClientGenerator: (SSHNPParams params, String sessionId) => - createAtClientCli( + atClientGenerator: (SshnpParams params) => createAtClientCli( homeDirectory: homeDirectory, atsign: params.clientAtSign, namespace: '${params.device}.sshnp', - pathExtension: sessionId, atKeysFilePath: params.atKeysFilePath ?? getDefaultAtKeysFilePath(homeDirectory, params.clientAtSign), rootDomain: params.rootDomain, ), - ) - .catchError((e) { + ).catchError((e) { if (e.stackTrace != null) { Error.throwWithStackTrace(e, e.stackTrace!); } @@ -66,49 +63,27 @@ void main(List args) async { if (params.listDevices) { stderr.writeln('Searching for devices...'); - var (active, off, info) = await sshnp!.listDevices(); - printDevices(active, off, info); + var deviceList = await sshnp!.listDevices(); + printDevices(deviceList); exit(0); } - await sshnp!.initialized.catchError((e) { - if (e.stackTrace != null) { - Error.throwWithStackTrace(e, e.stackTrace!); - } - throw e; - }); + SshnpResult res = await sshnp!.run(); - FutureOr runner = sshnp!.run(); - if (runner is Future) { - await runner.catchError((e) { - if (e.stackTrace != null) { - Error.throwWithStackTrace(e, e.stackTrace!); - } - throw e; - }); - } - SSHNPResult res = await runner; - - if (res is SSHNPError) { + if (res is SshnpError) { if (res.stackTrace != null) { Error.throwWithStackTrace(res, res.stackTrace!); } throw res; } - if (res is SSHNPCommand) { + if (res is SshnpCommand || res is SshnpNoOpSuccess) { stdout.write('$res\n'); - await sshnp!.done; - exit(0); - } - if (res is SSHNPNoOpSuccess) { - stderr.write('$res\n'); - await sshnp!.done; exit(0); } } on ArgumentError catch (error, stackTrace) { usageCallback(error, stackTrace); exit(1); - } on SSHNPError catch (error, stackTrace) { + } on SshnpError catch (error, stackTrace) { stderr.writeln(error.toString()); if (verbose) { stderr.writeln('\nStack Trace: ${stackTrace.toString()}'); @@ -117,7 +92,7 @@ void main(List args) async { } }, (Object error, StackTrace stackTrace) async { if (error is ArgumentError) return; - if (error is SSHNPError) return; + if (error is SshnpError) return; stderr.writeln('Unknown error: ${error.toString()}'); if (verbose) { stderr.writeln('\nStack Trace: ${stackTrace.toString()}'); @@ -129,16 +104,12 @@ void main(List args) async { void usageCallback(Object e, StackTrace s) { printVersion(); stderr.writeln( - SSHNPArg.createArgParser(parserType: ParserType.commandLine).usage); + SshnpArg.createArgParser(parserType: ParserType.commandLine).usage); stderr.writeln('\n$e'); } -void printDevices( - Iterable active, - Iterable off, - Map info, -) { - if (active.isEmpty && off.isEmpty) { +void printDevices(SshnpDeviceList deviceList) { + if (deviceList.activeDevices.isEmpty && deviceList.inactiveDevices.isEmpty) { stderr.writeln('[X] No devices found\n'); stderr.writeln( 'Note: only devices with sshnpd version 3.4.0 or higher are supported by this command.'); @@ -148,9 +119,9 @@ void printDevices( } stderr.writeln('Active Devices:'); - printDeviceList(active, info); + printDeviceList(deviceList.activeDevices, deviceList.info); stderr.writeln('Inactive Devices:'); - printDeviceList(off, info); + printDeviceList(deviceList.inactiveDevices, deviceList.info); } void printDeviceList(Iterable devices, Map info) { diff --git a/packages/sshnoports/bin/sshnpd.dart b/packages/sshnoports/bin/sshnpd.dart index 6ee59f3f6..08240f451 100644 --- a/packages/sshnoports/bin/sshnpd.dart +++ b/packages/sshnoports/bin/sshnpd.dart @@ -8,12 +8,12 @@ import 'package:sshnoports/print_version.dart'; void main(List args) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; - late final SSHNPD sshnpd; + late final Sshnpd sshnpd; try { - sshnpd = await SSHNPD.fromCommandLineArgs( + sshnpd = await Sshnpd.fromCommandLineArgs( args, - atClientGenerator: (SSHNPDParams p) => createAtClientCli( + atClientGenerator: (SshnpdParams p) => createAtClientCli( homeDirectory: p.homeDirectory, atsign: p.deviceAtsign, atKeysFilePath: p.atKeysFilePath, @@ -21,7 +21,7 @@ void main(List args) async { ), usageCallback: (e, s) { printVersion(); - stdout.writeln(SSHNPDParams.parser.usage); + stdout.writeln(SshnpdParams.parser.usage); stderr.writeln('\n$e'); }, ); diff --git a/packages/sshnoports/bin/sshrv.dart b/packages/sshnoports/bin/sshrv.dart index 54caf639a..45c076c0d 100644 --- a/packages/sshnoports/bin/sshrv.dart +++ b/packages/sshnoports/bin/sshrv.dart @@ -17,5 +17,5 @@ Future main(List args) async { localSshdPort = int.parse(args[2]); } - await SSHRV.dart(host, streamingPort, localSshdPort: localSshdPort).run(); + await Sshrv.dart(host, streamingPort, localSshdPort: localSshdPort).run(); } diff --git a/packages/sshnoports/bin/sshrvd.dart b/packages/sshnoports/bin/sshrvd.dart index 880d54fdd..4e04a30c4 100644 --- a/packages/sshnoports/bin/sshrvd.dart +++ b/packages/sshnoports/bin/sshrvd.dart @@ -8,22 +8,22 @@ import 'package:sshnoports/print_version.dart'; void main(List args) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; - late final SSHRVD sshrvd; + late final Sshrvd sshrvd; try { - sshrvd = await SSHRVD.fromCommandLineArgs( + sshrvd = await Sshrvd.fromCommandLineArgs( args, - atClientGenerator: (SSHRVDParams p) => createAtClientCli( + atClientGenerator: (SshrvdParams p) => createAtClientCli( homeDirectory: p.homeDirectory, subDirectory: '.sshrvd', atsign: p.atSign, atKeysFilePath: p.atKeysFilePath, - namespace: SSHRVD.namespace, + namespace: Sshrvd.namespace, rootDomain: p.rootDomain, ), usageCallback: (e, s) { printVersion(); - stdout.writeln(SSHRVDParams.parser.usage); + stdout.writeln(SshrvdParams.parser.usage); stderr.writeln('\n$e'); }, ); diff --git a/packages/sshnoports/lib/sshnp.dart b/packages/sshnoports/lib/sshnp.dart new file mode 100644 index 000000000..26be8f276 --- /dev/null +++ b/packages/sshnoports/lib/sshnp.dart @@ -0,0 +1,37 @@ +import 'package:noports_core/sshnp_foundation.dart'; +import 'package:at_client/at_client.dart'; + +typedef AtClientGenerator = Future Function(SshnpParams params); + +Future sshnpFromParamsWithFileBindings( + SshnpParams params, { + AtClient? atClient, + AtClientGenerator? atClientGenerator, +}) async { + atClient ??= await atClientGenerator?.call(params); + + if (atClient == null) { + throw ArgumentError( + 'atClient must be provided or atClientGenerator must be provided'); + } + + if (params.legacyDaemon) { + return Sshnp.unsigned( + atClient: atClient, + params: params, + ); + } + + switch (params.sshClient) { + case SupportedSshClient.exec: + return Sshnp.execLocal( + atClient: atClient, + params: params, + ); + case SupportedSshClient.dart: + return Sshnp.dartLocal( + atClient: atClient, + params: params, + ); + } +} diff --git a/packages/sshnoports/pubspec.lock b/packages/sshnoports/pubspec.lock index 1169af266..afb8fd6f8 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: "02331701ef45e985a637d17319e9969deaa6d53b9b171bb234d47e9c60aee94e" - url: "https://pub.dev" - source: hosted + path: "../noports_core" + relative: true + source: path version: "4.0.0-dev.3" openssh_ed25519: dependency: transitive diff --git a/packages/sshnp_gui/lib/src/controllers/config_controller.dart b/packages/sshnp_gui/lib/src/controllers/config_controller.dart index abb6838e2..0b57da34a 100644 --- a/packages/sshnp_gui/lib/src/controllers/config_controller.dart +++ b/packages/sshnp_gui/lib/src/controllers/config_controller.dart @@ -22,11 +22,11 @@ final configListController = /// A provider that exposes the [ConfigFamilyController] to the app. final configFamilyController = AutoDisposeAsyncNotifierProviderFamily< - ConfigFamilyController, SSHNPParams, String>( + ConfigFamilyController, SshnpParams, String>( ConfigFamilyController.new, ); -/// Holder model for the current [SSHNPParams] being edited +/// Holder model for the current [SshnpParams] being edited class CurrentConfigState { final String profileName; final ConfigFileWriteState configFileWriteState; @@ -35,7 +35,7 @@ class CurrentConfigState { {required this.profileName, required this.configFileWriteState}); } -/// Controller for the current [SSHNPParams] being edited +/// Controller for the current [SshnpParams] being edited class CurrentConfigController extends AutoDisposeNotifier { @override CurrentConfigState build() { @@ -72,34 +72,34 @@ class ConfigListController extends AutoDisposeAsyncNotifier> { } } -/// Controller for the family of [SSHNPParams] controllers +/// Controller for the family of [SshnpParams] controllers class ConfigFamilyController - extends AutoDisposeFamilyAsyncNotifier { + extends AutoDisposeFamilyAsyncNotifier { @override - Future build(String arg) async { + Future build(String arg) async { AtClient atClient = AtClientManager.getInstance().atClient; if (arg.isEmpty) { - return SSHNPParams.merge( - SSHNPParams.empty(), - SSHNPPartialParams(clientAtSign: atClient.getCurrentAtSign()!), + return SshnpParams.merge( + SshnpParams.empty(), + SshnpPartialParams(clientAtSign: atClient.getCurrentAtSign()!), ); } return ConfigKeyRepository.getParams(arg, atClient: atClient); } - Future putConfig(SSHNPParams params, + Future putConfig(SshnpParams params, {String? oldProfileName, BuildContext? context}) async { AtClient atClient = AtClientManager.getInstance().atClient; - SSHNPParams oldParams = state.value ?? SSHNPParams.empty(); + SshnpParams oldParams = state.value ?? SshnpParams.empty(); if (oldProfileName != null) { ref .read(configFamilyController(oldProfileName).notifier) .deleteConfig(context: context); } if (params.clientAtSign != atClient.getCurrentAtSign()) { - params = SSHNPParams.merge( + params = SshnpParams.merge( params, - SSHNPPartialParams( + SshnpPartialParams( clientAtSign: atClient.getCurrentAtSign(), ), ); @@ -122,7 +122,7 @@ class ConfigFamilyController atClient: AtClientManager.getInstance().atClient); ref.read(configListController.notifier).remove(arg); state = - AsyncValue.error('SSHNPParams has been disposed', StackTrace.current); + AsyncValue.error('SshnpParams has been disposed', StackTrace.current); } catch (e) { if (context?.mounted ?? false) { CustomSnackBar.error(content: 'Failed to delete profile: $arg'); diff --git a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart index 4676f889e..4bbeae255 100644 --- a/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart +++ b/packages/sshnp_gui/lib/src/presentation/screens/home_screen.dart @@ -33,38 +33,42 @@ class _HomeScreenState extends ConsumerState { Expanded( child: Padding( padding: const EdgeInsets.only(left: Sizes.p36, top: Sizes.p21), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - SvgPicture.asset( - 'assets/images/noports_light.svg', + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + 'assets/images/noports_light.svg', + ), + const HomeScreenActions(), + ], ), - const HomeScreenActions(), - ], - ), - gapH24, - Text(strings.availableConnections, textScaleFactor: 2), - gapH8, - profileNames.when( - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (e, s) => Text(e.toString()), - data: (profiles) { - if (profiles.isEmpty) { - return const Text('No SSHNP Configurations Found'); - } - final sortedProfiles = profiles.toList(); - sortedProfiles.sort(); - return Expanded( - child: ListView( - children: sortedProfiles.map((profileName) => ProfileBar(profileName)).toList(), + gapH24, + Text(strings.availableConnections, textScaleFactor: 2), + gapH8, + profileNames.when( + loading: () => const Center( + child: CircularProgressIndicator(), ), - ); - }, - ) - ]), + error: (e, s) => Text(e.toString()), + data: (profiles) { + if (profiles.isEmpty) { + return const Text('No Sshnp Configurations Found'); + } + final sortedProfiles = profiles.toList(); + sortedProfiles.sort(); + return Expanded( + child: ListView( + children: sortedProfiles + .map((profileName) => ProfileBar(profileName)) + .toList(), + ), + ); + }, + ) + ]), ), ), ], diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart index bff88ca25..87ce1742c 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/home_screen_actions/home_screen_action_callbacks.dart @@ -33,7 +33,7 @@ class HomeScreenActionCallbacks { final lines = (await file.readAsString()).split('\n'); ref .read(configFamilyController(profileName).notifier) - .putConfig(SSHNPParams.fromConfigLines(profileName, lines)); + .putConfig(SshnpParams.fromConfigLines(profileName, lines)); } } catch (e) { CustomSnackBar.error( diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart index 0dfc4a2b5..3e34d927e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_run_action.dart @@ -8,7 +8,7 @@ import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actio import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart'; class ProfileRunAction extends ConsumerStatefulWidget { - final SSHNPParams params; + final SshnpParams params; const ProfileRunAction(this.params, {Key? key}) : super(key: key); @override @@ -16,8 +16,8 @@ class ProfileRunAction extends ConsumerStatefulWidget { } class _ProfileRunActionState extends ConsumerState { - SSHNP? sshnp; - SSHNPResult? sshnpResult; + Sshnp? sshnp; + SshnpResult? sshnpResult; @override void initState() { @@ -30,9 +30,9 @@ class _ProfileRunActionState extends ConsumerState { .notifier) .start(); try { - SSHNPParams params = SSHNPParams.merge( + SshnpParams params = SshnpParams.merge( widget.params, - SSHNPPartialParams( + SshnpPartialParams( idleTimeout: 120, // 120 / 60 = 2 minutes addForwardsToTunnel: true, legacyDaemon: false, @@ -42,22 +42,21 @@ class _ProfileRunActionState extends ConsumerState { // TODO ensure that this keyPair gets uploaded to the app first AtClient atClient = AtClientManager.getInstance().atClient; - DartSSHKeyUtil keyUtil = DartSSHKeyUtil(); - AtSSHKeyPair keyPair = await keyUtil.getKeyPair( + DartSshKeyUtil keyUtil = DartSshKeyUtil(); + AtSshKeyPair keyPair = await keyUtil.getKeyPair( identifier: params.identityFile ?? 'id_${atClient.getCurrentAtSign()!.replaceAll('@', '')}', ); - sshnp = SSHNP.forwardPureDart( + sshnp = Sshnp.dartPure( params: params, atClient: atClient, identityKeyPair: keyPair, ); - await sshnp!.init(); sshnpResult = await sshnp!.run(); - if (sshnpResult is SSHNPError) { + if (sshnpResult is SshnpError) { throw sshnpResult!; } ref @@ -74,8 +73,8 @@ class _ProfileRunActionState extends ConsumerState { } Future onStop() async { - if (sshnpResult is SSHNPCommand) { - await (sshnpResult as SSHNPCommand).killConnectionBean(); + if (sshnpResult is SshnpCommand) { + await (sshnpResult as SshnpCommand).killConnectionBean(); } ref .read(backgroundSessionFamilyController(widget.params.profileName!) diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart index 10d1fe448..0d6b521e6 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_actions/profile_terminal_action.dart @@ -11,7 +11,7 @@ import 'package:sshnp_gui/src/presentation/widgets/utility/custom_snack_bar.dart import 'package:sshnp_gui/src/controllers/navigation_controller.dart'; class ProfileTerminalAction extends ConsumerStatefulWidget { - final SSHNPParams params; + final SshnpParams params; const ProfileTerminalAction(this.params, {Key? key}) : super(key: key); @override @@ -31,9 +31,9 @@ class _ProfileTerminalActionState extends ConsumerState { } try { - SSHNPParams params = SSHNPParams.merge( + SshnpParams params = SshnpParams.merge( widget.params, - SSHNPPartialParams( + SshnpPartialParams( legacyDaemon: false, sshClient: SupportedSshClient.dart, ), @@ -41,21 +41,20 @@ class _ProfileTerminalActionState extends ConsumerState { // TODO ensure that this keyPair gets uploaded to the app first AtClient atClient = AtClientManager.getInstance().atClient; - DartSSHKeyUtil keyUtil = DartSSHKeyUtil(); - AtSSHKeyPair keyPair = await keyUtil.getKeyPair( + DartSshKeyUtil keyUtil = DartSshKeyUtil(); + AtSshKeyPair keyPair = await keyUtil.getKeyPair( identifier: params.identityFile ?? 'id_${atClient.getCurrentAtSign()!.replaceAll('@', '')}', ); - final sshnp = SSHNP.forwardPureDart( + final sshnp = Sshnp.dartPure( params: params, atClient: atClient, identityKeyPair: keyPair, ); - await sshnp.init(); final result = await sshnp.run(); - if (result is SSHNPError) { + if (result is SshnpError) { throw result; } @@ -67,7 +66,7 @@ class _ProfileTerminalActionState extends ConsumerState { final sessionController = ref.watch(terminalSessionFamilyController(sessionId).notifier); - if (result is SSHNPCommand) { + if (result is SshnpCommand) { /// Set the command for the new session sessionController.setProcess( command: result.command, args: result.args); diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart index b8a66d1a2..e8d6e8f80 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_bar/profile_bar_actions.dart @@ -3,7 +3,7 @@ import 'package:noports_core/sshnp.dart'; import 'package:sshnp_gui/src/presentation/widgets/profile_actions/profile_actions.dart'; class ProfileBarActions extends StatelessWidget { - final SSHNPParams params; + final SshnpParams params; const ProfileBarActions(this.params, {Key? key}) : super(key: key); @override diff --git a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart index 07923d07a..31590f36e 100644 --- a/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart +++ b/packages/sshnp_gui/lib/src/presentation/widgets/profile_form/profile_form.dart @@ -20,13 +20,13 @@ class ProfileForm extends ConsumerStatefulWidget { class _ProfileFormState extends ConsumerState { final GlobalKey _formkey = GlobalKey(); late CurrentConfigState currentProfile; - SSHNPPartialParams newConfig = SSHNPPartialParams.empty(); + SshnpPartialParams newConfig = SshnpPartialParams.empty(); @override void initState() { super.initState(); } - void onSubmit(SSHNPParams oldConfig, SSHNPPartialParams newConfig) async { + void onSubmit(SshnpParams oldConfig, SshnpPartialParams newConfig) async { if (_formkey.currentState!.validate()) { _formkey.currentState!.save(); final controller = ref.read(configFamilyController( @@ -37,7 +37,7 @@ class _ProfileFormState extends ConsumerState { oldConfig.profileName != null && oldConfig.profileName!.isNotEmpty && newConfig.profileName != oldConfig.profileName; - SSHNPParams config = SSHNPParams.merge(oldConfig, newConfig); + SshnpParams config = SshnpParams.merge(oldConfig, newConfig); if (rename) { // delete old config file and write the new one @@ -78,9 +78,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.profileName, labelText: strings.profileName, onChanged: (value) { - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(profileName: value), + SshnpPartialParams(profileName: value), ); }, validator: FormValidator.validateProfileNameField, @@ -90,9 +90,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.device, labelText: strings.device, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(device: value), + SshnpPartialParams(device: value), ), ), ], @@ -105,9 +105,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.sshnpdAtSign, labelText: strings.sshnpdAtSign, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(sshnpdAtSign: value), + SshnpPartialParams(sshnpdAtSign: value), ), validator: FormValidator.validateAtsignField, ), @@ -116,9 +116,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.host, labelText: strings.host, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(host: value), + SshnpPartialParams(host: value), ), validator: FormValidator.validateRequiredField, ), @@ -133,9 +133,9 @@ class _ProfileFormState extends ConsumerState { // initialValue: oldConfig.sendSshPublicKey, // labelText: strings.sendSshPublicKey, // onChanged: (value) => - // newConfig = SSHNPPartialParams.merge( + // newConfig = SshnpPartialParams.merge( // newConfig, - // SSHNPPartialParams(sendSshPublicKey: value), + // SshnpPartialParams(sendSshPublicKey: value), // ), // ), gapW8, @@ -151,9 +151,9 @@ class _ProfileFormState extends ConsumerState { // value: newConfig.rsa ?? oldConfig.rsa, // onChanged: (newValue) { // setState(() { - // newConfig = SSHNPPartialParams.merge( + // newConfig = SshnpPartialParams.merge( // newConfig, - // SSHNPPartialParams(rsa: newValue), + // SshnpPartialParams(rsa: newValue), // ); // }); // }, @@ -171,9 +171,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.remoteUsername ?? '', labelText: strings.remoteUserName, onChanged: (value) { - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(remoteUsername: value), + SshnpPartialParams(remoteUsername: value), ); }), gapW8, @@ -181,9 +181,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.port.toString(), labelText: strings.port, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(port: int.tryParse(value)), + SshnpPartialParams(port: int.tryParse(value)), ), validator: FormValidator.validateRequiredField, ), @@ -197,9 +197,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.localPort.toString(), labelText: strings.localPort, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(localPort: int.tryParse(value)), + SshnpPartialParams(localPort: int.tryParse(value)), ), ), gapW8, @@ -207,9 +207,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.localSshdPort.toString(), labelText: strings.localSshdPort, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams( + SshnpPartialParams( localSshdPort: int.tryParse(value)), ), ), @@ -222,9 +222,9 @@ class _ProfileFormState extends ConsumerState { labelText: strings.localSshOptions, //Double the width of the text field (+8 for the gapW8) width: CustomTextFormField.defaultWidth * 2 + 8, - onChanged: (value) => newConfig = SSHNPPartialParams.merge( + onChanged: (value) => newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(localSshOptions: value.split(',')), + SshnpPartialParams(localSshOptions: value.split(',')), ), ), gapH10, @@ -235,9 +235,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.atKeysFilePath, labelText: strings.atKeysFilePath, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(atKeysFilePath: value), + SshnpPartialParams(atKeysFilePath: value), ), ), gapW8, @@ -245,9 +245,9 @@ class _ProfileFormState extends ConsumerState { initialValue: oldConfig.rootDomain, labelText: strings.rootDomain, onChanged: (value) => - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(rootDomain: value), + SshnpPartialParams(rootDomain: value), ), ), ], @@ -267,9 +267,9 @@ class _ProfileFormState extends ConsumerState { value: newConfig.verbose ?? oldConfig.verbose, onChanged: (newValue) { setState(() { - newConfig = SSHNPPartialParams.merge( + newConfig = SshnpPartialParams.merge( newConfig, - SSHNPPartialParams(verbose: newValue), + SshnpPartialParams(verbose: newValue), ); }); },