From fc6ebccb598a64e3ad0899641e912b3b1a4d594f Mon Sep 17 00:00:00 2001 From: alimu Date: Fri, 6 Mar 2026 00:16:10 +0400 Subject: [PATCH] Sync from upstream/ai-dev (squashed) --- .gitignore | 4 + ReadMe.md | 150 ++++++++++++++-------------- android-client/README.md | 19 +++- android-client/app/build.gradle.kts | 5 +- web-client/README.md | 23 ++--- 5 files changed, 112 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 7a326c0..25d92c4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ web-client/dist/ web-client/.vite deploy/certs/ deploy/keys/ + +# macOS metadata +.DS_Store +**/.DS_Store diff --git a/ReadMe.md b/ReadMe.md index a4a536b..609a541 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,45 +1,47 @@ # OnlineMsgServer -在线消息中转服务(WebSocket + RSA),支持客户端鉴权、单播转发、广播、签名校验、防重放、限流。 +在线消息中转服务(WebSocket + RSA),支持客户端鉴权、单播转发、广播、签名校验、防重放与限流。 -## 当前默认行为 +## 仓库结构 -- 服务端默认 `REQUIRE_WSS=false`(便于内外网测试) -- 仍支持 `WSS(TLS)`,可通过环境变量启用 -- 客户端需要完成 challenge-response 鉴权后才能发业务消息 +- `deploy/`:一键部署与生产产物脚本 +- `web-client/`:React Web 客户端 +- `android-client/`:Android(Kotlin + Compose)客户端 + +## 运行前提 + +- `.NET 8 SDK` +- `Docker` +- `openssl` +- 部署脚本 `deploy/deploy_test_ws.sh` 与 `deploy/redeploy_with_lan_cert.sh` 依赖 `ipconfig`、`route`(当前按 macOS 环境编写) ## 快速开始 -### 1) 测试模式(推荐先跑通) +先进入仓库根目录: + +```bash +cd +``` + +### 1) 测试模式(WS) ```bash -cd /Users/solux/Codes/OnlineMsgServer bash deploy/deploy_test_ws.sh ``` -这个脚本会自动: -- 生成/复用服务端 RSA 私钥(`deploy/keys`) -- 构建镜像并重启容器 -- 以 `REQUIRE_WSS=false` 启动服务 -- 输出可直接使用的 `ws://` 地址 +脚本会自动生成/复用协议私钥、构建镜像并以 `REQUIRE_WSS=false` 启动容器。 ### 2) 安全模式(WSS + 局域网证书) ```bash -cd /Users/solux/Codes/OnlineMsgServer bash deploy/redeploy_with_lan_cert.sh ``` -这个脚本会自动: -- 重新生成带当前 LAN IP 的 TLS 证书(`deploy/certs`) -- 构建镜像并重启容器 -- 以 `REQUIRE_WSS=true` 启动服务 -- 输出可直接使用的 `wss://` 地址 +脚本会重签包含当前局域网 IP 的证书、构建镜像并以 `REQUIRE_WSS=true` 启动容器。 ### 3) 生产准备(证书 + 镜像 + 部署产物) ```bash -cd /Users/solux/Codes/OnlineMsgServer DOMAIN=chat.example.com \ TLS_CERT_PEM=/path/fullchain.pem \ TLS_KEY_PEM=/path/privkey.pem \ @@ -48,16 +50,16 @@ CERT_PASSWORD='change-me' \ bash deploy/prepare_prod_release.sh ``` -脚本会自动: -- 准备服务端协议私钥(`deploy/keys`) -- 生成运行时 `server.pfx`(`deploy/certs`) -- 构建生产镜像(默认 `onlinemsgserver:prod`) -- 导出部署产物到 `deploy/output/prod`(`prod.env`、镜像 tar、运行示例脚本) +输出目录默认在 `deploy/output/prod`,包含 `prod.env`、镜像 tar(可选)和运行示例脚本。 -如果你暂时没有 CA 证书,也可用自签名兜底(仅测试): +无 CA 证书时可临时使用自签名(仅测试): ```bash -DOMAIN=chat.example.com SAN_LIST='DNS:www.chat.example.com,IP:10.0.0.8' GENERATE_SELF_SIGNED=true bash deploy/prepare_prod_release.sh +DOMAIN=chat.example.com \ +SAN_LIST='DNS:www.chat.example.com,IP:10.0.0.8' \ +GENERATE_SELF_SIGNED=true \ +CERT_PASSWORD='change-me' \ +bash deploy/prepare_prod_release.sh ``` ## 手动 Docker 启动示例 @@ -67,19 +69,19 @@ DOMAIN=chat.example.com SAN_LIST='DNS:www.chat.example.com,IP:10.0.0.8' GENERATE ```bash docker run -d --name onlinemsgserver --restart unless-stopped \ -p 13173:13173 \ - -v /Users/solux/Codes/OnlineMsgServer/deploy/keys:/app/keys:ro \ + -v "$(pwd)/deploy/keys:/app/keys:ro" \ -e REQUIRE_WSS=false \ -e SERVER_PRIVATE_KEY_PATH=/app/keys/server_rsa_pkcs8.b64 \ onlinemsgserver:latest ``` -### WSS(生产/半生产) +### WSS(生产/预生产) ```bash docker run -d --name onlinemsgserver --restart unless-stopped \ -p 13173:13173 \ - -v /Users/solux/Codes/OnlineMsgServer/deploy/certs:/app/certs:ro \ - -v /Users/solux/Codes/OnlineMsgServer/deploy/keys:/app/keys:ro \ + -v "$(pwd)/deploy/certs:/app/certs:ro" \ + -v "$(pwd)/deploy/keys:/app/keys:ro" \ -e REQUIRE_WSS=true \ -e TLS_CERT_PATH=/app/certs/server.pfx \ -e TLS_CERT_PASSWORD=changeit \ @@ -87,16 +89,16 @@ docker run -d --name onlinemsgserver --restart unless-stopped \ onlinemsgserver:latest ``` -## 协议说明(客户端 -> 服务端) +## 协议说明 ### 加密方式 - RSA-2048-OAEP-SHA256 -- 明文分块 190 字节加密 +- 明文按 190 字节分块加密 - 密文按 256 字节分块解密 -- 传输格式为 base64 字符串 +- 业务消息传输为 base64 字符串 -### 通用包结构 +### 通用包结构(客户端 -> 服务端) ```json { @@ -106,15 +108,29 @@ docker run -d --name onlinemsgserver --restart unless-stopped \ } ``` -### 1) 鉴权登记 `type=publickey` +### 连接首包(服务端 -> 客户端,明文) + +```json +{ + "type": "publickey", + "data": { + "publicKey": "服务端公钥(base64 SPKI)", + "authChallenge": "一次性挑战值", + "authTtlSeconds": 120, + "certFingerprintSha256": "TLS证书指纹(启用WSS时)" + } +} +``` + +### 鉴权登记 `type=publickey`(客户端 -> 服务端) -- `key`:用户名(可空) +- `key`:用户名(为空时服务端会生成匿名名) - `data`: ```json { "publicKey": "客户端公钥(base64 SPKI)", - "challenge": "服务端下发挑战值", + "challenge": "上一步 authChallenge", "timestamp": 1739600000, "nonce": "随机字符串", "signature": "签名(base64)" @@ -127,9 +143,9 @@ docker run -d --name onlinemsgserver --restart unless-stopped \ publickey\n{userName}\n{publicKey}\n{challenge}\n{timestamp}\n{nonce} ``` -### 2) 单播转发 `type=forward` +### 单播 `type=forward` -- `key`:目标公钥 +- `key`:目标客户端公钥 - `data`: ```json @@ -147,9 +163,9 @@ publickey\n{userName}\n{publicKey}\n{challenge}\n{timestamp}\n{nonce} forward\n{targetPublicKey}\n{payload}\n{timestamp}\n{nonce} ``` -### 3) 广播 `type=broadcast` +### 广播 `type=broadcast` -- `key`:可空 +- `key`:可为空字符串 - `data`:同 `forward` 签名串: @@ -160,43 +176,29 @@ broadcast\n{key}\n{payload}\n{timestamp}\n{nonce} ### 连接流程 -1. 客户端连接后,服务端先返回未加密 `publickey`(含服务端公钥、challenge、TTL、证书指纹)。 +1. 客户端建立 WebSocket 连接后接收明文 `publickey` 首包。 2. 客户端发送签名鉴权包(`type=publickey`)。 -3. 鉴权成功后发送 `forward` / `broadcast` 业务包。 +3. 鉴权成功后,客户端发送 `forward` / `broadcast` 业务消息(加密 + 签名)。 ## 环境变量 -- `LISTEN_PORT`:监听端口(默认 `13173`) -- `REQUIRE_WSS`:是否启用 WSS(默认 `false`) -- `TLS_CERT_PATH`:证书路径(WSS 必填) +- `LISTEN_PORT`:监听端口,默认 `13173` +- `REQUIRE_WSS`:是否启用 WSS,默认 `false` +- `TLS_CERT_PATH`:证书路径(启用 WSS 时必填) - `TLS_CERT_PASSWORD`:证书密码(可空) - `SERVER_PRIVATE_KEY_B64`:服务端私钥(PKCS8 base64) - `SERVER_PRIVATE_KEY_PATH`:服务端私钥文件路径(与上面二选一) -- `ALLOW_EPHEMERAL_SERVER_KEY`:允许仅内存临时私钥(默认 `false`) -- `MAX_CONNECTIONS`:最大连接数 -- `MAX_MESSAGE_BYTES`:最大消息字节数 -- `RATE_LIMIT_COUNT`:限流窗口内最大消息数 -- `RATE_LIMIT_WINDOW_SECONDS`:限流窗口秒数 -- `IP_BLOCK_SECONDS`:触发滥用后 IP 封禁秒数 -- `CHALLENGE_TTL_SECONDS`:challenge 有效期 -- `MAX_CLOCK_SKEW_SECONDS`:允许时钟偏差 -- `REPLAY_WINDOW_SECONDS`:防重放窗口 - -## 前端(React) - -前端目录:`/Users/solux/Codes/OnlineMsgServer/web-client` - -```bash -cd /Users/solux/Codes/OnlineMsgServer/web-client -npm install -npm run dev -``` - -当前前端能力: -- 默认隐藏协议细节,手动地址放在“高级连接设置” -- 支持广播/私聊、查看并复制自己的公钥 -- 每条消息支持一键复制 -- 自动处理超长消息换行,不溢出消息框 -- 用户名和客户端私钥本地持久化,刷新后继续使用 - -更多前端说明见 `web-client/README.md`。 +- `ALLOW_EPHEMERAL_SERVER_KEY`:允许使用临时内存私钥,默认 `false` +- `MAX_CONNECTIONS`:最大连接数,默认 `1000` +- `MAX_MESSAGE_BYTES`:单消息最大字节数,默认 `65536` +- `RATE_LIMIT_COUNT`:限流窗口允许消息数,默认 `30` +- `RATE_LIMIT_WINDOW_SECONDS`:限流窗口秒数,默认 `10` +- `IP_BLOCK_SECONDS`:触发滥用后的封禁秒数,默认 `120` +- `CHALLENGE_TTL_SECONDS`:挑战值有效期秒数,默认 `120` +- `MAX_CLOCK_SKEW_SECONDS`:允许时钟偏差秒数,默认 `60` +- `REPLAY_WINDOW_SECONDS`:防重放窗口秒数,默认 `120` + +## 客户端文档 + +- Web 客户端说明:`web-client/README.md` +- Android 客户端说明:`android-client/README.md` diff --git a/android-client/README.md b/android-client/README.md index 5fdb190..9312f95 100644 --- a/android-client/README.md +++ b/android-client/README.md @@ -2,7 +2,7 @@ 本目录是针对当前 `OnlineMsgServer` 协议实现的 Android 客户端。 -## 已实现能力 +## 主要能力 - Kotlin + Jetpack Compose + Material3 - 与当前服务端协议兼容: @@ -34,7 +34,14 @@ 2. 等待 Gradle Sync 完成。 3. 运行 `app`。 -## 联调建议 +命令行构建示例: + +```bash +cd android-client +./gradlew assembleDebug +``` + +## 联调地址建议 - 模拟器建议地址:`ws://10.0.2.2:13173/` - 真机建议地址:`ws://<你的局域网IP>:13173/` @@ -49,6 +56,14 @@ - `forward` 的 `key` 必须是目标公钥。 - `broadcast` 的 `key` 为空字符串。 +## 构建产物导出(可选) + +- `assembleDebug` 结束后会触发 `exportDebugApk` 任务,把 `app-debug.apk` 复制到导出目录。 +- 默认导出目录:`android-client/app/build/exports/apk-debug` +- 可通过以下方式覆盖导出目录: + - Gradle 属性:`-PdebugApkExportDir=/your/path` + - 环境变量:`DEBUG_APK_EXPORT_DIR=/your/path` + ## 已知限制 - 当前未内置证书固定(pinning);如用于公网生产,建议额外启用证书固定策略。 diff --git a/android-client/app/build.gradle.kts b/android-client/app/build.gradle.kts index b6ebb7c..a769b8f 100644 --- a/android-client/app/build.gradle.kts +++ b/android-client/app/build.gradle.kts @@ -83,7 +83,10 @@ dependencies { androidTestImplementation("androidx.compose.ui:ui-test-junit4") } -val debugApkExportDir = "/Users/solux/Docker/webdav/share/public/apk-release" +val debugApkExportDir: String = providers.gradleProperty("debugApkExportDir") + .orElse(providers.environmentVariable("DEBUG_APK_EXPORT_DIR")) + .orElse(layout.buildDirectory.dir("exports/apk-debug").map { it.asFile.absolutePath }) + .get() val debugApkExportName = "onlinemsgclient-debug.apk" val exportDebugApk by tasks.registering(Copy::class) { diff --git a/web-client/README.md b/web-client/README.md index 5d6bbdf..9571246 100644 --- a/web-client/README.md +++ b/web-client/README.md @@ -1,6 +1,6 @@ # OnlineMsg Web Client -React 前端客户端,适配当前仓库的加密消息协议,默认隐藏连接细节,仅保留聊天交互。 +React 前端客户端,适配当前仓库消息协议,默认隐藏协议细节并聚焦聊天交互。 ## 开发运行 @@ -25,17 +25,16 @@ npm run preview ## 使用说明 -- 打开页面后点击“连接” -- 默认会自动使用当前主机名拼接: - - `http` 页面下:`ws://:13173/` - - `https` 页面下:`wss://:13173/` -- 若你手动输入 `ws://`,前端会自动尝试升级到 `wss://` 一次 -- 如需手动指定服务器地址,在“高级连接设置”中填写,例如: - - `wss://example.com:13173/` - - `ws://127.0.0.1:13173/`(仅本地调试) -- “目标公钥”留空为广播,填写后为私聊转发 -- 用户名会自动保存在本地,刷新后继续使用 -- 客户端私钥会保存在本地浏览器(用于持续身份),刷新后不会重复生成 +1. 打开页面后点击“连接”。 +2. 默认服务器地址会根据当前页面协议和主机自动推断: + - 当页面是 `https` 且主机不是本机地址时:`wss:///msgws/` + - 其他情况:`ws://:13173/` +3. 若首连失败且当前地址是 `ws://`,客户端会自动切换到 `wss://` 重试 1 次。 +4. 如需手动指定服务器地址,在“高级连接设置”中填写,例如: + - `wss://example.com/msgws/` + - `ws://127.0.0.1:13173/`(本地调试) +5. “目标公钥”为空时发送广播,填写后发送私聊(`forward`)。 +6. 用户名、服务器地址历史、客户端私钥会保存在浏览器本地存储中。 ## 移动端注意事项