From ff7b65dcd19194b2e8bded823b92aaa51e55c6f3 Mon Sep 17 00:00:00 2001 From: GreenhandTan <2597417538@qq.com> Date: Mon, 15 Jun 2026 08:22:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A0=B8=E5=BF=83=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=B8=8E=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🛡️ 安全(Security): - 彻底修复 IPv6 及保留地址的 SSRF 绕过漏洞。 - 新增基于 IP 的连接频率限制 (10次/分钟) 防范暴力破解。 - 在 localStorage 中通过 Web Crypto (AES-GCM) 对记住的凭证进行加密存储。 - 连接时静默计算并输出服务器 Host Key 指纹 (SHA-256),兼顾安全与易用性。 - ✨ 功能(Feature): - 支持基于 Ed25519 的 SSH 纯文本私钥直接登录。 - 前端终端集成 zmodem.js,支持在终端中运行 rz/sz 实现浏览器与服务器文件的直接拖拽与无缝传输。 - 支持 WebSocket 会话漫游 (Roaming),网络短暂断开可借助 Durable Object 在 15 秒内静默重连恢复会话,免去重新输入密码的繁琐。 - 🎨 体验与界面(UX/UI): - 前端渲染引擎升级为 WebGL (@xterm/addon-webgl),大幅提升海量文本滚动的帧率与流畅度。 - 新增内置极客主题下拉框 (Cyberpunk / Glacier / Gruvbox) 及右键复制粘贴支持。 --- frontend/index.html | 7 +- frontend/package-lock.json | 31 +++++- frontend/package.json | 6 +- frontend/src/auth-form.ts | 146 +++++++++++++++++++++++-- frontend/src/main.ts | 5 + frontend/src/terminal.ts | 191 +++++++++++++++++++++++++-------- frontend/src/zmodem-handler.ts | 102 ++++++++++++++++++ src/ssh/auth.ts | 131 ++++++++++++++++++++++ src/types.ts | 2 + src/worker/durable-object.ts | 100 ++++++++++++----- src/worker/html.ts | 121 ++++++++++++++++++--- src/worker/index.ts | 21 ++++ src/worker/ssh-session.ts | 49 ++++++++- 13 files changed, 809 insertions(+), 103 deletions(-) create mode 100644 frontend/src/zmodem-handler.ts diff --git a/frontend/index.html b/frontend/index.html index 284fa5e..cedf839 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -73,7 +73,12 @@

CloudSSH

-
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1ca224c..df6dfcc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", - "@xterm/xterm": "^5.5.0" + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^5.5.0", + "zmodem.js": "^0.1.10" }, "devDependencies": { "typescript": "^5.7.0", @@ -848,12 +850,30 @@ "@xterm/xterm": "^5.0.0" } }, + "node_modules/@xterm/addon-webgl": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", + "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==", + "license": "MIT" + }, "node_modules/@xterm/xterm": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "license": "MIT" }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/esbuild": { "version": "0.25.12", "dev": true, @@ -1155,6 +1175,15 @@ "@rollup/rollup-win32-x64-msvc": "4.62.0", "fsevents": "~2.3.2" } + }, + "node_modules/zmodem.js": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/zmodem.js/-/zmodem.js-0.1.10.tgz", + "integrity": "sha512-Z1DWngunZ/j3BmIzSJpFZVNV73iHkj89rxXX4IciJdU9ga3nZ7rJ5LkfjV/QDsKhc7bazDWTTJCLJ+iRXD82dw==", + "license": "Apache-2.0", + "dependencies": { + "crc-32": "^1.1.1" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 42ef281..5f1c092 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,9 +8,11 @@ "preview": "vite preview" }, "dependencies": { - "@xterm/xterm": "^5.5.0", "@xterm/addon-fit": "^0.10.0", - "@xterm/addon-web-links": "^0.11.0" + "@xterm/addon-web-links": "^0.11.0", + "@xterm/addon-webgl": "^0.19.0", + "@xterm/xterm": "^5.5.0", + "zmodem.js": "^0.1.10" }, "devDependencies": { "typescript": "^5.7.0", diff --git a/frontend/src/auth-form.ts b/frontend/src/auth-form.ts index 15c3384..57cc6cd 100644 --- a/frontend/src/auth-form.ts +++ b/frontend/src/auth-form.ts @@ -1,11 +1,53 @@ import { SSHTerminal } from './terminal'; +// --- Credential encryption helpers --- +async function deriveKey(salt: Uint8Array): Promise { + const raw = new TextEncoder().encode(window.location.origin + ':cloudssh'); + const baseKey = await crypto.subtle.importKey('raw', raw, 'PBKDF2', false, ['deriveKey']); + return crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: salt as any, iterations: 100000, hash: 'SHA-256' }, + baseKey, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +async function encryptCredentials(data: object): Promise { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(salt); + const encoded = new TextEncoder().encode(JSON.stringify(data)); + const encrypted = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded)); + // Format: base64(salt + iv + ciphertext) + const combined = new Uint8Array(salt.length + iv.length + encrypted.length); + combined.set(salt, 0); + combined.set(iv, salt.length); + combined.set(encrypted, salt.length + iv.length); + return btoa(String.fromCharCode(...combined)); +} + +async function decryptCredentials(stored: string): Promise<{ host: string; port: string; username: string; password: string } | null> { + try { + const raw = Uint8Array.from(atob(stored), c => c.charCodeAt(0)); + const salt = raw.slice(0, 16); + const iv = raw.slice(16, 28); + const data = raw.slice(28); + const key = await deriveKey(salt); + const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data); + return JSON.parse(new TextDecoder().decode(decrypted)); + } catch { + return null; + } +} + export class ConnectionForm { private terminal: SSHTerminal; constructor(terminal: SSHTerminal) { this.terminal = terminal; this.render(); + this.loadSavedCredentials(); } private render(): void { @@ -37,13 +79,26 @@ export class ConnectionForm {
- -
- key - + +
+ + +
+
+
+ key + +
+
+
-
+
+ + +
+