diff --git a/.github/workflows/c_release.yml b/.github/workflows/c_release.yml index 8da626b5a..03141aa89 100644 --- a/.github/workflows/c_release.yml +++ b/.github/workflows/c_release.yml @@ -125,7 +125,7 @@ jobs: with: ref: c_release-${{github.run_number}} - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - run: | docker buildx build -t atsigncompany/sshnpdc -f sshnpd/tools/Dockerfile.package \ --platform ${{ matrix.platform }} -o type=tar,dest=bins.tar . @@ -163,7 +163,7 @@ jobs: with: ref: c_release-${{github.run_number}} - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - run: | docker buildx build -t atsigncompany/sshnpdcmusl -f sshnpd/tools/Dockerfile.musl \ --platform ${{ matrix.platform }} -o type=tar,dest=bins.tar . diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9e6722d83..579893145 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/init@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/autobuild@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/analyze@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dockerhub_sshnpd.yml b/.github/workflows/dockerhub_sshnpd.yml index 5255cd0d4..b157c2b9e 100644 --- a/.github/workflows/dockerhub_sshnpd.yml +++ b/.github/workflows/dockerhub_sshnpd.yml @@ -35,7 +35,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - name: Login to Docker Hub uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 with: diff --git a/.github/workflows/e2e_all.yaml b/.github/workflows/e2e_all.yaml index fef9f0eb5..52ac78bfe 100644 --- a/.github/workflows/e2e_all.yaml +++ b/.github/workflows/e2e_all.yaml @@ -29,7 +29,7 @@ jobs: steps: - name: execute tests on cicd vm - uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3 + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 env: SHA: ${{ github.event.pull_request.head.sha || github.sha }} with: diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index c5dbaa7d4..66324b5d3 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -197,7 +197,7 @@ jobs: - if: ${{ ! inputs.main_build_only }} uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 - if: ${{ ! inputs.main_build_only }} - uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1 - if: ${{ ! inputs.main_build_only }} run: | docker buildx build -t atsigncompany/sshnptarball -f ./tools/multibuild/Dockerfile.package \ diff --git a/.github/workflows/python-sshnpd-build-publish.yml b/.github/workflows/python-sshnpd-build-publish.yml index bdf347911..00ca41593 100644 --- a/.github/workflows/python-sshnpd-build-publish.yml +++ b/.github/workflows/python-sshnpd-build-publish.yml @@ -74,7 +74,7 @@ jobs: name: sshnpd-python-package path: dist/ - name: Publish distribution to TestPyPI - uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b # v1.10.2 + uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 with: skip-existing: true attestations: true @@ -99,7 +99,7 @@ jobs: name: sshnpd-python-package path: dist/ - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@897895f1e160c830e369f9779632ebc134688e1b # v1.10.2 + uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 with: attestations: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index ad0783887..d313ba40e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e2b3eafc8d227b0241d48be5f425d47c2d750a13 # v3.26.10 + uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 with: sarif_file: results.sarif diff --git a/packages/dart/noports_core/CHANGELOG.md b/packages/dart/noports_core/CHANGELOG.md index 77ad68c68..a7eb27e78 100644 --- a/packages/dart/noports_core/CHANGELOG.md +++ b/packages/dart/noports_core/CHANGELOG.md @@ -1,3 +1,5 @@ +# 6.2.0 +- feat: allow hyphens in device name # 6.1.1 - build[deps]: upgrade: \ at_client to 3.2.2 | at_onboarding_cli to 1.6.4 | at_utils to 3.0.19 | at_commons to 5.0.0 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 9f3e8fe7b..bceae692d 100644 --- a/packages/dart/noports_core/lib/src/common/default_args.dart +++ b/packages/dart/noports_core/lib/src/common/default_args.dart @@ -24,11 +24,16 @@ class DefaultArgs { static const bool authenticateDeviceToRvd = true; static const bool encryptRvdTraffic = true; - /// How long a client should wait for response after pinging a daemon + /// How long a client should wait for response after pinging a NoPorts daemon static const int daemonPingTimeoutSeconds = 20; static const Duration daemonPingTimeoutDuration = Duration(seconds: daemonPingTimeoutSeconds); + /// How long a client should wait for response from a NoPorts relay + static const int relayResponseTimeoutSeconds = 20; + static const Duration relayResponseTimeoutDuration = + Duration(seconds: relayResponseTimeoutSeconds); + /// How long srv should stay running if SocketConnector has no connections static const int srvTimeoutInSeconds = 30; static const Duration srvTimeout = Duration(seconds: srvTimeoutInSeconds); @@ -49,5 +54,5 @@ class DefaultSshnpdArgs { static const String deviceGroupName = '__none__'; static const String sshPublicKeyPermissions = ""; static const Duration policyHeartbeatFrequency = Duration(minutes: 5); - static const String permitOpen ='localhost:22,localhost:3389'; + static const String permitOpen = 'localhost:22,localhost:3389'; } diff --git a/packages/dart/noports_core/lib/src/common/validation_utils.dart b/packages/dart/noports_core/lib/src/common/validation_utils.dart index 88d0a2a0a..6c4a5756e 100644 --- a/packages/dart/noports_core/lib/src/common/validation_utils.dart +++ b/packages/dart/noports_core/lib/src/common/validation_utils.dart @@ -8,13 +8,22 @@ import 'package:noports_core/src/common/file_system_utils.dart'; import 'package:noports_core/src/common/io_types.dart'; import 'package:path/path.dart' as path; -const String sshnpDeviceNameRegex = r'[a-z0-9_]{1,36}'; +const String sshnpDeviceNameRegex = r'[a-z0-9_][a-z0-9_\-]{1,35}'; const String invalidDeviceNameMsg = 'Device name must be alphanumeric' - ' snake case, max length 36'; -const String deviceNameFormatHelp = 'Alphanumeric snake case, max length 36.'; + ' snake case, max length 36. First char must be _, a-z, or 0-9.'; +const String deviceNameFormatHelp = 'Alphanumeric snake case, max length 36.' + ' First char must be _, a-z, or 0-9.'; const String invalidSshKeyPermissionsMsg = 'Detected newline characters in the ssh public key permissions which malforms the authorized_keys file.'; +/// Returns deviceName with uppercase latin replaced by lowercase, and +/// whitespace replaced with underscores. Note that multiple consecutive +/// whitespace characters will be replaced by a single underscore. +String snakifyDeviceName(String deviceName) { + return deviceName.toLowerCase().replaceAll(RegExp(r'\s+'), '_'); +} + +/// Returns false if the device name does not match [sshnpDeviceNameRegex] bool invalidDeviceName(String test) { return RegExp(sshnpDeviceNameRegex).allMatches(test).first.group(0) != test; } diff --git a/packages/dart/noports_core/lib/src/srv/srv_impl.dart b/packages/dart/noports_core/lib/src/srv/srv_impl.dart index 875177704..ff7ef7340 100644 --- a/packages/dart/noports_core/lib/src/srv/srv_impl.dart +++ b/packages/dart/noports_core/lib/src/srv/srv_impl.dart @@ -407,11 +407,15 @@ class SrvImplDart implements Srv { Future run() async { try { var hosts = await InternetAddress.lookup(streamingHost); - late SocketConnector sc; + // Determines whether the traffic in the socket is encrypted or transmitted in plain text. + bool encryptRvdTraffic = + (sessionAESKeyString != null && sessionIVString != null); + if (bindLocalPort) { if (multi) { - if (sessionAESKeyString == null || sessionIVString == null) { + if (encryptRvdTraffic == true && + (sessionAESKeyString == null || sessionIVString == null)) { throw ArgumentError('Symmetric session encryption key required'); } sc = await _runClientSideMulti(hosts: hosts, timeout: timeout); @@ -421,7 +425,8 @@ class SrvImplDart implements Srv { } else { // daemon side if (multi) { - if (sessionAESKeyString == null || sessionIVString == null) { + if (encryptRvdTraffic == true && + (sessionAESKeyString == null || sessionIVString == null)) { throw ArgumentError('Symmetric session encryption key required'); } sc = await _runDaemonSideMulti(hosts: hosts, timeout: timeout); @@ -485,7 +490,6 @@ class SrvImplDart implements Srv { } }, ); - return sc; } @@ -495,7 +499,6 @@ class SrvImplDart implements Srv { }) async { // client side SocketConnector? socketConnector; - Socket sessionControlSocket = await Socket.connect( streamingHost, streamingPort, timeout: Duration(seconds: 10)); @@ -505,6 +508,74 @@ class SrvImplDart implements Srv { ' control socket connection to rvd'); sessionControlSocket.writeln(rvdAuthString); } + + if (sessionAESKeyString != null && sessionIVString != null) { + logger + .info('_runClientSideMulti: On the client-side traffic is encrypted'); + socketConnector = await _clientSideEncryptedSocket( + sessionControlSocket, socketConnector, hosts, timeout); + } else { + logger.info( + '_runClientSideMulti: On the client-side traffic is transmitted in plain text'); + socketConnector = await _clientSidePlainSocket( + sessionControlSocket, socketConnector, hosts, timeout); + } + + logger.info('_runClientSideMulti serverToSocket is ready'); + // upon socketConnector.done, destroy the control socket, and complete + unawaited(socketConnector.done.whenComplete(() { + logger.info('_runClientSideMulti sc.done'); + sessionControlSocket.destroy(); + })); + return socketConnector; + } + + /// On the client side, the data in this socket remains unencrypted and is transmitted in plain text + Future _clientSidePlainSocket( + Socket sessionControlSocket, + SocketConnector? socketConnector, + List hosts, + Duration timeout) async { + sessionControlSocket.listen((event) { + String response = String.fromCharCodes(event).trim(); + logger.info('_runClientSideMulti' + ' Received control socket response: [$response]'); + }, onError: (e) { + logger.severe('_runClientSideMulti controlSocket error: $e'); + socketConnector?.close(); + }, onDone: () { + logger.info('_runClientSideMulti controlSocket done'); + socketConnector?.close(); + }); + socketConnector = await SocketConnector.serverToSocket( + portA: localPort, + addressB: hosts[0], + portB: streamingPort, + verbose: false, + logger: ioSinkForLogger(logger), + multi: multi, + timeout: timeout, + beforeJoining: (Side sideA, Side sideB) { + logger.info('_runClientSideMulti Sending connect request'); + sessionControlSocket + .add(Uint8List.fromList('connect:no:encrypt\n'.codeUnits)); + // Authenticate the sideB socket (to the rvd) + if (rvdAuthString != null) { + logger + .info('_runClientSideMulti authenticating new connection to rvd'); + sideB.socket.writeln(rvdAuthString); + } + }, + ); + return socketConnector; + } + + /// On the client side, the data in encrypted and is transmitted through this socket. + Future _clientSideEncryptedSocket( + Socket sessionControlSocket, + SocketConnector? socketConnector, + List hosts, + Duration timeout) async { DataTransformer controlEncrypter = createEncrypter(sessionAESKeyString!, sessionIVString!); DataTransformer controlDecrypter = @@ -556,14 +627,6 @@ class SrvImplDart implements Srv { sideB.transformer = createDecrypter(socketAESKey, socketIV); }, ); - logger.info('_runClientSideMulti serverToSocket is ready'); - - // upon socketConnector.done, destroy the control socket, and complete - unawaited(socketConnector.done.whenComplete(() { - logger.info('_runClientSideMulti sc.done'); - sessionControlSocket.destroy(); - })); - return socketConnector; } @@ -575,30 +638,50 @@ class SrvImplDart implements Srv { List args = request.split(":"); switch (args.first) { case 'connect': - if (args.length != 3) { - logger.severe('Unknown request to control socket: [$request]'); + // Handles the request from the socket where data needs no encryption. + // When --no-encrypt-rvd-traffic flag is set to true. + if (request == 'connect:no:encrypt') { + await SocketConnector.socketToSocket( + connector: sc, + addressA: + (await InternetAddress.lookup(localHost ?? 'localhost'))[0], + portA: localPort, + addressB: hosts[0], + portB: streamingPort, + verbose: false, + logger: ioSinkForLogger(logger)); + if (rvdAuthString != null) { + logger.info('_runDaemonSideMulti authenticating' + ' new socket connection to rvd'); + sc.connections.last.sideB.socket.writeln(rvdAuthString); + } return; + } else { + // In this case, the data in the socket is encrypted. + if (args.length != 3) { + logger.severe('Unknown request to control socket: [$request]'); + return; + } + logger.info('_runDaemonSideMulti' + ' Control socket received ${args.first} request - ' + ' creating new socketToSocket connection'); + await SocketConnector.socketToSocket( + connector: sc, + addressA: + (await InternetAddress.lookup(localHost ?? 'localhost'))[0], + portA: localPort, + addressB: hosts[0], + portB: streamingPort, + verbose: false, + logger: ioSinkForLogger(logger), + transformAtoB: createEncrypter(args[1], args[2]), + transformBtoA: createDecrypter(args[1], args[2])); + if (rvdAuthString != null) { + logger.info('_runDaemonSideMulti authenticating' + ' new socket connection to rvd'); + sc.connections.last.sideB.socket.writeln(rvdAuthString); + } } - logger.info('_runDaemonSideMulti' - ' Control socket received ${args.first} request - ' - ' creating new socketToSocket connection'); - await SocketConnector.socketToSocket( - connector: sc, - addressA: - (await InternetAddress.lookup(localHost ?? 'localhost'))[0], - portA: localPort, - addressB: hosts[0], - portB: streamingPort, - verbose: false, - logger: ioSinkForLogger(logger), - transformAtoB: createEncrypter(args[1], args[2]), - transformBtoA: createDecrypter(args[1], args[2])); - if (rvdAuthString != null) { - logger.info('_runDaemonSideMulti authenticating' - ' new socket connection to rvd'); - sc.connections.last.sideB.socket.writeln(rvdAuthString); - } - break; default: logger.severe('Unknown request to control socket: [$request]'); @@ -622,6 +705,41 @@ class SrvImplDart implements Srv { ' control socket connection to rvd'); sessionControlSocket.writeln(rvdAuthString); } + + if (sessionAESKeyString != null && sessionIVString != null) { + logger + .info('_runDaemonSideMulti: On the daemon side traffic is encrypted'); + _daemonSideEncryptedSocket(sessionControlSocket, sc, hosts); + } else { + logger.info( + '_runDaemonSideMulti: On the daemon side traffic is transmitted in plain text'); + _daemonSidePlainSocket(sessionControlSocket, sc, hosts); + } + + // upon socketConnector.done, destroy the control socket, and complete + unawaited(sc.done.whenComplete(() { + sessionControlSocket.destroy(); + })); + + return sc; + } + + void _daemonSidePlainSocket(Socket sessionControlSocket, SocketConnector sc, + List hosts) { + Mutex controlStreamMutex = Mutex(); + sessionControlSocket.listen((event) async { + await _sessionControlSocketListener(controlStreamMutex, event, sc, hosts); + }, onError: (e) { + logger.severe('controlSocket error: $e'); + sc.close(); + }, onDone: () { + logger.info('controlSocket done'); + sc.close(); + }); + } + + void _daemonSideEncryptedSocket(Socket sessionControlSocket, + SocketConnector sc, List hosts) { DataTransformer controlEncrypter = createEncrypter(sessionAESKeyString!, sessionIVString!); DataTransformer controlDecrypter = @@ -636,38 +754,7 @@ class SrvImplDart implements Srv { Mutex controlStreamMutex = Mutex(); controlStream.listen((event) async { logger.info('Received event on control socket.'); - try { - await controlStreamMutex.acquire(); - if (event.isEmpty) { - logger.info('Empty control message (Uint8List) received'); - return; - } - String eventStr = String.fromCharCodes(event).trim(); - if (eventStr.isEmpty) { - logger.info('Empty control message (String) received'); - return; - } - // TODO The code below (splitting by `connect:`) resolves a - // particular issue for the moment, but the overall approach - // to handling control messages needs to be redone, e.g. : - // Ideally - send the control request, and a newline - // => as of this commit, this is the case - // Receive - wait for newline, handle the request, repeat - // => older npt clients don't send `\n` so we will need to add some - // magic to handle both (a) older clients which don't send `\n` - // as well as (b) newer ones which do. Cleanest is to add a - // flag to the npt request from the client stating that it sends - // `\n` . If so then we handle that cleanly; if not then we use - // this approach (split by `connect:`) - List requests = eventStr.split('connect:'); - for (String request in requests) { - if (request.isNotEmpty) { - await _handleMultiConnectRequest(sc, hosts, 'connect:$request'); - } - } - } finally { - controlStreamMutex.release(); - } + await _sessionControlSocketListener(controlStreamMutex, event, sc, hosts); }, onError: (e) { logger.severe('controlSocket error: $e'); sc.close(); @@ -675,13 +762,42 @@ class SrvImplDart implements Srv { logger.info('controlSocket done'); sc.close(); }); + } - // upon socketConnector.done, destroy the control socket, and complete - unawaited(sc.done.whenComplete(() { - sessionControlSocket.destroy(); - })); - - return sc; + Future _sessionControlSocketListener(Mutex controlStreamMutex, + List event, SocketConnector sc, List hosts) async { + try { + await controlStreamMutex.acquire(); + if (event.isEmpty) { + logger.info('Empty control message (Uint8List) received'); + return; + } + String eventStr = String.fromCharCodes(event).trim(); + if (eventStr.isEmpty) { + logger.info('Empty control message (String) received'); + return; + } + // TODO The code below (splitting by `connect:`) resolves a + // particular issue for the moment, but the overall approach + // to handling control messages needs to be redone, e.g. : + // Ideally - send the control request, and a newline + // => as of this commit, this is the case + // Receive - wait for newline, handle the request, repeat + // => older npt clients don't send `\n` so we will need to add some + // magic to handle both (a) older clients which don't send `\n` + // as well as (b) newer ones which do. Cleanest is to add a + // flag to the npt request from the client stating that it sends + // `\n` . If so then we handle that cleanly; if not then we use + // this approach (split by `connect:`) + List requests = eventStr.split('connect:'); + for (String request in requests) { + if (request.isNotEmpty) { + await _handleMultiConnectRequest(sc, hosts, 'connect:$request'); + } + } + } finally { + controlStreamMutex.release(); + } } Future _runDaemonSideSingle({ 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 e6c2eb950..45b647782 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 @@ -299,12 +299,14 @@ class SshnpParams extends ClientParamsBase 'srvdAtSign is mandatory, unless list-devices is passed.')); } + String device = partial.device ?? DefaultSshnpArgs.device; + device = snakifyDeviceName(device); return SshnpParams( profileName: partial.profileName, clientAtSign: partial.clientAtSign!, sshnpdAtSign: partial.sshnpdAtSign ?? "", srvdAtSign: partial.srvdAtSign ?? "", - device: partial.device ?? DefaultSshnpArgs.device, + device: device, localPort: partial.localPort ?? DefaultSshnpArgs.localPort, identityFile: partial.identityFile, identityPassphrase: partial.identityPassphrase, 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 index 91f3422c0..7f6a9b3b8 100644 --- 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 @@ -130,7 +130,8 @@ abstract class SrvdChannel with AsyncInitialization, AtClientBindings { @protected @visibleForTesting - Future getHostAndPortFromSrvd() async { + Future getHostAndPortFromSrvd( + {Duration timeout = DefaultArgs.relayResponseTimeoutDuration}) async { srvdAck = SrvdAck.notAcknowledged; subscribe(regex: '$sessionId.${Srvd.namespace}@', shouldDecrypt: true) .listen((notification) async { @@ -201,13 +202,16 @@ abstract class SrvdChannel with AsyncInitialization, AtClientBindings { ); int counter = 1; + int t = DateTime.now().add(timeout).millisecondsSinceEpoch; while (srvdAck == SrvdAck.notAcknowledged) { - if (counter % 20 == 0) { + // we'll log a message every two seconds while we're waiting + // (40 loops, 50 milliseconds sleep per loop) + if (counter % 40 == 0) { logger.info('Still waiting for srvd response'); } - await Future.delayed(Duration(milliseconds: 100)); + await Future.delayed(Duration(milliseconds: 50)); counter++; - if (counter > 150) { + if (DateTime.now().millisecondsSinceEpoch > t) { logger.warning('Timed out waiting for srvd response'); throw TimeoutException( 'Connection timeout to srvd ${params.srvdAtSign} service'); 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 ac25e881c..4323392a5 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart @@ -78,14 +78,15 @@ class SshnpdParams { } String homeDirectory = getHomeDirectory()!; - // Do we have a device ? - String device = r['device']; - SupportedSshClient sshClient = SupportedSshClient.values.firstWhere( (c) => c.toString() == r['ssh-client'], orElse: () => DefaultSshnpdArgs.sshClient); - // Do we have an ASCII ? + // Do we have a valid device name? + String device = r['device']; + // First of all let's snakify it + device = snakifyDeviceName(device); + // and now check it against desired regex if (invalidDeviceName(device)) { throw ArgumentError(invalidDeviceNameMsg); } @@ -109,7 +110,7 @@ class SshnpdParams { permitOpen = '*:*'; } return SshnpdParams( - device: r['device'], + device: device, username: getUserName(throwIfNull: true)!, homeDirectory: homeDirectory, managerAtsigns: managerAtsigns, @@ -133,7 +134,7 @@ class SshnpdParams { homeDirectory: homeDirectory, atSign: deviceAtsign, progName: '.sshnpd', - uniqueID: r['device']), + uniqueID: device), permitOpen: permitOpen, ); } diff --git a/packages/dart/noports_core/lib/src/version.dart b/packages/dart/noports_core/lib/src/version.dart index 2ca5b0dfe..9a6891ccb 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 = '6.1.0'; +const packageVersion = '6.2.0'; diff --git a/packages/dart/noports_core/pubspec.yaml b/packages/dart/noports_core/pubspec.yaml index 8afb9d427..3c62e45e6 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: 6.1.1 +version: 6.2.0 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/dart/noports_core/test/sshnp/models/sshnp_arg_test.dart b/packages/dart/noports_core/test/sshnp/models/sshnp_arg_test.dart index 3a520952b..2a0046a35 100644 --- a/packages/dart/noports_core/test/sshnp/models/sshnp_arg_test.dart +++ b/packages/dart/noports_core/test/sshnp/models/sshnp_arg_test.dart @@ -5,7 +5,7 @@ import 'package:test/test.dart'; void main() { group('ParserType', () { test('public API test', () { - // abitrary values + // arbitrary values ParserType parserType = ParserType.all; ParseWhen parseWhen = ParseWhen.always; 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 9c94d6504..ccb4b75f6 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 @@ -92,6 +92,16 @@ void main() { clientAtSign: '', sshnpdAtSign: '', srvdAtSign: '@my_srvd'); expect(params.srvdAtSign, equals('@my_srvd')); }); + test('Test snakifyDeviceName', () { + expect(snakifyDeviceName('ABCDEF'), 'abcdef'); + expect(snakifyDeviceName('Ab_cd_Ef'), 'ab_cd_ef'); + expect(snakifyDeviceName('Ab-cd-Ef'), 'ab-cd-ef'); + expect(snakifyDeviceName('Ab cD-Ef'), 'ab_cd-ef'); + expect(snakifyDeviceName('Ab-cD Ef'), 'ab-cd_ef'); + expect(snakifyDeviceName('Ab cD Ef'), 'ab_cd_ef'); + expect(snakifyDeviceName('Ab\tcD\nEf'), 'ab_cd_ef'); + expect(snakifyDeviceName('Ab \t\n cD Ef'), 'ab_cd_ef'); + }); test('SshnpParams.device invalid with uppercase test', () { expect( () => SshnpParams( @@ -107,7 +117,7 @@ void main() { clientAtSign: '', sshnpdAtSign: '', srvdAtSign: '', - device: 'my-device-name'), + device: 'my#device#name'), throwsA(TypeMatcher())); }); test('SshnpParams.device invalid too long test', () { @@ -119,7 +129,36 @@ void main() { device: 'abcde_12345_abcde_12345_abcde_12345_X'), throwsA(TypeMatcher())); }); - test('SshnpParams.device test', () { + test('SshnpParams.device invalid must start with a-z or 0-9', () { + final l = [ + '-abcde', + '#abcde', + ' abcde', + '@abcde', + '£abcde', + '\$abcde', + '^abcde', + ]; + for (final s in l) { + expect( + () => SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + srvdAtSign: '', + device: s), + throwsA(TypeMatcher())); + } + }); + test('SshnpParams.device may start with underscore', () { + String deviceName = '_my-device-name_12345'; + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + srvdAtSign: '', + device: deviceName); + expect(params.device, equals(deviceName)); + }); + test('SshnpParams.device test pure snake case', () { String deviceName = 'my_device_name_12345'; final params = SshnpParams( clientAtSign: '', @@ -128,6 +167,15 @@ void main() { device: deviceName); expect(params.device, equals(deviceName)); }); + test('SshnpParams.device test with hyphens', () { + String deviceName = 'my-device-name_12345'; + final params = SshnpParams( + clientAtSign: '', + sshnpdAtSign: '', + srvdAtSign: '', + device: deviceName); + expect(params.device, equals(deviceName)); + }); test('SshnpParams.localPort test', () { final params = SshnpParams( clientAtSign: '', 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 index 654508c67..18ee3ab47 100644 --- 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 @@ -330,7 +330,9 @@ void main() { sessionId: sessionId); expect( - () async => await srvdDartBindPortChannel.getHostAndPortFromSrvd(), + () async => await srvdDartBindPortChannel.getHostAndPortFromSrvd( + // set timeout to something short so unit test runs quickly + timeout: Duration(milliseconds: 50)), throwsA(predicate((dynamic e) => e is TimeoutException && e.message == 'Connection timeout to srvd @srvd service'))); diff --git a/packages/dart/noports_core/test/sshnpd/sshnpd_params_test.dart b/packages/dart/noports_core/test/sshnpd/sshnpd_params_test.dart index dabc885bd..54bc18f69 100644 --- a/packages/dart/noports_core/test/sshnpd/sshnpd_params_test.dart +++ b/packages/dart/noports_core/test/sshnpd/sshnpd_params_test.dart @@ -4,10 +4,10 @@ import 'package:test/test.dart'; void main() { group('test sshnpd params defaults', () { - test('require at least one of managers and policyManager options', () async { + test('require at least one of managers and policyManager options', + () async { List args = '-a @daemon'.split(' '); - await expectLater( - () => SshnpdParams.fromArgs(args), + await expectLater(() => SshnpdParams.fromArgs(args), throwsA(TypeMatcher())); }); test('just managers option supplied', () async { @@ -26,7 +26,7 @@ void main() { final p = await SshnpdParams.fromArgs(args); expect(p.deviceAtsign, '@daemon'); expect(p.policyManagerAtsign, '@policy'); - expect(p.managerAtsigns, ['@bob','@chuck']); + expect(p.managerAtsigns, ['@bob', '@chuck']); }); test('test permitOpen default without policyManager', () async { List args = '-a @daemon -m @bob'.split(' '); @@ -57,4 +57,4 @@ void main() { // local-sshd-port // sshpublickey-permissions }); -} \ No newline at end of file +} diff --git a/packages/dart/sshnoports/bin/npt.dart b/packages/dart/sshnoports/bin/npt.dart index c0f0792bc..dd66ddba5 100644 --- a/packages/dart/sshnoports/bin/npt.dart +++ b/packages/dart/sshnoports/bin/npt.dart @@ -4,7 +4,6 @@ import 'dart:io'; // other packages import 'package:args/args.dart'; - // atPlatform packages import 'package:at_cli_commons/at_cli_commons.dart' as cli; import 'package:at_utils/at_utils.dart'; @@ -12,7 +11,6 @@ import 'package:duration/duration.dart'; import 'package:noports_core/npt.dart'; import 'package:noports_core/sshnp_foundation.dart'; import 'package:sshnoports/src/extended_arg_parser.dart'; - // local packages import 'package:sshnoports/src/print_version.dart'; @@ -203,6 +201,16 @@ void main(List args) async { ' it has started its session.', ); + parser.addFlag( + 'encrypt-rvd-traffic', + aliases: ['et'], + 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, + negatable: true, + ); + // Parse Args ArgResults parsedArgs = parser.parse(args); @@ -233,6 +241,14 @@ void main(List args) async { bool quiet = parsedArgs[quietFlag]; bool keepAlive = parsedArgs['keep-alive']; + // Do we have a valid device name? + // First of all let's snakify it + device = snakifyDeviceName(device); + // and now check it against desired regex + if (invalidDeviceName(device)) { + throw ArgumentError(invalidDeviceNameMsg); + } + // A listen progress listener for the CLI // Will only log if verbose is false, since if verbose is true // there will already be a boatload of log messages. @@ -321,6 +337,7 @@ void main(List args) async { inline: inline, daemonPingTimeout: Duration(seconds: int.parse(parsedArgs['daemon-ping-timeout'])), + encryptRvdTraffic: parsedArgs['encrypt-rvd-traffic'], timeout: parseDuration(timeoutArg), ); diff --git a/packages/dart/sshnoports/lib/src/version.dart b/packages/dart/sshnoports/lib/src/version.dart index fb0682dfd..eb03b5af9 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 = '5.6.1'; +const packageVersion = '5.6.3'; diff --git a/packages/dart/sshnoports/pubspec.lock b/packages/dart/sshnoports/pubspec.lock index 03bdcfcb7..b73b0e318 100644 --- a/packages/dart/sshnoports/pubspec.lock +++ b/packages/dart/sshnoports/pubspec.lock @@ -593,7 +593,7 @@ packages: path: "../noports_core" relative: true source: path - version: "6.1.1" + version: "6.2.0" openssh_ed25519: dependency: transitive description: diff --git a/packages/dart/sshnoports/pubspec.yaml b/packages/dart/sshnoports/pubspec.yaml index eab105db8..b2e5415bd 100644 --- a/packages/dart/sshnoports/pubspec.yaml +++ b/packages/dart/sshnoports/pubspec.yaml @@ -1,7 +1,7 @@ name: sshnoports publish_to: none -version: 5.6.2 +version: 5.6.3 environment: sdk: ">=3.0.0 <4.0.0" @@ -9,7 +9,7 @@ environment: dependencies: noports_core: path: "../noports_core" - version: 6.1.1 + version: 6.2.0 at_onboarding_cli: 1.6.4 at_cli_commons: ^1.1.0 at_client: ^3.2.2 diff --git a/tests/e2e_all/scripts/common/common_functions.include.sh b/tests/e2e_all/scripts/common/common_functions.include.sh index 4a4d29cf5..10ab020e8 100644 --- a/tests/e2e_all/scripts/common/common_functions.include.sh +++ b/tests/e2e_all/scripts/common/common_functions.include.sh @@ -56,14 +56,18 @@ getBaseSshnpCommand() { } getBaseNptCommand() { - if (($# != 1)); then - logErrorAndExit "getBaseNptCommand requires 1 argument (clientBinaryPath)" + if (($# < 1 || $# > 2)); then + logErrorAndExit "getBaseNptCommand requires 1 mandatory argument (clientBinaryPath) and optionally a second argument (encryptRvdTraffic)" fi clientBinaryPath="$1" l1="$clientBinaryPath/npt -f $clientAtSign -d $deviceName" l2=" -t $daemonAtSign -r $srvAtSign" l3=" --root-domain $atDirectoryHost" - echo "$l1" "$l2" "$l3" + if [ -z "$2" ]; then + echo "$l1" "$l2" "$l3" + else + echo "$l1" "$l2" "$l3" "$2" + fi } getTestSshCommand() { diff --git a/tests/e2e_all/scripts/tests/npt_to_port_22_no_encrypt_traffic b/tests/e2e_all/scripts/tests/npt_to_port_22_no_encrypt_traffic new file mode 100755 index 000000000..3c1bb6402 --- /dev/null +++ b/tests/e2e_all/scripts/tests/npt_to_port_22_no_encrypt_traffic @@ -0,0 +1,87 @@ +#!/bin/bash + +scriptName=$(basename -- "$0") +testToRun="$scriptName" + +if test -z "$testScriptsDir"; then + echo -e " ${RED}check_env: testScriptsDir is not set${NC}" && exit 1 +fi + +source "$testScriptsDir/common/common_functions.include.sh" +source "$testScriptsDir/common/check_env.include.sh" || exit $? + +daemonVersion="$1" +clientVersion="$2" +extraFlags="--remote-port 22 --exit-when-connected" + +if [[ $(versionIsAtLeast "$clientVersion" "d:5.3.0") == "true" ]]; then + apkamApp=$(getApkamAppName) + apkamDev=$(getApkamDeviceName "client" "$commitId") + keysFile=$(getApkamKeysFile "$clientAtSign" "$apkamApp" "$apkamDev") + extraFlags="$extraFlags -k $keysFile" +fi + +# If client has already been released +# then it has already have been tested against all released daemon versions +# So only test it against the 'current' daemon +# i.e. if client != current and daemon != current then exit 50 + +if ! grep -q "current" <<<"$clientVersion" && ! grep -q "current" <<<"$daemonVersion"; then + logInfo " N/A because released client $(getVersionDescription "$clientVersion") has already been tested against released daemon $(getVersionDescription "$daemonVersion")" + exit 50 +fi + +# The -no-encrypt-rvd-traffic is supported from 5.6.2 release which is the current as of now. +if [[ "$clientVersion" != "d:current" ]] || [[ "$daemonVersion" != "d:current" ]]; then + logInfo " N/A The feature is supported on client and daemon version >= 5.6.2" + exit 50 # test rig interprets this exit status as 'test was not applicable' +fi + +deviceName=$(getDeviceNameWithFlags "$commitId" "$daemonVersion") + +# We will capture daemon log from now until end of test +outputDir=$(getOutputDir) +daemonLogFile="${outputDir}/daemons/${deviceName}.log" +daemonLogFragmentName="$(getDaemonLogFragmentName $testToRun $daemonVersion $clientVersion)" +tail -f -n 0 "$daemonLogFile" >>"$daemonLogFragmentName" & +tailPid=$! # We'll kill this later + +clientBinaryPath=$(getPathToBinariesForTypeAndVersion "$clientVersion") + +baseNptCommand=$(getBaseNptCommand "$clientBinaryPath" "--no-encrypt-rvd-traffic") + +# Let's put together the npt command we will execute +nptCommand="$baseNptCommand $extraFlags --verbose" + +# 1. Execute the npt command - its output is the port that npt is using +echo "$(iso8601Date) | Executing $nptCommand" +nptPort=$($nptCommand) + +# 2. Check the exit status +nptExitStatus=$? +if ((nptExitStatus != 0)); then + # Kill the daemon log tail, and exit with the exit status of the npt command + kill "$tailPid" + exit $nptExitStatus +fi + +echo "$(iso8601Date) | npt OK, local port is $nptPort" +echo "$(iso8601Date) | Running ps for the spawned srv process with port $nptPort BEFORE running ssh" +ps -ef | grep "srv " | grep "$nptPort" + +# 3. Execute an ssh +sshCommand="ssh -p $nptPort -o StrictHostKeyChecking=accept-new -o IdentitiesOnly=yes" +sshCommand="${sshCommand} ${remoteUsername}@localhost -i $identityFilename" + +echo "$(iso8601Date) | Executing $sshCommand" + +# shellcheck disable=SC2091 +$(getTestSshCommand "$sshCommand") +sshExitStatus=$? + +echo "$(iso8601Date) | Running ps for the spawned srv process with port $nptPort AFTER running ssh" +ps -ef | grep "srv " | grep "$nptPort" + +# 4. Kill the daemon log tail, and exit with the exit status of the ssh command +kill "$tailPid" +exit $sshExitStatus