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.

672 lines 33.3 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 pure_js_signing_fix_1 = require("./pure-js-signing-fix"); class SSH2StreamsTransport extends events_1.EventEmitter { isOpenSSHRSAKey(keyString) { try { const lines = keyString.split('\n'); const base64Data = lines .filter(line => !line.startsWith('-----')) .join('') .replace(/\s/g, ''); const keyBuffer = Buffer.from(base64Data, 'base64'); let offset = 0; // Skip magic bytes "openssh-key-v1\0" offset += 15; // Skip cipher name const cipherNameLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += cipherNameLength; // Skip KDF name const kdfNameLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += kdfNameLength; // Skip KDF options const kdfOptionsLength = keyBuffer.readUInt32BE(offset); offset += 4; offset += kdfOptionsLength; // Skip number of keys offset += 4; // Read public key section const publicKeyLength = keyBuffer.readUInt32BE(offset); offset += 4; const publicKeyData = keyBuffer.subarray(offset, offset + publicKeyLength); // Parse the public key to determine type let pubOffset = 0; const keyTypeLength = publicKeyData.readUInt32BE(pubOffset); pubOffset += 4; const keyTypeName = publicKeyData.subarray(pubOffset, pubOffset + keyTypeLength).toString(); return keyTypeName === 'ssh-rsa'; } catch (error) { return false; } } constructor(config) { super(); this.connected = false; this.authenticated = false; this.sftpChannel = null; this.pendingChannels = new Map(); this.currentWindowSize = 0; this.initialWindowSize = 0; // Will be set based on server capabilities this.maxPacketSize = 32768; // Will be adjusted based on server response this.serverMaxWindowSize = 0; // Detected from server this.adaptiveMetrics = { successfulTransfers: 0, timeouts: 0, avgTransferSpeed: 0, lastAdjustmentTime: 0, avgResponseTime: 100, // Track server response time serverCapabilities: { maxPacketSize: 32768, // Will be detected from server preferredTimeout: 30000, // Will be adapted based on performance optimalChunkSize: 8192 // Will be learned from successful transfers } }; this.config = config; this.socket = new net_1.Socket(); this.socket.setNoDelay(true); // Disable Nagle's algorithm for low latency // Enable pure JS signing fix for broader compatibility (0, pure_js_signing_fix_1.enablePureJSSigningFix)(); // 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 via ssh2-streams - 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', `ssh2-streams key detection failed: ${keyCheckError instanceof Error ? keyCheckError.message : String(keyCheckError)}`); // If ssh2-streams fails, check if it's an RSA key by looking at the key content if (typeof config.privateKey === 'string') { if (config.privateKey.includes('BEGIN RSA PRIVATE KEY') || config.privateKey.includes('ssh-rsa') || (config.privateKey.includes('BEGIN OPENSSH PRIVATE KEY') && this.isOpenSSHRSAKey(config.privateKey))) { needsRSAFix = true; this.emit('debug', 'RSA key detected via content analysis - revolutionary proxy fix will be applied as fallback'); } else { this.emit('debug', 'Non-RSA key detected via content analysis - no proxy needed'); } } } } else { this.emit('debug', 'Password authentication - no RSA-SHA2 fix needed'); } if (needsRSAFix) { // Apply revolutionary proxy fix for ALL RSA keys (both OpenSSH and PKCS#1) this.emit('debug', 'RSA key detected - using revolutionary proxy fix for RSA-SHA2 compatibility'); this.ssh = (0, revolutionary_proxy_fix_1.applyRevolutionaryProxyFix)(originalSSH, (msg) => this.emit('debug', msg)); } else { // No RSA-SHA2 fix needed for non-RSA keys (ECDSA, Ed25519, etc.) this.emit('debug', 'Non-RSA key detected - using standard ssh2-streams'); 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)}`); // Adaptive window size detection this.currentWindowSize = info.window; this.serverMaxWindowSize = info.window; // Auto-adjust initial window size based on server capability this.initialWindowSize = this.calculateOptimalWindowSize(info.window); this.emit('debug', `Server window: ${info.window} bytes, calculated optimal: ${this.initialWindowSize} bytes`); // Only adjust if server window is significantly smaller than our target if (this.currentWindowSize < this.initialWindowSize * 0.8) { const adjustment = this.initialWindowSize - this.currentWindowSize; this.emit('debug', `Adjusting window from ${this.currentWindowSize} to ${this.initialWindowSize} (+${adjustment} bytes)`); this.ssh.channelWindowAdjust(0, adjustment); this.currentWindowSize = this.initialWindowSize; } else { this.emit('debug', `Using server window size: ${this.currentWindowSize} bytes (no adjustment needed)`); this.initialWindowSize = this.currentWindowSize; // Use what server provides } // CRITICAL FIX: Track maximum packet size to prevent exceeding SSH channel limits this.maxPacketSize = info.packetSize || 32768; // Default to 32KB (server constraint) // SFTP WRITE overhead: 4(pkt_len) + 1(ssh_msg) + 4(channel) + 4(sftp_len) + 1(sftp_type) + 4(req_id) + 4(handle_len) + handle + 8(offset) + 4(data_len) // = 34 bytes + handle_length (typically 4-8 bytes, but can be up to 256 bytes per SFTP spec) const baseSftpOverhead = 34; // Fixed overhead without handle const maxHandleSize = 32; // Conservative estimate for handle size (SFTP spec allows up to 256, but most servers use 4-8) const sftpOverhead = baseSftpOverhead + maxHandleSize; // 66 bytes total conservative overhead const maxDataSize = this.maxPacketSize - sftpOverhead; this.emit('debug', `SSH max packet size: ${this.maxPacketSize}, SFTP overhead: ${sftpOverhead}B (${baseSftpOverhead}B + ${maxHandleSize}B handle), max data chunk: ${maxDataSize} bytes`); // Warn if the default chunk sizes might be too large if (maxDataSize < 32768) { this.emit('debug', `⚠️ SSH packet size limit (${this.maxPacketSize}) restricts SFTP chunks to ${maxDataSize} bytes`); } // 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, 131072); // Increased from 32KB to 128KB for better performance }); 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('CHANNEL_WINDOW_ADJUST:0', (bytesToAdd) => { // CRITICAL FIX: Handle server's window adjustment messages // This fixes the 32KB chunk timeout bug by properly tracking available window space const oldWindow = this.currentWindowSize; this.currentWindowSize += bytesToAdd; this.emit('debug', `Channel window adjusted: ${oldWindow} + ${bytesToAdd} = ${this.currentWindowSize} bytes`); }); 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; // Use enhanced key parser exclusively (pure JavaScript) this.emit('debug', 'Using pure JavaScript key parser (no dependencies)'); parsedKey = (0, enhanced_key_parser_1.parseKey)(this.config.privateKey, this.config.passphrase); if (parsedKey) { this.emit('debug', 'Pure JavaScript key parsing successful'); } else { throw new Error('Key parsing failed: Pure JavaScript parser could not parse the provided key'); } const publicKeySSH = parsedKey.getPublicSSH(); this.emit('debug', `Public key SSH format generated: ${publicKeySSH.length} bytes`); // Pure JS signing fix handles all key types including RSA-SHA2 this.emit('debug', `Using pure JS signing for ${parsedKey.type} key`); const signatureCallback = (buf, cb) => { try { // For RSA keys, use rsa-sha2-256 algorithm to match revolutionary proxy expectations const algorithm = (parsedKey.type === 'ssh-rsa') ? 'rsa-sha2-256' : undefined; const signature = parsedKey.sign(buf, algorithm); cb(signature); } catch (error) { throw new Error(`Pure JS 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) => { const oldWindow = this.currentWindowSize; // Let SSH handle window management internally - don't interfere // Just track current window for concurrency calculations this.emit('debug', `Sending packet: ${data.length} bytes (window tracking: ${this.currentWindowSize} bytes available)`); // Don't manually deplete window - ssh2-streams handles this correctly // Our role is just to provide window size for concurrency calculations // Add warning if window is getting low if (this.currentWindowSize < 65536) { // Less than 64KB remaining this.emit('debug', `⚠️ Channel window getting low: ${this.currentWindowSize} bytes remaining`); } this.ssh.channelData(info.recipient, data); }, end: () => { this.ssh.channelClose(info.recipient); } }); return channel; } connect() { return new Promise((resolve, reject) => { const connectTimeout = this.config?.connectTimeout ?? 30000; const timeout = setTimeout(() => { reject(new Error(`Connection timeout after ${connectTimeout}ms`)); }, connectTimeout); 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 sftpTimeout = this.config?.connectTimeout ?? 30000; const timeout = setTimeout(() => { reject(new Error(`SFTP open timeout after ${sftpTimeout}ms`)); }, sftpTimeout); // 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 adaptive window size based on server capabilities const sessionWindow = this.initialWindowSize || 524288; // Use adaptive or fallback to 512KB const sessionPacket = this.maxPacketSize; // Use detected packet size this.ssh.session(0, sessionWindow, sessionPacket); }); } disconnect() { if (this.socket && !this.socket.destroyed) { this.socket.destroy(); } this.connected = false; this.authenticated = false; } isConnected() { return this.connected && this.authenticated; } getCurrentWindowSize() { return this.currentWindowSize; } getSafeConcurrency(chunkSize, maxConcurrency = 64) { if (this.currentWindowSize <= 0) { return 1; // Conservative fallback } // Calculate how many chunks of this size can fit in the current window const windowBasedConcurrency = Math.floor(this.currentWindowSize / chunkSize); // Use the minimum of window-based limit and desired max concurrency const safeConcurrency = Math.min(maxConcurrency, Math.max(1, windowBasedConcurrency)); this.emit('debug', `Dynamic concurrency: window=${this.currentWindowSize}B, chunk=${chunkSize}B, safe=${safeConcurrency}x (max ${maxConcurrency}x)`); return safeConcurrency; } getOptimalConcurrency(chunkSize) { // Adaptive concurrency based on current performance metrics const baseTargetUsage = 0.8; // 80% window usage const targetWindowUsage = Math.floor(this.currentWindowSize * baseTargetUsage); const theoreticalMax = Math.floor(targetWindowUsage / chunkSize); // Adaptive practical limits based on server performance let practicalMax = 8; // Very conservative default for stability // Adjust based on success rate const totalOps = this.adaptiveMetrics.successfulTransfers + this.adaptiveMetrics.timeouts; if (totalOps > 5) { const successRate = this.adaptiveMetrics.successfulTransfers / totalOps; if (successRate > 0.95) { // Excellent performance - allow higher concurrency (but stay conservative) practicalMax = Math.min(12, practicalMax + 2); // Gradual increase } else if (successRate < 0.8) { // Poor performance - reduce concurrency practicalMax = Math.max(2, Math.floor(practicalMax * 0.7)); } } const optimalConcurrency = Math.min(theoreticalMax, practicalMax, Math.max(1, theoreticalMax)); this.emit('debug', `Adaptive concurrency: window=${this.currentWindowSize}B, chunk=${chunkSize}B, theoretical=${theoreticalMax}, practical=${practicalMax}, optimal=${optimalConcurrency}`); return optimalConcurrency; } /** * Get maximum safe SFTP chunk size based on SSH packet size limits */ getMaxSafeChunkSize() { // SFTP WRITE overhead: SSH headers + SFTP headers + handle (conservative estimate) const baseSftpOverhead = 34; // Fixed overhead without handle const maxHandleSize = 32; // Conservative estimate for handle size const sftpOverhead = baseSftpOverhead + maxHandleSize; // 66 bytes total return Math.max(8192, this.maxPacketSize - sftpOverhead); // At least 8KB, but respect packet limits } /** * Calculate adaptive timeout based on server performance */ getAdaptiveTimeout(operationType, dataSize = 0) { const baseTimeout = this.adaptiveMetrics.serverCapabilities.preferredTimeout; const responseTime = this.adaptiveMetrics.avgResponseTime; switch (operationType) { case 'control': // Control operations should be fast - base on response time return Math.max(5000, responseTime * 20); case 'data': // Data operations scale with size and server performance const sizeMultiplier = Math.max(1, Math.log10(dataSize / 1024)); // Scale with data size const performanceMultiplier = responseTime > 1000 ? 2 : 1; // Slower servers need more time return Math.max(10000, baseTimeout * sizeMultiplier * performanceMultiplier); case 'connection': // Connection operations based on server responsiveness return Math.max(15000, responseTime * 100); default: return baseTimeout; } } /** * Calculate optimal chunk size based on server capabilities and performance */ getAdaptiveChunkSize(transferType, bytesProcessed = 0) { const serverOptimal = this.adaptiveMetrics.serverCapabilities.optimalChunkSize; const maxSafe = this.getMaxSafeChunkSize(); // Different strategies for upload vs download if (transferType === 'upload') { // Uploads can be more aggressive based on server performance const successRate = this.getSuccessRate(); if (successRate > 0.95) { return Math.min(maxSafe, serverOptimal * 4); // Up to 4x for excellent performance } else if (successRate > 0.8) { return Math.min(maxSafe, serverOptimal * 2); // 2x for good performance } else { return Math.min(maxSafe, serverOptimal); // Conservative for poor performance } } else { // Downloads are more conservative due to server limitations we observed const baseSize = Math.min(serverOptimal, maxSafe / 4); // Start very conservative // Gradually increase based on bytes processed successfully if (bytesProcessed > 1024 * 1024) { // After 1MB return Math.min(maxSafe / 2, baseSize * 2); } else if (bytesProcessed > 256 * 1024) { // After 256KB return Math.min(maxSafe / 4, baseSize * 1.5); } else { return baseSize; } } } /** * Get current success rate for adaptive decisions */ getSuccessRate() { const total = this.adaptiveMetrics.successfulTransfers + this.adaptiveMetrics.timeouts; return total > 0 ? this.adaptiveMetrics.successfulTransfers / total : 1; } /** * Calculate optimal window size based on server capabilities */ calculateOptimalWindowSize(serverWindow) { // Fully adaptive calculation based on server-provided window // Use server capabilities without hardcoded limits const serverBasedMinimum = Math.max(32768, Math.floor(serverWindow * 0.1)); // 10% of server window as minimum const serverBasedMaximum = Math.min(serverWindow, Math.floor(serverWindow * 1.5)); // 150% of server window as maximum const targetWindow = Math.floor(serverWindow * 0.8); // 80% utilization target const optimalSize = Math.max(serverBasedMinimum, Math.min(serverBasedMaximum, targetWindow)); this.emit('debug', `Window calculation: server=${serverWindow}, target=${targetWindow}, optimal=${optimalSize}`); return optimalSize; } /** * Dynamically adjust parameters based on transfer performance */ adaptPerformance(transferSpeed, hadTimeout, responseTimeMs) { const now = Date.now(); // Update metrics if (hadTimeout) { this.adaptiveMetrics.timeouts++; // Increase preferred timeout on repeated timeouts this.adaptiveMetrics.serverCapabilities.preferredTimeout = Math.min(120000, this.adaptiveMetrics.serverCapabilities.preferredTimeout * 1.2); } else { this.adaptiveMetrics.successfulTransfers++; this.adaptiveMetrics.avgTransferSpeed = (this.adaptiveMetrics.avgTransferSpeed + transferSpeed) / 2; // Update response time tracking if (responseTimeMs) { this.adaptiveMetrics.avgResponseTime = (this.adaptiveMetrics.avgResponseTime + responseTimeMs) / 2; } // Learn optimal chunk size from successful transfers if (transferSpeed > 10) { // Good speed - try larger chunks this.adaptiveMetrics.serverCapabilities.optimalChunkSize = Math.min(this.getMaxSafeChunkSize(), this.adaptiveMetrics.serverCapabilities.optimalChunkSize * 1.1); } } // Only adjust every 30 seconds to avoid oscillation if (now - this.adaptiveMetrics.lastAdjustmentTime < 30000) { return; } this.adaptiveMetrics.lastAdjustmentTime = now; // Calculate success rate const totalOps = this.adaptiveMetrics.successfulTransfers + this.adaptiveMetrics.timeouts; const successRate = totalOps > 0 ? this.adaptiveMetrics.successfulTransfers / totalOps : 1; this.emit('debug', `Performance metrics: success=${successRate.toFixed(2)}, speed=${this.adaptiveMetrics.avgTransferSpeed.toFixed(2)}MB/s, timeouts=${this.adaptiveMetrics.timeouts}`); // Adapt window size based on performance - fully adaptive to server capabilities if (successRate < 0.8 && this.adaptiveMetrics.timeouts > 2) { // Poor performance - reduce window size based on server capabilities const serverMinimum = Math.max(32768, Math.floor(this.serverMaxWindowSize * 0.1)); const newWindow = Math.max(serverMinimum, Math.floor(this.initialWindowSize * 0.7)); if (newWindow !== this.initialWindowSize) { this.emit('debug', `Reducing window size due to timeouts: ${this.initialWindowSize} → ${newWindow}`); this.initialWindowSize = newWindow; } } else if (successRate > 0.95 && this.adaptiveMetrics.avgTransferSpeed > 50) { // Excellent performance - try increasing window up to server maximum const newWindow = Math.min(this.serverMaxWindowSize, Math.floor(this.initialWindowSize * 1.2)); if (newWindow !== this.initialWindowSize) { this.emit('debug', `Increasing window size due to good performance: ${this.initialWindowSize} → ${newWindow}`); this.initialWindowSize = newWindow; } } } /** * Send SSH keepalive ping - uses connection validation */ async ping() { if (!this.connected || !this.authenticated) { throw new Error('Not connected'); } return new Promise((resolve, reject) => { try { // Simple connection validation - the ssh2-streams library doesn't expose // a ping method, so we just validate the connection state this.emit('debug', 'Validating SSH connection for keepalive'); if (this.socket && !this.socket.destroyed && this.connected && this.authenticated) { this.emit('debug', 'SSH connection validation successful'); resolve(); } else { reject(new Error('SSH connection validation failed')); } } catch (err) { reject(err); } }); } } exports.SSH2StreamsTransport = SSH2StreamsTransport; //# sourceMappingURL=ssh2-streams-transport.js.map