diff --git a/.github/composite/setup_entrypoints/action.yaml b/.github/composite/setup_entrypoints/action.yaml index 46213cd89..4d4b07678 100644 --- a/.github/composite/setup_entrypoints/action.yaml +++ b/.github/composite/setup_entrypoints/action.yaml @@ -15,8 +15,8 @@ inputs: sshnpd_atsign: description: sshnpd atsign required: true - sshrvd_atsign: - description: sshrvd atsign + srvd_atsign: + description: srvd atsign required: true devicename: description: Unique sshnp devicename @@ -52,7 +52,7 @@ runs: esac ;; esac - ./setup-sshnp-entrypoint.sh ${{ inputs.devicename }} ${{ inputs.sshnp_atsign }} ${{ inputs.sshnpd_atsign }} ${{ inputs.sshrvd_atsign }} "$entrypoint_filename" "$args ${{ inputs.args }}" + ./setup-sshnp-entrypoint.sh ${{ inputs.devicename }} ${{ inputs.sshnp_atsign }} ${{ inputs.sshnpd_atsign }} ${{ inputs.srvd_atsign }} "$entrypoint_filename" "$args ${{ inputs.args }}" - name: Setup NPD entrypoint shell: bash @@ -73,4 +73,4 @@ runs: shell: bash working-directory: tests/end2end_tests/contexts/_init_ run: | - ./setup-sshrvd-entrypoint.sh ${{ inputs.sshrvd_atsign }} "sshrvd_entrypoint.sh" + ./setup-srvd-entrypoint.sh ${{ inputs.srvd_atsign }} "srvd_entrypoint.sh" diff --git a/.github/workflows/end2end_tests.yaml b/.github/workflows/end2end_tests.yaml index 6ee09e4ca..26f8c2ac1 100644 --- a/.github/workflows/end2end_tests.yaml +++ b/.github/workflows/end2end_tests.yaml @@ -16,7 +16,7 @@ on: env: SSHNP_ATSIGN: "@8incanteater" SSHNPD_ATSIGN: "@8052simple" - SSHRVD_ATSIGN: "@8485wealthy51" + SRVD_ATSIGN: "@8485wealthy51" PROD_AM_RVD_ATSIGN: "@rv_am" PROD_AP_RVD_ATSIGN: "@rv_ap" @@ -87,7 +87,7 @@ jobs: sshnp_atsign: ${{ env.SSHNP_ATSIGN }} sshnpd: ${{ matrix.npd }} sshnpd_atsign: ${{ env.SSHNPD_ATSIGN }} - sshrvd_atsign: ${{ env[env.PROD_RVD_ATSIGN] }} + srvd_atsign: ${{ env[env.PROD_RVD_ATSIGN] }} devicename: ${{ env.DEVICENAME }} - name: Ensure entrypoints exist @@ -95,7 +95,7 @@ jobs: run: | cat sshnp/entrypoint.sh cat sshnpd/entrypoint.sh - cat sshrvd/entrypoint.sh + cat srvd/entrypoint.sh - name: Create docker-compose.yaml working-directory: tests/end2end_tests/tests @@ -247,7 +247,7 @@ jobs: sshnp_atsign: ${{ env.SSHNP_ATSIGN }} sshnpd: ${{ matrix.npd }} sshnpd_atsign: ${{ env.SSHNPD_ATSIGN }} - sshrvd_atsign: ${{ env[env.PROD_RVD_ATSIGN] }} + srvd_atsign: ${{ env[env.PROD_RVD_ATSIGN] }} devicename: ${{ env.DEVICENAME }} args: "-P 55" @@ -256,7 +256,7 @@ jobs: run: | cat sshnp/entrypoint.sh cat sshnpd/entrypoint.sh - cat sshrvd/entrypoint.sh + cat srvd/entrypoint.sh - name: Create docker-compose.yaml working-directory: tests/end2end_tests/tests diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index 1d9491df0..30932ace9 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -55,11 +55,11 @@ jobs: - if: ${{ matrix.os != 'windows-latest' }} run: dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd${{ matrix.ext }} - if: ${{ matrix.os != 'windows-latest' }} - run: dart compile exe bin/sshrv.dart -v -o sshnp/sshrv${{ matrix.ext }} + run: dart compile exe bin/srv.dart -v -o sshnp/srv${{ matrix.ext }} - if: ${{ matrix.os != 'windows-latest' }} - run: dart compile exe bin/sshrvd.dart -v -o sshnp/sshrvd${{ matrix.ext }} + run: dart compile exe bin/srvd.dart -v -o sshnp/srvd${{ matrix.ext }} - if: ${{ matrix.os != 'windows-latest' }} - run: dart compile exe bin/sshrvd.dart -D ENABLE_SNOOP=true -v -o sshnp/debug/sshrvd${{ matrix.ext }} + run: dart compile exe bin/srvd.dart -D ENABLE_SNOOP=true -v -o sshnp/debug/srvd${{ matrix.ext }} - run: cp -r bundles/core/* sshnp/ - run: cp -r bundles/${{ matrix.bundle }}/* sshnp/ - run: cp LICENSE sshnp diff --git a/.github/workflows/prod_tests.yaml b/.github/workflows/prod_tests.yaml index 6cbff2587..88683e8d2 100644 --- a/.github/workflows/prod_tests.yaml +++ b/.github/workflows/prod_tests.yaml @@ -14,10 +14,10 @@ permissions: env: SSHNP_ATSIGN: "@8incanteater" SSHNPD_ATSIGN: "@8052simple" - SSHRVD_ATSIGN: "@8485wealthy51" - SSHRVD_AM_ATSIGN: "@rv_am" - SSHRVD_AP_ATSIGN: "@rv_ap" - SSHRVD_EU_ATSIGN: "@rv_eu" + SRVD_ATSIGN: "@8485wealthy51" + SRVD_AM_ATSIGN: "@rv_am" + SRVD_AP_ATSIGN: "@rv_ap" + SRVD_EU_ATSIGN: "@rv_eu" DOCKER_COMPOSE_BUILD_CMD: "docker compose build" DOCKER_COMPOSE_UP_CMD: "docker compose up --abort-on-container-exit" @@ -28,10 +28,10 @@ jobs: fail-fast: false # if one job fails, do not fail the others matrix: rvd: - # - ${{ env.SSHRVD_ATSIGN }} - # - ${{ env.SSHRVD_AM_ATSIGN }} - # - ${{ env.SSHRVD_AP_ATSIGN }} - # - ${{ env.SSHRVD_EU_ATSIGN }} + # - ${{ env.SRVD_ATSIGN }} + # - ${{ env.SRVD_AM_ATSIGN }} + # - ${{ env.SRVD_AP_ATSIGN }} + # - ${{ env.SRVD_EU_ATSIGN }} - "@8485wealthy51" - "@rv_am" - "@rv_ap" @@ -52,8 +52,8 @@ jobs: SSHNPD_ATKEYS="$(tr '[:lower:]' '[:upper:]' <<< '${{ env.SSHNPD_ATSIGN }}')" echo "SSHNPD_ATKEYS=ATKEYS_${SSHNPD_ATKEYS:1}" >> $GITHUB_ENV - SSHRVD_ATKEYS="$(tr '[:lower:]' '[:upper:]' <<< '${{ env.SSHRVD_ATSIGN }}')" - echo "SSHRVD_ATKEYS=ATKEYS_${SSHRVD_ATKEYS:1}" >> $GITHUB_ENV + SRVD_ATKEYS="$(tr '[:lower:]' '[:upper:]' <<< '${{ env.SRVD_ATSIGN }}')" + echo "SRVD_ATKEYS=ATKEYS_${SRVD_ATKEYS:1}" >> $GITHUB_ENV - name: Setup NP/NPD keys working-directory: tests/end2end_tests/contexts @@ -84,18 +84,18 @@ jobs: sshnpd_entrypoint.sh - name: Set up RVD keys and entrypoint - if: matrix.rvd == env.SSHRVD_ATSIGN + if: matrix.rvd == env.SRVD_ATSIGN working-directory: tests/end2end_tests run: | # setup keys - echo "${{ secrets[env.SSHRVD_ATKEYS] }}" > contexts/sshrvd/.atsign/keys/${{ env.SSHRVD_ATSIGN }}_key.atKeys + echo "${{ secrets[env.SRVD_ATKEYS] }}" > contexts/srvd/.atsign/keys/${{ env.SRVD_ATSIGN }}_key.atKeys - # set up sshrvd entrypoint + # set up srvd entrypoint cd contexts/_init_ - ./setup-sshrvd-entrypoint.sh \ + ./setup-srvd-entrypoint.sh \ ${{ matrix.rvd }} \ - sshrvd_entrypoint.sh - cd ../sshrvd + srvd_entrypoint.sh + cd ../srvd cat entrypoint.sh - name: Ensure entrypoints exist @@ -118,8 +118,8 @@ jobs: echo " condition: service_started" >> docker-compose.yaml echo " container-sshnpd:" >> docker-compose.yaml echo " condition: service_healthy" >> docker-compose.yaml - if [ "${{ matrix.rvd }}" == "${{ env.SSHRVD_ATSIGN }}" ]; then - echo " container-sshrvd:" >> docker-compose.yaml + if [ "${{ matrix.rvd }}" == "${{ env.SRVD_ATSIGN }}" ]; then + echo " container-srvd:" >> docker-compose.yaml echo " condition: service_healthy" >> docker-compose.yaml fi cat service-container-sshnpd.yaml >> docker-compose.yaml @@ -127,16 +127,16 @@ jobs: echo " depends_on:" >> docker-compose.yaml echo " image-runtime-release:" >> docker-compose.yaml echo " condition: service_started" >> docker-compose.yaml - if [ "${{ matrix.rvd }}" == "${{ env.SSHRVD_ATSIGN }}" ]; then - echo " container-sshrvd:" >> docker-compose.yaml + if [ "${{ matrix.rvd }}" == "${{ env.SRVD_ATSIGN }}" ]; then + echo " container-srvd:" >> docker-compose.yaml echo " condition: service_healthy" >> docker-compose.yaml fi - name: Add RVD service to docker-compose.yaml - if: matrix.rvd == env.SSHRVD_ATSIGN + if: matrix.rvd == env.SRVD_ATSIGN working-directory: tests/end2end_tests/tests run: | - cat service-container-sshrvd.yaml >> docker-compose.yaml + cat service-container-srvd.yaml >> docker-compose.yaml echo " image: atsigncompany/sshnp-e2e-runtime:latest" >> docker-compose.yaml echo " depends_on:" >> docker-compose.yaml echo " image-runtime-release:" >> docker-compose.yaml diff --git a/examples/automations/python/sshnoports_automation_python/sshnp_client.py b/examples/automations/python/sshnoports_automation_python/sshnp_client.py index 20a0eec41..2dfe0d4d9 100644 --- a/examples/automations/python/sshnoports_automation_python/sshnp_client.py +++ b/examples/automations/python/sshnoports_automation_python/sshnp_client.py @@ -252,7 +252,7 @@ def download_package_source(self, source: PackageSource) -> str: f"dart compile exe {target_path}/bin/sshnpd.dart -o {target_path}/sshnpd" ) self.client.run_command( - f"dart compile exe {target_path}/bin/sshrv.dart -o {target_path}/sshrv" + f"dart compile exe {target_path}/bin/srv.dart -o {target_path}/srv" ) self.client.run_command( f"dart compile exe {target_path}/bin/activate_cli.dart -o {target_path}/at_activate" @@ -280,7 +280,7 @@ def setup_main_binaries(self, source: str) -> None: if not self.is_connected(): raise Exception("SSHNPClient not connected to device") - binaries = "{" + ",".join(["sshnpd", "sshrv", "at_activate"]) + "}" + binaries = "{" + ",".join(["sshnpd", "srv", "at_activate"]) + "}" self.client.exec_command(f"cp -f {source}/{binaries} ~/.local/bin/") def update_sshnpd(self, source: PackageSource) -> None: diff --git a/packages/dart/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart b/packages/dart/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart index ce34c50d6..8c7a6d957 100644 --- a/packages/dart/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart +++ b/packages/dart/noports_core/lib/src/common/at_ssh_key_util/local_ssh_key_util.dart @@ -129,7 +129,8 @@ class LocalSshKeyUtil implements AtSshKeyUtil { }) async { // Check to see if the ssh public key is // supported keys by the dartssh2 package - if (!sshPublicKey.startsWith(RegExp(r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { + if (!sshPublicKey.startsWith(RegExp( + r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { throw ('$sshPublicKey does not look like a public key'); } diff --git a/packages/dart/noports_core/lib/src/common/default_args.dart b/packages/dart/noports_core/lib/src/common/default_args.dart index 638097e10..f23afe475 100644 --- a/packages/dart/noports_core/lib/src/common/default_args.dart +++ b/packages/dart/noports_core/lib/src/common/default_args.dart @@ -1,6 +1,6 @@ import 'package:noports_core/src/common/io_types.dart'; import 'package:noports_core/src/common/types.dart'; -import 'package:noports_core/sshrv.dart'; +import 'package:noports_core/srv.dart'; class DefaultArgs { static const String namespace = 'sshnp'; @@ -8,7 +8,7 @@ class DefaultArgs { SupportedSshAlgorithm.ed25519; static const bool verbose = false; static const String rootDomain = 'root.atsign.org'; - static const SshrvGenerator sshrvGenerator = Sshrv.exec; + static const SrvGenerator srvGenerator = Srv.exec; static const int localSshdPort = 22; static const int remoteSshdPort = 22; @@ -18,6 +18,10 @@ class DefaultArgs { static const bool addForwardsToTunnel = false; static final bool allowLocalFileSystem = Platform.isLinux || Platform.isMacOS || Platform.isWindows; + static const bool authenticateClientToRvd = false; + static const bool authenticateDeviceToRvd = false; + static const bool encryptRvdTraffic = false; + static const bool discoverDaemonFeatures = false; } class DefaultSshnpArgs { diff --git a/packages/dart/noports_core/lib/src/common/features.dart b/packages/dart/noports_core/lib/src/common/features.dart new file mode 100644 index 000000000..40eac77e0 --- /dev/null +++ b/packages/dart/noports_core/lib/src/common/features.dart @@ -0,0 +1,12 @@ +/// Features which can be supported by the NoPorts Daemon +enum DaemonFeatures { + /// daemon will accept public keys sent by clients (i.e. daemon has been + /// started with the `--sshpublickey` or `-s` flag) + acceptsPublicKeys, + + /// authenticate when connecting to the Socket Rendezvous (sr) + srAuth, + + /// End-to-end encrypt traffic sent via the SocketRendezvous (sr) + srE2ee, +} diff --git a/packages/dart/noports_core/lib/src/common/mixins/at_client_bindings.dart b/packages/dart/noports_core/lib/src/common/mixins/at_client_bindings.dart index e1012a547..8335d4a5d 100644 --- a/packages/dart/noports_core/lib/src/common/mixins/at_client_bindings.dart +++ b/packages/dart/noports_core/lib/src/common/mixins/at_client_bindings.dart @@ -3,19 +3,26 @@ 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'); - }); + String value, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + }) async { + await atClient.notificationService.notify( + NotificationParams.forUpdate(atKey, value: value), + checkForFinalDeliveryStatus: checkForFinalDeliveryStatus, + waitForFinalDeliveryStatus: waitForFinalDeliveryStatus, + onSuccess: (NotificationResult notification) { + logger.info('SUCCESS:$notification with key: ${atKey.toString()}'); + }, + onError: (notification) { + logger.info('ERROR:$notification'); + }, + ); } Stream subscribe( diff --git a/packages/dart/noports_core/lib/src/common/types.dart b/packages/dart/noports_core/lib/src/common/types.dart index 8137d48b5..c74bd2ce8 100644 --- a/packages/dart/noports_core/lib/src/common/types.dart +++ b/packages/dart/noports_core/lib/src/common/types.dart @@ -1,12 +1,21 @@ -import 'package:noports_core/sshrv.dart'; - -typedef SshrvGenerator = Sshrv Function(String, int, {int localSshdPort}); +import 'package:noports_core/srv.dart'; + +typedef SrvGenerator = Srv Function( + String, + int, { + required int localPort, + required bool bindLocalPort, + String? rvdAuthString, + String? sessionAESKeyString, + String? sessionIVString, +}); enum SupportedSshClient { openssh(cliArg: 'openssh'), dart(cliArg: 'dart'); final String _cliArg; + const SupportedSshClient({required String cliArg}) : _cliArg = cliArg; factory SupportedSshClient.fromString(String cliArg) { @@ -25,6 +34,7 @@ enum SupportedSshAlgorithm { rsa(cliArg: 'ssh-rsa'); final String _cliArg; + const SupportedSshAlgorithm({required String cliArg}) : _cliArg = cliArg; factory SupportedSshAlgorithm.fromString(String cliArg) { diff --git a/packages/dart/noports_core/lib/src/srv/srv.dart b/packages/dart/noports_core/lib/src/srv/srv.dart new file mode 100644 index 000000000..7b91a11a1 --- /dev/null +++ b/packages/dart/noports_core/lib/src/srv/srv.dart @@ -0,0 +1,96 @@ +import 'dart:io'; + +import 'package:noports_core/src/srv/srv_impl.dart'; +import 'package:socket_connector/socket_connector.dart'; + +abstract class Srv { + /// The internet address of the host to connect to. + abstract final String host; + + /// The port of the host to connect to. + abstract final int streamingPort; + + /// The local port to bridge to + /// Defaults to 22 + abstract final int localPort; + + /// A string which needs to be presented to the rvd before the rvd + /// will allow any further traffic on the socket + abstract final String? rvdAuthString; + + /// The AES key for encryption / decryption of the rv traffic + abstract final String? sessionAESKeyString; + + /// The IV to use with the [sessionAESKeyString] + abstract final String? sessionIVString; + + abstract final bool bindLocalPort; + + Future run(); + + // Can't use factory functions since Srv contains a generic type + static Srv exec( + String host, + int streamingPort, { + required int localPort, + required bool bindLocalPort, + String? rvdAuthString, + String? sessionAESKeyString, + String? sessionIVString, + }) { + return SrvImplExec( + host, + streamingPort, + localPort: localPort, + bindLocalPort: bindLocalPort, + rvdAuthString: rvdAuthString, + sessionAESKeyString: sessionAESKeyString, + sessionIVString: sessionIVString, + ); + } + + static Srv dart( + String host, + int streamingPort, { + required int localPort, + required bool bindLocalPort, + String? rvdAuthString, + String? sessionAESKeyString, + String? sessionIVString, + }) { + return SrvImplDart( + host, + streamingPort, + localPort: localPort, + bindLocalPort: bindLocalPort, + rvdAuthString: rvdAuthString, + sessionAESKeyString: sessionAESKeyString, + sessionIVString: sessionIVString, + ); + } + + static Future getLocalBinaryPath() async { + List binaryNames = ['srv', 'sshrv']; + for (var name in binaryNames) { + var binary = await _getBinaryPathByName(name); + if (binary != null) return binary; + } + return null; + } + + static Future _getBinaryPathByName(String name) async { + String postfix = Platform.isWindows ? '.exe' : ''; + List pathList = + Platform.resolvedExecutable.split(Platform.pathSeparator); + bool isExe = + (pathList.last == 'sshnp$postfix' || pathList.last == 'sshnpd$postfix'); + + pathList + ..removeLast() + ..add('$name$postfix'); + + File binaryName = File(pathList.join(Platform.pathSeparator)); + bool binaryExists = await binaryName.exists(); + return (isExe && binaryExists) ? binaryName.absolute.path : null; + } +} diff --git a/packages/dart/noports_core/lib/src/srv/srv_impl.dart b/packages/dart/noports_core/lib/src/srv/srv_impl.dart new file mode 100644 index 000000000..b9948ef7c --- /dev/null +++ b/packages/dart/noports_core/lib/src/srv/srv_impl.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:at_utils/at_utils.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:cryptography/dart.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/srv.dart'; +import 'package:socket_connector/socket_connector.dart'; + +@visibleForTesting +class SrvImplExec implements Srv { + static final AtSignLogger logger = AtSignLogger('SrvImplExec'); + + @override + final String host; + + @override + final int streamingPort; + + @override + final int localPort; + + @override + final bool bindLocalPort; + + @override + final String? rvdAuthString; + + @override + final String? sessionAESKeyString; + + @override + final String? sessionIVString; + + @visibleForTesting + static const completionString = 'rv is running'; + + SrvImplExec( + this.host, + this.streamingPort, { + required this.localPort, + required this.bindLocalPort, + this.rvdAuthString, + this.sessionAESKeyString, + this.sessionIVString, + }) { + if ((sessionAESKeyString == null && sessionIVString != null) || + (sessionAESKeyString != null && sessionIVString == null)) { + throw ArgumentError('Both AES key and IV are required, or neither'); + } + } + + @override + Future run() async { + String? command = await Srv.getLocalBinaryPath(); + String postfix = Platform.isWindows ? '.exe' : ''; + if (command == null) { + throw Exception( + 'Unable to locate srv$postfix binary.\n' + 'N.B. sshnp is expected to be compiled and run from source, not via the dart command.', + ); + } + var rvArgs = [ + '-h', + host, + '-p', + streamingPort.toString(), + '--local-port', + localPort.toString(), + ]; + if (bindLocalPort) { + rvArgs.add('--bind-local-port'); + } + Map environment = {}; + if (rvdAuthString != null) { + rvArgs.addAll(['--rv-auth']); + environment['RV_AUTH'] = rvdAuthString!; + } + if (sessionAESKeyString != null && sessionIVString != null) { + rvArgs.addAll(['--rv-e2ee']); + environment['RV_AES'] = sessionAESKeyString!; + environment['RV_IV'] = sessionIVString!; + } + + logger.info('$runtimeType.run(): executing $command' + ' ${rvArgs.join(' ')}'); + Process p = await Process.start( + command, + rvArgs, + mode: ProcessStartMode.detachedWithStdio, + includeParentEnvironment: true, + environment: environment, + ); + Completer rvPortBound = Completer(); + p.stdout.listen((List l) { + var s = utf8.decode(l).trim(); + logger.info('rv stdout | $s'); + }, onError: (e) {}); + p.stderr.listen((List l) { + var allLines = utf8.decode(l).trim(); + for (String s in allLines.split('\n')) { + logger.info('rv stderr | $s'); + if (s.endsWith(completionString) && !rvPortBound.isCompleted) { + rvPortBound.complete(); + } + } + }, onError: (e) { + if (!rvPortBound.isCompleted) { + rvPortBound.completeError(e); + } + }); + + await rvPortBound.future.timeout(Duration(seconds: 2)); + + return p; + } +} + +@visibleForTesting +class SrvImplDart implements Srv { + @override + final String host; + + @override + final int streamingPort; + + @override + final int localPort; + + @override + final bool bindLocalPort; + + @override + final String? rvdAuthString; + + @override + final String? sessionAESKeyString; + + @override + final String? sessionIVString; + + SrvImplDart( + this.host, + this.streamingPort, { + required this.localPort, + required this.bindLocalPort, + this.rvdAuthString, + this.sessionAESKeyString, + this.sessionIVString, + }) { + if ((sessionAESKeyString == null && sessionIVString != null) || + (sessionAESKeyString != null && sessionIVString == null)) { + throw ArgumentError('Both AES key and IV are required, or neither'); + } + } + + @override + Future run() async { + DataTransformer? encrypter; + DataTransformer? decrypter; + + if (sessionAESKeyString != null && sessionIVString != null) { + final DartAesCtr algorithm = DartAesCtr.with256bits( + macAlgorithm: Hmac.sha256(), + ); + final SecretKey sessionAESKey = + SecretKey(base64Decode(sessionAESKeyString!)); + final List sessionIV = base64Decode(sessionIVString!); + + encrypter = (Stream> stream) { + return algorithm.encryptStream( + stream, + secretKey: sessionAESKey, + nonce: sessionIV, + onMac: (mac) {}, + ); + }; + decrypter = (Stream> stream) { + return algorithm.decryptStream( + stream, + secretKey: sessionAESKey, + nonce: sessionIV, + mac: Mac.empty, + ); + }; + } + + try { + var hosts = await InternetAddress.lookup(host); + + late final SocketConnector socketConnector; + + if (bindLocalPort) { + socketConnector = await SocketConnector.serverToSocket( + portA: localPort, + addressB: hosts[0], + portB: streamingPort, + verbose: false, + transformAtoB: encrypter, + transformBtoA: decrypter); + if (rvdAuthString != null) { + stderr.writeln('authenticating socketB'); + socketConnector.pendingB.first.socket.writeln(rvdAuthString); + } + } else { + socketConnector = await SocketConnector.socketToSocket( + addressA: InternetAddress.loopbackIPv4, + portA: localPort, + addressB: hosts[0], + portB: streamingPort, + verbose: false, + transformAtoB: encrypter, + transformBtoA: decrypter); + if (rvdAuthString != null) { + stderr.writeln('authenticating socketB'); + socketConnector.connections.first.sideB.socket.writeln(rvdAuthString); + } + } + + // Do not remove this output; it is specifically looked for in + // SrvImplExec.run + stderr.writeln(SrvImplExec.completionString); + + return socketConnector; + } catch (e) { + AtSignLogger('srv').severe(e.toString()); + rethrow; + } + } + + Stream> encrypt(Stream> s) async* {} + + Stream> decrypt(Stream> s) async* {} +} diff --git a/packages/dart/noports_core/lib/src/srvd/build_env.dart b/packages/dart/noports_core/lib/src/srvd/build_env.dart new file mode 100644 index 000000000..77050b528 --- /dev/null +++ b/packages/dart/noports_core/lib/src/srvd/build_env.dart @@ -0,0 +1,6 @@ +import 'dart:io'; + +class BuildEnv { + static final bool enableSnoop = + (Platform.environment['ENABLE_SNOOP'] ?? "false").toLowerCase() == 'true'; +} diff --git a/packages/dart/noports_core/lib/src/srvd/signature_verifying_socket_authenticator.dart b/packages/dart/noports_core/lib/src/srvd/signature_verifying_socket_authenticator.dart new file mode 100644 index 000000000..0f9c2e6c0 --- /dev/null +++ b/packages/dart/noports_core/lib/src/srvd/signature_verifying_socket_authenticator.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:at_chops/at_chops.dart'; +import 'package:at_utils/at_logger.dart'; + +/// +/// Verifies signature of the data received over the socket using the same signing algorithm used to sign the data +/// See [SigningAlgoType] to know more about supported signing algorithms +/// See [HashingAlgoType] to know more about supported hashing algorithms +/// +/// Expects the first message received in JSON format, with the following structure: +/// { +/// "signature":"", +/// "hashingAlgo":"", +/// "signingAlgo":"" +/// } +/// +/// also expects signature to be base64 encoded +/// +/// +class SignatureAuthVerifier { + static final AtSignLogger logger = AtSignLogger('SignatureAuthVerifier'); + + /// Public key of the signing algorithm used to sign the data + String publicKey; + + /// data that was signed, this is the data that should be matched once the signature is verified + String dataToVerify; + + /// string generated by rvd which should be included in auth strings from sshnp and sshnpd + String rvdNonce; + + /// a tag to help decipher logs + String tag; + + SignatureAuthVerifier( + this.publicKey, + this.dataToVerify, + this.rvdNonce, + this.tag, + ); + + AtSigningResult _verifySignature(AtSigningVerificationInput input) { + AtChopsKeys atChopsKeys = AtChopsKeys(); + AtChops atChops = AtChopsImpl(atChopsKeys); + return atChops.verify(input); + } + + /// We expect the authenticating client to send a JSON message with + /// this structure: + /// ```json + /// { + /// "signature":"<signature>", + /// "hashingAlgo":"<algo>", + /// "signingAlgo":"<algo>", + /// "payload":<the data which was signed> + /// } + /// ``` + /// The signature is verified against [dataToVerify] and, although not + /// strictly necessary, the rvdNonce is also checked in what the client + /// send in the payload + Future<(bool, Stream?)> authenticate(Socket socket) async { + Completer<(bool, Stream?)> completer = Completer(); + bool authenticated = false; + StreamController sc = StreamController(); + logger.info('SignatureAuthVerifier $tag: starting listen'); + socket.listen((Uint8List data) { + if (authenticated) { + sc.add(data); + } else { + try { + final message = String.fromCharCodes(data); + logger.info('SignatureAuthVerifier $tag received data: $message'); + var envelope = jsonDecode(message); + + final hashingAlgo = + HashingAlgoType.values.byName(envelope['hashingAlgo']); + final signingAlgo = + SigningAlgoType.values.byName(envelope['signingAlgo']); + + var payload = envelope['payload']; + if (payload == null || payload is! Map) { + completer.completeError( + 'Received an auth signature which does not include the payload'); + return; + } + if (payload['rvdNonce'] != rvdNonce) { + completer.completeError( + 'Received rvdNonce which does not match what is expected'); + return; + } + + AtSigningVerificationInput input = AtSigningVerificationInput( + dataToVerify, base64Decode(envelope['signature']), publicKey) + ..signingAlgorithm = DefaultSigningAlgo(null, hashingAlgo) + ..signingMode = AtSigningMode.data + ..signingAlgoType = signingAlgo + ..hashingAlgoType = hashingAlgo; + + AtSigningResult atSigningResult = _verifySignature(input); + logger.info('Signing verification outcome is:' + ' ${atSigningResult.result}'); + bool result = atSigningResult.result; + + if (result == false) { + completer.completeError( + 'Signature verification failed. Signatures did not match.'); + return; + } + + authenticated = true; + completer.complete((true, sc.stream)); + } catch (e) { + completer.completeError('Error during socket authentication: $e'); + } + } + }, onError: (error) => sc.addError(error), onDone: () => sc.close()); + return completer.future; + } +} diff --git a/packages/dart/noports_core/lib/src/srvd/socket_connector.dart b/packages/dart/noports_core/lib/src/srvd/socket_connector.dart new file mode 100644 index 000000000..7f9636bf3 --- /dev/null +++ b/packages/dart/noports_core/lib/src/srvd/socket_connector.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:at_utils/at_logger.dart'; +import 'package:noports_core/src/srvd/srvd_impl.dart'; +import 'package:socket_connector/socket_connector.dart'; + +import 'signature_verifying_socket_authenticator.dart'; + +typedef ConnectorParams = ( + SendPort, + int, // portA + int, // portB + String, // session params + bool, // logTraffic + bool, // verbose +); +typedef PortPair = (int, int); + +/// This function is meant to be run in a separate isolate +/// It starts the socket connector, and sends back the assigned ports to the main isolate +/// It then waits for socket connector to die before shutting itself down +void socketConnector(ConnectorParams connectorParams) async { + var ( + sendPort, + portA, + portB, + srvdSessionParamsJsonString, + logTraffic, + verbose, + ) = connectorParams; + + if (verbose) { + AtSignLogger.root_level = 'INFO'; + } else { + AtSignLogger.root_level = 'WARNING'; + } + + final logger = AtSignLogger(' srvd / socket_connector '); + + SrvdSessionParams srvdSessionParams = + SrvdSessionParams.fromJson(jsonDecode(srvdSessionParamsJsonString)); + logger.info( + 'Starting socket connector session for ${srvdSessionParams.toJson()}'); + + /// Create the socketAuthVerifiers as required + Map expectedPayloadForSignature = { + 'sessionId': srvdSessionParams.sessionId, + 'clientNonce': srvdSessionParams.clientNonce, + 'rvdNonce': srvdSessionParams.rvdNonce, + }; + + SocketAuthVerifier? socketAuthVerifierA; + if (srvdSessionParams.authenticateSocketA) { + String? pkAtSignA = srvdSessionParams.publicKeyA; + if (pkAtSignA == null) { + logger.shout('Cannot spawn socket connector.' + ' Authenticator for ${srvdSessionParams.atSignA}' + ' could not be created as PublicKey could not be' + ' fetched from the atServer.'); + throw Exception('Failed to create SocketAuthenticator' + ' for ${srvdSessionParams.atSignA} due to failure to get public key for ${srvdSessionParams.atSignA}'); + } + socketAuthVerifierA = SignatureAuthVerifier( + pkAtSignA, + jsonEncode(expectedPayloadForSignature), + srvdSessionParams.rvdNonce!, + srvdSessionParams.atSignA, + ).authenticate; + } + + SocketAuthVerifier? socketAuthVerifierB; + if (srvdSessionParams.authenticateSocketB) { + String? pkAtSignB = srvdSessionParams.publicKeyB; + if (pkAtSignB == null) { + logger.shout('Cannot spawn socket connector.' + ' Authenticator for ${srvdSessionParams.atSignB}' + ' could not be created as PublicKey could not be' + ' fetched from the atServer'); + throw Exception('Failed to create SocketAuthenticator' + ' for ${srvdSessionParams.atSignB} due to failure to get public key for ${srvdSessionParams.atSignB}'); + } + socketAuthVerifierB = SignatureAuthVerifier( + pkAtSignB, + jsonEncode(expectedPayloadForSignature), + srvdSessionParams.rvdNonce!, + srvdSessionParams.atSignB!, + ).authenticate; + } + + /// Create the socket connector + SocketConnector connector = await SocketConnector.serverToServer( + addressA: InternetAddress.anyIPv4, + addressB: InternetAddress.anyIPv4, + portA: portA, + portB: portB, + verbose: verbose, + logTraffic: logTraffic, + socketAuthVerifierA: socketAuthVerifierA, + socketAuthVerifierB: socketAuthVerifierB); + + /// Get the assigned ports from the socket connector + portA = connector.sideAPort!; + portB = connector.sideBPort!; + + logger.info('Assigned ports [$portA, $portB]' + ' for session ${srvdSessionParams.sessionId}'); + + /// Return the assigned ports to the main isolate + sendPort.send((portA, portB)); + + /// Shut myself down once the socket connector closes + logger.info('Waiting for connector to close'); + await connector.done; + + logger.info('Finished session ${srvdSessionParams.sessionId}' + ' for ${srvdSessionParams.atSignA} to ${srvdSessionParams.atSignB}' + ' using ports [$portA, $portB]'); + + Isolate.current.kill(); +} diff --git a/packages/dart/noports_core/lib/src/sshrvd/sshrvd.dart b/packages/dart/noports_core/lib/src/srvd/srvd.dart similarity index 68% rename from packages/dart/noports_core/lib/src/sshrvd/sshrvd.dart rename to packages/dart/noports_core/lib/src/srvd/srvd.dart index 33f204861..b2dae476c 100644 --- a/packages/dart/noports_core/lib/src/sshrvd/sshrvd.dart +++ b/packages/dart/noports_core/lib/src/srvd/srvd.dart @@ -3,10 +3,10 @@ import 'dart:async'; import 'package:at_client/at_client.dart'; import 'package:at_utils/at_logger.dart'; import 'package:meta/meta.dart'; -import 'package:noports_core/src/sshrvd/sshrvd_impl.dart'; -import 'package:noports_core/src/sshrvd/sshrvd_params.dart'; +import 'package:noports_core/src/srvd/srvd_impl.dart'; +import 'package:noports_core/src/srvd/srvd_params.dart'; -abstract class Sshrvd { +abstract class Srvd { static const String namespace = 'sshrvd'; abstract final AtSignLogger logger; @@ -16,17 +16,18 @@ abstract class Sshrvd { abstract final String atKeysFilePath; abstract final String managerAtsign; abstract final String ipAddress; - abstract final bool snoop; + abstract final bool logTraffic; + bool verbose = false; /// true once [init] has completed @visibleForTesting bool initialized = false; - static Future fromCommandLineArgs(List args, + static Future fromCommandLineArgs(List args, {AtClient? atClient, - FutureOr Function(SshrvdParams)? atClientGenerator, + FutureOr Function(SrvdParams)? atClientGenerator, void Function(Object, StackTrace)? usageCallback}) async { - return SshrvdImpl.fromCommandLineArgs(args, + return SrvdImpl.fromCommandLineArgs(args, atClient: atClient, atClientGenerator: atClientGenerator, usageCallback: usageCallback); diff --git a/packages/dart/noports_core/lib/src/srvd/srvd_impl.dart b/packages/dart/noports_core/lib/src/srvd/srvd_impl.dart new file mode 100644 index 000000000..95662a165 --- /dev/null +++ b/packages/dart/noports_core/lib/src/srvd/srvd_impl.dart @@ -0,0 +1,342 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:convert'; +import 'package:at_client/at_client.dart'; +import 'package:at_utils/at_logger.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/validation_utils.dart'; +import 'package:noports_core/src/srvd/build_env.dart'; +import 'package:noports_core/src/srvd/socket_connector.dart'; +import 'package:noports_core/src/srvd/srvd.dart'; +import 'package:noports_core/src/srvd/srvd_params.dart'; + +@protected +class SrvdImpl implements Srvd { + @override + final AtSignLogger logger = AtSignLogger(' srvd '); + @override + AtClient atClient; + @override + final String atSign; + @override + final String homeDirectory; + @override + final String atKeysFilePath; + @override + final String managerAtsign; + @override + final String ipAddress; + @override + final bool logTraffic; + @override + bool verbose = false; + + @override + @visibleForTesting + bool initialized = false; + + static final String subscriptionRegex = '${Srvd.namespace}@'; + + late final SrvdUtil srvdUtil; + + SrvdImpl({ + required this.atClient, + required this.atSign, + required this.homeDirectory, + required this.atKeysFilePath, + required this.managerAtsign, + required this.ipAddress, + required this.logTraffic, + required this.verbose, + SrvdUtil? srvdUtil, + }) { + this.srvdUtil = srvdUtil ?? SrvdUtil(atClient); + logger.hierarchicalLoggingEnabled = true; + logger.logger.level = Level.SHOUT; + } + + static Future fromCommandLineArgs(List args, + {AtClient? atClient, + FutureOr Function(SrvdParams)? atClientGenerator, + void Function(Object, StackTrace)? usageCallback}) async { + try { + var p = await SrvdParams.fromArgs(args); + + if (!await File(p.atKeysFilePath).exists()) { + throw ('\n Unable to find .atKeys file : ${p.atKeysFilePath}'); + } + + AtSignLogger.root_level = 'SHOUT'; + if (p.verbose) { + AtSignLogger.root_level = 'INFO'; + } + + if (atClient == null && atClientGenerator == null) { + throw StateError('atClient and atClientGenerator are both null'); + } + + atClient ??= await atClientGenerator!(p); + + var srvd = SrvdImpl( + atClient: atClient, + atSign: p.atSign, + homeDirectory: p.homeDirectory, + atKeysFilePath: p.atKeysFilePath, + managerAtsign: p.managerAtsign, + ipAddress: p.ipAddress, + logTraffic: p.logTraffic, + verbose: p.verbose, + ); + + if (p.verbose) { + srvd.logger.logger.level = Level.INFO; + } + return srvd; + } catch (e, s) { + usageCallback?.call(e, s); + rethrow; + } + } + + @override + Future init() async { + if (initialized) { + throw StateError('Cannot init() - already initialized'); + } + + initialized = true; + } + + @override + Future run() async { + if (!initialized) { + throw StateError('Cannot run() - not initialized'); + } + NotificationService notificationService = atClient.notificationService; + + notificationService + .subscribe(regex: subscriptionRegex, shouldDecrypt: true) + .listen(_notificationHandler); + } + + void _notificationHandler(AtNotification notification) async { + if (!srvdUtil.accept(notification)) { + return; + } + late SrvdSessionParams sessionParams; + try { + sessionParams = await srvdUtil.getParams(notification); + + if (managerAtsign != 'open' && managerAtsign != sessionParams.atSignA) { + logger.shout( + 'Session ${sessionParams.sessionId} for ${sessionParams.atSignA} is denied'); + return; + } + } catch (e) { + logger.shout('Unable to provide the socket pair due to: $e'); + return; + } + + logger + .info('New session request: $sessionParams from ${notification.from}'); + + (int, int) ports = await _spawnSocketConnector( + 0, + 0, + sessionParams, + logTraffic, + verbose, + ); + var (portA, portB) = ports; + logger.warning( + 'Starting session ${sessionParams.sessionId} for ${sessionParams.atSignA} to ${sessionParams.atSignB} using ports $ports'); + + var metaData = Metadata() + ..isPublic = false + ..isEncrypted = true + ..ttl = 10000 + ..namespaceAware = true; + + var atKey = AtKey() + ..key = sessionParams.sessionId + ..sharedBy = atSign + ..sharedWith = notification.from + ..namespace = Srvd.namespace + ..metadata = metaData; + + String data = '$ipAddress,$portA,$portB,${sessionParams.rvdNonce}'; + + logger.info( + 'Sending response data for session ${sessionParams.sessionId} : [$data]'); + + try { + await atClient.notificationService.notify( + NotificationParams.forUpdate(atKey, value: data), + waitForFinalDeliveryStatus: false, + checkForFinalDeliveryStatus: false); + } catch (e) { + stderr.writeln("Error writing session ${notification.value} atKey"); + } + } + + /// This function spawns a new socketConnector in a background isolate + /// once the socketConnector has spawned and is ready to accept connections + /// it sends back the port numbers to the main isolate + /// then the port numbers are returned from this function + Future _spawnSocketConnector( + int portA, + int portB, + SrvdSessionParams srvdSessionParams, + bool logTraffic, + bool verbose, + ) async { + /// Spawn an isolate and wait for it to send back the issued port numbers + ReceivePort receivePort = ReceivePort(srvdSessionParams.sessionId); + + ConnectorParams parameters = ( + receivePort.sendPort, + portA, + portB, + jsonEncode(srvdSessionParams), + BuildEnv.enableSnoop && logTraffic, + verbose, + ); + + logger + .info("Spawning socket connector isolate with parameters $parameters"); + + unawaited(Isolate.spawn(socketConnector, parameters)); + + PortPair ports = await receivePort.first; + + logger.info('Received ports $ports in main isolate' + ' for session ${srvdSessionParams.sessionId}'); + + return ports; + } +} + +class SrvdSessionParams { + final String sessionId; + final String atSignA; + final String? atSignB; + final bool authenticateSocketA; + final bool authenticateSocketB; + final String? publicKeyA; + final String? publicKeyB; + final String? clientNonce; + final String? rvdNonce; + + SrvdSessionParams({ + required this.sessionId, + required this.atSignA, + this.atSignB, + this.authenticateSocketA = false, + this.authenticateSocketB = false, + this.publicKeyA, + this.publicKeyB, + this.rvdNonce, + this.clientNonce, + }); + + @override + String toString() => toJson().toString(); + + Map toJson() => { + 'sessionId': sessionId, + 'atSignA': atSignA, + 'atSignB': atSignB, + 'authenticateSocketA': authenticateSocketA, + 'authenticateSocketB': authenticateSocketB, + 'publicKeyA': publicKeyA, + 'publicKeyB': publicKeyB, + 'rvdNonce': rvdNonce, + 'clientNonce': clientNonce, + }; + + static SrvdSessionParams fromJson(Map json) { + return SrvdSessionParams( + sessionId: json['sessionId'], + atSignA: json['atSignA'], + atSignB: json['atSignB'], + authenticateSocketA: json['authenticateSocketA'], + authenticateSocketB: json['authenticateSocketB'], + publicKeyA: json['publicKeyA'], + publicKeyB: json['publicKeyB'], + rvdNonce: json['rvdNonce'], + clientNonce: json['clientNonce'], + ); + } +} + +class SrvdUtil { + final AtClient atClient; + + SrvdUtil(this.atClient); + + bool accept(AtNotification notification) { + return notification.key.contains(Srvd.namespace); + } + + Future getParams(AtNotification notification) async { + if (notification.key.contains('.request_ports.${Srvd.namespace}')) { + return await _processJSONRequest(notification); + } + return _processLegacyRequest(notification); + } + + SrvdSessionParams _processLegacyRequest(AtNotification notification) { + return SrvdSessionParams( + sessionId: notification.value!, + atSignA: notification.from, + ); + } + + Future _processJSONRequest( + AtNotification notification) async { + dynamic jsonValue = jsonDecode(notification.value ?? ''); + + assertValidValue(jsonValue, 'sessionId', String); + assertValidValue(jsonValue, 'atSignA', String); + assertValidValue(jsonValue, 'atSignB', String); + assertValidValue(jsonValue, 'clientNonce', String); + assertValidValue(jsonValue, 'authenticateSocketA', bool); + assertValidValue(jsonValue, 'authenticateSocketA', bool); + + final String sessionId = jsonValue['sessionId']; + final String atSignA = jsonValue['atSignA']; + final String atSignB = jsonValue['atSignB']; + final String clientNonce = jsonValue['clientNonce']; + final bool authenticateSocketA = jsonValue['authenticateSocketA']; + final bool authenticateSocketB = jsonValue['authenticateSocketB']; + + String rvdSessionNonce = DateTime.now().toIso8601String(); + + String? publicKeyA; + String? publicKeyB; + if (authenticateSocketA) { + publicKeyA = await _fetchPublicKey(atSignA); + } + if (authenticateSocketB) { + publicKeyB = await _fetchPublicKey(atSignB); + } + return SrvdSessionParams( + sessionId: sessionId, + atSignA: atSignA, + atSignB: atSignB, + authenticateSocketA: authenticateSocketA, + authenticateSocketB: authenticateSocketB, + publicKeyA: publicKeyA, + publicKeyB: publicKeyB, + rvdNonce: rvdSessionNonce, + clientNonce: clientNonce, + ); + } + + Future _fetchPublicKey(String atSign) async { + AtValue v = await atClient.get(AtKey.fromString('public:publickey$atSign')); + return v.value; + } +} diff --git a/packages/dart/noports_core/lib/src/sshrvd/sshrvd_params.dart b/packages/dart/noports_core/lib/src/srvd/srvd_params.dart similarity index 81% rename from packages/dart/noports_core/lib/src/sshrvd/sshrvd_params.dart rename to packages/dart/noports_core/lib/src/srvd/srvd_params.dart index 8cd7ba27a..8aa4bffe5 100644 --- a/packages/dart/noports_core/lib/src/sshrvd/sshrvd_params.dart +++ b/packages/dart/noports_core/lib/src/srvd/srvd_params.dart @@ -1,8 +1,8 @@ import 'package:args/args.dart'; import 'package:noports_core/src/common/file_system_utils.dart'; -import 'package:noports_core/src/sshrvd/build_env.dart'; +import 'package:noports_core/src/srvd/build_env.dart'; -class SshrvdParams { +class SrvdParams { final String username; final String atSign; final String homeDirectory; @@ -10,13 +10,13 @@ class SshrvdParams { final String managerAtsign; final String ipAddress; final bool verbose; - final bool snoop; + final bool logTraffic; final String rootDomain; // Non param variables static final ArgParser parser = _createArgParser(); - SshrvdParams({ + SrvdParams({ required this.username, required this.atSign, required this.homeDirectory, @@ -24,18 +24,18 @@ class SshrvdParams { required this.managerAtsign, required this.ipAddress, required this.verbose, - required this.snoop, + required this.logTraffic, 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 SrvdParams( username: getUserName(throwIfNull: true)!, atSign: atSign, homeDirectory: homeDirectory, @@ -44,7 +44,7 @@ class SshrvdParams { managerAtsign: r['manager'], ipAddress: r['ip'], verbose: r['verbose'], - snoop: BuildEnv.enableSnoop && r['snoop'], + logTraffic: BuildEnv.enableSnoop && r['snoop'], rootDomain: r['root-domain'], ); } @@ -64,7 +64,7 @@ class SshrvdParams { 'atsign', abbr: 'a', mandatory: true, - help: 'atSign for sshrvd', + help: 'atSign for srvd', ); parser.addOption( 'manager', @@ -72,7 +72,7 @@ class SshrvdParams { defaultsTo: 'open', mandatory: false, help: - 'Managers atSign that sshrvd will accept requests from. Default is any atSign can use sshrvd', + 'Managers atSign that srvd will accept requests from. Default is any atSign can use srvd', ); parser.addOption( 'ip', @@ -90,7 +90,7 @@ class SshrvdParams { 'snoop', abbr: 's', defaultsTo: false, - help: 'Snoop on traffic passing through service', + help: 'Log traffic passing through service', ); } parser.addOption( diff --git a/packages/dart/noports_core/lib/src/sshnp/impl/notification_request_message.dart b/packages/dart/noports_core/lib/src/sshnp/impl/notification_request_message.dart new file mode 100644 index 000000000..ba1b38d9e --- /dev/null +++ b/packages/dart/noports_core/lib/src/sshnp/impl/notification_request_message.dart @@ -0,0 +1,38 @@ +class SshnpSessionRequest { + final bool direct; + final String sessionId; + final String host; + final int port; + final bool authenticateToRvd; + final String clientNonce; + final String? rvdNonce; + final bool encryptRvdTraffic; + final String? clientEphemeralPK; + final String? clientEphemeralPKType; + + SshnpSessionRequest({ + required this.direct, + required this.sessionId, + required this.host, + required this.port, + required this.authenticateToRvd, + required this.clientNonce, + required this.rvdNonce, + required this.encryptRvdTraffic, + required this.clientEphemeralPK, + required this.clientEphemeralPKType, + }); + + Map toJson() => { + 'direct': direct, + 'sessionId': sessionId, + 'host': host, + 'port': port, + 'authenticateToRvd': authenticateToRvd, + 'clientNonce': clientNonce, + 'rvdNonce': rvdNonce, + 'encryptRvdTraffic': encryptRvdTraffic, + 'clientEphemeralPK': clientEphemeralPK, + 'clientEphemeralPKType': clientEphemeralPKType, + }; +} diff --git a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart index 1638a801b..ca76392e3 100644 --- a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_dart_pure_impl.dart @@ -2,14 +2,35 @@ import 'dart:async'; import 'package:at_client/at_client.dart'; import 'package:dartssh2/dartssh2.dart'; +import 'package:noports_core/src/sshnp/impl/notification_request_message.dart'; import 'package:noports_core/sshnp_foundation.dart'; class SshnpDartPureImpl extends SshnpCore with SshnpDartSshKeyHandler, DartSshSessionHandler { - SshnpDartPureImpl( - {required super.atClient, - required super.params, - required AtSshKeyPair? identityKeyPair}) { + SshnpDartPureImpl({ + required super.atClient, + required super.params, + required AtSshKeyPair? identityKeyPair, + }) { + // TODO Defensive code to prevent use of rvd auth and rv traffic encryption + // TODO until they have been properly implemented with an in-memory RV. + // TODO At that time, make these four params "final" again + if (params.discoverDaemonFeatures) { + logger.shout('$runtimeType: disabling discoverDaemonFeatures flag'); + params.discoverDaemonFeatures = false; + } + if (params.encryptRvdTraffic) { + logger.shout('$runtimeType: disabling encryptRvdTraffic flag'); + params.encryptRvdTraffic = false; + } + if (params.authenticateDeviceToRvd) { + logger.shout('$runtimeType: disabling authenticateDeviceToRvd flag'); + params.authenticateDeviceToRvd = false; + } + if (params.authenticateClientToRvd) { + logger.shout('$runtimeType: disabling authenticateClientToRvd flag'); + params.authenticateClientToRvd = false; + } this.identityKeyPair = identityKeyPair; _sshnpdChannel = SshnpdDefaultChannel( atClient: atClient, @@ -17,7 +38,7 @@ class SshnpDartPureImpl extends SshnpCore sessionId: sessionId, namespace: this.namespace, ); - _sshrvdChannel = SshrvdDartChannel( + _srvdChannel = SrvdDartChannel( atClient: atClient, params: params, sessionId: sessionId, @@ -29,8 +50,8 @@ class SshnpDartPureImpl extends SshnpCore late final SshnpdDefaultChannel _sshnpdChannel; @override - SshrvdDartChannel get sshrvdChannel => _sshrvdChannel; - late final SshrvdDartChannel _sshrvdChannel; + SrvdDartChannel get srvdChannel => _srvdChannel; + late final SrvdDartChannel _srvdChannel; @override Future initialize() async { @@ -60,12 +81,22 @@ class SshnpDartPureImpl extends SshnpCore ..sharedBy = params.clientAtSign ..sharedWith = params.sshnpdAtSign ..metadata = (Metadata()..ttl = 10000), - signAndWrapAndJsonEncode(atClient, { - 'direct': true, - 'sessionId': sessionId, - 'host': sshrvdChannel.host, - 'port': sshrvdChannel.port, - }), + signAndWrapAndJsonEncode( + atClient, + SshnpSessionRequest( + direct: true, + sessionId: sessionId, + host: srvdChannel.host, + port: srvdChannel.port, + authenticateToRvd: params.authenticateDeviceToRvd, + clientNonce: srvdChannel.clientNonce, + rvdNonce: srvdChannel.rvdNonce, + encryptRvdTraffic: params.encryptRvdTraffic, + clientEphemeralPK: params.sessionKP.atPublicKey.publicKey, + clientEphemeralPKType: params.sessionKPType.name, + ).toJson()), + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); /// Wait for a response from sshnpd diff --git a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart index f8a80b900..675891b96 100644 --- a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_openssh_local_impl.dart @@ -2,11 +2,15 @@ import 'dart:async'; import 'package:at_client/at_client.dart'; import 'package:noports_core/src/common/io_types.dart'; +import 'package:noports_core/src/sshnp/impl/notification_request_message.dart'; import 'package:noports_core/src/sshnp/util/ephemeral_port_binder.dart'; import 'package:noports_core/sshnp_foundation.dart'; class SshnpOpensshLocalImpl extends SshnpCore - with SshnpLocalSshKeyHandler, OpensshSshSessionHandler, EphemeralPortBinder { + with + SshnpLocalSshKeyHandler, + OpensshSshSessionHandler, + EphemeralPortBinder { SshnpOpensshLocalImpl({ required super.atClient, required super.params, @@ -17,7 +21,7 @@ class SshnpOpensshLocalImpl extends SshnpCore sessionId: sessionId, namespace: this.namespace, ); - _sshrvdChannel = SshrvdExecChannel( + _srvdChannel = SrvdExecChannel( atClient: atClient, params: params, sessionId: sessionId, @@ -29,8 +33,8 @@ class SshnpOpensshLocalImpl extends SshnpCore late final SshnpdDefaultChannel _sshnpdChannel; @override - SshrvdExecChannel get sshrvdChannel => _sshrvdChannel; - late final SshrvdExecChannel _sshrvdChannel; + SrvdExecChannel get srvdChannel => _srvdChannel; + late final SrvdExecChannel _srvdChannel; @override Future initialize() async { @@ -39,6 +43,7 @@ class SshnpOpensshLocalImpl extends SshnpCore await super.initialize(); completeInitialization(); } + @override Future run() async { /// Ensure that sshnp is initialized @@ -46,6 +51,10 @@ class SshnpOpensshLocalImpl extends SshnpCore logger.info('Sending request to sshnpd'); + final server = await ServerSocket.bind(InternetAddress.anyIPv4, 0); + int localRvPort = server.port; + await server.close(); + /// Send an ssh request to sshnpd await notify( AtKey() @@ -54,12 +63,22 @@ class SshnpOpensshLocalImpl extends SshnpCore ..sharedBy = params.clientAtSign ..sharedWith = params.sshnpdAtSign ..metadata = (Metadata()..ttl = 10000), - signAndWrapAndJsonEncode(atClient, { - 'direct': true, - 'sessionId': sessionId, - 'host': sshrvdChannel.host, - 'port': sshrvdChannel.port, - }), + signAndWrapAndJsonEncode( + atClient, + SshnpSessionRequest( + direct: true, + sessionId: sessionId, + host: srvdChannel.host, + port: srvdChannel.srvdPort!, + authenticateToRvd: params.authenticateDeviceToRvd, + clientNonce: srvdChannel.clientNonce, + rvdNonce: srvdChannel.rvdNonce, + encryptRvdTraffic: params.encryptRvdTraffic, + clientEphemeralPK: params.sessionKP.atPublicKey.publicKey, + clientEphemeralPKType: params.sessionKPType.name, + ).toJson()), + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); /// Wait for a response from sshnpd @@ -74,6 +93,14 @@ class SshnpOpensshLocalImpl extends SshnpCore ); } + /// Start srv + await srvdChannel.runSrv( + directSsh: true, + localRvPort: localRvPort, + sessionAESKeyString: sshnpdChannel.sessionAESKeyString, + sessionIVString: sshnpdChannel.sessionIVString, + ); + /// Load the ephemeral private key into a key pair AtSshKeyPair ephemeralKeyPair = AtSshKeyPair.fromPem( sshnpdChannel.ephemeralPrivateKey!, @@ -86,7 +113,9 @@ class SshnpOpensshLocalImpl extends SshnpCore /// Start the initial tunnel Process? bean = await startInitialTunnelSession( - ephemeralKeyPairIdentifier: ephemeralKeyPair.identifier); + ephemeralKeyPairIdentifier: ephemeralKeyPair.identifier, + localRvPort: localRvPort, + ); /// Remove the key pair from the key utility await keyUtil.deleteKeyPair(identifier: ephemeralKeyPair.identifier); diff --git a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart index 4f48d2db9..60b1af0bf 100644 --- a/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnp/impl/sshnp_unsigned_impl.dart @@ -22,7 +22,7 @@ class SshnpUnsignedImpl extends SshnpCore sessionId: sessionId, namespace: this.namespace, ); - _sshrvdChannel = SshrvdExecChannel( + _srvdChannel = SrvdExecChannel( atClient: atClient, params: params, sessionId: sessionId, @@ -34,8 +34,8 @@ class SshnpUnsignedImpl extends SshnpCore late final SshnpdUnsignedChannel _sshnpdChannel; @override - SshrvdExecChannel get sshrvdChannel => _sshrvdChannel; - late final SshrvdExecChannel _sshrvdChannel; + SrvdExecChannel get srvdChannel => _srvdChannel; + late final SrvdExecChannel _srvdChannel; @override Future initialize() async { @@ -66,6 +66,8 @@ class SshnpUnsignedImpl extends SshnpCore await notify( sendOurPrivateKeyToSshnpd, ephemeralKeyPair.privateKeyContents, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); completeInitialization(); @@ -76,9 +78,6 @@ class SshnpUnsignedImpl extends SshnpCore /// 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( @@ -88,7 +87,9 @@ class SshnpUnsignedImpl extends SshnpCore ..sharedBy = params.clientAtSign ..sharedWith = params.sshnpdAtSign ..metadata = (Metadata()..ttl = 10000), - '$localPort ${sshrvdChannel.port} ${keyUtil.username} ${sshrvdChannel.host} $sessionId', + '$localPort ${srvdChannel.port} ${keyUtil.username} ${srvdChannel.host} $sessionId', + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); /// Wait for a response from sshnpd @@ -97,6 +98,9 @@ class SshnpUnsignedImpl extends SshnpCore throw SshnpError('sshnpd did not acknowledge the request'); } + /// Start srv + var bean = await srvdChannel.runSrv(directSsh: false); + /// Ensure that we clean up after ourselves await callDisposal(); diff --git a/packages/dart/noports_core/lib/src/sshnp/models/sshnp_arg.dart b/packages/dart/noports_core/lib/src/sshnp/models/sshnp_arg.dart index 7947367a9..baf2bba4a 100644 --- a/packages/dart/noports_core/lib/src/sshnp/models/sshnp_arg.dart +++ b/packages/dart/noports_core/lib/src/sshnp/models/sshnp_arg.dart @@ -124,6 +124,10 @@ class SshnpArg { addForwardsToTunnelArg, configFileArg, listDevicesArg, + authenticateClientToRvdArg, + authenticateDeviceToRvdArg, + encryptRvdTrafficArg, + discoverDaemonFeaturesArg, ]; @override @@ -220,7 +224,7 @@ class SshnpArg { static const hostArg = SshnpArg( name: 'host', abbr: 'h', - help: 'atSign of sshrvd daemon or FQDN/IP address to connect back to', + help: 'atSign of srvd daemon or FQDN/IP address to connect back to', mandatory: true, ); static const portArg = SshnpArg( @@ -346,4 +350,48 @@ class SshnpArg { negatable: false, parseWhen: ParseWhen.commandLine, ); + static const authenticateClientToRvdArg = SshnpArg( + name: 'authenticate-client-to-rvd', + abbr: 'a', + help: 'When false, client will not authenticate itself to rvd', + defaultsTo: DefaultArgs.authenticateClientToRvd, + format: ArgFormat.flag, + parseWhen: ParseWhen.commandLine, + mandatory: false, + ); + static const authenticateDeviceToRvdArg = SshnpArg( + name: 'authenticate-device-to-rvd', + abbr: 'A', + help: 'When false, device will not authenticate to the socket rendezvous', + defaultsTo: DefaultArgs.authenticateDeviceToRvd, + format: ArgFormat.flag, + parseWhen: ParseWhen.commandLine, + mandatory: false, + ); + static const encryptRvdTrafficArg = SshnpArg( + name: 'encrypt-rvd-traffic', + abbr: 'E', + help: 'When true, traffic via the socket rendezvous is encrypted,' + ' in addition to whatever encryption the traffic already has' + ' (e.g. an ssh session)', + defaultsTo: DefaultArgs.encryptRvdTraffic, + format: ArgFormat.flag, + parseWhen: ParseWhen.commandLine, + mandatory: false, + ); + static const discoverDaemonFeaturesArg = SshnpArg( + name: 'discover-daemon-features', + abbr: 'F', + help: 'When this flag is set, this client starts by pinging the daemon to' + ' discover what features it supports, and exits if this client has ' + ' requested use of a feature which the daemon does not support.' + ' If you already know what features the daemon supports and are ' + ' setting other flags (--authenticate-device-to-rvd and' + ' --encrypt-rvd-traffic) based on that knowledge, then you should unset' + ' this flag to reduce total time-to-connection.', + defaultsTo: DefaultArgs.discoverDaemonFeatures, + format: ArgFormat.flag, + parseWhen: ParseWhen.commandLine, + mandatory: false, + ); } diff --git a/packages/dart/noports_core/lib/src/sshnp/models/sshnp_params.dart b/packages/dart/noports_core/lib/src/sshnp/models/sshnp_params.dart index 4f6a4bd6b..6bcf24ac8 100644 --- a/packages/dart/noports_core/lib/src/sshnp/models/sshnp_params.dart +++ b/packages/dart/noports_core/lib/src/sshnp/models/sshnp_params.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:at_chops/at_chops.dart'; +import 'package:at_utils/at_utils.dart'; import 'package:noports_core/src/common/types.dart'; import 'package:noports_core/src/sshnp/models/config_file_repository.dart'; import 'package:noports_core/src/sshnp/models/sshnp_arg.dart'; @@ -34,6 +36,12 @@ class SshnpParams { final bool addForwardsToTunnel; final String? atKeysFilePath; final SupportedSshAlgorithm sshAlgorithm; + // TODO Once pure dart impl supports these flags then they can be + // TODO made "final" again + bool authenticateClientToRvd; + bool authenticateDeviceToRvd; + bool encryptRvdTraffic; + bool discoverDaemonFeatures; /// Special Arguments @@ -43,6 +51,20 @@ class SshnpParams { /// Operation flags final bool listDevices; + /// An encryption keypair which should only ever reside in memory. + /// The public key is provided in responses to client 'pings', and is + /// used by clients to encrypt symmetric encryption keys intended for + /// one-time use in a NoPorts session, and share the encrypted details + /// as part of the session request payload. + AtEncryptionKeyPair get sessionKP { + _sessionKP ??= AtChopsUtil.generateAtEncryptionKeyPair(keySize: 2048); + return _sessionKP!; + } + + /// Generate the ephemeralKeyPair only on demand + AtEncryptionKeyPair? _sessionKP; + final EncryptionKeyType sessionKPType = EncryptionKeyType.rsa2048; + SshnpParams({ required this.clientAtSign, required this.sshnpdAtSign, @@ -66,6 +88,10 @@ class SshnpParams { this.idleTimeout = DefaultArgs.idleTimeout, this.addForwardsToTunnel = DefaultArgs.addForwardsToTunnel, this.sshAlgorithm = DefaultArgs.sshAlgorithm, + this.authenticateClientToRvd = DefaultArgs.authenticateClientToRvd, + this.authenticateDeviceToRvd = DefaultArgs.authenticateDeviceToRvd, + this.encryptRvdTraffic = DefaultArgs.encryptRvdTraffic, + this.discoverDaemonFeatures = DefaultArgs.discoverDaemonFeatures, }); factory SshnpParams.empty() { @@ -107,6 +133,13 @@ class SshnpParams { addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, sshAlgorithm: params2.sshAlgorithm ?? params1.sshAlgorithm, + authenticateClientToRvd: + params2.authenticateClientToRvd ?? params1.authenticateClientToRvd, + authenticateDeviceToRvd: + params2.authenticateDeviceToRvd ?? params1.authenticateDeviceToRvd, + encryptRvdTraffic: params2.encryptRvdTraffic ?? params1.encryptRvdTraffic, + discoverDaemonFeatures: + params2.discoverDaemonFeatures ?? params1.discoverDaemonFeatures, ); } @@ -147,6 +180,14 @@ class SshnpParams { addForwardsToTunnel: partial.addForwardsToTunnel ?? DefaultArgs.addForwardsToTunnel, sshAlgorithm: partial.sshAlgorithm ?? DefaultArgs.sshAlgorithm, + authenticateClientToRvd: partial.authenticateClientToRvd ?? + DefaultArgs.authenticateClientToRvd, + authenticateDeviceToRvd: partial.authenticateDeviceToRvd ?? + DefaultArgs.authenticateDeviceToRvd, + encryptRvdTraffic: + partial.encryptRvdTraffic ?? DefaultArgs.encryptRvdTraffic, + discoverDaemonFeatures: + partial.discoverDaemonFeatures ?? DefaultArgs.discoverDaemonFeatures, ); } @@ -195,6 +236,10 @@ class SshnpParams { SshnpArg.idleTimeoutArg.name: idleTimeout, SshnpArg.addForwardsToTunnelArg.name: addForwardsToTunnel, SshnpArg.sshAlgorithmArg.name: sshAlgorithm.toString(), + SshnpArg.authenticateClientToRvdArg.name: authenticateClientToRvd, + SshnpArg.authenticateDeviceToRvdArg.name: authenticateDeviceToRvd, + SshnpArg.encryptRvdTrafficArg.name: encryptRvdTraffic, + SshnpArg.discoverDaemonFeaturesArg.name: discoverDaemonFeatures, }; args.removeWhere( (key, value) => !parserType.shouldParse(SshnpArg.fromName(key).parseWhen), @@ -233,6 +278,10 @@ class SshnpPartialParams { final int? idleTimeout; final bool? addForwardsToTunnel; final SupportedSshAlgorithm? sshAlgorithm; + final bool? authenticateClientToRvd; + final bool? authenticateDeviceToRvd; + final bool? encryptRvdTraffic; + final bool? discoverDaemonFeatures; /// Operation flags final bool? listDevices; @@ -260,6 +309,10 @@ class SshnpPartialParams { this.idleTimeout, this.addForwardsToTunnel, this.sshAlgorithm, + this.authenticateClientToRvd, + this.authenticateDeviceToRvd, + this.encryptRvdTraffic, + this.discoverDaemonFeatures, }); factory SshnpPartialParams.empty() { @@ -296,6 +349,13 @@ class SshnpPartialParams { addForwardsToTunnel: params2.addForwardsToTunnel ?? params1.addForwardsToTunnel, sshAlgorithm: params2.sshAlgorithm ?? params1.sshAlgorithm, + authenticateClientToRvd: + params2.authenticateClientToRvd ?? params1.authenticateClientToRvd, + authenticateDeviceToRvd: + params2.authenticateDeviceToRvd ?? params1.authenticateDeviceToRvd, + encryptRvdTraffic: params2.encryptRvdTraffic ?? params1.encryptRvdTraffic, + discoverDaemonFeatures: + params2.discoverDaemonFeatures ?? params1.discoverDaemonFeatures, ); } @@ -319,8 +379,12 @@ class SshnpPartialParams { factory SshnpPartialParams.fromArgMap(Map args) { return SshnpPartialParams( profileName: args[SshnpArg.profileNameArg.name], - clientAtSign: args[SshnpArg.fromArg.name], - sshnpdAtSign: args[SshnpArg.toArg.name], + clientAtSign: args[SshnpArg.fromArg.name] == null + ? null + : AtUtils.fixAtSign(args[SshnpArg.fromArg.name]), + sshnpdAtSign: args[SshnpArg.toArg.name] == null + ? null + : AtUtils.fixAtSign(args[SshnpArg.toArg.name]), host: args[SshnpArg.hostArg.name], device: args[SshnpArg.deviceArg.name], port: args[SshnpArg.portArg.name], @@ -345,6 +409,10 @@ class SshnpPartialParams { ? null : SupportedSshAlgorithm.fromString( args[SshnpArg.sshAlgorithmArg.name]), + authenticateClientToRvd: args[SshnpArg.authenticateClientToRvdArg.name], + authenticateDeviceToRvd: args[SshnpArg.authenticateDeviceToRvdArg.name], + encryptRvdTraffic: args[SshnpArg.encryptRvdTrafficArg.name], + discoverDaemonFeatures: args[SshnpArg.discoverDaemonFeaturesArg.name], ); } diff --git a/packages/dart/noports_core/lib/src/sshnp/sshnp.dart b/packages/dart/noports_core/lib/src/sshnp/sshnp.dart index 4ee1f996e..0cffdf3be 100644 --- a/packages/dart/noports_core/lib/src/sshnp/sshnp.dart +++ b/packages/dart/noports_core/lib/src/sshnp/sshnp.dart @@ -50,7 +50,7 @@ abstract interface class Sshnp { SshnpParams get params; /// May only be run after [initialize] has been run. - /// - Sends request to sshrvd if required + /// - Sends request to srvd if required /// - Sends request to sshnpd; the response listener was started by [initialize] /// - Waits for success or error response, or time out after 10 secs /// - Make ssh tunnel connection using ephemeral keys diff --git a/packages/dart/noports_core/lib/src/sshnp/sshnp_core.dart b/packages/dart/noports_core/lib/src/sshnp/sshnp_core.dart index 0de4d3f25..b5e5acb0c 100644 --- a/packages/dart/noports_core/lib/src/sshnp/sshnp_core.dart +++ b/packages/dart/noports_core/lib/src/sshnp/sshnp_core.dart @@ -3,12 +3,14 @@ import 'dart:async'; import 'package:at_client/at_client.dart' hide StringBuffer; import 'package:at_utils/at_logger.dart'; import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/features.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/common/default_args.dart'; import 'package:noports_core/src/sshnp/util/sshnp_ssh_key_handler/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/src/sshnp/util/srvd_channel/srvd_channel.dart'; import 'package:noports_core/sshnp.dart'; import 'package:uuid/uuid.dart'; @@ -51,9 +53,9 @@ abstract class SshnpCore // * Communication Channels - /// The channel to communicate with the sshrvd (host) + /// The channel to communicate with the srvd (host) @protected - SshrvdChannel get sshrvdChannel; + SrvdChannel get srvdChannel; /// The channel to communicate with the sshnpd (daemon) @protected @@ -63,7 +65,7 @@ abstract class SshnpCore required this.atClient, required this.params, }) : sessionId = Uuid().v4(), - namespace = '${params.device}.sshnp', + namespace = '${params.device}.${DefaultArgs.namespace}', localPort = params.localPort { logger.level = params.verbose ? 'info' : 'shout'; @@ -83,6 +85,32 @@ abstract class SshnpCore /// Start the sshnpd payload handler await sshnpdChannel.callInitialization(); + if (params.discoverDaemonFeatures) { + late Map pingResponse; + try { + pingResponse = + await sshnpdChannel.ping().timeout(Duration(seconds: 10)); + } catch (e) { + logger.severe( + 'No ping response from ${params.device}${params.sshnpdAtSign}'); + rethrow; + } + + final daemonFeatures = pingResponse['supportedFeatures']; + if ((daemonFeatures[DaemonFeatures.srAuth.name] != true) && + (params.authenticateDeviceToRvd == true)) { + throw ArgumentError('This device daemon does not support' + ' authentication to the socket rendezvous.' + ' Please set --no-authenticate-device'); + } + if ((daemonFeatures[DaemonFeatures.srE2ee.name] != true) && + (params.encryptRvdTraffic == true)) { + throw ArgumentError('This device daemon does not support' + ' encryption of traffic to the socket rendezvous.' + ' Please set --no-encrypt-rvd-traffic'); + } + } + /// Set the remote username to use for the ssh session remoteUsername = await sshnpdChannel.resolveRemoteUsername(); @@ -93,8 +121,8 @@ abstract class SshnpCore /// Shares the public key if required await sshnpdChannel.sharePublicKeyIfRequired(identityKeyPair); - /// Retrieve the sshrvd host and port pair - await sshrvdChannel.callInitialization(); + /// Retrieve the srvd host and port pair + await srvdChannel.callInitialization(); } @override diff --git a/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/notification_request_message.dart b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/notification_request_message.dart new file mode 100644 index 000000000..7e5b69d5a --- /dev/null +++ b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/notification_request_message.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; + +class SocketRendezvousRequestMessage { + late String sessionId; + late String atSignA; + late String atSignB; + late bool authenticateSocketA; + late bool authenticateSocketB; + late String clientNonce; + + @override + String toString() { + Map m = {}; + m['sessionId'] = sessionId; + m['atSignA'] = atSignA; + m['atSignB'] = atSignB; + m['authenticateSocketA'] = authenticateSocketA; + m['authenticateSocketB'] = authenticateSocketB; + m['clientNonce'] = clientNonce; + return jsonEncode(m); + } +} diff --git a/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_channel.dart new file mode 100644 index 000000000..ef560bad9 --- /dev/null +++ b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_channel.dart @@ -0,0 +1,217 @@ +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/src/common/validation_utils.dart'; +import 'package:noports_core/src/sshnp/util/srvd_channel/notification_request_message.dart'; +import 'package:noports_core/sshnp.dart'; +import 'package:noports_core/srv.dart'; +import 'package:noports_core/srvd.dart'; + +@visibleForTesting +enum SrvdAck { + /// srvd acknowledged our request + acknowledged, + + /// srvd acknowledged our request and had errors + acknowledgedWithErrors, + + /// srvd did not acknowledge our request + notAcknowledged, +} + +abstract class SrvdChannel with AsyncInitialization, AtClientBindings { + @override + final logger = AtSignLogger(' SrvdChannel '); + + @override + final AtClient atClient; + + final SrvGenerator srvGenerator; + final SshnpParams params; + final String sessionId; + final String clientNonce = DateTime.now().toIso8601String(); + + // * Volatile fields which are set in [params] but may be overridden with + // * values provided by srvd + + String? _host; + int? _portA; + + String get host => _host ?? params.host; + + /// This is the port which the sshnp **client** will connect to + int get port => _portA ?? params.port; + + // * Volatile fields set at runtime + + String? rvdNonce; + String? sessionAESKeyString; + String? sessionIVString; + + /// Whether srvd acknowledged our request + @visibleForTesting + SrvdAck srvdAck = SrvdAck.notAcknowledged; + + /// The port srvd is listening on + int? _portB; + + /// This is the port which the sshnp **daemon** will connect to + int? get srvdPort => _portB; + + SrvdChannel({ + required this.atClient, + required this.params, + required this.sessionId, + required this.srvGenerator, + }) { + logger.level = params.verbose ? 'info' : 'shout'; + } + + @override + Future initialize() async { + if (params.host.startsWith('@')) { + await getHostAndPortFromSrvd(); + } else { + _host = params.host; + _portA = params.port; + } + completeInitialization(); + } + + Future runSrv({ + required bool directSsh, + int? localRvPort, + String? sessionAESKeyString, + String? sessionIVString, + }) async { + if (!directSsh && localRvPort != null) { + throw Exception( + 'localRvPort must be null when using reverseSsh (legacy)'); + } + if (directSsh && localRvPort == null) { + throw Exception( + 'localRvPort must be non-null when using directSsh (default)'); + } + await callInitialization(); + if (_portB == null) throw Exception('srvdPort is null'); + + // Connect to rendezvous point using background process. + // sshnp (this program) can then exit without issue. + + late Srv srv; + if (directSsh) { + srv = srvGenerator( + host, + _portA!, + localPort: localRvPort!, + bindLocalPort: true, + rvdAuthString: params.authenticateClientToRvd + ? signAndWrapAndJsonEncode(atClient, { + 'sessionId': sessionId, + 'clientNonce': clientNonce, + 'rvdNonce': rvdNonce, + }) + : null, + sessionAESKeyString: sessionAESKeyString, + sessionIVString: sessionIVString, + ); + } else { + // legacy behaviour + srv = srvGenerator( + host, + _portB!, + localPort: params.localSshdPort, + bindLocalPort: false, + ); + } + + return srv.run(); + } + + @protected + Future getHostAndPortFromSrvd() async { + srvdAck = SrvdAck.notAcknowledged; + subscribe(regex: '$sessionId.${Srvd.namespace}@', shouldDecrypt: true) + .listen((notification) async { + String ipPorts = notification.value.toString(); + logger.info('Received from srvd: $ipPorts'); + List results = ipPorts.split(','); + _host = results[0]; + _portA = int.parse(results[1]); + _portB = int.parse(results[2]); + if (results.length >= 4) { + rvdNonce = results[3]; + } + logger.info('Received from srvd:' + ' host:port $host:$port' + ' rvdNonce: $rvdNonce'); + logger.info('Set srvdPort to: $_portB'); + srvdAck = SrvdAck.acknowledged; + }); + logger.info('Started listening for srvd response'); + + late AtKey rvdRequestKey; + late String rvdRequestValue; + + if (params.authenticateClientToRvd || params.authenticateDeviceToRvd) { + rvdRequestKey = AtKey() + ..key = '${params.device}.request_ports.${Srvd.namespace}' + ..sharedBy = params.clientAtSign // shared by us + ..sharedWith = host // shared with the srvd host + ..metadata = (Metadata() + // as we are sending a notification to the srvd namespace, + // we don't want to append our namespace + ..namespaceAware = false + ..ttl = 10000); + + var message = SocketRendezvousRequestMessage(); + message.sessionId = sessionId; + message.atSignA = params.clientAtSign; + message.atSignB = params.sshnpdAtSign; + message.authenticateSocketA = params.authenticateClientToRvd; + message.authenticateSocketB = params.authenticateDeviceToRvd; + message.clientNonce = clientNonce; + + rvdRequestValue = message.toString(); + } else { + // send a legacy message since no new rvd features are being used + rvdRequestKey = AtKey() + ..key = '${params.device}.${Srvd.namespace}' + ..sharedBy = params.clientAtSign // shared by us + ..sharedWith = host // shared with the srvd host + ..metadata = (Metadata() + // as we are sending a notification to the srvd namespace, + // we don't want to append our namespace + ..namespaceAware = false + ..ttl = 10000); + + rvdRequestValue = sessionId; + } + + logger.info( + 'Sending notification to srvd with key $rvdRequestKey and value $rvdRequestValue'); + await notify( + rvdRequestKey, + rvdRequestValue, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + ); + + int counter = 1; + while (srvdAck == SrvdAck.notAcknowledged) { + if (counter % 20 == 0) { + logger.info('Still waiting for srvd response'); + } + await Future.delayed(Duration(milliseconds: 100)); + counter++; + if (counter > 150) { + logger.warning('Timed out waiting for srvd response'); + throw ('Connection timeout to srvd $host service\nhint: make sure host is valid and online'); + } + } + } +} diff --git a/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_dart_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_dart_channel.dart new file mode 100644 index 000000000..2d8a4512c --- /dev/null +++ b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_dart_channel.dart @@ -0,0 +1,11 @@ +import 'package:noports_core/src/sshnp/util/srvd_channel/srvd_channel.dart'; +import 'package:noports_core/srv.dart'; +import 'package:socket_connector/socket_connector.dart'; + +class SrvdDartChannel extends SrvdChannel { + SrvdDartChannel({ + required super.atClient, + required super.params, + required super.sessionId, + }) : super(srvGenerator: Srv.dart); +} diff --git a/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_exec_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_exec_channel.dart new file mode 100644 index 000000000..828cb87ea --- /dev/null +++ b/packages/dart/noports_core/lib/src/sshnp/util/srvd_channel/srvd_exec_channel.dart @@ -0,0 +1,11 @@ +import 'package:noports_core/src/common/io_types.dart'; +import 'package:noports_core/src/sshnp/util/srvd_channel/srvd_channel.dart'; +import 'package:noports_core/srv.dart'; + +class SrvdExecChannel extends SrvdChannel { + SrvdExecChannel({ + required super.atClient, + required super.params, + required super.sessionId, + }) : super(srvGenerator: Srv.exec); +} diff --git a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart index dd64a44e7..7078ac09d 100644 --- a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart +++ b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart @@ -19,22 +19,22 @@ mixin DartSshSessionHandler on SshnpCore @override Future startInitialTunnelSession( - {required String ephemeralKeyPairIdentifier}) async { - // If we are starting an initial tunnel, it should be to sshrvd, - // so it is safe to assume that sshrvdChannel is not null here + {required String ephemeralKeyPairIdentifier, int? localRvPort}) async { + // If we are starting an initial tunnel, it should be to srvd, + // so it is safe to assume that srvdChannel is not null here var username = tunnelUsername ?? getUserName(throwIfNull: true)!; logger.info('Starting tunnel ssh session as $username' - ' to ${sshrvdChannel.host} on port ${sshrvdChannel.sshrvdPort!}'); + ' to ${srvdChannel.host} on port ${srvdChannel.srvdPort!}'); AtSshKeyPair keyPair = await keyUtil.getKeyPair(identifier: ephemeralKeyPairIdentifier); SshClientHelper helper = SshClientHelper(logger); SSHClient tunnelSshClient = await helper.createSshClient( - host: sshrvdChannel.host, - port: sshrvdChannel.sshrvdPort!, + host: srvdChannel.host, + port: srvdChannel.srvdPort!, username: username, keyPair: keyPair, ); diff --git a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart index 3a6779172..80e8e144d 100644 --- a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart +++ b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/openssh_ssh_session_handler.dart @@ -11,13 +11,14 @@ mixin OpensshSshSessionHandler on SshnpCore @override Future startInitialTunnelSession({ required String ephemeralKeyPairIdentifier, + int? localRvPort, @visibleForTesting ProcessStarter startProcess = Process.start, }) 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 = '$tunnelUsername@${sshrvdChannel.host}' - ' -p ${sshrvdChannel.sshrvdPort}' + // If we are starting an initial tunnel, it should be to the local srv, + // so it is safe to assume that localRvPort is non-null + String argsString = '$tunnelUsername@localhost' + ' -p ${localRvPort!}' ' -i $ephemeralKeyPairIdentifier' ' -L $localPort:localhost:${params.remoteSshdPort}' ' -o LogLevel=VERBOSE' diff --git a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart index 5d7aa3d55..c5b66152b 100644 --- a/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart +++ b/packages/dart/noports_core/lib/src/sshnp/util/ssh_session_handler/ssh_session_handler.dart @@ -5,7 +5,7 @@ mixin SshSessionHandler { @protected @visibleForTesting Future startInitialTunnelSession( - {required String ephemeralKeyPairIdentifier}); + {required String ephemeralKeyPairIdentifier, int? localRvPort}); @protected @visibleForTesting diff --git a/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart index c33a0632e..f75eba87e 100644 --- a/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart +++ b/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_channel.dart @@ -62,7 +62,7 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { ).listen(handleSshnpdResponses); } - /// Main reponse handler for the daemon's notifications. + /// Main response handler for the daemon's notifications. @visibleForTesting Future handleSshnpdResponses(AtNotification notification) async { String notificationKey = notification.key @@ -86,17 +86,16 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { @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 { + /// Wait until we've received an acknowledgement from the daemon, or + /// have timed out while waiting. + Future waitForDaemonResponse({int maxWaitMillis = 15000}) async { // Timer to timeout after 10 Secs or after the Ack of connected/Errors for (int counter = 1; counter <= 100; counter++) { if (counter % 20 == 0) { logger.info('Still waiting for sshnpd response'); logger.info('sshnpdAck: $sshnpdAck'); } - await Future.delayed(Duration(milliseconds: 100)); + await Future.delayed(Duration(milliseconds: maxWaitMillis ~/ 100)); if (sshnpdAck != SshnpdAck.notAcknowledged) break; } return sshnpdAck; @@ -123,7 +122,8 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { try { logger.info('sharing ssh public key: $publicKeyContents'); // Check for Supported ssh keypairs from dartssh2 package - if (!publicKeyContents.startsWith(RegExp(r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { + if (!publicKeyContents.startsWith(RegExp( + r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { 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'); } @@ -132,7 +132,12 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { ..sharedBy = params.clientAtSign ..sharedWith = params.sshnpdAtSign ..metadata = (Metadata()..ttl = 10000); - await notify(sendOurPublicKeyToSshnpd, publicKeyContents); + await notify( + sendOurPublicKeyToSshnpd, + publicKeyContents, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + ); } catch (e, s) { throw SshnpError( 'Error opening or validating public key file or sending to remote atSign', @@ -171,13 +176,51 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { /// Otherwise, the username will be set to [remoteUsername] Future resolveTunnelUsername( {required String? remoteUsername}) async { - if (params.tunnelUsername != null) { + if (params.tunnelUsername != null && + params.tunnelUsername!.trim().isNotEmpty) { return params.tunnelUsername!; } else { return remoteUsername; } } + Future> ping() async { + Completer> completer = Completer(); + + subscribe( + regex: 'heartbeat' + '.${params.device}' + '.${DefaultArgs.namespace}', + shouldDecrypt: true, + ).listen((notification) { + logger.info( + 'Received ping response from ${notification.from} : ${notification.key} : ${notification.value}'); + if (notification.from == params.sshnpdAtSign) { + logger.info('Completing the future'); + completer.complete(jsonDecode(notification.value ?? '{}')); + } + }); + var pingKey = AtKey() + ..key = "ping.${params.device}" + ..sharedBy = params.clientAtSign + ..sharedWith = params.sshnpdAtSign + ..namespace = DefaultArgs.namespace + ..metadata = (Metadata() + ..isPublic = false + ..isEncrypted = true + ..namespaceAware = true); + + logger.info('Sending ping to sshnpd'); + await notify( + pingKey, + 'ping', + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + ); + + return completer.future; + } + /// 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 @@ -230,7 +273,12 @@ abstract class SshnpdChannel with AsyncInitialization, AtClientBindings { ..metadata = metaData; logger.info('Sending ping to sshnpd'); - unawaited(notify(pingKey, 'ping')); + unawaited(notify( + pingKey, + 'ping', + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + )); } // wait for 10 seconds in case any are being slow diff --git a/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart index 3444cc1e7..28ff5c2a4 100644 --- a/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart +++ b/packages/dart/noports_core/lib/src/sshnp/util/sshnpd_channel/sshnpd_default_channel.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart'; import 'package:meta/meta.dart'; import 'package:noports_core/src/common/io_types.dart'; @@ -18,6 +19,8 @@ class SshnpdDefaultChannel extends SshnpdChannel mixin SshnpdDefaultPayloadHandler on SshnpdChannel { String? ephemeralPrivateKey; + String? sessionAESKeyString; + String? sessionIVString; @visibleForTesting // disable publickey cache on windows @@ -66,8 +69,29 @@ mixin SshnpdDefaultPayloadHandler on SshnpdChannel { } logger.info('Verified signature of msg from ${params.sshnpdAtSign}'); - logger.info('Setting ephemeralPrivateKey'); + ephemeralPrivateKey = daemonResponse['ephemeralPrivateKey']; + logger.info('Received ephemeralPrivateKey: $ephemeralPrivateKey'); + + String? sessionAESKeyStringEncrypted = daemonResponse['sessionAESKey']; + logger.info( + 'Received encrypted sessionAESKey: $sessionAESKeyStringEncrypted'); + + String? sessionIVStringEncrypted = daemonResponse['sessionIV']; + logger.info('Received encrypted sessionIV: $sessionIVStringEncrypted'); + + if (sessionAESKeyStringEncrypted != null && + sessionIVStringEncrypted != null) { + AtChops atChops = + AtChopsImpl(AtChopsKeys.create(params.sessionKP, null)); + sessionAESKeyString = atChops + .decryptString(sessionAESKeyStringEncrypted, params.sessionKPType) + .result; + sessionIVString = atChops + .decryptString(sessionIVStringEncrypted, params.sessionKPType) + .result; + } + return SshnpdAck.acknowledged; } return SshnpdAck.acknowledgedWithErrors; diff --git a/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart deleted file mode 100644 index d2164083a..000000000 --- a/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart +++ /dev/null @@ -1,130 +0,0 @@ -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, - }) { - logger.level = params.verbose ? 'info' : 'shout'; - } - - @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( - host, - _sshrvdPort!, - localSshdPort: params.localSshdPort, - ); - return sshrv.run(); - } - - @protected - Future getHostAndPortFromSshrvd() async { - sshrvdAck = SshrvdAck.notAcknowledged; - 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 = 1; - while (sshrvdAck == SshrvdAck.notAcknowledged) { - if (counter % 20 == 0) { - logger.info('Still waiting for sshrvd response'); - } - 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/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart deleted file mode 100644 index 260b06e74..000000000 --- a/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_dart_channel.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:noports_core/src/sshnp/util/sshrvd_channel/sshrvd_channel.dart'; -import 'package:noports_core/sshrv.dart'; -import 'package:socket_connector/socket_connector.dart'; - -class SshrvdDartChannel extends SshrvdChannel { - SshrvdDartChannel({ - required super.atClient, - required super.params, - required super.sessionId, - }) : super(sshrvGenerator: Sshrv.dart); -} diff --git a/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart b/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart deleted file mode 100644 index 9f7932461..000000000 --- a/packages/dart/noports_core/lib/src/sshnp/util/sshrvd_channel/sshrvd_exec_channel.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:noports_core/src/common/io_types.dart'; -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/dart/noports_core/lib/src/sshnpd/sshnpd.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd.dart index a6ff10859..6459987c6 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd.dart @@ -10,7 +10,7 @@ import 'package:noports_core/src/sshnpd/sshnpd_params.dart'; abstract class Sshnpd { abstract final AtSignLogger logger; - /// The [AtClient] used to communicate with sshnpd and sshrvd + /// The [AtClient] used to communicate with sshnpd and srvd abstract AtClient atClient; // ==================================================================== diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart index 2969fd628..215f4b6f1 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart @@ -2,13 +2,15 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:at_chops/at_chops.dart'; import 'package:at_client/at_client.dart' hide StringBuffer; import 'package:at_utils/at_logger.dart'; import 'package:dartssh2/dartssh2.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:noports_core/src/common/features.dart'; import 'package:noports_core/src/common/openssh_binary_path.dart'; -import 'package:noports_core/src/sshrv/sshrv.dart'; +import 'package:noports_core/src/srv/srv.dart'; import 'package:noports_core/sshnpd.dart'; import 'package:noports_core/utils.dart'; import 'package:noports_core/src/version.dart'; @@ -164,13 +166,16 @@ class SshnpdImpl implements Sshnpd { try { await notificationService.notify( - NotificationParams.forUpdate(atKey, value: username), - waitForFinalDeliveryStatus: false, - checkForFinalDeliveryStatus: false, onSuccess: (notification) { - logger.info('SUCCESS:$notification $username'); - }, onError: (notification) { - logger.info('ERROR:$notification $username'); - }); + NotificationParams.forUpdate(atKey, value: username), + waitForFinalDeliveryStatus: false, + checkForFinalDeliveryStatus: false, + onSuccess: (notification) { + logger.info('SUCCESS:$notification $username'); + }, + onError: (notification) { + logger.info('ERROR:$notification $username'); + }, + ); } catch (e) { stderr.writeln(e.toString()); } @@ -298,13 +303,21 @@ class SshnpdImpl implements Sshnpd { ..namespaceAware = true); /// send a heartbeat back + var pingResponse = { + 'devicename': device, + 'version': packageVersion, + 'supportedFeatures': { + DaemonFeatures.srAuth.name: true, + DaemonFeatures.srE2ee.name: true, + DaemonFeatures.acceptsPublicKeys.name: addSshPublicKeys, + }, + }; unawaited( _notify( atKey: atKey, - value: jsonEncode({ - 'devicename': device, - 'version': packageVersion, - }), + value: jsonEncode(pingResponse), + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ), ); } @@ -329,9 +342,10 @@ class SshnpdImpl implements Sshnpd { 'ssh Public Key received from ${notification.from} notification id : ${notification.id}'); sshPublicKey = notification.value!; - // Check to see if the ssh public key is - // supported keys by the dartssh2 package - if (!sshPublicKey.startsWith(RegExp(r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { + // Check to see if the ssh public key is + // supported keys by the dartssh2 package + if (!sshPublicKey.startsWith(RegExp( + r'^(ecdsa-sha2-nistp)|(rsa-sha2-)|(ssh-rsa)|(ssh-ed25519)|(ecdsa-sha2-nistp)'))) { throw ('$sshPublicKey does not look like a public key'); } @@ -428,10 +442,17 @@ class SshnpdImpl implements Sshnpd { if (params['direct'] == true) { // direct ssh requested await startDirectSsh( - requestingAtsign: requestingAtsign, - sessionId: params['sessionId'], - host: params['host'], - port: params['port']); + requestingAtsign: requestingAtsign, + sessionId: params['sessionId'], + host: params['host'], + port: params['port'], + authenticateToRvd: params['authenticateToRvd'], + clientNonce: params['clientNonce'], + rvdNonce: params['rvdNonce'], + encryptRvdTraffic: params['encryptRvdTraffic'], + clientEphemeralPK: params['clientEphemeralPK'], + clientEphemeralPKType: params['clientEphemeralPKType'], + ); } else { // reverse ssh requested await startReverseSsh( @@ -480,7 +501,7 @@ class SshnpdImpl implements Sshnpd { remoteForwardPort: int.parse(remoteForwardPort)); } - /// - Starts an sshrv process bridging the rvd to localhost:$localSshdPort + /// - Starts an srv process bridging the rvd to localhost:$localSshdPort /// - Generates an ephemeral keypair and adds its public key to the /// `authorized_keys` file, limiting permissions (e.g. hosts and ports /// which can be forwarded to) as per the `--ephemeral-permissions` option @@ -488,19 +509,79 @@ class SshnpdImpl implements Sshnpd { /// ephemeral private key /// - starts a timer to remove the ephemeral key from `authorized_keys` /// after 15 seconds - Future startDirectSsh( - {required String requestingAtsign, - required String sessionId, - required String host, - required int port}) async { - logger.shout( + Future startDirectSsh({ + required String requestingAtsign, + required String sessionId, + required String host, + required int port, + required bool? authenticateToRvd, + required String? clientNonce, + required String? rvdNonce, + required bool? encryptRvdTraffic, + required String? clientEphemeralPK, + required String? clientEphemeralPKType, + }) async { + logger.info( 'Setting up ports for direct ssh session using ${sshClient.name} ($sshClient) from: $requestingAtsign session: $sessionId'); + authenticateToRvd ??= false; + encryptRvdTraffic ??= false; try { + String? rvdAuthString; + if (authenticateToRvd) { + rvdAuthString = signAndWrapAndJsonEncode(atClient, { + 'sessionId': sessionId, + 'clientNonce': clientNonce, + 'rvdNonce': rvdNonce, + }); + } + + String? sessionAESKey, sessionAESKeyEncrypted; + String? sessionIV, sessionIVEncrypted; + if (encryptRvdTraffic) { + if (clientEphemeralPK == null || clientEphemeralPKType == null) { + throw Exception( + 'encryptRvdTraffic was requested, but no client ephemeral public key / key type was provided'); + } + // 256-bit AES, 128-bit IV + sessionAESKey = + AtChopsUtil.generateSymmetricKey(EncryptionKeyType.aes256).key; + sessionIV = base64Encode(AtChopsUtil.generateRandomIV(16).ivBytes); + late EncryptionKeyType ect; + try { + ect = EncryptionKeyType.values.byName(clientEphemeralPKType); + } catch (e) { + throw Exception('Unknown ephemeralPKType: $clientEphemeralPKType'); + } + switch (ect) { + case EncryptionKeyType.rsa2048: + AtChops ac = AtChopsImpl(AtChopsKeys.create( + AtEncryptionKeyPair.create(clientEphemeralPK, 'n/a'), null)); + sessionAESKeyEncrypted = ac + .encryptString(sessionAESKey, + EncryptionKeyType.values.byName(clientEphemeralPKType)) + .result; + sessionIVEncrypted = ac + .encryptString(sessionIV, + EncryptionKeyType.values.byName(clientEphemeralPKType)) + .result; + break; + default: + throw Exception( + 'No handling for ephemeralPKType $clientEphemeralPKType'); + } + } // 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(); + Process rv = await Srv.exec( + host, + port, + localPort: localSshdPort, + bindLocalPort: false, + rvdAuthString: rvdAuthString, + sessionAESKeyString: sessionAESKey, + sessionIVString: sessionIV, + ).run(); logger.info('Started rv - pid is ${rv.pid}'); LocalSshKeyUtil keyUtil = LocalSshKeyUtil(); @@ -524,7 +605,11 @@ class SshnpdImpl implements Sshnpd { 'status': 'connected', 'sessionId': sessionId, 'ephemeralPrivateKey': keyPair.privateKeyContents, + 'sessionAESKey': sessionAESKeyEncrypted, + 'sessionIV': sessionIVEncrypted, }), + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, sessionId: sessionId, ); @@ -539,8 +624,10 @@ class SshnpdImpl implements Sshnpd { atKey: _createResponseAtKey( requestingAtsign: requestingAtsign, sessionId: sessionId), value: - 'Failed to start up the daemon side of the sshrv socket tunnel : $e', + 'Failed to start up the daemon side of the srv socket tunnel : $e', sessionId: sessionId, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); } } @@ -594,14 +681,19 @@ class SshnpdImpl implements Sshnpd { requestingAtsign: requestingAtsign, sessionId: sessionId), value: '$errorMessage (use --local-port to specify unused port)', sessionId: sessionId, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); } else { /// Notify sshnp that the connection has been made await _notify( - atKey: _createResponseAtKey( - requestingAtsign: requestingAtsign, sessionId: sessionId), - value: 'connected', - sessionId: sessionId); + atKey: _createResponseAtKey( + requestingAtsign: requestingAtsign, sessionId: sessionId), + value: 'connected', + sessionId: sessionId, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + ); } } catch (e) { logger.severe('SSH Client failure : $e'); @@ -611,6 +703,8 @@ class SshnpdImpl implements Sshnpd { requestingAtsign: requestingAtsign, sessionId: sessionId), value: 'Remote SSH Client failure : $e', sessionId: sessionId, + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, ); } } @@ -740,7 +834,7 @@ class SshnpdImpl implements Sshnpd { await Process.run('chmod', ['go-rwx', pemFile.absolute.path]); // When we receive notification 'sshd', WE are going to ssh to the host and port provided by sshnp - // which could be the host and port of a client machine, or the host and port of an sshrvd which is + // which could be the host and port of a client machine, or the host and port of an srvd which is // joined via socket connector to the client machine. Let's call it targetHostName/Port // // so: ssh username@targetHostName -p targetHostPort @@ -840,17 +934,24 @@ class SshnpdImpl implements Sshnpd { } /// This function sends a notification given an atKey and value - Future _notify( - {required AtKey atKey, - required String value, - String sessionId = ''}) async { - await atClient.notificationService - .notify(NotificationParams.forUpdate(atKey, value: value), - onSuccess: (notification) { - logger.info('SUCCESS:$notification for: $sessionId with value: $value'); - }, onError: (notification) { - logger.info('ERROR:$notification'); - }); + Future _notify({ + required AtKey atKey, + required String value, + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + String sessionId = '', + }) async { + await atClient.notificationService.notify( + NotificationParams.forUpdate(atKey, value: value), + checkForFinalDeliveryStatus: checkForFinalDeliveryStatus, + waitForFinalDeliveryStatus: waitForFinalDeliveryStatus, + onSuccess: (notification) { + logger.info('SUCCESS:$notification for: $sessionId with value: $value'); + }, + onError: (notification) { + logger.info('ERROR:$notification'); + }, + ); } /// This function creates an atKey which shares the device name with the client diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart index 1b46028e1..971fc5b1a 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart @@ -177,7 +177,8 @@ class SshnpdParams { parser.addOption( 'storage-path', mandatory: false, - help: r'Directory for local storage. Defaults to $HOME/.sshnp/${atSign}/storage', + help: + r'Directory for local storage. Defaults to $HOME/.sshnp/${atSign}/storage', ); return parser; diff --git a/packages/dart/noports_core/lib/src/sshrv/sshrv.dart b/packages/dart/noports_core/lib/src/sshrv/sshrv.dart deleted file mode 100644 index ffbc36ffc..000000000 --- a/packages/dart/noports_core/lib/src/sshrv/sshrv.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:io'; - -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 { - /// The internet address of the host to connect to. - abstract final String host; - - /// The port of the host to connect to. - abstract final int streamingPort; - - /// The local sshd port - /// Defaults to 22 - abstract final int localSshdPort; - - Future run(); - - // Can't use factory functions since SSHRV contains a generic type - static Sshrv exec( - String host, - int streamingPort, { - int localSshdPort = DefaultArgs.localSshdPort, - }) { - return SshrvImplExec( - host, - streamingPort, - localSshdPort: localSshdPort, - ); - } - - static Sshrv dart( - String host, - int streamingPort, { - int localSshdPort = 22, - }) { - return SshrvImplDart( - host, - streamingPort, - localSshdPort: localSshdPort, - ); - } - - static Future getLocalBinaryPath() async { - String postfix = Platform.isWindows ? '.exe' : ''; - List pathList = - Platform.resolvedExecutable.split(Platform.pathSeparator); - bool isExe = - (pathList.last == 'sshnp$postfix' || pathList.last == 'sshnpd$postfix'); - - pathList - ..removeLast() - ..add('sshrv$postfix'); - - File sshrvFile = File(pathList.join(Platform.pathSeparator)); - bool sshrvExists = await sshrvFile.exists(); - return (isExe && sshrvExists) ? sshrvFile.absolute.path : null; - } -} diff --git a/packages/dart/noports_core/lib/src/sshrv/sshrv_impl.dart b/packages/dart/noports_core/lib/src/sshrv/sshrv_impl.dart deleted file mode 100644 index f708708b4..000000000 --- a/packages/dart/noports_core/lib/src/sshrv/sshrv_impl.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:io'; - -import 'package:at_utils/at_utils.dart'; -import 'package:meta/meta.dart'; -import 'package:noports_core/sshrv.dart'; -import 'package:socket_connector/socket_connector.dart'; - -import 'package:noports_core/src/common/default_args.dart'; - -@visibleForTesting -class SshrvImplExec implements Sshrv { - @override - final String host; - - @override - final int streamingPort; - - @override - final int localSshdPort; - - const SshrvImplExec( - this.host, - this.streamingPort, { - this.localSshdPort = DefaultArgs.localSshdPort, - }); - - @override - Future run() async { - String? command = await Sshrv.getLocalBinaryPath(); - String postfix = Platform.isWindows ? '.exe' : ''; - if (command == null) { - throw Exception( - 'Unable to locate sshrv$postfix binary.\n' - 'N.B. sshnp is expected to be compiled and run from source, not via the dart command.', - ); - } - return Process.start( - command, - [host, streamingPort.toString(), localSshdPort.toString()], - mode: ProcessStartMode.detached, - ); - } -} - -@visibleForTesting -class SshrvImplDart implements Sshrv { - @override - final String host; - - @override - final int streamingPort; - - @override - final int localSshdPort; - - const SshrvImplDart( - this.host, - this.streamingPort, { - this.localSshdPort = 22, - }); - - @override - Future run() async { - try { - var hosts = await InternetAddress.lookup(host); - - return await SocketConnector.socketToSocket( - socketAddressA: InternetAddress.loopbackIPv4, - socketPortA: localSshdPort, - socketAddressB: hosts[0], - socketPortB: streamingPort, - verbose: true, - ); - } catch (e) { - AtSignLogger('sshrv').severe(e.toString()); - rethrow; - } - } -} diff --git a/packages/dart/noports_core/lib/src/sshrvd/build_env.dart b/packages/dart/noports_core/lib/src/sshrvd/build_env.dart deleted file mode 100644 index 4bbc87383..000000000 --- a/packages/dart/noports_core/lib/src/sshrvd/build_env.dart +++ /dev/null @@ -1,4 +0,0 @@ -class BuildEnv { - static final bool enableSnoop = - bool.fromEnvironment('ENABLE_SNOOP', defaultValue: false); -} diff --git a/packages/dart/noports_core/lib/src/sshrvd/socket_connector.dart b/packages/dart/noports_core/lib/src/sshrvd/socket_connector.dart deleted file mode 100644 index 2572d2df7..000000000 --- a/packages/dart/noports_core/lib/src/sshrvd/socket_connector.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; - -import 'package:at_utils/at_logger.dart'; -import 'package:socket_connector/socket_connector.dart'; - -typedef ConnectorParams = (SendPort, int, int, String, String, bool); -typedef PortPair = (int, int); - -final logger = AtSignLogger(' sshrvd / socket_connector '); - -/// This function is meant to be run in a separate isolate -/// It starts the socket connector, and sends back the assigned ports to the main isolate -/// It then waits for socket connector to die before shutting itself down -void socketConnector(ConnectorParams params) async { - var (sendPort, portA, portB, session, forAtsign, snoop) = params; - - logger.info('Starting socket connector session $session for $forAtsign'); - - /// Create the socket connector - SocketConnector socketStream = await SocketConnector.serverToServer( - serverAddressA: InternetAddress.anyIPv4, - serverAddressB: InternetAddress.anyIPv4, - serverPortA: portA, - serverPortB: portB, - verbose: snoop, - ); - - /// Get the assigned ports from the socket connector - portA = socketStream.senderPort()!; - portB = socketStream.receiverPort()!; - - logger.info('Assigned ports [$portA, $portB] for session $session'); - - /// Return the assigned ports to the main isolate - sendPort.send((portA, portB)); - - /// Shut myself down once the socket connector closes - bool closed = false; - while (closed == false) { - closed = await socketStream.closed(); - } - - logger.warning( - 'Finished session $session for $forAtsign using ports [$portA, $portB]'); - - Isolate.current.kill(); -} diff --git a/packages/dart/noports_core/lib/src/sshrvd/sshrvd_impl.dart b/packages/dart/noports_core/lib/src/sshrvd/sshrvd_impl.dart deleted file mode 100644 index 77eb9e834..000000000 --- a/packages/dart/noports_core/lib/src/sshrvd/sshrvd_impl.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:at_client/at_client.dart'; -import 'package:at_utils/at_logger.dart'; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:noports_core/src/sshrvd/build_env.dart'; -import 'package:noports_core/src/sshrvd/socket_connector.dart'; -import 'package:noports_core/src/sshrvd/sshrvd.dart'; -import 'package:noports_core/src/sshrvd/sshrvd_params.dart'; - -@protected -class SshrvdImpl implements Sshrvd { - @override - final AtSignLogger logger = AtSignLogger(' sshrvd '); - @override - AtClient atClient; - @override - final String atSign; - @override - final String homeDirectory; - @override - final String atKeysFilePath; - @override - final String managerAtsign; - @override - final String ipAddress; - @override - final bool snoop; - - @override - @visibleForTesting - bool initialized = false; - - SshrvdImpl({ - required this.atClient, - required this.atSign, - required this.homeDirectory, - required this.atKeysFilePath, - required this.managerAtsign, - required this.ipAddress, - required this.snoop, - }) { - logger.hierarchicalLoggingEnabled = true; - logger.logger.level = Level.SHOUT; - } - - static Future fromCommandLineArgs(List args, - {AtClient? atClient, - FutureOr Function(SshrvdParams)? atClientGenerator, - void Function(Object, StackTrace)? usageCallback}) async { - try { - var p = await SshrvdParams.fromArgs(args); - - if (!await File(p.atKeysFilePath).exists()) { - throw ('\n Unable to find .atKeys file : ${p.atKeysFilePath}'); - } - - AtSignLogger.root_level = 'SHOUT'; - if (p.verbose) { - AtSignLogger.root_level = 'INFO'; - } - - if (atClient == null && atClientGenerator == null) { - throw StateError('atClient and atClientGenerator are both null'); - } - - atClient ??= await atClientGenerator!(p); - - var sshrvd = SshrvdImpl( - atClient: atClient, - atSign: p.atSign, - homeDirectory: p.homeDirectory, - atKeysFilePath: p.atKeysFilePath, - managerAtsign: p.managerAtsign, - ipAddress: p.ipAddress, - snoop: p.snoop, - ); - - if (p.verbose) { - sshrvd.logger.logger.level = Level.INFO; - } - return sshrvd; - } catch (e, s) { - usageCallback?.call(e, s); - rethrow; - } - } - - @override - Future init() async { - if (initialized) { - throw StateError('Cannot init() - already initialized'); - } - - initialized = true; - } - - @override - Future run() async { - if (!initialized) { - throw StateError('Cannot run() - not initialized'); - } - NotificationService notificationService = atClient.notificationService; - - notificationService - .subscribe(regex: '${Sshrvd.namespace}@', shouldDecrypt: true) - .listen(_notificationHandler); - } - - void _notificationHandler(AtNotification notification) async { - if (!notification.key.contains(Sshrvd.namespace)) { - // ignore notifications not for this namespace - return; - } - - String session = notification.value!; - String forAtsign = notification.from; - - if (managerAtsign != 'open' && managerAtsign != forAtsign) { - logger.shout('Session $session for $forAtsign denied'); - return; - } - - (int, int) ports = - await _spawnSocketConnector(0, 0, session, forAtsign, snoop); - var (portA, portB) = ports; - logger - .warning('Starting session $session for $forAtsign using ports $ports'); - - var metaData = Metadata() - ..isPublic = false - ..isEncrypted = true - ..ttl = 10000 - ..namespaceAware = true; - - var atKey = AtKey() - ..key = notification.value - ..sharedBy = atSign - ..sharedWith = notification.from - ..namespace = Sshrvd.namespace - ..metadata = metaData; - - String data = '$ipAddress,$portA,$portB'; - - try { - await atClient.notificationService.notify( - NotificationParams.forUpdate(atKey, value: data), - waitForFinalDeliveryStatus: false, - checkForFinalDeliveryStatus: false); - } catch (e) { - stderr.writeln("Error writting session ${notification.value} atKey"); - } - } - - /// This function spawns a new socketConnector in a background isolate - /// once the socketConnector has spawned and is ready to accept connections - /// it sends back the port numbers to the main isolate - /// then the port numbers are returned from this function - Future _spawnSocketConnector( - int portA, - int portB, - String session, - String forAtsign, - bool snoop, - ) async { - /// Spawn an isolate and wait for it to send back the issued port numbers - ReceivePort receivePort = ReceivePort(session); - - ConnectorParams parameters = ( - receivePort.sendPort, - portA, - portB, - session, - forAtsign, - BuildEnv.enableSnoop && snoop, - ); - - logger - .info("Spawning socket connector isolate with parameters $parameters"); - - unawaited(Isolate.spawn(socketConnector, parameters)); - - PortPair ports = await receivePort.first; - - logger.info('Received ports $ports in main isolate for session $session'); - - return ports; - } -} diff --git a/packages/dart/noports_core/lib/src/version.dart b/packages/dart/noports_core/lib/src/version.dart index bb70bd214..d603e0da8 100644 --- a/packages/dart/noports_core/lib/src/version.dart +++ b/packages/dart/noports_core/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '5.0.4'; +const packageVersion = '5.1.0'; diff --git a/packages/dart/noports_core/lib/srv.dart b/packages/dart/noports_core/lib/srv.dart new file mode 100644 index 000000000..f2c30088c --- /dev/null +++ b/packages/dart/noports_core/lib/srv.dart @@ -0,0 +1,3 @@ +library noports_core_srv; + +export 'src/srv/srv.dart'; diff --git a/packages/dart/noports_core/lib/srvd.dart b/packages/dart/noports_core/lib/srvd.dart new file mode 100644 index 000000000..cec24ec15 --- /dev/null +++ b/packages/dart/noports_core/lib/srvd.dart @@ -0,0 +1,4 @@ +library noports_core_srvd; + +export 'src/srvd/srvd.dart'; +export 'src/srvd/srvd_params.dart'; diff --git a/packages/dart/noports_core/lib/sshnp_foundation.dart b/packages/dart/noports_core/lib/sshnp_foundation.dart index 59bedf3d3..fcdb2afba 100644 --- a/packages/dart/noports_core/lib/sshnp_foundation.dart +++ b/packages/dart/noports_core/lib/sshnp_foundation.dart @@ -20,9 +20,9 @@ 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/srvd_channel/srvd_channel.dart'; +export 'src/sshnp/util/srvd_channel/srvd_dart_channel.dart'; +export 'src/sshnp/util/srvd_channel/srvd_exec_channel.dart'; export 'src/sshnp/util/ssh_session_handler/ssh_session_handler.dart'; export 'src/sshnp/util/ssh_session_handler/dart_ssh_session_handler.dart'; diff --git a/packages/dart/noports_core/lib/sshrv.dart b/packages/dart/noports_core/lib/sshrv.dart deleted file mode 100644 index 3cfa5b3a9..000000000 --- a/packages/dart/noports_core/lib/sshrv.dart +++ /dev/null @@ -1,3 +0,0 @@ -library noports_core_sshrv; - -export 'src/sshrv/sshrv.dart'; diff --git a/packages/dart/noports_core/lib/sshrvd.dart b/packages/dart/noports_core/lib/sshrvd.dart deleted file mode 100644 index d31136bbb..000000000 --- a/packages/dart/noports_core/lib/sshrvd.dart +++ /dev/null @@ -1,4 +0,0 @@ -library noports_core_sshrvd; - -export 'src/sshrvd/sshrvd.dart'; -export 'src/sshrvd/sshrvd_params.dart'; diff --git a/packages/dart/noports_core/pubspec.yaml b/packages/dart/noports_core/pubspec.yaml index 7a960253d..99bdc2045 100644 --- a/packages/dart/noports_core/pubspec.yaml +++ b/packages/dart/noports_core/pubspec.yaml @@ -2,7 +2,7 @@ name: noports_core description: Core library code for sshnoports homepage: https://docs.atsign.com/ -version: 5.0.4 +version: 5.1.0 environment: sdk: ">=3.0.0 <4.0.0" @@ -10,7 +10,7 @@ environment: dependencies: args: ^2.4.2 at_chops: ^1.0.4 - at_client: ^3.0.65 + at_client: ^3.0.71 at_commons: ^3.0.56 at_utils: ^3.0.15 cryptography: ^2.7.0 @@ -24,6 +24,12 @@ dependencies: socket_connector: ^1.0.11 uuid: ^3.0.7 +dependency_overrides: + socket_connector: + git: + url: https://github.com/gkc/socket_connector.git + ref: socket-authenticator-option + dev_dependencies: build_runner: ^2.4.6 build_version: ^2.1.1 diff --git a/packages/dart/noports_core/test/srvd/notification_subscription_test.dart b/packages/dart/noports_core/test/srvd/notification_subscription_test.dart new file mode 100644 index 000000000..39d3a9e31 --- /dev/null +++ b/packages/dart/noports_core/test/srvd/notification_subscription_test.dart @@ -0,0 +1,17 @@ +import 'package:noports_core/src/srvd/srvd_impl.dart'; +import 'package:noports_core/srvd.dart'; +import 'package:test/test.dart'; + +void main() { + test('Test notification subscription regex', () { + expect( + RegExp(SrvdImpl.subscriptionRegex) + .hasMatch('jagan@test.${Srvd.namespace}@jagan'), + true); + expect(RegExp(SrvdImpl.subscriptionRegex).hasMatch('${Srvd.namespace}@'), + true); + expect( + RegExp(SrvdImpl.subscriptionRegex).hasMatch('${Srvd.namespace}.test@'), + false); + }); +} diff --git a/packages/dart/noports_core/test/srvd/signature_verifying_socket_authenticator_test.dart b/packages/dart/noports_core/test/srvd/signature_verifying_socket_authenticator_test.dart new file mode 100644 index 000000000..0292428c1 --- /dev/null +++ b/packages/dart/noports_core/test/srvd/signature_verifying_socket_authenticator_test.dart @@ -0,0 +1,175 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:at_chops/at_chops.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:noports_core/src/srvd/signature_verifying_socket_authenticator.dart'; +import 'package:test/test.dart'; +import 'package:uuid/uuid.dart'; + +void main() { + late AtChops atChops; + + setUpAll(() { + AtEncryptionKeyPair encryptionKeyPair = + AtChopsUtil.generateAtEncryptionKeyPair(keySize: 2048); + + atChops = AtChopsImpl(AtChopsKeys.create(encryptionKeyPair, null)); + }); + test('SignatureVerifyingSocketAuthenticator signature verification success', + () async { + String rvdSessionNonce = DateTime.now().toIso8601String(); + Map payload = {'sessionId': Uuid().v4(), 'rvdNonce': rvdSessionNonce}; + + late Function(Uint8List data) socketOnDataFn; + MockSocket mockSocket = MockSocket(); + + String signedEnvelope = signPayload(atChops, payload); + SignatureAuthVerifier sa = SignatureAuthVerifier( + atChops.atChopsKeys.atEncryptionKeyPair!.atPublicKey.publicKey, + jsonEncode(payload), // We'll verify the signature against this + rvdSessionNonce, + 'test_for_success'); + + List list = utf8.encode(signedEnvelope); + Uint8List data = Uint8List.fromList(list); + + when(() => mockSocket.listen(any(), + onError: any(named: "onError"), + onDone: any(named: "onDone"))).thenAnswer((Invocation invocation) { + socketOnDataFn = invocation.positionalArguments[0]; + + socketOnDataFn(data); + + return MockStreamSubscription(); + }); + + bool authenticated; + Stream? stream; + + (authenticated, stream) = await sa.authenticate(mockSocket); + expect(authenticated, true); + expect(stream, isNotNull); + }); + + test('SignatureVerifyingSocketAuthenticator signature verification failure', + () async { + String rvdSessionNonce = DateTime.now().toIso8601String(); + Map payload = { + 'sessionId': Uuid().v4().toString(), + 'rvdNonce': rvdSessionNonce + }; + + String signedEnvelope = signPayload(atChops, payload); + SignatureAuthVerifier sa = SignatureAuthVerifier( + atChops.atChopsKeys.atEncryptionKeyPair!.atPublicKey.publicKey, + // using a different payload; signature verification will fail + 'some other payload', + rvdSessionNonce, + 'test_for_failure'); + + List list = utf8.encode(signedEnvelope); + Uint8List data = Uint8List.fromList(list); + + late Function(Uint8List data) socketOnDataFn; + MockSocket mockSocket = MockSocket(); + + when(() => mockSocket.listen(any(), + onError: any(named: "onError"), + onDone: any(named: "onDone"))).thenAnswer((Invocation invocation) { + socketOnDataFn = invocation.positionalArguments[0]; + + socketOnDataFn(data); + + return MockStreamSubscription(); + }); + + bool somethingThrown = false; + try { + await sa.authenticate(mockSocket); + } catch (_) { + somethingThrown = true; + } + expect(somethingThrown, true); + }); + + test( + 'SignatureVerifyingSocketAuthenticator signature verification ok but mismatched nonce', + () async { + final uuidString = Uuid().v4().toString(); + String rvdSessionNonce = DateTime.now().toIso8601String(); + Map payload = {'sessionId': uuidString, 'rvdNonce': rvdSessionNonce}; + + String signedEnvelope = signPayload(atChops, payload); + SignatureAuthVerifier sa = SignatureAuthVerifier( + atChops.atChopsKeys.atEncryptionKeyPair!.atPublicKey.publicKey, + jsonEncode(payload), + rvdSessionNonce, + 'test_for_mismatch'); + + Map fakedEnvelope = jsonDecode(signedEnvelope); + fakedEnvelope['payload']['rvdNonce'] = 'not the same nonce'; + List list = utf8.encode(jsonEncode(fakedEnvelope)); + Uint8List data = Uint8List.fromList(list); + + late Function(Uint8List data) socketOnDataFn; + MockSocket mockSocket = MockSocket(); + + when(() => mockSocket.listen(any(), + onError: any(named: "onError"), + onDone: any(named: "onDone"))).thenAnswer((Invocation invocation) { + socketOnDataFn = invocation.positionalArguments[0]; + + socketOnDataFn(data); + + return MockStreamSubscription(); + }); + + bool somethingThrown = false; + try { + await sa.authenticate(mockSocket); + } catch (_) { + somethingThrown = true; + } + expect(somethingThrown, true); + }); +} + +String signPayload(AtChops atChops, Map payload) { + Map envelope = {'payload': payload}; + + final AtSigningInput signingInput = AtSigningInput(jsonEncode(payload)) + ..signingMode = AtSigningMode.data; + final AtSigningResult sr = atChops.sign(signingInput); + + final String signature = sr.result.toString(); + envelope['signature'] = signature; + envelope['hashingAlgo'] = sr.atSigningMetaData.hashingAlgoType!.name; + envelope['signingAlgo'] = sr.atSigningMetaData.signingAlgoType!.name; + return jsonEncode(envelope); +} + +bool verifySignature( + AtChops atChops, + String requestingAtsign, + Map envelope, +) { + final String signature = envelope['signature']; + Map payload = envelope['payload']; + final hashingAlgo = HashingAlgoType.values.byName(envelope['hashingAlgo']); + final signingAlgo = SigningAlgoType.values.byName(envelope['signingAlgo']); + final pk = atChops.atChopsKeys.atEncryptionKeyPair!.atPublicKey.publicKey; + AtSigningVerificationInput input = AtSigningVerificationInput( + jsonEncode(payload), base64Decode(signature), pk) + ..signingMode = AtSigningMode.data + ..signingAlgoType = signingAlgo + ..hashingAlgoType = hashingAlgo; + + AtSigningResult svr = atChops.verify(input); + return svr.result; +} + +class MockSocket extends Mock implements Socket {} + +class MockStreamSubscription extends Mock implements StreamSubscription {} diff --git a/packages/dart/noports_core/test/srvd/srvdutil_test.dart b/packages/dart/noports_core/test/srvd/srvdutil_test.dart new file mode 100644 index 000000000..1f0c53394 --- /dev/null +++ b/packages/dart/noports_core/test/srvd/srvdutil_test.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:at_client/at_client.dart'; +import 'package:noports_core/src/srvd/srvd_impl.dart'; +import 'package:noports_core/srvd.dart'; +import 'package:test/test.dart'; + +import '../sshnp/sshnp_mocks.dart'; + +void main() { + test('test notification subscription regex', () { + // Create a notification in rvd namespace + AtNotification notification = AtNotification.empty(); + notification.key = 'test.${Srvd.namespace}'; + }); + + test('srvd should accept notification in new request_ports format', () { + // Create a notification in rvd namespace + AtNotification notification = AtNotification.empty(); + notification.key = 'request_ports.test.${Srvd.namespace}'; + expect(SrvdUtil(MockAtClient()).accept(notification), true); + }); + + test( + 'srvd backwards compatibility test - should handle both legacy and new messages in JSON format', + () async { + Map m = {}; + m['session'] = 'hello'; + m['atSignA'] = '@4314sagittarius'; + m['atSignB'] = '@4314sagittarius'; + m['authenticateSocketA'] = false; + m['authenticateSocketB'] = false; + + // New message + AtNotification notification = AtNotification.empty(); + notification.key = 'request_ports.test.${Srvd.namespace}'; + notification.value = jsonEncode(m); + + expect(SrvdUtil(MockAtClient()).accept(notification), true); + }); +} diff --git a/packages/dart/noports_core/test/sshnp/models/sshnp_params_test.dart b/packages/dart/noports_core/test/sshnp/models/sshnp_params_test.dart index 2737ffe7c..7382d09ac 100644 --- a/packages/dart/noports_core/test/sshnp/models/sshnp_params_test.dart +++ b/packages/dart/noports_core/test/sshnp/models/sshnp_params_test.dart @@ -319,8 +319,8 @@ void main() { final params = SshnpParams.fromJson(json); expect(params.profileName, equals('myProfile')); - expect(params.clientAtSign, equals('@myClientAtSign')); - expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(params.host, equals('@myHost')); expect(params.device, equals('myDeviceName')); expect(params.port, equals(1234)); @@ -378,8 +378,8 @@ void main() { ]; final params = SshnpParams.fromConfigLines('myProfile', configLines); expect(params.profileName, equals('myProfile')); - expect(params.clientAtSign, equals('@myClientAtSign')); - expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(params.host, equals('@myHost')); expect(params.device, equals('myDeviceName')); expect(params.port, equals(1234)); @@ -426,8 +426,10 @@ void main() { final parsedParams = SshnpParams.fromConfigLines('myProfile', configLines); expect(parsedParams.profileName, equals('myProfile')); - expect(parsedParams.clientAtSign, equals('@myClientAtSign')); - expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect( + parsedParams.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect( + parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(parsedParams.host, equals('@myHost')); expect(parsedParams.device, equals('myDeviceName')); expect(parsedParams.port, equals(1234)); @@ -518,8 +520,10 @@ void main() { ); final json = params.toJson(); final parsedParams = SshnpParams.fromJson(json); - expect(parsedParams.clientAtSign, equals('@myClientAtSign')); - expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect( + parsedParams.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect( + parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(parsedParams.host, equals('@myHost')); expect(parsedParams.device, equals('myDeviceName')); expect(parsedParams.port, equals(1234)); @@ -822,8 +826,10 @@ void main() { final parsedParams = SshnpPartialParams.fromConfigLines('myProfile', configLines); expect(parsedParams.profileName, equals('myProfile')); - expect(parsedParams.clientAtSign, equals('@myClientAtSign')); - expect(parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect( + parsedParams.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect( + parsedParams.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(parsedParams.host, equals('@myHost')); expect(parsedParams.device, equals('myDeviceName')); expect(parsedParams.port, equals(1234)); @@ -865,8 +871,8 @@ void main() { final params = SshnpPartialParams.fromJson(json); expect(params.profileName, equals('myProfile')); - expect(params.clientAtSign, equals('@myClientAtSign')); - expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(params.host, equals('@myHost')); expect(params.device, equals('myDeviceName')); expect(params.port, equals(1234)); @@ -913,8 +919,8 @@ void main() { SshnpArg.sshAlgorithmArg.name: SupportedSshAlgorithm.rsa.toString(), }); expect(params.profileName, equals('myProfile')); - expect(params.clientAtSign, equals('@myClientAtSign')); - expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(params.host, equals('@myHost')); expect(params.device, equals('myDeviceName')); expect(params.port, equals(1234)); @@ -983,8 +989,8 @@ void main() { ]; final params = SshnpPartialParams.fromArgList(argList); expect(params.profileName, equals('myProfile')); - expect(params.clientAtSign, equals('@myClientAtSign')); - expect(params.sshnpdAtSign, equals('@mySshnpdAtSign')); + expect(params.clientAtSign, equals('@myClientAtSign'.toLowerCase())); + expect(params.sshnpdAtSign, equals('@mySshnpdAtSign'.toLowerCase())); expect(params.host, equals('@myHost')); expect(params.device, equals('myDeviceName')); expect(params.port, equals(1234)); diff --git a/packages/dart/noports_core/test/sshnp/sshnp_core_mocks.dart b/packages/dart/noports_core/test/sshnp/sshnp_core_mocks.dart index 3fdded77b..36bdca7ef 100644 --- a/packages/dart/noports_core/test/sshnp/sshnp_core_mocks.dart +++ b/packages/dart/noports_core/test/sshnp/sshnp_core_mocks.dart @@ -10,9 +10,9 @@ class StubbedSshnp extends SshnpCore with StubbedAsyncInitializationMixin { required super.atClient, required super.params, SshnpdChannel? sshnpdChannel, - SshrvdChannel? sshrvdChannel, + SrvdChannel? srvdChannel, }) : _sshnpdChannel = sshnpdChannel, - _sshrvdChannel = sshrvdChannel; + _srvdChannel = srvdChannel; @override Future initialize() async { @@ -37,9 +37,8 @@ class StubbedSshnp extends SshnpCore with StubbedAsyncInitializationMixin { final SshnpdChannel? _sshnpdChannel; @override - SshrvdChannel get sshrvdChannel => - _sshrvdChannel ?? (throw UnimplementedError()); - final SshrvdChannel? _sshrvdChannel; + SrvdChannel get srvdChannel => _srvdChannel ?? (throw UnimplementedError()); + final SrvdChannel? _srvdChannel; @override bool get canRunShell => throw UnimplementedError(); diff --git a/packages/dart/noports_core/test/sshnp/sshnp_core_test.dart b/packages/dart/noports_core/test/sshnp/sshnp_core_test.dart index a7726a33b..ad1d4f05b 100644 --- a/packages/dart/noports_core/test/sshnp/sshnp_core_test.dart +++ b/packages/dart/noports_core/test/sshnp/sshnp_core_test.dart @@ -14,7 +14,7 @@ void main() { late AtClient mockAtClient; late SshnpParams mockParams; late SshnpdChannel mockSshnpdChannel; - late SshrvdChannel mockSshrvdChannel; + late SrvdChannel mockSrvdChannel; /// Initialization stubs late FunctionStub stubbedCallInitialization; @@ -26,7 +26,7 @@ void main() { mockAtClient = MockAtClient(); mockParams = MockSshnpParams(); mockSshnpdChannel = MockSshnpdChannel(); - mockSshrvdChannel = MockSshrvdChannel(); + mockSrvdChannel = MockSrvdChannel(); registerFallbackValue(AtClientPreference()); /// Initialization @@ -37,9 +37,11 @@ void main() { /// When declaration setup for the constructor of [StubbedSshnp] whenConstructor({bool verbose = false}) { + when(() => mockParams.sshnpdAtSign).thenReturn('@sshnpd'); when(() => mockParams.device).thenReturn('mydevice'); when(() => mockParams.localPort).thenReturn(0); when(() => mockParams.verbose).thenReturn(verbose); + when(() => mockParams.discoverDaemonFeatures).thenReturn(false); when(() => mockAtClient.getPreferences()).thenReturn(null); when(() => mockAtClient.setPreferences(any())).thenReturn(null); } @@ -59,8 +61,7 @@ void main() { .thenAnswer((_) async => 'myTunnelUsername'); when(() => mockSshnpdChannel.sharePublicKeyIfRequired( identityKeyPair ?? any())).thenAnswer((_) async {}); - when(() => mockSshrvdChannel.callInitialization()) - .thenAnswer((_) async {}); + when(() => mockSrvdChannel.callInitialization()).thenAnswer((_) async {}); } group('Constructor', () { @@ -110,7 +111,7 @@ void main() { atClient: mockAtClient, params: mockParams, sshnpdChannel: mockSshnpdChannel, - sshrvdChannel: mockSshrvdChannel, + srvdChannel: mockSrvdChannel, ); /// Setup stubs for the mocks that are part of [MockAsyncInitializationMixin] @@ -140,7 +141,7 @@ void main() { remoteUsername: 'myRemoteUsername'), () => mockSshnpdChannel .sharePublicKeyIfRequired(sshnpCore.identityKeyPair), - () => mockSshrvdChannel.callInitialization(), + () => mockSrvdChannel.callInitialization(), () => stubbedCompleteInitialization(), ]); @@ -153,7 +154,7 @@ void main() { remoteUsername: 'myRemoteUsername')); verifyNever(() => mockSshnpdChannel .sharePublicKeyIfRequired(sshnpCore.identityKeyPair)); - verifyNever(() => mockSshrvdChannel.callInitialization()); + verifyNever(() => mockSrvdChannel.callInitialization()); verifyNever(() => stubbedCompleteInitialization()); /// Ensure [initialize()] is not ran a second time if we call @@ -162,7 +163,7 @@ void main() { verify(() => stubbedCallInitialization()).called(1); verifyNever(() => stubbedInitialize()); verifyNever(() => stubbedCompleteInitialization()); - verifyNever(() => mockSshrvdChannel.callInitialization()); + verifyNever(() => mockSrvdChannel.callInitialization()); }); test('tunnelUsername not supplied', () async { final params = SshnpParams( diff --git a/packages/dart/noports_core/test/sshnp/sshnp_mocks.dart b/packages/dart/noports_core/test/sshnp/sshnp_mocks.dart index 028d4c5d8..36e0b4330 100644 --- a/packages/dart/noports_core/test/sshnp/sshnp_mocks.dart +++ b/packages/dart/noports_core/test/sshnp/sshnp_mocks.dart @@ -12,7 +12,12 @@ abstract class FunctionCaller { class FunctionStub extends Mock implements FunctionCaller {} abstract class NotifyCaller { - Future call(AtKey key, String value); + Future call( + AtKey key, + String value, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + }); } class NotifyStub extends Mock implements NotifyCaller {} @@ -33,7 +38,7 @@ class MockSshnpParams extends Mock implements SshnpParams {} class MockSshnpdChannel extends Mock implements SshnpdChannel {} -class MockSshrvdChannel extends Mock implements SshrvdChannel {} +class MockSrvdChannel extends Mock implements SrvdChannel {} /// [dart:io] Mocks class MockProcess extends Mock implements Process {} diff --git a/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_mocks.dart b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_mocks.dart new file mode 100644 index 000000000..54711f253 --- /dev/null +++ b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_mocks.dart @@ -0,0 +1,73 @@ +import 'package:at_client/at_client.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:noports_core/sshnp_foundation.dart'; +import 'package:noports_core/srv.dart'; + +/// Stubbing for [SrvGenerator] typedef +abstract class SrvGeneratorCaller { + Srv call( + String host, + int port, { + required int localPort, + required bool bindLocalPort, + String? rvdAuthString, + String? sessionAESKeyString, + String? sessionIVString, + }); +} + +class SrvGeneratorStub extends Mock implements SrvGeneratorCaller {} + +class MockSrv extends Mock implements Srv {} + +/// Stubbed [SrvdChannel] which we are testing +class StubbedSrvdChannel extends SrvdChannel { + final Future Function( + AtKey, + String, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + })? _notify; + final Stream Function({String? regex, bool shouldDecrypt})? + _subscribe; + + StubbedSrvdChannel({ + required super.atClient, + required super.params, + required super.sessionId, + required super.srvGenerator, + Future Function( + AtKey, + String, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + })? notify, + Stream Function({String? regex, bool shouldDecrypt})? + subscribe, + }) : _notify = notify, + _subscribe = subscribe; + + @override + Future notify( + AtKey atKey, + String value, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + }) async { + return _notify?.call( + atKey, + value, + checkForFinalDeliveryStatus: checkForFinalDeliveryStatus, + waitForFinalDeliveryStatus: waitForFinalDeliveryStatus, + ); + } + + @override + Stream subscribe({ + String? regex, + bool shouldDecrypt = false, + }) { + return _subscribe?.call(regex: regex, shouldDecrypt: shouldDecrypt) ?? + Stream.empty(); + } +} diff --git a/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart new file mode 100644 index 000000000..20d6952c4 --- /dev/null +++ b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart @@ -0,0 +1,237 @@ +import 'dart:async'; + +import 'package:at_chops/at_chops.dart'; +import 'package:at_client/at_client.dart'; +import 'package:at_utils/at_utils.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:noports_core/sshnp_foundation.dart'; +import 'package:noports_core/srv.dart'; +import 'package:noports_core/srvd.dart'; +import 'package:test/test.dart'; +import 'package:uuid/uuid.dart'; + +import '../../sshnp_mocks.dart'; +import 'srvd_channel_mocks.dart'; + +void main() { + group('SrvdChannel', () { + late SrvGeneratorStub srvGeneratorStub; + late MockAtClient mockAtClient; + late StreamController notificationStreamController; + late NotifyStub notifyStub; + late SubscribeStub subscribeStub; + late MockSshnpParams mockParams; + late String sessionId; + late StubbedSrvdChannel stubbedSrvdChannel; + late MockSrv mockSrv; + + // Invocation patterns as closures so they can be referred to by name + // instead of explicitly writing these calls several times in the test + notifyInvocation() => notifyStub( + any(), + any(), + checkForFinalDeliveryStatus: + any(named: 'checkForFinalDeliveryStatus'), + waitForFinalDeliveryStatus: any(named: 'waitForFinalDeliveryStatus'), + ); + subscribeInvocation() => subscribeStub( + regex: any(named: 'regex'), + shouldDecrypt: any(named: 'shouldDecrypt'), + ); + srvGeneratorInvocation() => srvGeneratorStub(any(), any(), + localPort: any(named: 'localPort'), + bindLocalPort: any(named: 'bindLocalPort'), + rvdAuthString: any(named: 'rvdAuthString')); + srvRunInvocation() => mockSrv.run(); + + setUp(() { + srvGeneratorStub = SrvGeneratorStub(); + mockAtClient = MockAtClient(); + notificationStreamController = StreamController(); + notifyStub = NotifyStub(); + subscribeStub = SubscribeStub(); + mockParams = MockSshnpParams(); + when(() => mockParams.verbose).thenReturn(false); + sessionId = Uuid().v4(); + mockSrv = MockSrv(); + + stubbedSrvdChannel = StubbedSrvdChannel( + atClient: mockAtClient, + params: mockParams, + sessionId: sessionId, + srvGenerator: srvGeneratorStub, + notify: notifyStub, + subscribe: subscribeStub, + ); + + registerFallbackValue(AtKey()); + registerFallbackValue(NotificationParams.forUpdate(AtKey())); + + // Create an AtChops instance for testing + AtEncryptionKeyPair encryptionKeyPair = + AtChopsUtil.generateAtEncryptionKeyPair(); + + AtChops atChops = AtChopsImpl( + AtChopsKeys.create(encryptionKeyPair, null), + ); + + when(() => mockAtClient.atChops).thenReturn(atChops); + }); + + test('public API', () { + // This doesn't cover the full public API, but it covers all of the public + // members which do not need further tests + + // Base type + expect(stubbedSrvdChannel, isA>()); + expect(stubbedSrvdChannel, isA()); + expect(stubbedSrvdChannel, isA()); + + // final params + expect(stubbedSrvdChannel.logger, isA()); + expect( + stubbedSrvdChannel.srvGenerator, + isA< + Srv Function(String, int, + {required int localPort, + required bool bindLocalPort, + String? rvdAuthString})>(), + ); + expect(stubbedSrvdChannel.atClient, mockAtClient); + expect(stubbedSrvdChannel.params, mockParams); + expect(stubbedSrvdChannel.sessionId, sessionId); + }); // test public API + + whenInitializationWithSrvdHost() { + when(() => mockParams.host).thenReturn('@srvd'); + when(() => mockParams.device).thenReturn('mydevice'); + when(() => mockParams.clientAtSign).thenReturn('@client'); + when(() => mockParams.sshnpdAtSign).thenReturn('@sshnpd'); + when(() => mockParams.authenticateDeviceToRvd).thenReturn(true); + when(() => mockParams.authenticateClientToRvd).thenReturn(true); + when(() => mockParams.encryptRvdTraffic).thenReturn(true); + when(() => mockParams.discoverDaemonFeatures).thenReturn(false); + + when(subscribeInvocation) + .thenAnswer((_) => notificationStreamController.stream); + + when(notifyInvocation).thenAnswer( + (_) async { + final testIp = '123.123.123.123'; + final portA = 10456; + final portB = 10789; + final rvdSessionNonce = DateTime.now().toIso8601String(); + + notificationStreamController.add( + AtNotification.empty() + ..id = Uuid().v4() + ..key = '$sessionId.${Srvd.namespace}' + ..from = '@srvd' + ..to = '@client' + ..epochMillis = DateTime.now().millisecondsSinceEpoch + ..value = '$testIp,$portA,$portB,$rvdSessionNonce', + ); + }, + ); + } + + test('Initialization - srvd host', () async { + /// Set the required parameters + whenInitializationWithSrvdHost(); + expect(stubbedSrvdChannel.srvdAck, SrvdAck.notAcknowledged); + expect(stubbedSrvdChannel.initializeStarted, false); + + verifyNever(subscribeInvocation); + verifyNever(notifyInvocation); + + await expectLater(stubbedSrvdChannel.initialize(), completes); + + verifyInOrder([ + () => subscribeStub( + regex: '$sessionId.${Srvd.namespace}@', shouldDecrypt: true), + () => notifyStub( + any( + that: predicate( + // Predicate matching specifically the srvdIdKey format + (AtKey key) => + key.key == 'mydevice.request_ports.${Srvd.namespace}' && + key.sharedBy == '@client' && + key.sharedWith == '@srvd' && + key.metadata != null && + key.metadata!.namespaceAware == false && + key.metadata!.ttl == 10000, + ), + ), + any(), + checkForFinalDeliveryStatus: + any(named: 'checkForFinalDeliveryStatus'), + waitForFinalDeliveryStatus: + any(named: 'waitForFinalDeliveryStatus'), + ), + ]); + + verifyNever(subscribeInvocation); + verifyNever(notifyInvocation); + + expect(stubbedSrvdChannel.srvdAck, SrvdAck.acknowledged); + expect(stubbedSrvdChannel.host, '123.123.123.123'); + expect(stubbedSrvdChannel.port, 10456); + expect(stubbedSrvdChannel.srvdPort, 10789); + }); // test Initialization - srvd host + + test('Initialization - non-srvd host', () async { + when(() => mockParams.host).thenReturn('234.234.234.234'); + when(() => mockParams.port).thenReturn(135); + + await expectLater(stubbedSrvdChannel.initialize(), completes); + + expect(stubbedSrvdChannel.host, '234.234.234.234'); + expect(stubbedSrvdChannel.port, 135); + }); // test Initialization - non-srvd host + + test('Initialization completes - srvd host', () async { + /// Set the required parameters + whenInitializationWithSrvdHost(); + await expectLater(stubbedSrvdChannel.callInitialization(), completes); + await expectLater(stubbedSrvdChannel.initialized, completes); + }); + + test('Initialization completes - non-srvd host', () async { + when(() => mockParams.host).thenReturn('234.234.234.234'); + when(() => mockParams.port).thenReturn(135); + + await expectLater(stubbedSrvdChannel.callInitialization(), completes); + await expectLater(stubbedSrvdChannel.initialized, completes); + }); // test Initialization - non-srvd host + + test('runSrv', () async { + whenInitializationWithSrvdHost(); + + await expectLater(stubbedSrvdChannel.callInitialization(), completes); + expect(stubbedSrvdChannel.srvdAck, SrvdAck.acknowledged); + await expectLater(stubbedSrvdChannel.initialized, completes); + // Initialization should be complete + // Begin test for [runSrv()] + + when(() => mockParams.localSshdPort).thenReturn(23); + when(srvGeneratorInvocation).thenReturn(mockSrv); + when(srvRunInvocation).thenAnswer((_) async => 'called srv run'); + + verifyNever(srvGeneratorInvocation); + verifyNever(srvRunInvocation); + + await expectLater( + await stubbedSrvdChannel.runSrv(directSsh: false), + 'called srv run', + ); + + verifyInOrder([ + srvGeneratorInvocation, + srvRunInvocation, + ]); + + verifyNever(srvGeneratorInvocation); + verifyNever(srvRunInvocation); + }); // test runSrv + }); // group SrvdChannel +} diff --git a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_dart_channel_test.dart similarity index 83% rename from packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart rename to packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_dart_channel_test.dart index a8d558bc2..1e7ad5662 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_dart_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_dart_channel_test.dart @@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart'; import '../../sshnp_mocks.dart'; void main() { - group('SshrvdDartChannel', () { + group('SrvdDartChannel', () { late MockAtClient mockAtClient; late MockSshnpParams mockSshnpParams; late String sessionId; @@ -20,13 +20,13 @@ void main() { }); test('public API', () { expect( - SshrvdDartChannel( + SrvdDartChannel( atClient: mockAtClient, params: mockSshnpParams, sessionId: sessionId, ), - isA>(), + isA>(), ); }); - }); // group SshrvdDartChannel + }); // group SrvdDartChannel } diff --git a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_exec_channel_test.dart similarity index 84% rename from packages/dart/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart rename to packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_exec_channel_test.dart index 6fd0a1965..822c4dedc 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/ssrhvd_exec_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_exec_channel_test.dart @@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart'; import '../../sshnp_mocks.dart'; void main() { - group('SshrvdExecChannel', () { + group('SrvdExecChannel', () { late MockAtClient mockAtClient; late MockSshnpParams mockSshnpParams; late String sessionId; @@ -21,13 +21,13 @@ void main() { }); test('public API', () { expect( - SshrvdExecChannel( + SrvdExecChannel( atClient: mockAtClient, params: mockSshnpParams, sessionId: sessionId, ), - isA>(), + isA>(), ); }); - }); // group SshrvdDartChannel + }); // group SrvdDartChannel } diff --git a/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart b/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart index 7fd24ec82..8e125c214 100644 --- a/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart +++ b/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_mocks.dart @@ -27,11 +27,13 @@ mixin StubbedSshnpOpensshSshSessionHandler on OpensshSshSessionHandler { @override Future startInitialTunnelSession({ required String ephemeralKeyPairIdentifier, + int? localRvPort, ProcessStarter startProcess = Process.start, }) { _stubbedStartInitialTunnel(); return super.startInitialTunnelSession( ephemeralKeyPairIdentifier: ephemeralKeyPairIdentifier, + localRvPort: localRvPort, startProcess: _stubbedStartProcess.call, ); } @@ -44,9 +46,9 @@ class StubbedSshnp extends SshnpCore required super.atClient, required super.params, required SshnpdChannel sshnpdChannel, - required SshrvdChannel sshrvdChannel, + required SrvdChannel srvdChannel, }) : _sshnpdChannel = sshnpdChannel, - _sshrvdChannel = sshrvdChannel; + _srvdChannel = srvdChannel; @override AtSshKeyPair? get identityKeyPair => throw UnimplementedError(); @@ -62,8 +64,8 @@ class StubbedSshnp extends SshnpCore final SshnpdChannel _sshnpdChannel; @override - SshrvdChannel get sshrvdChannel => _sshrvdChannel; - final SshrvdChannel _sshrvdChannel; + SrvdChannel get srvdChannel => _srvdChannel; + final SrvdChannel _srvdChannel; @override Future startUserSession({required Process? tunnelSession}) { diff --git a/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart b/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart index 4734e9bdd..0561788ee 100644 --- a/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/ssh_session_handler/openssh_ssh_session_handler_test.dart @@ -13,14 +13,14 @@ void main() { late MockAtClient mockAtClient; late MockSshnpParams mockParams; late MockSshnpdChannel mockSshnpChannel; - late MockSshrvdChannel mockSshrvdChannel; + late MockSrvdChannel mockSrvdChannel; late StubbedSshnp stubbedSshnp; setUp(() { mockAtClient = MockAtClient(); mockParams = MockSshnpParams(); mockSshnpChannel = MockSshnpdChannel(); - mockSshrvdChannel = MockSshrvdChannel(); + mockSrvdChannel = MockSrvdChannel(); // Mocked SshnpCore Constructor calls registerFallbackValue(AtClientPreference()); @@ -34,11 +34,11 @@ void main() { atClient: mockAtClient, params: mockParams, sshnpdChannel: mockSshnpChannel, - sshrvdChannel: mockSshrvdChannel, + srvdChannel: mockSrvdChannel, ); // Mocked SshnpCore Initialization calls - // TODO sshrvd channel mock calls + // TODO srvd channel mock calls // TODO sshnpd channel mock calls }); diff --git a/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_local_ssh_key_handler_test.dart b/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_local_ssh_key_handler_test.dart index 95df64a6a..32383b860 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_local_ssh_key_handler_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_local_ssh_key_handler_test.dart @@ -14,7 +14,7 @@ void main() { late MockAtSshKeyPair keyPair; late MockSshnpdChannel mockSshnpdChannel; - late MockSshrvdChannel mockSshrvdChannel; + late MockSrvdChannel mockSrvdChannel; setUp(() { mockAtClient = MockAtClient(); @@ -23,14 +23,16 @@ void main() { keyPair = MockAtSshKeyPair(); mockSshnpdChannel = MockSshnpdChannel(); - mockSshrvdChannel = MockSshrvdChannel(); + mockSrvdChannel = MockSrvdChannel(); registerFallbackValue(AtClientPreference()); }); whenConstructor() { + when(() => mockParams.sshnpdAtSign).thenReturn('@sshnpd'); when(() => mockParams.device).thenReturn('mydevice'); when(() => mockParams.localPort).thenReturn(0); when(() => mockParams.verbose).thenReturn(false); + when(() => mockParams.discoverDaemonFeatures).thenReturn(false); when(() => mockAtClient.getPreferences()).thenReturn(null); when(() => mockAtClient.setPreferences(any())).thenReturn(null); } @@ -45,8 +47,7 @@ void main() { .thenAnswer((_) async => 'myTunnelUsername'); when(() => mockSshnpdChannel.sharePublicKeyIfRequired(identityKeyPair)) .thenAnswer((_) async {}); - when(() => mockSshrvdChannel.callInitialization()) - .thenAnswer((_) async {}); + when(() => mockSrvdChannel.callInitialization()).thenAnswer((_) async {}); } test('public API', () { @@ -66,7 +67,7 @@ void main() { params: mockParams, sshKeyUtil: keyUtil, sshnpdChannel: mockSshnpdChannel, - sshrvdChannel: mockSshrvdChannel, + srvdChannel: mockSrvdChannel, ); whenInitialization(identityKeyPair: keyPair); @@ -94,7 +95,7 @@ void main() { params: mockParams, sshKeyUtil: keyUtil, sshnpdChannel: mockSshnpdChannel, - sshrvdChannel: mockSshrvdChannel, + srvdChannel: mockSrvdChannel, ); whenInitialization(identityKeyPair: keyPair); @@ -123,7 +124,7 @@ void main() { params: mockParams, sshKeyUtil: keyUtil, sshnpdChannel: mockSshnpdChannel, - sshrvdChannel: mockSshrvdChannel, + srvdChannel: mockSrvdChannel, ); whenInitialization(); diff --git a/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart b/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart index d95f72988..16df1e958 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnp_ssh_key_handler/sshnp_ssh_key_handler_mocks.dart @@ -19,10 +19,10 @@ class StubbedSshnp extends SshnpCore with SshnpLocalSshKeyHandler { required super.params, LocalSshKeyUtil? sshKeyUtil, SshnpdChannel? sshnpdChannel, - SshrvdChannel? sshrvdChannel, + SrvdChannel? srvdChannel, }) : _sshKeyUtil = sshKeyUtil, _sshnpdChannel = sshnpdChannel, - _sshrvdChannel = sshrvdChannel; + _srvdChannel = srvdChannel; @override Future run() => throw UnimplementedError(); @@ -33,9 +33,8 @@ class StubbedSshnp extends SshnpCore with SshnpLocalSshKeyHandler { final SshnpdChannel? _sshnpdChannel; @override - SshrvdChannel get sshrvdChannel => - _sshrvdChannel ?? (throw UnimplementedError()); - final SshrvdChannel? _sshrvdChannel; + SrvdChannel get srvdChannel => _srvdChannel ?? (throw UnimplementedError()); + final SrvdChannel? _srvdChannel; @override bool get canRunShell => false; diff --git a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_mocks.dart b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_mocks.dart index e74fc6899..328cdee86 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_mocks.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_mocks.dart @@ -10,7 +10,12 @@ class HandleSshnpdPayloadStub extends Mock implements HandleSshnpdPayloadCaller {} class StubbedSshnpdChannel extends SshnpdChannel { - final Future Function(AtKey, String)? _notify; + final Future Function( + AtKey, + String, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + })? _notify; final Stream Function({String? regex, bool shouldDecrypt})? _subscribe; final Future Function(AtNotification notification)? @@ -21,7 +26,12 @@ class StubbedSshnpdChannel extends SshnpdChannel { required super.params, required super.sessionId, required super.namespace, - Future Function(AtKey, String)? notify, + Future Function( + AtKey, + String, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + })? notify, Stream Function({String? regex, bool shouldDecrypt})? subscribe, Future Function(AtNotification notification)? @@ -39,9 +49,16 @@ class StubbedSshnpdChannel extends SshnpdChannel { @override Future notify( AtKey atKey, - String value, - ) async { - return _notify?.call(atKey, value); + String value, { + required bool checkForFinalDeliveryStatus, + required bool waitForFinalDeliveryStatus, + }) async { + return _notify?.call( + atKey, + value, + checkForFinalDeliveryStatus: checkForFinalDeliveryStatus, + waitForFinalDeliveryStatus: waitForFinalDeliveryStatus, + ); } @override diff --git a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart index b20a5ed76..9472f5759 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart @@ -26,7 +26,13 @@ void main() { // Invocation patterns as closures so they can be referred to by name // instead of explicitly writing these calls several times in the test - notifyInvocation() => notifyStub(any(), any()); + notifyInvocation() => notifyStub( + any(), + any(), + checkForFinalDeliveryStatus: + any(named: 'checkForFinalDeliveryStatus'), + waitForFinalDeliveryStatus: any(named: 'waitForFinalDeliveryStatus'), + ); subscribeInvocation() => subscribeStub( regex: any(named: 'regex'), shouldDecrypt: any(named: 'shouldDecrypt'), @@ -167,7 +173,8 @@ void main() { when(payloadInvocation) .thenAnswer((_) async => SshnpdAck.notAcknowledged); - Future ack = stubbedSshnpdChannel.waitForDaemonResponse(); + Future ack = + stubbedSshnpdChannel.waitForDaemonResponse(maxWaitMillis: 300); // manually add a notification to the stream final String notificationId = Uuid().v4(); @@ -212,6 +219,10 @@ void main() { any( that: predicate((AtKey key) => key.key == 'sshpublickey')), any(), + checkForFinalDeliveryStatus: + any(named: 'checkForFinalDeliveryStatus'), + waitForFinalDeliveryStatus: + any(named: 'waitForFinalDeliveryStatus'), ), ).thenAnswer((_) async {}); @@ -227,6 +238,10 @@ void main() { any( that: predicate((AtKey key) => key.key == 'sshpublickey')), TestingKeyPair.public, + checkForFinalDeliveryStatus: + any(named: 'checkForFinalDeliveryStatus'), + waitForFinalDeliveryStatus: + any(named: 'waitForFinalDeliveryStatus'), ), ).called(1); }); // test sharePublicKeyIfRequired diff --git a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart index d342d7932..284009929 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart @@ -67,6 +67,10 @@ void main() { whenInitialization() { when(() => mockParams.sshnpdAtSign).thenReturn('@sshnpd'); + when(() => mockParams.authenticateDeviceToRvd).thenReturn(true); + when(() => mockParams.authenticateClientToRvd).thenReturn(true); + when(() => mockParams.encryptRvdTraffic).thenReturn(true); + when(() => mockParams.discoverDaemonFeatures).thenReturn(false); when(subscribeInvocation) .thenAnswer((_) => notificationStreamController.stream); } diff --git a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_mocks.dart b/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_mocks.dart deleted file mode 100644 index a9646eec8..000000000 --- a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_mocks.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:at_client/at_client.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:noports_core/sshnp_foundation.dart'; -import 'package:noports_core/sshrv.dart'; - -/// Stubbing for [SshrvGenerator] typedef -abstract class SshrvGeneratorCaller { - Sshrv call(String host, int port, {int localSshdPort}); -} - -class SshrvGeneratorStub extends Mock implements SshrvGeneratorCaller {} - -class MockSshrv extends Mock implements Sshrv {} - -/// Stubbed [SshrvdChannel] which we are testing -class StubbedSshrvdChannel extends SshrvdChannel { - final Future Function(AtKey, String)? _notify; - final Stream Function({String? regex, bool shouldDecrypt})? - _subscribe; - StubbedSshrvdChannel({ - required super.atClient, - required super.params, - required super.sessionId, - required super.sshrvGenerator, - Future Function(AtKey, String)? notify, - Stream Function({String? regex, bool shouldDecrypt})? - subscribe, - }) : _notify = notify, - _subscribe = subscribe; - - @override - Future notify( - AtKey atKey, - String value, - ) async { - return _notify?.call(atKey, value); - } - - @override - Stream subscribe({ - String? regex, - bool shouldDecrypt = false, - }) { - return _subscribe?.call(regex: regex, shouldDecrypt: shouldDecrypt) ?? - Stream.empty(); - } -} diff --git a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart deleted file mode 100644 index 9b460d318..000000000 --- a/packages/dart/noports_core/test/sshnp/util/sshrvd_channel/sshrvd_channel_test.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'dart:async'; - -import 'package:at_client/at_client.dart'; -import 'package:at_utils/at_utils.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:noports_core/sshnp_foundation.dart'; -import 'package:noports_core/sshrv.dart'; -import 'package:noports_core/sshrvd.dart'; -import 'package:test/test.dart'; -import 'package:uuid/uuid.dart'; - -import '../../sshnp_mocks.dart'; -import 'sshrvd_channel_mocks.dart'; - -void main() { - group('SshrvdChannel', () { - late SshrvGeneratorStub sshrvGeneratorStub; - late MockAtClient mockAtClient; - late StreamController notificationStreamController; - late NotifyStub notifyStub; - late SubscribeStub subscribeStub; - late MockSshnpParams mockParams; - late String sessionId; - late StubbedSshrvdChannel stubbedSshrvdChannel; - late MockSshrv mockSshrv; - - // Invocation patterns as closures so they can be referred to by name - // instead of explicitly writing these calls several times in the test - notifyInvocation() => notifyStub(any(), any()); - subscribeInvocation() => subscribeStub( - regex: any(named: 'regex'), - shouldDecrypt: any(named: 'shouldDecrypt'), - ); - sshrvGeneratorInvocation() => sshrvGeneratorStub( - any(), - any(), - localSshdPort: any(named: 'localSshdPort'), - ); - sshrvRunInvocation() => mockSshrv.run(); - - setUp(() { - sshrvGeneratorStub = SshrvGeneratorStub(); - mockAtClient = MockAtClient(); - notificationStreamController = StreamController(); - notifyStub = NotifyStub(); - subscribeStub = SubscribeStub(); - mockParams = MockSshnpParams(); - when(() => mockParams.verbose).thenReturn(false); - sessionId = Uuid().v4(); - mockSshrv = MockSshrv(); - - stubbedSshrvdChannel = StubbedSshrvdChannel( - atClient: mockAtClient, - params: mockParams, - sessionId: sessionId, - sshrvGenerator: sshrvGeneratorStub, - notify: notifyStub, - subscribe: subscribeStub, - ); - - registerFallbackValue(AtKey()); - registerFallbackValue(NotificationParams.forUpdate(AtKey())); - }); - - test('public API', () { - // This doesn't cover the full public API, but it covers all of the public - // members which do not need further tests - - // Base type - expect(stubbedSshrvdChannel, isA>()); - expect(stubbedSshrvdChannel, isA()); - expect(stubbedSshrvdChannel, isA()); - - // final params - expect(stubbedSshrvdChannel.logger, isA()); - expect( - stubbedSshrvdChannel.sshrvGenerator, - isA Function(String, int, {int localSshdPort})>(), - ); - expect(stubbedSshrvdChannel.atClient, mockAtClient); - expect(stubbedSshrvdChannel.params, mockParams); - expect(stubbedSshrvdChannel.sessionId, sessionId); - }); // test public API - - whenInitializationWithSshrvdHost() { - when(() => mockParams.host).thenReturn('@sshrvd'); - when(() => mockParams.device).thenReturn('mydevice'); - when(() => mockParams.clientAtSign).thenReturn('@client'); - - when(subscribeInvocation) - .thenAnswer((_) => notificationStreamController.stream); - - when(notifyInvocation).thenAnswer( - (_) async { - final testIp = '123.123.123.123'; - final portA = 10456; - final portB = 10789; - - notificationStreamController.add( - AtNotification.empty() - ..id = Uuid().v4() - ..key = '$sessionId.${Sshrvd.namespace}' - ..from = '@sshrvd' - ..to = '@client' - ..epochMillis = DateTime.now().millisecondsSinceEpoch - ..value = '$testIp,$portA,$portB', - ); - }, - ); - } - - test('Initialization - sshrvd host', () async { - /// Set the required parameters - whenInitializationWithSshrvdHost(); - expect(stubbedSshrvdChannel.sshrvdAck, SshrvdAck.notAcknowledged); - expect(stubbedSshrvdChannel.initializeStarted, false); - - verifyNever(subscribeInvocation); - verifyNever(notifyInvocation); - - await expectLater(stubbedSshrvdChannel.initialize(), completes); - - verifyInOrder([ - () => subscribeStub( - regex: '$sessionId.${Sshrvd.namespace}@', shouldDecrypt: true), - () => notifyStub( - any( - that: predicate( - // Predicate matching specifically the sshrvdIdKey format - (AtKey key) => - key.key == 'mydevice.${Sshrvd.namespace}' && - key.sharedBy == '@client' && - key.sharedWith == '@sshrvd' && - key.metadata != null && - key.metadata!.namespaceAware == false && - key.metadata!.ttl == 10000, - ), - ), - any(), - ), - ]); - - verifyNever(subscribeInvocation); - verifyNever(notifyInvocation); - - expect(stubbedSshrvdChannel.sshrvdAck, SshrvdAck.acknowledged); - expect(stubbedSshrvdChannel.host, '123.123.123.123'); - expect(stubbedSshrvdChannel.port, 10456); - expect(stubbedSshrvdChannel.sshrvdPort, 10789); - }); // test Initialization - sshrvd host - - test('Initialization - non-sshrvd host', () async { - when(() => mockParams.host).thenReturn('234.234.234.234'); - when(() => mockParams.port).thenReturn(135); - - await expectLater(stubbedSshrvdChannel.initialize(), completes); - - expect(stubbedSshrvdChannel.host, '234.234.234.234'); - expect(stubbedSshrvdChannel.port, 135); - }); // test Initialization - non-sshrvd host - - test('Initialization completes - sshrvd host', () async { - /// Set the required parameters - whenInitializationWithSshrvdHost(); - await expectLater(stubbedSshrvdChannel.callInitialization(), completes); - await expectLater(stubbedSshrvdChannel.initialized, completes); - }); - - test('Initialization completes - non-sshrvd host', () async { - when(() => mockParams.host).thenReturn('234.234.234.234'); - when(() => mockParams.port).thenReturn(135); - - await expectLater(stubbedSshrvdChannel.callInitialization(), completes); - await expectLater(stubbedSshrvdChannel.initialized, completes); - }); // test Initialization - non-sshrvd host - - test('runSshrv', () async { - whenInitializationWithSshrvdHost(); - - await expectLater(stubbedSshrvdChannel.callInitialization(), completes); - expect(stubbedSshrvdChannel.sshrvdAck, SshrvdAck.acknowledged); - await expectLater(stubbedSshrvdChannel.initialized, completes); - // Initialization should be complete - // Begin test for [runSshrv()] - - when(() => mockParams.localSshdPort).thenReturn(23); - when(sshrvGeneratorInvocation).thenReturn(mockSshrv); - when(sshrvRunInvocation).thenAnswer((_) async => 'called sshrv run'); - - verifyNever(sshrvGeneratorInvocation); - verifyNever(sshrvRunInvocation); - - await expectLater( - await stubbedSshrvdChannel.runSshrv(), - 'called sshrv run', - ); - - verifyInOrder([ - sshrvGeneratorInvocation, - sshrvRunInvocation, - ]); - - verifyNever(sshrvGeneratorInvocation); - verifyNever(sshrvRunInvocation); - }); // test runSshrv - }); // group SshrvdChannel -} diff --git a/packages/dart/sshnoports/README.md b/packages/dart/sshnoports/README.md index dd875a619..f3226aab0 100644 --- a/packages/dart/sshnoports/README.md +++ b/packages/dart/sshnoports/README.md @@ -24,11 +24,11 @@ There are five binaries:- `sshnp` : The client that sets up a connection to the device which you can then ssh to via your localhost interface -`sshrvd` : This daemon acts as a rendezvous service and provides Internet routable IP/Ports for sshnpd and sshrv to connect to +`srvd` : This daemon acts as a rendezvous service and provides Internet routable IP/Ports for sshnpd and srv to connect to -`sshrv` : This client is called by sshnp to connect the local sshd to the rendezvous point +`srv` : This client is called by sshnp to connect the local sshd to the rendezvous point -To get going you just need two (or three if you want to use your own sshrvd service) atSigns and their .atKeys files and the +To get going you just need two (or three if you want to use your own srvd service) atSigns and their .atKeys files and the binaries (from the [latest release](https://github.com/atsign-foundation/noports/releases)). Once you have the atSigns (free or paid atSigns from [atsign.com](https://atsign.com)), drop the binaries in place @@ -52,10 +52,10 @@ Once that has started up you can run the client code from another machine. The c ``` ./sshnp --from <@your_manager_atsign> --to <@your_devices_atsign> \ ---host --device -s <> +--host --device -s <> ``` -The --host specifies the atSign of the sshrvd or the DNS name of the openssh server of the client machine that the remote device can connect to. If everything goes to plan the client +The --host specifies the atSign of the srvd or the DNS name of the openssh server of the client machine that the remote device can connect to. If everything goes to plan the client will complete and tell you how to connect to the remote host for example. Example command would be:- @@ -76,11 +76,11 @@ If you want to do this in a single command use `$()` for example, note $(./sshnp -f @myclient -t @myserver -d mymachine -h @myrz -s id_ed25519.pub) ``` -Atsign provides a sshrvd service but if you want to run your own `sshrvd` you will need a machine that has an internet IP and all ports 1024-65535 unfirewalled and an atSign for the daemon to use. +Atsign provides a srvd service but if you want to run your own `srvd` you will need a machine that has an internet IP and all ports 1024-65535 unfirewalled and an atSign for the daemon to use. -To run your own rendezvous service, simply run the `sshrvd` binary. You may omit the manager atSign to allow all atSigns to use your rendezvous service. There are also flags like `-s` to snoop on traffic passing through the service. +To run your own rendezvous service, simply run the `srvd` binary. You may omit the manager atSign to allow all atSigns to use your rendezvous service. There are also flags like `-s` to snoop on traffic passing through the service. ``` -./sshrvd --atsign <@your_sshrvd_atsign> --manager <@manager_atsign> --ip +./srvd --atsign <@your_srvd_atsign> --manager <@manager_atsign> --ip ``` If you can now login using sshnp then you can now turn off sshd from listening on all external interfaces, and instead have ssh listen only on 127.0.0.1. @@ -130,7 +130,7 @@ after a reboot if for some reason the container crashes is all easily achieved. ## TWO Ways to run SSH! no ports daemons (root access NOT required) -### `sshnpd.sh` and `sshrvd.sh` - plain old shell scripts and log file +### `sshnpd.sh` and `srvd.sh` - plain old shell scripts and log file The scripts directory of this repo contains an example `sshnpd.sh` that can be run in a user's home directory (and assumes that the release has been @@ -148,7 +148,7 @@ You might also want to add a crontab entry to run the script on reboot: @reboot ~/sshnpd.sh > ~/sshnpd.log 2>&1 ``` -### `tmux-sshnpd.sh` and `tmux-sshrvd.sh` - the power of tmux, highly recommended if tmux is installed `sudo apt install tmux` +### `tmux-sshnpd.sh` and `tmux-srvd.sh` - the power of tmux, highly recommended if tmux is installed `sudo apt install tmux` This runs the daemon inside a tmux session, which can be connected to in order to see logs. diff --git a/packages/dart/sshnoports/bin/srv.dart b/packages/dart/sshnoports/bin/srv.dart new file mode 100644 index 000000000..b3f2dd8cc --- /dev/null +++ b/packages/dart/sshnoports/bin/srv.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:noports_core/srv.dart'; +import 'package:socket_connector/socket_connector.dart'; + +Future main(List args) async { + final ArgParser parser = ArgParser() + ..addOption('host', abbr: 'h', mandatory: true, help: 'rvd host') + ..addOption('port', abbr: 'p', mandatory: true, help: 'rvd port') + ..addOption('local-port', + defaultsTo: '22', + help: 'Local port (usually the sshd port) to bridge to; defaults to 22') + ..addFlag('bind-local-port', + defaultsTo: false, + negatable: false, + help: 'Set this flag when we are bridging from a local sender') + ..addFlag('rv-auth', + defaultsTo: false, + help: 'Whether this rv process will authenticate to rvd') + ..addFlag('rv-e2ee', + defaultsTo: false, + help: 'Whether this rv process will encrypt/decrypt' + ' all rvd socket traffic'); + final parsed = parser.parse(args); + + final String host = parsed['host']; + final int streamingPort = int.parse(parsed['port']); + final int localPort = int.parse(parsed['local-port']); + final bool bindLocalPort = parsed['bind-local-port']; + final bool rvAuth = parsed['rv-auth']; + final bool rvE2ee = parsed['rv-e2ee']; + + String? rvdAuthString = rvAuth ? Platform.environment['RV_AUTH'] : null; + String? sessionAESKeyString = rvE2ee ? Platform.environment['RV_AES'] : null; + String? sessionIVString = rvE2ee ? Platform.environment['RV_IV'] : null; + + SocketConnector connector = await Srv.dart( + host, + streamingPort, + localPort: localPort, + bindLocalPort: bindLocalPort, + rvdAuthString: rvdAuthString, + sessionAESKeyString: sessionAESKeyString, + sessionIVString: sessionIVString, + ).run(); + + /// Shut myself down once the socket connector closes + stderr.writeln('Waiting for connector to close'); + await connector.done; + + stderr.writeln('Closed - exiting'); + exit(0); +} diff --git a/packages/dart/sshnoports/bin/sshrvd.dart b/packages/dart/sshnoports/bin/srvd.dart similarity index 72% rename from packages/dart/sshnoports/bin/sshrvd.dart rename to packages/dart/sshnoports/bin/srvd.dart index af9341807..d104520cd 100644 --- a/packages/dart/sshnoports/bin/sshrvd.dart +++ b/packages/dart/sshnoports/bin/srvd.dart @@ -1,29 +1,29 @@ import 'dart:async'; import 'dart:io'; import 'package:at_utils/at_logger.dart'; -import 'package:noports_core/sshrvd.dart'; +import 'package:noports_core/srvd.dart'; import 'package:sshnoports/src/create_at_client_cli.dart'; import 'package:sshnoports/src/print_version.dart'; void main(List args) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; - late final Sshrvd sshrvd; + late final Srvd srvd; try { - sshrvd = await Sshrvd.fromCommandLineArgs( + srvd = await Srvd.fromCommandLineArgs( args, - atClientGenerator: (SshrvdParams p) => createAtClientCli( + atClientGenerator: (SrvdParams p) => createAtClientCli( homeDirectory: p.homeDirectory, - subDirectory: '.sshrvd', + subDirectory: '.srvd', atsign: p.atSign, atKeysFilePath: p.atKeysFilePath, - namespace: Sshrvd.namespace, + namespace: Srvd.namespace, rootDomain: p.rootDomain, ), usageCallback: (e, s) { printVersion(); - stdout.writeln(SshrvdParams.parser.usage); + stdout.writeln(SrvdParams.parser.usage); stderr.writeln('\n$e'); }, ); @@ -32,8 +32,8 @@ void main(List args) async { } await runZonedGuarded(() async { - await sshrvd.init(); - await sshrvd.run(); + await srvd.init(); + await srvd.run(); }, (Object error, StackTrace stackTrace) async { stderr.writeln('Error: ${error.toString()}'); stderr.writeln('Stack Trace: ${stackTrace.toString()}'); diff --git a/packages/dart/sshnoports/bin/sshnp.dart b/packages/dart/sshnoports/bin/sshnp.dart index d44ce6362..10c38df44 100644 --- a/packages/dart/sshnoports/bin/sshnp.dart +++ b/packages/dart/sshnoports/bin/sshnp.dart @@ -117,16 +117,23 @@ void main(List args) async { } on ArgumentError catch (error) { printUsage(error: error); exit(1); + } on FormatException catch (error) { + printUsage(error: error); + exit(1); } on SshnpError catch (error, stackTrace) { stderr.writeln(error.toString()); if (params?.verbose ?? true) { stderr.writeln('\nStack Trace: ${stackTrace.toString()}'); } exit(1); + } catch (error, stackTrace) { + stderr.writeln(error.toString()); + stderr.writeln('\nStack Trace: ${stackTrace.toString()}'); + exit(1); } }, (Object error, StackTrace stackTrace) async { - if (error is ArgumentError) return; - if (error is SshnpError) return; + // if (error is ArgumentError) return; + // if (error is SshnpError) return; stderr.writeln('Error: ${error.toString()}'); stderr.writeln('\nStack Trace: ${stackTrace.toString()}'); exit(1); diff --git a/packages/dart/sshnoports/bin/sshrv.dart b/packages/dart/sshnoports/bin/sshrv.dart deleted file mode 100644 index 45c076c0d..000000000 --- a/packages/dart/sshnoports/bin/sshrv.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:io'; - -import 'package:noports_core/sshrv.dart'; - -Future main(List args) async { - if (args.length < 2 || args.length > 3) { - stdout.writeln('sshrv [localhost sshd port, defaults to 22]'); - exit(-1); - } - - String host = args[0]; - int streamingPort = int.parse(args[1]); - - int localSshdPort = 22; - - if (args.length > 2) { - localSshdPort = int.parse(args[2]); - } - - await Sshrv.dart(host, streamingPort, localSshdPort: localSshdPort).run(); -} diff --git a/packages/dart/sshnoports/bundles/core/config/sshnp-config-template.env b/packages/dart/sshnoports/bundles/core/config/sshnp-config-template.env index 0996c8a7b..963759a3c 100644 --- a/packages/dart/sshnoports/bundles/core/config/sshnp-config-template.env +++ b/packages/dart/sshnoports/bundles/core/config/sshnp-config-template.env @@ -14,7 +14,7 @@ TO= # Receiving (a.k.a. remote) device name DEVICE= -# atSign of sshrvd daemon or FQDN/IP address to connect back to +# atSign of srvd daemon or FQDN/IP address to connect back to HOST= # TCP port to connect back to (only required if --host specified a FQDN/IP) diff --git a/packages/dart/sshnoports/bundles/shell/headless/README.md b/packages/dart/sshnoports/bundles/shell/headless/README.md index 807a59450..92c1a8fa5 100644 --- a/packages/dart/sshnoports/bundles/shell/headless/README.md +++ b/packages/dart/sshnoports/bundles/shell/headless/README.md @@ -39,24 +39,24 @@ To edit the crontab: crontab -e ``` -## sshrvd +## srvd ### Installation -The `sshrvd.service` file should be placed in `/etc/systemd/system` (as root). +The `srvd.service` file should be placed in `/etc/systemd/system` (as root). -Modify the `sshrvd.service` unit to use the appropriate atSign, +Modify the `srvd.service` unit to use the appropriate atSign, (The boilerplate uses @atsign) as well as the internet address. -Also change the username and make sure that username running sshrvd has the +Also change the username and make sure that username running srvd has the .atkeys file in place at '~/.atsign/keys'. -Run the following command to view full usage information of the sshrvd binary: +Run the following command to view full usage information of the srvd binary: ```sh -/usr/local/bin/sshrvd +/usr/local/bin/srvd ``` or if you didn't install the binaries as root: ```sh -~/.local/bin/sshrvd +~/.local/bin/srvd ``` ### Usage diff --git a/packages/dart/sshnoports/bundles/shell/headless/root_sshrvd.sh b/packages/dart/sshnoports/bundles/shell/headless/root_srvd.sh similarity index 85% rename from packages/dart/sshnoports/bundles/shell/headless/root_sshrvd.sh rename to packages/dart/sshnoports/bundles/shell/headless/root_srvd.sh index 041be6b80..83f3c212c 100755 --- a/packages/dart/sshnoports/bundles/shell/headless/root_sshrvd.sh +++ b/packages/dart/sshnoports/bundles/shell/headless/root_srvd.sh @@ -9,6 +9,6 @@ sleep 10; # allow machine to bring up network export USER="$user" while true; do - /usr/local/bin/sshrvd -a "$atsign" -i "$internet_address" + /usr/local/bin/srvd -a "$atsign" -i "$internet_address" sleep 10 done diff --git a/packages/dart/sshnoports/bundles/shell/headless/sshrvd.sh b/packages/dart/sshnoports/bundles/shell/headless/srvd.sh similarity index 84% rename from packages/dart/sshnoports/bundles/shell/headless/sshrvd.sh rename to packages/dart/sshnoports/bundles/shell/headless/srvd.sh index 091c5eda3..4bf891027 100755 --- a/packages/dart/sshnoports/bundles/shell/headless/sshrvd.sh +++ b/packages/dart/sshnoports/bundles/shell/headless/srvd.sh @@ -9,6 +9,6 @@ sleep 10; # allow machine to bring up network export USER="$user" while true; do - "$HOME"/.local/bin/sshrvd -a "$atsign" -i "$internet_address" + "$HOME"/.local/bin/srvd -a "$atsign" -i "$internet_address" sleep 10 done diff --git a/packages/dart/sshnoports/bundles/shell/install.sh b/packages/dart/sshnoports/bundles/shell/install.sh index fa7ddc643..4d52ece8f 100755 --- a/packages/dart/sshnoports/bundles/shell/install.sh +++ b/packages/dart/sshnoports/bundles/shell/install.sh @@ -53,25 +53,25 @@ usage() { echo "at_activate - install at_activate" echo "sshnp - install sshnp" echo "sshnpd - install sshnpd" - echo "sshrv - install sshrv" - echo "sshrvd - install sshrvd" + echo "srv - install srv" + echo "srvd - install srvd" echo "binaries - install all base binaries" echo "" - echo "debug_sshrvd - install sshrvd with debugging enabled" + echo "debug_srvd - install srvd with debugging enabled" echo "debug - install all debug binaries" echo "" echo "all - install all binaries (base and debug)" if ! is_darwin; then echo "" echo "systemd - install a systemd unit" - echo " available units: [sshnpd, sshrvd]" + echo " available units: [sshnpd, srvd]" fi echo "" echo "headless - install a headless cron job" - echo " available jobs: [sshnpd, sshrvd]" + echo " available jobs: [sshnpd, srvd]" echo "" echo "tmux - install a service in a tmux session" - echo " available services: [sshnpd, sshrvd]" + echo " available services: [sshnpd, srvd]" } # SETUP AUTHORIZED KEYS # @@ -104,8 +104,8 @@ install_base_binaries() { install_single_binary "at_activate" install_single_binary "sshnp" install_single_binary "sshnpd" - install_single_binary "sshrv" - install_single_binary "sshrvd" + install_single_binary "srv" + install_single_binary "srvd" } install_debug_binary() { @@ -120,7 +120,7 @@ install_debug_binary() { } install_debug_binaries() { - install_debug_binary "sshrvd" + install_debug_binary "srvd" } install_all_binaries() { @@ -153,14 +153,14 @@ install_systemd_unit() { install_systemd_sshnpd() { root_only install_single_binary "sshnpd" - install_single_binary "sshrv" + install_single_binary "srv" install_systemd_unit "sshnpd.service" } -install_systemd_sshrvd() { +install_systemd_srvd() { root_only - install_single_binary "sshrvd" - install_systemd_unit "sshrvd.service" + install_single_binary "srvd" + install_systemd_unit "srvd.service" } systemd() { @@ -172,7 +172,7 @@ systemd() { case "$1" in --help) usage; exit 0;; sshnpd) install_systemd_sshnpd;; - sshrvd) install_systemd_sshrvd;; + srvd) install_systemd_srvd;; *) echo "Unknown systemd unit: $1"; usage; @@ -234,20 +234,20 @@ install_headless_job() { install_headless_sshnpd() { install_single_binary "sshnpd" - install_single_binary "sshrv" + install_single_binary "srv" install_headless_job "sshnpd" } -install_headless_sshrvd() { - install_single_binary "sshrvd" - install_headless_job "sshrvd" +install_headless_srvd() { + install_single_binary "srvd" + install_headless_job "srvd" } headless() { case "$1" in --help|'') usage; exit 0;; sshnpd) install_headless_sshnpd;; - sshrvd) install_headless_sshrvd;; + srvd) install_headless_srvd;; *) echo "Unknown headless job: $1"; usage; @@ -302,20 +302,20 @@ install_tmux_service() { install_tmux_sshnpd() { install_single_binary "sshnpd" - install_single_binary "sshrv" + install_single_binary "srv" install_tmux_service "sshnpd" } -install_tmux_sshrvd() { - install_single_binary "sshrvd" - install_tmux_service "sshrvd" +install_tmux_srvd() { + install_single_binary "srvd" + install_tmux_service "srvd" } tmux() { case "$1" in --help|'') usage; exit 0;; sshnpd) install_tmux_sshnpd;; - sshrvd) install_tmux_sshrvd;; + srvd) install_tmux_srvd;; *) echo "Unknown tmux service: $1"; usage; @@ -333,9 +333,9 @@ main() { case "$1" in --help|'') usage; exit 0;; - at_activate|sshnp|sshnpd|sshrv|sshrvd) install_single_binary "$1";; + at_activate|sshnp|sshnpd|srv|srvd) install_single_binary "$1";; binaries) install_base_binaries;; - debug_sshrvd) install_debug_sshrvd;; + debug_srvd) install_debug_srvd;; debug) install_debug_binaries;; all) install_all_binaries;; systemd|headless|tmux) diff --git a/packages/dart/sshnoports/bundles/shell/systemd/README.md b/packages/dart/sshnoports/bundles/shell/systemd/README.md index d09061005..94e347761 100644 --- a/packages/dart/sshnoports/bundles/shell/systemd/README.md +++ b/packages/dart/sshnoports/bundles/shell/systemd/README.md @@ -40,20 +40,20 @@ To view the realtime logs, use journalctl: sudo journalctl -u sshnpd.service ``` -## sshrvd +## srvd ### Installation -The `sshrvd.service` file should be placed in `/etc/systemd/system` (as root). +The `srvd.service` file should be placed in `/etc/systemd/system` (as root). -Modify the `sshrvd.service` unit to use the appropriate atSign, +Modify the `srvd.service` unit to use the appropriate atSign, (The boilerplate uses @atsign) as well as the internet address. -Also change the username and make sure that username running sshrvd has the +Also change the username and make sure that username running srvd has the .atkeys file in place at '~/.atsign/keys'. -Run the following command to view full usage information of the sshrvd binary: +Run the following command to view full usage information of the srvd binary: ```sh -/usr/local/bin/sshrvd +/usr/local/bin/srvd ``` ### Usage @@ -61,18 +61,18 @@ Run the following command to view full usage information of the sshrvd binary: To enable the service use: ```sh -sudo systemctl enable sshrvd.service +sudo systemctl enable srvd.service ``` The services will then start at the next reboot, or can be started immediately with: ```sh -sudo systemctl start sshrvd.service +sudo systemctl start srvd.service ``` To view the realtime logs, use journalctl: ```sh -sudo journalctl -u sshrvd.service +sudo journalctl -u srvd.service ``` \ No newline at end of file diff --git a/packages/dart/sshnoports/bundles/shell/systemd/sshrvd.service b/packages/dart/sshnoports/bundles/shell/systemd/srvd.service similarity index 84% rename from packages/dart/sshnoports/bundles/shell/systemd/sshrvd.service rename to packages/dart/sshnoports/bundles/shell/systemd/srvd.service index 45aea48d4..0a196653b 100644 --- a/packages/dart/sshnoports/bundles/shell/systemd/sshrvd.service +++ b/packages/dart/sshnoports/bundles/shell/systemd/srvd.service @@ -13,7 +13,7 @@ RestartSec=3 # ExecStartPre=/bin/sleep 10 # TODO : set atsign, internet_address -ExecStart=/usr/local/bin/sshrvd -a <@atsign> -i +ExecStart=/usr/local/bin/srvd -a <@atsign> -i [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/packages/dart/sshnoports/lib/src/create_sshnp.dart b/packages/dart/sshnoports/lib/src/create_sshnp.dart index a90317316..c65eb2837 100644 --- a/packages/dart/sshnoports/lib/src/create_sshnp.dart +++ b/packages/dart/sshnoports/lib/src/create_sshnp.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:noports_core/sshnp_foundation.dart'; import 'package:at_client/at_client.dart'; import 'package:sshnoports/src/extended_arg_parser.dart'; +import 'package:at_utils/at_logger.dart'; typedef AtClientGenerator = Future Function(SshnpParams params); @@ -15,6 +16,9 @@ Future createSshnp( }) async { atClient ??= await atClientGenerator?.call(params); + if (params.verbose) { + AtSignLogger.root_level = 'INFO'; + } if (atClient == null) { throw ArgumentError( 'atClient must be provided or atClientGenerator must be provided'); diff --git a/packages/dart/sshnoports/lib/src/version.dart b/packages/dart/sshnoports/lib/src/version.dart index 2572e8013..2384f91ac 100644 --- a/packages/dart/sshnoports/lib/src/version.dart +++ b/packages/dart/sshnoports/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '4.0.5'; +const packageVersion = '4.1.0'; diff --git a/packages/dart/sshnoports/pubspec.lock b/packages/dart/sshnoports/pubspec.lock index f2df466fc..669dff789 100644 --- a/packages/dart/sshnoports/pubspec.lock +++ b/packages/dart/sshnoports/pubspec.lock @@ -26,7 +26,7 @@ packages: source: hosted version: "3.3.9" args: - dependency: "direct overridden" + dependency: "direct main" description: name: args sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 @@ -34,7 +34,7 @@ packages: source: hosted version: "2.4.2" asn1lib: - dependency: transitive + dependency: "direct overridden" description: name: asn1lib sha256: b74e3842a52c61f8819a1ec8444b4de5419b41a7465e69d4aa681445377398b0 @@ -77,10 +77,10 @@ packages: dependency: "direct overridden" description: name: at_client - sha256: b25f991409efa860a6196399c20bbebb8e763695197996ea4013789b4b96f297 + sha256: "4904549d31a41d893e0df39636acb867b604aef7f2a3b40d9bb20e30b2e248d1" url: "https://pub.dev" source: hosted - version: "3.0.68" + version: "3.0.71" at_commons: dependency: transitive description: @@ -370,7 +370,7 @@ packages: source: hosted version: "0.3.10" encrypt: - dependency: transitive + dependency: "direct overridden" description: name: encrypt sha256: "4fd4e4fdc21b9d7d4141823e1e6515cd94e7b8d84749504c232999fba25d9bbb" @@ -572,11 +572,10 @@ packages: noports_core: dependency: "direct main" description: - name: noports_core - sha256: a74b987dde3f5781bab4a4646a3c825d63b6c8147b9f6eeedc4fa5dd7ddb18d2 - url: "https://pub.dev" - source: hosted - version: "5.0.4" + path: "../noports_core" + relative: true + source: path + version: "5.1.0" openssh_ed25519: dependency: transitive description: @@ -714,13 +713,14 @@ packages: source: hosted version: "1.0.4" socket_connector: - dependency: "direct overridden" + dependency: "direct main" description: - name: socket_connector - sha256: "8a5b67ae79e232186caa166ae640fac2247db390d4f5d11ed8975f95527fd355" - url: "https://pub.dev" - source: hosted - version: "1.0.11" + path: "." + ref: socket-authenticator-option + resolved-ref: ed751077eaa4fa081773720c69916deb6077cd09 + url: "https://github.com/gkc/socket_connector.git" + source: git + version: "2.0.0" source_map_stack_trace: dependency: transitive description: diff --git a/packages/dart/sshnoports/pubspec.yaml b/packages/dart/sshnoports/pubspec.yaml index de3da2e50..867c7931b 100644 --- a/packages/dart/sshnoports/pubspec.yaml +++ b/packages/dart/sshnoports/pubspec.yaml @@ -1,30 +1,40 @@ name: sshnoports publish_to: none -version: 4.0.5 +version: 4.1.0 environment: sdk: ">=3.0.0 <4.0.0" dependencies: - noports_core: 5.0.4 + noports_core: 5.1.0 at_onboarding_cli: 1.4.1 + args: 2.4.2 + socket_connector: ^2.0.0 dependency_overrides: + socket_connector: + git: + url: https://github.com/gkc/socket_connector.git + ref: socket-authenticator-option + noports_core: + path: ../noports_core # Pin the dependencies of noports_core archive: 3.3.9 args: 2.4.2 - at_client: 3.0.68 + at_client: 3.0.71 at_lookup: 3.0.41 at_utils: 3.0.15 + asn1lib: 1.4.1 + encrypt: 5.0.1 crypton: 2.1.0 dartssh2: 2.8.2 logging: 1.2.0 meta: 1.9.1 - socket_connector: 1.0.11 ssh_key: 0.8.0 uuid: 3.0.7 version: 3.0.2 + dev_dependencies: lints: ^3.0.0 test: ^1.25.1 diff --git a/packages/dart/sshnoports/tools/Dockerfile b/packages/dart/sshnoports/tools/Dockerfile index 6cc89d344..170558134 100644 --- a/packages/dart/sshnoports/tools/Dockerfile +++ b/packages/dart/sshnoports/tools/Dockerfile @@ -14,7 +14,7 @@ RUN \ dart pub get ; \ dart run build_runner build --delete-conflicting-outputs ; \ dart compile exe bin/sshnpd.dart -o ${BINARYDIR}/sshnpd ; \ - dart compile exe bin/sshrv.dart -o ${BINARYDIR}/sshrv + dart compile exe bin/srv.dart -o ${BINARYDIR}/srv # Second stage of build FROM debian-slim FROM debian:stable-20240110-slim@sha256:f7235f31d948d45b37de1faabc7e518859d2b9cf0508486d71c1772cfc9bed8a @@ -41,6 +41,6 @@ RUN \ chmod 755 /${USER}/.startup.sh COPY --from=buildimage --chown=${USER}:${USER} /usr/local/at/sshnpd /usr/local/at/ -COPY --from=buildimage --chown=${USER}:${USER} /usr/local/at/sshrv /usr/local/at/ +COPY --from=buildimage --chown=${USER}:${USER} /usr/local/at/srv /usr/local/at/ WORKDIR ${HOMEDIR} ENTRYPOINT ["/atsign/.startup.sh"] diff --git a/packages/dart/sshnoports/tools/Dockerfile.package b/packages/dart/sshnoports/tools/Dockerfile.package index 81cc09897..764eeb643 100644 --- a/packages/dart/sshnoports/tools/Dockerfile.package +++ b/packages/dart/sshnoports/tools/Dockerfile.package @@ -7,10 +7,10 @@ WORKDIR /sshnoports COPY . . RUN set -eux; \ case "$(dpkg --print-architecture)" in \ - amd64) ARCH="x64";; \ - armhf) ARCH="arm";; \ - arm64) ARCH="arm64";; \ - riscv64) ARCH="riscv64";; \ + amd64) ARCH="x64";; \ + armhf) ARCH="arm";; \ + arm64) ARCH="arm64";; \ + riscv64) ARCH="riscv64";; \ esac; \ mkdir -p sshnp/debug; \ mkdir tarball; \ @@ -19,9 +19,9 @@ RUN set -eux; \ dart compile exe bin/activate_cli.dart -v -o sshnp/at_activate; \ dart compile exe bin/sshnp.dart -v -o sshnp/sshnp; \ dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd; \ - dart compile exe bin/sshrv.dart -v -o sshnp/sshrv; \ - dart compile exe bin/sshrvd.dart -v -o sshnp/sshrvd; \ - dart compile exe bin/sshrvd.dart -D ENABLE_SNOOP=true -v -o sshnp/debug/sshrvd; \ + dart compile exe bin/srv.dart -v -o sshnp/srv; \ + dart compile exe bin/srvd.dart -v -o sshnp/srvd; \ + dart compile exe bin/srvd.dart -D ENABLE_SNOOP=true -v -o sshnp/debug/srvd; \ cp -r bundles/core/* sshnp/; \ cp -r bundles/shell/* sshnp/; \ cp LICENSE sshnp/; \ diff --git a/packages/dart/sshnp_flutter/pubspec.lock b/packages/dart/sshnp_flutter/pubspec.lock index 4dedb685c..16b6149b6 100644 --- a/packages/dart/sshnp_flutter/pubspec.lock +++ b/packages/dart/sshnp_flutter/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: at_client - sha256: dbb841c6f26d47a77605606aa82c67570990b2c07baa3e630ed931efb8747331 + sha256: "41c5028179a1e765084ab85fcb6582640252b90421c4a4c37818d346e1d8ef9b" url: "https://pub.dev" source: hosted - version: "3.0.69" + version: "3.0.72" at_client_mobile: dependency: "direct main" description: @@ -261,10 +261,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -705,10 +705,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -744,11 +744,10 @@ packages: noports_core: dependency: "direct main" description: - name: noports_core - sha256: "4b4e74403ee7cee3aacc449c2f20f10f98a1d223d0820c1dfff78c539067da5b" - url: "https://pub.dev" - source: hosted - version: "4.0.1" + path: "../noports_core" + relative: true + source: path + version: "5.1.0" openssh_ed25519: dependency: transitive description: @@ -1095,10 +1094,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -1111,10 +1110,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1135,10 +1134,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" tutorial_coach_mark: dependency: transitive description: @@ -1271,10 +1270,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" webview_flutter: dependency: transitive description: @@ -1364,5 +1363,5 @@ packages: source: hosted version: "0.2.1" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.13.0" diff --git a/tests/end2end_tests/contexts/_init_/setup-srvd-entrypoint.sh b/tests/end2end_tests/contexts/_init_/setup-srvd-entrypoint.sh new file mode 100755 index 000000000..813e295fc --- /dev/null +++ b/tests/end2end_tests/contexts/_init_/setup-srvd-entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# this script copies the template srvd entrypoint to ../srvd/entrypoint.sh +# then also replaces the @srvdatsign with the provided argument (e.g. @alice) +# example usage: ./setup-srvd-entrypoint.sh @alice + +srvd=$1 # e.g. @alice +template_name=$2 # e.g. "srvd_entrypoint.sh" + +cp ../../entrypoints/"$template_name" ../srvd/entrypoint.sh # copy template to the mounted folder + +prefix="sed -i" + +# if on MacOS +if [[ $(uname) == "Darwin" ]]; +then + prefix="$prefix ''" +fi + +eval "$prefix" "s/@srvdatsign/${srvd}/g" ../srvd/entrypoint.sh \ No newline at end of file diff --git a/tests/end2end_tests/contexts/_init_/setup-srvd-keys.sh b/tests/end2end_tests/contexts/_init_/setup-srvd-keys.sh new file mode 100755 index 000000000..6b5cdd4e5 --- /dev/null +++ b/tests/end2end_tests/contexts/_init_/setup-srvd-keys.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# this script copies the keys from ~/.atsign/keys to ../srvd/keys +# example usage: ./setup-srvd-keys.sh @alice + +srvd=$1 + +cp ~/.atsign/keys/"$srvd"_key.atKeys ../srvd/.atsign/keys/"$srvd"_key.atKeys # copy keys to the mounted folder + +if [[ ! -f ../srvd/.atsign/keys/${srvd}_key.atKeys ]]; +then + echo "Could not copy ${srvd}_key.atKeys to ../srvd/.atsign/keys/${srvd}_key.atKeys" + exit 1 +fi \ No newline at end of file diff --git a/tests/end2end_tests/contexts/_init_/setup-sshnp-entrypoint.sh b/tests/end2end_tests/contexts/_init_/setup-sshnp-entrypoint.sh index 1ab50424a..57fad084d 100755 --- a/tests/end2end_tests/contexts/_init_/setup-sshnp-entrypoint.sh +++ b/tests/end2end_tests/contexts/_init_/setup-sshnp-entrypoint.sh @@ -1,13 +1,13 @@ #!/bin/bash # this script copies the template sshnp entrypoint to ../sshnp/entrypoint.sh -# then also replaces the device name, sshnp atSign, sshnpd atSign, and sshrvd atSign with the provided arguments +# then also replaces the device name, sshnp atSign, sshnpd atSign, and srvd atSign with the provided arguments # example usage: ./setup-sshnp-entrypoint.sh e2e @alice @alice @alice device=$1 # e.g. e2e sshnp=$2 # e.g. @alice sshnpd=$3 # e.g. @alice -sshrvd=$4 # e.g. @alice +srvd=$4 # e.g. @alice template_name=$5 # e.g. sshnp_entrypoint.sh args="$6" # e.g. "arg1 arg2 arg3" @@ -23,7 +23,7 @@ fi eval "$prefix" "s/@sshnpatsign/${sshnp}/g" ../sshnp/entrypoint.sh eval "$prefix" "s/@sshnpdatsign/${sshnpd}/g" ../sshnp/entrypoint.sh -eval "$prefix" "s/@sshrvdatsign/${sshrvd}/g" ../sshnp/entrypoint.sh +eval "$prefix" "s/@srvdatsign/${srvd}/g" ../sshnp/entrypoint.sh eval "$prefix" "s/deviceName/${device}/g" ../sshnp/entrypoint.sh # Don't use eval for this one, because it will try to evaluate the args stored in $args diff --git a/tests/end2end_tests/contexts/_init_/setup-sshrvd-entrypoint.sh b/tests/end2end_tests/contexts/_init_/setup-sshrvd-entrypoint.sh deleted file mode 100755 index 743c7cb9e..000000000 --- a/tests/end2end_tests/contexts/_init_/setup-sshrvd-entrypoint.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# this script copies the template sshrvd entrypoint to ../sshrvd/entrypoint.sh -# then also replaces the @sshrvdatsign with the provided argument (e.g. @alice) -# example usage: ./setup-sshrvd-entrypoint.sh @alice - -sshrvd=$1 # e.g. @alice -template_name=$2 # e.g. "sshrvd_entrypoint.sh" - -cp ../../entrypoints/"$template_name" ../sshrvd/entrypoint.sh # copy template to the mounted folder - -prefix="sed -i" - -# if on MacOS -if [[ $(uname) == "Darwin" ]]; -then - prefix="$prefix ''" -fi - -eval "$prefix" "s/@sshrvdatsign/${sshrvd}/g" ../sshrvd/entrypoint.sh \ No newline at end of file diff --git a/tests/end2end_tests/contexts/_init_/setup-sshrvd-keys.sh b/tests/end2end_tests/contexts/_init_/setup-sshrvd-keys.sh deleted file mode 100755 index eb66a797c..000000000 --- a/tests/end2end_tests/contexts/_init_/setup-sshrvd-keys.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# this script copies the keys from ~/.atsign/keys to ../sshrvd/keys -# example usage: ./setup-sshrvd-keys.sh @alice - -sshrvd=$1 - -cp ~/.atsign/keys/"$sshrvd"_key.atKeys ../sshrvd/.atsign/keys/"$sshrvd"_key.atKeys # copy keys to the mounted folder - -if [[ ! -f ../sshrvd/.atsign/keys/${sshrvd}_key.atKeys ]]; -then - echo "Could not copy ${sshrvd}_key.atKeys to ../sshrvd/.atsign/keys/${sshrvd}_key.atKeys" - exit 1 -fi \ No newline at end of file diff --git a/tests/end2end_tests/contexts/sshrvd/.atsign/keys/.gitkeep b/tests/end2end_tests/contexts/srvd/.atsign/keys/.gitkeep similarity index 100% rename from tests/end2end_tests/contexts/sshrvd/.atsign/keys/.gitkeep rename to tests/end2end_tests/contexts/srvd/.atsign/keys/.gitkeep diff --git a/tests/end2end_tests/entrypoints/srvd_entrypoint.sh b/tests/end2end_tests/entrypoints/srvd_entrypoint.sh new file mode 100644 index 000000000..82243b1b6 --- /dev/null +++ b/tests/end2end_tests/entrypoints/srvd_entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/bash +"$HOME"/.local/bin/srvd -a @srvdatsign -i "$(hostname -i)" -v -s 2>&1 | tee -a srvd.log diff --git a/tests/end2end_tests/entrypoints/sshnp_entrypoint.sh b/tests/end2end_tests/entrypoints/sshnp_entrypoint.sh index 29bf9a4f3..6c970cfc7 100644 --- a/tests/end2end_tests/entrypoints/sshnp_entrypoint.sh +++ b/tests/end2end_tests/entrypoints/sshnp_entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash echo "SSHNP START ENTRY" -SSHNP_COMMAND="$HOME/.local/bin/sshnp -f @sshnpatsign -t @sshnpdatsign -d deviceName -h @sshrvdatsign args > sshnp.log" +SSHNP_COMMAND="$HOME/.local/bin/sshnp -f @sshnpatsign -t @sshnpdatsign -d deviceName -h @srvdatsign args > sshnp.log" run_test() { diff --git a/tests/end2end_tests/entrypoints/sshnp_installer_entrypoint.sh b/tests/end2end_tests/entrypoints/sshnp_installer_entrypoint.sh index 5c4114f01..de76b5a7a 100644 --- a/tests/end2end_tests/entrypoints/sshnp_installer_entrypoint.sh +++ b/tests/end2end_tests/entrypoints/sshnp_installer_entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash -SSHNP_COMMAND="$HOME/.local/bin/sshnp@sshnpdatsign -d deviceName -h @sshrvdatsign args > sshnp.log" +SSHNP_COMMAND="$HOME/.local/bin/sshnp@sshnpdatsign -d deviceName -h @srvdatsign args > sshnp.log" echo "Running: $SSHNP_COMMAND" eval "$SSHNP_COMMAND" cat sshnp.log diff --git a/tests/end2end_tests/entrypoints/sshrvd_entrypoint.sh b/tests/end2end_tests/entrypoints/sshrvd_entrypoint.sh deleted file mode 100644 index c41a11bc8..000000000 --- a/tests/end2end_tests/entrypoints/sshrvd_entrypoint.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -"$HOME"/.local/bin/sshrvd -a @sshrvdatsign -i "$(hostname -i)" -v -s 2>&1 | tee -a sshrvd.log diff --git a/tests/end2end_tests/image/Dockerfile b/tests/end2end_tests/image/Dockerfile index 1f081bce5..1a3d9f773 100644 --- a/tests/end2end_tests/image/Dockerfile +++ b/tests/end2end_tests/image/Dockerfile @@ -47,8 +47,8 @@ RUN set -eux ; \ dart pub get -C ${PACKAGE_DIR}; \ dart compile exe ${PACKAGE_DIR}/bin/sshnp.dart -o ${OUTPUT_DIR}/sshnp ; \ dart compile exe ${PACKAGE_DIR}/bin/sshnpd.dart -o ${OUTPUT_DIR}/sshnpd ; \ - dart compile exe ${PACKAGE_DIR}/bin/sshrv.dart -o ${OUTPUT_DIR}/sshrv ; \ - dart compile exe ${PACKAGE_DIR}/bin/sshrvd.dart -o ${OUTPUT_DIR}/sshrvd ; \ + dart compile exe ${PACKAGE_DIR}/bin/srv.dart -o ${OUTPUT_DIR}/srv ; \ + dart compile exe ${PACKAGE_DIR}/bin/srvd.dart -o ${OUTPUT_DIR}/srvd ; \ dart compile exe ${PACKAGE_DIR}/bin/activate_cli.dart -o ${OUTPUT_DIR}/at_activate ; # RUNTIME BRANCH @@ -83,8 +83,8 @@ RUN set -eux ; \ dart pub get -C ${PACKAGE_DIR}; \ dart compile exe ${PACKAGE_DIR}/bin/sshnp.dart -o ${OUTPUT_DIR}/sshnp ; \ dart compile exe ${PACKAGE_DIR}/bin/sshnpd.dart -o ${OUTPUT_DIR}/sshnpd ; \ - dart compile exe ${PACKAGE_DIR}/bin/sshrv.dart -o ${OUTPUT_DIR}/sshrv ; \ - dart compile exe ${PACKAGE_DIR}/bin/sshrvd.dart -o ${OUTPUT_DIR}/sshrvd ; \ + dart compile exe ${PACKAGE_DIR}/bin/srv.dart -o ${OUTPUT_DIR}/srv ; \ + dart compile exe ${PACKAGE_DIR}/bin/srvd.dart -o ${OUTPUT_DIR}/srvd ; \ dart compile exe ${PACKAGE_DIR}/bin/activate_cli.dart -o ${OUTPUT_DIR}/at_activate ; # RUNTIME LOCAL @@ -134,7 +134,7 @@ RUN apt-get update ; \ tar -xvf sshnp-linux-${ARCH}.tgz ; \ rm sshnp-linux-${ARCH}.tgz ; \ cd sshnp ; \ - mv sshnp sshnpd sshrv sshrvd at_activate ${OUTPUT_DIR} ; + mv sshnp sshnpd srv srvd at_activate ${OUTPUT_DIR} ; # RUNTIME RELEASE FROM base AS runtime-release diff --git a/tests/end2end_tests/tests/service-container-sshrvd.yaml b/tests/end2end_tests/tests/service-container-srvd.yaml similarity index 81% rename from tests/end2end_tests/tests/service-container-sshrvd.yaml rename to tests/end2end_tests/tests/service-container-srvd.yaml index ee3da03cf..a3ea127f2 100644 --- a/tests/end2end_tests/tests/service-container-sshrvd.yaml +++ b/tests/end2end_tests/tests/service-container-srvd.yaml @@ -1,10 +1,10 @@ - container-sshrvd: - container_name: sshrvd + container-srvd: + container_name: srvd volumes: - - ../contexts/sshrvd:/mount + - ../contexts/srvd:/mount network_mode: host healthcheck: - test: ["CMD", "grep", "-Eq", "monitor started for @", "/atsign/sshrvd.log"] + test: ["CMD", "grep", "-Eq", "monitor started for @", "/atsign/srvd.log"] start_period: 10s # Wait 10 seconds before checking interval: 5s # Check every 5 seconds timeout: 1s # If a check takes longer than a second, consider it a failed check diff --git a/tests/end2end_tests/tests/service-container-sshnpd.yaml b/tests/end2end_tests/tests/service-container-sshnpd.yaml index 17f319fd9..ae79bc438 100644 --- a/tests/end2end_tests/tests/service-container-sshnpd.yaml +++ b/tests/end2end_tests/tests/service-container-sshnpd.yaml @@ -12,4 +12,4 @@ retries: 36 # Retry the check n times # auto added: # - image - # - depends_on: (sshrvd + runtime service) + # - depends_on: (srvd + runtime service) diff --git a/tools/manual-docker/README.md b/tools/manual-docker/README.md index f4ecc7b4e..0373c8f24 100644 --- a/tools/manual-docker/README.md +++ b/tools/manual-docker/README.md @@ -49,7 +49,7 @@ $ ./run-manual-docker.sh usage: ./run-manual-docker.sh -h|--help - -t|--tag (required) - docker container tag + -t|--tag (required) - docker container tag --no-cache (optional) - docker build without cache --rm (optional) - remove container after exit ONE OF THE FOLLOWING (required) @@ -60,7 +60,7 @@ usage: ./run-manual-docker.sh example: ./run-manual-docker.sh -t sshnp -b trunk example: ./run-manual-docker.sh -t sshnpd -l - example: ./run-manual-docker.sh -t sshrvd -r v3.3.0 + example: ./run-manual-docker.sh -t srvd -r v3.3.0 example: ./run-manual-docker.sh -t sshnp --release example: ./run-manual-docker.sh -t sshnp --blank ``` diff --git a/tools/manual-docker/blank/docker-compose.yaml b/tools/manual-docker/blank/docker-compose.yaml index 2b90a9a31..cf1df788e 100644 --- a/tools/manual-docker/blank/docker-compose.yaml +++ b/tools/manual-docker/blank/docker-compose.yaml @@ -1,5 +1,4 @@ - -version: '3.8' +version: "3.8" services: image-manual-blank: @@ -29,11 +28,11 @@ services: - sshnpd depends_on: - image-manual-blank - container-sshrvd: + container-srvd: image: atsigncompany/sshnp-e2e-manual:blank - container_name: manual_blank_sshrvd + container_name: manual_blank_srvd volumes: - - ../../../tests/end2end_tests/contexts/sshrvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys + - ../../../tests/end2end_tests/contexts/srvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys network_mode: host depends_on: - image-manual-blank @@ -44,4 +43,4 @@ networks: driver: bridge sshnp: name: atsigncompany/sshnp-e2e-manual-network-sshnp - driver: bridge \ No newline at end of file + driver: bridge diff --git a/tools/manual-docker/branch/docker-compose.yaml b/tools/manual-docker/branch/docker-compose.yaml index 13c3953ce..830479cc8 100644 --- a/tools/manual-docker/branch/docker-compose.yaml +++ b/tools/manual-docker/branch/docker-compose.yaml @@ -1,5 +1,4 @@ - -version: '3.8' +version: "3.8" services: image-manual-branch: @@ -31,16 +30,15 @@ services: - sshnpd depends_on: - image-manual-branch - container-sshrvd: + container-srvd: image: atsigncompany/sshnp-e2e-manual:branch - container_name: manual_branch_sshrvd + container_name: manual_branch_srvd volumes: - - ../../../tests/end2end_tests/contexts/sshrvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys + - ../../../tests/end2end_tests/contexts/srvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys network_mode: host depends_on: - image-manual-branch - networks: sshnpd: name: atsigncompany/sshnp-e2e-manual-network-sshnpd diff --git a/tools/manual-docker/local/docker-compose.yaml b/tools/manual-docker/local/docker-compose.yaml index 37bebeecc..3a972978e 100644 --- a/tools/manual-docker/local/docker-compose.yaml +++ b/tools/manual-docker/local/docker-compose.yaml @@ -1,5 +1,4 @@ - -version: '3.8' +version: "3.8" services: image-manual-local: @@ -29,11 +28,11 @@ services: - sshnp depends_on: - image-manual-local - # container-sshrvd: + # container-srvd: # image: atsigncompany/sshnp-e2e-manual:local - # container_name: manual_local_sshrvd + # container_name: manual_local_srvd # volumes: - # - ../../../../contexts/sshrvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys + # - ../../../../contexts/srvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys # network_mode: host # depends_on: # - image-manual-local @@ -44,4 +43,4 @@ networks: driver: bridge sshnp: name: atsigncompany/sshnp-e2e-manual-network-sshnp - driver: bridge \ No newline at end of file + driver: bridge diff --git a/tools/manual-docker/release/docker-compose.yaml b/tools/manual-docker/release/docker-compose.yaml index a333f07cc..6a354479c 100644 --- a/tools/manual-docker/release/docker-compose.yaml +++ b/tools/manual-docker/release/docker-compose.yaml @@ -29,11 +29,11 @@ services: - sshnpd depends_on: - image-manual-release - container-sshrvd: + container-srvd: image: atsigncompany/sshnp-e2e-manual:release container_name: manual_release_sshnp volumes: - - ../../../tests/end2end_tests/contexts/sshrvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys + - ../../../tests/end2end_tests/contexts/srvd/.atsign/keys/:/atsign/.atsign/keys/ # mount keys network_mode: host depends_on: - image-manual-release diff --git a/tools/manual-docker/run-manual-docker.sh b/tools/manual-docker/run-manual-docker.sh index 2ba40d2ef..bd8ee9e7c 100755 --- a/tools/manual-docker/run-manual-docker.sh +++ b/tools/manual-docker/run-manual-docker.sh @@ -6,7 +6,7 @@ usage() { echo "" echo "usage: $0" echo " -h|--help" - echo " -t|--tag (required) - docker container tag" + echo " -t|--tag (required) - docker container tag" echo " --no-cache (optional) - docker build without cache" echo " --rm (optional) - remove container after exit" echo " ONE OF THE FOLLOWING (required)" @@ -17,7 +17,7 @@ usage() { echo "" echo " example: $0 -t sshnp -b trunk" echo " example: $0 -t sshnpd -l" - echo " example: $0 -t sshrvd -r v3.3.0" + echo " example: $0 -t srvd -r v3.3.0" echo " example: $0 -t sshnp --release" echo " example: $0 -t sshnp --blank" echo "" @@ -91,10 +91,10 @@ parse_args() { exit 1 fi - # check that tag is one of: sshnp/sshnpd/sshrvd - if [[ $tag != "sshnp" && $tag != "sshnpd" && $tag != "sshrvd" ]]; + # check that tag is one of: sshnp/sshnpd/srvd + if [[ $tag != "sshnp" && $tag != "sshnpd" && $tag != "srvd" ]]; then - echo "Invalid tag: $tag, must be one of: sshnp/sshnpd/sshrvd" + echo "Invalid tag: $tag, must be one of: sshnp/sshnpd/srvd" usage exit 1 fi diff --git a/tools/notarize-macos.sh b/tools/notarize-macos.sh index c9ca542ec..906ef2849 100755 --- a/tools/notarize-macos.sh +++ b/tools/notarize-macos.sh @@ -56,10 +56,10 @@ codesign \ --options=runtime \ -s "$SIGNING_IDENTITY" \ -v \ - "$WORKING_DIR"/sshnp/{ssh*,at_activate,debug/sshrvd}; + "$WORKING_DIR"/sshnp/{ssh*,at_activate,debug/srvd}; echo Verifying signatures: -codesign -vvv --deep --strict "$WORKING_DIR"/sshnp/{ssh*,at_activate,debug/sshrvd}; +codesign -vvv --deep --strict "$WORKING_DIR"/sshnp/{ssh*,at_activate,debug/srvd}; # Zip the signed binaries ditto -c -k --keepParent "$WORKING_DIR"/sshnp "$WORKING_DIR/$OUTPUT_FILE".zip diff --git a/tools/package-macos-arm64.sh b/tools/package-macos-arm64.sh index ba0591ab4..d39e3c3dc 100755 --- a/tools/package-macos-arm64.sh +++ b/tools/package-macos-arm64.sh @@ -47,10 +47,10 @@ mkdir -p "$OUTPUT_DIR/debug" eval "$DART compile exe -o $OUTPUT_DIR/sshnpd $SRC_DIR/bin/sshnpd.dart" eval "$DART compile exe -o $OUTPUT_DIR/sshnp $SRC_DIR/bin/sshnp.dart" -eval "$DART compile exe -o $OUTPUT_DIR/sshrvd $SRC_DIR/bin/sshrvd.dart" -eval "$DART compile exe -o $OUTPUT_DIR/sshrv $SRC_DIR/bin/sshrv.dart" +eval "$DART compile exe -o $OUTPUT_DIR/srvd $SRC_DIR/bin/srvd.dart" +eval "$DART compile exe -o $OUTPUT_DIR/srv $SRC_DIR/bin/srv.dart" eval "$DART compile exe -o $OUTPUT_DIR/at_activate $SRC_DIR/bin/activate_cli.dart" -eval "$DART compile exe -o $OUTPUT_DIR/debug/sshrvd -D ENABLE_SNOOP=true $SRC_DIR/bin/sshrvd.dart" +eval "$DART compile exe -o $OUTPUT_DIR/debug/srvd -D ENABLE_SNOOP=true $SRC_DIR/bin/srvd.dart" cp -r "$SRC_DIR/bundles/core"/* "$OUTPUT_DIR/"; cp -r "$SRC_DIR/bundles/shell"/* "$OUTPUT_DIR/";