UNPKG

pure-js-sftp

Version:

A pure JavaScript SFTP client with revolutionary RSA-SHA2 compatibility fixes. Zero native dependencies, built on ssh2-streams with 100% SSH key support.

387 lines 17.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SSH2StreamsTransport = void 0; const events_1 = require("events"); const net_1 = require("net"); const ssh2Streams = __importStar(require("ssh2-streams")); const revolutionary_proxy_fix_1 = require("./revolutionary-proxy-fix"); const enhanced_key_parser_1 = require("./enhanced-key-parser"); const rsa_sha2_wrapper_1 = require("./rsa-sha2-wrapper"); class SSH2StreamsTransport extends events_1.EventEmitter { constructor(config) { super(); this.connected = false; this.authenticated = false; this.sftpChannel = null; this.pendingChannels = new Map(); this.config = config; this.socket = new net_1.Socket(); // Use modern SSH algorithms compatible with OpenSSH 8.0+ const defaultAlgorithms = { kex: [ 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', // Fallback for ssh2-streams compatibility 'diffie-hellman-group14-sha1' ], cipher: [ 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', 'aes128-ctr', 'aes192-ctr', 'aes256-ctr', // Fallback ciphers 'aes128-cbc', 'aes256-cbc' ], hmac: [ 'hmac-sha2-256', 'hmac-sha2-512', // Fallback MAC for compatibility 'hmac-sha1' ], compress: ['none'] }; // Create SSH2Stream instance const originalSSH = new ssh2Streams.SSH2Stream({ server: false, algorithms: config.algorithms || defaultAlgorithms }); // Apply RSA-SHA2 proxy fix only when needed (RSA keys with modern SSH servers) let needsRSAFix = false; if (config.privateKey) { try { // Check if this is an RSA key that might need the revolutionary fix const parsedKey = ssh2Streams.utils.parseKey(config.privateKey, config.passphrase); if (parsedKey) { const key = Array.isArray(parsedKey) ? parsedKey[0] : parsedKey; if (key && key.type === 'ssh-rsa') { needsRSAFix = true; this.emit('debug', 'RSA key detected - revolutionary proxy fix will be applied for modern SSH server compatibility'); } else { this.emit('debug', `${key?.type || 'Unknown'} key detected - no RSA-SHA2 fix needed`); } } } catch (keyCheckError) { this.emit('debug', `Key type detection failed: ${keyCheckError instanceof Error ? keyCheckError.message : String(keyCheckError)} - will apply proxy as fallback`); // If we can't determine the key type, apply the proxy as a safety measure needsRSAFix = true; } } else { this.emit('debug', 'Password authentication - no RSA-SHA2 fix needed'); } if (needsRSAFix) { this.ssh = (0, revolutionary_proxy_fix_1.applyRevolutionaryProxyFix)(originalSSH, (msg) => this.emit('debug', msg)); } else { this.ssh = originalSSH; } this.setupEventHandlers(); } setupEventHandlers() { // Socket events this.socket.on('connect', () => { this.emit('debug', 'Socket connected'); }); this.socket.on('error', (err) => { this.emit('error', err); }); this.socket.on('close', () => { this.connected = false; this.authenticated = false; this.emit('close'); }); // SSH events this.ssh.on('ready', () => { this.emit('debug', 'SSH handshake complete, requesting ssh-userauth'); this.ssh.service('ssh-userauth'); }); this.ssh.on('SERVICE_ACCEPT', (serviceName) => { this.emit('debug', `Service accepted: ${serviceName}`); if (serviceName === 'ssh-userauth') { this.startAuthentication(); } }); this.ssh.on('USERAUTH_SUCCESS', () => { this.emit('debug', 'Authentication successful'); this.authenticated = true; this.connected = true; this.emit('ready'); }); this.ssh.on('USERAUTH_FAILURE', (methods, _partial) => { this.emit('error', new Error(`Authentication failed. Available methods: ${methods.join(', ')}`)); }); this.ssh.on('USERAUTH_BANNER', (_message) => { this.emit('debug', 'Server banner received'); }); // Channel events this.ssh.on('CHANNEL_OPEN_CONFIRMATION:0', (info) => { this.emit('debug', `Channel opened successfully: ${JSON.stringify(info)}`); // Adjust channel window size if it's 0 if (info.window === 0) { this.emit('debug', 'Channel window size is 0, adjusting to 65536 bytes'); this.ssh.channelWindowAdjust(0, 65536); } // Create channel wrapper immediately and store it const channel = this.createChannelWrapper(info); this.sftpChannel = channel; const callback = this.pendingChannels.get(0); if (callback) { callback(undefined, channel); this.pendingChannels.delete(0); } else { this.emit('debug', 'WARNING: No pending callback for channel 0'); } }); this.ssh.on('CHANNEL_SUCCESS:0', () => { this.emit('debug', 'SFTP subsystem started successfully'); // Also adjust window here to ensure we can receive SFTP data this.emit('debug', 'Adjusting window size after SFTP subsystem start'); this.ssh.channelWindowAdjust(0, 32768); }); this.ssh.on('CHANNEL_FAILURE:0', () => { this.emit('error', new Error('SFTP subsystem request failed')); }); this.ssh.on('CHANNEL_DATA:0', (data) => { this.emit('debug', `Received CHANNEL_DATA:0: ${data.length} bytes`); if (this.sftpChannel) { this.sftpChannel.emit('data', data); } else { this.emit('debug', 'WARNING: Received channel data but no SFTP channel to forward to'); } }); this.ssh.on('CHANNEL_CLOSE:0', () => { if (this.sftpChannel) { this.sftpChannel.emit('close'); } }); this.ssh.on('GLOBAL_REQUEST', () => { // Ignore global requests like host key announcements }); this.ssh.on('error', (err) => { this.emit('error', err); }); this.ssh.on('close', () => { this.emit('close'); }); // Standard SSH stream piping - RSA-SHA2 fix is applied via proxy this.socket.pipe(this.ssh).pipe(this.socket); } startAuthentication() { if (this.config.password) { this.emit('debug', 'Starting password authentication'); this.ssh.authPassword(this.config.username, this.config.password); } else if (this.config.privateKey) { this.emit('debug', 'Starting public key authentication'); try { let parsedKey = null; // First try ssh2-streams parser try { this.emit('debug', 'Attempting ssh2-streams key parsing'); const parsedKeys = ssh2Streams.utils.parseKey(this.config.privateKey, this.config.passphrase); // Check if parsing was successful if (parsedKeys && (Array.isArray(parsedKeys) ? parsedKeys.length > 0 : true)) { const key = Array.isArray(parsedKeys) ? parsedKeys[0] : parsedKeys; if (key && typeof key.getPublicSSH === 'function') { parsedKey = key; this.emit('debug', 'ssh2-streams key parsing successful'); } } } catch (ssh2StreamsError) { this.emit('debug', `ssh2-streams parsing failed: ${ssh2StreamsError instanceof Error ? ssh2StreamsError.message : String(ssh2StreamsError)}`); } // If ssh2-streams failed, try our enhanced key parser if (!parsedKey) { this.emit('debug', 'Falling back to enhanced key parser'); parsedKey = (0, enhanced_key_parser_1.parseKey)(this.config.privateKey, this.config.passphrase); if (parsedKey) { this.emit('debug', 'Enhanced key parsing successful'); } else { throw new Error('Key parsing failed: Enhanced parser with sshpk fallback could not parse the provided key'); } } const publicKeySSH = parsedKey.getPublicSSH(); this.emit('debug', `Public key SSH format generated: ${publicKeySSH.length} bytes`); // Use RSA-SHA2 signature wrapper for RSA keys to ensure modern cryptographic signatures let signatureCallback; if (parsedKey.type === 'ssh-rsa') { this.emit('debug', 'Using RSA-SHA2 signature wrapper'); try { signatureCallback = (0, rsa_sha2_wrapper_1.createRSASHA2SignatureCallback)(parsedKey, this.config.privateKey, this.config.passphrase, 'sha256'); } catch (wrapperError) { this.emit('debug', `RSA-SHA2 wrapper failed, using original signing: ${wrapperError instanceof Error ? wrapperError.message : String(wrapperError)}`); signatureCallback = (buf, cb) => { try { const signature = parsedKey.sign(buf); cb(signature); } catch (error) { throw new Error(`Signing failed: ${error instanceof Error ? error.message : String(error)}`); } }; } } else { // Use original signing for non-RSA keys (Ed25519, ECDSA) this.emit('debug', `Using original signing for ${parsedKey.type} key`); signatureCallback = (buf, cb) => { try { const signature = parsedKey.sign(buf); cb(signature); } catch (error) { throw new Error(`Signing failed: ${error instanceof Error ? error.message : String(error)}`); } }; } // Authenticate with public key using appropriate signature method this.ssh.authPK(this.config.username, publicKeySSH, (buf, cb) => { try { this.emit('debug', `Generating signature for ${buf.length} bytes of challenge data`); signatureCallback(buf, (signature) => { this.emit('debug', `Signature generated: ${signature?.length || 'undefined'} bytes`); cb(signature); }); } catch (error) { this.emit('error', new Error(`Authentication signing failed: ${error instanceof Error ? error.message : String(error)}`)); } }); } catch (error) { this.emit('error', new Error(`Key parsing failed: ${error instanceof Error ? error.message : String(error)}`)); } } else { this.emit('error', new Error('No authentication method provided: must provide either password or privateKey')); } } createChannelWrapper(info) { const channelEmitter = new events_1.EventEmitter(); // Extend EventEmitter with our methods const channel = Object.assign(channelEmitter, { write: (data) => { this.emit('debug', `Sending channel data: ${data.length} bytes`); this.ssh.channelData(info.recipient, data); }, end: () => { this.ssh.channelClose(info.recipient); } }); return channel; } connect() { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Connection timeout')); }, 30000); this.once('ready', () => { clearTimeout(timeout); resolve(); }); this.once('error', (err) => { clearTimeout(timeout); reject(err); }); // Connect to server this.socket.connect(this.config.port || 22, this.config.host); }); } async openSFTP() { if (!this.connected || !this.authenticated) { throw new Error('Not connected or authenticated'); } return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('SFTP open timeout')); }, 15000); // Use channel 0 for session (ssh2-streams default) const channelId = 0; this.pendingChannels.set(channelId, (err, channel) => { clearTimeout(timeout); if (err) { reject(err); return; } if (!channel) { reject(new Error('No channel received')); return; } // Request SFTP subsystem this.ssh.subsystem(0, 'sftp', true); // Wait for subsystem success const successHandler = () => { this.ssh.removeListener('CHANNEL_FAILURE:0', failureHandler); resolve(channel); }; const failureHandler = () => { this.ssh.removeListener('CHANNEL_SUCCESS:0', successHandler); reject(new Error('SFTP subsystem request failed')); }; this.ssh.once('CHANNEL_SUCCESS:0', successHandler); this.ssh.once('CHANNEL_FAILURE:0', failureHandler); }); // Open session channel with proper window size and packet size this.ssh.session(0, 65536, 32768); }); } disconnect() { if (this.socket && !this.socket.destroyed) { this.socket.destroy(); } this.connected = false; this.authenticated = false; } isConnected() { return this.connected && this.authenticated; } } exports.SSH2StreamsTransport = SSH2StreamsTransport; //# sourceMappingURL=ssh2-streams-transport.js.map