feat: SSH 协议增强、文件传输优化及多标签页支持
SSH 协议层: - 新增 Curve25519 密钥交换算法支持 - 新增 AES-CTR 加密模式 (aes256-ctr, aes192-ctr, aes128-ctr) - 新增 HMAC 完整性校验 (hmac-sha2-256, hmac-sha2-512, hmac-sha1) - 优化算法协商和密钥派生流程 前端: - 文件传输从 Zmodem 迁移到 trzsz,支持拖拽上传和 tmux 兼容 - 新增多标签页 SSH 支持(登录用户可在新标签页打开独立终端) - 修复 WebSocket 二进制数据处理问题 后端: - 恢复 Hibernation API 支持长连接 - 兼容 Dropbear 等不发送 shell 确认的 SSH 服务器 - 关闭调试日志 构建: - 迁移到 pnpm 包管理器 - 添加 GitHub Actions 自动部署
This commit is contained in:
1 parent
0c16e3ca93
commit
06a79a33c2
27 files changed
+3584
-5648
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# Cloudflare Workers 构建忽略文件
|
||||
# 以下文件/目录的变动不会触发重新构建
|
||||
|
||||
# 文档文件
|
||||
README.md
|
||||
README_en.md
|
||||
LICENSE
|
||||
|
||||
# 图片文件
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.svg
|
||||
*.ico
|
||||
|
||||
# Git 相关
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# 临时文件
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
.DS_Store
|
||||
|
||||
# 本地开发配置
|
||||
.dev.vars
|
||||
.wrangler/
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Deploy to Cloudflare Workers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [test]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 'latest'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install
|
||||
cd frontend && pnpm install
|
||||
|
||||
- name: Build frontend and deploy
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: pnpm run deploy
|
||||
@@ -57,41 +57,86 @@
|
||||
|
||||
### 安全可靠
|
||||
|
||||
- **端到端加密**:完整的 SSH-2.0 协议实现,包括 ECDH 密钥交换、Ed25519 签名认证以及 AES-256-GCM 数据加密。
|
||||
- **安全加固体系**:内置针对 IPv6 与保留地址的 SSRF 防护、API 请求频率限制(防爆破),并在本地使用 AES-GCM 算法加密存储您的服务器凭证。
|
||||
- **端到端加密**:完整的 SSH-2.0 协议实现,支持 Curve25519-SHA256(优先)和 ECDH-NISTP256 密钥交换,AES-256-GCM(优先)/ AES-128-GCM / AES-256-CTR 数据加密,以及 HMAC-SHA2-256/512 完整性校验。
|
||||
- **多算法主机密钥验证**:支持 Ed25519、ECDSA P-256、RSA 签名验证,首次连接展示 SHA-256 指纹(TOFU 模式)。
|
||||
- **安全加固体系**:内置针对 IPv6 与保留地址的 SSRF 防护、API 请求频率限制(防爆破),并在本地使用 AES-256-GCM 算法加密存储您的服务器凭证。
|
||||
- **人机验证**:支持 Cloudflare Turnstile 验证,防止恶意机器人滥用。
|
||||
- **隔离的会话状态**:借助 Cloudflare Durable Objects 和 Hibernation API,每个终端会话都在沙盒内安全、持久地运行。
|
||||
- **凭据零暴露**:One-Time-Token 机制确保密码/私钥不经过前端,完全在 Worker 内部安全流转。
|
||||
|
||||
<a id="features"></a>
|
||||
## 核心特性
|
||||
|
||||
- **完整的 SSH 握手**:原生 TypeScript 实现 SSH 传输层协议与用户认证协议。
|
||||
- **纯 TypeScript SSH-2.0 实现**:完全自研的 SSH 协议栈,不依赖任何第三方 SSH 库,基于 Web Crypto API 实现全部加密操作。
|
||||
- **多算法密钥交换**:支持 Curve25519-SHA256(优先)和 ECDH-NISTP256 两种 KEX 算法,适配各类 SSH 服务器(包括 Dropbear)。
|
||||
- **IPv4/IPv6 双栈**:完整支持 IPv4 和 IPv6 地址连接,包括 IPv6 方括号格式自动处理。
|
||||
- **多种认证方式**:支持标准 SSH 密码认证以及基于 Ed25519 的纯文本私钥认证。
|
||||
- **防范中间人攻击 (TOFU)**:首次连接自动提取服务器 Host Key(SHA-256 指纹)并显示,防止被恶意节点窃听。
|
||||
- **防范中间人攻击 (TOFU)**:首次连接自动提取服务器 Host Key(SHA-256 指纹)并显示,支持 Ed25519/ECDSA/RSA 签名验证。
|
||||
- **全功能极客终端**:基于 `@xterm/xterm` 与 `@xterm/addon-webgl` 硬件加速渲染引擎,保证海量日志输出顺滑不卡顿。
|
||||
- **个性化 UI**:提供 Cyberpunk、Glacier、Gruvbox 等经典终端主题一键切换,支持移动端适配。
|
||||
- **原生文件传输**:集成 zmodem.js,只需在终端中执行 `rz` / `sz` 命令,即可在浏览器直接与服务器双向拖放/下载文件。
|
||||
- **原生文件传输**:集成 [trzsz.js](https://github.com/trzsz/trzsz.js),支持 `trz`(上传)/ `tsz`(下载)命令进行文件传输,兼容 tmux 会话。还支持拖拽文件到终端窗口直接上传、目录传输及断点续传等高级功能。(需远程服务器安装 [trzsz](https://trzsz.github.io/))
|
||||
- **GitHub OAuth 集成**:支持 GitHub 登录,用户可保存和管理常用 SSH 服务器,实现一键连接。
|
||||
|
||||
<a id="architecture"></a>
|
||||
## 架构说明
|
||||
|
||||
### 系统架构
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Browser["浏览器客户端<br/>(TypeScript + xterm.js)"]
|
||||
CF["Cloudflare Edge Network"]
|
||||
DO["Durable Object<br/>(SSH 会话管理)"]
|
||||
Server["目标 SSH 服务器<br/>(如 Linux VPS)"]
|
||||
subgraph "浏览器客户端"
|
||||
UI["前端 UI<br/>TypeScript + xterm.js"]
|
||||
Trzsz["trzsz 文件传输"]
|
||||
end
|
||||
|
||||
subgraph "Cloudflare Edge Network"
|
||||
Worker["Worker<br/>路由 + API"]
|
||||
SSH_DO["SSHSessionDO<br/>SSH 会话管理"]
|
||||
User_DO["UserDBDO<br/>用户数据管理"]
|
||||
end
|
||||
|
||||
subgraph "目标服务器"
|
||||
SSH["SSH 服务器<br/>(OpenSSH/Dropbear)"]
|
||||
end
|
||||
|
||||
Browser <-->|"WebSocket<br/>(前端 UI 输入与终端输出)"| CF
|
||||
CF <-->|"WebSocket"<br/>路由与长连接| DO
|
||||
DO <-->|"TCP Socket<br/>@cloudflare/sockets"| Server
|
||||
UI <-->|"WebSocket<br/>终端 I/O"| Worker
|
||||
Trzsz <-->|"trzsz 协议"| UI
|
||||
Worker <-->|"WebSocket"| SSH_DO
|
||||
Worker <-->|"Internal API"| User_DO
|
||||
SSH_DO <-->|"TCP Socket<br/>@cloudflare/sockets"| SSH
|
||||
```
|
||||
|
||||
1. 用户在前端输入主机 IP、账号和密码。
|
||||
### 核心组件
|
||||
|
||||
| 组件 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| **Worker 入口** | `src/worker/index.ts` | HTTP 路由、API 处理、WebSocket 升级 |
|
||||
| **SSHSessionDO** | `src/worker/durable-object.ts` | SSH 会话生命周期管理、SSRF 防护 |
|
||||
| **UserDBDO** | `src/worker/user-db.ts` | 用户数据、服务器配置、速率限制(SQLite) |
|
||||
| **SSHSession** | `src/worker/ssh-session.ts` | SSH 协议状态机(连接→版本→密钥交换→认证→交互) |
|
||||
| **SSH 协议栈** | `src/ssh/*.ts` | 纯 TypeScript SSH-2.0 实现(传输层、加密、认证、通道) |
|
||||
| **前端终端** | `frontend/src/terminal.ts` | xterm.js 封装、trzsz 集成、WebSocket 管理 |
|
||||
|
||||
### SSH 协议实现
|
||||
|
||||
本项目实现了完整的 SSH-2.0 协议栈:
|
||||
|
||||
| 层级 | 实现 | 支持算法 |
|
||||
|------|------|----------|
|
||||
| **密钥交换** | `kex-curve25519.ts` / `kex-ecdh.ts` | curve25519-sha256, ecdh-sha2-nistp256 |
|
||||
| **数据加密** | `crypto.ts` | aes256-gcm, aes128-gcm, aes256-ctr, aes192-ctr, aes128-ctr |
|
||||
| **完整性校验** | `crypto.ts` | hmac-sha2-256, hmac-sha2-512, hmac-sha1 |
|
||||
| **主机密钥** | `ssh-session.ts` | Ed25519, ECDSA P-256, RSA |
|
||||
| **用户认证** | `auth.ts` | 密码认证, Ed25519 公钥认证 |
|
||||
| **通道管理** | `channel.ts` | session channel, PTY, shell, window-change |
|
||||
|
||||
### 数据流
|
||||
|
||||
1. 用户在前端输入主机 IP、账号和密码(或通过 GitHub OAuth 选择已保存的服务器)。
|
||||
2. 前端与后端的 Durable Object 建立 WebSocket 连接。
|
||||
3. DO 接收凭据,使用 `@cloudflare/sockets` 与目标 SSH 服务器建立 TCP 连接。
|
||||
4. DO 纯代码实现 SSH 协议协商(密钥交换、密码认证等),并将加密后的终端数据通过 WebSocket 转发给前端。
|
||||
3. SSHSessionDO 接收凭据,使用 `@cloudflare/sockets` 与目标 SSH 服务器建立 TCP 连接。
|
||||
4. SSHSession 执行完整的 SSH 协议协商(版本交换→密钥交换→认证→打开通道→PTY→Shell)。
|
||||
5. 加密的终端数据通过 WebSocket 在前端和 SSH 服务器之间双向转发。
|
||||
|
||||
<a id="quick-start"></a>
|
||||
## 快速部署
|
||||
@@ -107,9 +152,10 @@ flowchart TB
|
||||
#### 方式一:通过 GitHub 绑定自动部署(推荐)
|
||||
|
||||
1. **Fork 本仓库** 到你的 GitHub 账号。
|
||||
2. **修改域名**:在进行部署前,请先将 `wrangler.toml` 中的自定义域名改成你自己的域名(要求:域名需要先在 Cloudflare 中完成注册或接入)。
|
||||
3. **一键部署**:登录 Cloudflare,进入 Workers & Pages 绑定你的 GitHub 账号,选择刚才 Fork 的仓库进行应用创建。
|
||||
4. **填写构建命令**:在部署设置中,请务必将“构建命令”(Build command)填写为 `npm install && npm run build:frontend`,然后点击保存并部署(无需填写构建输出目录)。
|
||||
2. **一键部署**:登录 Cloudflare,进入 Workers & Pages 绑定你的 GitHub 账号,选择刚才 Fork 的仓库进行应用创建。
|
||||
3. **填写构建命令**:在部署设置中,请务必将"构建命令"(Build command)填写为 `pnpm run build:frontend`,然后点击保存并部署(无需填写构建输出目录)。
|
||||
4. **访问应用**:部署成功后,可通过默认域名 `https://cloudssh.<你的子域>.workers.dev` 访问。
|
||||
5. **绑定自定义域名**(可选):在 Cloudflare Dashboard 的 Workers 设置中,进入 "Triggers" → "Custom Domains" 添加你的域名。
|
||||
|
||||
#### 方式二:本地命令行部署
|
||||
|
||||
@@ -121,7 +167,8 @@ flowchart TB
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **登录 Cloudflare**
|
||||
@@ -131,22 +178,22 @@ flowchart TB
|
||||
|
||||
4. **一键部署**
|
||||
```bash
|
||||
npm run deploy
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
部署完成后,Wrangler 会输出你的 Worker URL。打开浏览器访问该 URL,即可开始使用你的 Web SSH 终端。
|
||||
|
||||
5. **绑定自定义域名**(可选):在 Cloudflare Dashboard 的 Workers 设置中,进入 "Triggers" → "Custom Domains" 添加你的域名。
|
||||
|
||||
#### 可选:配置 Turnstile 人机验证
|
||||
|
||||
为防止恶意机器人滥用,建议启用 Cloudflare Turnstile 验证:
|
||||
|
||||
1. **创建 Turnstile Widget**:登录 [Cloudflare Dashboard](https://dash.cloudflare.com/),进入 Turnstile 页面创建一个新的 Widget。
|
||||
2. **获取密钥**:创建后会获得一个 **Site Key**(公开)和一个 **Secret Key**(保密)。
|
||||
3. **配置环境变量**:
|
||||
- **方式一部署**:在 Cloudflare Dashboard 的 Workers 设置中,添加以下环境变量:
|
||||
- `TURNSTILE_SECRET` = 你的 Secret Key
|
||||
- `TURNSTILE_SITEKEY` = 你的 Site Key
|
||||
- **方式二部署**:取消 `wrangler.toml` 中 `TURNSTILE_SECRET` 和 `TURNSTILE_SITEKEY` 的注释,填入对应密钥。
|
||||
3. **配置环境变量**:在 Cloudflare Dashboard 的 Workers 设置中,进入 "Settings" → "Variables and Secrets",添加以下环境变量:
|
||||
- `TURNSTILE_SECRET` = 你的 Secret Key(类型选择 Secret)
|
||||
- `TURNSTILE_SITEKEY` = 你的 Site Key(类型选择 Text)
|
||||
4. **重新部署**:运行部署命令使配置生效。
|
||||
|
||||
> **说明**:Turnstile 验证为会话级别,用户通过验证后当前会话内所有功能可用,关闭浏览器后需重新验证。
|
||||
@@ -162,38 +209,66 @@ flowchart TB
|
||||
- **Authorization callback URL**:`https://your-domain.com/api/auth/callback`
|
||||
- 创建后获得 **Client ID**,点击 **Generate a new client secret** 生成 **Client Secret**(仅显示一次,请立即保存)
|
||||
|
||||
2. **配置环境变量**:
|
||||
- **方式一部署**:在 Cloudflare Dashboard 的 Workers 设置中,添加以下环境变量:
|
||||
- `GITHUB_CLIENT_ID` = 你的 Client ID
|
||||
- `BASE_URL` = `https://your-domain.com`(你的部署域名)
|
||||
- **方式二部署**:取消 `wrangler.toml` 中 `GITHUB_CLIENT_ID` 和 `BASE_URL` 的注释,填入对应值。
|
||||
2. **配置环境变量**:在 Cloudflare Dashboard 的 Workers 设置中,进入 "Settings" → "Variables and Secrets",添加以下环境变量:
|
||||
- `GITHUB_CLIENT_ID` = 你的 Client ID(类型选择 Text)
|
||||
- `BASE_URL` = `https://your-domain.com`(你的部署域名,类型选择 Text)
|
||||
- `GITHUB_CLIENT_SECRET` = 你的 Client Secret(类型选择 Secret)
|
||||
|
||||
3. **设置 Secrets(敏感密钥)**:
|
||||
- 部署项目后,进入 Cloudflare Dashboard → 你的 Worker 项目 (`cloudssh`) → **Settings (设置)** → **Variables and Secrets (变量和机密)**。
|
||||
- 点击 **Add (添加)** 环境变量:
|
||||
- **Type (类型)**:选择 **Secret (机密)**(非常重要,不要选 Text)
|
||||
- **变量名**:`GITHUB_CLIENT_SECRET`
|
||||
- **值**:粘贴你获取到的 Client Secret
|
||||
- 随后点击 **Save and deploy (保存并部署)**。
|
||||
|
||||
4. **重新部署**:如果你是刚刚修改了环境变量,且是首次启用该功能,请务必删除旧版并全新部署以初始化数据库。
|
||||
3. **重新部署**:如果你是刚刚修改了环境变量,且是首次启用该功能,请务必删除旧版并全新部署以初始化数据库。
|
||||
|
||||
> **说明**:服务器凭据(密码/私钥)在数据库中使用 AES-256-GCM 加密存储,本地加密密钥将自动生成并安全地存储在数据库中(也可在环境变量中手动设置 `SESSION_SECRET` 来指定)。连接时凭据不经过前端,通过 one-time-token 机制安全传递。
|
||||
|
||||
> **注意**:首次启用此功能需要从零部署(删除旧 Worker 后重新部署),因为需要初始化新的 Durable Object。可通过 `npx wrangler delete cloudssh` 删除旧 Worker,然后运行 `npm run deploy` 重新部署。
|
||||
> **注意**:首次启用此功能需要从零部署(删除旧 Worker 后重新部署),因为需要初始化新的 Durable Object。可通过 `npx wrangler delete cloudssh` 删除旧 Worker,然后运行 `pnpm run deploy` 重新部署。
|
||||
|
||||
<a id="development"></a>
|
||||
## 开发说明
|
||||
|
||||
本项目分为两部分:
|
||||
1. **Frontend (前端)**:在 `frontend/` 目录下,使用 Vite 构建。
|
||||
2. **Worker (后端)**:在 `src/` 目录下,包含 Cloudflare Worker 入口与 SSH 协议的核心实现。
|
||||
### 项目结构
|
||||
|
||||
本项目采用 pnpm monorepo 工作区结构:
|
||||
|
||||
在本地开发时,可以运行:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
此命令将启动 Wrangler 的本地开发环境服务器。
|
||||
CloudSSH/
|
||||
├── src/ # 后端源码 (Cloudflare Worker)
|
||||
│ ├── ssh/ # SSH 协议纯实现层
|
||||
│ └── worker/ # Worker 入口和 Durable Objects
|
||||
├── frontend/ # 前端源码 (独立 workspace)
|
||||
│ ├── src/ # TypeScript + xterm.js + trzsz
|
||||
│ └── package.json # 前端依赖
|
||||
├── scripts/ # 构建脚本
|
||||
├── pnpm-workspace.yaml # pnpm 工作区配置
|
||||
└── wrangler.toml # Cloudflare 部署配置
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
|
||||
1. **安装依赖**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **启动开发服务器**
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
此命令将构建前端并启动 Wrangler 本地开发环境。
|
||||
|
||||
3. **仅构建前端**
|
||||
```bash
|
||||
pnpm run build:frontend
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 | 说明 |
|
||||
|------|------|------|
|
||||
| **前端** | TypeScript + Vite + xterm.js | Web 终端模拟器,WebGL 硬件加速 |
|
||||
| **UI 框架** | Tailwind CSS (CDN) | 赛博朋克风格暗色主题 |
|
||||
| **文件传输** | trzsz.js | 支持 trz/tsz 命令、拖拽上传、断点续传 |
|
||||
| **后端** | Cloudflare Workers | Serverless 边缘计算 |
|
||||
| **会话管理** | Durable Objects | SSH 会话隔离、Hibernation API |
|
||||
| **数据存储** | Durable Objects SQLite | 用户数据、服务器配置 |
|
||||
| **包管理** | pnpm (workspace) | Monorepo 依赖管理 |
|
||||
|
||||
<a id="license"></a>
|
||||
## 开源协议
|
||||
@@ -213,4 +288,3 @@ npm run dev
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=newbietan/CloudSSH&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
+122
-47
@@ -57,41 +57,86 @@
|
||||
|
||||
### Secure and Reliable
|
||||
|
||||
- **End-to-End Encryption**: Complete implementation of the SSH-2.0 protocol, including ECDH key exchange, Ed25519 signature authentication, and AES-256-GCM data encryption.
|
||||
- **Security Hardening**: Built-in SSRF protection against IPv6 and reserved IPs, API rate limiting (anti-brute force), and local AES-GCM encryption for your stored server credentials.
|
||||
- **End-to-End Encryption**: Complete SSH-2.0 protocol implementation supporting Curve25519-SHA256 (preferred) and ECDH-NISTP256 key exchange, AES-256-GCM (preferred) / AES-128-GCM / AES-256-CTR data encryption, and HMAC-SHA2-256/512 integrity verification.
|
||||
- **Multi-Algorithm Host Key Verification**: Supports Ed25519, ECDSA P-256, and RSA signature verification, with SHA-256 fingerprint display on first connection (TOFU mode).
|
||||
- **Security Hardening**: Built-in SSRF protection against IPv6 and reserved IPs, API rate limiting (anti-brute force), and local AES-256-GCM encryption for your stored server credentials.
|
||||
- **Human Verification**: Supports Cloudflare Turnstile verification to prevent malicious bot abuse.
|
||||
- **Isolated Session State**: Leveraging Cloudflare Durable Objects and the Hibernation API, every terminal session runs securely and persistently within its sandbox.
|
||||
- **Zero Credential Exposure**: One-Time-Token mechanism ensures passwords/private keys never pass through the frontend, securely flowing entirely within the Worker.
|
||||
|
||||
<a id="features"></a>
|
||||
## Features
|
||||
|
||||
- **Full SSH Handshake**: Native TypeScript implementation of the SSH transport layer and user authentication protocols.
|
||||
- **Pure TypeScript SSH-2.0 Implementation**: Fully self-developed SSH protocol stack, with no dependency on any third-party SSH libraries, implementing all cryptographic operations based on Web Crypto API.
|
||||
- **Multi-Algorithm Key Exchange**: Supports Curve25519-SHA256 (preferred) and ECDH-NISTP256 KEX algorithms, compatible with various SSH servers (including Dropbear).
|
||||
- **IPv4/IPv6 Dual Stack**: Full support for both IPv4 and IPv6 address connections, including automatic handling of IPv6 bracket notation.
|
||||
- **Multiple Auth Methods**: Supports standard SSH password authentication as well as Ed25519 plaintext private key authentication.
|
||||
- **MitM Protection (TOFU)**: Automatically extracts and prints the server's Host Key (SHA-256 fingerprint) on the first connection, preventing eavesdropping by malicious nodes.
|
||||
- **MitM Protection (TOFU)**: Automatically extracts and prints the server's Host Key (SHA-256 fingerprint) on the first connection, supporting Ed25519/ECDSA/RSA signature verification.
|
||||
- **Geek Terminal Experience**: Powered by `@xterm/xterm` and the `@xterm/addon-webgl` hardware acceleration rendering engine, ensuring silky smooth scrolling even with massive log outputs.
|
||||
- **Customizable UI**: Switch seamlessly between classic terminal themes like Cyberpunk, Glacier, and Gruvbox, fully optimized for mobile devices.
|
||||
- **Native File Transfer**: Integrated with `zmodem.js`. Just run `rz` or `sz` commands in the terminal to easily upload/download files via drag & drop directly in the browser.
|
||||
- **Native File Transfer**: Integrated with [trzsz.js](https://github.com/trzsz/trzsz.js), supporting `trz` (upload) / `tsz` (download) commands for file transfer, fully compatible with tmux sessions. Also supports drag-and-drop file upload to the terminal, directory transfer, and resumable transfers. (Requires [trzsz](https://trzsz.github.io/) installed on the remote server)
|
||||
- **GitHub OAuth Integration**: Supports GitHub login, allowing users to save and manage frequently used SSH servers for one-click connections.
|
||||
|
||||
<a id="architecture"></a>
|
||||
## Architecture
|
||||
|
||||
### System Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Browser["Browser Client<br/>(TypeScript + xterm.js)"]
|
||||
CF["Cloudflare Edge Network"]
|
||||
DO["Durable Object<br/>(SSH Session Management)"]
|
||||
Server["Target SSH Server<br/>(e.g., Linux VPS)"]
|
||||
subgraph "Browser Client"
|
||||
UI["Frontend UI<br/>TypeScript + xterm.js"]
|
||||
Trzsz["trzsz File Transfer"]
|
||||
end
|
||||
|
||||
subgraph "Cloudflare Edge Network"
|
||||
Worker["Worker<br/>Routing + API"]
|
||||
SSH_DO["SSHSessionDO<br/>SSH Session Management"]
|
||||
User_DO["UserDBDO<br/>User Data Management"]
|
||||
end
|
||||
|
||||
subgraph "Target Server"
|
||||
SSH["SSH Server<br/>(OpenSSH/Dropbear)"
|
||||
end
|
||||
|
||||
Browser <-->|"WebSocket<br/>(Frontend UI Input & Terminal Output)"| CF
|
||||
CF <-->|"WebSocket"<br/>Routing & Long-lived Connection| DO
|
||||
DO <-->|"TCP Socket<br/>@cloudflare/sockets"| Server
|
||||
UI <-->|"WebSocket<br/>Terminal I/O"| Worker
|
||||
Trzsz <-->|"trzsz Protocol"| UI
|
||||
Worker <-->|"WebSocket"| SSH_DO
|
||||
Worker <-->|"Internal API"| User_DO
|
||||
SSH_DO <-->|"TCP Socket<br/>@cloudflare/sockets"| SSH
|
||||
```
|
||||
|
||||
1. The user enters the host IP, username, and password on the frontend.
|
||||
### Core Components
|
||||
|
||||
| Component | File | Responsibility |
|
||||
|-----------|------|----------------|
|
||||
| **Worker Entry** | `src/worker/index.ts` | HTTP routing, API handling, WebSocket upgrade |
|
||||
| **SSHSessionDO** | `src/worker/durable-object.ts` | SSH session lifecycle management, SSRF protection |
|
||||
| **UserDBDO** | `src/worker/user-db.ts` | User data, server configs, rate limiting (SQLite) |
|
||||
| **SSHSession** | `src/worker/ssh-session.ts` | SSH protocol state machine (connect→version→kex→auth→interactive) |
|
||||
| **SSH Protocol Stack** | `src/ssh/*.ts` | Pure TypeScript SSH-2.0 implementation (transport, crypto, auth, channels) |
|
||||
| **Frontend Terminal** | `frontend/src/terminal.ts` | xterm.js wrapper, trzsz integration, WebSocket management |
|
||||
|
||||
### SSH Protocol Implementation
|
||||
|
||||
This project implements a complete SSH-2.0 protocol stack:
|
||||
|
||||
| Layer | Implementation | Supported Algorithms |
|
||||
|-------|----------------|---------------------|
|
||||
| **Key Exchange** | `kex-curve25519.ts` / `kex-ecdh.ts` | curve25519-sha256, ecdh-sha2-nistp256 |
|
||||
| **Data Encryption** | `crypto.ts` | aes256-gcm, aes128-gcm, aes256-ctr, aes192-ctr, aes128-ctr |
|
||||
| **Integrity** | `crypto.ts` | hmac-sha2-256, hmac-sha2-512, hmac-sha1 |
|
||||
| **Host Keys** | `ssh-session.ts` | Ed25519, ECDSA P-256, RSA |
|
||||
| **User Auth** | `auth.ts` | Password authentication, Ed25519 public key authentication |
|
||||
| **Channel Management** | `channel.ts` | Session channel, PTY, shell, window-change |
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. The user enters the host IP, username, and password on the frontend (or selects a saved server via GitHub OAuth).
|
||||
2. The frontend establishes a WebSocket connection with the backend Durable Object.
|
||||
3. The DO receives the credentials and establishes a TCP connection with the target SSH server using `@cloudflare/sockets`.
|
||||
4. The DO handles the entire SSH protocol negotiation (key exchange, password auth, etc.) in pure code and forwards the encrypted terminal data to the frontend via WebSocket.
|
||||
3. SSHSessionDO receives the credentials and establishes a TCP connection with the target SSH server using `@cloudflare/sockets`.
|
||||
4. SSHSession executes the complete SSH protocol negotiation (version exchange → key exchange → authentication → channel open → PTY → Shell).
|
||||
5. Encrypted terminal data is bidirectionally forwarded between the frontend and SSH server via WebSocket.
|
||||
|
||||
<a id="quick-start"></a>
|
||||
## Quick Deployment
|
||||
@@ -107,9 +152,10 @@ flowchart TB
|
||||
#### Method 1: Deploy via GitHub Integration (Recommended)
|
||||
|
||||
1. **Fork this repository** to your GitHub account.
|
||||
2. **Configure Domain**: Before deploying, you must modify the custom domain in `wrangler.toml` to your own domain (Note: The domain must be registered or active in Cloudflare beforehand).
|
||||
3. **Setup Deployment**: Log in to the Cloudflare dashboard, go to Workers & Pages to connect your GitHub account, and select the forked repository.
|
||||
4. **Build Command**: During the deployment configuration, make sure to enter `npm install && npm run build:frontend` as the Build command, then deploy with one click (the build output directory can be left blank).
|
||||
2. **Setup Deployment**: Log in to the Cloudflare dashboard, go to Workers & Pages to connect your GitHub account, and select the forked repository.
|
||||
3. **Build Command**: During the deployment configuration, make sure to enter `pnpm run build:frontend` as the Build command, then deploy with one click (the build output directory can be left blank).
|
||||
4. **Access the App**: After successful deployment, you can access it via the default domain `https://cloudssh.<your-subdomain>.workers.dev`.
|
||||
5. **Bind Custom Domain** (Optional): In the Cloudflare Dashboard Workers settings, go to "Triggers" → "Custom Domains" to add your domain.
|
||||
|
||||
#### Method 2: Local CLI Deployment
|
||||
|
||||
@@ -121,7 +167,8 @@ flowchart TB
|
||||
|
||||
2. **Install Dependencies**
|
||||
```bash
|
||||
npm install
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
3. **Login to Cloudflare**
|
||||
@@ -131,22 +178,22 @@ flowchart TB
|
||||
|
||||
4. **One-Click Deploy**
|
||||
```bash
|
||||
npm run deploy
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
Once deployed, Wrangler will output your Worker URL. Open that URL in your browser to start using your Web SSH terminal.
|
||||
|
||||
5. **Bind Custom Domain** (Optional): In the Cloudflare Dashboard Workers settings, go to "Triggers" → "Custom Domains" to add your domain.
|
||||
|
||||
#### Optional: Configure Turnstile Human Verification
|
||||
|
||||
To prevent malicious bot abuse, it is recommended to enable Cloudflare Turnstile verification:
|
||||
|
||||
1. **Create Turnstile Widget**: Log in to [Cloudflare Dashboard](https://dash.cloudflare.com/), go to the Turnstile page and create a new Widget.
|
||||
2. **Get Keys**: After creation, you will receive a **Site Key** (public) and a **Secret Key** (private).
|
||||
3. **Configure Environment Variables**:
|
||||
- **Method 1 (GitHub)**: In the Cloudflare Dashboard Workers settings, add the following environment variables:
|
||||
- `TURNSTILE_SECRET` = your Secret Key
|
||||
- `TURNSTILE_SITEKEY` = your Site Key
|
||||
- **Method 2 (CLI)**: Uncomment `TURNSTILE_SECRET` and `TURNSTILE_SITEKEY` in `wrangler.toml` and enter your keys.
|
||||
3. **Configure Environment Variables**: In the Cloudflare Dashboard Workers settings, go to "Settings" → "Variables and Secrets", add the following environment variables:
|
||||
- `TURNSTILE_SECRET` = your Secret Key (Type: Secret)
|
||||
- `TURNSTILE_SITEKEY` = your Site Key (Type: Text)
|
||||
4. **Redeploy**: Run the deployment command to apply the configuration.
|
||||
|
||||
> **Note**: Turnstile verification is session-level. After verification, all features are available for the current session. Closing the browser will require re-verification.
|
||||
@@ -162,38 +209,66 @@ With GitHub OAuth enabled, users can log in with their GitHub account and save/m
|
||||
- **Authorization callback URL**: `https://your-domain.com/api/auth/callback`
|
||||
- After creation, note the **Client ID**, then click **Generate a new client secret** to get the **Client Secret** (shown only once, save it immediately)
|
||||
|
||||
2. **Configure Environment Variables**:
|
||||
- **Method 1 (GitHub)**: In the Cloudflare Dashboard Workers settings, add:
|
||||
- `GITHUB_CLIENT_ID` = your Client ID
|
||||
- `BASE_URL` = `https://your-domain.com` (your deployed domain)
|
||||
- **Method 2 (CLI)**: Uncomment `GITHUB_CLIENT_ID` and `BASE_URL` in `wrangler.toml` and enter your values.
|
||||
2. **Configure Environment Variables**: In the Cloudflare Dashboard Workers settings, go to "Settings" → "Variables and Secrets", add the following environment variables:
|
||||
- `GITHUB_CLIENT_ID` = your Client ID (Type: Text)
|
||||
- `BASE_URL` = `https://your-domain.com` (your deployed domain, Type: Text)
|
||||
- `GITHUB_CLIENT_SECRET` = your Client Secret (Type: Secret)
|
||||
|
||||
3. **Set Secrets (Sensitive Information)**:
|
||||
- After deploying the project, go to Cloudflare Dashboard → your Worker project (`cloudssh`) → **Settings** → **Variables and Secrets**.
|
||||
- Click **Add** under Environment Variables:
|
||||
- **Type**: Select **Secret** (Important: Do not select Text)
|
||||
- **Variable name**: `GITHUB_CLIENT_SECRET`
|
||||
- **Value**: Paste your Client Secret
|
||||
- Click **Save and deploy**.
|
||||
|
||||
4. **Redeploy**: If you just added the variables and are enabling the feature for the first time, you must delete the old deployment and redeploy to initialize the database.
|
||||
3. **Redeploy**: If you just added the variables and are enabling the feature for the first time, you must delete the old deployment and redeploy to initialize the database.
|
||||
|
||||
> **Note**: Server credentials (passwords/private keys) are encrypted with AES-256-GCM before storage. The local encryption key is automatically generated and safely stored in the database (you can also manually override it by setting `SESSION_SECRET` in environment variables). During connection, credentials never pass through the frontend — they are securely transmitted via a one-time-token mechanism.
|
||||
|
||||
> **Important**: Enabling this feature for the first time requires a clean deployment (delete the old Worker first, then redeploy) to initialize the new Durable Object. Use `npx wrangler delete cloudssh` to remove the old Worker, then run `npm run deploy` to redeploy.
|
||||
> **Important**: Enabling this feature for the first time requires a clean deployment (delete the old Worker first, then redeploy) to initialize the new Durable Object. Use `npx wrangler delete cloudssh` to remove the old Worker, then run `pnpm run deploy` to redeploy.
|
||||
|
||||
<a id="development"></a>
|
||||
## Development
|
||||
|
||||
This project consists of two parts:
|
||||
1. **Frontend**: Located in the `frontend/` directory, built with Vite.
|
||||
2. **Worker**: Located in the `src/` directory, containing the Cloudflare Worker entry point and the core SSH protocol implementation.
|
||||
### Project Structure
|
||||
|
||||
This project uses pnpm monorepo workspace structure:
|
||||
|
||||
For local development, you can run:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
This command starts Wrangler's local development environment server.
|
||||
CloudSSH/
|
||||
├── src/ # Backend source (Cloudflare Worker)
|
||||
│ ├── ssh/ # SSH protocol pure implementation layer
|
||||
│ └── worker/ # Worker entry and Durable Objects
|
||||
├── frontend/ # Frontend source (independent workspace)
|
||||
│ ├── src/ # TypeScript + xterm.js + trzsz
|
||||
│ └── package.json # Frontend dependencies
|
||||
├── scripts/ # Build scripts
|
||||
├── pnpm-workspace.yaml # pnpm workspace configuration
|
||||
└── wrangler.toml # Cloudflare deployment configuration
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Install Dependencies**
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Start Development Server**
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
This command builds the frontend and starts the Wrangler local development environment.
|
||||
|
||||
3. **Build Frontend Only**
|
||||
```bash
|
||||
pnpm run build:frontend
|
||||
```
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Layer | Technology | Description |
|
||||
|-------|------------|-------------|
|
||||
| **Frontend** | TypeScript + Vite + xterm.js | Web terminal emulator, WebGL hardware acceleration |
|
||||
| **UI Framework** | Tailwind CSS (CDN) | Cyberpunk-style dark theme |
|
||||
| **File Transfer** | trzsz.js | Supports trz/tsz commands, drag-and-drop upload, resumable transfers |
|
||||
| **Backend** | Cloudflare Workers | Serverless edge computing |
|
||||
| **Session Management** | Durable Objects | SSH session isolation, Hibernation API |
|
||||
| **Data Storage** | Durable Objects SQLite | User data, server configurations |
|
||||
| **Package Manager** | pnpm (workspace) | Monorepo dependency management |
|
||||
|
||||
<a id="license"></a>
|
||||
## License
|
||||
|
||||
Generated
-1929
File diff suppressed because it is too large.
Load diff
@@ -5,18 +5,16 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"postinstall": "patch-package"
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.19.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"zmodem.js": "^0.1.10"
|
||||
"trzsz": "^1.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"patch-package": "^8.0.1",
|
||||
"typescript": "^5.7.0",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/zmodem.js/src/zmodem_browser.js b/node_modules/zmodem.js/src/zmodem_browser.js
|
||||
index 7405f54..32a6c29 100644
|
||||
--- a/node_modules/zmodem.js/src/zmodem_browser.js
|
||||
+++ b/node_modules/zmodem.js/src/zmodem_browser.js
|
||||
@@ -123,7 +123,7 @@ Zmodem.Browser = {
|
||||
};
|
||||
|
||||
reader.onload = function reader_onload(e) {
|
||||
- piece = new Uint8Array(e.target.result, xfer, piece)
|
||||
+ piece = new Uint8Array(e.target.result, xfer.get_offset())
|
||||
|
||||
_check_aborted(session);
|
||||
|
||||
+44
-4
@@ -9,6 +9,43 @@ let connectionForm: ConnectionForm | null = null;
|
||||
let serverList: ServerList | null = null;
|
||||
let isLoggedIn = false;
|
||||
|
||||
// ==================== 独立终端标签页模式 ====================
|
||||
|
||||
function isTerminalTab(): boolean {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.has('wsUrl');
|
||||
}
|
||||
|
||||
function initTerminalTab(): void {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const wsUrl = params.get('wsUrl')!;
|
||||
const serverName = params.get('name') || 'Server';
|
||||
|
||||
// 隐藏所有非终端元素
|
||||
document.getElementById('auth-section')!.classList.add('hidden');
|
||||
document.getElementById('user-space-section')!.classList.add('hidden');
|
||||
document.getElementById('user-space-section')!.classList.remove('flex');
|
||||
document.getElementById('terminal-section')!.classList.remove('hidden');
|
||||
document.getElementById('terminal-section')!.classList.add('flex');
|
||||
|
||||
// 更新终端状态栏
|
||||
document.getElementById('term-host')!.textContent = `Server: ${serverName}`;
|
||||
document.getElementById('term-user')!.textContent = '';
|
||||
document.getElementById('term-port')!.textContent = '';
|
||||
|
||||
terminal.mount();
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
terminal.connectWithWebSocket(ws);
|
||||
|
||||
// 断开连接时关闭标签页
|
||||
document.getElementById('disconnect-btn')?.addEventListener('click', () => {
|
||||
terminal.disconnect();
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 页面切换 ====================
|
||||
|
||||
function showAuthSection(): void {
|
||||
@@ -35,10 +72,6 @@ function showUserSpace(user: { id: number; github_id: number; username: string;
|
||||
|
||||
serverList = new ServerList(
|
||||
user,
|
||||
// onConnect 回调
|
||||
(wsUrl: string, serverName: string) => {
|
||||
showTerminalFromServer(wsUrl, serverName);
|
||||
},
|
||||
// onLogout 回调
|
||||
() => {
|
||||
isLoggedIn = false;
|
||||
@@ -64,6 +97,7 @@ function showTerminalFromServer(wsUrl: string, serverName: string): void {
|
||||
|
||||
// 通过 wsUrl(含 one-time-token)建立连接
|
||||
const ws = new WebSocket(wsUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
terminal.connectWithWebSocket(ws);
|
||||
}
|
||||
|
||||
@@ -103,6 +137,12 @@ async function init(): Promise<void> {
|
||||
copyrightYearSpan.textContent = new Date().getFullYear().toString();
|
||||
}
|
||||
|
||||
// 独立终端标签页模式:URL 包含 wsUrl 参数
|
||||
if (isTerminalTab()) {
|
||||
initTerminalTab();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否已登录
|
||||
const meRes = await fetch('/api/auth/me');
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { SSHTerminal } from './terminal';
|
||||
|
||||
interface UserInfo {
|
||||
id: number;
|
||||
github_id: number;
|
||||
@@ -25,18 +23,15 @@ interface ServerConfig {
|
||||
export class ServerList {
|
||||
private user: UserInfo;
|
||||
private servers: ServerConfig[] = [];
|
||||
private onConnect: (wsUrl: string, serverName: string) => void;
|
||||
private onLogout: () => void;
|
||||
private editingServerId: number | null = null;
|
||||
private modalAuthMode: 'password' | 'key' = 'password';
|
||||
|
||||
constructor(
|
||||
user: UserInfo,
|
||||
onConnect: (wsUrl: string, serverName: string) => void,
|
||||
onLogout: () => void
|
||||
) {
|
||||
this.user = user;
|
||||
this.onConnect = onConnect;
|
||||
this.onLogout = onLogout;
|
||||
this.init();
|
||||
}
|
||||
@@ -209,9 +204,12 @@ export class ServerList {
|
||||
}
|
||||
|
||||
const { wsUrl } = await res.json() as { wsUrl: string };
|
||||
this.onConnect(wsUrl, server.name);
|
||||
|
||||
// 连接成功跳转终端后,立即恢复按钮的原始状态,以便用户断开连接返回后看到的是正常按钮
|
||||
// 在新标签页打开终端
|
||||
const terminalUrl = `/?wsUrl=${encodeURIComponent(wsUrl)}&name=${encodeURIComponent(server.name)}`;
|
||||
window.open(terminalUrl, '_blank');
|
||||
|
||||
// 恢复按钮状态
|
||||
if (connectBtn) {
|
||||
connectBtn.innerHTML = `
|
||||
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
|
||||
|
||||
+80
-26
@@ -2,7 +2,7 @@ import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import { WebglAddon } from '@xterm/addon-webgl';
|
||||
import { ZmodemHandler } from './zmodem-handler';
|
||||
import { TrzszFilter } from 'trzsz';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
export interface SSHConnectionConfig {
|
||||
@@ -46,6 +46,7 @@ export class SSHTerminal {
|
||||
private container: HTMLElement;
|
||||
private disposables: { dispose(): void }[] = [];
|
||||
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private trzszFilter: TrzszFilter | null = null;
|
||||
private reconnectAttempts: number = 0;
|
||||
private maxReconnectAttempts: number = 5;
|
||||
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -82,6 +83,21 @@ export class SSHTerminal {
|
||||
console.error('Failed to read clipboard', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag-and-drop file upload support (trzsz)
|
||||
this.container.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
this.container.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.trzszFilter && e.dataTransfer?.items) {
|
||||
this.trzszFilter.uploadFiles(e.dataTransfer.items)
|
||||
.then(() => console.log('[trzsz] Drag-drop upload success'))
|
||||
.catch((err: any) => console.error('[trzsz] Drag-drop upload error:', err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTheme(themeName: keyof typeof THEMES): void {
|
||||
@@ -112,6 +128,7 @@ export class SSHTerminal {
|
||||
}
|
||||
|
||||
async connect(config: SSHConnectionConfig): Promise<void> {
|
||||
this.resetActiveConnection();
|
||||
this.lastConfig = config;
|
||||
this.terminal.clear();
|
||||
|
||||
@@ -124,6 +141,7 @@ export class SSHTerminal {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new WebSocket(wsUrl.toString());
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.terminal.writeln('\x1b[32m[+] WebSocket connected, sending credentials...\x1b[0m');
|
||||
@@ -159,8 +177,10 @@ export class SSHTerminal {
|
||||
* 服务器已通过 token 获取凭据,无需前端发送
|
||||
*/
|
||||
connectWithWebSocket(ws: WebSocket): void {
|
||||
this.resetActiveConnection();
|
||||
this.lastConfig = null;
|
||||
this.ws = ws;
|
||||
ws.binaryType = 'arraybuffer';
|
||||
this.terminal.clear();
|
||||
|
||||
const termStatus = document.getElementById('term-status');
|
||||
@@ -177,15 +197,26 @@ export class SSHTerminal {
|
||||
private setupWebSocketHandlers(rejectFn?: (reason?: any) => void): void {
|
||||
if (!this.ws) return;
|
||||
|
||||
// Zmodem support
|
||||
const zmodemHandler = new ZmodemHandler(
|
||||
(data) => this.terminal.write(data),
|
||||
(data) => {
|
||||
// Trzsz file transfer support
|
||||
this.trzszFilter = new TrzszFilter({
|
||||
writeToTerminal: (data: string | ArrayBuffer | Uint8Array | Blob) => {
|
||||
if (typeof data === 'string') {
|
||||
this.terminal.write(data);
|
||||
} else if (data instanceof Uint8Array) {
|
||||
this.terminal.write(data);
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
this.terminal.write(new Uint8Array(data));
|
||||
} else if (data instanceof Blob) {
|
||||
data.arrayBuffer().then(buf => this.terminal.write(new Uint8Array(buf)));
|
||||
}
|
||||
},
|
||||
sendToServer: (data: string | Uint8Array) => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(data);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
terminalColumns: this.terminal.cols,
|
||||
});
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
if (typeof event.data === 'string') {
|
||||
@@ -203,16 +234,16 @@ export class SSHTerminal {
|
||||
case 'error':
|
||||
this.terminal.writeln(`\x1b[31m[!] ${msg.message}\x1b[0m`);
|
||||
break;
|
||||
case 'pong':
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
this.terminal.write(event.data);
|
||||
// Non-JSON string data — pass through trzsz filter
|
||||
this.trzszFilter!.processServerOutput(event.data);
|
||||
}
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
zmodemHandler.consume(reader.result as ArrayBuffer);
|
||||
};
|
||||
reader.readAsArrayBuffer(event.data);
|
||||
// Binary data — pass through trzsz filter
|
||||
this.trzszFilter!.processServerOutput(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -236,14 +267,21 @@ export class SSHTerminal {
|
||||
if (rejectFn) rejectFn(new Error('WebSocket connection failed'));
|
||||
};
|
||||
|
||||
// User input goes through trzsz filter
|
||||
this.disposables.push(
|
||||
this.terminal.onData((data) => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(data);
|
||||
}
|
||||
this.trzszFilter!.processTerminalInput(data);
|
||||
})
|
||||
);
|
||||
|
||||
// Binary input support
|
||||
this.disposables.push(
|
||||
this.terminal.onBinary((data) => {
|
||||
this.trzszFilter!.processBinaryInput(data);
|
||||
})
|
||||
);
|
||||
|
||||
// Terminal resize: send to server + update trzsz column count
|
||||
this.disposables.push(
|
||||
this.terminal.onResize(({ cols, rows }) => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
@@ -253,6 +291,7 @@ export class SSHTerminal {
|
||||
rows,
|
||||
}));
|
||||
}
|
||||
this.trzszFilter?.setTerminalColumns(cols);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -277,10 +316,32 @@ export class SSHTerminal {
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
private disposeConnectionDisposables(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
}
|
||||
|
||||
private clearReconnectTimeout(): void {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
private resetActiveConnection(): void {
|
||||
this.stopHeartbeat();
|
||||
this.clearReconnectTimeout();
|
||||
this.disposeConnectionDisposables();
|
||||
|
||||
if (this.ws && this.ws.readyState !== WebSocket.CLOSED && this.ws.readyState !== WebSocket.CLOSING) {
|
||||
this.ws.close(1000);
|
||||
}
|
||||
this.ws = null;
|
||||
this.trzszFilter = null;
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
this.clearReconnectTimeout();
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||
@@ -288,6 +349,7 @@ export class SSHTerminal {
|
||||
this.terminal.writeln(`\x1b[33m[*] Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...\x1b[0m`);
|
||||
|
||||
this.reconnectTimeout = setTimeout(async () => {
|
||||
this.reconnectTimeout = null;
|
||||
if (this.lastConfig) {
|
||||
this.terminal.writeln('\x1b[32m[+] Reconnecting...\x1b[0m');
|
||||
try {
|
||||
@@ -300,16 +362,8 @@ export class SSHTerminal {
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.stopHeartbeat();
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
this.reconnectAttempts = this.maxReconnectAttempts;
|
||||
this.ws?.close(1000);
|
||||
this.ws = null;
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables = [];
|
||||
this.resetActiveConnection();
|
||||
this.lastConfig = null;
|
||||
this.terminal.clear();
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
// @ts-ignore
|
||||
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
|
||||
|
||||
export class ZmodemHandler {
|
||||
private sentry: any;
|
||||
private session: any | null = null;
|
||||
private onTerminalWrite: (data: Uint8Array) => void;
|
||||
private onWsSend: (data: Uint8Array) => void;
|
||||
|
||||
constructor(
|
||||
onTerminalWrite: (data: Uint8Array) => void,
|
||||
onWsSend: (data: Uint8Array) => void
|
||||
) {
|
||||
this.onTerminalWrite = onTerminalWrite;
|
||||
this.onWsSend = onWsSend;
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.sentry = new Zmodem.Sentry({
|
||||
to_terminal: (octets: Array<number>) => {
|
||||
this.onTerminalWrite(new Uint8Array(octets));
|
||||
},
|
||||
sender: (octets: Array<number>) => {
|
||||
this.onWsSend(new Uint8Array(octets));
|
||||
},
|
||||
on_detect: (detection: any) => {
|
||||
const zsession = detection.confirm();
|
||||
this.session = zsession;
|
||||
|
||||
if (zsession.type === 'receive') {
|
||||
this.handleReceive(zsession);
|
||||
} else {
|
||||
this.handleSend(zsession);
|
||||
}
|
||||
},
|
||||
on_retract: () => {
|
||||
this.session = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public consume(data: ArrayBuffer) {
|
||||
this.sentry.consume(data);
|
||||
}
|
||||
|
||||
private handleReceive(zsession: any) {
|
||||
zsession.on('offer', (xfer: any) => {
|
||||
const name = xfer.get_details().name;
|
||||
const size = xfer.get_details().size;
|
||||
|
||||
const accept = confirm("接受来自服务器的文件下载?\n" + name + " (" + size + " bytes)");
|
||||
if (!accept) {
|
||||
xfer.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const packets: Uint8Array[] = [];
|
||||
xfer.on('input', (payload: number[]) => {
|
||||
packets.push(new Uint8Array(payload));
|
||||
});
|
||||
|
||||
xfer.accept().then(() => {
|
||||
Zmodem.Browser.save_to_disk(packets, name);
|
||||
});
|
||||
});
|
||||
|
||||
zsession.start();
|
||||
}
|
||||
|
||||
private handleSend(zsession: any) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.style.display = 'none';
|
||||
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
Zmodem.Browser.send_files(zsession, files, {
|
||||
on_offer_response: (file: any, xfer: any) => {
|
||||
if (!xfer) console.warn('Server rejected file:', file.name);
|
||||
},
|
||||
on_progress: (file: any, xfer: any, chunk: any) => {
|
||||
// Can implement progress bar here
|
||||
},
|
||||
on_file_complete: (file: any) => {
|
||||
console.log('Finished uploading:', file.name);
|
||||
}
|
||||
}).then(() => {
|
||||
zsession.close();
|
||||
input.remove();
|
||||
});
|
||||
} else {
|
||||
zsession.close();
|
||||
input.remove();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle cancel (focus returns without file selection)
|
||||
const onFocusBack = () => {
|
||||
setTimeout(() => {
|
||||
if (input.files?.length === 0) {
|
||||
zsession.close();
|
||||
input.remove();
|
||||
}
|
||||
}, 1000);
|
||||
window.removeEventListener('focus', onFocusBack);
|
||||
};
|
||||
window.addEventListener('focus', onFocusBack);
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
Generated
-3046
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -4,8 +4,8 @@
|
||||
"description": "纯 Cloudflare 架构 Web SSH 工具",
|
||||
"main": "src/worker/index.ts",
|
||||
"scripts": {
|
||||
"dev": "node scripts/build-html.js && npx wrangler dev",
|
||||
"deploy": "node scripts/build-html.js && npx wrangler deploy",
|
||||
"dev": "node scripts/build-html.js && wrangler dev",
|
||||
"deploy": "node scripts/build-html.js && wrangler deploy",
|
||||
"build:frontend": "node scripts/build-html.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
|
||||
Generated
+2234
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- '.'
|
||||
- 'frontend'
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
workerd: true
|
||||
@@ -11,8 +11,8 @@ const htmlTsFile = path.join(workerDir, 'html.ts');
|
||||
console.log('Building frontend...');
|
||||
try {
|
||||
// 1. Install and build frontend
|
||||
execSync('npm install', { cwd: frontendDir, stdio: 'inherit' });
|
||||
execSync('npm run build', { cwd: frontendDir, stdio: 'inherit' });
|
||||
execSync('pnpm install', { cwd: frontendDir, stdio: 'inherit' });
|
||||
execSync('pnpm run build', { cwd: frontendDir, stdio: 'inherit' });
|
||||
|
||||
console.log('Inlining assets...');
|
||||
// 2. Read dist/index.html
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
export const KEX_ALGORITHM_CURVE25519_SHA256 = 'curve25519-sha256';
|
||||
export const KEX_ALGORITHM_ECDH_NISTP256 = 'ecdh-sha2-nistp256';
|
||||
|
||||
export const SUPPORTED_KEX_ALGORITHMS = [
|
||||
KEX_ALGORITHM_CURVE25519_SHA256,
|
||||
KEX_ALGORITHM_ECDH_NISTP256,
|
||||
];
|
||||
|
||||
export const SUPPORTED_ENCRYPTION_ALGORITHMS = [
|
||||
'aes256-gcm@openssh.com',
|
||||
'aes128-gcm@openssh.com',
|
||||
'aes256-ctr',
|
||||
'aes192-ctr',
|
||||
'aes128-ctr',
|
||||
];
|
||||
|
||||
export const SUPPORTED_MAC_ALGORITHMS = [
|
||||
'hmac-sha2-256',
|
||||
'hmac-sha2-512',
|
||||
'hmac-sha1',
|
||||
];
|
||||
|
||||
export interface CipherSpec {
|
||||
mode: 'gcm' | 'ctr';
|
||||
blockSize: number;
|
||||
ivLength: number;
|
||||
keyLength: number;
|
||||
aead: boolean;
|
||||
}
|
||||
|
||||
export interface MacSpec {
|
||||
length: number;
|
||||
keyLength: number;
|
||||
}
|
||||
|
||||
const CIPHER_SPECS: Record<string, CipherSpec> = {
|
||||
'aes256-gcm@openssh.com': { mode: 'gcm', blockSize: 16, ivLength: 12, keyLength: 32, aead: true },
|
||||
'aes128-gcm@openssh.com': { mode: 'gcm', blockSize: 16, ivLength: 12, keyLength: 16, aead: true },
|
||||
'aes256-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 32, aead: false },
|
||||
'aes192-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 24, aead: false },
|
||||
'aes128-ctr': { mode: 'ctr', blockSize: 16, ivLength: 16, keyLength: 16, aead: false },
|
||||
};
|
||||
|
||||
const MAC_SPECS: Record<string, MacSpec> = {
|
||||
none: { length: 0, keyLength: 32 },
|
||||
'hmac-sha1': { length: 20, keyLength: 20 },
|
||||
'hmac-sha2-256': { length: 32, keyLength: 32 },
|
||||
'hmac-sha2-512': { length: 64, keyLength: 64 },
|
||||
};
|
||||
|
||||
export function isCurve25519KEXAlgorithm(algorithm: string): boolean {
|
||||
return algorithm === KEX_ALGORITHM_CURVE25519_SHA256;
|
||||
}
|
||||
|
||||
export function getCipherSpec(algorithm: string): CipherSpec {
|
||||
const spec = CIPHER_SPECS[algorithm];
|
||||
if (!spec) throw new Error(`Unsupported cipher: ${algorithm}`);
|
||||
return spec;
|
||||
}
|
||||
|
||||
export function getMacSpec(algorithm: string): MacSpec {
|
||||
const spec = MAC_SPECS[algorithm];
|
||||
if (!spec) throw new Error(`Unsupported MAC algorithm: ${algorithm}`);
|
||||
return spec;
|
||||
}
|
||||
|
||||
export function getMacAlgorithmsForCipher(cipher: string): string[] {
|
||||
if (getCipherSpec(cipher).aead) return ['none'];
|
||||
return SUPPORTED_MAC_ALGORITHMS;
|
||||
}
|
||||
+121
-4
@@ -1,3 +1,5 @@
|
||||
import { concat, encodeUint32 } from './utils';
|
||||
|
||||
export class SSHAESGCMCipher {
|
||||
private key: CryptoKey | null = null;
|
||||
private iv: Uint8Array;
|
||||
@@ -32,7 +34,7 @@ export class SSHAESGCMCipher {
|
||||
}
|
||||
}
|
||||
|
||||
async encrypt(plaintext: Uint8Array, _seqNum?: number, aad?: Uint8Array): Promise<Uint8Array> {
|
||||
async encrypt(plaintext: Uint8Array, _seqNum?: number, aad?: Uint8Array, _commit: boolean = true): Promise<Uint8Array> {
|
||||
if (!this.key) throw new Error('Cipher not initialized');
|
||||
const nonce = new Uint8Array(this.iv);
|
||||
|
||||
@@ -40,14 +42,14 @@ export class SSHAESGCMCipher {
|
||||
if (aad) alg.additionalData = aad;
|
||||
|
||||
const encrypted = new Uint8Array(
|
||||
await crypto.subtle.encrypt(alg as AesGcmParams, this.key, plaintext)
|
||||
await crypto.subtle.encrypt(alg as unknown as SubtleCryptoEncryptAlgorithm, this.key, plaintext)
|
||||
);
|
||||
|
||||
this.incIV();
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: Uint8Array, _seqNum?: number, aad?: Uint8Array): Promise<Uint8Array | null> {
|
||||
async decrypt(ciphertext: Uint8Array, _seqNum?: number, aad?: Uint8Array, _commit: boolean = true): Promise<Uint8Array | null> {
|
||||
if (!this.key) throw new Error('Cipher not initialized');
|
||||
const nonce = new Uint8Array(this.iv);
|
||||
|
||||
@@ -56,7 +58,7 @@ export class SSHAESGCMCipher {
|
||||
|
||||
try {
|
||||
const decrypted = new Uint8Array(
|
||||
await crypto.subtle.decrypt(alg as AesGcmParams, this.key, ciphertext)
|
||||
await crypto.subtle.decrypt(alg as unknown as SubtleCryptoEncryptAlgorithm, this.key, ciphertext)
|
||||
);
|
||||
this.incIV();
|
||||
return decrypted;
|
||||
@@ -67,6 +69,121 @@ export class SSHAESGCMCipher {
|
||||
}
|
||||
}
|
||||
|
||||
export class SSHAESCTRCipher {
|
||||
private key: CryptoKey | null = null;
|
||||
private counter: Uint8Array;
|
||||
private rawKey: Uint8Array;
|
||||
|
||||
constructor(rawKey: Uint8Array, iv: Uint8Array) {
|
||||
if (iv.length !== 16) {
|
||||
throw new Error(`AES-CTR requires a 16-byte IV, got ${iv.length}`);
|
||||
}
|
||||
this.counter = new Uint8Array(iv);
|
||||
this.rawKey = rawKey;
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
this.rawKey,
|
||||
{ name: 'AES-CTR', length: this.rawKey.length * 8 },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
private incCounter(blocks: number): void {
|
||||
for (let block = 0; block < blocks; block++) {
|
||||
for (let i = 15; i >= 0; i--) {
|
||||
this.counter[i]++;
|
||||
if (this.counter[i] !== 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async encrypt(plaintext: Uint8Array, _seqNum?: number, _aad?: Uint8Array, commit: boolean = true): Promise<Uint8Array> {
|
||||
if (!this.key) throw new Error('Cipher not initialized');
|
||||
const counter = new Uint8Array(this.counter);
|
||||
const encrypted = new Uint8Array(
|
||||
await crypto.subtle.encrypt(
|
||||
{ name: 'AES-CTR', counter, length: 128 } as SubtleCryptoEncryptAlgorithm,
|
||||
this.key,
|
||||
plaintext
|
||||
)
|
||||
);
|
||||
if (commit) {
|
||||
this.incCounter(Math.ceil(plaintext.length / 16));
|
||||
}
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
async decrypt(ciphertext: Uint8Array, _seqNum?: number, _aad?: Uint8Array, commit: boolean = true): Promise<Uint8Array | null> {
|
||||
if (!this.key) throw new Error('Cipher not initialized');
|
||||
const counter = new Uint8Array(this.counter);
|
||||
try {
|
||||
const decrypted = new Uint8Array(
|
||||
await crypto.subtle.decrypt(
|
||||
{ name: 'AES-CTR', counter, length: 128 } as SubtleCryptoEncryptAlgorithm,
|
||||
this.key,
|
||||
ciphertext
|
||||
)
|
||||
);
|
||||
if (commit) {
|
||||
this.incCounter(Math.ceil(ciphertext.length / 16));
|
||||
}
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
console.error('[CRYPTO] AES-CTR decrypt failed, ciphertextLen:', ciphertext.length, 'error:', e instanceof Error ? e.message : String(e));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SSHHMAC {
|
||||
private key: CryptoKey | null = null;
|
||||
private rawKey: Uint8Array;
|
||||
private hash: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
||||
readonly length: number;
|
||||
|
||||
constructor(algorithm: string, rawKey: Uint8Array) {
|
||||
this.rawKey = rawKey;
|
||||
if (algorithm === 'hmac-sha1') {
|
||||
this.hash = 'SHA-1';
|
||||
this.length = 20;
|
||||
} else if (algorithm === 'hmac-sha2-256') {
|
||||
this.hash = 'SHA-256';
|
||||
this.length = 32;
|
||||
} else if (algorithm === 'hmac-sha2-512') {
|
||||
this.hash = 'SHA-512';
|
||||
this.length = 64;
|
||||
} else {
|
||||
throw new Error(`Unsupported MAC algorithm: ${algorithm}`);
|
||||
}
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
this.rawKey,
|
||||
{ name: 'HMAC', hash: this.hash },
|
||||
false,
|
||||
['sign', 'verify']
|
||||
);
|
||||
}
|
||||
|
||||
async sign(packet: Uint8Array, seqNum: number): Promise<Uint8Array> {
|
||||
if (!this.key) throw new Error('MAC not initialized');
|
||||
const data = concat(encodeUint32(seqNum), packet);
|
||||
return new Uint8Array(await crypto.subtle.sign('HMAC', this.key, data));
|
||||
}
|
||||
|
||||
async verify(packet: Uint8Array, seqNum: number, expected: Uint8Array): Promise<boolean> {
|
||||
if (!this.key) throw new Error('MAC not initialized');
|
||||
const data = concat(encodeUint32(seqNum), packet);
|
||||
return crypto.subtle.verify('HMAC', this.key, expected, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const REKEY_THRESHOLD = 1 << 30;
|
||||
|
||||
export function shouldRekey(seqNum: number): boolean {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { SSH_MSG_KEX_ECDH_INIT } from '../types';
|
||||
import { concat, readUint32, encodeString, toSSHMPInt } from './utils';
|
||||
|
||||
type X25519SubtleCrypto = {
|
||||
generateKey(
|
||||
algorithm: { name: 'X25519' },
|
||||
extractable: boolean,
|
||||
keyUsages: string[]
|
||||
): Promise<CryptoKeyPair>;
|
||||
exportKey(format: 'raw', key: CryptoKey): Promise<ArrayBuffer>;
|
||||
importKey(
|
||||
format: 'raw',
|
||||
keyData: ArrayBuffer | ArrayBufferView,
|
||||
algorithm: { name: 'X25519' },
|
||||
extractable: boolean,
|
||||
keyUsages: string[]
|
||||
): Promise<CryptoKey>;
|
||||
deriveBits(
|
||||
algorithm: { name: 'X25519'; public: CryptoKey },
|
||||
baseKey: CryptoKey,
|
||||
length: number
|
||||
): Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
function x25519Subtle(): X25519SubtleCrypto {
|
||||
return crypto.subtle as unknown as X25519SubtleCrypto;
|
||||
}
|
||||
|
||||
function isAllZero(bytes: Uint8Array): boolean {
|
||||
for (const byte of bytes) {
|
||||
if (byte !== 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export type Curve25519KeyPair = CryptoKeyPair;
|
||||
|
||||
export class Curve25519KeyExchange {
|
||||
static async generateKeyPair(): Promise<Curve25519KeyPair> {
|
||||
return x25519Subtle().generateKey(
|
||||
{ name: 'X25519' },
|
||||
true,
|
||||
['deriveBits']
|
||||
);
|
||||
}
|
||||
|
||||
static async exportRawPublicKey(keyPair: Curve25519KeyPair): Promise<Uint8Array> {
|
||||
return new Uint8Array(
|
||||
await x25519Subtle().exportKey('raw', keyPair.publicKey)
|
||||
);
|
||||
}
|
||||
|
||||
static buildInit(clientRawPublicKey: Uint8Array): Uint8Array {
|
||||
if (clientRawPublicKey.length !== 32) {
|
||||
throw new Error(`Invalid Curve25519 client public key length: ${clientRawPublicKey.length}`);
|
||||
}
|
||||
|
||||
return concat(
|
||||
new Uint8Array([SSH_MSG_KEX_ECDH_INIT]),
|
||||
encodeString(clientRawPublicKey)
|
||||
);
|
||||
}
|
||||
|
||||
static parseReply(data: Uint8Array): {
|
||||
hostKey: Uint8Array;
|
||||
serverRawPublicKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
} {
|
||||
let offset = 1;
|
||||
|
||||
const hostKeyLen = readUint32(data, offset);
|
||||
offset += 4;
|
||||
const hostKey = data.slice(offset, offset + hostKeyLen);
|
||||
offset += hostKeyLen;
|
||||
|
||||
const qSLen = readUint32(data, offset);
|
||||
offset += 4;
|
||||
const serverRawPublicKey = data.slice(offset, offset + qSLen);
|
||||
offset += qSLen;
|
||||
|
||||
const sigLen = readUint32(data, offset);
|
||||
offset += 4;
|
||||
const signature = data.slice(offset, offset + sigLen);
|
||||
|
||||
return { hostKey, serverRawPublicKey, signature };
|
||||
}
|
||||
|
||||
static async computeSharedSecret(
|
||||
privateKey: CryptoKey,
|
||||
serverRawPublicKey: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
if (serverRawPublicKey.length !== 32) {
|
||||
throw new Error(`Invalid Curve25519 server public key length: ${serverRawPublicKey.length}`);
|
||||
}
|
||||
|
||||
const serverKey = await x25519Subtle().importKey(
|
||||
'raw',
|
||||
serverRawPublicKey,
|
||||
{ name: 'X25519' },
|
||||
false,
|
||||
[]
|
||||
);
|
||||
|
||||
const sharedSecret = new Uint8Array(
|
||||
await x25519Subtle().deriveBits(
|
||||
{ name: 'X25519', public: serverKey },
|
||||
privateKey,
|
||||
256
|
||||
)
|
||||
);
|
||||
if (isAllZero(sharedSecret)) {
|
||||
throw new Error('Curve25519 key exchange failed: all-zero shared secret');
|
||||
}
|
||||
return toSSHMPInt(sharedSecret);
|
||||
}
|
||||
|
||||
static async computeExchangeHash(
|
||||
clientVersion: string,
|
||||
serverVersion: string,
|
||||
clientKEXInit: Uint8Array,
|
||||
serverKEXInit: Uint8Array,
|
||||
hostKey: Uint8Array,
|
||||
clientRawPublicKey: Uint8Array,
|
||||
serverRawPublicKey: Uint8Array,
|
||||
sharedSecret: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
const v_c = encodeString(clientVersion);
|
||||
const v_s = encodeString(serverVersion);
|
||||
const i_c = encodeString(clientKEXInit);
|
||||
const i_s = encodeString(serverKEXInit);
|
||||
const k_s = encodeString(hostKey);
|
||||
const e = encodeString(clientRawPublicKey);
|
||||
const f = encodeString(serverRawPublicKey);
|
||||
const k = sharedSecret;
|
||||
|
||||
const data = concat(v_c, v_s, i_c, i_s, k_s, e, f, k);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
return new Uint8Array(hashBuffer);
|
||||
}
|
||||
}
|
||||
+29
-12
@@ -1,9 +1,34 @@
|
||||
import { SSH_MSG_KEX_ECDH_INIT } from '../types';
|
||||
import { concat, readUint32, encodeString, toSSHMPInt } from './utils';
|
||||
|
||||
type ECDHSubtleCrypto = {
|
||||
generateKey(
|
||||
algorithm: { name: 'ECDH'; namedCurve: 'P-256' },
|
||||
extractable: boolean,
|
||||
keyUsages: string[]
|
||||
): Promise<CryptoKeyPair>;
|
||||
exportKey(format: 'raw', key: CryptoKey): Promise<ArrayBuffer>;
|
||||
importKey(
|
||||
format: 'raw',
|
||||
keyData: ArrayBuffer | ArrayBufferView,
|
||||
algorithm: { name: 'ECDH'; namedCurve: 'P-256' },
|
||||
extractable: boolean,
|
||||
keyUsages: string[]
|
||||
): Promise<CryptoKey>;
|
||||
deriveBits(
|
||||
algorithm: { name: 'ECDH'; public: CryptoKey },
|
||||
baseKey: CryptoKey,
|
||||
length: number
|
||||
): Promise<ArrayBuffer>;
|
||||
};
|
||||
|
||||
function ecdhSubtle(): ECDHSubtleCrypto {
|
||||
return crypto.subtle as unknown as ECDHSubtleCrypto;
|
||||
}
|
||||
|
||||
export class ECDHKeyExchange {
|
||||
static async generateKeyPair(): Promise<CryptoKeyPair> {
|
||||
return crypto.subtle.generateKey(
|
||||
return ecdhSubtle().generateKey(
|
||||
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||
true,
|
||||
['deriveBits']
|
||||
@@ -12,7 +37,7 @@ export class ECDHKeyExchange {
|
||||
|
||||
static async exportRawPublicKey(keyPair: CryptoKeyPair): Promise<Uint8Array> {
|
||||
return new Uint8Array(
|
||||
await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
||||
await ecdhSubtle().exportKey('raw', keyPair.publicKey)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +76,7 @@ export class ECDHKeyExchange {
|
||||
privateKey: CryptoKey,
|
||||
serverRawPublicKey: Uint8Array
|
||||
): Promise<Uint8Array> {
|
||||
const serverKey = await crypto.subtle.importKey(
|
||||
const serverKey = await ecdhSubtle().importKey(
|
||||
'raw',
|
||||
serverRawPublicKey,
|
||||
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||
@@ -59,7 +84,7 @@ export class ECDHKeyExchange {
|
||||
[]
|
||||
);
|
||||
|
||||
const sharedBits = await crypto.subtle.deriveBits(
|
||||
const sharedBits = await ecdhSubtle().deriveBits(
|
||||
{ name: 'ECDH', public: serverKey },
|
||||
privateKey,
|
||||
256
|
||||
@@ -87,15 +112,7 @@ export class ECDHKeyExchange {
|
||||
const f = encodeString(serverRawPublicKey);
|
||||
const k = sharedSecret;
|
||||
|
||||
console.log('[KEX] Hash input lengths: V_C=' + v_c.length + ' V_S=' + v_s.length +
|
||||
' I_C=' + i_c.length + ' I_S=' + i_s.length +
|
||||
' K_S=' + k_s.length + ' e=' + e.length + ' f=' + f.length + ' K=' + k.length);
|
||||
console.log('[KEX] V_C hex=' + Array.from(v_c).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
console.log('[KEX] V_S hex=' + Array.from(v_s).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
|
||||
const data = concat(v_c, v_s, i_c, i_s, k_s, e, f, k);
|
||||
console.log('[KEX] Hash input total length: ' + data.length);
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
return new Uint8Array(hashBuffer);
|
||||
}
|
||||
|
||||
+13
-8
@@ -1,5 +1,10 @@
|
||||
import { SSH_MSG_KEXINIT, KEXInitMessage } from '../types';
|
||||
import { encodeUint32, concat } from './utils';
|
||||
import {
|
||||
SUPPORTED_ENCRYPTION_ALGORITHMS,
|
||||
SUPPORTED_KEX_ALGORITHMS,
|
||||
SUPPORTED_MAC_ALGORITHMS
|
||||
} from './algorithms';
|
||||
import { concat } from './utils';
|
||||
|
||||
export class KEXInitBuilder {
|
||||
static build(): Uint8Array {
|
||||
@@ -12,12 +17,12 @@ export class KEXInitBuilder {
|
||||
parts.push(cookie);
|
||||
|
||||
const algorithmLists = [
|
||||
'ecdh-sha2-nistp256',
|
||||
SUPPORTED_KEX_ALGORITHMS.join(','),
|
||||
'ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa',
|
||||
'aes256-gcm@openssh.com,aes128-gcm@openssh.com',
|
||||
'aes256-gcm@openssh.com,aes128-gcm@openssh.com',
|
||||
'none',
|
||||
'none',
|
||||
SUPPORTED_ENCRYPTION_ALGORITHMS.join(','),
|
||||
SUPPORTED_ENCRYPTION_ALGORITHMS.join(','),
|
||||
SUPPORTED_MAC_ALGORITHMS.join(','),
|
||||
SUPPORTED_MAC_ALGORITHMS.join(','),
|
||||
'none',
|
||||
'none',
|
||||
'',
|
||||
@@ -68,9 +73,9 @@ export function parseKEXInit(data: Uint8Array): KEXInitMessage {
|
||||
};
|
||||
}
|
||||
|
||||
export function negotiate(clientList: string[], serverList: string[]): string {
|
||||
export function negotiate(clientList: string[], serverList: string[], category: string = 'algorithm'): string {
|
||||
for (const algo of clientList) {
|
||||
if (serverList.includes(algo)) return algo;
|
||||
}
|
||||
throw new Error(`No common algorithm: ${clientList} vs ${serverList}`);
|
||||
throw new Error(`No common ${category}: client=[${clientList.join(',')}] server=[${serverList.join(',')}]`);
|
||||
}
|
||||
+9
-5
@@ -5,14 +5,18 @@ export class KeyDerivation {
|
||||
static async deriveKeys(
|
||||
sharedSecret: Uint8Array,
|
||||
exchangeHash: Uint8Array,
|
||||
sessionID: Uint8Array
|
||||
sessionID: Uint8Array,
|
||||
ivLengthC2S: number = 12,
|
||||
ivLengthS2C: number = 12,
|
||||
integrityKeyLengthC2S: number = 32,
|
||||
integrityKeyLengthS2C: number = 32
|
||||
): Promise<SessionKeys> {
|
||||
const ivC2S = await this.expandKey(sharedSecret, exchangeHash, 'A', sessionID, 12);
|
||||
const ivS2C = await this.expandKey(sharedSecret, exchangeHash, 'B', sessionID, 12);
|
||||
const ivC2S = await this.expandKey(sharedSecret, exchangeHash, 'A', sessionID, ivLengthC2S);
|
||||
const ivS2C = await this.expandKey(sharedSecret, exchangeHash, 'B', sessionID, ivLengthS2C);
|
||||
const keyC2S = await this.expandKey(sharedSecret, exchangeHash, 'C', sessionID, 32);
|
||||
const keyS2C = await this.expandKey(sharedSecret, exchangeHash, 'D', sessionID, 32);
|
||||
const intKeyC2S = await this.expandKey(sharedSecret, exchangeHash, 'E', sessionID, 32);
|
||||
const intKeyS2C = await this.expandKey(sharedSecret, exchangeHash, 'F', sessionID, 32);
|
||||
const intKeyC2S = await this.expandKey(sharedSecret, exchangeHash, 'E', sessionID, integrityKeyLengthC2S);
|
||||
const intKeyS2C = await this.expandKey(sharedSecret, exchangeHash, 'F', sessionID, integrityKeyLengthS2C);
|
||||
|
||||
return {
|
||||
ivClientToServer: ivC2S,
|
||||
|
||||
+29
-10
@@ -13,8 +13,10 @@ export class SSHPacketParser {
|
||||
}
|
||||
|
||||
async nextPacket(blockSize: number, decrypt: (
|
||||
data: Uint8Array, seq: number, aad?: Uint8Array
|
||||
) => Uint8Array | Promise<Uint8Array | null> | null, hasAuthTag: boolean = false): Promise<SSHPacket | null> {
|
||||
data: Uint8Array, seq: number, aad?: Uint8Array, commit?: boolean
|
||||
) => Uint8Array | Promise<Uint8Array | null> | null, hasAuthTag: boolean = false,
|
||||
macLength: number = 0,
|
||||
verifyMac?: (packet: Uint8Array, mac: Uint8Array, seq: number) => boolean | Promise<boolean>): Promise<SSHPacket | null> {
|
||||
if (hasAuthTag) {
|
||||
if (this.buffer.length < 4) return null;
|
||||
const packetLength = readUint32(this.buffer, 0);
|
||||
@@ -27,7 +29,7 @@ export class SSHPacketParser {
|
||||
|
||||
const lengthField = raw.slice(0, 4);
|
||||
const dataToDecrypt = raw.slice(4);
|
||||
const decrypted = await decrypt(dataToDecrypt, this.seqNum, lengthField);
|
||||
const decrypted = await decrypt(dataToDecrypt, this.seqNum, lengthField, true);
|
||||
if (!decrypted) return null;
|
||||
|
||||
const paddingLength = decrypted[0];
|
||||
@@ -46,7 +48,7 @@ export class SSHPacketParser {
|
||||
if (this.buffer.length < blockSize) return null;
|
||||
|
||||
const header = await decrypt(
|
||||
this.buffer.slice(0, blockSize), this.seqNum
|
||||
this.buffer.slice(0, blockSize), this.seqNum, undefined, false
|
||||
);
|
||||
if (!header) return null;
|
||||
|
||||
@@ -56,14 +58,22 @@ export class SSHPacketParser {
|
||||
const totalBlocks = Math.ceil((4 + packetLength) / blockSize);
|
||||
const totalSize = totalBlocks * blockSize;
|
||||
|
||||
if (this.buffer.length < totalSize) return null;
|
||||
if (this.buffer.length < totalSize + macLength) return null;
|
||||
|
||||
const encryptedPacket = this.buffer.slice(0, totalSize);
|
||||
this.buffer = this.buffer.slice(totalSize);
|
||||
const mac = this.buffer.slice(totalSize, totalSize + macLength);
|
||||
this.buffer = this.buffer.slice(totalSize + macLength);
|
||||
|
||||
const decrypted = await decrypt(encryptedPacket, this.seqNum);
|
||||
const decrypted = await decrypt(encryptedPacket, this.seqNum, undefined, true);
|
||||
if (!decrypted) return null;
|
||||
|
||||
if (verifyMac && macLength > 0) {
|
||||
const macValid = await verifyMac(decrypted, mac, this.seqNum);
|
||||
if (!macValid) {
|
||||
throw new Error('Invalid packet MAC');
|
||||
}
|
||||
}
|
||||
|
||||
const paddingLength = decrypted[4];
|
||||
const payload = decrypted.slice(5, 5 + packetLength - 1 - paddingLength);
|
||||
|
||||
@@ -73,6 +83,7 @@ export class SSHPacketParser {
|
||||
length: packetLength,
|
||||
paddingLength,
|
||||
payload,
|
||||
mac,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,7 +106,8 @@ export class SSHPacketBuilder {
|
||||
blockSize: number,
|
||||
encrypt: ((data: Uint8Array, seq: number, aad?: Uint8Array) => Uint8Array | Promise<Uint8Array>) | null,
|
||||
seqNum: number,
|
||||
hasAuthTag: boolean = false
|
||||
hasAuthTag: boolean = false,
|
||||
mac?: (packet: Uint8Array, seq: number) => Uint8Array | Promise<Uint8Array>
|
||||
): Promise<Uint8Array> {
|
||||
const packetLength = 1 + payload.length;
|
||||
// For AES-GCM (hasAuthTag), padding aligns the encrypted portion
|
||||
@@ -123,7 +135,6 @@ export class SSHPacketBuilder {
|
||||
|
||||
packet.set(payload, 5);
|
||||
|
||||
const crypto = globalThis.crypto;
|
||||
const randomPadding = new Uint8Array(paddingLength);
|
||||
crypto.getRandomValues(randomPadding);
|
||||
packet.set(randomPadding, 5 + payload.length);
|
||||
@@ -138,7 +149,15 @@ export class SSHPacketBuilder {
|
||||
result.set(encryptedData, 4);
|
||||
return result;
|
||||
}
|
||||
return await encrypt(packet, seqNum);
|
||||
const encryptedPacket = await encrypt(packet, seqNum);
|
||||
if (mac) {
|
||||
const macBytes = await mac(packet, seqNum);
|
||||
const result = new Uint8Array(encryptedPacket.length + macBytes.length);
|
||||
result.set(encryptedPacket, 0);
|
||||
result.set(macBytes, encryptedPacket.length);
|
||||
return result;
|
||||
}
|
||||
return encryptedPacket;
|
||||
}
|
||||
|
||||
return packet;
|
||||
|
||||
@@ -50,7 +50,6 @@ export class SSHSessionDO {
|
||||
return new Response('Expected WebSocket', { status: 400 });
|
||||
}
|
||||
|
||||
// 检查是否有预填充配置(来自 one-time-token 连接)
|
||||
const url = new URL(request.url);
|
||||
const configParam = url.searchParams.get('config');
|
||||
let prefilledConfig: SSHConnectionConfig | null = null;
|
||||
@@ -66,13 +65,10 @@ export class SSHSessionDO {
|
||||
const pair = new WebSocketPair();
|
||||
const [client, server] = [pair[0], pair[1]];
|
||||
|
||||
// Use Hibernation API for long-lived WebSocket connections
|
||||
this.state.acceptWebSocket(server);
|
||||
|
||||
if (prefilledConfig) {
|
||||
// 预填充模式:直接发起 SSH 连接,不等待前端凭据
|
||||
server.serializeAttachment({ state: 'prefilled' });
|
||||
// 使用 queueMicrotask 确保 WebSocket 就绪后再连接
|
||||
queueMicrotask(async () => {
|
||||
try {
|
||||
await this.initSSHSession(server, prefilledConfig!);
|
||||
@@ -85,8 +81,6 @@ export class SSHSessionDO {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 匿名模式:等待前端发送凭据
|
||||
// Set a timeout for receiving credentials
|
||||
const timeout = setTimeout(() => {
|
||||
try {
|
||||
server.send(JSON.stringify({ type: 'error', message: 'Connection timeout' }));
|
||||
@@ -104,7 +98,6 @@ export class SSHSessionDO {
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Hibernation API: called when a WebSocket receives a message
|
||||
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void> {
|
||||
const session = this.sessions.get(ws);
|
||||
if (session) {
|
||||
@@ -112,7 +105,6 @@ export class SSHSessionDO {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is the first message (credentials)
|
||||
const timeout = this._pendingTimeouts.get(ws);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
@@ -135,7 +127,6 @@ export class SSHSessionDO {
|
||||
}
|
||||
}
|
||||
|
||||
// Hibernation API: called when a WebSocket is closed
|
||||
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
|
||||
const session = this.sessions.get(ws);
|
||||
if (session) {
|
||||
@@ -149,7 +140,6 @@ export class SSHSessionDO {
|
||||
}
|
||||
}
|
||||
|
||||
// Hibernation API: called when a WebSocket error occurs
|
||||
async webSocketError(ws: WebSocket, error: unknown): Promise<void> {
|
||||
await this.webSocketClose(ws, 1011, 'Error', false);
|
||||
}
|
||||
|
||||
+282
-274
@@ -1,55 +1,55 @@
|
||||
// Automatically generated by scripts/build-html.js. Do not edit directly.
|
||||
export const HTML = `<!DOCTYPE html>
|
||||
<html class="dark" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>CloudSSH</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"background": "#131313",
|
||||
"on-surface": "#e5e2e1",
|
||||
"on-surface-variant": "#bbccb0",
|
||||
"outline": "#86957d",
|
||||
"outline-variant": "#3c4b36",
|
||||
"primary-container": "#4af626",
|
||||
"on-primary-fixed": "#022100",
|
||||
"surface": "#131313",
|
||||
"surface-variant": "#353534",
|
||||
"secondary-container": "#14d1ff",
|
||||
"secondary-fixed": "#b7eaff",
|
||||
"error": "#ffb4ab",
|
||||
"error-container": "#93000a",
|
||||
"surface-container-lowest": "#0e0e0e"
|
||||
},
|
||||
fontFamily: {
|
||||
"body": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"label": ["JetBrains Mono"],
|
||||
"code": ["JetBrains Mono"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script type="module">var Ws=Object.defineProperty;var $s=(r,e,_)=>e in r?Ws(r,e,{enumerable:!0,configurable:!0,writable:!0,value:_}):r[e]=_;var Y=(r,e,_)=>$s(r,typeof e!="symbol"?e+"":e,_);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const g of document.querySelectorAll('link[rel="modulepreload"]'))C(g);new MutationObserver(g=>{for(const k of g)if(k.type==="childList")for(const w of k.addedNodes)w.tagName==="LINK"&&w.rel==="modulepreload"&&C(w)}).observe(document,{childList:!0,subtree:!0});function _(g){const k={};return g.integrity&&(k.integrity=g.integrity),g.referrerPolicy&&(k.referrerPolicy=g.referrerPolicy),g.crossOrigin==="use-credentials"?k.credentials="include":g.crossOrigin==="anonymous"?k.credentials="omit":k.credentials="same-origin",k}function C(g){if(g.ep)return;g.ep=!0;const k=_(g);fetch(g.href,k)}})();var wt={exports:{}},di;function Us(){return di||(di=1,(function(r,e){(function(_,C){r.exports=C()})(globalThis,(()=>(()=>{var _={4567:function(w,s,a){var l=this&&this.__decorate||function(i,n,c,m){var S,y=arguments.length,f=y<3?n:m===null?m=Object.getOwnPropertyDescriptor(n,c):m;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")f=Reflect.decorate(i,n,c,m);else for(var L=i.length-1;L>=0;L--)(S=i[L])&&(f=(y<3?S(f):y>3?S(n,c,f):S(n,c))||f);return y>3&&f&&Object.defineProperty(n,c,f),f},d=this&&this.__param||function(i,n){return function(c,m){n(c,m,i)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const h=a(9042),u=a(9924),v=a(844),b=a(4725),p=a(2585),t=a(3656);let o=s.AccessibilityManager=class extends v.Disposable{constructor(i,n,c,m){super(),this._terminal=i,this._coreBrowserService=c,this._renderService=m,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new u.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
|
||||
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,t.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,v.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(i){for(let n=0;n<i;n++)this._handleChar(" ")}_handleChar(i){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==i&&(this._charsToAnnounce+=i):this._charsToAnnounce+=i,i===\`
|
||||
\`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=h.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(i){this._clearLiveRegion(),new RegExp("\\\\p{Control}","u").test(i)||this._charsToConsume.push(i)}_refreshRows(i,n){this._liveRegionDebouncer.refresh(i,n,this._terminal.rows)}_renderRows(i,n){const c=this._terminal.buffer,m=c.lines.length.toString();for(let S=i;S<=n;S++){const y=c.lines.get(c.ydisp+S),f=[],L=(y==null?void 0:y.translateToString(!0,void 0,void 0,f))||"",M=(c.ydisp+S+1).toString(),T=this._rowElements[S];T&&(L.length===0?(T.innerText=" ",this._rowColumns.set(T,[0,1])):(T.textContent=L,this._rowColumns.set(T,f)),T.setAttribute("aria-posinset",M),T.setAttribute("aria-setsize",m))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(i,n){const c=i.target,m=this._rowElements[n===0?1:this._rowElements.length-2];if(c.getAttribute("aria-posinset")===(n===0?"1":\`\${this._terminal.buffer.lines.length}\`)||i.relatedTarget!==m)return;let S,y;if(n===0?(S=c,y=this._rowElements.pop(),this._rowContainer.removeChild(y)):(S=this._rowElements.shift(),y=c,this._rowContainer.removeChild(S)),S.removeEventListener("focus",this._topBoundaryFocusListener),y.removeEventListener("focus",this._bottomBoundaryFocusListener),n===0){const f=this._createAccessibilityTreeNode();this._rowElements.unshift(f),this._rowContainer.insertAdjacentElement("afterbegin",f)}else{const f=this._createAccessibilityTreeNode();this._rowElements.push(f),this._rowContainer.appendChild(f)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(n===0?-1:1),this._rowElements[n===0?1:this._rowElements.length-2].focus(),i.preventDefault(),i.stopImmediatePropagation()}_handleSelectionChange(){var L;if(this._rowElements.length===0)return;const i=document.getSelection();if(!i)return;if(i.isCollapsed)return void(this._rowContainer.contains(i.anchorNode)&&this._terminal.clearSelection());if(!i.anchorNode||!i.focusNode)return void console.error("anchorNode and/or focusNode are null");let n={node:i.anchorNode,offset:i.anchorOffset},c={node:i.focusNode,offset:i.focusOffset};if((n.node.compareDocumentPosition(c.node)&Node.DOCUMENT_POSITION_PRECEDING||n.node===c.node&&n.offset>c.offset)&&([n,c]=[c,n]),n.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(n={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(n.node))return;const m=this._rowElements.slice(-1)[0];if(c.node.compareDocumentPosition(m)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(c={node:m,offset:((L=m.textContent)==null?void 0:L.length)??0}),!this._rowContainer.contains(c.node))return;const S=({node:M,offset:T})=>{const B=M instanceof Text?M.parentNode:M;let R=parseInt(B==null?void 0:B.getAttribute("aria-posinset"),10)-1;if(isNaN(R))return console.warn("row is invalid. Race condition?"),null;const O=this._rowColumns.get(B);if(!O)return console.warn("columns is null. Race condition?"),null;let F=T<O.length?O[T]:O.slice(-1)[0]+1;return F>=this._terminal.cols&&(++R,F=0),{row:R,column:F}},y=S(n),f=S(c);if(y&&f){if(y.row>f.row||y.row===f.row&&y.column>=f.column)throw new Error("invalid range");this._terminal.select(y.column,y.row,(f.row-y.row)*this._terminal.cols-y.column+f.column)}}_handleResize(i){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let n=this._rowContainer.children.length;n<this._terminal.rows;n++)this._rowElements[n]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[n]);for(;this._rowElements.length>i;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const i=this._coreBrowserService.mainDocument.createElement("div");return i.setAttribute("role","listitem"),i.tabIndex=-1,this._refreshRowDimensions(i),i}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=\`\${this._renderService.dimensions.css.canvas.width}px\`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let i=0;i<this._terminal.rows;i++)this._refreshRowDimensions(this._rowElements[i])}}_refreshRowDimensions(i){i.style.height=\`\${this._renderService.dimensions.css.cell.height}px\`}};s.AccessibilityManager=o=l([d(1,p.IInstantiationService),d(2,b.ICoreBrowserService),d(3,b.IRenderService)],o)},3614:(w,s)=>{function a(u){return u.replace(/\\r?\\n/g,"\\Line truncated
|
||||
export const HTML = `<!DOCTYPE html>
|
||||
<html class="dark" lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+CiAgPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzEzMTMxMyIvPgogIDxyZWN0IHg9IjIiIHk9IjIiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyOCIgcng9IjMiIGZpbGw9IiMxYTFhMWEiIHN0cm9rZT0iIzRhZjYyNiIgc3Ryb2tlLXdpZHRoPSIxIi8+CiAgPHBhdGggZD0iTTggMTFMMTQgMTZMOCAyMSIgc3Ryb2tlPSIjNGFmNjI2IiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBmaWxsPSJub25lIi8+CiAgPGxpbmUgeDE9IjE2IiB5MT0iMjEiIHgyPSIyNCIgeTI9IjIxIiBzdHJva2U9IiM0YWY2MjYiIHN0cm9rZS13aWR0aD0iMi41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KPC9zdmc+" />
|
||||
<title>CloudSSH</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"background": "#131313",
|
||||
"on-surface": "#e5e2e1",
|
||||
"on-surface-variant": "#bbccb0",
|
||||
"outline": "#86957d",
|
||||
"outline-variant": "#3c4b36",
|
||||
"primary-container": "#4af626",
|
||||
"on-primary-fixed": "#022100",
|
||||
"surface": "#131313",
|
||||
"surface-variant": "#353534",
|
||||
"secondary-container": "#14d1ff",
|
||||
"secondary-fixed": "#b7eaff",
|
||||
"error": "#ffb4ab",
|
||||
"error-container": "#93000a",
|
||||
"surface-container-lowest": "#0e0e0e"
|
||||
},
|
||||
fontFamily: {
|
||||
"body": ["JetBrains Mono"],
|
||||
"headline": ["JetBrains Mono"],
|
||||
"label": ["JetBrains Mono"],
|
||||
"code": ["JetBrains Mono"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script type="module">var ao=Object.defineProperty;var ho=(i,e,t)=>e in i?ao(i,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):i[e]=t;var Q=(i,e,t)=>ho(i,typeof e!="symbol"?e+"":e,t);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const n of document.querySelectorAll('link[rel="modulepreload"]'))r(n);new MutationObserver(n=>{for(const o of n)if(o.type==="childList")for(const _ of o.addedNodes)_.tagName==="LINK"&&_.rel==="modulepreload"&&r(_)}).observe(document,{childList:!0,subtree:!0});function t(n){const o={};return n.integrity&&(o.integrity=n.integrity),n.referrerPolicy&&(o.referrerPolicy=n.referrerPolicy),n.crossOrigin==="use-credentials"?o.credentials="include":n.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(n){if(n.ep)return;n.ep=!0;const o=t(n);fetch(n.href,o)}})();var Hi={exports:{}},$s;function lo(){return $s||($s=1,(function(i,e){(function(t,r){i.exports=r()})(globalThis,(()=>(()=>{var t={4567:function(_,s,h){var a=this&&this.__decorate||function(l,c,v,y){var S,x=arguments.length,b=x<3?c:y===null?y=Object.getOwnPropertyDescriptor(c,v):y;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")b=Reflect.decorate(l,c,v,y);else for(var E=l.length-1;E>=0;E--)(S=l[E])&&(b=(x<3?S(b):x>3?S(c,v,b):S(c,v))||b);return x>3&&b&&Object.defineProperty(c,v,b),b},d=this&&this.__param||function(l,c){return function(v,y){c(v,y,l)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const u=h(9042),p=h(9924),m=h(844),C=h(4725),w=h(2585),f=h(3656);let g=s.AccessibilityManager=class extends m.Disposable{constructor(l,c,v,y){super(),this._terminal=l,this._coreBrowserService=v,this._renderService=y,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new p.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
|
||||
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,f.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,m.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(l){for(let c=0;c<l;c++)this._handleChar(" ")}_handleChar(l){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==l&&(this._charsToAnnounce+=l):this._charsToAnnounce+=l,l===\`
|
||||
\`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=u.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(l){this._clearLiveRegion(),new RegExp("\\\\p{Control}","u").test(l)||this._charsToConsume.push(l)}_refreshRows(l,c){this._liveRegionDebouncer.refresh(l,c,this._terminal.rows)}_renderRows(l,c){const v=this._terminal.buffer,y=v.lines.length.toString();for(let S=l;S<=c;S++){const x=v.lines.get(v.ydisp+S),b=[],E=(x==null?void 0:x.translateToString(!0,void 0,void 0,b))||"",B=(v.ydisp+S+1).toString(),T=this._rowElements[S];T&&(E.length===0?(T.innerText=" ",this._rowColumns.set(T,[0,1])):(T.textContent=E,this._rowColumns.set(T,b)),T.setAttribute("aria-posinset",B),T.setAttribute("aria-setsize",y))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(l,c){const v=l.target,y=this._rowElements[c===0?1:this._rowElements.length-2];if(v.getAttribute("aria-posinset")===(c===0?"1":\`\${this._terminal.buffer.lines.length}\`)||l.relatedTarget!==y)return;let S,x;if(c===0?(S=v,x=this._rowElements.pop(),this._rowContainer.removeChild(x)):(S=this._rowElements.shift(),x=v,this._rowContainer.removeChild(S)),S.removeEventListener("focus",this._topBoundaryFocusListener),x.removeEventListener("focus",this._bottomBoundaryFocusListener),c===0){const b=this._createAccessibilityTreeNode();this._rowElements.unshift(b),this._rowContainer.insertAdjacentElement("afterbegin",b)}else{const b=this._createAccessibilityTreeNode();this._rowElements.push(b),this._rowContainer.appendChild(b)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(c===0?-1:1),this._rowElements[c===0?1:this._rowElements.length-2].focus(),l.preventDefault(),l.stopImmediatePropagation()}_handleSelectionChange(){var E;if(this._rowElements.length===0)return;const l=document.getSelection();if(!l)return;if(l.isCollapsed)return void(this._rowContainer.contains(l.anchorNode)&&this._terminal.clearSelection());if(!l.anchorNode||!l.focusNode)return void console.error("anchorNode and/or focusNode are null");let c={node:l.anchorNode,offset:l.anchorOffset},v={node:l.focusNode,offset:l.focusOffset};if((c.node.compareDocumentPosition(v.node)&Node.DOCUMENT_POSITION_PRECEDING||c.node===v.node&&c.offset>v.offset)&&([c,v]=[v,c]),c.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(c={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(c.node))return;const y=this._rowElements.slice(-1)[0];if(v.node.compareDocumentPosition(y)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(v={node:y,offset:((E=y.textContent)==null?void 0:E.length)??0}),!this._rowContainer.contains(v.node))return;const S=({node:B,offset:T})=>{const M=B instanceof Text?B.parentNode:B;let k=parseInt(M==null?void 0:M.getAttribute("aria-posinset"),10)-1;if(isNaN(k))return console.warn("row is invalid. Race condition?"),null;const I=this._rowColumns.get(M);if(!I)return console.warn("columns is null. Race condition?"),null;let F=T<I.length?I[T]:I.slice(-1)[0]+1;return F>=this._terminal.cols&&(++k,F=0),{row:k,column:F}},x=S(c),b=S(v);if(x&&b){if(x.row>b.row||x.row===b.row&&x.column>=b.column)throw new Error("invalid range");this._terminal.select(x.column,x.row,(b.row-x.row)*this._terminal.cols-x.column+b.column)}}_handleResize(l){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let c=this._rowContainer.children.length;c<this._terminal.rows;c++)this._rowElements[c]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[c]);for(;this._rowElements.length>l;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const l=this._coreBrowserService.mainDocument.createElement("div");return l.setAttribute("role","listitem"),l.tabIndex=-1,this._refreshRowDimensions(l),l}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=\`\${this._renderService.dimensions.css.canvas.width}px\`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let l=0;l<this._terminal.rows;l++)this._refreshRowDimensions(this._rowElements[l])}}_refreshRowDimensions(l){l.style.height=\`\${this._renderService.dimensions.css.cell.height}px\`}};s.AccessibilityManager=g=a([d(1,w.IInstantiationService),d(2,C.ICoreBrowserService),d(3,C.IRenderService)],g)},3614:(_,s)=>{function h(p){return p.replace(/\\r?\\n/g,"\\Line truncated
|
||||
|
||||
WARNING: This link could potentially be dangerous\`)){const o=window.open();if(o){try{o.opener=null}catch{}o.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}}s.OscLinkProvider=v=l([d(0,u.IBufferService),d(1,u.IOptionsService),d(2,u.IOscLinkService)],v)},6193:(w,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.RenderDebouncer=void 0,s.RenderDebouncer=class{constructor(a,l){this._renderCallback=a,this._coreBrowserService=l,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(a){return this._refreshCallbacks.push(a),this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(a,l,d){this._rowCount=d,a=a!==void 0?a:0,l=l!==void 0?l:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,a):a,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,l):l,this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return void this._runRefreshCallbacks();const a=Math.max(this._rowStart,0),l=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(a,l),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const a of this._refreshCallbacks)a(0);this._refreshCallbacks=[]}}},3236:(w,s,a)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Terminal=void 0;const l=a(3614),d=a(3656),h=a(3551),u=a(9042),v=a(3730),b=a(1680),p=a(3107),t=a(5744),o=a(2950),i=a(1296),n=a(428),c=a(4269),m=a(5114),S=a(8934),y=a(3230),f=a(9312),L=a(4725),M=a(6731),T=a(8055),B=a(8969),R=a(8460),O=a(844),F=a(6114),U=a(8437),$=a(2584),W=a(7399),x=a(5941),A=a(9074),D=a(2585),P=a(5435),z=a(4567),q=a(779);class K extends B.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(I={}){super(I),this.browser=F,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new O.MutableDisposable),this._onCursorMove=this.register(new R.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new R.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new R.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new R.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new R.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new R.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new R.EventEmitter),this._onBlur=this.register(new R.EventEmitter),this._onA11yCharEmitter=this.register(new R.EventEmitter),this._onA11yTabEmitter=this.register(new R.EventEmitter),this._onWillOpen=this.register(new R.EventEmitter),this._setup(),this._decorationService=this._instantiationService.createInstance(A.DecorationService),this._instantiationService.setService(D.IDecorationService,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(q.LinkProviderService),this._instantiationService.setService(L.ILinkProviderService,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(v.OscLinkProvider)),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((E,N)=>this.refresh(E,N)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((E=>this._reportWindowsOptions(E)))),this.register(this._inputHandler.onColor((E=>this._handleColorEvent(E)))),this.register((0,R.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,R.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,R.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,R.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((E=>this._afterResize(E.cols,E.rows)))),this.register((0,O.toDisposable)((()=>{var E,N;this._customKeyEventHandler=void 0,(N=(E=this.element)==null?void 0:E.parentNode)==null||N.removeChild(this.element)})))}_handleColorEvent(I){if(this._themeService)for(const E of I){let N,H="";switch(E.index){case 256:N="foreground",H="10";break;case 257:N="background",H="11";
Line truncated
|
||||
WARNING: This link could potentially be dangerous\`)){const g=window.open();if(g){try{g.opener=null}catch{}g.location.href=f}else console.warn("Opening link blocked as opener could not be cleared")}}s.OscLinkProvider=m=a([d(0,p.IBufferService),d(1,p.IOptionsService),d(2,p.IOscLinkService)],m)},6193:(_,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.RenderDebouncer=void 0,s.RenderDebouncer=class{constructor(h,a){this._renderCallback=h,this._coreBrowserService=a,this._refreshCallbacks=[]}dispose(){this._animationFrame&&(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),this._animationFrame=void 0)}addRefreshCallback(h){return this._refreshCallbacks.push(h),this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh()))),this._animationFrame}refresh(h,a,d){this._rowCount=d,h=h!==void 0?h:0,a=a!==void 0?a:this._rowCount-1,this._rowStart=this._rowStart!==void 0?Math.min(this._rowStart,h):h,this._rowEnd=this._rowEnd!==void 0?Math.max(this._rowEnd,a):a,this._animationFrame||(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._innerRefresh())))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return void this._runRefreshCallbacks();const h=Math.max(this._rowStart,0),a=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(h,a),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(const h of this._refreshCallbacks)h(0);this._refreshCallbacks=[]}}},3236:(_,s,h)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Terminal=void 0;const a=h(3614),d=h(3656),u=h(3551),p=h(9042),m=h(3730),C=h(1680),w=h(3107),f=h(5744),g=h(2950),l=h(1296),c=h(428),v=h(4269),y=h(5114),S=h(8934),x=h(3230),b=h(9312),E=h(4725),B=h(6731),T=h(8055),M=h(8969),k=h(8460),I=h(844),F=h(6114),H=h(8437),z=h(2584),$=h(7399),L=h(5941),R=h(9074),D=h(2585),P=h(5435),U=h(4567),j=h(779);class G extends M.CoreTerminal{get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}constructor(O={}){super(O),this.browser=F,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this.register(new I.MutableDisposable),this._onCursorMove=this.register(new k.EventEmitter),this.onCursorMove=this._onCursorMove.event,this._onKey=this.register(new k.EventEmitter),this.onKey=this._onKey.event,this._onRender=this.register(new k.EventEmitter),this.onRender=this._onRender.event,this._onSelectionChange=this.register(new k.EventEmitter),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this.register(new k.EventEmitter),this.onTitleChange=this._onTitleChange.event,this._onBell=this.register(new k.EventEmitter),this.onBell=this._onBell.event,this._onFocus=this.register(new k.EventEmitter),this._onBlur=this.register(new k.EventEmitter),this._onA11yCharEmitter=this.register(new k.EventEmitter),this._onA11yTabEmitter=this.register(new k.EventEmitter),this._onWillOpen=this.register(new k.EventEmitter),this._setup(),this._decorationService=this._instantiationService.createInstance(R.DecorationService),this._instantiationService.setService(D.IDecorationService,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(j.LinkProviderService),this._instantiationService.setService(E.ILinkProviderService,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(m.OscLinkProvider)),this.register(this._inputHandler.onRequestBell((()=>this._onBell.fire()))),this.register(this._inputHandler.onRequestRefreshRows(((A,N)=>this.refresh(A,N)))),this.register(this._inputHandler.onRequestSendFocus((()=>this._reportFocus()))),this.register(this._inputHandler.onRequestReset((()=>this.reset()))),this.register(this._inputHandler.onRequestWindowsOptionsReport((A=>this._reportWindowsOptions(A)))),this.register(this._inputHandler.onColor((A=>this._handleColorEvent(A)))),this.register((0,k.forwardEvent)(this._inputHandler.onCursorMove,this._onCursorMove)),this.register((0,k.forwardEvent)(this._inputHandler.onTitleChange,this._onTitleChange)),this.register((0,k.forwardEvent)(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this.register((0,k.forwardEvent)(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this.register(this._bufferService.onResize((A=>this._afterResize(A.cols,A.rows)))),this.register((0,I.toDisposable)((()=>{var A,N;this._customKeyEventHandler=void 0,(N=(A=this.element)==null?void 0:A.parentNode)==null||N.removeChild(this.element)})))}_handleColorEvent(O){if(this._themeService)for(const A of O){let N,W="";switch(A.index){case 256:N="foreground",W="10";break;case 257:N="background",W="11";
Line truncated
|
||||
\`:\`
|
||||
\`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(f){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),o.isLinux&&f&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(f){const L=this._getMouseBufferCoords(f),M=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!!(M&&T&&L)&&this._areCoordsInSelection(L,M,T)}isCellInSelection(f,L){const M=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!(!M||!T)&&this._areCoordsInSelection([f,L],M,T)}_areCoordsInSelection(f,L,M){return f[1]>L[1]&&f[1]<M[1]||L[1]===M[1]&&f[1]===L[1]&&f[0]>=L[0]&&f[0]<M[0]||L[1]<M[1]&&f[1]===M[1]&&f[0]<M[0]||L[1]<M[1]&&f[1]===L[1]&&f[0]>=L[0]}_selectWordAtCursor(f,L){var B,R;const M=(R=(B=this._linkifier.currentLink)==null?void 0:B.link)==null?void 0:R.range;if(M)return this._model.selectionStart=[M.start.x-1,M.start.y-1],this._model.selectionStartLength=(0,i.getRangeLength)(M,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const T=this._getMouseBufferCoords(f);return!!T&&(this._selectWordAt(T,L),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(f,L){this._model.clearSelection(),f=Math.max(f,0),L=Math.min(L,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,f],this._model.selectionEnd=[this._bufferService.cols,L],this.refresh(),this._onSelectionChange.fire()}_handleTrim(f){this._model.handleTrim(f)&&this.refresh()}_getMouseBufferCoords(f){const L=this._mouseService.getCoords(f,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(L)return L[0]--,L[1]--,L[1]+=this._bufferService.buffer.ydisp,L}_getMouseEventScrollAmount(f){let L=(0,h.getCoordsRelativeToElement)(this._coreBrowserService.window,f,this._screenElement)[1];const M=this._renderService.dimensions.css.canvas.height;return L>=0&&L<=M?0:(L>M&&(L-=M),L=Math.min(Math.max(L,-50),50),L/=50,L/Math.abs(L)+Math.round(14*L))}shouldForceSelection(f){return o.isMac?f.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:f.shiftKey}handleMouseDown(f){if(this._mouseDownTimeStamp=f.timeStamp,(f.button!==2||!this.hasSelection)&&f.button===0){if(!this._enabled){if(!this.shouldForceSelection(f))return;f.stopPropagation()}f.preventDefault(),this._dragScrollAmount=0,this._enabled&&f.shiftKey?this._handleIncrementalClick(f):f.detail===1?this._handleSingleClick(f):f.detail===2?this._handleDoubleClick(f):f.detail===3&&this._handleTripleClick(f),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(f){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(f))}_handleSingleClick(f){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(f)?3:0,this._model.selectionStart=this._getMouseBufferCoords(f),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const L=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);L&&L.length!==this._model.selectionStart[0]&&L.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(f){this._selectWordAtCursor(f,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(f){const L=this._getMouseBufferCoords(f);L&&(this._activeSelectionMode=2,this._selectLineAt(L[1]))}shouldColumnSelect(f){return f.altKey&&!(o.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(f){if(f.stopImmediatePropagation(),!this._model.selectionStart)return;const L=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(f),!this._model.selectionEnd)return void this.refresh(!0);this._activeSelectionMode===2?this._model.selectionEnd[1]<this._model.selectionStart[1]?this._model.selectionEnd[0]=0:this._model.selectionEnd[0]=this._buf
Line truncated
|
||||
\`,h.VT="\\v",h.FF="\\f",h.CR="\\r",h.SO="",h.SI="",h.DLE="",h.DC1="",h.DC2="",h.DC3="",h.DC4="",h.NAK="",h.SYN="",h.ETB="",h.CAN="",h.EM="",h.SUB="",h.ESC="\\x1B",h.FS="",h.GS="",h.RS="",h.US="",h.SP=" ",h.DEL=""})(a||(s.C0=a={})),(function(h){h.PAD="",h.HOP="",h.BPH="",h.NBH="",h.IND="",h.NEL="
",h.SSA="",h.ESA="",h.HTS="",h.HTJ="",h.VTS="",h.PLD="",h.PLU="",h.RI="",h.SS2="",h.SS3="",h.DCS="",h.PU1="",h.PU2="",h.STS="",h.CCH="",h.MW="",h.SPA="",h.EPA="",h.SOS="",h.SGCI="",h.SCI="",h.CSI="",h.ST="",h.OSC="",h.PM="",h.APC=""})(l||(s.C1=l={})),(function(h){h.ST=\`\${a.ESC}\\\\\`})(d||(s.C1_ESCAPED=d={}))},7399:(w,s,a)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.evaluateKeyboardEvent=void 0;const l=a(2584),d={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["\`","~"],219:["[","{"],220:["\\\\","|"],221:["]","}"],222:["'",'"']};s.evaluateKeyboardEvent=function(h,u,v,b){const p={type:0,cancel:!1,key:void 0},t=(h.shiftKey?1:0)|(h.altKey?2:0)|(h.ctrlKey?4:0)|(h.metaKey?8:0);switch(h.keyCode){case 0:h.key==="UIKeyInputUpArrow"?p.key=u?l.C0.ESC+"OA":l.C0.ESC+"[A":h.key==="UIKeyInputLeftArrow"?p.key=u?l.C0.ESC+"OD":l.C0.ESC+"[D":h.key==="UIKeyInputRightArrow"?p.key=u?l.C0.ESC+"OC":l.C0.ESC+"[C":h.key==="UIKeyInputDownArrow"&&(p.key=u?l.C0.ESC+"OB":l.C0.ESC+"[B");break;case 8:p.key=h.ctrlKey?"\\b":l.C0.DEL,h.altKey&&(p.key=l.C0.ESC+p.key);break;case 9:if(h.shiftKey){p.key=l.C0.ESC+"[Z";break}p.key=l.C0.HT,p.cancel=!0;break;case 13:p.key=h.altKey?l.C0.ESC+l.C0.CR:l.C0.CR,p.cancel=!0;break;case 27:p.key=l.C0.ESC,h.altKey&&(p.key=l.C0.ESC+l.C0.ESC),p.cancel=!0;break;case 37:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"D",p.key===l.C0.ESC+"[1;3D"&&(p.key=l.C0.ESC+(v?"b":"[1;5D"))):p.key=u?l.C0.ESC+"OD":l.C0.ESC+"[D";break;case 39:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"C",p.key===l.C0.ESC+"[1;3C"&&(p.key=l.C0.ESC+(v?"f":"[1;5C"))):p.key=u?l.C0.ESC+"OC":l.C0.ESC+"[C";break;case 38:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"A",v||p.key!==l.C0.ESC+"[1;3A"||(p.key=l.C0.ESC+"[1;5A")):p.key=u?l.C0.ESC+"OA":l.C0.ESC+"[A";break;case 40:if(h.metaKey)break;t?(p.key=l.C0.ESC+"[1;"+(t+1)+"B",v||p.key!==l.C0.ESC+"[1;3B"||(p.key=l.C0.ESC+"[1;5B")):p.key=u?l.C0.ESC+"OB":l.C0.ESC+"[B";break;case 45:h.shiftKey||h.ctrlKey||(p.key=l.C0.ESC+"[2~");break;case 46:p.key=t?l.C0.ESC+"[3;"+(t+1)+"~":l.C0.ESC+"[3~";break;case 36:p.key=t?l.C0.ESC+"[1;"+(t+1)+"H":u?l.C0.ESC+"OH":l.C0.ESC+"[H";break;case 35:p.key=t?l.C0.ESC+"[1;"+(t+1)+"F":u?l.C0.ESC+"OF":l.C0.ESC+"[F";break;case 33:h.shiftKey?p.type=2:h.ctrlKey?p.key=l.C0.ESC+"[5;"+(t+1)+"~":p.key=l.C0.ESC+"[5~";break;case 34:h.shiftKey?p.type=3:h.ctrlKey?p.key=l.C0.ESC+"[6;"+(t+1)+"~":p.key=l.C0.ESC+"[6~";break;case 112:p.key=t?l.C0.ESC+"[1;"+(t+1)+"P":l.C0.ESC+"OP";break;case 113:p.key=t?l.C0.ESC+"[1;"+(t+1)+"Q":l.C0.ESC+"OQ";break;case 114:p.key=t?l.C0.ESC+"[1;"+(t+1)+"R":l.C0.ESC+"OR";break;case 115:p.key=t?l.C0.ESC+"[1;"+(t+1)+"S":l.C0.ESC+"OS";break;case 116:p.key=t?l.C0.ESC+"[15;"+(t+1)+"~":l.C0.ESC+"[15~";break;case 117:p.key=t?l.C0.ESC+"[17;"+(t+1)+"~":l.C0.ESC+"[17~";break;case 118:p.key=t?l.C0.ESC+"[18;"+(t+1)+"~":l.C0.ESC+"[18~";break;case 119:p.key=t?l.C0.ESC+"[19;"+(t+1)+"~":l.C0.ESC+"[19~";break;case 120:p.key=t?l.C0.ESC+"[20;"+(t+1)+"~":l.C0.ESC+"[20~";break;case 121:p.key=t?l.C0.ESC+"[21;"+(t+1)+"~":l.C0.ESC+"[21~";break;case 122:p.key=t?l.C0.ESC+"[23;"+(t+1)+"~":l.C0.ESC+"[23~";break;case 123:p.key=t?l.C0.ESC+"[24;"+(t+1)+"~":l.C0.ESC+"[24~";break;default:if(!h.ctrlKey||h.shiftKey||h.altKey||h.metaKey)if(v&&!b||!h.altKey||h.metaKey)!v||h.altKey||h.ctrlKey||h.shiftKey||!h.metaKey?h.key&&!h.ctrlKey&&!h.altKey&&!h.metaKey&&h.keyCode>=48&&h.key.length===1?p.key=h.key:h.key&&h.ctrlKey&&(h.key==="_"&&(p.key=l.C0.US),h.key==="@"&&(p.key=l.C0.NUL)):h.keyCode===65&&(p.type=1);else{const o=d[h.keyCode],i=o==null?void 0:o[h.shiftKey?1:0];if(i)p.key=l.C0.ESC+i;else if(h.keyCode>=65&&h.keyCode<=90){const n=h.ctrlKey?h.keyCode-64:h.keyCode+32;let c=String.fromCharCode(n);h.shiftKey&&(c=c.toUpperCase()),p.key=l.C0.ESC+c}else if(h.keyCode===32)p.key=l.C0.ESC+(h.ctrlKey?l.C0.NUL:" ");else if(h.key==="Dead"&&h.code.startsWith("Key")){let n=h.code.slice(3,4);h.shiftKey||(n=n.toLowerCase()),p.key=l.C0.ESC+n,p.cancel=!0}}else h.keyCode>=65&&h.keyCode<=90?p.key=String.fromCharCode(h.keyCode-64):h.keyCode===32?p.key=l.C0.NUL:h.keyCode>=51&&h.keyCode<=55?p.key=String.fromCharCode(h.keyCode-51+27):h.keyCode===56?p.key=l.C0.DEL:h.keyCode===219?p.key=l.C0.ESC:h.keyCode===220?p.key=l.C0.FS:h.keyCode===221&&(p.key=l.C0.GS)}return p}},482:(w,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Utf8ToUtf32=s.StringToUtf32=s.utf32ToString=s.stringFromCodePoint=void 0,s.stringFromCodePoint=function(a){return a>65535?(a-=65536,String.f
Line truncated
|
||||
\`,i)}paste(o){this._core.paste(o)}refresh(o,i){this._verifyIntegers(o,i),this._core.refresh(o,i)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(o){this._addonManager.loadAddon(this,o)}static get strings(){return s}_verifyIntegers(...o){for(const i of o)if(i===1/0||isNaN(i)||i%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...o){for(const i of o)if(i&&(i===1/0||isNaN(i)||i%1!=0||i<0))throw new Error("This API only accepts positive integers")}}w.Terminal=p})(),k})()))})(wt)),wt.exports}var zs=Us(),yt={exports:{}},ui;function js(){return ui||(ui=1,(function(r,e){(function(_,C){r.exports=C()})(self,(()=>(()=>{var _={};return(()=>{var C=_;Object.defineProperty(C,"__esModule",{value:!0}),C.FitAddon=void 0,C.FitAddon=class{activate(g){this._terminal=g}dispose(){}fit(){const g=this.proposeDimensions();if(!g||!this._terminal||isNaN(g.cols)||isNaN(g.rows))return;const k=this._terminal._core;this._terminal.rows===g.rows&&this._terminal.cols===g.cols||(k._renderService.clear(),this._terminal.resize(g.cols,g.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;const g=this._terminal._core,k=g._renderService.dimensions;if(k.css.cell.width===0||k.css.cell.height===0)return;const w=this._terminal.options.scrollback===0?0:g.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),a=parseInt(s.getPropertyValue("height")),l=Math.max(0,parseInt(s.getPropertyValue("width"))),d=window.getComputedStyle(this._terminal.element),h=a-(parseInt(d.getPropertyValue("padding-top"))+parseInt(d.getPropertyValue("padding-bottom"))),u=l-(parseInt(d.getPropertyValue("padding-right"))+parseInt(d.getPropertyValue("padding-left")))-w;return{cols:Math.max(2,Math.floor(u/k.css.cell.width)),rows:Math.max(1,Math.floor(h/k.css.cell.height))}}}})(),_})()))})(yt)),yt.exports}var Zs=js(),xt={exports:{}},_i;function qs(){return _i||(_i=1,(function(r,e){(function(_,C){r.exports=C()})(self,(()=>(()=>{var _={6:(w,s)=>{function a(d){try{const h=new URL(d),u=h.password&&h.username?\`\${h.protocol}//\${h.username}:\${h.password}@\${h.host}\`:h.username?\`\${h.protocol}//\${h.username}@\${h.host}\`:\`\${h.protocol}//\${h.host}\`;return d.toLocaleLowerCase().startsWith(u.toLocaleLowerCase())}catch{return!1}}Object.defineProperty(s,"__esModule",{value:!0}),s.LinkComputer=s.WebLinkProvider=void 0,s.WebLinkProvider=class{constructor(d,h,u,v={}){this._terminal=d,this._regex=h,this._handler=u,this._options=v}provideLinks(d,h){const u=l.computeLink(d,this._regex,this._terminal,this._handler);h(this._addCallbacks(u))}_addCallbacks(d){return d.map((h=>(h.leave=this._options.leave,h.hover=(u,v)=>{if(this._options.hover){const{range:b}=h;this._options.hover(u,v,b)}},h)))}};class l{static computeLink(h,u,v,b){const p=new RegExp(u.source,(u.flags||"")+"g"),[t,o]=l._getWindowedLineStrings(h-1,v),i=t.join("");let n;const c=[];for(;n=p.exec(i);){const m=n[0];if(!a(m))continue;const[S,y]=l._mapStrIdx(v,o,0,n.index),[f,L]=l._mapStrIdx(v,S,y,m.length);if(S===-1||y===-1||f===-1||L===-1)continue;const M={start:{x:y+1,y:S+1},end:{x:L,y:f+1}};c.push({range:M,text:m,activate:b})}return c}static _getWindowedLineStrings(h,u){let v,b=h,p=h,t=0,o="";const i=[];if(v=u.buffer.active.getLine(h)){const n=v.translateToString(!0);if(v.isWrapped&&n[0]!==" "){for(t=0;(v=u.buffer.active.getLine(--b))&&t<2048&&(o=v.translateToString(!0),t+=o.length,i.push(o),v.isWrapped&&o.indexOf(" ")===-1););i.reverse()}for(i.push(n),t=0;(v=u.buffer.active.getLine(++p))&&v.isWrapped&&t<2048&&(o=v.translateToString(!0),t+=o.length,i.push(o),o.indexOf(" ")===-1););}return[i,b]}static _mapStrIdx(h,u,v,b){const p=h.buffer.active,t=p.getNullCell();let o=v;for(;b;){const i=p.getLine(u);if(!i)return[-1,-1];for(let n=o;n<i.length;++n){i.getCell(n,t);const c=t.getChars();if(t.getWidth()&&(b-=c.length||1,n===i.length-1&&c==="")){const m=p.getLine(u+1);m&&m.isWrapped&&(m.getCell(0,t),t.getWidth()===2&&(b+=1))}if(b<0)return[u,n]}u++,o=0}return[u,o]}}s.LinkComputer=l}},C={};function g(w){var s=C[w];if(s!==void 0)return s.exports;var a=C[w]={exports:{}};return _[w](a,a.exports,g),a.exports}var k={};return(()=>{var w=k;Object.defineProperty(w,"__esModule",{value:!0}),w.WebLinksAddon=void 0;const s=g(6),a=/(https?|HTTPS?):[/]{2}[^\\s"'!*(){}|\\\\\\^<>\`]*[^\\s"':,.!?{}|\\\\\\^~\\[\\]\`()<>]/;function l(d,h){const u=window.open();if(u){try{u.opener=null}catch{}u.location.href=h}else console.warn("Opening link blocked as opener could not be cleared")}w.WebLinksAddon=class{constructor(d=l,h={}){this._handler=d,this._options=h}activate(d){this._terminal=d;const h=this._options,u=h.urlRegex||a;this._linkProvider=this._terminal.registerLinkProvider(new s.WebLinkProvider(this._terminal,u,this._handler,h))}dispose(){var d;(d=this._linkProvider)==null||d.dispose()}}})(),k})()))})(xt)),xt.exports}var Ks=qs();/**
|
||||
\`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(b){this._refreshAnimationFrame||(this._refreshAnimationFrame=this._coreBrowserService.window.requestAnimationFrame((()=>this._refresh()))),g.isLinux&&b&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(b){const E=this._getMouseBufferCoords(b),B=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!!(B&&T&&E)&&this._areCoordsInSelection(E,B,T)}isCellInSelection(b,E){const B=this._model.finalSelectionStart,T=this._model.finalSelectionEnd;return!(!B||!T)&&this._areCoordsInSelection([b,E],B,T)}_areCoordsInSelection(b,E,B){return b[1]>E[1]&&b[1]<B[1]||E[1]===B[1]&&b[1]===E[1]&&b[0]>=E[0]&&b[0]<B[0]||E[1]<B[1]&&b[1]===B[1]&&b[0]<B[0]||E[1]<B[1]&&b[1]===E[1]&&b[0]>=E[0]}_selectWordAtCursor(b,E){var M,k;const B=(k=(M=this._linkifier.currentLink)==null?void 0:M.link)==null?void 0:k.range;if(B)return this._model.selectionStart=[B.start.x-1,B.start.y-1],this._model.selectionStartLength=(0,l.getRangeLength)(B,this._bufferService.cols),this._model.selectionEnd=void 0,!0;const T=this._getMouseBufferCoords(b);return!!T&&(this._selectWordAt(T,E),this._model.selectionEnd=void 0,!0)}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(b,E){this._model.clearSelection(),b=Math.max(b,0),E=Math.min(E,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,b],this._model.selectionEnd=[this._bufferService.cols,E],this.refresh(),this._onSelectionChange.fire()}_handleTrim(b){this._model.handleTrim(b)&&this.refresh()}_getMouseBufferCoords(b){const E=this._mouseService.getCoords(b,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(E)return E[0]--,E[1]--,E[1]+=this._bufferService.buffer.ydisp,E}_getMouseEventScrollAmount(b){let E=(0,u.getCoordsRelativeToElement)(this._coreBrowserService.window,b,this._screenElement)[1];const B=this._renderService.dimensions.css.canvas.height;return E>=0&&E<=B?0:(E>B&&(E-=B),E=Math.min(Math.max(E,-50),50),E/=50,E/Math.abs(E)+Math.round(14*E))}shouldForceSelection(b){return g.isMac?b.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:b.shiftKey}handleMouseDown(b){if(this._mouseDownTimeStamp=b.timeStamp,(b.button!==2||!this.hasSelection)&&b.button===0){if(!this._enabled){if(!this.shouldForceSelection(b))return;b.stopPropagation()}b.preventDefault(),this._dragScrollAmount=0,this._enabled&&b.shiftKey?this._handleIncrementalClick(b):b.detail===1?this._handleSingleClick(b):b.detail===2?this._handleDoubleClick(b):b.detail===3&&this._handleTripleClick(b),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener("mouseup",this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval((()=>this._dragScroll()),50)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener("mousemove",this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener("mouseup",this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(b){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(b))}_handleSingleClick(b){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(b)?3:0,this._model.selectionStart=this._getMouseBufferCoords(b),!this._model.selectionStart)return;this._model.selectionEnd=void 0;const E=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);E&&E.length!==this._model.selectionStart[0]&&E.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(b){this._selectWordAtCursor(b,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(b){const E=this._getMouseBufferCoords(b);E&&(this._activeSelectionMode=2,this._selectLineAt(E[1]))}shouldColumnSelect(b){return b.altKey&&!(g.isMac&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(b){if(b.stopImmediatePropagation(),!this._model.selectionStart)return;const E=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(b),!this._model.selectionEnd)return void this.refresh(!0);this._activeSelectionMode===2?this._model.selectionEnd[1]<this._model.selectionStart[1]?this._model.selectionEnd[0]=0:this._model.selectionEnd[0]=this._buf
Line truncated
|
||||
\`,u.VT="\\v",u.FF="\\f",u.CR="\\r",u.SO="",u.SI="",u.DLE="",u.DC1="",u.DC2="",u.DC3="",u.DC4="",u.NAK="",u.SYN="",u.ETB="",u.CAN="",u.EM="",u.SUB="",u.ESC="\\x1B",u.FS="",u.GS="",u.RS="",u.US="",u.SP=" ",u.DEL=""})(h||(s.C0=h={})),(function(u){u.PAD="",u.HOP="",u.BPH="",u.NBH="",u.IND="",u.NEL="
",u.SSA="",u.ESA="",u.HTS="",u.HTJ="",u.VTS="",u.PLD="",u.PLU="",u.RI="",u.SS2="",u.SS3="",u.DCS="",u.PU1="",u.PU2="",u.STS="",u.CCH="",u.MW="",u.SPA="",u.EPA="",u.SOS="",u.SGCI="",u.SCI="",u.CSI="",u.ST="",u.OSC="",u.PM="",u.APC=""})(a||(s.C1=a={})),(function(u){u.ST=\`\${h.ESC}\\\\\`})(d||(s.C1_ESCAPED=d={}))},7399:(_,s,h)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.evaluateKeyboardEvent=void 0;const a=h(2584),d={48:["0",")"],49:["1","!"],50:["2","@"],51:["3","#"],52:["4","$"],53:["5","%"],54:["6","^"],55:["7","&"],56:["8","*"],57:["9","("],186:[";",":"],187:["=","+"],188:[",","<"],189:["-","_"],190:[".",">"],191:["/","?"],192:["\`","~"],219:["[","{"],220:["\\\\","|"],221:["]","}"],222:["'",'"']};s.evaluateKeyboardEvent=function(u,p,m,C){const w={type:0,cancel:!1,key:void 0},f=(u.shiftKey?1:0)|(u.altKey?2:0)|(u.ctrlKey?4:0)|(u.metaKey?8:0);switch(u.keyCode){case 0:u.key==="UIKeyInputUpArrow"?w.key=p?a.C0.ESC+"OA":a.C0.ESC+"[A":u.key==="UIKeyInputLeftArrow"?w.key=p?a.C0.ESC+"OD":a.C0.ESC+"[D":u.key==="UIKeyInputRightArrow"?w.key=p?a.C0.ESC+"OC":a.C0.ESC+"[C":u.key==="UIKeyInputDownArrow"&&(w.key=p?a.C0.ESC+"OB":a.C0.ESC+"[B");break;case 8:w.key=u.ctrlKey?"\\b":a.C0.DEL,u.altKey&&(w.key=a.C0.ESC+w.key);break;case 9:if(u.shiftKey){w.key=a.C0.ESC+"[Z";break}w.key=a.C0.HT,w.cancel=!0;break;case 13:w.key=u.altKey?a.C0.ESC+a.C0.CR:a.C0.CR,w.cancel=!0;break;case 27:w.key=a.C0.ESC,u.altKey&&(w.key=a.C0.ESC+a.C0.ESC),w.cancel=!0;break;case 37:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"D",w.key===a.C0.ESC+"[1;3D"&&(w.key=a.C0.ESC+(m?"b":"[1;5D"))):w.key=p?a.C0.ESC+"OD":a.C0.ESC+"[D";break;case 39:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"C",w.key===a.C0.ESC+"[1;3C"&&(w.key=a.C0.ESC+(m?"f":"[1;5C"))):w.key=p?a.C0.ESC+"OC":a.C0.ESC+"[C";break;case 38:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"A",m||w.key!==a.C0.ESC+"[1;3A"||(w.key=a.C0.ESC+"[1;5A")):w.key=p?a.C0.ESC+"OA":a.C0.ESC+"[A";break;case 40:if(u.metaKey)break;f?(w.key=a.C0.ESC+"[1;"+(f+1)+"B",m||w.key!==a.C0.ESC+"[1;3B"||(w.key=a.C0.ESC+"[1;5B")):w.key=p?a.C0.ESC+"OB":a.C0.ESC+"[B";break;case 45:u.shiftKey||u.ctrlKey||(w.key=a.C0.ESC+"[2~");break;case 46:w.key=f?a.C0.ESC+"[3;"+(f+1)+"~":a.C0.ESC+"[3~";break;case 36:w.key=f?a.C0.ESC+"[1;"+(f+1)+"H":p?a.C0.ESC+"OH":a.C0.ESC+"[H";break;case 35:w.key=f?a.C0.ESC+"[1;"+(f+1)+"F":p?a.C0.ESC+"OF":a.C0.ESC+"[F";break;case 33:u.shiftKey?w.type=2:u.ctrlKey?w.key=a.C0.ESC+"[5;"+(f+1)+"~":w.key=a.C0.ESC+"[5~";break;case 34:u.shiftKey?w.type=3:u.ctrlKey?w.key=a.C0.ESC+"[6;"+(f+1)+"~":w.key=a.C0.ESC+"[6~";break;case 112:w.key=f?a.C0.ESC+"[1;"+(f+1)+"P":a.C0.ESC+"OP";break;case 113:w.key=f?a.C0.ESC+"[1;"+(f+1)+"Q":a.C0.ESC+"OQ";break;case 114:w.key=f?a.C0.ESC+"[1;"+(f+1)+"R":a.C0.ESC+"OR";break;case 115:w.key=f?a.C0.ESC+"[1;"+(f+1)+"S":a.C0.ESC+"OS";break;case 116:w.key=f?a.C0.ESC+"[15;"+(f+1)+"~":a.C0.ESC+"[15~";break;case 117:w.key=f?a.C0.ESC+"[17;"+(f+1)+"~":a.C0.ESC+"[17~";break;case 118:w.key=f?a.C0.ESC+"[18;"+(f+1)+"~":a.C0.ESC+"[18~";break;case 119:w.key=f?a.C0.ESC+"[19;"+(f+1)+"~":a.C0.ESC+"[19~";break;case 120:w.key=f?a.C0.ESC+"[20;"+(f+1)+"~":a.C0.ESC+"[20~";break;case 121:w.key=f?a.C0.ESC+"[21;"+(f+1)+"~":a.C0.ESC+"[21~";break;case 122:w.key=f?a.C0.ESC+"[23;"+(f+1)+"~":a.C0.ESC+"[23~";break;case 123:w.key=f?a.C0.ESC+"[24;"+(f+1)+"~":a.C0.ESC+"[24~";break;default:if(!u.ctrlKey||u.shiftKey||u.altKey||u.metaKey)if(m&&!C||!u.altKey||u.metaKey)!m||u.altKey||u.ctrlKey||u.shiftKey||!u.metaKey?u.key&&!u.ctrlKey&&!u.altKey&&!u.metaKey&&u.keyCode>=48&&u.key.length===1?w.key=u.key:u.key&&u.ctrlKey&&(u.key==="_"&&(w.key=a.C0.US),u.key==="@"&&(w.key=a.C0.NUL)):u.keyCode===65&&(w.type=1);else{const g=d[u.keyCode],l=g==null?void 0:g[u.shiftKey?1:0];if(l)w.key=a.C0.ESC+l;else if(u.keyCode>=65&&u.keyCode<=90){const c=u.ctrlKey?u.keyCode-64:u.keyCode+32;let v=String.fromCharCode(c);u.shiftKey&&(v=v.toUpperCase()),w.key=a.C0.ESC+v}else if(u.keyCode===32)w.key=a.C0.ESC+(u.ctrlKey?a.C0.NUL:" ");else if(u.key==="Dead"&&u.code.startsWith("Key")){let c=u.code.slice(3,4);u.shiftKey||(c=c.toLowerCase()),w.key=a.C0.ESC+c,w.cancel=!0}}else u.keyCode>=65&&u.keyCode<=90?w.key=String.fromCharCode(u.keyCode-64):u.keyCode===32?w.key=a.C0.NUL:u.keyCode>=51&&u.keyCode<=55?w.key=String.fromCharCode(u.keyCode-51+27):u.keyCode===56?w.key=a.C0.DEL:u.keyCode===219?w.key=a.C0.ESC:u.keyCode===220?w.key=a.C0.FS:u.keyCode===221&&(w.key=a.C0.GS)}return w}},482:(_,s)=>{Object.defineProperty(s,"__esModule",{value:!0}),s.Utf8ToUtf32=s.StringToUtf32=s.utf32ToString=s.stringFromCodePoint=void 0,s.stringFromCodePoint=function(h){return h>65535?(h-=65536,String.f
Line truncated
|
||||
\`,l)}paste(g){this._core.paste(g)}refresh(g,l){this._verifyIntegers(g,l),this._core.refresh(g,l)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(g){this._addonManager.loadAddon(this,g)}static get strings(){return s}_verifyIntegers(...g){for(const l of g)if(l===1/0||isNaN(l)||l%1!=0)throw new Error("This API only accepts integers")}_verifyPositiveIntegers(...g){for(const l of g)if(l&&(l===1/0||isNaN(l)||l%1!=0||l<0))throw new Error("This API only accepts positive integers")}}_.Terminal=w})(),o})()))})(Hi)),Hi.exports}var co=lo(),zi={exports:{}},Hs;function uo(){return Hs||(Hs=1,(function(i,e){(function(t,r){i.exports=r()})(self,(()=>(()=>{var t={};return(()=>{var r=t;Object.defineProperty(r,"__esModule",{value:!0}),r.FitAddon=void 0,r.FitAddon=class{activate(n){this._terminal=n}dispose(){}fit(){const n=this.proposeDimensions();if(!n||!this._terminal||isNaN(n.cols)||isNaN(n.rows))return;const o=this._terminal._core;this._terminal.rows===n.rows&&this._terminal.cols===n.cols||(o._renderService.clear(),this._terminal.resize(n.cols,n.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;const n=this._terminal._core,o=n._renderService.dimensions;if(o.css.cell.width===0||o.css.cell.height===0)return;const _=this._terminal.options.scrollback===0?0:n.viewport.scrollBarWidth,s=window.getComputedStyle(this._terminal.element.parentElement),h=parseInt(s.getPropertyValue("height")),a=Math.max(0,parseInt(s.getPropertyValue("width"))),d=window.getComputedStyle(this._terminal.element),u=h-(parseInt(d.getPropertyValue("padding-top"))+parseInt(d.getPropertyValue("padding-bottom"))),p=a-(parseInt(d.getPropertyValue("padding-right"))+parseInt(d.getPropertyValue("padding-left")))-_;return{cols:Math.max(2,Math.floor(p/o.css.cell.width)),rows:Math.max(1,Math.floor(u/o.css.cell.height))}}}})(),t})()))})(zi)),zi.exports}var fo=uo(),Wi={exports:{}},zs;function _o(){return zs||(zs=1,(function(i,e){(function(t,r){i.exports=r()})(self,(()=>(()=>{var t={6:(_,s)=>{function h(d){try{const u=new URL(d),p=u.password&&u.username?\`\${u.protocol}//\${u.username}:\${u.password}@\${u.host}\`:u.username?\`\${u.protocol}//\${u.username}@\${u.host}\`:\`\${u.protocol}//\${u.host}\`;return d.toLocaleLowerCase().startsWith(p.toLocaleLowerCase())}catch{return!1}}Object.defineProperty(s,"__esModule",{value:!0}),s.LinkComputer=s.WebLinkProvider=void 0,s.WebLinkProvider=class{constructor(d,u,p,m={}){this._terminal=d,this._regex=u,this._handler=p,this._options=m}provideLinks(d,u){const p=a.computeLink(d,this._regex,this._terminal,this._handler);u(this._addCallbacks(p))}_addCallbacks(d){return d.map((u=>(u.leave=this._options.leave,u.hover=(p,m)=>{if(this._options.hover){const{range:C}=u;this._options.hover(p,m,C)}},u)))}};class a{static computeLink(u,p,m,C){const w=new RegExp(p.source,(p.flags||"")+"g"),[f,g]=a._getWindowedLineStrings(u-1,m),l=f.join("");let c;const v=[];for(;c=w.exec(l);){const y=c[0];if(!h(y))continue;const[S,x]=a._mapStrIdx(m,g,0,c.index),[b,E]=a._mapStrIdx(m,S,x,y.length);if(S===-1||x===-1||b===-1||E===-1)continue;const B={start:{x:x+1,y:S+1},end:{x:E,y:b+1}};v.push({range:B,text:y,activate:C})}return v}static _getWindowedLineStrings(u,p){let m,C=u,w=u,f=0,g="";const l=[];if(m=p.buffer.active.getLine(u)){const c=m.translateToString(!0);if(m.isWrapped&&c[0]!==" "){for(f=0;(m=p.buffer.active.getLine(--C))&&f<2048&&(g=m.translateToString(!0),f+=g.length,l.push(g),m.isWrapped&&g.indexOf(" ")===-1););l.reverse()}for(l.push(c),f=0;(m=p.buffer.active.getLine(++w))&&m.isWrapped&&f<2048&&(g=m.translateToString(!0),f+=g.length,l.push(g),g.indexOf(" ")===-1););}return[l,C]}static _mapStrIdx(u,p,m,C){const w=u.buffer.active,f=w.getNullCell();let g=m;for(;C;){const l=w.getLine(p);if(!l)return[-1,-1];for(let c=g;c<l.length;++c){l.getCell(c,f);const v=f.getChars();if(f.getWidth()&&(C-=v.length||1,c===l.length-1&&v==="")){const y=w.getLine(p+1);y&&y.isWrapped&&(y.getCell(0,f),f.getWidth()===2&&(C+=1))}if(C<0)return[p,c]}p++,g=0}return[p,g]}}s.LinkComputer=a}},r={};function n(_){var s=r[_];if(s!==void 0)return s.exports;var h=r[_]={exports:{}};return t[_](h,h.exports,n),h.exports}var o={};return(()=>{var _=o;Object.defineProperty(_,"__esModule",{value:!0}),_.WebLinksAddon=void 0;const s=n(6),h=/(https?|HTTPS?):[/]{2}[^\\s"'!*(){}|\\\\\\^<>\`]*[^\\s"':,.!?{}|\\\\\\^~\\[\\]\`()<>]/;function a(d,u){const p=window.open();if(p){try{p.opener=null}catch{}p.location.href=u}else console.warn("Opening link blocked as opener could not be cleared")}_.WebLinksAddon=class{constructor(d=a,u={}){this._handler=d,this._options=u}activate(d){this._terminal=d;const u=this._options,p=u.urlRegex||h;this._linkProvider=this._terminal.registerLinkProvider(new s.WebLinkProvider(this._terminal,p,this._handler,u))}dispose(){var d;(d=this._linkProvider)==null||d.dispose()}}})(),o})()))})(Wi)),Wi.exports}var go=_o();/**
|
||||
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
@@ -60,13 +60,13 @@ WARNING: This link could potentially be dangerous\`)){const o=window.open();if(o
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
*/var Vs=(r,e,_,C)=>{for(var g=e,k=r.length-1,w;k>=0;k--)(w=r[k])&&(g=w(g)||g);return g},Gs=(r,e)=>(_,C)=>e(_,C,r),Xs=class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(r){setTimeout(()=>{throw r.stack?fi.isErrorNoTelemetry(r)?new fi(r.message+\`
|
||||
*/var po=(i,e,t,r)=>{for(var n=e,o=i.length-1,_;o>=0;o--)(_=i[o])&&(n=_(n)||n);return n},vo=(i,e)=>(t,r)=>e(t,r,i),mo=class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(e){setTimeout(()=>{throw e.stack?Ws.isErrorNoTelemetry(e)?new Ws(e.message+\`
|
||||
|
||||
\`+r.stack):new Error(r.message+\`
|
||||
\`+e.stack):new Error(e.message+\`
|
||||
|
||||
\`+r.stack):r},0)}}addListener(r){return this.listeners.push(r),()=>{this._removeListener(r)}}emit(r){this.listeners.forEach(e=>{e(r)})}_removeListener(r){this.listeners.splice(this.listeners.indexOf(r),1)}setUnexpectedErrorHandler(r){this.unexpectedErrorHandler=r}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(r){this.unexpectedErrorHandler(r),this.emit(r)}onUnexpectedExternalError(r){this.unexpectedErrorHandler(r)}},Ys=new Xs;function Et(r){Js(r)||Ys.onUnexpectedError(r)}var Jt="Canceled";function Js(r){return r instanceof Qs?!0:r instanceof Error&&r.name===Jt&&r.message===Jt}var Qs=class extends Error{constructor(){super(Jt),this.name=this.message}},fi=class Qt extends Error{constructor(e){super(e),this.name="CodeExpectedError"}static fromError(e){if(e instanceof Qt)return e;let _=new Qt;return _.message=e.message,_.stack=e.stack,_}static isErrorNoTelemetry(e){return e.name==="CodeExpectedError"}},er;(r=>{function e(k){return k<0}r.isLessThan=e;function _(k){return k<=0}r.isLessThanOrEqual=_;function C(k){return k>0}r.isGreaterThan=C;function g(k){return k===0}r.isNeitherLessOrGreaterThan=g,r.greaterThan=1,r.lessThan=-1,r.neitherLessOrGreaterThan=0})(er||(er={}));function tr(r,e){let _=this,C=!1,g;return function(){return C||(C=!0,e||(g=r.apply(_,arguments))),g}}var ls;(r=>{function e(c){return c&&typeof c=="object"&&typeof c[Symbol.iterator]=="function"}r.is=e;let _=Object.freeze([]);function C(){return _}r.empty=C;function*g(c){yield c}r.single=g;function k(c){return e(c)?c:g(c)}r.wrap=k;function w(c){return c||_}r.from=w;function*s(c){for(let m=c.length-1;m>=0;m--)yield c[m]}r.reverse=s;function a(c){return!c||c[Symbol.iterator]().next().done===!0}r.isEmpty=a;function l(c){return c[Symbol.iterator]().next().value}r.first=l;function d(c,m){let S=0;for(let y of c)if(m(y,S++))return!0;return!1}r.some=d;function h(c,m){for(let S of c)if(m(S))return S}r.find=h;function*u(c,m){for(let S of c)m(S)&&(yield S)}r.filter=u;function*v(c,m){let S=0;for(let y of c)yield m(y,S++)}r.map=v;function*b(c,m){let S=0;for(let y of c)yield*m(y,S++)}r.flatMap=b;function*p(...c){for(let m of c)yield*m}r.concat=p;function t(c,m,S){let y=S;for(let f of c)y=m(y,f);return y}r.reduce=t;function*o(c,m,S=c.length){for(m<0&&(m+=c.length),S<0?S+=c.length:S>c.length&&(S=c.length);m<S;m++)yield c[m]}r.slice=o;function i(c,m=Number.POSITIVE_INFINITY){let S=[];if(m===0)return[S,c];let y=c[Symbol.iterator]();for(let f=0;f<m;f++){let L=y.next();if(L.done)return[S,r.empty()];S.push(L.value)}return[S,{[Symbol.iterator](){return y}}]}r.consume=i;async function n(c){let m=[];for await(let S of c)m.push(S);return Promise.resolve(m)}r.asyncToArray=n})(ls||(ls={}));function cs(r){if(ls.is(r)){let e=[];for(let _ of r)if(_)try{_.dispose()}catch(C){e.push(C)}if(e.length===1)throw e[0];if(e.length>1)throw new AggregateError(e,"Encountered errors while disposing of store");return Array.isArray(r)?[]:r}else if(r)return r.dispose(),r}function ds(...r){return _e(()=>cs(r))}function _e(r){return{dispose:tr(()=>{r()})}}var us=class _s{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{cs(this._toDispose)}finally{this._toDispose.clear()}}add(e){if(!e)return e;if(e===this)throw new Error("Cannot register a disposable on itself!");return this._isDisposed?_s.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(e),e}delete(e){if(e){if(e===this)throw new Error("Cannot dispose a disposable on itself!");this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),void 0)}};us.DISABLE_DISPOSED_WARNING=!1;var Ve=us,we=class{constructor(){this._store=new Ve,this._store}dispose(){this._store.dispose()}_register(r){if(r===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(r)}};we.None=Object.freeze({dispose(){}});var et=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(r){var e;this._isDisposed||r===this._value||((e=this._value)==null||e.dispose(),this._value=r)}clear(){this.value=void 0}dispose(){var r;this._isDisposed=!0,(r=this._value)==null||r.dispose(),this._value=void 0}clearAndLeak(){let r=this._value;return this._value=void 0,r}},oi=typeof process<"u"&&"title"in process,St=oi?"node":navigator.userAgent,ir=oi?"node":navigator.platform,sr=St.includes("Firefox"),rr=St.includes("Edge"),fs=/^((?!chrome|android).)*safari/i.test(St);function nr(){if(!fs)return 0;let r=St.match(/Version\\/(\\d+)/);return r===null||r.length<2?0:parseInt(r[1])}ir.indexOf("Linux")>=0;var or="",ae=0,he=0,le=0,re=0,me={css:"#00000000",rgba:0},pe;(r=>{function e(g,k,w,s){return s!==void 0?\`#\${He(g)}\${He(k)}\${He(w)}\${He(s)}\`:\`#\Line truncated
|
||||
\`+e.stack):e},0)}}addListener(e){return this.listeners.push(e),()=>{this._removeListener(e)}}emit(e){this.listeners.forEach(t=>{t(e)})}_removeListener(e){this.listeners.splice(this.listeners.indexOf(e),1)}setUnexpectedErrorHandler(e){this.unexpectedErrorHandler=e}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(e){this.unexpectedErrorHandler(e),this.emit(e)}onUnexpectedExternalError(e){this.unexpectedErrorHandler(e)}},wo=new mo;function Ui(i){bo(i)||wo.onUnexpectedError(i)}var ws="Canceled";function bo(i){return i instanceof So?!0:i instanceof Error&&i.name===ws&&i.message===ws}var So=class extends Error{constructor(){super(ws),this.name=this.message}},Ws=class bs extends Error{constructor(e){super(e),this.name="CodeExpectedError"}static fromError(e){if(e instanceof bs)return e;let t=new bs;return t.message=e.message,t.stack=e.stack,t}static isErrorNoTelemetry(e){return e.name==="CodeExpectedError"}},yo;(i=>{function e(o){return o<0}i.isLessThan=e;function t(o){return o<=0}i.isLessThanOrEqual=t;function r(o){return o>0}i.isGreaterThan=r;function n(o){return o===0}i.isNeitherLessOrGreaterThan=n,i.greaterThan=1,i.lessThan=-1,i.neitherLessOrGreaterThan=0})(yo||(yo={}));function Co(i,e){let t=this,r=!1,n;return function(){return r||(r=!0,e||(n=i.apply(t,arguments))),n}}var sn;(i=>{function e(v){return v&&typeof v=="object"&&typeof v[Symbol.iterator]=="function"}i.is=e;let t=Object.freeze([]);function r(){return t}i.empty=r;function*n(v){yield v}i.single=n;function o(v){return e(v)?v:n(v)}i.wrap=o;function _(v){return v||t}i.from=_;function*s(v){for(let y=v.length-1;y>=0;y--)yield v[y]}i.reverse=s;function h(v){return!v||v[Symbol.iterator]().next().done===!0}i.isEmpty=h;function a(v){return v[Symbol.iterator]().next().value}i.first=a;function d(v,y){let S=0;for(let x of v)if(y(x,S++))return!0;return!1}i.some=d;function u(v,y){for(let S of v)if(y(S))return S}i.find=u;function*p(v,y){for(let S of v)y(S)&&(yield S)}i.filter=p;function*m(v,y){let S=0;for(let x of v)yield y(x,S++)}i.map=m;function*C(v,y){let S=0;for(let x of v)yield*y(x,S++)}i.flatMap=C;function*w(...v){for(let y of v)yield*y}i.concat=w;function f(v,y,S){let x=S;for(let b of v)x=y(x,b);return x}i.reduce=f;function*g(v,y,S=v.length){for(y<0&&(y+=v.length),S<0?S+=v.length:S>v.length&&(S=v.length);y<S;y++)yield v[y]}i.slice=g;function l(v,y=Number.POSITIVE_INFINITY){let S=[];if(y===0)return[S,v];let x=v[Symbol.iterator]();for(let b=0;b<y;b++){let E=x.next();if(E.done)return[S,i.empty()];S.push(E.value)}return[S,{[Symbol.iterator](){return x}}]}i.consume=l;async function c(v){let y=[];for await(let S of v)y.push(S);return Promise.resolve(y)}i.asyncToArray=c})(sn||(sn={}));function rn(i){if(sn.is(i)){let e=[];for(let t of i)if(t)try{t.dispose()}catch(r){e.push(r)}if(e.length===1)throw e[0];if(e.length>1)throw new AggregateError(e,"Encountered errors while disposing of store");return Array.isArray(i)?[]:i}else if(i)return i.dispose(),i}function nn(...i){return Se(()=>rn(i))}function Se(i){return{dispose:Co(()=>{i()})}}var on=class an{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{rn(this._toDispose)}finally{this._toDispose.clear()}}add(e){if(!e)return e;if(e===this)throw new Error("Cannot register a disposable on itself!");return this._isDisposed?an.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(e),e}delete(e){if(e){if(e===this)throw new Error("Cannot dispose a disposable on itself!");this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),void 0)}};on.DISABLE_DISPOSED_WARNING=!1;var kt=on,Pe=class{constructor(){this._store=new kt,this._store}dispose(){this._store.dispose()}_register(e){if(e===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(e)}};Pe.None=Object.freeze({dispose(){}});var Ot=class{constructor(){this._isDisposed=!1}get value(){return this._isDisposed?void 0:this._value}set value(e){var t;this._isDisposed||e===this._value||((t=this._value)==null||t.dispose(),this._value=e)}clear(){this.value=void 0}dispose(){var e;this._isDisposed=!0,(e=this._value)==null||e.dispose(),this._value=void 0}clearAndLeak(){let e=this._value;return this._value=void 0,e}},Ts=typeof process<"u"&&"title"in process,Ii=Ts?"node":navigator.userAgent,xo=Ts?"node":navigator.platform,Eo=Ii.includes("Firefox"),Lo=Ii.includes("Edge"),hn=/^((?!chrome|android).)*safari/i.test(Ii);function ko(){if(!hn)return 0;let i=Ii.match(/Version\\/(\\d+)/);return i===null||i.length<2?0:parseInt(i[1])}xo.indexOf("Linux")>=0;var Ao="",_e=0,ge=0,pe=0,le=0,Re={css:"#00000000",rgba:0},Ce;(i=>{function e(n,o,_,s){return s!==void 0?\`#\${lt(n)}\${lt(o)}\${lt(_)}\${lt(s)}\`:\`#\Line truncated
|
||||
\`).slice(2).join(\`
|
||||
\`))}},Ir=class extends Error{constructor(r,e){super(r),this.name="ListenerLeakError",this.stack=e}},Or=class extends Error{constructor(r,e){super(r),this.name="ListenerRefusalError",this.stack=e}},Pr=0,kt=class{constructor(r){this.value=r,this.id=Pr++}},Fr=2,Hr,ee=class{constructor(r){var e,_,C,g;this._size=0,this._options=r,this._leakageMon=(e=this._options)!=null&&e.leakWarningThreshold?new Tr((r==null?void 0:r.onListenerError)??Et,((_=this._options)==null?void 0:_.leakWarningThreshold)??Br):void 0,this._perfMon=(C=this._options)!=null&&C._profName?new Mr(this._options._profName):void 0,this._deliveryQueue=(g=this._options)==null?void 0:g.deliveryQueue}dispose(){var r,e,_,C;this._disposed||(this._disposed=!0,((r=this._deliveryQueue)==null?void 0:r.current)===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),(_=(e=this._options)==null?void 0:e.onDidRemoveLastListener)==null||_.call(e),(C=this._leakageMon)==null||C.dispose())}get event(){return this._event??(this._event=(r,e,_)=>{var w,s,a,l,d;if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let h=\`[\${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (\${this._size} vs \${this._leakageMon.threshold})\`;console.warn(h);let u=this._leakageMon.getMostFrequentStack()??["UNKNOWN stack",-1],v=new Or(\`\${h}. HINT: Stack shows most frequent listener (\${u[1]}-times)\`,u[0]);return(((w=this._options)==null?void 0:w.onListenerError)||Et)(v),we.None}if(this._disposed)return we.None;e&&(r=r.bind(e));let C=new kt(r),g;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(C.stack=Dr.create(),g=this._leakageMon.check(C.stack,this._size+1)),this._listeners?this._listeners instanceof kt?(this._deliveryQueue??(this._deliveryQueue=new Nr),this._listeners=[this._listeners,C]):this._listeners.push(C):((a=(s=this._options)==null?void 0:s.onWillAddFirstListener)==null||a.call(s,this),this._listeners=C,(d=(l=this._options)==null?void 0:l.onDidAddFirstListener)==null||d.call(l,this)),this._size++;let k=_e(()=>{g==null||g(),this._removeListener(C)});return _ instanceof Ve?_.add(k):Array.isArray(_)&&_.push(k),k}),this._event}_removeListener(r){var g,k,w,s;if((k=(g=this._options)==null?void 0:g.onWillRemoveListener)==null||k.call(g,this),!this._listeners)return;if(this._size===1){this._listeners=void 0,(s=(w=this._options)==null?void 0:w.onDidRemoveLastListener)==null||s.call(w,this),this._size=0;return}let e=this._listeners,_=e.indexOf(r);if(_===-1)throw console.log("disposed?",this._disposed),console.log("size?",this._size),console.log("arr?",JSON.stringify(this._listeners)),new Error("Attempted to dispose unknown listener");this._size--,e[_]=void 0;let C=this._deliveryQueue.current===this;if(this._size*Fr<=e.length){let a=0;for(let l=0;l<e.length;l++)e[l]?e[a++]=e[l]:C&&(this._deliveryQueue.end--,a<this._deliveryQueue.i&&this._deliveryQueue.i--);e.length=a}}_deliver(r,e){var C;if(!r)return;let _=((C=this._options)==null?void 0:C.onListenerError)||Et;if(!_){r.value(e);return}try{r.value(e)}catch(g){_(g)}}_deliverQueue(r){let e=r.current._listeners;for(;r.i<r.end;)this._deliver(e[r.i++],r.value);r.reset()}fire(r){var e,_,C,g;if((e=this._deliveryQueue)!=null&&e.current&&(this._deliverQueue(this._deliveryQueue),(_=this._perfMon)==null||_.stop()),(C=this._perfMon)==null||C.start(this._size),this._listeners)if(this._listeners instanceof kt)this._deliver(this._listeners,r);else{let k=this._deliveryQueue;k.enqueue(this,r,this._listeners.length),this._deliverQueue(k)}(g=this._perfMon)==null||g.stop()}hasListeners(){return this._size>0}},Nr=class{constructor(){this.i=-1,this.end=0}enqueue(r,e,_){this.i=0,this.end=_,this.current=r,this.value=e}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},Si={texturePage:0,texturePosition:{x:0,y:0},texturePositionClipSpace:{x:0,y:0},offset:{x:0,y:0},size:{x:0,y:0},sizeClipSpace:{x:0,y:0}},tt=2,it,Pe=class Ze{constructor(e,_,C){this._document=e,this._config=_,this._unicodeService=C,this._didWarmUp=!1,this._cacheMap=new mi,this._cacheMapCombined=new mi,this._pages=[],this._activePages=[],this._workBoundingBox={top:0,left:0,bottom:0,right:0},this._workAttributeData=new qe,this._textureSize=512,this._onAddTextureAtlasCanvas=new ee,this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=new ee,this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._requestClearModel=!1,this._createNewPage(),this._tmpCanvas=ks(e,this._config.deviceCellWidth*4+tt*2,this._config.deviceCellHeight+tt*2),this._tmpCtx=ne(this._tmpCanvas.getContext("2d",{alpha:this._config.allowTransparency,willReadFrequently:!0}))}get pages(){return this._pages}dispose(){this._tmpCanvas.remove();for(let e of this.pages)e.canvas.remove();this._onAddTextureAtlasCanvas.dispose()}warmUp(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=!0)}_doWarmUp(){let e=new kr;for(let _=33;_<126;_++)e.enqueue(()=>{if
Line truncated
|
||||
\`))}},ia=class extends Error{constructor(e,t){super(e),this.name="ListenerLeakError",this.stack=t}},sa=class extends Error{constructor(e,t){super(e),this.name="ListenerRefusalError",this.stack=t}},ra=0,ji=class{constructor(e){this.value=e,this.id=ra++}},na=2,oa,re=class{constructor(e){var t,r,n,o;this._size=0,this._options=e,this._leakageMon=(t=this._options)!=null&&t.leakWarningThreshold?new ea((e==null?void 0:e.onListenerError)??Ui,((r=this._options)==null?void 0:r.leakWarningThreshold)??Qo):void 0,this._perfMon=(n=this._options)!=null&&n._profName?new Jo(this._options._profName):void 0,this._deliveryQueue=(o=this._options)==null?void 0:o.deliveryQueue}dispose(){var e,t,r,n;this._disposed||(this._disposed=!0,((e=this._deliveryQueue)==null?void 0:e.current)===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),(r=(t=this._options)==null?void 0:t.onDidRemoveLastListener)==null||r.call(t),(n=this._leakageMon)==null||n.dispose())}get event(){return this._event??(this._event=(e,t,r)=>{var s,h,a,d,u;if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let p=\`[\${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (\${this._size} vs \${this._leakageMon.threshold})\`;console.warn(p);let m=this._leakageMon.getMostFrequentStack()??["UNKNOWN stack",-1],C=new sa(\`\${p}. HINT: Stack shows most frequent listener (\${m[1]}-times)\`,m[0]);return(((s=this._options)==null?void 0:s.onListenerError)||Ui)(C),Pe.None}if(this._disposed)return Pe.None;t&&(e=e.bind(t));let n=new ji(e),o;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(n.stack=ta.create(),o=this._leakageMon.check(n.stack,this._size+1)),this._listeners?this._listeners instanceof ji?(this._deliveryQueue??(this._deliveryQueue=new aa),this._listeners=[this._listeners,n]):this._listeners.push(n):((a=(h=this._options)==null?void 0:h.onWillAddFirstListener)==null||a.call(h,this),this._listeners=n,(u=(d=this._options)==null?void 0:d.onDidAddFirstListener)==null||u.call(d,this)),this._size++;let _=Se(()=>{o==null||o(),this._removeListener(n)});return r instanceof kt?r.add(_):Array.isArray(r)&&r.push(_),_}),this._event}_removeListener(e){var o,_,s,h;if((_=(o=this._options)==null?void 0:o.onWillRemoveListener)==null||_.call(o,this),!this._listeners)return;if(this._size===1){this._listeners=void 0,(h=(s=this._options)==null?void 0:s.onDidRemoveLastListener)==null||h.call(s,this),this._size=0;return}let t=this._listeners,r=t.indexOf(e);if(r===-1)throw console.log("disposed?",this._disposed),console.log("size?",this._size),console.log("arr?",JSON.stringify(this._listeners)),new Error("Attempted to dispose unknown listener");this._size--,t[r]=void 0;let n=this._deliveryQueue.current===this;if(this._size*na<=t.length){let a=0;for(let d=0;d<t.length;d++)t[d]?t[a++]=t[d]:n&&(this._deliveryQueue.end--,a<this._deliveryQueue.i&&this._deliveryQueue.i--);t.length=a}}_deliver(e,t){var n;if(!e)return;let r=((n=this._options)==null?void 0:n.onListenerError)||Ui;if(!r){e.value(t);return}try{e.value(t)}catch(o){r(o)}}_deliverQueue(e){let t=e.current._listeners;for(;e.i<e.end;)this._deliver(t[e.i++],e.value);e.reset()}fire(e){var t,r,n,o;if((t=this._deliveryQueue)!=null&&t.current&&(this._deliverQueue(this._deliveryQueue),(r=this._perfMon)==null||r.stop()),(n=this._perfMon)==null||n.start(this._size),this._listeners)if(this._listeners instanceof ji)this._deliver(this._listeners,e);else{let _=this._deliveryQueue;_.enqueue(this,e,this._listeners.length),this._deliverQueue(_)}(o=this._perfMon)==null||o.stop()}hasListeners(){return this._size>0}},aa=class{constructor(){this.i=-1,this.end=0}enqueue(e,t,r){this.i=0,this.end=r,this.current=e,this.value=t}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},Ks={texturePage:0,texturePosition:{x:0,y:0},texturePositionClipSpace:{x:0,y:0},offset:{x:0,y:0},size:{x:0,y:0},sizeClipSpace:{x:0,y:0}},Ft=2,$t,st=class Ct{constructor(e,t,r){this._document=e,this._config=t,this._unicodeService=r,this._didWarmUp=!1,this._cacheMap=new qs,this._cacheMapCombined=new qs,this._pages=[],this._activePages=[],this._workBoundingBox={top:0,left:0,bottom:0,right:0},this._workAttributeData=new xt,this._textureSize=512,this._onAddTextureAtlasCanvas=new re,this.onAddTextureAtlasCanvas=this._onAddTextureAtlasCanvas.event,this._onRemoveTextureAtlasCanvas=new re,this.onRemoveTextureAtlasCanvas=this._onRemoveTextureAtlasCanvas.event,this._requestClearModel=!1,this._createNewPage(),this._tmpCanvas=Sn(e,this._config.deviceCellWidth*4+Ft*2,this._config.deviceCellHeight+Ft*2),this._tmpCtx=ce(this._tmpCanvas.getContext("2d",{alpha:this._config.allowTransparency,willReadFrequently:!0}))}get pages(){return this._pages}dispose(){this._tmpCanvas.remove();for(let e of this.pages)e.canvas.remove();this._onAddTextureAtlasCanvas.dispose()}warmUp(){this._didWarmUp||(this._doWarmUp(),this._didWarmUp=!0)}_doWarmUp(){let e=new Zo;for(let t=33;t<126;t++)e.enqueue(()=>{if
Line truncated
|
||||
layout (location = 0) in vec2 a_unitquad;
|
||||
layout (location = 1) in vec2 a_cellpos;
|
||||
layout (location = 2) in vec2 a_offset;
|
||||
@@ -86,13 +86,13 @@ void main() {
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_texpage = int(a_texpage);
|
||||
v_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}\`;function Vr(r){let e="";for(let _=1;_<r;_++)e+=\` else if (v_texpage == \${_}) { outColor = texture(u_texture[\${_}], v_texcoord); }\`;return\`#version 300 es
|
||||
}\`;function pa(i){let e="";for(let t=1;t<i;t++)e+=\` else if (v_texpage == \${t}) { outColor = texture(u_texture[\${t}], v_texcoord); }\`;return\`#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
flat in int v_texpage;
|
||||
|
||||
uniform sampler2D u_texture[\${r}];
|
||||
uniform sampler2D u_texture[\${i}];
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
@@ -100,7 +100,7 @@ void main() {
|
||||
if (v_texpage == 0) {
|
||||
outColor = texture(u_texture[0], v_texcoord);
|
||||
} \${e}
|
||||
}\`}var Fe=11,ze=Fe*Float32Array.BYTES_PER_ELEMENT,Gr=2,se=0,Q,Mt=0,st=0,Xr=class extends we{constructor(r,e,_,C){super(),this._terminal=r,this._gl=e,this._dimensions=_,this._optionsService=C,this._activeBuffer=0,this._vertices={count:0,attributes:new Float32Array(0),attributesBuffers:[new Float32Array(0),new Float32Array(0)]};let g=this._gl;Pe.maxAtlasPages===void 0&&(Pe.maxAtlasPages=Math.min(32,ne(g.getParameter(g.MAX_TEXTURE_IMAGE_UNITS))),Pe.maxTextureSize=ne(g.getParameter(g.MAX_TEXTURE_SIZE))),this._program=ne(Bs(g,Kr,Vr(Pe.maxAtlasPages))),this._register(_e(()=>g.deleteProgram(this._program))),this._projectionLocation=ne(g.getUniformLocation(this._program,"u_projection")),this._resolutionLocation=ne(g.getUniformLocation(this._program,"u_resolution")),this._textureLocation=ne(g.getUniformLocation(this._program,"u_texture")),this._vertexArrayObject=g.createVertexArray(),g.bindVertexArray(this._vertexArrayObject);let k=new Float32Array([0,0,1,0,0,1,1,1]),w=g.createBuffer();this._register(_e(()=>g.deleteBuffer(w))),g.bindBuffer(g.ARRAY_BUFFER,w),g.bufferData(g.ARRAY_BUFFER,k,g.STATIC_DRAW),g.enableVertexAttribArray(0),g.vertexAttribPointer(0,2,this._gl.FLOAT,!1,0,0);let s=new Uint8Array([0,1,2,3]),a=g.createBuffer();this._register(_e(()=>g.deleteBuffer(a))),g.bindBuffer(g.ELEMENT_ARRAY_BUFFER,a),g.bufferData(g.ELEMENT_ARRAY_BUFFER,s,g.STATIC_DRAW),this._attributesBuffer=ne(g.createBuffer()),this._register(_e(()=>g.deleteBuffer(this._attributesBuffer))),g.bindBuffer(g.ARRAY_BUFFER,this._attributesBuffer),g.enableVertexAttribArray(2),g.vertexAttribPointer(2,2,g.FLOAT,!1,ze,0),g.vertexAttribDivisor(2,1),g.enableVertexAttribArray(3),g.vertexAttribPointer(3,2,g.FLOAT,!1,ze,2*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(3,1),g.enableVertexAttribArray(4),g.vertexAttribPointer(4,1,g.FLOAT,!1,ze,4*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(4,1),g.enableVertexAttribArray(5),g.vertexAttribPointer(5,2,g.FLOAT,!1,ze,5*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(5,1),g.enableVertexAttribArray(6),g.vertexAttribPointer(6,2,g.FLOAT,!1,ze,7*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(6,1),g.enableVertexAttribArray(1),g.vertexAttribPointer(1,2,g.FLOAT,!1,ze,9*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(1,1),g.useProgram(this._program);let l=new Int32Array(Pe.maxAtlasPages);for(let d=0;d<Pe.maxAtlasPages;d++)l[d]=d;g.uniform1iv(this._textureLocation,l),g.uniformMatrix4fv(this._projectionLocation,!1,Ms),this._atlasTextures=[];for(let d=0;d<Pe.maxAtlasPages;d++){let h=new qr(ne(g.createTexture()));this._register(_e(()=>g.deleteTexture(h.texture))),g.activeTexture(g.TEXTURE0+d),g.bindTexture(g.TEXTURE_2D,h.texture),g.texParameteri(g.TEXTURE_2D,g.TEXTURE_WRAP_S,g.CLAMP_TO_EDGE),g.texParameteri(g.TEXTURE_2D,g.TEXTURE_WRAP_T,g.CLAMP_TO_EDGE),g.texImage2D(g.TEXTURE_2D,0,g.RGBA,1,1,0,g.RGBA,g.UNSIGNED_BYTE,new Uint8Array([255,0,0,255])),this._atlasTextures[d]=h}g.enable(g.BLEND),g.blendFunc(g.SRC_ALPHA,g.ONE_MINUS_SRC_ALPHA),this.handleResize()}beginFrame(){return this._atlas?this._atlas.beginFrame():!0}updateCell(r,e,_,C,g,k,w,s,a){this._updateCell(this._vertices.attributes,r,e,_,C,g,k,w,s,a)}_updateCell(r,e,_,C,g,k,w,s,a,l){if(se=(_*this._terminal.cols+e)*Fe,C===0||C===void 0){r.fill(0,se,se+Fe-1-Gr);return}this._atlas&&(s&&s.length>1?Q=this._atlas.getRasterizedGlyphCombinedChar(s,g,k,w,!1,this._terminal.element):Q=this._atlas.getRasterizedGlyph(C,g,k,w,!1,this._terminal.element),Mt=Math.floor((this._dimensions.device.cell.width-this._dimensions.device.char.width)/2),g!==l&&Q.offset.x>Mt?(st=Q.offset.x-Mt,r[se]=-(Q.offset.x-st)+this._dimensions.device.char.left,r[se+1]=-Q.offset.y+this._dimensions.device.char.top,r[se+2]=(Q.size.x-st)/this._dimensions.device.canvas.width,r[se+3]=Q.size.y/this._dimensions.device.canvas.height,r[se+4]=Q.texturePage,r[se+5]=Q.texturePositionClipSpace.x+st/this._atlas.pages[Q.texturePage].canvas.width,r[se+6]=Q.texturePositionClipSpace.y,r[se+7]=Q.sizeClipSpace.x-st/this._atlas.pages[Q.texturePage].canvas.width,r[se+8]=Q.sizeClipSpace.y):(r[se]=-Q.offset.x+this._dimensions.device.char.left,r[se+1]=-Q.offset.y+this._dimensions.device.char.top,r[se+2]=Q.size.x/this._dimensions.device.canvas.width,r[se+3]=Q.size.y/this._dimensions.device.canvas.height,r[se+4]=Q.texturePage,r[se+5]=Q.texturePositionClipSpace.x,r[se+6]=Q.texturePositionClipSpace.y,r[se+7]=Q.sizeClipSpace.x,r[se+8]=Q.sizeClipSpace.y),this._optionsService.rawOptions.rescaleOverlappingGlyphs&&ur(C,a,Q.size.x,this._dimensions.device.cell.width)&&(r[se+2]=(this._dimensions.device.cell.width-1)/this._dimensions.device.canvas.width))}clear(){let r=this._terminal,e=r.cols*r.rows*Fe;this._vertices.count!==e?this._vertices.attributes=new Float32Array(e):this._vertices.attributes.fill(0);let _=0;for(;_<this._vertices.attributesBuffers.length;_++)this._vertices.count!==e?this._vertices.attributesBuffers[_]=new Float32Array(e):this._vertices.attributesBuffers[_].fill(0);this._vertices.count=e,_=0;for
Line truncated
|
||||
}\`}var rt=11,bt=rt*Float32Array.BYTES_PER_ELEMENT,va=2,he=0,se,Vi=0,Ht=0,ma=class extends Pe{constructor(e,t,r,n){super(),this._terminal=e,this._gl=t,this._dimensions=r,this._optionsService=n,this._activeBuffer=0,this._vertices={count:0,attributes:new Float32Array(0),attributesBuffers:[new Float32Array(0),new Float32Array(0)]};let o=this._gl;st.maxAtlasPages===void 0&&(st.maxAtlasPages=Math.min(32,ce(o.getParameter(o.MAX_TEXTURE_IMAGE_UNITS))),st.maxTextureSize=ce(o.getParameter(o.MAX_TEXTURE_SIZE))),this._program=ce(En(o,ga,pa(st.maxAtlasPages))),this._register(Se(()=>o.deleteProgram(this._program))),this._projectionLocation=ce(o.getUniformLocation(this._program,"u_projection")),this._resolutionLocation=ce(o.getUniformLocation(this._program,"u_resolution")),this._textureLocation=ce(o.getUniformLocation(this._program,"u_texture")),this._vertexArrayObject=o.createVertexArray(),o.bindVertexArray(this._vertexArrayObject);let _=new Float32Array([0,0,1,0,0,1,1,1]),s=o.createBuffer();this._register(Se(()=>o.deleteBuffer(s))),o.bindBuffer(o.ARRAY_BUFFER,s),o.bufferData(o.ARRAY_BUFFER,_,o.STATIC_DRAW),o.enableVertexAttribArray(0),o.vertexAttribPointer(0,2,this._gl.FLOAT,!1,0,0);let h=new Uint8Array([0,1,2,3]),a=o.createBuffer();this._register(Se(()=>o.deleteBuffer(a))),o.bindBuffer(o.ELEMENT_ARRAY_BUFFER,a),o.bufferData(o.ELEMENT_ARRAY_BUFFER,h,o.STATIC_DRAW),this._attributesBuffer=ce(o.createBuffer()),this._register(Se(()=>o.deleteBuffer(this._attributesBuffer))),o.bindBuffer(o.ARRAY_BUFFER,this._attributesBuffer),o.enableVertexAttribArray(2),o.vertexAttribPointer(2,2,o.FLOAT,!1,bt,0),o.vertexAttribDivisor(2,1),o.enableVertexAttribArray(3),o.vertexAttribPointer(3,2,o.FLOAT,!1,bt,2*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(3,1),o.enableVertexAttribArray(4),o.vertexAttribPointer(4,1,o.FLOAT,!1,bt,4*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(4,1),o.enableVertexAttribArray(5),o.vertexAttribPointer(5,2,o.FLOAT,!1,bt,5*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(5,1),o.enableVertexAttribArray(6),o.vertexAttribPointer(6,2,o.FLOAT,!1,bt,7*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(6,1),o.enableVertexAttribArray(1),o.vertexAttribPointer(1,2,o.FLOAT,!1,bt,9*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(1,1),o.useProgram(this._program);let d=new Int32Array(st.maxAtlasPages);for(let u=0;u<st.maxAtlasPages;u++)d[u]=u;o.uniform1iv(this._textureLocation,d),o.uniformMatrix4fv(this._projectionLocation,!1,xn),this._atlasTextures=[];for(let u=0;u<st.maxAtlasPages;u++){let p=new _a(ce(o.createTexture()));this._register(Se(()=>o.deleteTexture(p.texture))),o.activeTexture(o.TEXTURE0+u),o.bindTexture(o.TEXTURE_2D,p.texture),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE),o.texImage2D(o.TEXTURE_2D,0,o.RGBA,1,1,0,o.RGBA,o.UNSIGNED_BYTE,new Uint8Array([255,0,0,255])),this._atlasTextures[u]=p}o.enable(o.BLEND),o.blendFunc(o.SRC_ALPHA,o.ONE_MINUS_SRC_ALPHA),this.handleResize()}beginFrame(){return this._atlas?this._atlas.beginFrame():!0}updateCell(e,t,r,n,o,_,s,h,a){this._updateCell(this._vertices.attributes,e,t,r,n,o,_,s,h,a)}_updateCell(e,t,r,n,o,_,s,h,a,d){if(he=(r*this._terminal.cols+t)*rt,n===0||n===void 0){e.fill(0,he,he+rt-1-va);return}this._atlas&&(h&&h.length>1?se=this._atlas.getRasterizedGlyphCombinedChar(h,o,_,s,!1,this._terminal.element):se=this._atlas.getRasterizedGlyph(n,o,_,s,!1,this._terminal.element),Vi=Math.floor((this._dimensions.device.cell.width-this._dimensions.device.char.width)/2),o!==d&&se.offset.x>Vi?(Ht=se.offset.x-Vi,e[he]=-(se.offset.x-Ht)+this._dimensions.device.char.left,e[he+1]=-se.offset.y+this._dimensions.device.char.top,e[he+2]=(se.size.x-Ht)/this._dimensions.device.canvas.width,e[he+3]=se.size.y/this._dimensions.device.canvas.height,e[he+4]=se.texturePage,e[he+5]=se.texturePositionClipSpace.x+Ht/this._atlas.pages[se.texturePage].canvas.width,e[he+6]=se.texturePositionClipSpace.y,e[he+7]=se.sizeClipSpace.x-Ht/this._atlas.pages[se.texturePage].canvas.width,e[he+8]=se.sizeClipSpace.y):(e[he]=-se.offset.x+this._dimensions.device.char.left,e[he+1]=-se.offset.y+this._dimensions.device.char.top,e[he+2]=se.size.x/this._dimensions.device.canvas.width,e[he+3]=se.size.y/this._dimensions.device.canvas.height,e[he+4]=se.texturePage,e[he+5]=se.texturePositionClipSpace.x,e[he+6]=se.texturePositionClipSpace.y,e[he+7]=se.sizeClipSpace.x,e[he+8]=se.sizeClipSpace.y),this._optionsService.rawOptions.rescaleOverlappingGlyphs&&Io(n,a,se.size.x,this._dimensions.device.cell.width)&&(e[he+2]=(this._dimensions.device.cell.width-1)/this._dimensions.device.canvas.width))}clear(){let e=this._terminal,t=e.cols*e.rows*rt;this._vertices.count!==t?this._vertices.attributes=new Float32Array(t):this._vertices.attributes.fill(0);let r=0;for(;r<this._vertices.attributesBuffers.length;r++)this._vertices.count!==t?this._vertices.attributesBuffers[r]=new Float32Array(t):this._vertices.attributesBuffers[r].fill(0);this
Line truncated
|
||||
layout (location = 0) in vec2 a_position;
|
||||
layout (location = 1) in vec2 a_size;
|
||||
layout (location = 2) in vec4 a_color;
|
||||
@@ -114,7 +114,7 @@ void main() {
|
||||
vec2 zeroToOne = a_position + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}\`,sn=\`#version 300 es
|
||||
}\`,xa=\`#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec4 v_color;
|
||||
@@ -123,13 +123,21 @@ out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = v_color;
|
||||
}\`,Me=8,Tt=Me*Float32Array.BYTES_PER_ELEMENT,rn=20*Me,Ei=class{constructor(){this.attributes=new Float32Array(rn),this.count=0}},Ae=0,Li=0,ki=0,Ri=0,Ai=0,Mi=0,Bi=0,nn=class extends we{constructor(r,e,_,C){super(),this._terminal=r,this._gl=e,this._dimensions=_,this._themeService=C,this._vertices=new Ei,this._verticesCursor=new Ei;let g=this._gl;this._program=ne(Bs(g,tn,sn)),this._register(_e(()=>g.deleteProgram(this._program))),this._projectionLocation=ne(g.getUniformLocation(this._program,"u_projection")),this._vertexArrayObject=g.createVertexArray(),g.bindVertexArray(this._vertexArrayObject);let k=new Float32Array([0,0,1,0,0,1,1,1]),w=g.createBuffer();this._register(_e(()=>g.deleteBuffer(w))),g.bindBuffer(g.ARRAY_BUFFER,w),g.bufferData(g.ARRAY_BUFFER,k,g.STATIC_DRAW),g.enableVertexAttribArray(3),g.vertexAttribPointer(3,2,this._gl.FLOAT,!1,0,0);let s=new Uint8Array([0,1,2,3]),a=g.createBuffer();this._register(_e(()=>g.deleteBuffer(a))),g.bindBuffer(g.ELEMENT_ARRAY_BUFFER,a),g.bufferData(g.ELEMENT_ARRAY_BUFFER,s,g.STATIC_DRAW),this._attributesBuffer=ne(g.createBuffer()),this._register(_e(()=>g.deleteBuffer(this._attributesBuffer))),g.bindBuffer(g.ARRAY_BUFFER,this._attributesBuffer),g.enableVertexAttribArray(0),g.vertexAttribPointer(0,2,g.FLOAT,!1,Tt,0),g.vertexAttribDivisor(0,1),g.enableVertexAttribArray(1),g.vertexAttribPointer(1,2,g.FLOAT,!1,Tt,2*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(1,1),g.enableVertexAttribArray(2),g.vertexAttribPointer(2,4,g.FLOAT,!1,Tt,4*Float32Array.BYTES_PER_ELEMENT),g.vertexAttribDivisor(2,1),this._updateCachedColors(C.colors),this._register(this._themeService.onChangeColors(l=>{this._updateCachedColors(l),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(r){let e=this._gl;e.useProgram(this._program),e.bindVertexArray(this._vertexArrayObject),e.uniformMatrix4fv(this._projectionLocation,!1,Ms),e.bindBuffer(e.ARRAY_BUFFER,this._attributesBuffer),e.bufferData(e.ARRAY_BUFFER,r.attributes,e.DYNAMIC_DRAW),e.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,e.UNSIGNED_BYTE,0,r.count)}handleResize(){this._updateViewportRectangle()}setDimensions(r){this._dimensions=r}_updateCachedColors(r){this._bgFloat=this._colorToFloat32Array(r.background),this._cursorFloat=this._colorToFloat32Array(r.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(r){let e=this._terminal,_=this._vertices,C=1,g,k,w,s,a,l,d,h,u,v,b;for(g=0;g<e.rows;g++){for(w=-1,s=0,a=0,l=!1,k=0;k<e.cols;k++)d=(g*e.cols+k)*mt,h=r.cells[d+pt],u=r.cells[d+vt],v=!!(u&67108864),(h!==s||u!==a&&(l||v))&&((s!==0||l&&a!==0)&&(b=C++*Me,this._updateRectangle(_,b,a,s,w,k,g)),w=k,s=h,a=u,l=v);(s!==0||l&&a!==0)&&(b=C++*Me,this._updateRectangle(_,b,a,s,w,e.cols,g))}_.count=C}updateCursor(r){let e=this._verticesCursor,_=r.cursor;if(!_||_.style==="block"){e.count=0;return}let C,g=0;(_.style==="bar"||_.style==="outline")&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,_.y*this._dimensions.device.cell.height,_.style==="bar"?_.dpr*_.cursorWidth:_.dpr,this._dimensions.device.cell.height,this._cursorFloat)),(_.style==="underline"||_.style==="outline")&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,(_.y+1)*this._dimensions.device.cell.height-_.dpr,_.width*this._dimensions.device.cell.width,_.dpr,this._cursorFloat)),_.style==="outline"&&(C=g++*Me,this._addRectangleFloat(e.attributes,C,_.x*this._dimensions.device.cell.width,_.y*this._dimensions.device.cell.height,_.width*this._dimensions.device.cell.width,_.dpr,this._cursorFloat),C=g++*Me,this._addRectangleFloat(e.attributes,C,(_.x+_.width)*this._dimensions.device.cell.width-_.dpr,_.y*this._dimensions.device.cell.height,_.dpr,this._dimensions.device.cell.height,this._cursorFloat)),e.count=g}_updateRectangle(r,e,_,C,g,k,w){if(_&67108864)switch(_&50331648){case 16777216:case 33554432:Ae=this._themeService.colors.ansi[_&255].rgba;break;case 50331648:Ae=(_&16777215)<<8;break;case 0:default:Ae=this._themeService.colors.foreground.rgba}else switch(C&50331648){case 16777216:case 33554432:Ae=this._themeService.colors.ansi[C&255].rgba;break;case 50331648:Ae=(C&16777215)<<8;break;case 0:default:Ae=this._themeService.colors.background.rgba}r.attributes.length<e+4&&(r.attributes=Zr(r.attributes,this._terminal.rows*this._terminal.cols*Me)),Li=g*this._dimensions.device.cell.width,ki=w*this._dimensions.device.cell.height,Ri=(Ae>>24&255)/255,Ai=(Ae>>16&255)/255,Mi=(Ae>>8&255)/255,Bi=1,this._addRectangle(r.attributes,e,Li,ki,(k-g)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,Ri,Ai,Mi,Bi)}_addRectangle(r,e,_,C,g,k,w,s,a,l){r[e]=_/this._dimensions.device.canvas.width,r[e+1]=C/this._dimensions.device.canvas.heig
Line truncated
|
||||
\`+C+" ("+g+" bytes)")){_.skip();return}const w=[];_.on("input",s=>{w.push(s)}),_.accept().then(()=>{Yt.Browser.save_to_disk(w,C)})}),e.start()}handleSend(e){const _=document.createElement("input");_.type="file",_.multiple=!0,_.style.display="none",_.onchange=C=>{const g=C.target.files;g&&g.length>0?Yt.Browser.send_files(e,g,{on_offer_response:(k,w)=>{w||console.warn("Server rejected file:",k.name)},on_progress:(k,w,s)=>{},on_file_complete:k=>{console.log("Finished uploading:",k.name)}}).then(()=>{e.close()}):e.close()},document.body.appendChild(_),_.click(),document.body.removeChild(_)}}const ts={cyberpunk:{background:"#0a0a0a",foreground:"#4af626",cursor:"#14d1ff",cursorAccent:"#0a0a0a",selectionBackground:"#273747"},glacier:{background:"#0a192f",foreground:"#64ffda",cursor:"#e6f1ff",cursorAccent:"#0a192f",selectionBackground:"#112240"},gruvbox:{background:"#282828",foreground:"#ebdbb2",cursor:"#d3869b",cursorAccent:"#282828",selectionBackground:"#504945"}};class Wn{constructor(e){Y(this,"terminal");Y(this,"fitAddon");Y(this,"webglAddon");Y(this,"ws",null);Y(this,"container");Y(this,"disposables",[]);Y(this,"heartbeatInterval",null);Y(this,"reconnectAttempts",0);Y(this,"maxReconnectAttempts",5);Y(this,"reconnectTimeout",null);Y(this,"lastConfig",null);this.container=document.getElementById(e),this.terminal=new zs.Terminal({cursorBlink:!0,cursorStyle:"block",fontSize:14,fontFamily:'"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',theme:ts.cyberpunk,allowProposedApi:!0,scrollback:1e4}),this.fitAddon=new Zs.FitAddon,this.terminal.loadAddon(this.fitAddon),this.terminal.loadAddon(new Ks.WebLinksAddon),window.addEventListener("resize",()=>this.fit()),this.container.addEventListener("contextmenu",async _=>{var C;_.preventDefault();try{const g=await navigator.clipboard.readText();g&&((C=this.ws)==null?void 0:C.readyState)===WebSocket.OPEN&&this.ws.send(g)}catch(g){console.error("Failed to read clipboard",g)}})}setTheme(e){this.terminal.options.theme=ts[e]}mount(){this.terminal.open(this.container);try{this.webglAddon=new An,this.webglAddon.onContextLoss(e=>{console.warn("WebGL context lost",e),this.webglAddon.dispose()}),this.terminal.loadAddon(this.webglAddon)}catch(e){console.warn("WebGL addon failed to load, falling back to canvas/dom",e)}this.fit(),this.terminal.writeln("\\x1B[1;33m╔══════════════════════════════════╗\\x1B[0m"),this.terminal.writeln("\\x1B[1;33m║ Connecting to CloudSSH ║\\x1B[0m"),this.terminal.writeln("\\x1B[1;33m╚══════════════════════════════════╝\\x1B[0m"),this.terminal.writeln("")}async connect(e){this.lastConfig=e;const _=new URL(window.location.href);return _.protocol=_.protocol==="https:"?"wss:":"ws:",_.pathname="/api/ssh",new Promise((C,g)=>{this.ws=new WebSocket(_.toString()),this.ws.onopen=()=>{var k;this.terminal.writeln("\\x1B[32m[+] WebSocket connected, sending credentials...\\x1B[0m"),(k=this.ws)==null||k.send(JSON.stringify({host:e.host,port:e.port,username:e.username,password:e.password,authMethod:e.authMethod,privateKey:e.privateKey})),this.reconnectAttempts=0,this.startHeartbeat(),C()},this.ws.onerror=()=>{g(new Error("WebSocket connection failed"))},this.ws.onclose=()=>{this.terminal.writeln("\\x1B[31m[-] 连接已关闭\\x1B[0m");const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE')},this.setupWebSocketHandlers(g)})}connectWithWebSocket(e){this.lastConfig=null,this.ws=e,e.onopen=()=>{this.terminal.writeln("\\x1B[32m[+] WebSocket connected, authenticating...\\x1B[0m"),this.reconnectAttempts=0,this.startHeartbeat()},this.setupWebSocketHandlers()}setupWebSocketHandlers(e){if(!this.ws)return;const _=new Nn(C=>this.terminal.write(C),C=>{var g;((g=this.ws)==null?void 0:g.readyState)===WebSocket.OPEN&&this.ws.send(C)});this.ws.onmessage=C=>{if(typeof C.data=="string")try{const g=JSON.parse(C.data);switch(g.type){case"status":if(this.terminal.writeln(\`\\x1B[32m[*] \${g.message}\\x1B[0m\`),g.message==="认证成功"){const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#4af626] inline-block animate-pulse"></span> STATUS: ONLINE')}break;case"error":this.terminal.writeln(\`\\x1B[31m[!] \${g.message}\\x1B[0m\`);break}}catch{this.terminal.write(C.data)}else{const g=new FileReader;g.onload=()=>{_.consume(g.result)},g.readAsArrayBuffer(C.data)}},this.ws.onclose=C=>{this.stopHeartbeat(),this.terminal.writeln(\`\\x1B[33m[*] Connection closed (code=\${C.code})\\x1B[0m\`);const g=document.getElementById("term-status");g&&(g.innerHTML='<div class="w-2 h-2 bg-red-500"></div> Disconnected');const k=document.getElementById("status-text");k&&(k.innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'),C.code!==1e3&&this.lastConfig&&this.reconnectAttempts<this.maxReconnectAttempts&&thi
Line truncated
|
||||
}\`,Ge=8,Zi=Ge*Float32Array.BYTES_PER_ELEMENT,Ea=20*Ge,Js=class{constructor(){this.attributes=new Float32Array(Ea),this.count=0}},Ke=0,Qs=0,er=0,tr=0,ir=0,sr=0,rr=0,La=class extends Pe{constructor(e,t,r,n){super(),this._terminal=e,this._gl=t,this._dimensions=r,this._themeService=n,this._vertices=new Js,this._verticesCursor=new Js;let o=this._gl;this._program=ce(En(o,Ca,xa)),this._register(Se(()=>o.deleteProgram(this._program))),this._projectionLocation=ce(o.getUniformLocation(this._program,"u_projection")),this._vertexArrayObject=o.createVertexArray(),o.bindVertexArray(this._vertexArrayObject);let _=new Float32Array([0,0,1,0,0,1,1,1]),s=o.createBuffer();this._register(Se(()=>o.deleteBuffer(s))),o.bindBuffer(o.ARRAY_BUFFER,s),o.bufferData(o.ARRAY_BUFFER,_,o.STATIC_DRAW),o.enableVertexAttribArray(3),o.vertexAttribPointer(3,2,this._gl.FLOAT,!1,0,0);let h=new Uint8Array([0,1,2,3]),a=o.createBuffer();this._register(Se(()=>o.deleteBuffer(a))),o.bindBuffer(o.ELEMENT_ARRAY_BUFFER,a),o.bufferData(o.ELEMENT_ARRAY_BUFFER,h,o.STATIC_DRAW),this._attributesBuffer=ce(o.createBuffer()),this._register(Se(()=>o.deleteBuffer(this._attributesBuffer))),o.bindBuffer(o.ARRAY_BUFFER,this._attributesBuffer),o.enableVertexAttribArray(0),o.vertexAttribPointer(0,2,o.FLOAT,!1,Zi,0),o.vertexAttribDivisor(0,1),o.enableVertexAttribArray(1),o.vertexAttribPointer(1,2,o.FLOAT,!1,Zi,2*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(1,1),o.enableVertexAttribArray(2),o.vertexAttribPointer(2,4,o.FLOAT,!1,Zi,4*Float32Array.BYTES_PER_ELEMENT),o.vertexAttribDivisor(2,1),this._updateCachedColors(n.colors),this._register(this._themeService.onChangeColors(d=>{this._updateCachedColors(d),this._updateViewportRectangle()}))}renderBackgrounds(){this._renderVertices(this._vertices)}renderCursor(){this._renderVertices(this._verticesCursor)}_renderVertices(e){let t=this._gl;t.useProgram(this._program),t.bindVertexArray(this._vertexArrayObject),t.uniformMatrix4fv(this._projectionLocation,!1,xn),t.bindBuffer(t.ARRAY_BUFFER,this._attributesBuffer),t.bufferData(t.ARRAY_BUFFER,e.attributes,t.DYNAMIC_DRAW),t.drawElementsInstanced(this._gl.TRIANGLE_STRIP,4,t.UNSIGNED_BYTE,0,e.count)}handleResize(){this._updateViewportRectangle()}setDimensions(e){this._dimensions=e}_updateCachedColors(e){this._bgFloat=this._colorToFloat32Array(e.background),this._cursorFloat=this._colorToFloat32Array(e.cursor)}_updateViewportRectangle(){this._addRectangleFloat(this._vertices.attributes,0,0,0,this._terminal.cols*this._dimensions.device.cell.width,this._terminal.rows*this._dimensions.device.cell.height,this._bgFloat)}updateBackgrounds(e){let t=this._terminal,r=this._vertices,n=1,o,_,s,h,a,d,u,p,m,C,w;for(o=0;o<t.rows;o++){for(s=-1,h=0,a=0,d=!1,_=0;_<t.cols;_++)u=(o*t.cols+_)*Ai,p=e.cells[u+xi],m=e.cells[u+Ei],C=!!(m&67108864),(p!==h||m!==a&&(d||C))&&((h!==0||d&&a!==0)&&(w=n++*Ge,this._updateRectangle(r,w,a,h,s,_,o)),s=_,h=p,a=m,d=C);(h!==0||d&&a!==0)&&(w=n++*Ge,this._updateRectangle(r,w,a,h,s,t.cols,o))}r.count=n}updateCursor(e){let t=this._verticesCursor,r=e.cursor;if(!r||r.style==="block"){t.count=0;return}let n,o=0;(r.style==="bar"||r.style==="outline")&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,r.y*this._dimensions.device.cell.height,r.style==="bar"?r.dpr*r.cursorWidth:r.dpr,this._dimensions.device.cell.height,this._cursorFloat)),(r.style==="underline"||r.style==="outline")&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,(r.y+1)*this._dimensions.device.cell.height-r.dpr,r.width*this._dimensions.device.cell.width,r.dpr,this._cursorFloat)),r.style==="outline"&&(n=o++*Ge,this._addRectangleFloat(t.attributes,n,r.x*this._dimensions.device.cell.width,r.y*this._dimensions.device.cell.height,r.width*this._dimensions.device.cell.width,r.dpr,this._cursorFloat),n=o++*Ge,this._addRectangleFloat(t.attributes,n,(r.x+r.width)*this._dimensions.device.cell.width-r.dpr,r.y*this._dimensions.device.cell.height,r.dpr,this._dimensions.device.cell.height,this._cursorFloat)),t.count=o}_updateRectangle(e,t,r,n,o,_,s){if(r&67108864)switch(r&50331648){case 16777216:case 33554432:Ke=this._themeService.colors.ansi[r&255].rgba;break;case 50331648:Ke=(r&16777215)<<8;break;case 0:default:Ke=this._themeService.colors.foreground.rgba}else switch(n&50331648){case 16777216:case 33554432:Ke=this._themeService.colors.ansi[n&255].rgba;break;case 50331648:Ke=(n&16777215)<<8;break;case 0:default:Ke=this._themeService.colors.background.rgba}e.attributes.length<t+4&&(e.attributes=fa(e.attributes,this._terminal.rows*this._terminal.cols*Ge)),Qs=o*this._dimensions.device.cell.width,er=s*this._dimensions.device.cell.height,tr=(Ke>>24&255)/255,ir=(Ke>>16&255)/255,sr=(Ke>>8&255)/255,rr=1,this._addRectangle(e.attributes,t,Qs,er,(_-o)*this._dimensions.device.cell.width,this._dimensions.device.cell.height,tr,ir,sr,rr)}_addRectangle(e,t,r,n,o,_,s,h,a,d){e[t]=r/this._dimensions.device.canvas.width,e[t+1]=n/this._dimensions.device.canvas.heig
Line truncated
|
||||
- \`)}const te=eo("fs"),Je=eo("path");function eo(i){try{return require(i)}catch{return{}}}(function(i,e){for(const t of e)i[t+"Async"]=(...r)=>new Promise(n=>{i[t](...r,o=>n(!o))})})(te,["rm","rmdir","unlink","access"]),(function(i,e){for(const t of e)i[t+"Async"]=(...r)=>new Promise((n,o)=>{i[t](...r,(_,s)=>{_?o(_):n(s)})})})(te,["stat","mkdir","readdir","close","open","realpath","write"]);class $r{constructor(e,t,r,n,o){this.closed=!1,this.fd=null,this.pathId=e,this.absPath=t,this.relPath=r,this.dir=n,this.size=o}getPathId(){return this.pathId}getRelPath(){return this.relPath}isDir(){return this.dir}getSize(){return this.size}readFile(e){return q(this,void 0,void 0,function*(){if(this.closed)throw new X(\`File closed: \${this.absPath}\`,null,!0);this.fd===null&&(this.fd=yield te.openAsync(this.absPath,"r"));const t=new Uint8Array(e);return(function(r,n,o,_,s){return q(this,void 0,void 0,function*(){return new Promise((h,a)=>te.read(r,n,o,_,s,(d,u,p)=>{d?a(d):h(p.subarray(0,u))}))})})(this.fd,t,0,t.length,null)})}closeFile(){return q(this,void 0,void 0,function*(){this.closed||(this.closed=!0,this.fd!==null&&(yield te.closeAsync(this.fd),this.fd=null))})}}function to(i,e,t,r,n,o){return q(this,void 0,void 0,function*(){if(!t.isDirectory()){if(!t.isFile())throw new X(\`Not a regular file: \${e}\`);if(!(yield te.accessAsync(e,te.constants.R_OK)))throw new X(\`No permission to read: \${e}\`);return void r.push(new $r(i,e,n,!1,t.size))}const _=yield te.realpathAsync(e);if(o.has(_))throw new X(\`Duplicate link: \${e}\`);o.add(_),r.push(new $r(i,e,n,!0,0));const s=yield te.readdirAsync(e);for(const h of s){const a=Je.join(e,h),d=yield te.statAsync(a);yield to(i,a,d,r,[...n,h],o)}})}function Hr(i){return q(this,arguments,void 0,function*(e,t=!1){if(!e||!e.length)return;const r=[],n=e.entries();for(const[o,_]of n){const s=Je.resolve(_);if(!(yield te.accessAsync(s)))throw new X(\`No such file: \${s}\`);const h=yield te.statAsync(s);if(!t&&h.isDirectory())throw new X(\`Is a directory: \${s}\`);const a=new Set;yield to(o,s,h,r,[Je.basename(s)],a)}return r})}class ps{constructor(e,t,r,n,o=!1){this.closed=!1,this.absPath=e,this.fileName=t,this.localName=r,this.fd=n,this.dir=o}getFileName(){return this.fileName}getLocalName(){return this.localName}isDir(){return this.dir}writeFile(e){return q(this,void 0,void 0,function*(){yield te.writeAsync(this.fd,e)})}closeFile(){return q(this,void 0,void 0,function*(){this.closed||(this.closed=!0,this.fd!==null&&(yield te.closeAsync(this.fd),this.fd=null))})}deleteFile(){return q(this,void 0,void 0,function*(){if(!this.absPath||!(yield te.accessAsync(this.absPath)))return"";try{if(yield this.closeFile(),typeof te.rm=="function"){if(yield te.rmAsync(this.absPath,{recursive:!0}))return this.absPath}else if(this.isDir){if(yield te.rmdirAsync(this.absPath,{recursive:!0}))return this.absPath}else if(yield te.unlinkAsync(this.absPath))return this.absPath}catch(e){console.log(\`delete [\${this.absPath}] failed\`,e)}return""})}}function zr(i,e){return q(this,void 0,void 0,function*(){if(!(yield te.accessAsync(Je.join(i,e))))return e;for(let t=0;t<1e3;t++){const r=\`\${e}.\${t}\`;if(!(yield te.accessAsync(Je.join(i,r))))return r}throw new X("Fail to assign new file name")})}function Wr(i){return q(this,void 0,void 0,function*(){try{return yield te.openAsync(i,"w")}catch(e){throw e.errno===-13||e.errno===-4048?new X(\`No permission to write: \${i}\`):e.errno===-21||e.errno===-4068?new X(\`Is a directory: \${i}\`):e}})}function Ur(i){return q(this,void 0,void 0,function*(){if(!(yield te.accessAsync(i)))return yield te.mkdirAsync(i,{recursive:!0,mode:493}),!0;if(!(yield te.statAsync(i)).isDirectory())throw new X(\`Not a directory: \${i}\`);return!1})}function el(i,e,t,r){return q(this,void 0,void 0,function*(){if(!t)return(function(h,a,d){return q(this,void 0,void 0,function*(){const u=d?a:yield zr(h,a),p=Je.join(h,u),m=yield Wr(p);return new ps(p,a,u,m)})})(i.path,e,r);const n=JSON.parse(e);if(!n.hasOwnProperty("path_name")||!n.hasOwnProperty("path_id")||!n.hasOwnProperty("is_dir")||n.path_name.length<1)throw new X(\`Invalid name: \${e}\`);let o,_;if(e=n.path_name[n.path_name.length-1],r?o=n.path_name[0]:i.maps.has(n.path_id)?o=i.maps.get(n.path_id):(o=yield zr(i.path,n.path_name[0]),i.maps.set(n.path_id,o)),n.path_name.length>1){const h=Je.join(i.path,o,...n.path_name.slice(1,n.path_name.length-1));yield Ur(h),_=Je.join(h,e)}else _=Je.join(i.path,o);if(n.is_dir===!0){let h="";return(yield Ur(_))&&(h=_),new ps(h,e,o,null,!0)}const s=yield Wr(_);return new ps(_,e,o,s)})}class ai{constructor(e,t,r,n){this.closed=!1,this.pos=0,this.pathId=e,this.relPath=t,this.file=r,this.dir=n}getPathId(){return this.pathId}getRelPath(){return this.relPath}isDir(){return this.dir}getSize(){return this.file.size}readFile(e){return q(this,void 0,void 0,function*(){if(this.pos>=this.file.size)return new Uint8Array(0);try{const t=Math.min(e.byteLength,this.file.size-this.pos),r=this.file.slice(this.pos,this.pos+t
Line truncated
|
||||
\`,this.writer=e,this.isWindowsShell=t,this.maxDataChunkSize=r}cleanup(){for(const e of this.openedFiles)e.closeFile()}addReceivedData(e){this.stopped||this.buffer.addBuffer(e),this.lastInputTime=Date.now()}stopTransferring(){return q(this,void 0,void 0,function*(){this.cleanTimeoutInMilliseconds=Math.max(2*this.maxChunkTimeInMilliseconds,500),this.stopped=!0,this.buffer.stopBuffer()})}cleanInput(e){return q(this,void 0,void 0,function*(){for(this.stopped=!0,this.buffer.drainBuffer(),this.lastInputTime=Date.now();;){const t=e-(Date.now()-this.lastInputTime);if(t<=0)return;yield new Promise(r=>setTimeout(r,t))}})}sendLine(e,t){return q(this,void 0,void 0,function*(){this.writer(\`#\${e}:\${t}\${this.protocolNewline}\`)})}recvLine(e){return q(this,arguments,void 0,function*(t,r=!1){if(this.stopped)throw new X("Stopped");if(this.isWindowsShell||this.remoteIsWindows){let o=yield this.buffer.readLineOnWindows();const _=o.lastIndexOf("#"+t+":");if(_>=0)o=o.substring(_);else{const s=o.lastIndexOf("#");s>0&&(o=o.substring(s))}return o}let n=yield this.buffer.readLine();if(this.tmuxOutputJunk||r){if(n.length>0)for(;n[n.length-1]==="\\r";)n=n.substring(0,n.length-1)+(yield this.buffer.readLine());const o=n.lastIndexOf("#"+t+":");if(o>=0)n=n.substring(o);else{const _=n.lastIndexOf("#");_>0&&(n=n.substring(_))}n=(function(_){for(;;){const s=_.indexOf("\\x1BP=");if(s<0)return _;let h=s+3;const a=_.substring(h).indexOf("\\x1BP=");if(a<0)return _.substring(0,s);h+=a+3;const d=_.substring(h).indexOf("\\x1B\\\\");if(d<0)return _.substring(0,s);h+=d+2,_=_.substring(0,s)+_.substring(h)}})(n)}return n})}recvCheck(e){return q(this,arguments,void 0,function*(t,r=!1){const n=yield this.recvLine(t,r),o=n.indexOf(":");if(o<1)throw new X(gs(n),"colon",!0);const _=n.substring(1,o),s=n.substring(o+1);if(_!=t)throw new X(s,_,!0);return s})}sendInteger(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,t.toString())})}recvInteger(e){return q(this,arguments,void 0,function*(t,r=!1){const n=yield this.recvCheck(t,r);return Number(n)})}checkInteger(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("SUCC");if(t!==e)throw new X(\`Integer check [\${t}] <> [\${e}]\`,null,!0)})}sendString(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,gs(t))})}recvString(e){return q(this,arguments,void 0,function*(t,r=!1){return Di(As(yield this.recvCheck(t,r)),"utf8")})}checkString(e){return q(this,void 0,void 0,function*(){const t=yield this.recvString("SUCC");if(t!==e)throw new X(\`String check [\${t}] <> [\${e}]\`,null,!0)})}sendBinary(e,t){return q(this,void 0,void 0,function*(){yield this.sendLine(e,gs(t))})}recvBinary(e){return q(this,arguments,void 0,function*(t,r=!1){return As(yield this.recvCheck(t,r))})}checkBinary(e){return q(this,void 0,void 0,function*(){const t=yield this.recvBinary("SUCC");if(t.length!=e.length)throw new X(\`Binary length check [\${t.length}] <> [\${e.length}]\`,null,!0);for(let r=0;r<t.length;r++)if(t[r]!=e[r])throw new X(\`Binary check [\${t[r]}] <> [\${e[r]}]\`,null,!0)})}sendData(e,t,r){return q(this,void 0,void 0,function*(){if(!t)return void(yield this.sendBinary("DATA",e));const n=(function(o,_){if(!_.length)return o;const s=new Uint8Array(2*o.length);let h=0;for(let a=0;a<o.length;a++){let d=-1;for(let u=0;u<_.length;u++)if(o[a]==_[u][0]){d=u;break}d<0?s[h++]=o[a]:(s[h++]=_[d][1],s[h++]=_[d][2])}return s.subarray(0,h)})(e,r);this.writer(\`#DATA:\${n.length}
|
||||
\`),this.writer(n)})}recvData(e,t,r){return q(this,void 0,void 0,function*(){return yield Promise.race([new Promise((n,o)=>setTimeout(()=>{this.cleanTimeoutInMilliseconds=3e3,o(new X("Receive data timeout"))},r)),q(this,void 0,void 0,function*(){if(!e)return yield this.recvBinary("DATA");const n=yield this.recvInteger("DATA");return(function(o,_){if(!_.length)return o;const s=new Uint8Array(o.length);let h=0;for(let a=0;a<o.length;a++){let d=-1;if(a<o.length-1){for(let u=0;u<_.length;u++)if(o[a]==_[u][1]&&o[a+1]==_[u][2]){d=u;break}}d<0?s[h++]=o[a]:(s[h++]=_[d][0],a++)}return s.subarray(0,h)})(yield this.buffer.readBinary(n),t)})])})}sendAction(e,t){return q(this,void 0,void 0,function*(){const r={lang:"js",confirm:e,version:"1.1.6",support_dir:!0};(this.isWindowsShell||t)&&(r.binary=!1,r.newline=\`!
|
||||
\`),t&&(this.remoteIsWindows=!0,this.protocolNewline=\`!
|
||||
\`),yield this.sendString("ACT",JSON.stringify(r))})}recvAction(){return q(this,void 0,void 0,function*(){const e=yield this.recvString("ACT",!0),t=JSON.parse(e);return t.newline&&(this.protocolNewline=t.newline),t})}sendConfig(e,t,r,n){return q(this,void 0,void 0,function*(){const o={lang:"js"};e.quiet&&(o.quiet=!0),e.binary&&(o.binary=!0,o.escape_chars=t),e.directory&&(o.directory=!0),e.bufsize&&(o.bufsize=e.bufsize),e.timeout&&(o.timeout=e.timeout),e.overwrite&&(o.overwrite=!0),r==Qh&&(o.tmux_output_junk=!0),n>0&&(o.tmux_pane_width=n);let _=JSON.stringify(o);_=_.replace(/[\\u007F-\\uFFFF]/g,function(s){return"\\\\u"+("0000"+s.charCodeAt(0).toString(16)).slice(-4)}),this.transferConfig=o,yield this.sendString("CFG",_)})}recvConfig(){return q(this,void 0,void 0,function*(){const e=yield this.recvString("CFG",!0);return this.transferConfig=JSON.parse(e),this.tmuxOutputJunk=this.transferConfig.tmux_output_junk===!0,this.transferConfig})}clientExit(e){return q(this,void 0,void 0,function*(){yield this.sendString("EXIT",e)})}recvExit(){return q(this,void 0,void 0,function*(){return this.recvString("EXIT")})}serverExit(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(500),yield(function(){return q(this,void 0,void 0,function*(){})})(),this.isWindowsShell?(e=e.replace(/\\n/g,\`\\r
|
||||
\`),process.stdout.write("\\x1B[H\\x1B[2J\\x1B[?1049l")):process.stdout.write("\\x1B8\\x1B[0J"),process.stdout.write(e),process.stdout.write(\`\\r
|
||||
\`),process.stdout.write("\\x1B[?25h"),this.transferConfig.tmux_output_junk&&(yield(function(){return q(this,void 0,void 0,function*(){yield require("util").promisify(require("child_process").exec)("tmux refresh-client")})})())})}deleteCreatedFiles(){return q(this,void 0,void 0,function*(){const e=[];for(const t of this.createdFiles){const r=yield t.deleteFile();r&&e.push(r)}return e})}clientError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(r=e.isTraceBack(),e.isRemoteExit())return;if(e.isRemoteFail())return void(r&&console.log(t))}yield this.sendString(r?"FAIL":"fail",t),r&&console.log(t)})}serverError(e){return q(this,void 0,void 0,function*(){yield this.cleanInput(this.cleanTimeoutInMilliseconds);const t=X.getErrorMessage(e);let r=!0;if(e instanceof X){if(e.isStopAndDelete()){const n=yield this.deleteCreatedFiles();if(n&&n.length)return void(yield this.serverExit([e.message+":"].concat(n).join(\`\\r
|
||||
- \`)))}if(r=e.isTraceBack(),e.isRemoteExit()||e.isRemoteFail())return void(yield this.serverExit(t))}yield this.sendString(r?"FAIL":"fail",t),yield this.serverExit(t)})}sendFileNum(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("NUM",e),yield this.checkInteger(e),t&&t.onNum(e)})}sendFileName(e,t,r){return q(this,void 0,void 0,function*(){const n=e.getRelPath(),o=n[n.length-1];if(t){const s={path_id:e.getPathId(),path_name:n,is_dir:e.isDir()};yield this.sendString("NAME",JSON.stringify(s))}else yield this.sendString("NAME",o);const _=yield this.recvString("SUCC");return r&&r.onName(o),_})}sendFileSize(e,t){return q(this,void 0,void 0,function*(){yield this.sendInteger("SIZE",e),yield this.checkInteger(e),t&&t.onSize(e)})}sendFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);let h=1024,a=new ArrayBuffer(h);const d=new hi;for(;s<t;){const u=Date.now(),p=yield e.readFile(a);yield this.sendData(p,r,n),d.appendByteArray(p),yield this.checkInteger(p.length),s+=p.length,_&&_.onStep(s);const m=Date.now()-u;p.length==h&&m<500&&h<o?(h=Math.min(2*h,o),a=new ArrayBuffer(h)):m>=2e3&&h>1024&&(h=1024,a=new ArrayBuffer(h)),m>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=m)}return new Uint8Array(d.end(!0).buffer)})}sendFileMD5(e,t){return q(this,void 0,void 0,function*(){yield this.sendBinary("MD5",e),yield this.checkBinary(e),t&&t.onDone()})}sendFiles(e,t){return q(this,void 0,void 0,function*(){this.openedFiles.push(...e);const r=this.transferConfig.binary===!0,n=this.transferConfig.directory===!0,o=this.transferConfig.bufsize?Math.min(this.transferConfig.bufsize,this.maxDataChunkSize):this.maxDataChunkSize,_=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[];yield this.sendFileNum(e.length,t);const s=[];for(const h of e){const a=yield this.sendFileName(h,n,t);if(s.includes(a)||s.push(a),h.isDir())continue;const d=h.getSize();yield this.sendFileSize(d,t);const u=yield this.sendFileData(h,d,r,_,o,t);h.closeFile(),yield this.sendFileMD5(u,t)}return s})}recvFileNum(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("NUM");return yield this.sendInteger("SUCC",t),e&&e.onNum(t),t})}recvFileName(e,t,r,n,o){return q(this,void 0,void 0,function*(){const _=yield this.recvString("NAME"),s=yield t(e,_,r,n);return this.createdFiles.push(s),yield this.sendString("SUCC",s.getLocalName()),o&&o.onName(s.getFileName()),s})}recvFileSize(e){return q(this,void 0,void 0,function*(){const t=yield this.recvInteger("SIZE");return yield this.sendInteger("SUCC",t),e&&e.onSize(t),t})}recvFileData(e,t,r,n,o,_){return q(this,void 0,void 0,function*(){let s=0;_&&_.onStep(s);const h=new hi;for(;s<t;){const a=Date.now(),d=yield this.recvData(r,n,o);yield e.writeFile(d),s+=d.length,_&&_.onStep(s),yield this.sendInteger("SUCC",d.length),h.appendByteArray(d);const u=Date.now()-a;u>this.maxChunkTimeInMilliseconds&&(this.maxChunkTimeInMilliseconds=u)}return new Uint8Array(h.end(!0).buffer)})}recvFileMD5(e,t){return q(this,void 0,void 0,function*(){const r=yield this.recvBinary("MD5");if(e.length!=r.length)throw new X("Check MD5 failed");for(let n=0;n<e.length;n++)if(e[n]!=r[n])throw new X("Check MD5 failed");yield this.sendBinary("SUCC",e),t&&t.onDone()})}recvFiles(e,t,r){return q(this,void 0,void 0,function*(){const n=this.transferConfig.binary===!0,o=this.transferConfig.directory===!0,_=this.transferConfig.overwrite===!0,s=this.transferConfig.timeout?1e3*this.transferConfig.timeout:1e5,h=this.transferConfig.escape_chars?qr(this.transferConfig.escape_chars):[],a=yield this.recvFileNum(r),d=[];for(let u=0;u<a;u++){const p=yield this.recvFileName(e,t,o,_,r);if(d.includes(p.getLocalName())||d.push(p.getLocalName()),p.isDir())continue;this.openedFiles.push(p);const m=yield this.recvFileSize(r),C=yield this.recvFileData(p,m,n,h,s,r);p.closeFile(),yield this.recvFileMD5(C,r)}return d})}}function Ci(i,e){e-=3;let t=0,r="";for(let n=0;n<i.length;n++){if(i.charCodeAt(n)>=19968&&i.charCodeAt(n)<=40869){if(t+2>e)return{sub:r+"...",len:t+3};t+=2}else{if(t+1>e)return{sub:r+"...",len:t+3};t+=1}r+=i[n]}return{sub:r+"...",len:t+3}}function Kr(i){let e="B";do{if(i<1024||(e="KB",(i/=1024)<1024)||(e="MB",(i/=1024)<1024)||(e="GB",(i/=1024)<1024))break;i/=1024,e="TB"}while(!1);return i>=100?\`\${i.toFixed(0)} \${e}\`:i>=10?\`\${i.toFixed(1)} \${e}\`:\`\${i.toFixed(2)} \${e}\`}Oi.MAX_DATA_CHUNK_SIZE=10485760;class ol{constructor(e,t,r=void 0){this.lastUpdateTime=0,this.firstWrite=!0,this.speedCnt=0,this.speedIdx=0,this.timeArray=new Array(30),this.stepArray=new Array(30),this.writer=e,this.tmuxPaneColumns=r||0,this.columns=this.tmuxPaneColumns>1?this.tmuxPaneColumns-1:t}setTerminalColumns(e){this.columns=e,this.tmuxPaneColumns>0&&(this.tmuxPaneColumns=0)}onNum(e){this.fileCount=e,this.fileIdx=0}onName(e){this.fileName=e,this.fileIdx+=1,this.startTime=Date.now(),this.timeArray[0]=this.startTime,this.stepArray[0]=0,this.speedCnt=1,this.speedIdx=1,this.fileStep=-
Line truncated
|
||||
\`)}setTimeout(()=>this.detectAndHandleTrzsz(e),10),this.writeToTerminal(e)}}processTerminalInput(e){this.isTransferringFiles()?e===""&&this.stopTransferringFiles():this.sendToServer(e)}processBinaryInput(e){this.isTransferringFiles()||this.sendToServer(Ti(e))}setTerminalColumns(e){this.terminalColumns=e,this.textProgressBar!=null&&this.textProgressBar.setTerminalColumns(e)}isTransferringFiles(){return this.trzszTransfer!=null}stopTransferringFiles(){this.isTransferringFiles()&&this.trzszTransfer.stopTransferring()}uploadFiles(e){return q(this,void 0,void 0,function*(){if(this.uploadFilesList||this.isTransferringFiles())throw new Error("The previous upload has not been completed yet");if(!Ut&&(function(r,n){if(!Array.isArray(r))return!1;for(const o of r)if(typeof o!==n)return!1;return!0})(e,"string"))this.uploadFilesList=yield Hr(e,!0);else{if(!(typeof DataTransferItemList<"u"&&e instanceof DataTransferItemList))throw new Error("The upload items type is not supported");this.uploadFilesList=yield(function(r){return q(this,void 0,void 0,function*(){const n=[],o=[];for(const _ of r)o.push(_.webkitGetAsEntry());for(let _=0;_<o.length;_++){const s=o[_];s&&(yield io(_,s,n,[s.name]))}return n})})(e)}if(!this.uploadFilesList||!this.uploadFilesList.length)throw this.uploadFilesList=null,new Error("No files to upload");let t=!1;for(const r of this.uploadFilesList)if(r.isDir()||r.getRelPath().length>1){t=!0;break}return this.uploadInterrupting=!0,this.sendToServer(""),yield new Promise(r=>setTimeout(r,200)),this.uploadInterrupting=!1,this.uploadSkipTrzCommand=!0,this.sendToServer(t?"trz -d\\r":"trz\\r"),setTimeout(()=>{this.uploadFilesList&&(this.uploadFilesList=null,this.uploadFilesResolve=null,this.uploadFilesReject&&(this.uploadFilesReject("Upload does not start"),this.uploadFilesReject=null))},this.dragInitTimeout),new Promise((r,n)=>{this.uploadFilesResolve=r,this.uploadFilesReject=n})})}uniqueIdExists(e){if(e.length<8||!this.isWindowsShell&&e.length==14&&e.endsWith("00"))return!1;if(this.uniqueIdMaps.has(e))return!0;if(this.uniqueIdMaps.size>=100){const t=new Map;for(const[r,n]of this.uniqueIdMaps)n>=50&&t.set(r,n-50);this.uniqueIdMaps=t}return this.uniqueIdMaps.set(e,this.uniqueIdMaps.size),!1}detectAndHandleTrzsz(e){return q(this,void 0,void 0,function*(){const t=yield(function(h){return q(this,void 0,void 0,function*(){if(typeof h=="string"){const p=h.lastIndexOf(so);return p<0?null:h.substring(p)}let a;if(h instanceof ArrayBuffer)a=new Uint8Array(h);else if(h instanceof Uint8Array)a=h;else{if(!(h instanceof Blob))return null;a=new Uint8Array(yield h.arrayBuffer())}if(a.length<26)return null;let d=-1,u=-1;for(;;){if(d=a.indexOf(58,d+1),d<0||a.length-d<26)return u>=0?Di(a.subarray(u)):null;const p=new Float64Array(a.buffer.slice(a.byteOffset+d,a.byteOffset+d+16));p[0]==Vr[0]&&p[1]==Vr[1]&&(u=d,d+=25)}})})(e);if(!t)return;const r=t.match(al);if(!r)return;const n=r.length>3?r[3]:"";if(this.uniqueIdExists(n))return;const o=r[1],_=r[2];let s=!1;(n==":1"||n.length==14&&n.endsWith("10"))&&(s=!0);try{this.trzszTransfer=new Oi(this.sendToServer,this.isWindowsShell,this.maxDataChunkSize),o==="S"?yield this.handleTrzszDownloadFiles(_,s):o==="R"?yield this.handleTrzszUploadFiles(_,!1,s):o==="D"&&(yield this.handleTrzszUploadFiles(_,!0,s)),this.uploadFilesResolve&&this.uploadFilesResolve()}catch(h){yield this.trzszTransfer.clientError(h),this.uploadFilesReject&&this.uploadFilesReject(h)}finally{this.uploadFilesResolve=null,this.uploadFilesReject=null,this.trzszTransfer.cleanup(),this.textProgressBar&&this.textProgressBar.showCursor(),this.textProgressBar=null,this.trzszTransfer=null}})}createProgressBar(e,t){e!==!0?(this.textProgressBar=new ol(this.writeToTerminal,this.terminalColumns,t),this.textProgressBar.hideCursor()):this.textProgressBar=null}handleTrzszDownloadFiles(e,t){return q(this,void 0,void 0,function*(){let r,n,o;if(Ut){const h=yield(function(){return q(this,void 0,void 0,function*(){if(typeof window.showDirectoryPicker!="function")throw Bs();try{return yield window.showDirectoryPicker({id:"trzsz_download",startIn:"downloads",mode:"readwrite"})}catch(a){if(a.name==="AbortError")return;throw a}})})();if(!h)return void(yield this.trzszTransfer.sendAction(!1,t));r=h.name,n={handle:h,maps:new Map},o=il}else{if(r=yield this.chooseSaveDirectory(),!r)return void(yield this.trzszTransfer.sendAction(!1,t));yield(function(h){return q(this,void 0,void 0,function*(){if(!h)return!1;if(!(yield te.accessAsync(h)))throw new X(\`No such directory: \${h}\`);if(!(yield te.statAsync(h)).isDirectory())throw new X(\`Not a directory: \${h}\`);if(!(yield te.accessAsync(h,te.constants.W_OK)))throw new X(\`No permission to write: \${h}\`);return!0})})(r),n={path:r,maps:new Map},o=el}yield this.trzszTransfer.sendAction(!0,t);const _=yield this.trzszTransfer.recvConfig();this.createProgressBar(_.quiet,_.tmux_pane_width);const s=yield this.trzszTransfer.recvFiles(n,o,this.textProgressBar);yield this.trzszTransfer.clientExit(Fr(s,r))})
Line truncated
|
||||
<button type="button" id="github-login-btn" class="github-login-btn text-[11px] font-bold tracking-[0.1em] text-[#bbccb0] hover:text-[#4af626] transition-all cursor-pointer flex items-center gap-1.5 bg-transparent border border-[#3c4b36] px-3 py-1 hover:border-[#4af626]">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
||||
LOGIN
|
||||
</button>
|
||||
\`,(_=document.getElementById("github-login-btn"))==null||_.addEventListener("click",()=>{window.location.href="/api/auth/github"}))}renderTurnstile(){const e=document.getElementById("turnstile-widget");if(!e||!window.turnstile)return;const _=document.getElementById("turnstile-container");_&&(_.style.display="block"),this.turnstileWidgetId=window.turnstile.render(e,{sitekey:this.turnstileSitekey,theme:"dark",callback:async C=>{try{if((await(await fetch("/api/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:C})})).json()).success){this.turnstileVerified=!0;const w=document.getElementById("turnstile-container");w&&(w.style.display="none")}}catch{this.turnstileVerified=!1}},"expired-callback":()=>{this.turnstileVerified=!1},"error-callback":()=>{this.turnstileVerified=!1}})}render(){const e=document.getElementById("connection-form-container");e.innerHTML=\`
|
||||
\`,(t=document.getElementById("github-login-btn"))==null||t.addEventListener("click",()=>{window.location.href="/api/auth/github"}))}renderTurnstile(){const e=document.getElementById("turnstile-widget");if(!e||!window.turnstile)return;const t=document.getElementById("turnstile-container");t&&(t.style.display="block"),this.turnstileWidgetId=window.turnstile.render(e,{sitekey:this.turnstileSitekey,theme:"dark",callback:async r=>{try{if((await(await fetch("/api/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:r})})).json()).success){this.turnstileVerified=!0;const _=document.getElementById("turnstile-container");_&&(_.style.display="none")}}catch{this.turnstileVerified=!1}},"expired-callback":()=>{this.turnstileVerified=!1},"error-callback":()=>{this.turnstileVerified=!1}})}render(){const e=document.getElementById("connection-form-container");e.innerHTML=\`
|
||||
<form class="space-y-6" id="connection-form">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="col-span-3">
|
||||
@@ -190,10 +198,10 @@ void main() {
|
||||
<span id="github-login-placeholder"></span>
|
||||
</div>
|
||||
</form>
|
||||
\`,document.getElementById("connect-btn").addEventListener("click",()=>{this.handleConnect()}),document.getElementById("connection-form").addEventListener("keypress",_=>{_.key==="Enter"&&this.handleConnect()}),document.getElementById("auth-tab-password").addEventListener("click",()=>{this.setAuthMode("password")}),document.getElementById("auth-tab-key").addEventListener("click",()=>{this.setAuthMode("key")})}setAuthMode(e){this.authMode=e;const _=document.getElementById("auth-tab-password"),C=document.getElementById("auth-tab-key"),g=document.getElementById("auth-password-section"),k=document.getElementById("auth-key-section");e==="password"?(_.style.background="#4af626",_.style.color="#0a0a0a",_.style.borderColor="#4af626",C.style.background="transparent",C.style.color="#bbccb0",C.style.borderColor="#3c4b36",g.style.display="",k.style.display="none"):(C.style.background="#4af626",C.style.color="#0a0a0a",C.style.borderColor="#4af626",_.style.background="transparent",_.style.color="#bbccb0",_.style.borderColor="#3c4b36",k.style.display="",g.style.display="none")}async loadSavedCredentials(){const e=localStorage.getItem("cloudssh_cred");if(!e)return;const _=await Un(e);if(!_){localStorage.removeItem("cloudssh_cred");return}document.getElementById("host").value=_.host||"",document.getElementById("port").value=_.port||"22",document.getElementById("username").value=_.username||"",document.getElementById("password").value=_.password||"",document.getElementById("private-key").value=_.privateKey||"",document.getElementById("remember-me").checked=!0,_.authMethod==="key"?this.setAuthMode("key"):this.setAuthMode("password")}async handleConnect(){const _=document.getElementById("host").value.replace(/^\\[|\\]$/g,"").trim(),C=parseInt(document.getElementById("port").value||"22"),g=document.getElementById("username").value,k=document.getElementById("password").value,w=document.getElementById("private-key").value,s=document.getElementById("remember-me").checked;if(!_||!g){alert("请填写主机名和用户名");return}if(this.authMode==="password"&&!k){alert("请输入密码");return}if(this.authMode==="key"&&!w){alert("请粘贴私钥内容");return}if(this.turnstileEnabled&&!this.turnstileVerified){alert("请完成人机验证");return}if(s){const d=await $n({host:_,port:C.toString(),username:g,password:k,privateKey:w,authMethod:this.authMode==="key"?"publickey":"password"});localStorage.setItem("cloudssh_cred",d)}else localStorage.removeItem("cloudssh_cred");const a=document.getElementById("auth-section"),l=document.getElementById("terminal-section");a.classList.add("hidden"),l.classList.remove("hidden"),l.classList.add("flex"),document.getElementById("term-host").textContent="Host: "+_,document.getElementById("term-user").textContent="User: "+g,document.getElementById("term-port").textContent="Port: "+C,this.terminal.mount();try{await this.terminal.connect({host:_,port:C,username:g,password:k,authMethod:this.authMode==="key"?"publickey":"password",privateKey:w})}catch{l.classList.add("hidden"),l.classList.remove("flex"),a.classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'}}}class jn{constructor(e,_,C){Y(this,"user");Y(this,"servers",[]);Y(this,"onConnect");Y(this,"onLogout");Y(this,"editingServerId",null);Y(this,"modalAuthMode","password");this.user=e,this.onConnect=_,this.onLogout=C,this.init()}async init(){this.renderUserInfo(),this.bindEvents(),await this.fetchServers();const e=document.getElementById("user-copyright-year");e&&(e.textContent=new Date().getFullYear().toString())}renderUserInfo(){const e=document.getElementById("user-info");e&&(e.innerHTML=\`
|
||||
\`,document.getElementById("connect-btn").addEventListener("click",()=>{this.handleConnect()}),document.getElementById("connection-form").addEventListener("keypress",t=>{t.key==="Enter"&&this.handleConnect()}),document.getElementById("auth-tab-password").addEventListener("click",()=>{this.setAuthMode("password")}),document.getElementById("auth-tab-key").addEventListener("click",()=>{this.setAuthMode("key")})}setAuthMode(e){this.authMode=e;const t=document.getElementById("auth-tab-password"),r=document.getElementById("auth-tab-key"),n=document.getElementById("auth-password-section"),o=document.getElementById("auth-key-section");e==="password"?(t.style.background="#4af626",t.style.color="#0a0a0a",t.style.borderColor="#4af626",r.style.background="transparent",r.style.color="#bbccb0",r.style.borderColor="#3c4b36",n.style.display="",o.style.display="none"):(r.style.background="#4af626",r.style.color="#0a0a0a",r.style.borderColor="#4af626",t.style.background="transparent",t.style.color="#bbccb0",t.style.borderColor="#3c4b36",o.style.display="",n.style.display="none")}async loadSavedCredentials(){const e=localStorage.getItem("cloudssh_cred");if(!e)return;const t=await dl(e);if(!t){localStorage.removeItem("cloudssh_cred");return}document.getElementById("host").value=t.host||"",document.getElementById("port").value=t.port||"22",document.getElementById("username").value=t.username||"",document.getElementById("password").value=t.password||"",document.getElementById("private-key").value=t.privateKey||"",document.getElementById("remember-me").checked=!0,t.authMethod==="key"?this.setAuthMode("key"):this.setAuthMode("password")}async handleConnect(){const t=document.getElementById("host").value.replace(/^\\[|\\]$/g,"").trim(),r=parseInt(document.getElementById("port").value||"22"),n=document.getElementById("username").value,o=document.getElementById("password").value,_=document.getElementById("private-key").value,s=document.getElementById("remember-me").checked;if(!t||!n){alert("请填写主机名和用户名");return}if(this.authMode==="password"&&!o){alert("请输入密码");return}if(this.authMode==="key"&&!_){alert("请粘贴私钥内容");return}if(this.turnstileEnabled&&!this.turnstileVerified){alert("请完成人机验证");return}if(s){const d=await cl({host:t,port:r.toString(),username:n,password:o,privateKey:_,authMethod:this.authMode==="key"?"publickey":"password"});localStorage.setItem("cloudssh_cred",d)}else localStorage.removeItem("cloudssh_cred");const h=document.getElementById("auth-section"),a=document.getElementById("terminal-section");h.classList.add("hidden"),a.classList.remove("hidden"),a.classList.add("flex"),document.getElementById("term-host").textContent="Host: "+t,document.getElementById("term-user").textContent="User: "+n,document.getElementById("term-port").textContent="Port: "+r,this.terminal.mount();try{await this.terminal.connect({host:t,port:r,username:n,password:o,authMethod:this.authMode==="key"?"publickey":"password",privateKey:_})}catch{a.classList.add("hidden"),a.classList.remove("flex"),h.classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'}}}class fl{constructor(e,t){Q(this,"user");Q(this,"servers",[]);Q(this,"onLogout");Q(this,"editingServerId",null);Q(this,"modalAuthMode","password");this.user=e,this.onLogout=t,this.init()}async init(){this.renderUserInfo(),this.bindEvents(),await this.fetchServers();const e=document.getElementById("user-copyright-year");e&&(e.textContent=new Date().getFullYear().toString())}renderUserInfo(){const e=document.getElementById("user-info");e&&(e.innerHTML=\`
|
||||
<img src="\${this.user.avatar_url}" alt="\${this.user.username}" class="user-avatar w-8 h-8" />
|
||||
<span class="text-xs font-bold tracking-[0.1em] text-[#bbccb0]">\${this.user.username}</span>
|
||||
\`)}bindEvents(){var e,_,C,g,k,w,s,a,l;(e=document.getElementById("logout-btn"))==null||e.addEventListener("click",()=>this.logout()),(_=document.getElementById("add-server-btn"))==null||_.addEventListener("click",()=>this.showModal("add")),(C=document.getElementById("empty-add-btn"))==null||C.addEventListener("click",()=>this.showModal("add")),(g=document.getElementById("modal-close-btn"))==null||g.addEventListener("click",()=>this.hideModal()),(k=document.getElementById("modal-backdrop"))==null||k.addEventListener("click",()=>this.hideModal()),(w=document.getElementById("server-submit-btn"))==null||w.addEventListener("click",()=>this.handleSubmit()),(s=document.getElementById("modal-auth-tab-password"))==null||s.addEventListener("click",()=>this.setModalAuthMode("password")),(a=document.getElementById("modal-auth-tab-key"))==null||a.addEventListener("click",()=>this.setModalAuthMode("key")),(l=document.getElementById("server-form"))==null||l.addEventListener("keypress",d=>{d.key==="Enter"&&(d.preventDefault(),this.handleSubmit())})}async fetchServers(){try{const e=await fetch("/api/servers");if(!e.ok)throw new Error("Failed to fetch servers");this.servers=await e.json(),this.renderServerGrid()}catch(e){console.error("Failed to fetch servers:",e),this.servers=[],this.renderServerGrid()}}renderServerGrid(){const e=document.getElementById("server-grid"),_=document.getElementById("empty-state");if(!(!e||!_)){if(this.servers.length===0){e.innerHTML="",_.classList.remove("hidden"),_.classList.add("flex");return}_.classList.add("hidden"),_.classList.remove("flex"),e.innerHTML=this.servers.map(C=>this.renderServerCard(C)).join(""),this.servers.forEach(C=>{var g,k,w;(g=document.getElementById(\`connect-\${C.id}\`))==null||g.addEventListener("click",()=>this.connectServer(C.id)),(k=document.getElementById(\`edit-\${C.id}\`))==null||k.addEventListener("click",()=>this.showModal("edit",C)),(w=document.getElementById(\`delete-\${C.id}\`))==null||w.addEventListener("click",()=>this.deleteServer(C.id))})}}renderServerCard(e){const _=e.auth_method==="publickey"?"vpn_key":"password",C=e.auth_method==="publickey"?"KEY":"PWD";return\`
|
||||
\`)}bindEvents(){var e,t,r,n,o,_,s,h,a;(e=document.getElementById("logout-btn"))==null||e.addEventListener("click",()=>this.logout()),(t=document.getElementById("add-server-btn"))==null||t.addEventListener("click",()=>this.showModal("add")),(r=document.getElementById("empty-add-btn"))==null||r.addEventListener("click",()=>this.showModal("add")),(n=document.getElementById("modal-close-btn"))==null||n.addEventListener("click",()=>this.hideModal()),(o=document.getElementById("modal-backdrop"))==null||o.addEventListener("click",()=>this.hideModal()),(_=document.getElementById("server-submit-btn"))==null||_.addEventListener("click",()=>this.handleSubmit()),(s=document.getElementById("modal-auth-tab-password"))==null||s.addEventListener("click",()=>this.setModalAuthMode("password")),(h=document.getElementById("modal-auth-tab-key"))==null||h.addEventListener("click",()=>this.setModalAuthMode("key")),(a=document.getElementById("server-form"))==null||a.addEventListener("keypress",d=>{d.key==="Enter"&&(d.preventDefault(),this.handleSubmit())})}async fetchServers(){try{const e=await fetch("/api/servers");if(!e.ok)throw new Error("Failed to fetch servers");this.servers=await e.json(),this.renderServerGrid()}catch(e){console.error("Failed to fetch servers:",e),this.servers=[],this.renderServerGrid()}}renderServerGrid(){const e=document.getElementById("server-grid"),t=document.getElementById("empty-state");if(!(!e||!t)){if(this.servers.length===0){e.innerHTML="",t.classList.remove("hidden"),t.classList.add("flex");return}t.classList.add("hidden"),t.classList.remove("flex"),e.innerHTML=this.servers.map(r=>this.renderServerCard(r)).join(""),this.servers.forEach(r=>{var n,o,_;(n=document.getElementById(\`connect-\${r.id}\`))==null||n.addEventListener("click",()=>this.connectServer(r.id)),(o=document.getElementById(\`edit-\${r.id}\`))==null||o.addEventListener("click",()=>this.showModal("edit",r)),(_=document.getElementById(\`delete-\${r.id}\`))==null||_.addEventListener("click",()=>this.deleteServer(r.id))})}}renderServerCard(e){const t=e.auth_method==="publickey"?"vpn_key":"password",r=e.auth_method==="publickey"?"KEY":"PWD";return\`
|
||||
<div class="server-card p-5 relative group" id="card-\${e.id}">
|
||||
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-[#3c4b36] to-transparent group-hover:via-[#4af626] transition-all duration-300"></div>
|
||||
|
||||
@@ -203,8 +211,8 @@ void main() {
|
||||
<h3 class="text-sm font-bold text-[#4af626] tracking-[0.05em]">\${this.escapeHtml(e.name)}</h3>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold tracking-[0.1em] text-[#bbccb0] border border-[#3c4b36] px-2 py-0.5 flex items-center gap-1">
|
||||
<span class="material-symbols-outlined" style="font-size: 12px;">\${_}</span>
|
||||
\${C}
|
||||
<span class="material-symbols-outlined" style="font-size: 12px;">\${t}</span>
|
||||
\${r}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -232,25 +240,25 @@ void main() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
\`}async connectServer(e){const _=this.servers.find(g=>g.id===e);if(!_)return;const C=document.getElementById(\`connect-\${e}\`);C&&(C.innerHTML=\`
|
||||
\`}async connectServer(e){const t=this.servers.find(n=>n.id===e);if(!t)return;const r=document.getElementById(\`connect-\${e}\`);r&&(r.innerHTML=\`
|
||||
<span class="material-symbols-outlined animate-spin" style="font-size: 14px;">progress_activity</span>
|
||||
CONNECTING...
|
||||
\`,C.disabled=!0);try{const g=await fetch(\`/api/servers/\${e}/connect\`,{method:"POST"});if(!g.ok){const w=await g.json();throw new Error(w.error||"Connection failed")}const{wsUrl:k}=await g.json();this.onConnect(k,_.name),C&&(C.innerHTML=\`
|
||||
\`,r.disabled=!0);try{const n=await fetch(\`/api/servers/\${e}/connect\`,{method:"POST"});if(!n.ok){const s=await n.json();throw new Error(s.error||"Connection failed")}const{wsUrl:o}=await n.json(),_=\`/?wsUrl=\${encodeURIComponent(o)}&name=\${encodeURIComponent(t.name)}\`;window.open(_,"_blank"),r&&(r.innerHTML=\`
|
||||
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
|
||||
CONNECT
|
||||
\`,C.disabled=!1)}catch(g){alert(\`连接失败: \${g instanceof Error?g.message:String(g)}\`),C&&(C.innerHTML=\`
|
||||
\`,r.disabled=!1)}catch(n){alert(\`连接失败: \${n instanceof Error?n.message:String(n)}\`),r&&(r.innerHTML=\`
|
||||
<span class="material-symbols-outlined" style="font-size: 14px;">power_settings_new</span>
|
||||
CONNECT
|
||||
\`,C.disabled=!1)}}async deleteServer(e){const _=this.servers.find(C=>C.id===e);if(_&&confirm(\`确认删除服务器 "\${_.name}" ?\`))try{const C=document.getElementById(\`card-\${e}\`);if(C&&C.classList.add("removing"),!(await fetch(\`/api/servers/\${e}\`,{method:"DELETE"})).ok)throw new Error("Delete failed");await new Promise(k=>setTimeout(k,300)),this.servers=this.servers.filter(k=>k.id!==e),this.renderServerGrid()}catch(C){alert(\`删除失败: \${C instanceof Error?C.message:String(C)}\`),await this.fetchServers()}}showModal(e,_){this.editingServerId=e==="edit"&&_?_.id:null;const C=document.getElementById("server-modal"),g=document.getElementById("modal-title"),k=document.getElementById("server-submit-btn");!C||!g||!k||(g.textContent=e==="add"?"ADD_SERVER":"EDIT_SERVER",k.innerHTML=\`
|
||||
\`,r.disabled=!1)}}async deleteServer(e){const t=this.servers.find(r=>r.id===e);if(t&&confirm(\`确认删除服务器 "\${t.name}" ?\`))try{const r=document.getElementById(\`card-\${e}\`);if(r&&r.classList.add("removing"),!(await fetch(\`/api/servers/\${e}\`,{method:"DELETE"})).ok)throw new Error("Delete failed");await new Promise(o=>setTimeout(o,300)),this.servers=this.servers.filter(o=>o.id!==e),this.renderServerGrid()}catch(r){alert(\`删除失败: \${r instanceof Error?r.message:String(r)}\`),await this.fetchServers()}}showModal(e,t){this.editingServerId=e==="edit"&&t?t.id:null;const r=document.getElementById("server-modal"),n=document.getElementById("modal-title"),o=document.getElementById("server-submit-btn");!r||!n||!o||(n.textContent=e==="add"?"ADD_SERVER":"EDIT_SERVER",o.innerHTML=\`
|
||||
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
|
||||
\${e==="add"?"SAVE_SERVER":"UPDATE_SERVER"}
|
||||
\`,e==="edit"&&_?(document.getElementById("server-name").value=_.name,document.getElementById("server-host").value=_.host,document.getElementById("server-port").value=_.port.toString(),document.getElementById("server-username").value=_.username,document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",_.auth_method==="publickey"?this.setModalAuthMode("key"):this.setModalAuthMode("password")):(document.getElementById("server-name").value="",document.getElementById("server-host").value="",document.getElementById("server-port").value="22",document.getElementById("server-username").value="",document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",this.setModalAuthMode("password")),C.classList.remove("hidden"),C.classList.add("flex"),setTimeout(()=>{var w;(w=document.getElementById("server-name"))==null||w.focus()},100))}hideModal(){const e=document.getElementById("server-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex")),this.editingServerId=null}setModalAuthMode(e){this.modalAuthMode=e;const _=document.getElementById("modal-auth-tab-password"),C=document.getElementById("modal-auth-tab-key"),g=document.getElementById("modal-password-section"),k=document.getElementById("modal-key-section");e==="password"?(_.style.background="#4af626",_.style.color="#0a0a0a",_.style.borderColor="#4af626",C.style.background="transparent",C.style.color="#bbccb0",C.style.borderColor="#3c4b36",g.style.display="",k.style.display="none"):(C.style.background="#4af626",C.style.color="#0a0a0a",C.style.borderColor="#4af626",_.style.background="transparent",_.style.color="#bbccb0",_.style.borderColor="#3c4b36",k.style.display="",g.style.display="none")}async handleSubmit(){const e=document.getElementById("server-name").value.trim(),_=document.getElementById("server-host").value.trim(),C=parseInt(document.getElementById("server-port").value||"22"),g=document.getElementById("server-username").value.trim(),k=document.getElementById("server-password").value,w=document.getElementById("server-private-key").value;if(!e||!_||!g){alert("请填写服务器名称、主机和用户名");return}const s=this.modalAuthMode==="key"?"publickey":"password",a=s==="publickey"?w:k;if(!this.editingServerId&&!a){alert(s==="publickey"?"请粘贴私钥内容":"请输入密码");return}const l=document.getElementById("server-submit-btn");l.disabled=!0,l.innerHTML=\`
|
||||
\`,e==="edit"&&t?(document.getElementById("server-name").value=t.name,document.getElementById("server-host").value=t.host,document.getElementById("server-port").value=t.port.toString(),document.getElementById("server-username").value=t.username,document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",t.auth_method==="publickey"?this.setModalAuthMode("key"):this.setModalAuthMode("password")):(document.getElementById("server-name").value="",document.getElementById("server-host").value="",document.getElementById("server-port").value="22",document.getElementById("server-username").value="",document.getElementById("server-password").value="",document.getElementById("server-private-key").value="",this.setModalAuthMode("password")),r.classList.remove("hidden"),r.classList.add("flex"),setTimeout(()=>{var _;(_=document.getElementById("server-name"))==null||_.focus()},100))}hideModal(){const e=document.getElementById("server-modal");e&&(e.classList.add("hidden"),e.classList.remove("flex")),this.editingServerId=null}setModalAuthMode(e){this.modalAuthMode=e;const t=document.getElementById("modal-auth-tab-password"),r=document.getElementById("modal-auth-tab-key"),n=document.getElementById("modal-password-section"),o=document.getElementById("modal-key-section");e==="password"?(t.style.background="#4af626",t.style.color="#0a0a0a",t.style.borderColor="#4af626",r.style.background="transparent",r.style.color="#bbccb0",r.style.borderColor="#3c4b36",n.style.display="",o.style.display="none"):(r.style.background="#4af626",r.style.color="#0a0a0a",r.style.borderColor="#4af626",t.style.background="transparent",t.style.color="#bbccb0",t.style.borderColor="#3c4b36",o.style.display="",n.style.display="none")}async handleSubmit(){const e=document.getElementById("server-name").value.trim(),t=document.getElementById("server-host").value.trim(),r=parseInt(document.getElementById("server-port").value||"22"),n=document.getElementById("server-username").value.trim(),o=document.getElementById("server-password").value,_=document.getElementById("server-private-key").value;if(!e||!t||!n){alert("请填写服务器名称、主机和用户名");return}const s=this.modalAuthMode==="key"?"publickey":"password",h=s==="publickey"?_:o;if(!this.editingServerId&&!h){alert(s==="publickey"?"请粘贴私钥内容":"请输入密码");return}const a=document.getElementById("server-submit-btn");a.disabled=!0,a.innerHTML=\`
|
||||
<span class="material-symbols-outlined animate-spin" style="font-size: 18px;">progress_activity</span>
|
||||
SAVING...
|
||||
\`;try{const d={name:e,host:_,port:C,username:g,auth_method:s};a&&(d.credential=a);let h;if(this.editingServerId?h=await fetch(\`/api/servers/\${this.editingServerId}\`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}):h=await fetch("/api/servers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}),!h.ok){const u=await h.json();throw new Error(u.error||"Save failed")}this.hideModal(),await this.fetchServers()}catch(d){alert(\`保存失败: \${d instanceof Error?d.message:String(d)}\`)}finally{l.disabled=!1,l.innerHTML=\`
|
||||
\`;try{const d={name:e,host:t,port:r,username:n,auth_method:s};h&&(d.credential=h);let u;if(this.editingServerId?u=await fetch(\`/api/servers/\${this.editingServerId}\`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}):u=await fetch("/api/servers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)}),!u.ok){const p=await u.json();throw new Error(p.error||"Save failed")}this.hideModal(),await this.fetchServers()}catch(d){alert(\`保存失败: \${d instanceof Error?d.message:String(d)}\`)}finally{a.disabled=!1,a.innerHTML=\`
|
||||
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
|
||||
\${this.editingServerId?"UPDATE_SERVER":"SAVE_SERVER"}
|
||||
\`}}async logout(){try{await fetch("/api/auth/logout",{method:"POST"})}catch{}this.onLogout()}escapeHtml(e){const _=document.createElement("div");return _.textContent=e,_.innerHTML}}const ot=new Wn("terminal-container");let is=null,ni=!1;function Hs(){document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),document.getElementById("server-modal").classList.add("hidden"),document.getElementById("server-modal").classList.remove("flex"),is||(is=new zn(ot))}function Zn(r){ni=!0,document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),new jn(r,(e,_)=>{qn(e,_)},()=>{ni=!1,Hs()})}function qn(r,e){document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.remove("hidden"),document.getElementById("terminal-section").classList.add("flex"),document.getElementById("term-host").textContent=\`Server: \${e}\`,document.getElementById("term-user").textContent="",document.getElementById("term-port").textContent="",ot.mount();const _=new WebSocket(r);ot.connectWithWebSocket(_)}var os;(os=document.getElementById("disconnect-btn"))==null||os.addEventListener("click",()=>{ot.disconnect();const r=document.getElementById("terminal-section");r.classList.add("hidden"),r.classList.remove("flex"),ni?(document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex")):document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'});var as;(as=document.getElementById("theme-selector"))==null||as.addEventListener("change",r=>{const e=r.target.value;ot.setTheme(e)});async function Kn(){const r=document.getElementById("copyright-year");r&&(r.textContent=new Date().getFullYear().toString());try{const e=await fetch("/api/auth/me");if(e.ok){const _=await e.json();Zn(_);return}}catch{}Hs()}Kn();
|
||||
\`}}async logout(){try{await fetch("/api/auth/logout",{method:"POST"})}catch{}this.onLogout()}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}}const Rt=new ll("terminal-container");let Zr=null,Ms=!1;function _l(){return new URLSearchParams(window.location.search).has("wsUrl")}function gl(){var n;const i=new URLSearchParams(window.location.search),e=i.get("wsUrl"),t=i.get("name")||"Server";document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.remove("hidden"),document.getElementById("terminal-section").classList.add("flex"),document.getElementById("term-host").textContent=\`Server: \${t}\`,document.getElementById("term-user").textContent="",document.getElementById("term-port").textContent="",Rt.mount();const r=new WebSocket(e);r.binaryType="arraybuffer",Rt.connectWithWebSocket(r),(n=document.getElementById("disconnect-btn"))==null||n.addEventListener("click",()=>{Rt.disconnect(),window.close()})}function no(){document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),document.getElementById("server-modal").classList.add("hidden"),document.getElementById("server-modal").classList.remove("flex"),Zr||(Zr=new ul(Rt))}function pl(i){Ms=!0,document.getElementById("auth-section").classList.add("hidden"),document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex"),document.getElementById("terminal-section").classList.add("hidden"),document.getElementById("terminal-section").classList.remove("flex"),new fl(i,()=>{Ms=!1,no()})}var Qr;(Qr=document.getElementById("disconnect-btn"))==null||Qr.addEventListener("click",()=>{Rt.disconnect();const i=document.getElementById("terminal-section");i.classList.add("hidden"),i.classList.remove("flex"),Ms?(document.getElementById("user-space-section").classList.remove("hidden"),document.getElementById("user-space-section").classList.add("flex")):document.getElementById("auth-section").classList.remove("hidden"),document.getElementById("status-text").innerHTML='<span class="w-2 h-2 bg-[#353534] inline-block"></span> STATUS: OFFLINE'});var en;(en=document.getElementById("theme-selector"))==null||en.addEventListener("change",i=>{const e=i.target.value;Rt.setTheme(e)});async function vl(){const i=document.getElementById("copyright-year");if(i&&(i.textContent=new Date().getFullYear().toString()),_l()){gl();return}try{const e=await fetch("/api/auth/me");if(e.ok){const t=await e.json();pl(t);return}}catch{}no()}vl();
|
||||
</script>
|
||||
<style>*{margin:0;padding:0;box-sizing:border-box}body{font-family:JetBrains Mono,monospace;background-color:#0a0a0a;color:#4af626;min-height:100vh;overflow:hidden}.scanlines{position:fixed;top:0;left:0;width:100vw;height:100vh;background:linear-gradient(to bottom,#fff0,#fff0 50%,#0000001a 50%,#0000001a);background-size:100% 4px;pointer-events:none;z-index:50}.flicker{animation:flicker .15s infinite;pointer-events:none;position:fixed;top:0;left:0;width:100%;height:100%;background:#4af62605;z-index:49}@keyframes flicker{0%{opacity:.8}50%{opacity:1}to{opacity:.9}}.terminal-input{background:transparent;border:none;border-bottom:1px solid #3c4b36;color:#4af626;font-family:JetBrains Mono,monospace;padding:8px 0;width:100%;outline:none;transition:border-color .2s}.terminal-input:focus{border-bottom:1px solid #4af626;box-shadow:none}.terminal-input::placeholder{color:#3c4b36}.blinking-cursor:after{content:"█";color:#14d1ff;animation:blink 1s step-end infinite;margin-left:4px;font-size:.9em}@keyframes blink{0%,to{opacity:1}50%{opacity:0}}.cyber-box{background-color:#121212;border:1px solid #1f1f1f}.cyber-button{border:1px solid #4af626;color:#4af626;transition:all .2s ease;position:relative;overflow:hidden;cursor:pointer}.cyber-button:hover{background-color:#4af626;color:#0a0a0a}.cursor-blink{animation:blink 1s step-end infinite}.scanline{background:linear-gradient(to bottom,#fff0,#fff0 50%,#0000001a 50%,#0000001a);background-size:100% 4px}.custom-scrollbar::-webkit-scrollbar{width:8px}.custom-scrollbar::-webkit-scrollbar-track{background:#1c1b1b80}.custom-scrollbar::-webkit-scrollbar-thumb{background:#3c4b36cc;border-radius:4px}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background:#86957dcc}.server-card{background-color:#121212;border:1px solid #1f1f1f;transition:all .3s ease;position:relative}.server-card:hover{border-color:#4af626;transform:translateY(-2px);box-shadow:0 4px 20px #4af62614}.server-card.removing{animation:fadeOut .3s ease forwards}@keyframes fadeOut{to{opacity:0;transform:translateY(-10px)}}.modal-overlay{background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.user-avatar{border:2px solid #4af626;border-radius:50%;transition:border-color .2s}.user-avatar:hover{border-color:#14d1ff}.github-login-btn{transition:all .2s ease}.github-login-btn:hover{border-color:#4af626;color:#4af626}.animate-spin{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
@@ -285,207 +293,207 @@ void main() {
|
||||
* other features.
|
||||
*/.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:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.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}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen font-body text-sm overflow-hidden">
|
||||
<div id="app">
|
||||
<!-- Login Page (匿名连接) -->
|
||||
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
|
||||
<div class="scanlines"></div>
|
||||
<div class="flicker"></div>
|
||||
<main class="w-full max-w-md relative z-10">
|
||||
<div class="mb-8 text-center">
|
||||
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
|
||||
</div>
|
||||
<div class="cyber-box p-6 shadow-2xl relative">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
|
||||
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
|
||||
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
|
||||
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
|
||||
</div>
|
||||
<div id="connection-form-container"></div>
|
||||
</div>
|
||||
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- User Space Page (登录后) -->
|
||||
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
|
||||
<div class="flex items-center gap-6">
|
||||
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div id="user-info" class="flex items-center gap-3">
|
||||
<!-- 动态渲染:头像 + 用户名 -->
|
||||
</div>
|
||||
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Server List Area -->
|
||||
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
|
||||
<div class="scanlines pointer-events-none"></div>
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
|
||||
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
|
||||
</div>
|
||||
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
|
||||
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
|
||||
ADD_SERVER
|
||||
</button>
|
||||
</div>
|
||||
<!-- 服务器卡片网格 -->
|
||||
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- 由 JS 动态渲染 -->
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
|
||||
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
|
||||
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
|
||||
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
|
||||
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
|
||||
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
|
||||
ADD_FIRST_SERVER
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
|
||||
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Server Modal (添加/编辑) -->
|
||||
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
|
||||
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
|
||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
|
||||
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
|
||||
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
|
||||
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="server-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
|
||||
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="col-span-3">
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-[#bbccb0] mr-2">></span>
|
||||
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-[#bbccb0] mr-2">:</span>
|
||||
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
|
||||
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
|
||||
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
|
||||
</div>
|
||||
<div id="modal-password-section">
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
|
||||
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-key-section" style="display:none;">
|
||||
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ...粘贴私钥内容... -----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
|
||||
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
|
||||
SAVE_SERVER
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Page -->
|
||||
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
|
||||
<div class="flex items-center gap-6">
|
||||
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-primary-container">
|
||||
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
|
||||
<option value="cyberpunk">Cyberpunk</option>
|
||||
<option value="glacier">Glacier</option>
|
||||
<option value="gruvbox">Gruvbox</option>
|
||||
</select>
|
||||
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Terminal -->
|
||||
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
|
||||
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
|
||||
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
|
||||
<!-- Status Bar -->
|
||||
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
|
||||
<div class="flex gap-6">
|
||||
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
|
||||
<span id="term-host">Host: -</span>
|
||||
<span id="term-user">User: -</span>
|
||||
<span id="term-port">Port: -</span>
|
||||
</div>
|
||||
<div>
|
||||
<span id="term-info"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Terminal Output -->
|
||||
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
|
||||
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body class="min-h-screen font-body text-sm overflow-hidden">
|
||||
<div id="app">
|
||||
<!-- Login Page (匿名连接) -->
|
||||
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
|
||||
<div class="scanlines"></div>
|
||||
<div class="flicker"></div>
|
||||
<main class="w-full max-w-md relative z-10">
|
||||
<div class="mb-8 text-center">
|
||||
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
|
||||
</div>
|
||||
<div class="cyber-box p-6 shadow-2xl relative">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
|
||||
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
|
||||
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
|
||||
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
|
||||
</div>
|
||||
<div id="connection-form-container"></div>
|
||||
</div>
|
||||
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- User Space Page (登录后) -->
|
||||
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
|
||||
<div class="flex items-center gap-6">
|
||||
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div id="user-info" class="flex items-center gap-3">
|
||||
<!-- 动态渲染:头像 + 用户名 -->
|
||||
</div>
|
||||
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Server List Area -->
|
||||
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
|
||||
<div class="scanlines pointer-events-none"></div>
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
|
||||
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
|
||||
</div>
|
||||
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
|
||||
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
|
||||
ADD_SERVER
|
||||
</button>
|
||||
</div>
|
||||
<!-- 服务器卡片网格 -->
|
||||
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- 由 JS 动态渲染 -->
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
|
||||
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
|
||||
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
|
||||
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
|
||||
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
|
||||
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
|
||||
ADD_FIRST_SERVER
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
|
||||
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Server Modal (添加/编辑) -->
|
||||
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
|
||||
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
|
||||
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
|
||||
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
|
||||
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
|
||||
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="server-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
|
||||
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="col-span-3">
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-[#bbccb0] mr-2">></span>
|
||||
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
|
||||
<div class="flex items-center">
|
||||
<span class="text-[#bbccb0] mr-2">:</span>
|
||||
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
|
||||
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
|
||||
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
|
||||
</div>
|
||||
<div id="modal-password-section">
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
|
||||
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-key-section" style="display:none;">
|
||||
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ...粘贴私钥内容... -----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
|
||||
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
|
||||
SAVE_SERVER
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Page -->
|
||||
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
|
||||
<div class="flex items-center gap-6">
|
||||
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-primary-container">
|
||||
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
|
||||
<option value="cyberpunk">Cyberpunk</option>
|
||||
<option value="glacier">Glacier</option>
|
||||
<option value="gruvbox">Gruvbox</option>
|
||||
</select>
|
||||
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Terminal -->
|
||||
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
|
||||
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
|
||||
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
|
||||
<!-- Status Bar -->
|
||||
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
|
||||
<div class="flex gap-6">
|
||||
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
|
||||
<span id="term-host">Host: -</span>
|
||||
<span id="term-user">User: -</span>
|
||||
<span id="term-port">Port: -</span>
|
||||
</div>
|
||||
<div>
|
||||
<span id="term-info"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Terminal Output -->
|
||||
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
|
||||
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
+204
-76
@@ -25,10 +25,22 @@ import {
|
||||
} from '../types';
|
||||
import { SSHTransport } from '../ssh/transport';
|
||||
import { SSHPacketParser, SSHPacketBuilder } from '../ssh/packet';
|
||||
import { KEXInitBuilder, parseKEXInit, negotiate } from '../ssh/kex';
|
||||
import {
|
||||
KEXInitBuilder,
|
||||
parseKEXInit,
|
||||
negotiate
|
||||
} from '../ssh/kex';
|
||||
import {
|
||||
getCipherSpec,
|
||||
getMacAlgorithmsForCipher,
|
||||
getMacSpec,
|
||||
KEX_ALGORITHM_ECDH_NISTP256,
|
||||
isCurve25519KEXAlgorithm
|
||||
} from '../ssh/algorithms';
|
||||
import { ECDHKeyExchange } from '../ssh/kex-ecdh';
|
||||
import { Curve25519KeyExchange, Curve25519KeyPair } from '../ssh/kex-curve25519';
|
||||
import { KeyDerivation } from '../ssh/keys';
|
||||
import { SSHAESGCMCipher } from '../ssh/crypto';
|
||||
import { SSHAESCTRCipher, SSHAESGCMCipher, SSHHMAC } from '../ssh/crypto';
|
||||
import { SSHAuth } from '../ssh/auth';
|
||||
import { SSHChannel } from '../ssh/channel';
|
||||
|
||||
@@ -41,8 +53,10 @@ export class SSHSession {
|
||||
private transport: SSHTransport;
|
||||
private packetParser: SSHPacketParser;
|
||||
private channel: SSHChannel;
|
||||
private encryptCipher: SSHAESGCMCipher | null = null;
|
||||
private decryptCipher: SSHAESGCMCipher | null = null;
|
||||
private encryptCipher: SSHAESGCMCipher | SSHAESCTRCipher | null = null;
|
||||
private decryptCipher: SSHAESGCMCipher | SSHAESCTRCipher | null = null;
|
||||
private encryptMac: SSHHMAC | null = null;
|
||||
private decryptMac: SSHHMAC | null = null;
|
||||
private derivedKeys: SessionKeys | null = null;
|
||||
|
||||
private seqNumSend: number = 0;
|
||||
@@ -52,18 +66,23 @@ export class SSHSession {
|
||||
private kexInitLocal: Uint8Array | null = null;
|
||||
private kexInitRemote: Uint8Array | null = null;
|
||||
|
||||
private ecdhKeyPair!: CryptoKeyPair;
|
||||
private ecdhRawPublicKey!: Uint8Array;
|
||||
private negotiatedKexAlgorithm: string | null = null;
|
||||
private ecdhKeyPair: CryptoKeyPair | null = null;
|
||||
private curve25519KeyPair: Curve25519KeyPair | null = null;
|
||||
private kexRawPublicKey: Uint8Array | null = null;
|
||||
|
||||
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'ready'
|
||||
private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'shell-requested' | 'ready'
|
||||
= 'connecting';
|
||||
private hostKeyFingerprint: string = '';
|
||||
|
||||
private versionRawBuffer: Uint8Array = new Uint8Array(0);
|
||||
private negotiatedCipherC2S: string = 'aes256-gcm@openssh.com';
|
||||
private negotiatedCipherS2C: string = 'aes256-gcm@openssh.com';
|
||||
private negotiatedMacC2S: string = 'none';
|
||||
private negotiatedMacS2C: string = 'none';
|
||||
|
||||
private keepaliveInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private shellReadyTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
constructor(
|
||||
ws: WebSocket,
|
||||
@@ -196,16 +215,33 @@ export class SSHSession {
|
||||
);
|
||||
await this.writeSocket(packet);
|
||||
console.log('[KEX] KEXINIT sent');
|
||||
}
|
||||
|
||||
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
|
||||
this.ecdhRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(this.ecdhKeyPair);
|
||||
private async sendKEXECDHInit(): Promise<void> {
|
||||
if (!this.negotiatedKexAlgorithm) {
|
||||
throw new Error('KEX algorithm not negotiated');
|
||||
}
|
||||
|
||||
let kexInit: Uint8Array;
|
||||
if (isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)) {
|
||||
this.curve25519KeyPair = await Curve25519KeyExchange.generateKeyPair();
|
||||
this.ecdhKeyPair = null;
|
||||
this.kexRawPublicKey = await Curve25519KeyExchange.exportRawPublicKey(this.curve25519KeyPair);
|
||||
kexInit = Curve25519KeyExchange.buildInit(this.kexRawPublicKey);
|
||||
} else if (this.negotiatedKexAlgorithm === KEX_ALGORITHM_ECDH_NISTP256) {
|
||||
this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair();
|
||||
this.curve25519KeyPair = null;
|
||||
this.kexRawPublicKey = await ECDHKeyExchange.exportRawPublicKey(this.ecdhKeyPair);
|
||||
kexInit = ECDHKeyExchange.buildInit(this.kexRawPublicKey);
|
||||
} else {
|
||||
throw new Error(`Unsupported KEX algorithm: ${this.negotiatedKexAlgorithm}`);
|
||||
}
|
||||
|
||||
const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhRawPublicKey);
|
||||
const ecdhPacket = await SSHPacketBuilder.build(
|
||||
ecdhInit, 8, null, this.seqNumSend++
|
||||
kexInit, 8, null, this.seqNumSend++
|
||||
);
|
||||
await this.writeSocket(ecdhPacket);
|
||||
console.log('[KEX] ECDH_INIT sent, waiting for server reply');
|
||||
console.log(`[KEX] ECDH_INIT sent using ${this.negotiatedKexAlgorithm}, waiting for server reply`);
|
||||
}
|
||||
|
||||
private async writeSocket(data: Uint8Array): Promise<void> {
|
||||
@@ -214,8 +250,29 @@ export class SSHSession {
|
||||
writer.releaseLock();
|
||||
}
|
||||
|
||||
private async buildEncryptedPacket(payload: Uint8Array): Promise<Uint8Array> {
|
||||
if (!this.encryptCipher) {
|
||||
throw new Error('Encryption not initialized');
|
||||
}
|
||||
|
||||
const cipher = getCipherSpec(this.negotiatedCipherC2S);
|
||||
return SSHPacketBuilder.build(
|
||||
payload,
|
||||
cipher.blockSize,
|
||||
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
|
||||
this.seqNumSend++,
|
||||
cipher.aead,
|
||||
this.encryptMac
|
||||
? (packetData, seq) => this.encryptMac!.sign(packetData, seq)
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
private async processPackets(): Promise<void> {
|
||||
const blockSize = this.decryptCipher ? 16 : 8;
|
||||
const cipher = this.decryptCipher ? getCipherSpec(this.negotiatedCipherS2C) : null;
|
||||
const blockSize = cipher ? cipher.blockSize : 8;
|
||||
const hasAuthTag = !!cipher?.aead;
|
||||
const macLength = this.decryptCipher && !hasAuthTag ? getMacSpec(this.negotiatedMacS2C).length : 0;
|
||||
const hasDecrypt = !!this.decryptCipher;
|
||||
this.sendDebug(`processPackets: blockSize=${blockSize}, hasDecrypt=${hasDecrypt}, bufferLen=${this.packetParser.getBufferLength()}`);
|
||||
|
||||
@@ -224,9 +281,13 @@ export class SSHSession {
|
||||
const packet = await this.packetParser.nextPacket(
|
||||
blockSize,
|
||||
this.decryptCipher
|
||||
? (data, seq, aad) => this.decryptCipher!.decrypt(data, seq, aad)
|
||||
? (data, seq, aad, commit) => this.decryptCipher!.decrypt(data, seq, aad, commit)
|
||||
: (data) => data,
|
||||
!!this.decryptCipher
|
||||
hasAuthTag,
|
||||
macLength,
|
||||
this.decryptMac
|
||||
? (packet, mac, seq) => this.decryptMac!.verify(packet, seq, mac)
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (!packet) {
|
||||
@@ -273,6 +334,7 @@ export class SSHSession {
|
||||
break;
|
||||
|
||||
case 'shell':
|
||||
case 'shell-requested':
|
||||
case 'ready':
|
||||
await this.handleSessionPacket(msgType, packet.payload);
|
||||
break;
|
||||
@@ -329,9 +391,17 @@ export class SSHSession {
|
||||
try {
|
||||
const serverKex = parseKEXInit(payload);
|
||||
const clientKex = parseKEXInit(this.kexInitLocal!);
|
||||
this.negotiatedCipherC2S = negotiate(clientKex.encryptionC2S, serverKex.encryptionC2S);
|
||||
this.negotiatedCipherS2C = negotiate(clientKex.encryptionS2C, serverKex.encryptionS2C);
|
||||
this.sendDebug(`Negotiated C2S: ${this.negotiatedCipherC2S}, S2C: ${this.negotiatedCipherS2C}`);
|
||||
this.negotiatedKexAlgorithm = negotiate(clientKex.kexAlgorithms, serverKex.kexAlgorithms, 'KEX algorithm');
|
||||
this.negotiatedCipherC2S = negotiate(clientKex.encryptionC2S, serverKex.encryptionC2S, 'C2S cipher');
|
||||
this.negotiatedCipherS2C = negotiate(clientKex.encryptionS2C, serverKex.encryptionS2C, 'S2C cipher');
|
||||
this.negotiatedMacC2S = getCipherSpec(this.negotiatedCipherC2S).aead
|
||||
? 'none'
|
||||
: negotiate(getMacAlgorithmsForCipher(this.negotiatedCipherC2S), serverKex.macC2S, 'C2S MAC');
|
||||
this.negotiatedMacS2C = getCipherSpec(this.negotiatedCipherS2C).aead
|
||||
? 'none'
|
||||
: negotiate(getMacAlgorithmsForCipher(this.negotiatedCipherS2C), serverKex.macS2C, 'S2C MAC');
|
||||
this.sendDebug(`Negotiated KEX: ${this.negotiatedKexAlgorithm}, C2S: ${this.negotiatedCipherC2S}/${this.negotiatedMacC2S}, S2C: ${this.negotiatedCipherS2C}/${this.negotiatedMacS2C}`);
|
||||
await this.sendKEXECDHInit();
|
||||
} catch (e) {
|
||||
const errMsg = e instanceof Error ? e.message : String(e);
|
||||
this.sendError('算法协商失败: ' + errMsg);
|
||||
@@ -386,22 +456,53 @@ export class SSHSession {
|
||||
ECDHKeyExchange.parseReply(payload);
|
||||
this.sendDebug(`ECDH_REPLY parsed: hostKey=${hostKey.length}, serverPubKey=${serverRawPublicKey.length}, sig=${signature.length}`);
|
||||
|
||||
const sharedSecret = await ECDHKeyExchange.computeSharedSecret(
|
||||
this.ecdhKeyPair.privateKey,
|
||||
serverRawPublicKey
|
||||
);
|
||||
if (!this.negotiatedKexAlgorithm || !this.kexRawPublicKey) {
|
||||
throw new Error('KEX reply received before KEX init was sent');
|
||||
}
|
||||
|
||||
let sharedSecret: Uint8Array;
|
||||
if (isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)) {
|
||||
if (!this.curve25519KeyPair) {
|
||||
throw new Error('Curve25519 key pair not initialized');
|
||||
}
|
||||
sharedSecret = await Curve25519KeyExchange.computeSharedSecret(
|
||||
this.curve25519KeyPair.privateKey,
|
||||
serverRawPublicKey
|
||||
);
|
||||
} else if (this.negotiatedKexAlgorithm === KEX_ALGORITHM_ECDH_NISTP256) {
|
||||
if (!this.ecdhKeyPair) {
|
||||
throw new Error('ECDH key pair not initialized');
|
||||
}
|
||||
sharedSecret = await ECDHKeyExchange.computeSharedSecret(
|
||||
this.ecdhKeyPair.privateKey,
|
||||
serverRawPublicKey
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Unsupported KEX algorithm: ${this.negotiatedKexAlgorithm}`);
|
||||
}
|
||||
this.sendDebug(`Shared secret: ${sharedSecret.length} bytes`);
|
||||
|
||||
const H = await ECDHKeyExchange.computeExchangeHash(
|
||||
this.transport.getLocalVersion(),
|
||||
this.transport.getRemoteVersion(),
|
||||
this.kexInitLocal!,
|
||||
this.kexInitRemote!,
|
||||
hostKey,
|
||||
this.ecdhRawPublicKey,
|
||||
serverRawPublicKey,
|
||||
sharedSecret
|
||||
);
|
||||
const H = isCurve25519KEXAlgorithm(this.negotiatedKexAlgorithm)
|
||||
? await Curve25519KeyExchange.computeExchangeHash(
|
||||
this.transport.getLocalVersion(),
|
||||
this.transport.getRemoteVersion(),
|
||||
this.kexInitLocal!,
|
||||
this.kexInitRemote!,
|
||||
hostKey,
|
||||
this.kexRawPublicKey,
|
||||
serverRawPublicKey,
|
||||
sharedSecret
|
||||
)
|
||||
: await ECDHKeyExchange.computeExchangeHash(
|
||||
this.transport.getLocalVersion(),
|
||||
this.transport.getRemoteVersion(),
|
||||
this.kexInitLocal!,
|
||||
this.kexInitRemote!,
|
||||
hostKey,
|
||||
this.kexRawPublicKey,
|
||||
serverRawPublicKey,
|
||||
sharedSecret
|
||||
);
|
||||
const hHex = Array.from(H).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
this.sendDebug(`Exchange hash H=${hHex}`);
|
||||
|
||||
@@ -449,7 +550,20 @@ export class SSHSession {
|
||||
this.sendDebug('Session ID set');
|
||||
}
|
||||
|
||||
this.derivedKeys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!);
|
||||
const cipherC2S = getCipherSpec(this.negotiatedCipherC2S);
|
||||
const cipherS2C = getCipherSpec(this.negotiatedCipherS2C);
|
||||
const macC2S = getMacSpec(this.negotiatedMacC2S);
|
||||
const macS2C = getMacSpec(this.negotiatedMacS2C);
|
||||
|
||||
this.derivedKeys = await KeyDerivation.deriveKeys(
|
||||
sharedSecret,
|
||||
H,
|
||||
this.sessionID!,
|
||||
cipherC2S.ivLength,
|
||||
cipherS2C.ivLength,
|
||||
macC2S.keyLength,
|
||||
macS2C.keyLength
|
||||
);
|
||||
this.sendDebug('Keys derived, waiting for NEWKEYS');
|
||||
}
|
||||
|
||||
@@ -622,29 +736,36 @@ export class SSHSession {
|
||||
|
||||
private async enableEncryption(): Promise<void> {
|
||||
const keys = this.derivedKeys!;
|
||||
let encKeyC2S = keys.encKeyClientToServer;
|
||||
let encKeyS2C = keys.encKeyServerToClient;
|
||||
|
||||
if (this.negotiatedCipherC2S === 'aes128-gcm@openssh.com') {
|
||||
encKeyC2S = encKeyC2S.slice(0, 16);
|
||||
}
|
||||
if (this.negotiatedCipherS2C === 'aes128-gcm@openssh.com') {
|
||||
encKeyS2C = encKeyS2C.slice(0, 16);
|
||||
}
|
||||
const cipherC2S = getCipherSpec(this.negotiatedCipherC2S);
|
||||
const cipherS2C = getCipherSpec(this.negotiatedCipherS2C);
|
||||
const encKeyC2S = keys.encKeyClientToServer.slice(0, cipherC2S.keyLength);
|
||||
const encKeyS2C = keys.encKeyServerToClient.slice(0, cipherS2C.keyLength);
|
||||
|
||||
this.sendDebug('Initializing ciphers');
|
||||
|
||||
this.encryptCipher = new SSHAESGCMCipher(
|
||||
encKeyC2S,
|
||||
keys.ivClientToServer
|
||||
);
|
||||
if (cipherC2S.mode === 'gcm') {
|
||||
this.encryptCipher = new SSHAESGCMCipher(encKeyC2S, keys.ivClientToServer);
|
||||
this.encryptMac = null;
|
||||
} else {
|
||||
this.encryptCipher = new SSHAESCTRCipher(encKeyC2S, keys.ivClientToServer);
|
||||
this.encryptMac = this.negotiatedMacC2S === 'none'
|
||||
? null
|
||||
: new SSHHMAC(this.negotiatedMacC2S, keys.integrityKeyC2S);
|
||||
}
|
||||
await this.encryptCipher.init();
|
||||
if (this.encryptMac) await this.encryptMac.init();
|
||||
|
||||
this.decryptCipher = new SSHAESGCMCipher(
|
||||
encKeyS2C,
|
||||
keys.ivServerToClient
|
||||
);
|
||||
if (cipherS2C.mode === 'gcm') {
|
||||
this.decryptCipher = new SSHAESGCMCipher(encKeyS2C, keys.ivServerToClient);
|
||||
this.decryptMac = null;
|
||||
} else {
|
||||
this.decryptCipher = new SSHAESCTRCipher(encKeyS2C, keys.ivServerToClient);
|
||||
this.decryptMac = this.negotiatedMacS2C === 'none'
|
||||
? null
|
||||
: new SSHHMAC(this.negotiatedMacS2C, keys.integrityKeyS2C);
|
||||
}
|
||||
await this.decryptCipher.init();
|
||||
if (this.decryptMac) await this.decryptMac.init();
|
||||
|
||||
this.sendDebug('Ciphers initialized');
|
||||
}
|
||||
@@ -660,15 +781,7 @@ export class SSHSession {
|
||||
console.log('[AUTH] SERVICE_REQUEST payload len=' + serviceRequest.length + ', seqNum=' + this.seqNumSend);
|
||||
console.log('[AUTH] encryptCipher exists=' + !!this.encryptCipher);
|
||||
|
||||
const packet = await SSHPacketBuilder.build(
|
||||
serviceRequest, 16,
|
||||
(data, seq, aad) => {
|
||||
console.log('[AUTH] Encrypting: dataLen=' + data.length + ', seq=' + seq + ', aadLen=' + aad?.length);
|
||||
return this.encryptCipher!.encrypt(data, seq, aad);
|
||||
},
|
||||
this.seqNumSend++,
|
||||
true
|
||||
);
|
||||
const packet = await this.buildEncryptedPacket(serviceRequest);
|
||||
console.log('[AUTH] Encrypted packet len=' + packet.length + ', first16=' + Array.from(packet.slice(0, 16)).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
await this.writeSocket(packet);
|
||||
console.log('[AUTH] SERVICE_REQUEST sent to socket');
|
||||
@@ -691,12 +804,7 @@ export class SSHSession {
|
||||
);
|
||||
}
|
||||
|
||||
const packet = await SSHPacketBuilder.build(
|
||||
authRequest, 16,
|
||||
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
|
||||
this.seqNumSend++,
|
||||
true
|
||||
);
|
||||
const packet = await this.buildEncryptedPacket(authRequest);
|
||||
await this.writeSocket(packet);
|
||||
}
|
||||
|
||||
@@ -745,21 +853,46 @@ export class SSHSession {
|
||||
|
||||
case SSH_MSG_CHANNEL_SUCCESS:
|
||||
if (this.state === 'shell') {
|
||||
// PTY 请求确认,发送 shell 请求
|
||||
const shellReq = this.channel.buildShellRequest();
|
||||
await this.sendEncrypted(shellReq);
|
||||
this.state = 'shell-requested';
|
||||
// 设置超时兜底:如果服务器不发送 shell 确认,3秒后自动进入 ready
|
||||
this.shellReadyTimeout = setTimeout(() => {
|
||||
if (this.state === 'shell-requested') {
|
||||
this.state = 'ready';
|
||||
this.sendStatus('Shell 已就绪');
|
||||
}
|
||||
}, 3000);
|
||||
} else if (this.state === 'shell-requested') {
|
||||
// Shell 请求确认,进入 ready 状态
|
||||
if (this.shellReadyTimeout) {
|
||||
clearTimeout(this.shellReadyTimeout);
|
||||
this.shellReadyTimeout = null;
|
||||
}
|
||||
this.state = 'ready';
|
||||
this.sendStatus('Shell 已就绪');
|
||||
}
|
||||
break;
|
||||
|
||||
case SSH_MSG_CHANNEL_FAILURE:
|
||||
if (this.state === 'shell') {
|
||||
if (this.state === 'shell' || this.state === 'shell-requested') {
|
||||
this.sendError('PTY 或 Shell 请求被拒绝');
|
||||
this.close();
|
||||
}
|
||||
break;
|
||||
|
||||
case SSH_MSG_CHANNEL_DATA: {
|
||||
// 某些 SSH 服务器(如 Dropbear)不会为 shell 请求发送 CHANNEL_SUCCESS,
|
||||
// 而是直接发送 shell 输出。收到 CHANNEL_DATA 说明 shell 已就绪。
|
||||
if (this.state === 'shell-requested') {
|
||||
if (this.shellReadyTimeout) {
|
||||
clearTimeout(this.shellReadyTimeout);
|
||||
this.shellReadyTimeout = null;
|
||||
}
|
||||
this.state = 'ready';
|
||||
this.sendStatus('Shell 已就绪');
|
||||
}
|
||||
const outputData = this.channel.handleChannelData(payload);
|
||||
this.ws.send(outputData.slice().buffer as ArrayBuffer);
|
||||
const adjustMsg = this.channel.buildWindowAdjust(outputData.length);
|
||||
@@ -823,16 +956,7 @@ export class SSHSession {
|
||||
|
||||
private async sendEncrypted(payload: Uint8Array): Promise<void> {
|
||||
const operation = this.sendMutex.then(async () => {
|
||||
if (!this.encryptCipher) {
|
||||
throw new Error('Encryption not initialized');
|
||||
}
|
||||
|
||||
const encrypted = await SSHPacketBuilder.build(
|
||||
payload, 16,
|
||||
(data, seq, aad) => this.encryptCipher!.encrypt(data, seq, aad),
|
||||
this.seqNumSend++,
|
||||
true
|
||||
);
|
||||
const encrypted = await this.buildEncryptedPacket(payload);
|
||||
await this.writeSocket(encrypted);
|
||||
});
|
||||
|
||||
@@ -864,6 +988,10 @@ export class SSHSession {
|
||||
clearInterval(this.keepaliveInterval);
|
||||
this.keepaliveInterval = null;
|
||||
}
|
||||
if (this.shellReadyTimeout) {
|
||||
clearTimeout(this.shellReadyTimeout);
|
||||
this.shellReadyTimeout = null;
|
||||
}
|
||||
try { this.socket.close(); } catch {}
|
||||
try { this.ws.close(normal ? 1000 : 1011); } catch {}
|
||||
}
|
||||
|
||||
+9
-6
@@ -3,12 +3,15 @@ main = "src/worker/index.ts"
|
||||
compatibility_date = "2024-12-01"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
# no_bundle = true
|
||||
workers_dev = false
|
||||
workers_dev = true
|
||||
|
||||
# 构建监视路径:仅源代码变动时触发重新构建
|
||||
[build]
|
||||
watch_dir = "."
|
||||
|
||||
# 下述注释的配置推荐采用环境变量的形式在worker页面直接手动添加
|
||||
# GitHub OAuth(可选,不配置则登录功能自动隐藏)
|
||||
# 创建 GitHub OAuth App 后,取消下方注释并填入你的 Client ID
|
||||
# Secret 通过 CLI 设置:npx wrangler secret set GITHUB_CLIENT_SECRET
|
||||
# npx wrangler secret set SESSION_SECRET
|
||||
# [vars]
|
||||
# GITHUB_CLIENT_ID = "your-github-oauth-client-id"
|
||||
# BASE_URL = "https://your-domain.com"
|
||||
@@ -19,9 +22,9 @@ workers_dev = false
|
||||
# TURNSTILE_SECRET = "your-turnstile-secret-key"
|
||||
# TURNSTILE_SITEKEY = "your-turnstile-site-key"
|
||||
|
||||
[[routes]]
|
||||
pattern = "ssh.newbietan.cn"
|
||||
custom_domain = true
|
||||
#[[routes]]
|
||||
#pattern = "自定义域名"
|
||||
#custom_domain = true
|
||||
|
||||
[[durable_objects.bindings]]
|
||||
name = "SSH_SESSION"
|
||||
|
||||
Reference in new issue
Block a user