@electerm/ssh2 1.17.0 → 1.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -362,6 +362,10 @@ class Client extends EventEmitter {
362
362
  },
363
363
  onHandshakeComplete: (negotiated) => {
364
364
  this.emit('handshake', negotiated);
365
+ // Start keepalive monitoring after handshake
366
+ if (this.config.keepaliveInterval > 0) {
367
+ resetKA();
368
+ }
365
369
  if (!ready) {
366
370
  ready = true;
367
371
  proto.service('ssh-userauth');
@@ -540,10 +544,14 @@ class Client extends EventEmitter {
540
544
  }
541
545
  },
542
546
  REQUEST_SUCCESS: (p, data) => {
547
+ // Reset keepalive on any protocol activity
548
+ this._resetKA();
543
549
  if (callbacks.length)
544
550
  callbacks.shift()(false, data);
545
551
  },
546
552
  REQUEST_FAILURE: (p) => {
553
+ // Reset keepalive on any protocol activity
554
+ this._resetKA();
547
555
  if (callbacks.length)
548
556
  callbacks.shift()(true);
549
557
  },
@@ -748,39 +756,71 @@ class Client extends EventEmitter {
748
756
 
749
757
  sock.pause();
750
758
 
751
- // TODO: check keepalive implementation
752
- // Keepalive-related
759
+ // Enhanced keepalive implementation with TCP-level and SSH protocol-level keepalives
753
760
  const kainterval = this.config.keepaliveInterval;
754
761
  const kacountmax = this.config.keepaliveCountMax;
755
762
  let kacount = 0;
756
763
  let katimer;
764
+ let lastActivity = Date.now();
765
+
766
+ // Enable TCP keepalive for network-level connection monitoring
767
+ // This helps detect dead connections even when SSH protocol keepalive is not used
768
+ if (typeof sock.setKeepAlive === 'function') {
769
+ sock.setKeepAlive(true, 10000); // Start probing after 10 seconds idle
770
+ }
771
+
757
772
  const sendKA = () => {
773
+ const now = Date.now();
774
+ const timeSinceLastActivity = now - lastActivity;
775
+
776
+ // If we've received any activity recently, reset counter and continue
777
+ if (timeSinceLastActivity < kainterval) {
778
+ kacount = 0;
779
+ return;
780
+ }
781
+
758
782
  if (++kacount > kacountmax) {
759
783
  clearInterval(katimer);
760
784
  if (sock.readable) {
761
- const err = new Error('Keepalive timeout');
785
+ const err = new Error(
786
+ `Keepalive timeout: no response after ${kacountmax} attempts`
787
+ );
762
788
  err.level = 'client-timeout';
763
789
  this.emit('error', err);
764
790
  sock.destroy();
765
791
  }
766
792
  return;
767
793
  }
794
+
768
795
  if (isWritable(sock)) {
769
- // Append dummy callback to keep correct callback order
770
- callbacks.push(resetKA);
796
+ if (debug) {
797
+ debug(
798
+ `Client: Sending keepalive (${kacount}/${kacountmax}), ` +
799
+ `${timeSinceLastActivity}ms since last activity`
800
+ );
801
+ }
802
+ // Send SSH protocol-level keepalive
803
+ // Use global request instead of relying on callbacks
771
804
  proto.ping();
805
+
806
+ // Don't rely on callback - we'll reset based on any activity
772
807
  } else {
773
808
  clearInterval(katimer);
774
809
  }
775
810
  };
811
+
776
812
  function resetKA() {
813
+ lastActivity = Date.now();
814
+ kacount = 0;
815
+
777
816
  if (kainterval > 0) {
778
- kacount = 0;
779
817
  clearInterval(katimer);
780
- if (isWritable(sock))
818
+ if (isWritable(sock)) {
781
819
  katimer = setInterval(sendKA, kainterval);
820
+ }
782
821
  }
783
822
  }
823
+
784
824
  this._resetKA = resetKA;
785
825
 
786
826
  const onDone = (() => {
@@ -629,35 +629,65 @@ class Protocol {
629
629
  sendPacket(this, this._packetRW.write.finalize(packet));
630
630
  }
631
631
  authPK(username, pubKey, keyAlgo, cbSign) {
632
+ if (pubKey && pubKey.certificate) {
633
+ return this.authPKCert(username, pubKey, keyAlgo, cbSign);
634
+ }
635
+ if (this._server)
636
+ throw new Error('Client-only method called in server mode');
637
+
638
+ pubKey = parseKey(pubKey);
639
+ if (pubKey instanceof Error)
640
+ throw new Error('Invalid key');
641
+
642
+ let keyType = pubKey.type;
643
+ if (keyType === 'ssh-rsa') {
644
+ for (const algo of ['rsa-sha2-512', 'rsa-sha2-256']) {
645
+ if (this._remoteHostKeyAlgorithms.includes(algo)) {
646
+ keyType = algo;
647
+ break;
648
+ }
649
+ }
650
+ }
651
+ pubKey = pubKey.getPublicSSH();
652
+
653
+ if (typeof keyAlgo === 'function') {
654
+ cbSign = keyAlgo;
655
+ keyAlgo = undefined;
656
+ }
657
+ if (!keyAlgo)
658
+ keyAlgo = keyType;
659
+
660
+ // For standard public keys, the algorithm name on the wire (keyAlgo)
661
+ // is the same as the algorithm used for signing (signAlgo).
662
+ this._authPKCommon(username, keyAlgo, pubKey, keyAlgo, cbSign);
663
+ }
664
+
665
+ authPKCert(username, pubKey, keyAlgo, cbSign) {
632
666
  if (this._server)
633
667
  throw new Error('Client-only method called in server mode');
634
668
 
635
- const origPubKey = pubKey;
636
-
637
669
  // Only parse if it's not already a parsed key object
638
- // Check for common key object properties
639
- if (typeof pubKey !== 'object' || pubKey === null ||
670
+ if (typeof pubKey !== 'object' || pubKey === null ||
640
671
  typeof pubKey.type !== 'string' || typeof pubKey.getPublicSSH !== 'function') {
641
672
  pubKey = parseKey(pubKey);
642
673
  if (pubKey instanceof Error)
643
674
  throw new Error('Invalid key');
644
675
  }
645
676
 
646
- let keyType = pubKey.type;
647
-
648
677
  // Check if this is a certificate-wrapped key
649
- let pubKeyData = pubKey.getPublicSSH();
678
+ let pubKeyData;
650
679
  let isCertificate = false;
651
680
  if (pubKey.getCertificateBuffer) {
652
- // This is a CertificateKey, use the certificate data instead
653
681
  pubKeyData = pubKey.getCertificateBuffer();
654
682
  isCertificate = true;
655
683
  this._debug && this._debug('Using SSH certificate for authentication');
684
+ } else {
685
+ pubKeyData = pubKey.getPublicSSH();
656
686
  }
657
687
 
658
- // Upgrade RSA to SHA2 variants if server supports them
659
- // signAlgo is the actual algorithm used for signing
660
- // NOTE: Order must match getKeyAlgos() in client.js - rsa-sha2-256 first
688
+ let keyType = pubKey.type;
689
+
690
+ // Determine the base signing algorithm (upgrading RSA if needed)
661
691
  let signAlgo = keyType;
662
692
  if (keyType === 'ssh-rsa') {
663
693
  for (const algo of ['rsa-sha2-256', 'rsa-sha2-512']) {
@@ -667,12 +697,10 @@ class Protocol {
667
697
  }
668
698
  }
669
699
  }
670
-
671
- // For certificates, append the cert suffix to the algorithm for the packet
672
- // but keep signAlgo as the base signing algorithm
700
+
701
+ // Determine the algorithm name to use in the packet (keyAlgo)
673
702
  if (isCertificate) {
674
703
  // Map the signing algorithm to certificate algorithm
675
- // e.g., rsa-sha2-512 -> rsa-sha2-512-cert-v01@openssh.com
676
704
  if (signAlgo === 'rsa-sha2-512') {
677
705
  keyType = 'rsa-sha2-512-cert-v01@openssh.com';
678
706
  } else if (signAlgo === 'rsa-sha2-256') {
@@ -692,7 +720,6 @@ class Protocol {
692
720
  }
693
721
  this._debug && this._debug(`Certificate key algorithm: ${keyType}`);
694
722
  } else {
695
- // For non-certificates, keyType should be signAlgo
696
723
  keyType = signAlgo;
697
724
  }
698
725
 
@@ -700,118 +727,119 @@ class Protocol {
700
727
  cbSign = keyAlgo;
701
728
  keyAlgo = undefined;
702
729
  }
703
-
704
- // For certificates, we must use the certificate algorithm regardless of what was passed
730
+
731
+ // For certificates, default to the determined cert type if not provided
705
732
  if (isCertificate) {
706
733
  keyAlgo = keyType;
707
734
  } else if (!keyAlgo) {
708
735
  keyAlgo = keyType;
709
736
  }
710
737
 
711
- const userLen = Buffer.byteLength(username);
712
- const algoLen = Buffer.byteLength(keyAlgo);
713
- // For certificates, signAlgo is the base signing algorithm (e.g., rsa-sha2-512)
714
- // For non-certificates, signAlgo equals keyAlgo
715
- const signAlgoLen = Buffer.byteLength(signAlgo);
716
- const pubKeyLen = pubKeyData.length;
717
- const sessionID = this._kex.sessionID;
718
- const sesLen = sessionID.length;
719
- const payloadLen =
720
- (cbSign ? 4 + sesLen : 0)
721
- + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen;
722
- let packet;
723
- let p;
724
- if (cbSign) {
725
- packet = Buffer.allocUnsafe(payloadLen);
726
- p = 0;
727
- writeUInt32BE(packet, sesLen, p);
728
- packet.set(sessionID, p += 4);
729
- p += sesLen;
730
- } else {
731
- packet = this._packetRW.write.alloc(payloadLen);
732
- p = this._packetRW.write.allocStart;
733
- }
734
-
735
- packet[p] = MESSAGE.USERAUTH_REQUEST;
736
-
737
- writeUInt32BE(packet, userLen, ++p);
738
- packet.utf8Write(username, p += 4, userLen);
739
-
740
- writeUInt32BE(packet, 14, p += userLen);
741
- packet.utf8Write('ssh-connection', p += 4, 14);
742
-
743
- writeUInt32BE(packet, 9, p += 14);
744
- packet.utf8Write('publickey', p += 4, 9);
745
-
746
- packet[p += 9] = (cbSign ? 1 : 0);
747
-
748
- writeUInt32BE(packet, algoLen, ++p);
749
- packet.utf8Write(keyAlgo, p += 4, algoLen);
738
+ this._authPKCommon(username, keyAlgo, pubKeyData, signAlgo, cbSign);
739
+ }
750
740
 
751
- writeUInt32BE(packet, pubKeyLen, p += algoLen);
752
- packet.set(pubKeyData, p += 4);
741
+ _authPKCommon(username, algo, pubKey, signAlgo, cbSign) {
742
+ const userLen = Buffer.byteLength(username);
743
+ const algoLen = Buffer.byteLength(algo);
744
+ const pubKeyLen = pubKey.length;
745
+
746
+ // Calculate payload length for the basic packet
747
+ // Header: MSG_USERAUTH_REQUEST (1) + user (4+len) + service (4+14) + method (4+9) + sign_flag (1) + algo (4+len) + key (4+len)
748
+ const basePayloadLen = 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen;
753
749
 
754
750
  if (!cbSign) {
755
- this._authsQueue.push('publickey');
751
+ // -----------------------------------------------------------------------
752
+ // Send "Check" Packet (Signature Flag = 0)
753
+ // -----------------------------------------------------------------------
754
+ const packet = this._packetRW.write.alloc(basePayloadLen);
755
+ let p = this._packetRW.write.allocStart;
756
756
 
757
- this._debug && this._debug(
758
- 'Outbound: Sending USERAUTH_REQUEST (publickey -- check)'
759
- );
760
- sendPacket(this, this._packetRW.write.finalize(packet));
761
- return;
762
- }
763
-
764
- cbSign(packet, (signature) => {
765
- signature = convertSignature(signature, signAlgo);
766
- if (signature === false)
767
- throw new Error('Error while converting handshake signature');
768
-
769
- const sigLen = signature.length;
770
- p = this._packetRW.write.allocStart;
771
- packet = this._packetRW.write.alloc(
772
- 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
773
- + 4 + signAlgoLen + 4 + sigLen
774
- );
775
-
776
- // TODO: simply copy from original "packet" to new `packet` to avoid
777
- // having to write each individual field a second time?
778
757
  packet[p] = MESSAGE.USERAUTH_REQUEST;
779
-
780
758
  writeUInt32BE(packet, userLen, ++p);
781
759
  packet.utf8Write(username, p += 4, userLen);
782
-
783
760
  writeUInt32BE(packet, 14, p += userLen);
784
761
  packet.utf8Write('ssh-connection', p += 4, 14);
785
-
786
762
  writeUInt32BE(packet, 9, p += 14);
787
763
  packet.utf8Write('publickey', p += 4, 9);
788
-
789
- packet[p += 9] = 1;
790
-
764
+ packet[p += 9] = 0; // Not signed
791
765
  writeUInt32BE(packet, algoLen, ++p);
792
- packet.utf8Write(keyAlgo, p += 4, algoLen);
793
-
766
+ packet.utf8Write(algo, p += 4, algoLen);
794
767
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
795
- packet.set(pubKeyData, p += 4);
768
+ packet.set(pubKey, p += 4);
796
769
 
797
- // Signature blob: length-prefixed (algorithm string + raw signature)
798
- // For RSA signatures, use the base signing algorithm (without cert suffix)
799
- // The cert suffix is only used in the public key algorithm field of the main packet
800
- writeUInt32BE(packet, 4 + signAlgoLen + 4 + sigLen, p += pubKeyLen);
770
+ this._authsQueue.push('publickey');
771
+ this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (publickey -- check)');
772
+ sendPacket(this, this._packetRW.write.finalize(packet));
773
+ return;
774
+ }
801
775
 
776
+ // -------------------------------------------------------------------------
777
+ // Signing Process
778
+ // -------------------------------------------------------------------------
779
+ const sessionID = this._kex.sessionID;
780
+ const sesLen = sessionID.length;
781
+
782
+ // Construct the data payload to be signed: SessionID + UserAuth Request Packet
783
+ // (Note: The packet construction here mimics the one above but sets sign_flag = 1)
784
+ const dataToSignLen = 4 + sesLen + basePayloadLen;
785
+ const dataToSign = Buffer.allocUnsafe(dataToSignLen);
786
+ let p = 0;
787
+
788
+ writeUInt32BE(dataToSign, sesLen, p);
789
+ dataToSign.set(sessionID, p += 4);
790
+ p += sesLen;
791
+
792
+ dataToSign[p] = MESSAGE.USERAUTH_REQUEST;
793
+ writeUInt32BE(dataToSign, userLen, ++p);
794
+ dataToSign.utf8Write(username, p += 4, userLen);
795
+ writeUInt32BE(dataToSign, 14, p += userLen);
796
+ dataToSign.utf8Write('ssh-connection', p += 4, 14);
797
+ writeUInt32BE(dataToSign, 9, p += 14);
798
+ dataToSign.utf8Write('publickey', p += 4, 9);
799
+ dataToSign[p += 9] = 1; // Signed
800
+ writeUInt32BE(dataToSign, algoLen, ++p);
801
+ dataToSign.utf8Write(algo, p += 4, algoLen);
802
+ writeUInt32BE(dataToSign, pubKeyLen, p += algoLen);
803
+ dataToSign.set(pubKey, p += 4);
804
+
805
+ cbSign(dataToSign, (signature) => {
806
+ signature = convertSignature(signature, signAlgo);
807
+ if (signature === false)
808
+ throw new Error('Error while converting handshake signature');
809
+
810
+ const sigLen = signature.length;
811
+ const signAlgoLen = Buffer.byteLength(signAlgo);
812
+
813
+ // -----------------------------------------------------------------------
814
+ // Send Signed Packet
815
+ // -----------------------------------------------------------------------
816
+ // We reconstruct the packet to send. It is identical to the data part of
817
+ // dataToSign (minus SessionID), plus the signature blob appended at the end.
818
+
819
+ const totalPacketLen = basePayloadLen + 4 + 4 + signAlgoLen + 4 + sigLen;
820
+ p = this._packetRW.write.allocStart;
821
+ const packet = this._packetRW.write.alloc(totalPacketLen);
822
+
823
+ // Copy the standard fields from our signing buffer (skipping sessionID)
824
+ // Offset of packet start in dataToSign is 4 + sesLen
825
+ const packetStartInSigData = 4 + sesLen;
826
+ bufferCopy(dataToSign, packet, packetStartInSigData, dataToSign.length, p);
827
+
828
+ // Move pointer to end of copied data
829
+ p += (dataToSign.length - packetStartInSigData);
830
+
831
+ // Append Signature Blob
832
+ // Length of signature structure (string len + string + blob len + blob)
833
+ writeUInt32BE(packet, 4 + signAlgoLen + 4 + sigLen, p);
834
+
802
835
  writeUInt32BE(packet, signAlgoLen, p += 4);
803
836
  packet.utf8Write(signAlgo, p += 4, signAlgoLen);
804
-
837
+
805
838
  writeUInt32BE(packet, sigLen, p += signAlgoLen);
806
839
  packet.set(signature, p += 4);
807
840
 
808
- // Servers shouldn't send packet type 60 in response to signed publickey
809
- // attempts, but if they do, interpret as type 60.
810
841
  this._authsQueue.push('publickey');
811
-
812
- this._debug && this._debug(
813
- 'Outbound: Sending USERAUTH_REQUEST (publickey)'
814
- );
842
+ this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (publickey)');
815
843
  sendPacket(this, this._packetRW.write.finalize(packet));
816
844
  });
817
845
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/ssh2",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "author": "Brian White <mscdex@mscdex.net>",
5
5
  "description": "SSH2 client and server modules written in pure JavaScript for node.js",
6
6
  "main": "./lib/index.js",