# WebSocket Proxy HTTP Request

[![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.3+-blue.svg)](https://www.typescriptlang.org/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Status](https://img.shields.io/badge/Status-Docker%20Ready%20%7C%20Security%20Review%20Required-orange.svg)](.)

通过WebSocket将外网HTTP流量转发到内网服务的高性能代理工具。

## ✨ 特性

- 🚀 **高性能**: P99延迟30ms，RPS达629
- 🔄 **自动重连**: 断线自动恢复，最多重试3次
- 💓 **心跳保活**: 30秒心跳间隔，保持连接稳定
- 🔐 **安全认证**: 支持Token认证（可选）
- 📊 **完整日志**: 请求全链路可追踪
- 🎯 **类型安全**: TypeScript完整类型定义
- 🛠️ **易于部署**: 支持PM2和Docker

## 📖 快速开始

### 方式一：使用 npx（推荐）

无需安装，直接运行：

```bash
# 1. 克隆项目
git clone <repository-url>
cd websocket-proxy-http-request

# 2. 编译项目
npm install
npm run build

# 3. 启动代理服务器（外网）
npx ws-proxy-server
# 或
npx ws-proxy proxy-server

# 4. 启动客户端代理（内网，新终端）
npx ws-client-agent
# 或
npx ws-proxy client-agent
```

### 方式二：本地安装

```bash
# 安装依赖
npm install

# 编译
npm run build
```

### 配置

创建 `.env.proxy` (代理服务器):
```bash
PROXY_PORT=8080
WS_PORT=8081
AUTH_ENABLED=false
LOG_LEVEL=info
```

创建 `.env.client` (客户端):
```bash
PROXY_WS_URL=ws://localhost:8081
LOCAL_TARGET_HOST=localhost
LOCAL_TARGET_PORT=3000
CLIENT_ID=client-001
```

### 启动

#### 使用 npx（推荐）

```bash
# 启动代理服务器（外网机器）
npx ws-proxy-server
# 或使用子命令
npx ws-proxy proxy-server

# 启动客户端（内网机器，新终端）
npx ws-client-agent
# 或使用子命令
npx ws-proxy client-agent
```

#### 使用 npm scripts

```bash
# 编译
npm run build

# 启动代理服务器（外网机器）
npm run start:proxy

# 启动客户端（内网机器）
npm run start:client
```

#### 直接运行

```bash
# 编译
npm run build

# 启动代理服务器（外网机器）
node dist/src/proxy-server.js

# 启动客户端（内网机器）
node dist/src/client-agent.js
```

### 测试

使用自定义动态装箱/拆箱方式发送请求：

```bash
# GET请求示例（带查询参数）
curl -X POST http://localhost:8080/proxy \
  -H "Content-Type: application/json" \
  -d '{
    "url": "http://localhost:9000/api/users",
    "method": "get",
    "params": {"name": "allen"}
  }'

# POST请求示例
curl -X POST http://localhost:8080/proxy \
  -H "Content-Type: application/json" \
  -d '{
    "url": "http://localhost:9000/api/users",
    "method": "post",
    "body": {"name": "allen", "age": 25}
  }'

# 指定特定客户端（clientId路由）
curl -X POST http://localhost:8080/proxy \
  -H "Content-Type: application/json" \
  -d '{
    "url": "http://localhost:9000/api/users",
    "method": "get",
    "clientId": "client-001"
  }'
```

**请求参数**：
- `url` (必需): 内网服务器的完整URL
- `method` (必需): HTTP方法（get/post/put/delete等）
- `params` (可选): URL查询参数对象，自动序列化为查询字符串
- `body` (可选): 请求体，用于POST/PUT等方法
- `clientId` (可选): 指定客户端ID，将请求路由到特定客户端。如果不指定，将使用第一个可用客户端

### 按客户端 ID 路径代理

除了 `POST /proxy` 装箱接口，代理服务器也支持直接使用客户端 ID 作为路径前缀。该路由与 WebSocket 共用 `WS_PORT`，例如 `1315`：

```text
GET  http://170.106.115.192:1315/client-001/       -> client 本地目标服务 /
GET  http://170.106.115.192:1315/client-001/aa     -> client 本地目标服务 /aa
POST http://170.106.115.192:1315/client-001/api/users -> client 本地目标服务 /api/users
```

client 的 `.env.client` 决定本地目标服务：

```dotenv
PROXY_WS_URL=ws://170.106.115.192:1315
LOCAL_TARGET_HOST=host.docker.internal
LOCAL_TARGET_PORT=3000
CLIENT_ID=client-001
```

调用示例：

```bash
curl http://170.106.115.192:1315/client-001/health

curl -X POST http://170.106.115.192:1315/client-001/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"allen","age":25}'
```

路径中的 `client-001` 必须与 client 注册时的 `CLIENT_ID` 一致。GET、POST、PUT、PATCH、DELETE 等 HTTP 方法都会按同样的路径规则转发；原有 `POST /proxy` 接口仍然保留。

### 健康检查

```bash
# 检查服务器状态
curl http://localhost:8080/health

# 响应包含服务器状态、客户端连接信息、内存使用等
```

## 🏗️ 架构

```
外网HTTP请求 (8080端口)
    ↓
Proxy Server (装箱)
    ↓
WebSocket 传输 (8081端口)
    ↓
Client Agent (拆箱)
    ↓
内网HTTP请求 (3000端口)
    ↓
响应原路返回
```

## 📊 性能指标

| 指标 | 数值 | 评级 |
|------|------|------|
| 平均响应时间 | 3.42ms | ⭐⭐⭐⭐⭐ |
| P99延迟 | 30ms | ⭐⭐⭐⭐⭐ |
| 吞吐量（RPS） | 629 | ⭐⭐⭐⭐⭐ |
| 并发处理 | 50+ | ⭐⭐⭐⭐⭐ |
| 请求成功率 | 100% | ⭐⭐⭐⭐⭐ |

## 🎯 核心功能

### ✅ 已实现

- [x] HTTP请求代理（所有方法）
- [x] WebSocket双向通信
- [x] 请求装箱/拆箱
- [x] 响应原路返回
- [x] 请求超时控制（30秒）
- [x] 心跳保活机制（30秒）
- [x] 断线自动重连（3次）
- [x] Token认证（可选）
- [x] 完整日志系统
- [x] 健康检查接口
- [x] 连接数统计

## 📁 项目结构

```
websocket-proxy-http-request/
├── src/
│   ├── proxy-server.ts          # 代理服务器（核心）
│   ├── client-agent.ts          # 客户端代理（核心）
│   ├── types/                   # TypeScript类型
│   │   ├── protocol.ts          # 消息协议
│   │   └── config.ts            # 配置类型
│   ├── config/                  # 配置管理
│   │   ├── proxy-server.config.ts
│   │   ├── client-agent.config.ts
│   │   └── logger.config.ts
│   └── utils/                   # 工具函数
│       ├── logger.ts            # 日志管理
│       └── id-generator.ts      # ID生成
├── mock/                        # Mock测试服务
├── test-client.js               # 功能测试
├── test-performance.js          # 性能测试
└── dist/                        # 编译输出
```

## 🔧 配置说明

### 代理服务器配置

| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| PROXY_PORT | HTTP代理端口 | 8080 |
| WS_PORT | WebSocket端口 | 8081 |
| AUTH_ENABLED | 是否启用认证 | false |
| REQUEST_TIMEOUT | 请求超时(ms) | 30000 |
| HEARTBEAT_INTERVAL | 心跳间隔(ms) | 30000 |

### 客户端配置

| 配置项 | 说明 | 默认值 |
|--------|------|--------|
| PROXY_WS_URL | 代理服务器地址 | ws://localhost:8081 |
| LOCAL_TARGET_HOST | 内网目标主机 | localhost |
| LOCAL_TARGET_PORT | 内网目标端口 | 3000 |
| CLIENT_ID | 客户端标识 | client-001 |
| RECONNECT_ENABLED | 自动重连 | true |

## 🧪 测试

### 功能测试

```bash
# 运行完整功能测试
node test-client.js

# 结果：11/11 测试通过 ✅
```

### 性能测试

```bash
# 运行性能测试
node test-performance.js

# 结果：
# - 平均响应时间: 3.42ms
# - P99延迟: 30ms
# - 吞吐量: 629 RPS
```

## 🚀 部署

### 使用PM2

```bash
# 安装PM2
npm install -g pm2

# 启动服务
pm2 start dist/src/proxy-server.js --name proxy-server
pm2 start dist/src/client-agent.js --name client-agent

# 查看状态
pm2 status

# 查看日志
pm2 logs
```

### 使用Docker

项目提供两个独立镜像：`server` 负责 HTTP 代理和 WebSocket 服务，`client` 负责连接 server 并访问内网目标服务。

#### 1. 准备配置文件

在项目根目录创建 `.env.proxy`。该文件只在运行时注入，不会被 Dockerfile 复制进镜像：

```dotenv
PROXY_PORT=8080
WS_PORT=8081
AUTH_ENABLED=true
AUTH_TOKEN=replace-with-a-long-random-token
LOG_LEVEL=warn
LOG_TO_FILE=false
```

创建 `.env.client`：

```dotenv
PROXY_WS_URL=ws://your-server-host:8081
AUTH_TOKEN=replace-with-the-same-token
LOCAL_TARGET_HOST=host.docker.internal
LOCAL_TARGET_PORT=3000
CLIENT_ID=client-001
```

`AUTH_TOKEN` 必须使用随机长字符串，且 server/client 两端保持一致。不要把真实 token 提交到 Git 或写入 Dockerfile。

#### 2. 构建镜像

```bash
docker build --pull -f Dockerfile.server -t websocket-proxy-server:local .
docker build --pull -f Dockerfile.client -t websocket-proxy-client:local .
```

#### 3. 启动 server

```bash
docker run -d --name websocket-proxy-server \
  --restart unless-stopped \
  --env-file .env.proxy \
  -p 8080:8080 \
  -p 8081:8081 \
  websocket-proxy-server:local
```

验证 server：

```bash
curl http://127.0.0.1:8080/health
docker logs --tail 100 websocket-proxy-server
```

#### 4. 启动 client

```bash
docker run -d --name websocket-proxy-client \
  --restart unless-stopped \
  --env-file .env.client \
  websocket-proxy-client:local
```

验证 client 是否已连接：

```bash
docker logs --tail 100 websocket-proxy-client
curl http://127.0.0.1:8080/health
```

如果 client 访问的是 Linux 宿主机上的服务，需要额外增加：

```bash
--add-host host.docker.internal:host-gateway
```

然后将 `LOCAL_TARGET_HOST` 设置为 `host.docker.internal`。如果目标服务也运行在 Docker 中，建议把 server、client 和目标服务加入同一个用户自定义 Docker network，并使用目标容器名访问，不要使用 `--network host` 绕过网络隔离。

停止和删除容器：

```bash
docker stop websocket-proxy-client websocket-proxy-server
docker rm websocket-proxy-client websocket-proxy-server
```

### 发布到 Docker Hub

当前镜像构建和 server/client 互联已经验证，但本项目当前不适合直接以“生产就绪”的 `latest` 标签公开发布：本次 `npm audit --omit=dev --audit-level=high` 检测到 `4` 个 high 和 `4` 个 moderate 漏洞，且 `axios`、`ws` 等依赖存在可用修复版本。建议先完成依赖修复，再发布公开稳定标签。

#### 发布前检查

```bash
npm audit --omit=dev --audit-level=high
npm audit fix
npm run build
```

依赖升级后必须重新运行功能测试和 Docker 构建；如果审计仍有 high/critical 漏洞，不要发布 `latest`，可以先发布私有仓库或带明确 `-dev` / `-rc` 后缀的测试标签。

#### 创建仓库并登录

建议在 Docker Hub 创建两个仓库：

- `websocket-proxy-server`
- `websocket-proxy-client`

登录时使用 Docker Hub Access Token，不要在命令行直接写密码：

```bash
docker login --username YOUR_DOCKERHUB_USERNAME
```

CI 环境可使用 `--password-stdin`，并通过 CI Secret 注入 token：

```bash
printf '%s' "$DOCKERHUB_TOKEN" | docker login \
  --username "$DOCKERHUB_USERNAME" \
  --password-stdin
```

#### 构建并推送版本标签

将 `YOUR_DOCKERHUB_USERNAME` 替换为 Docker Hub 用户名，将 `0.0.4` 替换为实际发布版本：

```bash
export DOCKERHUB_USERNAME=YOUR_DOCKERHUB_USERNAME
export IMAGE_VERSION=0.0.4

docker build --pull -f Dockerfile.server \
  -t "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION" \
  -t "$DOCKERHUB_USERNAME/websocket-proxy-server:latest" .

docker build --pull -f Dockerfile.client \
  -t "$DOCKERHUB_USERNAME/websocket-proxy-client:$IMAGE_VERSION" \
  -t "$DOCKERHUB_USERNAME/websocket-proxy-client:latest" .

docker push "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION"
docker push "$DOCKERHUB_USERNAME/websocket-proxy-server:latest"
docker push "$DOCKERHUB_USERNAME/websocket-proxy-client:$IMAGE_VERSION"
docker push "$DOCKERHUB_USERNAME/websocket-proxy-client:latest"
```

生产部署应固定版本标签，不要只依赖 `latest`：

```bash
docker pull "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION"
docker pull "$DOCKERHUB_USERNAME/websocket-proxy-client:$IMAGE_VERSION"
docker image inspect \
  "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION" \
  "$DOCKERHUB_USERNAME/websocket-proxy-client:$IMAGE_VERSION"

# 登录 Docker Hub 后执行镜像 CVE/SBOM 检查
docker scout cves "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION"
docker scout cves "$DOCKERHUB_USERNAME/websocket-proxy-client:$IMAGE_VERSION"
docker scout sbom "$DOCKERHUB_USERNAME/websocket-proxy-server:$IMAGE_VERSION"
```

多架构发布可以将 `docker build` 替换为 `docker buildx build --platform linux/amd64,linux/arm64 --push`，并同时指定版本标签和 `latest` 标签。

Docker 官方命令参考：[docker login](https://docs.docker.com/reference/cli/docker/login/)、[docker push](https://docs.docker.com/reference/cli/docker/image/push/)、[Docker Hub repositories](https://docs.docker.com/docker-hub/repos/)。

### Docker Hub 发布安全检查

镜像层面的安全措施：

- 两个运行时镜像使用 `nodejs` 非 root 用户。
- `.dockerignore` 排除了 `.env`、`.history`、日志、测试和文档；运行时只复制 `dist`、生产依赖和包元数据。
- 当前构建检查未发现 `.env`、私钥或 token 文件进入镜像。
- Dockerfile 不包含认证 token；配置应通过 `--env-file`、环境变量或 Secret 注入。

仍需由部署方承担的风险：

- `AUTH_ENABLED` 默认是 `false`；当前认证只保护 WebSocket client 注册，`/proxy`、`/health` 和 `/:clientId/*` 没有 HTTP 认证。公网部署必须在反向代理、API Gateway 或防火墙层限制访问。
- client 会按请求中的完整 URL 发起内网 HTTP 请求。未经限制时，任何能调用 `/proxy` 的人都可能借此访问 client 网络可达的内网、云元数据或管理接口，属于 SSRF/横向访问风险。生产环境应使用网络出口控制和目标 URL allowlist；当前代码尚未实现 allowlist。
- 默认使用明文 `http://` 和 `ws://`。跨公网传输必须放在 HTTPS/WSS 反向代理、VPN 或私有网络内，不能直接暴露 8080/8081。
- server/client 会把 URL、请求头、请求体和响应信息写入控制台或日志，可能泄露 `Authorization`、Cookie 和业务数据。生产环境应避免敏感字段，并保护容器日志；`LOG_TO_FILE=false` 不能关闭源码中的 `console.log`。
- 当前没有 HTTP rate limit；WebSocket 也没有显式的消息大小上限，公网部署应增加网关限流和连接限制。
- `node:20-alpine` 使用可变 tag。正式供应链要求应固定基础镜像 digest，并在 CI 中执行镜像 CVE/SBOM 扫描。

因此，当前结论是：**适合发布为经过说明的开发/测试镜像或私有镜像；不建议在依赖审计和公网访问控制完成前，把它作为生产就绪的公开 `latest` 镜像。**

## 📚 文档

- [快速开始指南.md](./快速开始指南.md) - 详细使用教程
- [测试报告.md](./测试报告.md) - 完整测试结果
- [项目总结.md](./项目总结.md) - 技术总结
- [演示说明.md](./演示说明.md) - 演示指南
- [项目验收清单.md](./项目验收清单.md) - 验收清单

## 🎓 技术栈

- **运行时**: Node.js 18+
- **语言**: TypeScript 5.3+
- **WebSocket**: ws 8.16+
- **HTTP服务**: Express 4.18+
- **HTTP客户端**: Axios 1.6+
- **日志**: Winston 3.11+

## 🔍 监控

### 健康检查

```bash
curl http://localhost:8080/health

# 返回：
{
  "status": "ok",
  "clients": 1,
  "pendingRequests": 0
}
```

### 日志

```bash
# 查看日志
tail -f logs/app.log

# 查看错误日志
tail -f logs/error.log
```

## 💡 使用场景

1. **远程开发**: 在家访问公司内网开发环境
2. **临时演示**: 将内网测试环境暴露给客户
3. **跨网络调试**: 调试不同网络的服务
4. **内网穿透**: 将内网服务暴露到公网

## 🤝 贡献

欢迎提交Issue和Pull Request！

## 📄 许可证

MIT License

## 🎉 项目状态

- ✅ **功能完整**: 所有核心和辅助功能已实现
- ✅ **测试通过**: 11项功能测试 + 4项性能测试全部通过
- ✅ **性能优秀**: P99延迟30ms，RPS达629
- ✅ **文档完善**: 6份完整文档
- ⚠️ **发布状态**: Docker 镜像可构建；公开生产发布需先完成依赖审计和网络访问控制

## 📞 联系方式

如有问题或建议，欢迎反馈！

---

**⭐ 如果这个项目对你有帮助，请给个Star！**
