UNPKG

iop

Version:
434 lines 19.8 kB
"use strict"; // SSH client wrapper logic will go here 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SSHClient = exports.getSSHCredentials = void 0; const ssh2_promise_1 = __importDefault(require("ssh2-promise")); const utils_1 = require("./utils"); Object.defineProperty(exports, "getSSHCredentials", { enumerable: true, get: function () { return utils_1.getSSHCredentials; } }); const promises_1 = require("fs/promises"); class SSHClient { constructor(connectOptions) { this.verbose = false; this.suppressConnectionErrors = false; this.connectOptions = connectOptions; this.ssh = new ssh2_promise_1.default(this.connectOptions); this.host = connectOptions.host; } static async create(options) { const connectOpts = { host: options.host, port: options.port || 22, username: options.username, password: options.password, passphrase: options.passphrase, agent: options.agent, }; // Skip host key verification for fresh servers if (options.skipHostKeyVerification) { connectOpts.hostHash = 'sha256'; connectOpts.hostVerifier = () => true; } // ssh2-promise allows 'identity' for path, ssh2 'privateKey' for content const ssh2PromiseConfig = { ...connectOpts }; if (options.identity) { ssh2PromiseConfig.identity = options.identity; } else if (options.privateKey) { ssh2PromiseConfig.privateKey = options.privateKey; } // Add ssh2-promise specific options ssh2PromiseConfig.reconnect = true; ssh2PromiseConfig.reconnectDelay = 2000; ssh2PromiseConfig.reconnectTries = 5; const client = new SSHClient(ssh2PromiseConfig); client.setVerbose(options.verbose || false); client.setSuppressConnectionErrors(options.suppressConnectionErrors || false); return client; } setVerbose(verbose) { this.verbose = verbose; } setSuppressConnectionErrors(suppress) { this.suppressConnectionErrors = suppress; } async connect() { try { await this.ssh.connect(); if (this.verbose) { console.log(`SSH connection established to ${this.host}`); } } catch (err) { if (!this.suppressConnectionErrors) { console.error(`SSH connection failed to ${this.host}:`, err); } throw err; } } async exec(command) { // Check if this is a sensitive command containing credentials const isSensitiveCommand = command.includes("password") || command.includes("login") || command.includes('echo "') || command.includes("cat >"); // Create a sanitized version for logging const sanitizedCommand = isSensitiveCommand ? command .replace(/echo ".*?"/g, 'echo "***REDACTED***"') .replace(/cat > .*?<< ['"]?EOF/g, "cat > ***REDACTED*** << EOF") : command; if (this.verbose) { console.log(`[${this.host}] Executing: ${sanitizedCommand}`); } return new Promise(async (resolve, reject) => { try { // Use shell command with explicit exit code checking // This command runs the original command and captures both stdout/stderr and exit code const wrappedCommand = `${command}; echo "EXIT_CODE:$?"`; const result = await this.ssh.exec(wrappedCommand); // Parse the result to extract exit code const lines = result.split('\n'); let exitCodeLine = ''; let output = ''; // Find the exit code line (should be last non-empty line) for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].trim()) { if (lines[i].startsWith('EXIT_CODE:')) { exitCodeLine = lines[i]; // Everything before this line is the actual output output = lines.slice(0, i).join('\n'); break; } } } const exitCode = exitCodeLine ? parseInt(exitCodeLine.replace('EXIT_CODE:', '')) : 0; if (exitCode === 0) { // Command succeeded resolve(output); } else { // Command failed if (this.verbose) { console.error(`Command "${sanitizedCommand}" on ${this.host} failed with exit code ${exitCode}`); } reject(new Error(`Command failed with exit code ${exitCode}: ${output}`)); } } catch (err) { // Handle the original ssh2-promise behavior for stderr as error const errorMessage = String(err); // If this is just a Docker warning or similar, treat as success if (errorMessage.includes("WARNING: No swap limit support") || (errorMessage.includes("WARNING:") && !errorMessage.includes("error") && !errorMessage.includes("failed"))) { if (this.verbose) { console.warn(`[${this.host}] Command "${sanitizedCommand}" succeeded but had warnings:\n${errorMessage}`); } resolve(errorMessage); } else { // This is a real error if (this.verbose) { console.error(`Error executing command "${sanitizedCommand}" on ${this.host}:`, errorMessage); } reject(err); } } }); } // Helper method to sanitize potentially sensitive output sanitizeErrorOutput(output) { // Replace potential Docker login password in command output = output.replace(/echo ".*?" \| docker login/g, 'echo "***REDACTED***" | docker login'); // Replace other sensitive patterns output = output.replace(/password=.*?( |$|\n)/gi, "password=***REDACTED*** "); output = output.replace(/password:.*?( |$|\n)/gi, "password:***REDACTED*** "); output = output.replace(/--password[-_]?\w*\s+["']?[\w!@#$%^&*(),.?;:|<>]*["']?/gi, "--password ***REDACTED***"); return output; } async close() { try { await this.ssh.close(); if (this.verbose) { console.log(`SSH connection closed to ${this.host}`); } } catch (err) { if (this.verbose) { console.error(`Error closing SSH connection to ${this.host}:`, err); } } } /** * Detects the platform architecture of the remote server * @returns Promise<string> Platform string (e.g., "linux/amd64", "linux/arm64") */ async detectServerPlatform() { if (this.platformCache) { return this.platformCache; } try { if (this.verbose) { console.log(`[${this.host}] Detecting server platform...`); } // Get architecture and OS information const arch = await this.exec("uname -m"); const os = await this.exec("uname -s"); // Map common architectures to Docker platform format const cleanArch = arch.trim().toLowerCase(); const cleanOs = os.trim().toLowerCase(); let dockerArch; switch (cleanArch) { case "x86_64": case "amd64": dockerArch = "amd64"; break; case "aarch64": case "arm64": dockerArch = "arm64"; break; case "armv7l": case "armv7": dockerArch = "arm/v7"; break; case "armv6l": case "armv6": dockerArch = "arm/v6"; break; case "i386": case "i686": dockerArch = "386"; break; default: // Default to amd64 for unknown architectures if (this.verbose) { console.warn(`[${this.host}] Unknown architecture '${cleanArch}', defaulting to amd64`); } dockerArch = "amd64"; } // Construct platform string (typically linux/amd64, linux/arm64, etc.) let platform; if (cleanOs === "linux") { platform = `linux/${dockerArch}`; } else if (cleanOs === "darwin") { platform = `darwin/${dockerArch}`; } else { // Default to linux for unknown OS if (this.verbose) { console.warn(`[${this.host}] Unknown OS '${cleanOs}', defaulting to linux`); } platform = `linux/${dockerArch}`; } // Cache the result this.platformCache = platform; if (this.verbose) { console.log(`[${this.host}] Detected platform: ${platform} (arch: ${cleanArch}, os: ${cleanOs})`); } return platform; } catch (error) { if (this.verbose) { console.warn(`[${this.host}] Failed to detect server platform, defaulting to linux/amd64:`, error); } // Default fallback this.platformCache = "linux/amd64"; return this.platformCache; } } /** * Upload a file to the remote server using native rsync or scp command (fastest for large files) */ async uploadFile(localPath, remotePath, onProgress) { if (this.verbose) { console.log(`[${this.host}] Uploading ${localPath} to ${remotePath} via rsync or SCP`); } try { // Get file stats for progress info const stats = await (0, promises_1.stat)(localPath); const totalSize = stats.size; if (this.verbose) { console.log(`[${this.host}] File size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`); } // Create remote directory first via SSH await this.ssh.exec(`mkdir -p $(dirname "${remotePath}")`); // Use native rsync or scp command for maximum speed // Try rsync first (faster for large files), fall back to scp const rsyncCommand = `rsync -avz --progress "${localPath}" ${this.connectOptions.username}@${this.host}:"${remotePath}"`; const scpCommand = `scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -C "${localPath}" ${this.connectOptions.username}@${this.host}:"${remotePath}"`; if (this.verbose) { console.log(`[${this.host}] Trying rsync for faster transfer`); } // Execute rsync or scp command directly const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process"))); try { // Try rsync first with progress monitoring try { if (this.verbose) { console.log(`[${this.host}] Executing rsync: ${rsyncCommand.replace(localPath, "***")}`); } await new Promise((resolve, reject) => { const rsyncArgs = ['-avz', '--progress', localPath, `${this.connectOptions.username}@${this.host}:${remotePath}`]; const rsyncProcess = spawn('rsync', rsyncArgs); let lastProgress = 0; rsyncProcess.stdout?.on('data', (data) => { const output = data.toString(); if (this.verbose) { console.log(`[${this.host}] rsync stdout:`, output.trim()); } if (onProgress) { // Parse rsync progress output const progressMatch = output.match(/(\d+)\s+(\d+)%\s+(\d+\.\d+[kMG]B\/s)/); if (progressMatch) { const percentage = parseInt(progressMatch[2]); const transferred = Math.floor((percentage / 100) * totalSize); if (transferred > lastProgress) { lastProgress = transferred; onProgress(transferred, totalSize); } } } }); rsyncProcess.stderr?.on('data', (data) => { if (this.verbose) { console.log(`[${this.host}] rsync stderr:`, data.toString().trim()); } }); rsyncProcess.on('close', (code) => { if (code === 0) { onProgress?.(totalSize, totalSize); // Final progress update resolve(); } else { reject(new Error(`rsync exited with code ${code}`)); } }); rsyncProcess.on('error', (err) => { reject(err); }); }); if (this.verbose) { console.log(`[${this.host}] rsync upload completed: ${remotePath}`); } } catch (rsyncError) { // Fallback to SCP if rsync fails if (this.verbose) { console.log(`[${this.host}] rsync failed, falling back to SCP: ${rsyncError}`); } await new Promise((resolve, reject) => { const scpArgs = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-C', localPath, `${this.connectOptions.username}@${this.host}:${remotePath}`]; const scpProcess = spawn('scp', scpArgs); // For SCP, simulate progress based on time (not ideal but better than nothing) let progressInterval; if (onProgress) { const startTime = Date.now(); const estimatedDurationMs = Math.max(1000, totalSize / (1024 * 1024) * 1000); // Rough estimate: 1MB/s progressInterval = setInterval(() => { const elapsed = Date.now() - startTime; const progress = Math.min(0.9, elapsed / estimatedDurationMs); // Cap at 90% until complete const transferred = Math.floor(progress * totalSize); onProgress(transferred, totalSize); }, 200); } scpProcess.stdout?.on('data', (data) => { if (this.verbose) { console.log(`[${this.host}] scp stdout:`, data.toString().trim()); } }); scpProcess.stderr?.on('data', (data) => { if (this.verbose) { console.log(`[${this.host}] scp stderr:`, data.toString().trim()); } }); scpProcess.on('close', (code) => { if (progressInterval) { clearInterval(progressInterval); } if (code === 0) { onProgress?.(totalSize, totalSize); // Final progress update resolve(); } else { reject(new Error(`scp exited with code ${code}`)); } }); scpProcess.on('error', (err) => { if (progressInterval) { clearInterval(progressInterval); } reject(err); }); }); if (this.verbose) { console.log(`[${this.host}] SCP upload completed: ${remotePath}`); } } } catch (transferError) { throw transferError; } } catch (err) { console.error(`[${this.host}] Failed to upload file ${localPath} to ${remotePath} via rsync or SCP:`, err); throw err; } } /** * Download a file from the remote server using SFTP */ async downloadFile(remotePath, localPath) { if (this.verbose) { console.log(`[${this.host}] Downloading ${remotePath} to ${localPath}`); } try { const sftp = await this.ssh.sftp(); await sftp.fastGet(remotePath, localPath); if (this.verbose) { console.log(`[${this.host}] Download completed: ${localPath}`); } } catch (err) { console.error(`[${this.host}] Failed to download file ${remotePath} to ${localPath}:`, err); throw err; } } } exports.SSHClient = SSHClient; //# sourceMappingURL=index.js.map