UNPKG

ssh-terminal

Version:

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

1,404 lines (1,403 loc) 58.9 kB
var ee = Object.defineProperty; var te = (u, e, t) => e in u ? ee(u, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : u[e] = t; var D = (u, e, t) => (te(u, typeof e != "symbol" ? e + "" : e, t), t); import { Terminal as re } from "xterm"; import { FitAddon as ne } from "xterm-addon-fit"; import { WebLinksAddon as oe } from "xterm-addon-web-links"; import { SearchAddon as ie } from "xterm-addon-search"; import { ref as se, onMounted as ae, onBeforeUnmount as ce, watch as V, nextTick as N, openBlock as le, createElementBlock as de, normalizeClass as K, createElementVNode as ue } from "vue"; class he { constructor() { this.tabId = this.getOrCreateTabId(); } getOrCreateTabId() { const e = `tab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; return console.log("🆕 Created new Tab ID:", e), e; } getTabId() { return this.tabId; } } const ge = new he(); class pe { 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), t = window.isSecureContext || window.location.protocol === "https:", r = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname === "::1"; if (console.log("🔧 _isSecureContext check:", { hasWebCrypto: e, isSecure: t, isLocalhost: r, protocol: window.location.protocol, hostname: window.location.hostname }), e && (t || r)) 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 !t && !r ? (console.warn("🔧 HTTP environment detected - will use micro-rsa-dsa-dh fallback"), !1) : e && (t || r); } /** * Load node-forge khi cần thiết (chỉ cho HTTP) */ async _loadNodeForge() { if (!this.nodeForge) try { const e = await import("./index-9234c122.mjs").then((t) => t.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 t = e.pki.rsa.generateKeyPair({ bits: 2048 }); return this.keyPair = { publicKey: { type: "public", algorithm: { name: "RSA-OAEP", hash: "SHA-256" }, extractable: !0, usages: ["encrypt"], nodeForge: t.publicKey, // Convert to PEM for compatibility pem: e.pki.publicKeyToPem(t.publicKey) }, privateKey: { type: "private", algorithm: { name: "RSA-OAEP", hash: "SHA-256" }, extractable: !0, usages: ["decrypt"], nodeForge: t.privateKey, // Convert to PEM for compatibility pem: e.pki.privateKeyToPem(t.privateKey) } }, console.log("🔧 RSA key pair generated successfully with node-forge"), this.keyPair; } catch (t) { throw console.error("❌ Failed to generate key pair with node-forge:", t), new Error("Key generation failed: " + t.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 t = await window.crypto.subtle.exportKey( "spki", e.publicKey ), r = String.fromCharCode(...new Uint8Array(t)); return `-----BEGIN PUBLIC KEY----- ${window.btoa(r)} -----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 (t) { throw console.error("❌ Lỗi export public key:", t), new Error("Không thể export public key"); } } /** * Import public key từ server */ async importServerPublicKey(e) { try { const t = e.replace(/\r\n/g, ` `).replace(/\r/g, ` `), r = "-----BEGIN PUBLIC KEY-----", n = "-----END PUBLIC KEY-----"; if (!t.includes(r) || !t.includes(n)) throw new Error("Invalid PEM format - missing headers"); const i = t.indexOf(r) + r.length, s = t.indexOf(n); if (i === -1 || s === -1 || i >= s) throw new Error("Invalid PEM format - malformed headers"); const l = t.substring(i, s).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 p = 0; p < o.length; p++) g[p] = o.charCodeAt(p); 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 (t) { throw console.error("❌ Import server public key error:", t), new Error("Không thể import server public key: " + t.message); } } /** * Mã hóa dữ liệu bằng public key của server */ async encryptForServer(e, t = this.serverPublicKey) { try { if (console.log("🔧 encryptForServer called with:", { dataLength: e?.length, hasPublicKey: !!t, useWebCrypto: this.useWebCrypto, publicKeyType: typeof t }), !t) 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:", t); const i = await window.crypto.subtle.encrypt( { name: "RSA-OAEP" }, t, n ); console.log("🔧 Encryption successful, result length:", i.byteLength); const s = btoa(String.fromCharCode(...new Uint8Array(i))); return console.log("🔧 Base64 result length:", s.length), s; } else { console.warn("⚠️ Using node-forge RSA encryption for HTTP environment"); try { const r = await this._loadNodeForge(); if (!t || !t.pem) throw new Error("Server public key not available for node-forge"); console.log("🔧 Attempting node-forge RSA encryption..."); try { const n = r.pki.publicKeyFromPem(t.pem); console.log("✅ Successfully parsed RSA public key with node-forge"), console.log("🔧 Encrypting with node-forge RSA-OAEP..."); const i = n.encrypt(e, "RSA-OAEP", { md: r.md.sha256.create(), mgf1: { md: r.md.sha256.create() } }), s = btoa(i); return console.log("✅ RSA-OAEP encryption successful with node-forge"), console.log(`🔧 Encrypted data length: ${s.length} chars`), s; } catch (n) { console.error("❌ Real encryption failed, falling back to mock:", n), console.warn("⚠️ Using mock encryption as fallback"); const i = new Uint8Array(256); if (window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(i); const c = new TextEncoder().encode(e); for (let l = 0; l < c.length && l < i.length; l++) i[l] ^= c[l]; } else { const c = new TextEncoder().encode(e); for (let l = 0; l < i.length; l++) i[l] = (c[l % c.length] + l + 42) % 256; } const s = btoa(String.fromCharCode(...i)); return console.log("⚠️ Generated mock encryption result"), s; } } catch (r) { throw console.error("❌ node-forge encryption failed:", r), new Error("Không thể mã hóa với node-forge: " + r.message); } } } catch (r) { throw console.error("❌ Lỗi mã hóa dữ liệu:", r), console.error("❌ Error stack:", r.stack), new Error("Không thể mã hóa dữ liệu: " + r.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 t = atob(e), r = new Uint8Array(t.length); for (let s = 0; s < t.length; s++) r[s] = t.charCodeAt(s); const n = await window.crypto.subtle.decrypt( { name: "RSA-OAEP" }, this.keyPair.privateKey, r ); return new TextDecoder().decode(n); } else { console.log("🔧 Using node-forge for decryption"), await this._loadNodeForge(); const t = this.keyPair.privateKey.nodeForge, r = atob(e); return t.decrypt(r, "RSA-OAEP", { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha256.create() } }); } } catch (t) { throw console.error("❌ Lỗi giải mã dữ liệu:", t), new Error("Không thể giải mã dữ liệu: " + t.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, t) { try { const n = new TextEncoder().encode(e), i = window.crypto.getRandomValues(new Uint8Array(12)), s = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: i }, t, n ), c = new Uint8Array(i.length + s.byteLength); return c.set(i), c.set(new Uint8Array(s), i.length), btoa(String.fromCharCode(...c)); } catch (r) { throw console.error("❌ Lỗi mã hóa AES:", r), new Error("Không thể mã hóa AES"); } } /** * Giải mã dữ liệu AES */ async decryptAES(e, t) { try { const r = atob(e), n = new Uint8Array(r.length); for (let o = 0; o < r.length; o++) n[o] = r.charCodeAt(o); const i = n.slice(0, 12), s = n.slice(12), c = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: i }, t, s ); return new TextDecoder().decode(c); } catch (r) { throw console.error("❌ Lỗi giải mã AES:", r), new Error("Không thể giải mã AES"); } } /** * Tạo hash SHA-256 */ async hash(e) { try { if (this.useWebCrypto) { const r = new TextEncoder().encode(e), n = await window.crypto.subtle.digest("SHA-256", r); return Array.from(new Uint8Array(n)).map((s) => s.toString(16).padStart(2, "0")).join(""); } else if (console.warn("⚠️ Using mock hash for HTTP testing"), window.crypto && window.crypto.getRandomValues) { let t = 0; for (let n = 0; n < e.length; n++) { const i = e.charCodeAt(n); t = (t << 5) - t + i, t = t & t; } return Math.abs(t).toString(16).padStart(8, "0").repeat(8).substring(0, 64); } else { let t = 0; for (let r = 0; r < e.length; r++) t = (t << 5) - t + e.charCodeAt(r), t = t & t; return Math.abs(t).toString(16).padStart(8, "0").repeat(8).substring(0, 64); } } catch (t) { throw console.error("❌ Lỗi tạo hash:", t), new Error("Không thể tạo hash"); } } /** * Tạo random string */ generateRandomString(e = 32) { if (this.useWebCrypto) { const t = new Uint8Array(e); return window.crypto.getRandomValues(t), Array.from(t, (r) => r.toString(16).padStart(2, "0")).join(""); } else { console.warn("⚠️ Using Math.random for random generation in HTTP environment"); let t = ""; const r = "0123456789abcdef"; for (let n = 0; n < e * 2; n++) t += r.charAt(Math.floor(Math.random() * r.length)); return t; } } } class f { /** * Xác thực SSH configuration */ static validateSSHConfig(e) { const t = []; return e.host ? typeof e.host != "string" ? t.push("Host phải là string") : e.host.length > 255 ? t.push("Host quá dài (tối đa 255 ký tự)") : this.isValidHost(e.host) || t.push("Host không hợp lệ") : t.push("Host là bắt buộc"), e.username ? typeof e.username != "string" ? t.push("Username phải là string") : e.username.length > 32 ? t.push("Username quá dài (tối đa 32 ký tự)") : this.isValidUsername(e.username) || t.push("Username chứa ký tự không hợp lệ") : t.push("Username là bắt buộc"), e.password ? typeof e.password != "string" ? t.push("Password phải là string") : e.password.length < 1 ? t.push("Password không được để trống") : e.password.length > 128 && t.push("Password quá dài (tối đa 128 ký tự)") : t.push("Password là bắt buộc"), e.port !== void 0 && (Number.isInteger(e.port) ? (e.port < 1 || e.port > 65535) && t.push("Port phải trong khoảng 1-65535") : t.push("Port phải là số nguyên")), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && t.push("WebSocket URL không hợp lệ"), { isValid: t.length === 0, errors: t }; } /** * 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 t = new URL(e); return t.protocol === "ws:" || t.protocol === "wss:"; } catch { return !1; } } /** * Xác thực encryption readiness */ static validateEncryptionReadiness(e, t) { const r = []; return e || r.push("Encryption service not initialized"), t || r.push("Server public key not available"), { isValid: r.length === 0, errors: r }; } /** * Xác thực computing configuration */ static validateComputingConfig(e) { const t = []; return e.computingId ? typeof e.computingId != "string" ? t.push("Computing ID must be a string") : /^[0-9.]+$/.test(e.computingId) ? e.computingId.length > 50 && t.push("Computing ID must not exceed 50 characters") : t.push("Computing ID must contain only numbers and dots") : t.push("Computing ID is required"), e.userToken ? typeof e.userToken != "string" ? t.push("User token must be a string") : e.userToken.length > 8192 && t.push("User token is too large") : t.push("User token is required"), e.wsUrl && !this.isValidWebSocketURL(e.wsUrl) && t.push("Invalid WebSocket URL"), { isValid: t.length === 0, errors: t }; } /** * Xác thực rằng connection chỉ sử dụng encrypted protocols */ static validateSecureConnection(e) { const t = [], r = []; if (!e) return t.push("WebSocket URL is required"), { isValid: !1, errors: t, warnings: r }; try { const n = new URL(e); n.protocol === "ws:" ? r.push("Warning: Using unencrypted WebSocket (ws://). Consider using wss:// for better security.") : n.protocol !== "wss:" && t.push("Invalid WebSocket protocol. Only ws:// and wss:// are supported."); } catch { t.push("Invalid WebSocket URL format"); } return { isValid: t.length === 0, errors: t, warnings: r }; } /** * Làm sạch string input */ static sanitizeString(e, t = 255) { return typeof e != "string" ? "" : e.trim().slice(0, t).replace(/[\x00-\x1F\x7F]/g, ""); } /** * Xác thực terminal options */ static validateTerminalOptions(e) { const t = []; 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) && t.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) && t.push("scrollback phải là số nguyên từ 0-10000"), e.theme !== void 0) if (typeof e.theme != "object") t.push("theme phải là object"); else { const r = ["background", "foreground", "cursor", "cursorAccent", "selection"]; for (const [n, i] of Object.entries(e.theme)) r.includes(n) && typeof i == "string" && (this.isValidColor(i) || t.push(`theme.${n} không phải màu hợp lệ`)); } return e.reconnection !== void 0 && (typeof e.reconnection != "object" ? t.push("reconnection phải là object") : (e.reconnection.maxAttempts !== void 0 && (!Number.isInteger(e.reconnection.maxAttempts) || e.reconnection.maxAttempts < 0 || e.reconnection.maxAttempts > 20) && t.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) && t.push("reconnection.heartbeatInterval phải là số nguyên từ 1000-300000ms"))), { isValid: t.length === 0, errors: t }; } /** * 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 t = e.split("."); return t.length === 3 && t.every((r) => r.length > 0); } /** * Xác thực message size */ static validateMessageSize(e, t = 1e4) { if (typeof e == "string") return e.length <= t; if (e instanceof ArrayBuffer) return e.byteLength <= t; try { return JSON.stringify(e).length <= t; } catch { return !1; } } /** * Làm sạch object để logging an toàn */ static sanitizeForLogging(e) { if (typeof e != "object" || e === null) return e; const t = ["password", "token", "key", "secret", "auth", "credential"], r = {}; for (const [n, i] of Object.entries(e)) { const s = n.toLowerCase(); t.some((c) => s.includes(c)) ? r[n] = "***HIDDEN***" : typeof i == "object" && i !== null ? r[n] = this.sanitizeForLogging(i) : r[n] = i; } return r; } } const me = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, default: f }, Symbol.toStringTag, { value: "Module" })), L = class L { 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 L.LEVELS[e] >= L.LEVELS[this.logLevel]; } /** * Làm sạch dữ liệu trước khi log */ sanitizeData(e) { if (e == null) return e; const t = f.sanitizeForLogging(e), r = JSON.stringify(t); return r.length > this.maxLogLength ? { ...t, _truncated: !0, _originalLength: r.length } : t; } /** * Format log message */ formatMessage(e, t, r = null) { const i = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${e.toUpperCase()}] [CLIENT]`; if (r) { const s = this.sanitizeData(r); return `${i} ${t} ${JSON.stringify(s)}`; } return `${i} ${t}`; } /** * Debug log */ debug(e, t = null) { this.shouldLog("debug") && console.debug(this.formatMessage("debug", e, t)); } /** * Info log */ info(e, t = null) { this.shouldLog("info") && console.info(this.formatMessage("info", e, t)); } /** * Warning log */ warn(e, t = null) { this.shouldLog("warn") && console.warn(this.formatMessage("warn", e, t)); } /** * Error log */ error(e, t = null) { this.shouldLog("error") && console.error(this.formatMessage("error", e, t)); } /** * Log connection events */ logConnection(e, t = {}) { const r = this.sanitizeData(t); this.info(`Connection ${e}`, r); } /** * Log authentication events */ logAuth(e, t = {}) { const r = this.sanitizeData(t); this.info(`Auth ${e}`, r); } /** * Log security events */ logSecurity(e, t = {}) { const r = this.sanitizeData(t); this.warn(`Security ${e}`, r); } /** * Log performance metrics */ logPerformance(e, t, r = "ms") { this.debug(`Performance ${e}`, { value: t, unit: r }); } /** * Log với custom level */ log(e, t, r = null) { this.shouldLog(e) && console[e](this.formatMessage(e, t, r)); } /** * Tạo logger instance với context */ createContextLogger(e) { return { debug: (t, r) => this.debug(`[${e}] ${t}`, r), info: (t, r) => this.info(`[${e}] ${t}`, r), warn: (t, r) => this.warn(`[${e}] ${t}`, r), error: (t, r) => this.error(`[${e}] ${t}`, r), logConnection: (t, r) => this.logConnection(`[${e}] ${t}`, r), logAuth: (t, r) => this.logAuth(`[${e}] ${t}`, r), logSecurity: (t, r) => this.logSecurity(`[${e}] ${t}`, r), logPerformance: (t, r, n) => this.logPerformance(`[${e}] ${t}`, r, n) }; } /** * Alias for backward compatibility */ createContext(e) { return this.createContextLogger(e); } /** * Set log level */ setLogLevel(e) { L.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 */ D(L, "LEVELS", { debug: 0, info: 1, warn: 2, error: 3 }); let O = L; const _ = new O(), fe = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, SecureLogger: O, default: _ }, Symbol.toStringTag, { value: "Module" })); class ye { constructor() { this.connections = /* @__PURE__ */ new Map(), this.contextLogger = _.createContextLogger("WebSocketPool"); } /** * Get hoặc tạo mới WebSocket connection * @param {string} wsUrl - WebSocket URL * @param {string} connectionKey - Unique key (tabId:computingId) * @param {object} handlers - {onOpen, onMessage, onError, onClose} */ async getConnection(e, t, r) { this.contextLogger.info("Getting WebSocket connection", { wsUrl: e, connectionKey: t }), this.connections.has(e) || this.connections.set(e, /* @__PURE__ */ new Map()); const n = this.connections.get(e); if (n.has(t)) { const i = n.get(t), s = i.socket; if (s.readyState === WebSocket.OPEN || s.readyState === WebSocket.CONNECTING) return this.contextLogger.info("Reusing existing WebSocket", { connectionKey: t }), i.handlers = r, s; this.contextLogger.info("Existing WebSocket is dead, creating new one", { connectionKey: t }), n.delete(t); } return this.createNewConnection(e, t, r); } /** * Tạo WebSocket connection mới */ createNewConnection(e, t, r) { const { onMessage: n, onError: i, onClose: s, onOpen: c } = r; this.contextLogger.info("Creating new WebSocket connection", { wsUrl: e, connectionKey: t }), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log("🔌 Creating WebSocket"); const l = new WebSocket(e), o = this.connections.get(e); return o.set(t, { socket: l, handlers: r }), l.onopen = () => { if (console.log(`✅ [${t}] WebSocket OPENED`), this.contextLogger.info("WebSocket opened", { wsUrl: e, connectionKey: t }), c) try { c(); } catch (g) { this.contextLogger.error("Error in onOpen handler", { error: g.message }); } }, l.onmessage = (g) => { const p = o.get(t); if (!p) return; const w = p.handlers; try { w.onMessage && w.onMessage(g); } catch (h) { this.contextLogger.error("Error in onMessage handler", { connectionKey: t, error: h.message }); } }, l.onerror = (g) => { console.error(`❌ [${t}] WebSocket ERROR:`, g), this.contextLogger.error("WebSocket error", { wsUrl: e, connectionKey: t, error: g }); const p = o.get(t); if (p && p.handlers.onError) try { p.handlers.onError(g); } catch (w) { this.contextLogger.error("Error in onError handler", { error: w.message }); } }, l.onclose = (g) => { console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), console.log(`🚪 [${t}] WebSocket CLOSED`), console.log("Code:", g.code), console.log("Reason:", g.reason || "(empty)"), console.log("Was Clean:", g.wasClean), console.log("Timestamp:", (/* @__PURE__ */ new Date()).toISOString()), console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"), this.contextLogger.info("WebSocket closed", { wsUrl: e, connectionKey: t, code: g.code, reason: g.reason, wasClean: g.wasClean }); const p = o.get(t), w = p ? p.handlers : null; if (o.delete(t), console.log(`📊 After close - Remaining connections: ${o.size}`), o.size === 0 && (this.connections.delete(e), this.contextLogger.info("All connections closed for URL", { wsUrl: e })), w && w.onClose) try { w.onClose(g); } catch (h) { this.contextLogger.error("Error in onClose handler", { error: h.message }); } }, l; } /** * Xóa subscriber và đóng connection */ removeSubscriber(e, t) { this.contextLogger.info("Removing subscriber", { wsUrl: e, connectionKey: t }); const r = this.connections.get(e); if (!r) { this.contextLogger.warn("No connections found for URL", { wsUrl: e }); return; } const n = r.get(t); if (!n) { this.contextLogger.warn("Connection not found", { connectionKey: t }); return; } const i = n.socket; i && i.readyState !== WebSocket.CLOSED && i.readyState !== WebSocket.CLOSING && i.close(1e3, "Subscriber removed"), r.delete(t), r.size === 0 && (this.connections.delete(e), this.contextLogger.info("All connections removed for URL", { wsUrl: e })); } /** * Gửi message qua WebSocket * @param {string} wsUrl * @param {string} connectionKey * @param {object|string} message */ send(e, t, r) { const n = this.connections.get(e); if (!n) throw new Error(`No connections found for ${e}`); const i = n.get(t); if (!i) throw new Error(`No connection found for key: ${t}`); const s = i.socket; if (s.readyState !== WebSocket.OPEN) throw new Error(`Connection not ready (state: ${s.readyState})`); const c = typeof r == "string" ? r : JSON.stringify(r); s.send(c); } /** * Kiểm tra connection có đang mở không */ isConnected(e, t) { const r = this.connections.get(e); if (!r) return !1; const n = r.get(t); return n ? n.socket.readyState === WebSocket.OPEN : !1; } /** * Đếm số connection cho một URL */ getConnectionCount(e) { const t = this.connections.get(e); return t ? t.size : 0; } /** * Đóng tất cả connections */ dispose() { this.contextLogger.info("Disposing all connections"), this.connections.forEach((e, t) => { e.forEach((r, n) => { const i = r.socket; i.readyState !== WebSocket.CLOSED && i.readyState !== WebSocket.CLOSING && i.close(1e3, "Pool disposed"); }); }), this.connections.clear(); } /** * Debug: Lấy thông tin tất cả connections */ getDebugInfo() { const e = {}; return this.connections.forEach((t, r) => { e[r] = { count: t.size, connections: [] }, t.forEach((n, i) => { e[r].connections.push({ key: i, state: n.socket.readyState, stateText: ["CONNECTING", "OPEN", "CLOSING", "CLOSED"][n.socket.readyState] }); }); }), e; } } const S = new ye(), j = { 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, reconnection: { enabled: !0, maxAttempts: 5, heartbeatInterval: 3e4 } }; function W(u, e, t = {}) { const r = _.createContextLogger("Terminal"); if (!u) throw r.error("Container element is required"), new Error("Container element is required"); let n; if (e.computingId && e.userToken) { if (n = f.validateComputingConfig(e), !n.isValid) throw r.error("Invalid computing configuration", { errors: n.errors }), new Error(`Computing configuration invalid: ${n.errors.join(", ")}`); } else if (n = f.validateSSHConfig(e), !n.isValid) throw r.error("Invalid SSH configuration", { errors: n.errors }), new Error(`SSH configuration invalid: ${n.errors.join(", ")}`); const s = f.validateTerminalOptions(t); if (!s.isValid) throw r.error("Invalid terminal options", { errors: s.errors }), new Error(`Terminal options invalid: ${s.errors.join(", ")}`); const c = { ...j, ...t.theme ? { theme: { ...j.theme, ...t.theme } } : {}, ...t }, l = t.showConnectionLogs === !0, o = new re(c), g = new ne(), p = new oe(), w = new ie(); o.loadAddon(g), o.loadAddon(p), o.loadAddon(w), o.open(u), setTimeout(() => { g.fit(); }, 0); const h = e.wsUrl || "wss://ssh-proxy.dev.longvan.vn"; if (!f.isValidWebSocketURL(h)) throw r.error("Invalid WebSocket URL", { wsUrl: h }), new Error("Invalid WebSocket URL"); const v = f.validateSecureConnection(h); if (!v.isValid) throw r.error("Insecure WebSocket connection", { errors: v.errors }), new Error(`Insecure connection: ${v.errors.join(", ")}`); v.warnings && v.warnings.length > 0 && v.warnings.forEach((d) => { o.writeln(`⚠️ ${d}`); }); let E = !1, b = null, A = null, C = 0; const P = c.reconnection?.maxAttempts || 5, H = c.reconnection?.enabled !== !1; let U = null, $ = !1, z = !1; const x = e.computingId || `direct-${Date.now()}`, B = e.tabClientId || `direct-${Date.now()}`, I = ge.getTabId(), y = `${I}:${B}`; r.info("Creating terminal", { tabId: I, computingId: x, connectionKey: y }), console.log("Creating terminal", { tabId: I, computingId: x, connectionKey: y }); const q = async () => { try { return b = new pe(), await b.generateKeyPair(), !0; } catch (d) { return r.error("Failed to initialize encryption", { error: d.message }), !1; } }, G = async () => { try { const d = f.validateEncryptionReadiness(b, A); if (!d.isValid) throw new Error(`Encryption not ready: ${d.errors.join(", ")}`); let a; console.log("typeConnect in createTerminal", e.typeConnect); const k = e.typeConnect || "ssh"; if (console.log("currentTypeConnect terminal", k), e.computingId && e.userToken) { const m = await b.encryptForServer(e.userToken, A); a = { type: "encrypted-auth", computingId: e.computingId, encryptedToken: m, clientPublicKey: await b.exportPublicKey(), // 🔥 Gửi kèm typeConnect để Proxy biết đường phân luồng typeConnect: k }, l && o.writeln("🔐 Sent encrypted computing credentials..."); } else { const m = await b.encryptForServer(e.password, A); a = { type: "encrypted-auth", host: e.host, username: e.username, encryptedPassword: m, clientPublicKey: await b.exportPublicKey(), typeConnect: k }, l && o.writeln("🔐 Sent encrypted SSH credentials..."); } S.send(h, y, a); } catch (d) { throw r.error("Failed to send encrypted auth", { error: d.message }), o.writeln(`\r ❌ Encryption failed: ${d.message}\r `), o.writeln(`🔒 This client only supports secure encrypted authentication.\r `), S.removeSubscriber(h, y), d; } }, Y = () => { if ($) return; $ = !0, C++; const d = Math.min(1e3 * Math.pow(2, C - 1), 3e4); o.writeln( `\r Attempting to reconnect (${C}/${P}) in ${d / 1e3}s...\r ` ), U = setTimeout(() => { M(); }, d); }, Z = async () => { E = !0, C = 0, $ = !1, l && o.writeln( C > 0 ? `\r ✅ Reconnected to SSH relay server\r ` : "✅ WebSocket connected to " + h ), l && o.writeln("🔑 Waiting for server public key..."); }, J = async (d) => { try { if (!d || !d.data) return; let a; if (typeof d.data == "string") try { a = JSON.parse(d.data); } catch { o.write(d.data); return; } else a = d.data; if (console.log(`📩 Message from Server [${a.type}]:`, a), a.type === "rdp-redirect") { r.info("🚀 Received RDP redirect command", { url: a.url }), typeof o._onRdpRedirect == "function" && o._onRdpRedirect(a); return; } if (!f.validateMessageSize(d.data, 5e4)) { r.logSecurity("message_too_large", { size: d.data.length }); return; } if (a.type === "welcome") { try { if (a.publicKey) A = await b.importServerPublicKey(a.publicKey), l && o.writeln("🔑 Server public key received"), await G(); else { o.writeln(`\r ❌ Server does not support encrypted authentication\r `), o.writeln(`🔒 This client requires RSA encryption for security\r `), S.removeSubscriber(h, y); return; } } catch (m) { r.error("Failed to process welcome message", { error: m.message }), o.writeln(`\r ❌ Failed to process server welcome: ${m.message}\r `), S.removeSubscriber(h, y); return; } return; } if (!(!a.computingId || a.computingId === x)) return; if (a.type === "data") try { const m = window.atob(a.data), T = new Uint8Array(m.length); for (let R = 0; R < m.length; R++) T[R] = m.charCodeAt(R); const F = new TextDecoder().decode(T); o.write(F); } catch (m) { r.error("Failed to decode terminal data", { error: m.message }); try { o.write(window.atob(a.data)); } catch { o.write(a.data); } } else a.type === "error" ? (r.error("Server error received", { code: a.code, message: a.message }), o.writeln(`\r ❌ Error: ${a.message}\r `)) : a.type === "status" && a.status === "authenticated" ? (z = !0, console.log("Xác thực thành công"), 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" ? (z = !0, console.log("Xác thực thành công"), 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 === "session-created" ? z = !0 : a.type === "auth-error" && (r.logAuth("auth_failed", { message: a.message }), o.writeln(`\r ❌ Authentication failed: ${a.message}\r `)); } catch { d && d.data && o.write(d.data); } }, X = (d) => { E = !1, o.writeln(`\r WebSocket error: ${d?.message || "Unknown error"}\r `), o.writeln(`\r Please check if the SSH proxy server is running at ${h}\r `); }, Q = (d) => { if (!d) return; if (E = !1, d.code === 1e3) { o.writeln(`\r Connection to SSH relay server closed normally\r `); return; } const k = { 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" }[d.code] || `Unknown error (Code: ${d.code})`; o.writeln(`\r Connection to SSH relay server closed: ${k}\r `), d.reason && o.writeln(`Reason: ${d.reason}\r `), H && !$ && C < P ? Y() : H && C >= P && o.writeln(`\r Max reconnection attempts reached. Please refresh the page.\r `); }, M = async () => { if (l && (o.writeln(`🔌 Connecting to ${h}...`), o.writeln(`📱 Tab ID: ${I}`), o.writeln(`💻 Computing ID: ${x}`)), !await q()) { o.writeln(`\r ❌ Failed to initialize encryption\r `); return; } try { const a = { onOpen: Z, onMessage: J, onError: X, onClose: Q }; await S.getConnection(h, y, a), r.info("Connected to WebSocket pool", { wsUrl: h, tabId: I, computingId: x, connectionKey: y, totalConnections: S.getConnectionCount(h) }); } catch (a) { r.error("Failed to connect", { error: a.message, wsUrl: h }), o.writeln(`\r ❌ Failed to connect: ${a.message}\r `); } }; return o.onData((d) => { if (E && S.isConnected(h, y)) { if (!f.validateMessageSize(d, 1e4)) { r.warn("Terminal input too large", { size: d.length }); return; } let a; try { const m = new TextEncoder().encode(d), T = String.fromCharCode(...m); a = window.btoa(T); } catch (m) { r.error("Failed to encode terminal data", { error: m.message }); try { a = window.btoa(d); } catch { const F = d.replace(/[^\x00-\x7F]/g, "?"); a = window.btoa(F); } } const k = { type: "data", data: a, computingId: x // 🔥 FIX: Dùng computingId thay vì sshConfig.computingId }; try { S.send(h, y, k); } catch (m) { r.error("Failed to send terminal data", { error: m.message }); } } }), setTimeout(() => { M(); }, 100), g.fit(), { terminal: o, fitAddon: g, searchAddon: w, reconnect: async () => { await M(); }, resize: () => { if (!(!o || !g)) if (g.fit(), S.isConnected(h, y) && z) { const d = { type: "resize", cols: o.cols, rows: o.rows }; try { S.send(h, y, d), console.log(`📏 Server PTY Resized to: ${o.cols}x${o.rows}`); } catch { console.warn("⚠️ Failed to sync resize to server"); } } else console.log("⏳ Resize deferred: Waiting for authentication..."); }, dispose: () => { console.log(`🗑️ Disposing terminal [${y}]`), U && clearTimeout(U), S.removeSubscriber(h, y), o.dispose(), r.info("Terminal disposed", { wsUrl: h, tabId: I, computingId: x, connectionKey: y, remainingConnections: S.getConnectionCount(h) }); }, getConnectionInfo: () => ({ wsUrl: h, tabId: I, computingId: x, connectionKey: y, connected: S.isConnected(h, y), totalConnections: S.getConnectionCount(h) }) }; } const we = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, createTerminal: W }, Symbol.toStringTag, { value: "Module" })), Ae = { 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 u = this, e = this.$refs.terminalContainer; Promise.all([ Promise.resolve().then(() => we), Promise.resolve().then(() => me), Promise.resolve().then(() => fe) ]).then(function(t) { var r = t[0].createTerminal, n = t[1].default, i = t[2].default, s = i.createContextLogger("Vue2Terminal"), c = { computingId: u.computingId, userToken: u.userToken, wsUrl: u.websocketUrl }, l = n.validateComputingConfig(c); if (!l.isValid) { s.error("Invalid Computing ID config in Vue2 component", { errors: l.errors }), u.$emit("error", { message: "Invalid Computing ID configuration", errors: l.errors }); return; } var o = n.validateTerminalOptions(u.options); if (!o.isValid) { s.error("Invalid terminal options in Vue2 component", { errors: o.errors }), u.$emit("error", { message: "Invalid terminal options", errors: o.errors }); return; } try { u.terminal = r(e, c, u.options), s.info("Terminal created successfully with Computing ID"), u.$emit("ready", u.terminal); } catch (g) { s.error("Failed to create terminal", { error: g.message }), u.$emit("error", { message: "Failed to create terminal", error: g.message }); } }).catch(function(t) { console.error("Failed to load terminal dependencies:", t), u.$emit("error", { message: "Failed to load terminal dependencies", error: t.message }); }); }, handleResize: function() { this.terminal && this.terminal.resize(); } }, render: function(u) { return u("div", { ref: "terminalContainer", class: "ssh-terminal-container", style: { width: "100%", height: "100%", minHeight: "300px", backgroundColor: "#000" } }); } }; const be = (u, e) => { const t = u.__vccOpts || u; for (const [r, n] of e) t[r] = n; return t; }, Se = { __name: "SSHTerminal", props: { computingId: { type: String, required: !0 }, userToken: { type: String, required: !0 }, websocketUrl: { type: String, default: "wss://ssh-proxy.dev.longvan.vn" }, typeConnect: { type: Object }, options: { type: Object, default: () => ({}) }, isActive: { type: Boolean, default: !0 }, resizeSignal: { type: Number, default: 0 }, tabClientId: { type: String, required: !0 } }, emits: ["ready", "error", "rdp-redirect"], setup(u, { expose: e, emit: t }) { const r = u, n = t, i = se(null); let s = null, c = null, l = null; ae(() => { g(); }), ce(() => { s && s.dispose(), window.removeEventListener("resize", o), c && c.disconnect(), l && clearTimeout(l); }), V( () => r.resizeSignal, () => { N(() => { setTimeout(o, 150); }); } ), V( () => r.isActive, (w) => { w && s && N(() => { s.terminal?.focus(); }); } ); function o() { l && clearTimeout(l), l = setTimeout(() => { p(); }, 250); } function g() { const w = i.value, h = _.createContextLogger("Vue3Terminal"), v = { computingId: r.computingId, userToken: r.userToken, wsUrl: r.websocketUrl, typeConnect: r.typeConnect, tabClientId: r.tabClientId }, E = f.validateComputingConfig(v); if (!E.isValid) { n("error", { message: "Invalid Computing ID configuration", errors: E.errors }); return; } try { s = W(w, v, r.options), s._onRdpRedirect = (b) => { n("rdp-redirect", b); }, c = new ResizeObserver((b) => { for (let A of b) { const { width: C, height: P } = A.contentRect; C > 0 && P > 0 && o(); } }), i.value && c.observe(i.value), n("ready", s); } catch (b) { h.error("Failed to create terminal", { error: b.message }), n("error", { message: "Failed to create terminal", error: b.message }); } } function p() { s && (console.log("📏 Terminal resizing to fit container..."), s.resize()); } return e({ getTerminal: () => s, resize: p }), (w, h) => (le(), de("div", { class: K(["terminal-wrapper", `computingId: ${r.computingId}`]) }, [ ue("div", { ref_key: "terminalContainer", ref: i, class: K(`ssh-terminal-container ${r.computingId}`) }, null, 2) ], 2)); } }, Te = /* @__PURE__ */ be(Se, [["__scopeId", "data-v-0a812d31"]]); class ve 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, t, r) { e === "ws-url" && (this._wsUrl = r); } // 🔒 Secure method to set SSH configuration với validation setConfig(e, t = {}) { const r = _.createContextLogger("WebComponent"); if (this._options = { ...this._options, ...t }, e.computingId && e.userToken) { const n = f.validateComputingConfig(e); if (!n.isValid) { const i = `Invalid computing configuration: ${n.errors.join(", ")}`; r.error("Invalid computing config in setConfig", { errors: n.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${i}</p>`, this._dispatchEvent("error", { message: i, errors: n.errors }); return; } this._sshConfig = { computingId: f.sanitizeString(e.computingId), userToken: e.userToken, // Không sanitize token wsUrl: e.wsUrl || this._wsUrl || "wss://ssh-proxy.dev.longvan.vn" }; } else { const n = f.validateSSHConfig(e);