ssh-terminal
Version:
SSH Terminal component based on xterm.js for multiple frontend frameworks
1,186 lines (1,185 loc) • 46.8 kB
JavaScript
var M = Object.defineProperty;
var D = (g, e, r) => e in g ? M(g, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : g[e] = r;
var W = (g, e, r) => (D(g, typeof e != "symbol" ? e + "" : e, r), r);
import { Terminal as N } from "xterm";
import { FitAddon as j } 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 i = r.indexOf(t) + t.length, s = r.indexOf(n);
if (i === -1 || s === -1 || i >= s)
throw new Error("Invalid PEM format - malformed headers");
const l = r.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 m = new Uint8Array(o.length);
for (let w = 0; w < o.length; w++)
m[w] = o.charCodeAt(w);
return this.useWebCrypto ? (this.serverPublicKey = await window.crypto.subtle.importKey(
"spki",
m.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 i = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
r,
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 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 i = n.encrypt(e, "RSA-OAEP", {
md: t.md.sha256.create(),
mgf1: {
md: t.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 (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 s = 0; s < r.length; s++)
t[s] = r.charCodeAt(s);
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), i = window.crypto.getRandomValues(new Uint8Array(12)), s = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: i
},
r,
n
), c = new Uint8Array(i.length + s.byteLength);
return c.set(i), c.set(new Uint8Array(s), i.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 i = n.slice(0, 12), s = n.slice(12), c = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: i
},
r,
s
);
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((s) => s.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 i = e.charCodeAt(n);
r = (r << 5) - r + i, 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, i] of Object.entries(e.theme))
t.includes(n) && typeof i == "string" && (this.isValidColor(i) || 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, i] of Object.entries(e)) {
const s = n.toLowerCase();
r.some((c) => s.includes(c)) ? t[n] = "***HIDDEN***" : typeof i == "object" && i !== null ? t[n] = this.sanitizeForLogging(i) : t[n] = i;
}
return t;
}
}
const 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 i = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${e.toUpperCase()}] [CLIENT]`;
if (t) {
const s = this.sanitizeData(t);
return `${i} ${r} ${JSON.stringify(s)}`;
}
return `${i} ${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
*/
W(E, "LEVELS", {
debug: 0,
info: 1,
warn: 2,
error: 3
});
let R = E;
const A = new R(), F = {
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 K(g, e, r = {}) {
const t = A.createContextLogger("Terminal");
if (!g)
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 s = p.validateTerminalOptions(r);
if (!s.isValid)
throw t.error("Invalid terminal options", { errors: s.errors }), new Error(`Terminal options invalid: ${s.errors.join(", ")}`);
const c = {
...F,
...r.theme ? { theme: { ...F.theme, ...r.theme } } : {},
...r
}, l = r.showConnectionLogs === !0, o = new N(c), m = new j(), w = new B(), S = new q();
o.loadAddon(m), o.loadAddon(w), o.loadAddon(S), o.open(g), setTimeout(() => {
m.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((h) => {
o.writeln(`⚠️ ${h}`);
});
let u = null, _ = !1, x = null, P = null, k = 0;
const L = c.reconnection?.maxAttempts || 5, U = c.reconnection?.enabled !== !1;
let z = null, I = !1;
const H = () => {
if (I)
return;
I = !0, k++;
const h = Math.min(1e3 * Math.pow(2, k - 1), 3e4);
o.writeln(`\r
Attempting to reconnect (${k}/${L}) in ${h / 1e3}s...\r
`), z = setTimeout(() => {
$();
}, h);
}, O = async () => {
try {
return x = new X(), await x.generateKeyPair(), !0;
} catch (h) {
return t.error("Failed to initialize encryption", { error: h.message }), !1;
}
}, V = async () => {
try {
const h = p.validateEncryptionReadiness(x, P);
if (!h.isValid)
throw new Error(`Encryption not ready: ${h.errors.join(", ")}`);
let d;
if (e.computingId && e.userToken) {
const a = await x.encryptForServer(e.userToken, P);
d = {
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);
d = {
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...");
}
u.send(JSON.stringify(d));
} catch (h) {
throw t.error("Failed to send encrypted auth", { error: h.message }), o.writeln(`\r
❌ Encryption failed: ${h.message}\r
`), o.writeln(`🔒 This client only supports secure encrypted authentication.\r
`), o.writeln(`Please ensure the server supports RSA encryption.\r
`), u && u.readyState === WebSocket.OPEN && u.close(1e3, "Encryption required"), h;
}
}, $ = async () => {
if (u && u.readyState !== WebSocket.CLOSED && u.close(), l && o.writeln(`🔌 Connecting to ${y}...`), !await O()) {
o.writeln(`\r
❌ Failed to initialize encryption\r
`);
return;
}
try {
u = new WebSocket(y);
} catch (d) {
t.error("Failed to create WebSocket", { error: d.message, wsUrl: y }), o.writeln(`\r
❌ Failed to connect to ${y}: ${d.message}\r
`);
return;
}
u.onopen = async () => {
_ = !0, k = 0, I = !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...");
}, u.onmessage = async (d) => {
try {
const a = JSON.parse(d.data);
if (!p.validateMessageSize(d.data, 5e4)) {
t.logSecurity("message_too_large", { size: d.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
`), u && u.readyState === WebSocket.OPEN && u.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
`), u && u.readyState === WebSocket.OPEN && u.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 T = new TextDecoder().decode(v);
o.write(T);
} 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(d.data);
}
}, u.onclose = (d) => {
if (_ = !1, d.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"
}[d.code] || `Unknown error (Code: ${d.code})`;
o.writeln(`\r
Connection to SSH relay server closed: ${f}\r
`), d.reason && o.writeln(`Reason: ${d.reason}\r
`), U && !I && k < L ? H() : U && k >= L ? 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
`);
}, u.onerror = (d) => {
_ = !1, o.writeln(`\r
WebSocket error: ${d.message || "Unknown error"}\r
`), o.writeln(`\r
Please check if the SSH proxy server is running at ${y}\r
`);
}, o.onData((d) => {
if (_ && u.readyState === WebSocket.OPEN) {
if (!p.validateMessageSize(d, 1e4)) {
t.warn("Terminal input too large", { size: d.length });
return;
}
let a;
try {
const v = new TextEncoder().encode(d), T = String.fromCharCode(...v);
a = window.btoa(T);
} catch (v) {
t.error("Failed to encode terminal data", { error: v.message });
try {
a = window.btoa(d);
} catch {
const C = d.replace(/[^\x00-\x7F]/g, "?");
a = window.btoa(C);
}
}
const f = {
type: "data",
data: a
};
try {
u.send(JSON.stringify(f));
} catch (v) {
t.error("Failed to send terminal data", { error: v.message });
}
}
});
};
return setTimeout(() => {
$();
}, 100), m.fit(), {
terminal: o,
fitAddon: m,
searchAddon: S,
reconnect: async () => {
await $();
},
resize: () => {
m.fit();
},
dispose: () => {
z && clearTimeout(z), u && u.close(), o.dispose();
},
search: (h, d) => {
S.findNext(h, d);
},
searchPrevious: (h, d) => {
S.findPrevious(h, d);
}
};
}
const ee = (g, e) => {
const r = g.__vccOpts || g;
for (const [t, n] of e)
r[t] = n;
return r;
}, re = {
__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(g, { expose: e, emit: r }) {
const t = g, n = r, i = G(null);
let s = null;
J(() => {
c(), window.addEventListener("resize", l);
}), Z(() => {
s && s.dispose(), window.removeEventListener("resize", l);
});
function c() {
const o = i.value, m = A.createContextLogger("Vue3Terminal"), w = {
computingId: t.computingId,
userToken: t.userToken,
wsUrl: t.websocketUrl
}, S = p.validateComputingConfig(w);
if (!S.isValid) {
m.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) {
m.error("Invalid terminal options in Vue3 component", { errors: y.errors }), n("error", {
message: "Invalid terminal options",
errors: y.errors
});
return;
}
try {
s = K(o, w, t.options), m.info("Terminal created successfully with Computing ID"), n("ready", s);
} catch (b) {
m.error("Failed to create terminal", { error: b.message }), n("error", {
message: "Failed to create terminal",
error: b.message
});
}
}
function l() {
s && s.resize();
}
return e({
getTerminal: () => s
}), (o, m) => (Y(), Q("div", {
ref_key: "terminalContainer",
ref: i,
class: "ssh-terminal-container"
}, null, 512));
}
}, le = /* @__PURE__ */ ee(re, [["__scopeId", "data-v-660095fd"]]);
class te 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 = A.createContextLogger("WebComponent");
if (this._options = { ...this._options, ...r }, e.computingId && e.userToken) {
const n = p.validateComputingConfig(e);
if (!n.isValid) {
const i = `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;">${i}</p>`, this._dispatchEvent("error", { message: i, 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 i = `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;">${i}</p>`, this._dispatchEvent("error", { message: i, 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 = A.createContextLogger("WebComponent");
try {
this._terminal = K(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 = A.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: i } = this._sshConfig;
let s;
if (this._sshConfig.computingId && this._sshConfig.token) {
if (s = p.validateComputingConfig(this._sshConfig), !s.isValid) {
const c = `Invalid computing configuration: ${s.errors.join(", ")}`;
e.error("Computing config validation failed in initTerminal", { errors: s.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${c}</p>`, this._dispatchEvent("error", { message: c, errors: s.errors });
return;
}
} else if (s = p.validateSSHConfig(this._sshConfig), !s.isValid) {
const c = `Invalid SSH configuration: ${s.errors.join(", ")}`;
e.error("SSH config validation failed in initTerminal", { errors: s.errors }), this._container.innerHTML = `<p style="color:red;padding:8px;font-family:monospace;">${c}</p>`, this._dispatchEvent("error", { message: c, errors: s.errors });
return;
}
try {
this._terminalInstance = K(this._container, {
host: r,
username: t,
password: n,
wsUrl: i
}), setTimeout(() => {
this._dispatchEvent("ready", {
host: r,
// Only host is relatively safe to expose
wsUrl: i
});
}, 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", te);
typeof window < "u" && window.customElements && !window.customElements.get("ssh-terminal") && console.log("SSH Terminal Web Component registered for Vue 3 build");
export {
le as SSHTerminal,
te as SSHTerminalElement,
le as Vue3SSHTerminal,
K as createTerminal
};