Files
SimpleRemoter/server/go/README.md

502 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SimpleRemoter Go TCP Server Framework
基于 Go 语言实现的高性能 TCP 服务端框架,用于替代原有的 C++ IOCP 服务端。
## 项目结构
```
server/go/
├── go.mod # Go 模块定义
├── auth/
│ └── auth.go # 授权验证模块 (TOKEN_AUTH + Heartbeat HMAC)
├── buffer/
│ └── buffer.go # 线程安全的动态缓冲区
├── connection/
│ ├── context.go # 连接上下文
│ ├── errors.go # 错误定义
│ └── manager.go # 连接管理器
├── protocol/
│ ├── parser.go # 协议解析器
│ ├── codec.go # 编解码和压缩 (ZSTD)
│ ├── header.go # 协议头解密 (8种加密方式)
│ └── commands.go # 命令常量和LOGIN_INFOR解析
├── server/
│ ├── server.go # TCP 服务器核心
│ └── pool.go # Goroutine 工作池
├── logger/
│ └── logger.go # 日志模块 (基于 zerolog)
├── hub/
│ └── hub.go # 在线设备注册表 + 事件订阅
├── wsauth/
│ └── wsauth.go # Web 鉴权 (challenge-response + 不透明 token)
├── web/
│ ├── embed.go # //go:embed 嵌入 HTML/xterm.js 等 web 资源
│ ├── server.go # HTTP server (静态页面 + REST + WS 路由)
│ ├── ws.go # WebSocket 连接生命周期
│ ├── ws_handlers.go # WS 消息分发与处理
│ └── assets/
│ ├── index.html # 从 ../../web/index.html sync 而来 (gitignored)
│ └── static/ # 第三方 xterm.js 资源 (checked in)
├── licensing/
│ ├── signer.go # Signer interface (Sign / Mode / Close)
│ ├── local.go # LocalSigner — operator 部署HMAC 直连
│ ├── remote.go # RemoteSigner — 客户部署HTTPS + singleflight + 24h cache + heartbeat ticker
│ ├── noop.go # NoOpSigner — free tier返回空签名
│ ├── token.go # JWT RS256 校验 + RSA 公钥加载
│ ├── quota.go # 配额追踪 (Reserve / RefreshExisting / 5min eviction)
│ ├── server.go # License Server HTTP handlers (/license/sign, /license/heartbeat) + Issue()
│ ├── factory.go # NewFromEnv / LicenseServerFromEnv + 模式选择
│ └── licensing_test.go # 17 个单元测试
└── cmd/
└── main.go # 程序入口
```
## 核心特性
底层基础设施:
- **高并发**: 基于 Goroutine 池管理并发连接
- **协议兼容**: 支持原有客户端的多种协议标识 (Hell/Hello/Shine/Fuck)
- **协议头解密**: 支持 8 种协议头加密方式 (V0-V6 + Default)
- **授权验证**: TOKEN_AUTH 和 Heartbeat HMAC-SHA256 双重授权
- **XOR 编码 / ZSTD 压缩**: 与客户端完全兼容
- **字符编码自适应**: 根据客户端能力位选择 UTF-8 直通或 GBK→UTF-8 转换
- **线程安全 / 优雅关闭 / 多端口监听 / 结构化日志**
Web 应用能力 (Phase 3-7)
- **Web 鉴权**: challenge-response 登录 + 不透明 token与 users.json schema 互通
- **登录加固**: 双维度速率限制10 次/分钟·IP + 5 次/15 分钟·用户名)+ 失败固定延迟,防口令枚举;`/get_salt` 用确定性假盐响应未知用户杜绝用户名探测WebSocket Origin 同源校验 + 显式白名单;`/api/devices` Bearer Token 鉴权
- **设备列表与监控**: 在线设备 / RTT / 活动窗口 / 分辨率 实时下发
- **Web 远程桌面**: 浏览器 WebCodecs 解码 H.264,二进制 WS 帧低延迟中继late-join 自动重发最近 IDR优雅 BYE 关闭防止客户端无意义重连
- **鼠标 / 键盘输入**: Win32 消息映射 (`WM_*` / `VK_*` / `MK_*`)MSG64 48 字节布局直传客户端
- **Web 终端**: xterm.js + Windows ConPTY / 旧 cmd 管道双模式;二进制 "TRM1" 帧分流;尺寸自适应;单设备单 viewer
- **用户与分组**: admin 可创建/删除 viewer 账号、配置 allowed_groupsusers.json 原子写入
## 支持的命令
### 客户端 → 服务端
| Token | 值 | 用途 |
| ---- | ---- | ---- |
| `TOKEN_AUTH` | 100 | 授权请求SN + Passcode + HMAC |
| `TOKEN_HEARTBEAT` | 101 | 心跳包(携带 ActiveWnd / Ping / SN |
| `TOKEN_LOGIN` | 102 | 主连接登录 |
| `TOKEN_BITMAPINFO` | 115 | 屏幕子连接首包,含分辨率 + clientID |
| `TOKEN_FIRSTSCREEN` | 116 | 原始 BGRA 首帧Go 侧丢弃) |
| `TOKEN_NEXTSCREEN` | 117 | H.264 屏幕帧 |
| `TOKEN_SHELL_START` | 128 | 旧 cmd-pipe 终端子连接首包 |
| `TOKEN_KEYFRAME` | 134 | GOP 关键帧DEFAULT_GOP 无限大,实际未用) |
| `TOKEN_TERMINAL_START` | 232 | PTY 终端子连接首包 |
| `TOKEN_TERMINAL_CLOSE` | 233 | 终端关闭通知 |
| `TOKEN_CONN_AUTH` | 246 | 子连接身份握手,含 clientID |
| (raw bytes) | — | 终端 sub-conn 绑定后裸字节即 shell 输出 |
### 服务端 → 客户端
| Command | 值 | 用途 |
| ---- | ---- | ---- |
| `COMMAND_SCREEN_SPY` | 16 | 启动屏幕捕获 |
| `COMMAND_SCREEN_CONTROL` | 20 | 鼠标 / 键盘输入MSG64 批次) |
| `COMMAND_NEXT` | 30 | 解除客户端读线程阻塞 |
| `COMMAND_SHELL` | 40 | 请求开启 shell 子连接 |
| `CMD_TERMINAL_RESIZE` | 81 | PTY 尺寸 (cols / rows int16 LE) |
| `COMMAND_BYE` | 204 | 优雅断开屏幕 / 终端 |
| `CMD_MASTERSETTING` | 215 | 主控配置 + HMAC 签名 (1000B) |
| `CMD_HEARTBEAT_ACK` | 216 | 心跳响应(携带 Authorized 字段) |
| `TOKEN_CONN_AUTH` | 246 | 子连接身份握手响应 (256B) |
未列出的命令字节会被记录为 Debug 日志,按需扩展。
## 快速开始
### 安装依赖
```bash
cd server/go
go mod tidy
```
### 编译
推荐用 Makefile编译前会自动从 `server/web/` 同步 HTML 到 `web/assets/`
```bash
make build # 当前平台
make windows # Windows amd64
make linux # Linux amd64
```
也可以直接用 `go build`,但要先手动 sync
```bash
make sync && go build -o simpleremoter-server ./cmd
```
VSCode F5 调试时由 `sync-web-assets` preLaunchTask 自动同步。
### 运行
```bash
./simpleremoter-server
```
默认监听 TCP 端口 `6543`被控设备HTTP 端口 `8080`(浏览器 Web UI。日志写到 `logs/server.log`
### 命令行参数
| 参数 | 默认值 | 说明 |
| ---- | ------ | ---- |
| `-port` / `-p` | `6543` | TCP 监听端口,分号分隔可多端口(如 `6543;6544` |
| `-http-port` | `8080` | HTTP 监听端口Web UI`0` 禁用 |
| `-no-console` | `false` | 关闭控制台输出(守护进程模式) |
### 环境变量
| 变量 | 说明 | 示例 |
| ---- | ---- | ---- |
| `YAMA_PWDHASH` | 密码的 SHA256 哈希值 (64位十六进制) | `61f04dd6...` |
| `YAMA_PWD` | 超级密码,用于 HMAC 签名验证;也作为 Web admin 密码的默认来源 | `your_super_password` |
| `YAMA_WEB_ADMIN_PASS` | Web UI 的 admin 密码(明文);优先于 `YAMA_PWD`。两者都未设置时 Web 登录禁用 | `your_admin_password` |
| `YAMA_SIGN_PASSWORD` | **[LocalSigner 模式]** HMAC-SHA256 master key直接给 CMD_MASTERSETTING 签名。Operator 自己的部署用。设置此变量后进入 LocalSigner 模式(见下方"签名模式")。 | `<deployment-shared-secret>` |
| `YAMA_LICENSE_SERVER` | **[RemoteSigner 模式]** Operator 的 License Server 公开 URL。客户部署设置此变量后进入 RemoteSigner 模式 —— 每次新设备登录会 HTTPS POST 给 License Server 拿签名,本机永远看不到 HMAC master key。必须与 `YAMA_LICENSE_TOKEN` 同时设置。 | `https://license.example.com` |
| `YAMA_LICENSE_TOKEN` | **[RemoteSigner 模式]** Operator 颁发的客户 JWTRS256作为 Bearer token 鉴权。每个客户一份。 | `eyJhbGciOiJSUzI1NiI...` |
| `YAMA_LICENSE_OFFLINE_HRS` | **[RemoteSigner 模式]** License Server 短暂不可达时,本地缓存签名的宽限期(小时)。默认 24。0 → 不缓存,每次新登录必须联网。 | `24` |
| `YAMA_LICENSE_PUBLIC_KEY` | **[License Server 模式]** Operator 自己(已经是 LocalSigner想顺便对外提供 License Server 时,用来验证客户提交的 JWT 的 RSA 公钥 PEM 路径。必须与 `YAMA_LICENSE_HTTP_ADDR` 同时设置。 | `./license_pub.pem` |
| `YAMA_LICENSE_HTTP_ADDR` | **[License Server 模式]** License Server HTTP 监听地址。**仅在 LocalSigner 模式下生效**RemoteSigner 客户不能反向当 license server。建议挂 nginx/Caddy 加 TLS 后对外。 | `:8443` |
| `YAMA_USERS_FILE` | Path to the JSON file that persists non-admin web users (allowed_groups, password hash, salt). Default is `users.json` in the working directory. | `users.json` |
| `YAMA_WEB_ALLOWED_ORIGINS` | Comma-separated WebSocket Origin allowlist for cross-origin upgrades. Empty (default) → only same-origin upgrades are accepted, which is correct when the web UI and `/ws` share a host. Add an entry per trusted PWA / dev origin. | `https://yama.example.com,https://yama-mobile.example.com` |
| `YAMA_WEB_TRUST_PROXY` | Set to `1` only when running behind a reverse proxy you control (caddy / nginx / cloudflare). Switches client-IP extraction to use the last entry of `X-Forwarded-For` instead of `RemoteAddr`, so per-IP login rate limit sees the real client. Direct-exposure deployments MUST leave this unset — otherwise attackers can spoof the header to evade rate limits. | `1` |
```bash
# Linux/macOS
export YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
export YAMA_PWD="your_super_password"
./simpleremoter-server
# Windows PowerShell
$env:YAMA_PWDHASH="61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
$env:YAMA_PWD="your_super_password"
.\simpleremoter-server.exe
```
## 签名模式CMD_MASTERSETTING signer
单个 Go 二进制按启动时的环境变量自动选择三种签名模式之一。同一个 master HMAC key 永远不会出现在客户机器上 —— 这是把 Go server 商业化部署给付费客户的核心安全前提。
| 模式 | 触发条件 | 用途 |
| ---- | -------- | ---- |
| **LocalSigner** | `YAMA_SIGN_PASSWORD` 已设 | Operator 自己的部署。master HMAC key 在本机内存,签名直连 HMAC微秒级延迟。**可选**:再设 `YAMA_LICENSE_PUBLIC_KEY` + `YAMA_LICENSE_HTTP_ADDR` 让本进程同时对外提供 License Server HTTP 服务。 |
| **RemoteSigner** | `YAMA_LICENSE_SERVER` + `YAMA_LICENSE_TOKEN` 已设 | 客户部署。本机**永远看不到** master HMAC key —— 每次新设备登录会 HTTPS POST 到 operator 的 License Server拿到签名后塞进 CMD_MASTERSETTING。同 (clientID, startTime) 元组的签名缓存 24h可调`YAMA_LICENSE_OFFLINE_HRS`),用于扛短暂网络故障。 |
| **NoOpSigner** | 上述都没设 | Free tier。返回空签名 → 客户端私有库拒绝启动 screen/file 功能。设备列表仍然可用。 |
注:`YAMA_SIGN_PASSWORD``YAMA_LICENSE_SERVER` 同时设置时 LocalSigner 优先operator 自己的 server 不应该回连自己)。
### License Server endpoints仅 LocalSigner 暴露)
设了 `YAMA_LICENSE_PUBLIC_KEY` + `YAMA_LICENSE_HTTP_ADDR` 后,本进程会监听 `YAMA_LICENSE_HTTP_ADDR` 提供两个端点:
- `POST /license/sign` — body `{"client_id":"...","start_time":"..."}`header `Authorization: Bearer <customer-JWT>`,回 `{"signature":"<64-hex>"}`。强制按 JWT 的 `tier` + `max_devices` 配额限制。
- `POST /license/heartbeat` — body `{"active_device_count":N,"active_device_ids":["..."]}`,回 `{"server_view_count":M,"drift":N-M}`。Drift 大于阈值时记日志,供 operator 反作弊审核。
部署建议:本进程只跑 plain HTTP前面挂 nginx/Caddy/cloudflare 加 TLS。JWT 校验已经把 `alg` 锁死成 RS256杜绝 `alg:none` 攻击。
### 颁发客户 JWT
```bash
# 一次性生成 RSA 密钥对(私钥 operator 自己保管,公钥用于 License Server 验证)
openssl genrsa -out license_priv.pem 2048
openssl rsa -in license_priv.pem -pubout -out license_pub.pem
```
底层 API 是 `licensing.Issue(privKey, sub, tier, maxDevices, ttl)`(见 [`licensing/server.go`](licensing/server.go))。一个开箱即用的 CLI 包装在独立仓库 [`yama-issue-token`](https://github.com/yuanyuanxiang/yama-issue-token)go.mod `replace` 指向本仓库的 `licensing` 包),用法:
```bash
yama-issue-token -priv license_priv.pem -sub acme-corp -tier paid -max 100 -days 365
```
| Tier | max_devices 默认 | 备注 |
| ---- | ---------------- | ---- |
| `trial` | 20JWT 未指定时) | 移植 C++ 反代理 RTT 逻辑 |
| `paid` | JWT 必须显式指定 | 长 TTL token |
## 使用示例
完整的 TCP + Hub + Web 集成示例就是 [`cmd/main.go`](cmd/main.go),那是程序入口本身、也是最权威的范例 —— 包含 handler 装配、hub 注册、web HTTP/WS 服务、信号优雅关闭等。
如果只想用 TCP 框架做自定义服务端(不要 Web/Hub最小示例如下
```go
package main
import (
"os"
"os/signal"
"syscall"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/server"
)
type MyHandler struct{ log *logger.Logger }
func (h *MyHandler) OnConnect(ctx *connection.Context) {}
func (h *MyHandler) OnDisconnect(ctx *connection.Context) {}
func (h *MyHandler) OnReceive(ctx *connection.Context, data []byte) {
if len(data) == 0 {
return
}
if data[0] == protocol.TokenLogin {
info, _ := protocol.ParseLoginInfo(data)
h.log.Info("login: %s (%s)", info.PCName, info.OsVerInfo)
}
}
func main() {
log := logger.New(logger.DefaultConfig())
srv := server.New(server.DefaultConfig())
srv.SetLogger(log.WithPrefix("Server"))
srv.SetHandler(&MyHandler{log: log})
if err := srv.Start(); err != nil {
log.Fatal("start: %v", err)
}
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
srv.Stop()
}
```
## 配置选项
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| Port | 8080 | 监听端口 |
| MaxConnections | 10000 | 最大连接数 |
| MinWorkers | 4 | 最小工作协程数 |
| MaxWorkers | 100 | 最大工作协程数 |
| ReadBufferSize | 8192 | 读缓冲区大小 |
| WriteBufferSize | 8192 | 写缓冲区大小 |
| KeepAliveTime | 5min | 连接保活时间 |
| ReadTimeout | 2min | 读超时时间 |
| WriteTimeout | 30s | 写超时时间 |
## 日志配置
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| Level | Info | 日志级别 (Debug/Info/Warn/Error/Fatal) |
| Console | true | 是否输出到控制台 |
| File | "" | 日志文件路径 (空则不写文件) |
| MaxSize | 100 | 单个日志文件最大 MB |
| MaxBackups | 3 | 保留的旧日志文件数量 |
| MaxAge | 30 | 旧日志保留天数 |
| Compress | true | 是否压缩轮转的日志 |
日志示例输出:
```json
{"level":"info","module":"Server","time":"2025-12-19T13:17:32+01:00","message":"Server started on port 6543"}
{"level":"info","module":"Handler","event":"login","client_id":1,"ip":"192.168.0.92","computer":"DESKTOP-BI6RGEJ","os":"Windows 10","version":"Dec 19 2025","time":"2025-12-19T13:17:32+01:00"}
{"level":"debug","module":"Handler","time":"2025-12-19T13:17:47+01:00","message":"Heartbeat from client 1 (DESKTOP-BI6RGEJ)"}
```
## 协议格式
数据包格式与 C++ 版本兼容:
```
+----------+------------+------------+------------------+
| Flag | TotalLen | OrigLen | Compressed Data |
| (N bytes)| (4 bytes) | (4 bytes) | (variable) |
+----------+------------+------------+------------------+
```
### 协议标识
| 标识 | Flag长度 | 压缩方式 | 说明 |
|------|----------|----------|------|
| HELL | 8 bytes | ZSTD | 主要协议 |
| Hello? | 8 bytes | None | 无压缩协议 |
| Shine | 5 bytes | ZSTD | 备用协议 |
| <<FUCK>> | 11 bytes | ZSTD | 备用协议 |
### 协议头加密
支持8种加密方式服务端自动检测并解密
- V0 (Default): 动态密钥4种操作
- V1: 交替加减
- V2: 带旋转的异或
- V3: 带位置的动态密钥
- V4: 对称的伪随机异或
- V5: 带位移的动态密钥
- V6: 带位置的伪随机
- V7: 纯异或
### LOGIN_INFOR 结构
客户端登录信息结构体 (考虑 C++ 内存对齐)
| 字段 | 偏移 | 大小 | 说明 |
|------|------|------|------|
| bToken | 0 | 1 | 命令标识 (102) |
| OsVerInfoEx | 1 | 156 | 操作系统版本 |
| (padding) | 157 | 3 | 对齐填充 |
| dwCPUMHz | 160 | 4 | CPU 频率 |
| moduleVersion | 164 | 24 | 模块版本 |
| szPCName | 188 | 240 | 计算机名 |
| szMasterID | 428 | 20 | 主控 ID |
| bWebCamExist | 448 | 4 | 是否有摄像头 |
| dwSpeed | 452 | 4 | 网速 |
| szStartTime | 456 | 20 | 启动时间 |
| szReserved | 476 | 512 | 扩展字段(多字段以 `\|` 分隔) |
### Heartbeat 结构
客户端心跳包结构 (1024 字节)
| 字段 | 偏移 | 大小 | 说明 |
|------|------|------|------|
| Time | 0 | 8 | 时间戳 (uint64) |
| ActiveWnd | 8 | 512 | 当前活动窗口 |
| Ping | 520 | 4 | 延迟 (int) |
| HasSoftware | 524 | 4 | 软件标识 (int) |
| SN | 528 | 20 | 序列号 (用于授权验证) |
| Passcode | 548 | 44 | 授权码 (格式: v0-v1-v2-v3-v4-v5) |
| PwdHmac | 592 | 8 | HMAC 签名 (uint64) |
| Reserved | 600 | 424 | 保留字段 |
### HeartbeatACK 结构
服务端心跳响应结构 (32 字节)
| 字段 | 偏移 | 大小 | 说明 |
|------|------|------|------|
| Time | 0 | 8 | 原始时间戳 (uint64) |
| Authorized | 8 | 1 | 授权状态 (1=已授权, 0=未授权) |
| Reserved | 9 | 23 | 保留字段 |
### 授权验证流程
```
客户端 Heartbeat 服务端
│ │
│ SN + Passcode + PwdHmac │
│ ────────────────────────────────► │
│ │ 1. 验证 Passcode 格式
│ │ 2. 验证 Passcode 哈希
│ │ 3. 验证 HMAC 签名
│ HeartbeatACK │
│ ◄──────────────────────────────── │
│ (Authorized=1 或 0) │
```
## API 参考
### Server
```go
// 创建服务器
srv := server.New(config)
// 设置日志
srv.SetLogger(log)
// 设置事件处理器
srv.SetHandler(handler)
// 启动服务器
srv.Start()
// 停止服务器
srv.Stop()
// 发送数据到指定连接
srv.Send(ctx, data)
// 广播数据到所有连接
srv.Broadcast(data)
// 获取当前连接数
count := srv.ConnectionCount()
```
### Connection Context
```go
// 发送数据
ctx.Send(data)
// 关闭连接
ctx.Close()
// 获取客户端 IP
ip := ctx.GetPeerIP()
// 检查连接状态
closed := ctx.IsClosed()
// 获取/更新最后活跃时间 (线程安全)
lastActive := ctx.LastActive()
ctx.UpdateLastActive()
duration := ctx.TimeSinceLastActive()
// 设置/获取客户端信息
ctx.SetInfo(clientInfo)
info := ctx.GetInfo()
// 设置/获取用户数据
ctx.SetUserData(myData)
data := ctx.GetUserData()
```
### Protocol
```go
// 解析登录信息
info, err := protocol.ParseLoginInfo(data)
if err == nil {
fmt.Println(info.PCName) // 计算机名
fmt.Println(info.OsVerInfo) // 操作系统
fmt.Println(info.ModuleVersion) // 版本
fmt.Println(info.WebCamExist) // 是否有摄像头
}
// 获取扩展字段
reserved := info.ParseReserved() // 返回 []string
clientType := info.GetReservedField(0) // 客户端类型
cpuCores := info.GetReservedField(2) // CPU 核数
filePath := info.GetReservedField(4) // 文件路径
publicIP := info.GetReservedField(11) // 公网 IP
```
## 与 C++ 版本对比
| 特性 | C++ (IOCP) | Go |
| ---- | ---- | ---- |
| 并发模型 | IOCP + 线程池 | Goroutine 池 |
| 跨平台 | Windows | 全平台 |
| 内存管理 | 手动 | GC |
| 代码复杂度 | 高 | 低 |
| 压缩 / XOR / 头加密 | 完整 8 套加密方式 + XOREncoder16 + ZSTD | 完全对齐 |
| 字符编码 | GBK | UTF-8 直通 / GBK→UTF-8 (按客户端能力位) |
| 设备列表与监控 | MFC 列表控件 | Web UI |
| Web 远程桌面 | 内嵌浏览器 + H.264 | 完全对齐WebCodecs 解码) |
| 鼠标键盘转发 | 已实现 | 完全对齐 |
| Web 终端 | 内嵌 xterm.js + ConPTY | 完全对齐(含旧 cmd-pipe 兼容) |
| 用户 / 分组管理 | 已实现 | users.json schema 互通 |
| 文件传输 / 摄像头 / 录音 等 | 已实现 | 暂未实现(按需扩展) |
## 依赖
- [github.com/klauspost/compress/zstd](https://github.com/klauspost/compress) - ZSTD 压缩
- [github.com/rs/zerolog](https://github.com/rs/zerolog) - 高性能日志
- [gopkg.in/natefinch/lumberjack.v2](https://github.com/natefinch/lumberjack) - 日志轮转
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) - GBK 编码转换
## License
MIT License