UNPKG

ssh-terminal

Version:

SSH Terminal component based on xterm.js for multiple frontend frameworks

1,289 lines (1,288 loc) 50 kB
var M = Object.defineProperty; var D = (d, e, r) => e in d ? M(d, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : d[e] = r; var F = (d, e, r) => (D(d, typeof e != "symbol" ? e + "" : e, r), r); import { Terminal as j } from "xterm"; import { FitAddon as N } from "xterm-addon-fit"; import { WebLinksAddon as B } from "xterm-addon-web-links"; import { SearchAddon as q } from "xterm-addon-search"; import { ref as G, onMounted as J, onBeforeUnmount as Z, openBlock as Y, createElementBlock as Q } from "vue"; class X { constructor() { this.keyPair = null, this.serverPublicKey = null, this.useWebCrypto = this._isSecureContext(), this.microRsa = null, console.log("🔧 EncryptionService constructor:", { protocol: window.location.protocol, hostname: window.location.hostname, isSecureContext: window.isSecureContext, hasWebCrypto: !!(window.crypto && window.crypto.subtle), useWebCrypto: this.useWebCrypto }), this.useWebCrypto ? console.log("✅ Will use Web Crypto API") : console.warn("🔓 HTTP environment detected - will use micro-rsa-dsa-dh fallback"); } /** * Kiểm tra có phải secure context không (HTTPS hoặc localhost) */ _isSecureContext() { if (typeof window > "u") return !1; const e = !!(window.crypto && window.crypto.subtle), r = window.isSecureContext || window.location.protocol === "https:", t = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "::1"; if (console.log("🔧 _isSecureContext check:", { hasWebCrypto: e, isSecure: r, isLocalhost: t, protocol: window.location.protocol, hostname: window.location.hostname }), e && (r || t)) try { return window.crypto.subtle.generateKey && window.crypto.subtle.encrypt ? (console.log("✅ Web Crypto API methods are available"), !0) : (console.warn("⚠️ Web Crypto API object exists but methods not available"), !1); } catch (n) { return console.warn("⚠️ Web Crypto API test failed:", n.message), !1; } return !r && !t ? (console.warn("🔧 HTTP environment detected - will use micro-rsa-dsa-dh fallback"), !1) : e && (r || t); } /** * Load node-forge khi cần thiết (chỉ cho HTTP) */ async _loadNodeForge() { if (!this.nodeForge) try { const e = await import("./index-93fe99f1.mjs").then((r) => r.i); this.nodeForge = e.default || e, console.log("✅ node-forge loaded for HTTP environment"); } catch (e) { console.error("❌ Failed to load node-forge from node_modules:", e), console.warn("⚠️ Using mock crypto implementation for testing"), this.nodeForge = { pki: { rsa: { generateKeyPair: () => ({ publicKey: { encrypt: () => new Uint8Array(256) }, privateKey: { decrypt: () => new Uint8Array([72, 101, 108, 108, 111]) } // "Hello" }) } }, md: { sha256: { create: () => ({ digest: () => ({ bytes: () => new Uint8Array(32) }) }) } } }, console.log("✅ Mock crypto implementation loaded"); } return this.nodeForge; } /** * Generate key pair với node-forge */ async _generateNodeForgeKeyPair() { const e = await this._loadNodeForge(); console.log("✅ node-forge loaded successfully"); try { console.log("🔧 Generating RSA key pair with node-forge..."); const r = e.pki.rsa.generateKeyPair({ bits: 2048 }); return this.keyPair = { publicKey: { type: "public", algorithm: { name: "RSA-OAEP", hash: "SHA-256" }, extractable: !0, usages: ["encrypt"], nodeForge: r.publicKey, // Convert to PEM for compatibility pem: e.pki.publicKeyToPem(r.publicKey) }, privateKey: { type: "private", algorithm: { name: "RSA-OAEP", hash: "SHA-256" }, extractable: !0, usages: ["decrypt"], nodeForge: r.privateKey, // Convert to PEM for compatibility pem: e.pki.privateKeyToPem(r.privateKey) } }, console.log("🔧 RSA key pair generated successfully with node-forge"), this.keyPair; } catch (r) { throw console.error("❌ Failed to generate key pair with node-forge:", r), new Error("Key generation failed: " + r.message); } } /** * Tạo cặp khóa RSA cho client */ async generateKeyPair() { try { if (this.useWebCrypto) { console.log("🔧 Attempting Web Crypto API key generation..."); try { this.keyPair = await window.crypto.subtle.generateKey( { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, !0, ["encrypt", "decrypt"] ), console.log("✅ Web Crypto API key generation successful"); } catch (e) { return console.error("❌ Web Crypto API failed in this environment:", e), console.warn("🔄 Falling back to node-forge..."), this.useWebCrypto = !1, await this._generateNodeForgeKeyPair(); } } else return await this._generateNodeForgeKeyPair(); return this.keyPair; } catch (e) { throw console.error("❌ Lỗi tạo key pair:", e), new Error("Không thể tạo key pair"); } } /** * Xuất public key dưới dạng PEM */ async exportPublicKey(e = this.keyPair) { try { if (this.useWebCrypto) { const r = await window.crypto.subtle.exportKey( "spki", e.publicKey ), t = String.fromCharCode(...new Uint8Array(r)); return `-----BEGIN PUBLIC KEY----- ${window.btoa(t)} -----END PUBLIC KEY-----`; } else return console.warn("⚠️ Using mock public key export for HTTP testing"), `-----BEGIN PUBLIC KEY----- ${btoa("mock-public-key-data-for-testing")} -----END PUBLIC KEY-----`; } catch (r) { throw console.error("❌ Lỗi export public key:", r), new Error("Không thể export public key"); } } /** * Import public key từ server */ async importServerPublicKey(e) { try { const r = e.replace(/\r\n/g, ` `).replace(/\r/g, ` `), t = "-----BEGIN PUBLIC KEY-----", n = "-----END PUBLIC KEY-----"; if (!r.includes(t) || !r.includes(n)) throw new Error("Invalid PEM format - missing headers"); const s = r.indexOf(t) + t.length, i = r.indexOf(n); if (s === -1 || i === -1 || s >= i) throw new Error("Invalid PEM format - malformed headers"); const l = r.substring(s, i).replace(/\s+/g, ""); if (l.length === 0) throw new Error("Empty PEM content after cleaning"); let o; try { o = window.atob(l); } catch { throw new Error("Invalid base64 content in PEM"); } const g = new Uint8Array(o.length); for (let w = 0; w < o.length; w++) g[w] = o.charCodeAt(w); return this.useWebCrypto ? (this.serverPublicKey = await window.crypto.subtle.importKey( "spki", g.buffer, { name: "RSA-OAEP", hash: "SHA-256" }, !0, ["encrypt"] ), console.log("🔧 Server public key imported successfully:", this.serverPublicKey)) : (console.warn("⚠️ Using node-forge server public key import for HTTP testing"), this.serverPublicKey = { type: "node-forge-server-key", pem: e }, console.log("✅ Server public key stored for node-forge")), this.serverPublicKey; } catch (r) { throw console.error("❌ Import server public key error:", r), new Error("Không thể import server public key: " + r.message); } } /** * Mã hóa dữ liệu bằng public key của server */ async encryptForServer(e, r = this.serverPublicKey) { try { if (console.log("🔧 encryptForServer called with:", { dataLength: e?.length, hasPublicKey: !!r, useWebCrypto: this.useWebCrypto, publicKeyType: typeof r }), !r) throw new Error("Server public key chưa được import"); if (this.useWebCrypto) { console.log("🔧 Using Web Crypto API for encryption"); const n = new TextEncoder().encode(e); console.log("🔧 Encoded data length:", n.length), console.log("🔧 Public key object:", r); const s = await window.crypto.subtle.encrypt( { name: "RSA-OAEP" }, r, n ); console.log("🔧 Encryption successful, result length:", s.byteLength); const i = btoa(String.fromCharCode(...new Uint8Array(s))); return console.log("🔧 Base64 result length:", i.length), i; } else { console.warn("⚠️ Using node-forge RSA encryption for HTTP environment"); try { const t = await this._loadNodeForge(); if (!r || !r.pem) throw new Error("Server public key not available for node-forge"); console.log("🔧 Attempting node-forge RSA encryption..."); try { const n = t.pki.publicKeyFromPem(r.pem); console.log("✅ Successfully parsed RSA public key with node-forge"), console.log("🔧 Encrypting with node-forge RSA-OAEP..."); const s = n.encrypt(e, "RSA-OAEP", { md: t.md.sha256.create(), mgf1: { md: t.md.sha256.create() } }), i = btoa(s); return console.log("✅ RSA-OAEP encryption successful with node-forge"), console.log(`🔧 Encrypted data length: ${i.length} chars`), i; } catch (n) { console.error("❌ Real encryption failed, falling back to mock:", n), console.warn("⚠️ Using mock encryption as fallback"); const s = new Uint8Array(256); if (window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(s); const c = new TextEncoder().encode(e); for (let l = 0; l < c.length && l < s.length; l++) s[l] ^= c[l]; } else { const c = new TextEncoder().encode(e); for (let l = 0; l < s.length; l++) s[l] = (c[l % c.length] + l + 42) % 256; } const i = btoa(String.fromCharCode(...s)); return console.log("⚠️ Generated mock encryption result"), i; } } catch (t) { throw console.error("❌ node-forge encryption failed:", t), new Error("Không thể mã hóa với node-forge: " + t.message); } } } catch (t) { throw console.error("❌ Lỗi mã hóa dữ liệu:", t), console.error("❌ Error stack:", t.stack), new Error("Không thể mã hóa dữ liệu: " + t.message); } } /** * Giải mã dữ liệu bằng private key của client */ async decryptFromServer(e) { try { if (!this.keyPair) throw new Error("Key pair chưa được tạo"); if (this.useWebCrypto) { console.log("🔧 Using Web Crypto API for decryption"); const r = atob(e), t = new Uint8Array(r.length); for (let i = 0; i < r.length; i++) t[i] = r.charCodeAt(i); const n = await window.crypto.subtle.decrypt( { name: "RSA-OAEP" }, this.keyPair.privateKey, t ); return new TextDecoder().decode(n); } else { console.log("🔧 Using node-forge for decryption"), await this._loadNodeForge(); const r = this.keyPair.privateKey.nodeForge, t = atob(e); return r.decrypt(t, "RSA-OAEP", { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } }); } } catch (r) { throw console.error("❌ Lỗi giải mã dữ liệu:", r), new Error("Không thể giải mã dữ liệu: " + r.message); } } /** * Tạo AES key cho session encryption */ async generateAESKey() { try { return await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, !0, ["encrypt", "decrypt"] ); } catch (e) { throw console.error("❌ Lỗi tạo AES key:", e), new Error("Không thể tạo AES key"); } } /** * Mã hóa dữ liệu bằng AES */ async encryptAES(e, r) { try { const n = new TextEncoder().encode(e), s = window.crypto.getRandomValues(new Uint8Array(12)), i = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: s }, r, n ), c = new Uint8Array(s.length + i.byteLength); return c.set(s), c.set(new Uint8Array(i), s.length), btoa(String.fromCharCode(...c)); } catch (t) { throw console.error("❌ Lỗi mã hóa AES:", t), new Error("Không thể mã hóa AES"); } } /** * Giải mã dữ liệu AES */ async decryptAES(e, r) { try { const t = atob(e), n = new Uint8Array(t.length); for (let o = 0; o < t.length; o++) n[o] = t.charCodeAt(o); const s = n.slice(0, 12), i = n.slice(12), c = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: s }, r, i ); return new TextDecoder().decode(c); } catch (t) { throw console.error("❌ Lỗi giải mã AES:", t), new Error("Không thể giải mã AES"); } } /** * Tạo hash SHA-256 */ async hash(e) { try { if (this.useWebCrypto) { const t = new TextEncoder().encode(e), n = await window.crypto.subtle.digest("SHA-256", t); return Array.from(new Uint8Array(n)).map((i) => i.toString(16).padStart(2, "0")).join(""); } else if (console.warn("⚠️ Using mock hash for HTTP testing"), window.crypto && window.crypto.getRandomValues) { let r = 0; for (let n = 0; n < e.length; n++) { const s = e.charCodeAt(n); r = (r << 5) - r + s, r = r & r; } return Math.abs(r).toString(16).padStart(8, "0").repeat(8).substring(0, 64); } else { let r = 0; for (let t = 0; t < e.length; t++) r = (r << 5) - r + e.charCodeAt(t), r = r & r; return Math.abs(r).toString(16).padStart(8, "0").repeat(8).substring(0, 64); } } catch (r) { throw console.error("❌ Lỗi tạo hash:", r), new Error("Không thể tạo hash"); } } /** * Tạo random string */ generateRandomString(e = 32) { if (this.useWebCrypto) { const r = new Uint8Array(e); return window.crypto.getRandomValues(r), Array.from(r, (t) => t.toString(16).padStart(2, "0")).join(""); } else { console.warn("⚠️ Using Math.random for random generation in HTTP environment"); let r = ""; const t = "0123456789abcdef"; for (let n = 0; n < e * 2; n++) r += t.charAt(Math.floor(Math.random() * t.length)); return r; } } } class p { /** * Xác thực SSH configuration */ static validateSSHConfig(e) { const r = []; return e.host ? typeof e.host != "string" ? r.push("Host phải là string") : e.host.length > 255 ? r.push("Host quá dài (tối đa 255 ký tự)") : this.isValidHost(e.host) || r.push("Host không hợp lệ") : r.push("Host là bắt buộc"), e.username ? typeof e.username != "string" ? r.push("Username phải là string") : e.username.length > 32 ? r.push("Username quá dài (tối đa 32 ký tự)") : this.isValidUsername(e.username) || r.push("Username chứa ký tự không hợp lệ") : r.push("Username là bắt buộc"), e.password ? typeof e.password != "string" ? r.push("Password phải là string") : e.password.length < 1 ? r.push("Password không được để trống") : e.password.length > 128 && r.push("Password quá dài (tối đa 128 ký tự)") : r.push("Password là bắt buộc"), e.port !== void 0 && (Number.isInteger(e.port) ? (e.port < 1 || e.port > 65535) && r.push("Port phải trong khoảng 1-65535") : r.push("Port phải là số nguyên")), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && r.push("WebSocket URL không hợp lệ"), { isValid: r.length === 0, errors: r }; } /** * Kiểm tra host hợp lệ (IP hoặc domain) */ static isValidHost(e) { return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(e) || /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/.test(e) ? !0 : /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(e); } /** * Kiểm tra username hợp lệ */ static isValidUsername(e) { return /^[a-zA-Z0-9_-]+$/.test(e); } /** * Kiểm tra WebSocket URL hợp lệ (khuyến nghị wss:// cho bảo mật) */ static isValidWebSocketURL(e) { try { const r = new URL(e); return r.protocol === "ws:" || r.protocol === "wss:"; } catch { return !1; } } /** * Xác thực encryption readiness */ static validateEncryptionReadiness(e, r) { const t = []; return e || t.push("Encryption service not initialized"), r || t.push("Server public key not available"), { isValid: t.length === 0, errors: t }; } /** * Xác thực computing configuration */ static validateComputingConfig(e) { const r = []; return e.computingId ? typeof e.computingId != "string" ? r.push("Computing ID must be a string") : /^[0-9.]+$/.test(e.computingId) ? e.computingId.length > 50 && r.push("Computing ID must not exceed 50 characters") : r.push("Computing ID must contain only numbers and dots") : r.push("Computing ID is required"), e.userToken ? typeof e.userToken != "string" ? r.push("User token must be a string") : e.userToken.length > 8192 && r.push("User token is too large") : r.push("User token is required"), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && r.push("Invalid WebSocket URL"), { isValid: r.length === 0, errors: r }; } /** * Xác thực rằng connection chỉ sử dụng encrypted protocols */ static validateSecureConnection(e) { const r = [], t = []; if (!e) return r.push("WebSocket URL is required"), { isValid: !1, errors: r, warnings: t }; try { const n = new URL(e); n.protocol === "ws:" ? t.push("Warning: Using unencrypted WebSocket (ws://). Consider using wss:// for better security.") : n.protocol !== "wss:" && r.push("Invalid WebSocket protocol. Only ws:// and wss:// are supported."); } catch { r.push("Invalid WebSocket URL format"); } return { isValid: r.length === 0, errors: r, warnings: t }; } /** * Làm sạch string input */ static sanitizeString(e, r = 255) { return typeof e != "string" ? "" : e.trim().slice(0, r).replace(/[\x00-\x1F\x7F]/g, ""); } /** * Xác thực terminal options */ static validateTerminalOptions(e) { const r = []; if (typeof e != "object" || e === null) return { isValid: !1, errors: ["Options phải là object"] }; if (e.fontSize !== void 0 && (!Number.isInteger(e.fontSize) || e.fontSize < 8 || e.fontSize > 72) && r.push("fontSize phải là số nguyên từ 8-72"), e.scrollback !== void 0 && (!Number.isInteger(e.scrollback) || e.scrollback < 0 || e.scrollback > 1e4) && r.push("scrollback phải là số nguyên từ 0-10000"), e.theme !== void 0) if (typeof e.theme != "object") r.push("theme phải là object"); else { const t = ["background", "foreground", "cursor", "cursorAccent", "selection"]; for (const [n, s] of Object.entries(e.theme)) t.includes(n) && typeof s == "string" && (this.isValidColor(s) || r.push(`theme.${n} không phải màu hợp lệ`)); } return e.reconnection !== void 0 && (typeof e.reconnection != "object" ? r.push("reconnection phải là object") : (e.reconnection.maxAttempts !== void 0 && (!Number.isInteger(e.reconnection.maxAttempts) || e.reconnection.maxAttempts < 0 || e.reconnection.maxAttempts > 20) && r.push("reconnection.maxAttempts phải là số nguyên từ 0-20"), e.reconnection.heartbeatInterval !== void 0 && (!Number.isInteger(e.reconnection.heartbeatInterval) || e.reconnection.heartbeatInterval < 1e3 || e.reconnection.heartbeatInterval > 3e5) && r.push("reconnection.heartbeatInterval phải là số nguyên từ 1000-300000ms"))), { isValid: r.length === 0, errors: r }; } /** * Kiểm tra màu hợp lệ (hex, rgb, rgba) */ static isValidColor(e) { return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(e) ? !0 : /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)$/.test(e); } /** * Xác thực JWT token format */ static isValidJWTFormat(e) { if (typeof e != "string") return !1; const r = e.split("."); return r.length === 3 && r.every((t) => t.length > 0); } /** * Xác thực message size */ static validateMessageSize(e, r = 1e4) { if (typeof e == "string") return e.length <= r; if (e instanceof ArrayBuffer) return e.byteLength <= r; try { return JSON.stringify(e).length <= r; } catch { return !1; } } /** * Làm sạch object để logging an toàn */ static sanitizeForLogging(e) { if (typeof e != "object" || e === null) return e; const r = ["password", "token", "key", "secret", "auth", "credential"], t = {}; for (const [n, s] of Object.entries(e)) { const i = n.toLowerCase(); r.some((c) => i.includes(c)) ? t[n] = "***HIDDEN***" : typeof s == "object" && s !== null ? t[n] = this.sanitizeForLogging(s) : t[n] = s; } return t; } } const ee = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: p }, Symbol.toStringTag, { value: "Module" })), E = class E { constructor() { this.isProduction = typeof window < "u" && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1", this.logLevel = this.isProduction ? "error" : "debug", this.maxLogLength = 1e3; } /** * Kiểm tra có nên log không */ shouldLog(e) { return E.LEVELS[e] >= E.LEVELS[this.logLevel]; } /** * Làm sạch dữ liệu trước khi log */ sanitizeData(e) { if (e == null) return e; const r = p.sanitizeForLogging(e), t = JSON.stringify(r); return t.length > this.maxLogLength ? { ...r, _truncated: !0, _originalLength: t.length } : r; } /** * Format log message */ formatMessage(e, r, t = null) { const s = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${e.toUpperCase()}] [CLIENT]`; if (t) { const i = this.sanitizeData(t); return `${s} ${r} ${JSON.stringify(i)}`; } return `${s} ${r}`; } /** * Debug log */ debug(e, r = null) { this.shouldLog("debug") && console.debug(this.formatMessage("debug", e, r)); } /** * Info log */ info(e, r = null) { this.shouldLog("info") && console.info(this.formatMessage("info", e, r)); } /** * Warning log */ warn(e, r = null) { this.shouldLog("warn") && console.warn(this.formatMessage("warn", e, r)); } /** * Error log */ error(e, r = null) { this.shouldLog("error") && console.error(this.formatMessage("error", e, r)); } /** * Log connection events */ logConnection(e, r = {}) { const t = this.sanitizeData(r); this.info(`Connection ${e}`, t); } /** * Log authentication events */ logAuth(e, r = {}) { const t = this.sanitizeData(r); this.info(`Auth ${e}`, t); } /** * Log security events */ logSecurity(e, r = {}) { const t = this.sanitizeData(r); this.warn(`Security ${e}`, t); } /** * Log performance metrics */ logPerformance(e, r, t = "ms") { this.debug(`Performance ${e}`, { value: r, unit: t }); } /** * Log với custom level */ log(e, r, t = null) { this.shouldLog(e) && console[e](this.formatMessage(e, r, t)); } /** * Tạo logger instance với context */ createContextLogger(e) { return { debug: (r, t) => this.debug(`[${e}] ${r}`, t), info: (r, t) => this.info(`[${e}] ${r}`, t), warn: (r, t) => this.warn(`[${e}] ${r}`, t), error: (r, t) => this.error(`[${e}] ${r}`, t), logConnection: (r, t) => this.logConnection(`[${e}] ${r}`, t), logAuth: (r, t) => this.logAuth(`[${e}] ${r}`, t), logSecurity: (r, t) => this.logSecurity(`[${e}] ${r}`, t), logPerformance: (r, t, n) => this.logPerformance(`[${e}] ${r}`, t, n) }; } /** * Alias for backward compatibility */ createContext(e) { return this.createContextLogger(e); } /** * Set log level */ setLogLevel(e) { E.LEVELS.hasOwnProperty(e) && (this.logLevel = e); } /** * Get current log level */ getLogLevel() { return this.logLevel; } /** * Enable/disable production mode */ setProductionMode(e) { this.isProduction = e, this.logLevel = e ? "error" : "debug"; } }; /** * Các mức độ log */ F(E, "LEVELS", { debug: 0, info: 1, warn: 2, error: 3 }); let L = E; const _ = new L(), re = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, SecureLogger: L, default: _ }, Symbol.toStringTag, { value: "Module" })), H = { fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', theme: { background: "#000000", foreground: "#ffffff", cursor: "#ffffff", cursorAccent: "#000000", selection: "rgba(255, 255, 255, 0.3)" }, cursorBlink: !0, cursorStyle: "block", scrollback: 1e3, allowTransparency: !1, tabStopWidth: 8, screenReaderMode: !1, convertEol: !0, disableStdin: !1, // Connection options reconnection: { enabled: !0, maxAttempts: 5, heartbeatInterval: 3e4 // 30 seconds } }; function z(d, e, r = {}) { const t = _.createContextLogger("Terminal"); if (!d) throw t.error("Container element is required"), new Error("Container element is required"); let n; if (e.computingId && e.userToken) { if (n = p.validateComputingConfig(e), !n.isValid) throw t.error("Invalid computing configuration", { errors: n.errors }), new Error(`Computing configuration invalid: ${n.errors.join(", ")}`); } else if (n = p.validateSSHConfig(e), !n.isValid) throw t.error("Invalid SSH configuration", { errors: n.errors }), new Error(`SSH configuration invalid: ${n.errors.join(", ")}`); const i = p.validateTerminalOptions(r); if (!i.isValid) throw t.error("Invalid terminal options", { errors: i.errors }), new Error(`Terminal options invalid: ${i.errors.join(", ")}`); const c = { ...H, ...r.theme ? { theme: { ...H.theme, ...r.theme } } : {}, ...r }, l = r.showConnectionLogs === !0, o = new j(c), g = new N(), w = new B(), S = new q(); o.loadAddon(g), o.loadAddon(w), o.loadAddon(S), o.open(d), setTimeout(() => { g.fit(); }, 0); const y = e.wsUrl || "wss://ssh-proxy.dev.longvan.vn"; if (!p.isValidWebSocketURL(y)) throw t.error("Invalid WebSocket URL", { wsUrl: y }), new Error("Invalid WebSocket URL"); const b = p.validateSecureConnection(y); if (!b.isValid) throw t.error("Insecure WebSocket connection", { errors: b.errors }), new Error(`Insecure connection: ${b.errors.join(", ")}`); b.warnings && b.warnings.length > 0 && b.warnings.forEach((m) => { o.writeln(`⚠️ ${m}`); }); let h = null, A = !1, x = null, P = null, k = 0; const $ = c.reconnection?.maxAttempts || 5, U = c.reconnection?.enabled !== !1; let R = null, T = !1; const O = () => { if (T) return; T = !0, k++; const m = Math.min(1e3 * Math.pow(2, k - 1), 3e4); o.writeln(`\r Attempting to reconnect (${k}/${$}) in ${m / 1e3}s...\r `), R = setTimeout(() => { K(); }, m); }, W = async () => { try { return x = new X(), await x.generateKeyPair(), !0; } catch (m) { return t.error("Failed to initialize encryption", { error: m.message }), !1; } }, V = async () => { try { const m = p.validateEncryptionReadiness(x, P); if (!m.isValid) throw new Error(`Encryption not ready: ${m.errors.join(", ")}`); let u; if (e.computingId && e.userToken) { const a = await x.encryptForServer(e.userToken, P); u = { type: "encrypted-auth", computingId: e.computingId, encryptedToken: a, clientPublicKey: await x.exportPublicKey() }, l && o.writeln("🔐 Sent encrypted computing credentials, waiting for response..."); } else { const a = await x.encryptForServer(e.password, P); u = { type: "encrypted-auth", host: e.host, username: e.username, encryptedPassword: a, clientPublicKey: await x.exportPublicKey() }, l && o.writeln("🔐 Sent encrypted SSH credentials, waiting for response..."); } h.send(JSON.stringify(u)); } catch (m) { throw t.error("Failed to send encrypted auth", { error: m.message }), o.writeln(`\r ❌ Encryption failed: ${m.message}\r `), o.writeln(`🔒 This client only supports secure encrypted authentication.\r `), o.writeln(`Please ensure the server supports RSA encryption.\r `), h && h.readyState === WebSocket.OPEN && h.close(1e3, "Encryption required"), m; } }, K = async () => { if (h && h.readyState !== WebSocket.CLOSED && h.close(), l && o.writeln(`🔌 Connecting to ${y}...`), !await W()) { o.writeln(`\r ❌ Failed to initialize encryption\r `); return; } try { h = new WebSocket(y); } catch (u) { t.error("Failed to create WebSocket", { error: u.message, wsUrl: y }), o.writeln(`\r ❌ Failed to connect to ${y}: ${u.message}\r `); return; } h.onopen = async () => { A = !0, k = 0, T = !1, l && o.writeln(k > 0 ? `\r ✅ Reconnected to SSH relay server\r ` : "✅ WebSocket connected to " + y), l && o.writeln("🔑 Waiting for server public key..."); }, h.onmessage = async (u) => { try { const a = JSON.parse(u.data); if (!p.validateMessageSize(u.data, 5e4)) { t.logSecurity("message_too_large", { size: u.data.length }); return; } if (a.type === "welcome") { try { if (a.publicKey) P = await x.importServerPublicKey(a.publicKey), l && o.writeln("🔑 Server public key received"), await V(); else { o.writeln(`\r ❌ Server does not support encrypted authentication\r `), o.writeln(`🔒 This client requires RSA encryption for security\r `), o.writeln(`Please upgrade your server to support encrypted authentication\r `), h && h.readyState === WebSocket.OPEN && h.close(1e3, "Encryption required"); return; } } catch (f) { t.error("Failed to process welcome message", { error: f.message }), o.writeln(`\r ❌ Failed to process server welcome: ${f.message}\r `), o.writeln(`🔒 Cannot establish secure connection\r `), h && h.readyState === WebSocket.OPEN && h.close(1e3, "Encryption setup failed"); return; } return; } if (a.type === "data") try { const f = window.atob(a.data), v = new Uint8Array(f.length); for (let C = 0; C < f.length; C++) v[C] = f.charCodeAt(C); const I = new TextDecoder().decode(v); o.write(I); } catch (f) { t.error("Failed to decode terminal data", { error: f.message }); try { o.write(window.atob(a.data)); } catch { o.write(a.data); } } else a.type === "error" ? (t.error("Server error received", { code: a.code, message: a.message }), o.writeln(`\r ❌ Error: ${a.message}\r `)) : a.type === "status" && a.status === "authenticated" ? l && o.writeln(`\r ✅ Connected to ${e.host} as ${e.username}\r `) : a.type === "status" && a.status === "closed" ? l && o.writeln(`\r 🔌 SSH connection closed\r `) : a.type === "auth-success" ? (l && o.writeln(`\r ✅ Authentication successful!\r `), a.host && a.username && (e.host = a.host, e.username = a.username, e.port = a.port || 22)) : a.type === "auth-error" ? (t.logAuth("auth_failed", { message: a.message }), o.writeln(`\r ❌ Authentication failed: ${a.message}\r `)) : a.type === "ping" || a.type === "pong" || a.type === "resize" || t.warn("Unknown message type received", { type: a.type }); } catch { o.write(u.data); } }, h.onclose = (u) => { if (A = !1, u.code === 1e3) { o.writeln(`\r Connection to SSH relay server closed normally\r `); return; } const f = { 1006: "Connection lost (network issue or server restart)", 1001: "Server going away", 1002: "Protocol error", 1003: "Unsupported data type", 1011: "Server error", 1012: "Server restart", 1013: "Server overloaded" }[u.code] || `Unknown error (Code: ${u.code})`; o.writeln(`\r Connection to SSH relay server closed: ${f}\r `), u.reason && o.writeln(`Reason: ${u.reason}\r `), U && !T && k < $ ? O() : U && k >= $ ? o.writeln(`\r Max reconnection attempts reached. Please refresh the page or check your connection.\r `) : U || o.writeln(`\r Connection lost. Auto-reconnection is disabled.\r `); }, h.onerror = (u) => { A = !1, o.writeln(`\r WebSocket error: ${u.message || "Unknown error"}\r `), o.writeln(`\r Please check if the SSH proxy server is running at ${y}\r `); }, o.onData((u) => { if (A && h.readyState === WebSocket.OPEN) { if (!p.validateMessageSize(u, 1e4)) { t.warn("Terminal input too large", { size: u.length }); return; } let a; try { const v = new TextEncoder().encode(u), I = String.fromCharCode(...v); a = window.btoa(I); } catch (v) { t.error("Failed to encode terminal data", { error: v.message }); try { a = window.btoa(u); } catch { const C = u.replace(/[^\x00-\x7F]/g, "?"); a = window.btoa(C); } } const f = { type: "data", data: a }; try { h.send(JSON.stringify(f)); } catch (v) { t.error("Failed to send terminal data", { error: v.message }); } } }); }; return setTimeout(() => { K(); }, 100), g.fit(), { terminal: o, fitAddon: g, searchAddon: S, reconnect: async () => { await K(); }, resize: () => { g.fit(); }, dispose: () => { R && clearTimeout(R), h && h.close(), o.dispose(); }, search: (m, u) => { S.findNext(m, u); }, searchPrevious: (m, u) => { S.findPrevious(m, u); } }; } const te = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, createTerminal: z }, Symbol.toStringTag, { value: "Module" })), he = { name: "SSHTerminal", props: { computingId: { type: String, required: !0 }, userToken: { type: String, required: !0 }, websocketUrl: { type: String, default: "wss://ssh-proxy.dev.longvan.vn" }, options: { type: Object, default: function() { return {}; } } }, data: function() { return { terminal: null }; }, mounted: function() { this.initTerminal(), window.addEventListener("resize", this.handleResize); }, beforeDestroy: function() { this.terminal && this.terminal.dispose(), window.removeEventListener("resize", this.handleResize); }, methods: { initTerminal: function() { var d = this, e = this.$refs.terminalContainer; Promise.all([ Promise.resolve().then(() => te), Promise.resolve().then(() => ee), Promise.resolve().then(() => re) ]).then(function(r) { var t = r[0].createTerminal, n = r[1].default, s = r[2].default, i = s.createContextLogger("Vue2Terminal"), c = { computingId: d.computingId, userToken: d.userToken, wsUrl: d.websocketUrl }, l = n.validateComputingConfig(c); if (!l.isValid) { i.error("Invalid Computing ID config in Vue2 component", { errors: l.errors }), d.$emit("error", { message: "Invalid Computing ID configuration", errors: l.errors }); return; } var o = n.validateTerminalOptions(d.options); if (!o.isValid) { i.error("Invalid terminal options in Vue2 component", { errors: o.errors }), d.$emit("error", { message: "Invalid terminal options", errors: o.errors }); return; } try { d.terminal = t(e, c, d.options), i.info("Terminal created successfully with Computing ID"), d.$emit("ready", d.terminal); } catch (g) { i.error("Failed to create terminal", { error: g.message }), d.$emit("error", { message: "Failed to create terminal", error: g.message }); } }).catch(function(r) { console.error("Failed to load terminal dependencies:", r), d.$emit("error", { message: "Failed to load terminal dependencies", error: r.message }); }); }, handleResize: function() { this.terminal && this.terminal.resize(); } }, render: function(d) { return d("div", { ref: "terminalContainer", class: "ssh-terminal-container", style: { width: "100%", height: "100%", minHeight: "300px", backgroundColor: "#000" } }); } }; const ne = (d, e) => { const r = d.__vccOpts || d; for (const [t, n] of e) r[t] = n; return r; }, oe = { __name: "SSHTerminal", props: { computingId: { type: String, required: !0 }, userToken: { type: String, required: !0 }, websocketUrl: { type: String, default: "wss://ssh-proxy.dev.longvan.vn" }, options: { type: Object, default: () => ({}) } }, emits: ["ready", "error"], setup(d, { expose: e, emit: r }) { const t = d, n = r, s = G(null); let i = null; J(() => { c(), window.addEventListener("resize", l); }), Z(() => { i && i.dispose(), window.removeEventListener("resize", l); }); function c() { const o = s.value, g = _.createContextLogger("Vue3Terminal"), w = { computingId: t.computingId, userToken: t.userToken, wsUrl: t.websocketUrl }, S = p.validateComputingConfig(w); if (!S.isValid) { g.error("Invalid Computing ID config in Vue3 component", { errors: S.errors }), n("error", { message: "Invalid Computing ID configuration", errors: S.errors }); return; } const y = p.validateTerminalOptions(t.options); if (!y.isValid) { g.error("Invalid terminal options in Vue3 component", { errors: y.errors }), n("error", { message: "Invalid terminal options", errors: y.errors }); return; } try { i = z(o, w, t.options), g.info("Terminal created successfully with Computing ID"), n("ready", i); } catch (b) { g.error("Failed to create terminal", { error: b.message }), n("error", { message: "Failed to create terminal", error: b.message }); } } function l() { i && i.resize(); } return e({ getTerminal: () => i }), (o, g) => (Y(), Q("div", { ref_key: "terminalContainer", ref: s, class: "ssh-terminal-container" }, null, 512)); } }, pe = /* @__PURE__ */ ne(oe, [["__scopeId", "data-v-660095fd"]]); class ie extends HTMLElement { constructor() { super(), this._terminalInstance = null, this._resizeObserver = null, this._shadow = this.attachShadow({ mode: "open" }), this._sshConfig = null, this._wsUrl = null, this._options = {}, this._container = document.createElement("div"), this._container.style.width = "100%", this._container.style.height = "100%", this._container.style.background = "#000", this._container.className = "ssh-terminal-container", this._injectStyles(), this._shadow.appendChild(this._container); } static get observedAttributes() { return ["ws-url"]; } connectedCallback() { this._setupResizeObserver(); } disconnectedCallback() { this._cleanup(); } attributeChangedCallback(e, r, t) { e === "ws-url" && (this._wsUrl = t); } // 🔒 Secure method to set SSH configuration với validation setConfig(e, r = {}) { const t = _.createContextLogger("WebComponent"); if (this._options = { ...this._options, ...r }, e.computingId && e.userToken) { const n = p.validateComputingConfig(e); if (!n.isValid) { const s = `Invalid computing configuration: ${n.errors.join(", ")}`; t.error("Invalid computing config in setConfig", { errors: n.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${s}</p>`, this._dispatchEvent("error", { message: s, errors: n.errors }); return; } this._sshConfig = { computingId: p.sanitizeString(e.computingId), userToken: e.userToken, // Không sanitize token wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn" }; } else { const n = p.validateSSHConfig(e); if (!n.isValid) { const s = `Invalid SSH configuration: ${n.errors.join(", ")}`; t.error("Invalid SSH config in setConfig", { errors: n.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${s}</p>`, this._dispatchEvent("error", { message: s, errors: n.errors }); return; } this._sshConfig = { host: p.sanitizeString(e.host), username: p.sanitizeString(e.username), password: e.password, // Không sanitize password wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn" }; } this._sshConfig.computingId ? this.initComputingTerminal() : this.initTerminal(); } _injectStyles() { const e = document.createElement("style"); e.textContent = ` .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}.ssh-terminal-container[data-v-f4a58236],.ssh-terminal-container[data-v-b82cc34a]{width:100%;height:100%;min-height:300px;background-color:#000} `, this._shadow.appendChild(e); } _setupResizeObserver() { this._resizeObserver && this._resizeObserver.disconnect(), this._resizeObserver = new ResizeObserver(() => { this._terminalInstance?.resize && (clearTimeout(this._resizeTimeout), this._resizeTimeout = setTimeout(() => { this._terminalInstance.resize(); }, 100)); }), this._resizeObserver.observe(this); } _cleanup() { this._resizeObserver && (this._resizeObserver.disconnect(), this._resizeObserver = null), this._resizeTimeout && clearTimeout(this._resizeTimeout), this._terminalInstance?.dispose && (this._terminalInstance.dispose(), this._terminalInstance = null); } _dispatchEvent(e, r = {}) { this.dispatchEvent( new CustomEvent(e, { detail: r, bubbles: !0, composed: !0 }) ); } initComputingTerminal() { const e = _.createContextLogger("WebComponent"); try { this._terminal = z(this._container, this._sshConfig, this._options), this._dispatchEvent("ready", { message: "Computing terminal initialized", computingId: this._sshConfig.computingId }); } catch (r) { e.error("Failed to create computing terminal", { error: r.message }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">Failed to create terminal: ${r.message}</p>`, this._dispatchEvent("error", { message: r.message }); } } initTerminal() { const e = _.createContextLogger("WebComponent"); if (!this._sshConfig) { const c = "SSH configuration not set. Call setConfig() first."; e.error(c), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${c}</p>`, this._dispatchEvent("error", { message: c }); return; } const { host: r, username: t, password: n, wsUrl: s } = this._sshConfig; let i; if (this._sshConfig.computingId && this._sshConfig.token) { if (i = p.validateComputingConfig(this._sshConfig), !i.isValid) { const c = `Invalid computing configuration: ${i.errors.join(", ")}`; e.error("Computing config validation failed in initTerminal", { errors: i.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${c}</p>`, this._dispatchEvent("error", { message: c, errors: i.errors }); return; } } else if (i = p.validateSSHConfig(this._sshConfig), !i.isValid) { const c = `Invalid SSH configuration: ${i.errors.join(", ")}`; e.error("SSH config validation failed in initTerminal", { errors: i.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${c}</p>`, this._dispatchEvent("error", { message: c, errors: i.errors }); return; } try { this._terminalInstance = z(this._container, { host: r, username: t, password: n, wsUrl: s }), setTimeout(() => { this._dispatchEvent("ready", { host: r, // Only host is relatively safe to expose wsUrl: s }); }, 100); } catch (c) { const l = `Failed to create terminal: ${c.message}`; e.error("Failed to create terminal", { error: c.message }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${l}</p>`, this._dispatchEvent("error", { message: l, error: c }); } } // Public API methods reconnect() { try { this._terminalInstance?.reconnect(), this._dispatchEvent("reconnecting"); } catch (e) { console.error("Reconnect failed:", e), this._dispatchEvent("error", { message: "Reconnect failed", error: e }); } } resize() { try { this._terminalInstance?.resize(); } catch (e) { console.error("Resize failed:", e); } } search(e) { try { return this._terminalInstance?.search(e); } catch (r) { return console.error("Search failed:", r), !1; } } searchPrevious(e) { try { return this._terminalInstance?.searchPrevious(e); } catch (r) { return console.error("Search previous failed:", r), !1; } } // Get terminal instance for advanced usage getTerminalInstance() { return this._terminalInstance; } // Check if terminal is connected isConnected() { return this._terminalInstance && this._terminalInstance.terminal; } } customElements.define("ssh-terminal", ie); typeof window < "u" && window.customElements && !window.customElements.get("ssh-terminal") && console.log("SSH Terminal Web Component registered"); export { ie as SSHTerminalElement, he as Vue2SSHTerminal, pe as Vue3SSHTerminal, z as createTerminal };