ssh-terminal
Version:
SSH Terminal component based on xterm.js for multiple frontend frameworks
1,289 lines (1,288 loc) • 50 kB
JavaScript
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
};