# 云游补丁授权服务器 · 使用说明

## 一、本机快速跑通（1分钟）

```bash
cd 授权服务器
pip install -r requirements.txt        # 只装 flask
set LICENSE_ADMIN_KEY=我的管理密钥123   # Linux 用 export；必改！
python server.py
```
服务监听 `0.0.0.0:5000`，数据库自动创建为同目录 `license.db`。

## 二、API 一览

| 接口 | 方法 | 说明 |
|---|---|---|
| `/api/ping` | POST | 健康检查 |
| `/api/activate` | POST | 激活：`{"card_code":"XXXX-...","hwid":"..."}` 一码绑一机 |
| `/api/verify` | POST | 验证：`{"hwid":"..."}`（可附 card_code 严格校验），返回 `expire_at`/`remaining_days` |
| `/api/admin/cards/generate` | POST | 发卡：`{"count":10,"duration_days":30,"note":"批次A"}`，`duration_days=0` 为永久卡 |
| `/api/admin/cards` | GET | 列卡：`?status=active&limit=200` |
| `/api/admin/cards/revoke` | POST | 封禁：`{"card_code":"..."}` |
| `/api/admin/cards/unbind` | POST | 解绑换机：`{"card_code":"..."}`（不重置到期时间） |
| `/api/admin/verify_log` | GET | 验证日志：`?limit=100` |

管理接口全部需要请求头 `X-Admin-Key: 你的管理密钥`。

## 三、curl 示例

```bash
# 发10张月卡
curl -X POST http://127.0.0.1:5000/api/admin/cards/generate \
  -H "X-Admin-Key: 我的管理密钥123" -H "Content-Type: application/json" \
  -d '{"count":10,"duration_days":30,"note":"首批"}'

# 激活
curl -X POST http://127.0.0.1:5000/api/activate \
  -H "Content-Type: application/json" \
  -d '{"card_code":"ABCD-EFGH-JKMN-PQRS","hwid":"A1B2C3..."}'

# 验证（客户端每次启动调用）
curl -X POST http://127.0.0.1:5000/api/verify \
  -H "Content-Type: application/json" \
  -d '{"hwid":"A1B2C3..."}'
```

所有授权响应带 `sig`（HMAC-SHA256）签名，客户端用共享密钥验签即可发现中间人篡改。

## 四、部署到云服务器（生产）

1. **HTTPS 必须**：套 Nginx/Caddy 反代 + 免费证书（certbot），程序本身只跑 HTTP；
2. 进程守护：`waitress-serve --host 127.0.0.1 --port 5000 server:app`（pip install waitress），
   再用 systemd / NSSM 拉起；
3. 改默认密钥：环境变量 `LICENSE_ADMIN_KEY`、`LICENSE_HMAC_SECRET`，与客户端示例里的
   `HMAC_SECRET` 保持一致；
4. 换 MySQL：按 `schema_mysql.sql` 建库，`server.py` 中 `sqlite3.connect` 换 `pymysql.connect`
   （`pip install pymysql`），SQL 占位符 `?` 改 `%s`；
5. 定期备份 `license.db`（或 MySQL dump）。

## 五、安全要点

- 注册码一码一机，换机走管理接口"解绑"，不会重置到期时间；
- 到期判定在**服务端**，客户端本地改时间无效；
- 响应有 HMAC 签名，抓包改包无效；
- 数据库在服务器上，客户端拿不到任何卡信息；
- 想更狠：管理接口只监听 127.0.0.1 或加 IP 白名单；验证接口加 nginx 层限速。

## 六、启动器与补丁下发（资产不上机 · 反爆破核心）

`启动器_授权版.py` 的流程：**取机器码 → 验证授权 → 下载 HWID 加密补丁+原版基线 →
基线还原原版 → 写入补丁 → 拉起游戏 → 守护还原（动态瞬时还原法，见第九节）**。

部署四步：
1. 上传**打好补丁**的 `dark.frag`（只需一次）：
   ```bash
   curl -X POST http://服务器:5000/api/admin/patch/upload \
     -H "X-Admin-Key: 你的管理密钥" --data-binary @dark.frag已补丁版
   ```
2. 上传**原版** `dark.frag`（非机密，作为"动态瞬时还原"的基线，见第九节）：
   ```bash
   curl -X POST http://服务器:5000/api/admin/patch/upload_original \
     -H "X-Admin-Key: 你的管理密钥" --data-binary @dark.frag原版
   ```
3. 把 `启动器_授权版.py` 顶部的 `API`、`CLIENT_DIR`、`HMAC_SECRET`、`GAME_PROC`
   （守护监测的进程名，若登录器会另拉起别的进程请改成真身）改成实际值；
4. 按"第七节"混淆加固后发给用户。用户磁盘上**平时只有原版文件**。

> 为什么这是防爆破的根治方案：爆破者Patch掉启动器里的验证跳转后，拿到的只是一个
> "没有资产的空壳"——补丁内容在服务器上，且密文绑定了授权用户的机器码，抓包复用也
> 只能在那台机器上解出。验证逻辑可以爆破，"资产不在本地"无法爆破。

## 七、客户端混淆加固（按启动器语言选路线）

**Python 启动器（本项目）**：
```bash
pip install pyarmor pyinstaller
pyarmor gen 启动器_授权版.py          # 生成 dist/启动器_授权版.py(混淆字节码)
pyinstaller -F -w dist/启动器_授权版.py  # 打单文件exe, -w=无控制台
```
PyArmor 还支持把脚本绑定到设备/有效期（`--bind-device` 等），双层绑定。

**若换 C# 启动器**：ConfuserEx 或 .NET Reactor——符号混淆+字符串加密+控制流
混乱+反调试；代码里不要出现明文 URL/密钥，统一走常量加密。

**若换 C++ 启动器**：OLLVM（`-mllvm -sub -fla -bcf` 指令替换+控制流平坦化+虚假分支），
发布版再过 VMProtect；配合启动时自校验（CRC 自身代码段）+ 反调试。

**通用纪律**（比混淆更重要的四条）：
1. 资产服务端持有（本方案已实现）——混淆只是提高成本，"没有本地资产"才是护城河；
2. 到期判定只在服务端；客户端本地看到的 `expire_at` 仅用于展示；
3. 关键分支做多点冗余校验（验证结果在多个位置各验一次），单点Patch跳转直接残废；
4. 接受现实：任何客户端校验理论上都可逆向，目标是让破解成本 > 一张卡的价格。

## 九、动态瞬时还原法（补丁资产零驻留）

**原理**：shader(dark.frag) 在进入游戏主城那一刻被读进显存（GLProgram 按名字缓存），
之后磁盘上那份删了也不影响运行。启动器时序：
**基线还原原版（服务器下发原版，防"备份投毒"）→ 写入补丁 → 拉起游戏 →
守护（进程退出或时限到）→ 从磁盘擦除补丁、还原原版**，`finally` 兜底保证任何崩溃路径都会还原。

**还原策略 `RESTORE_MODE`**：
- `"cap"`（默认）：游戏进程存活超过 `RESTORE_CAP`（默认120秒）后还原——资产暴露窗口最小；
  慢盘/长登录队列的玩家若在时限后才进主城，本局不亮，重开启动器即可；
- `"exit"`：游戏进程退出才还原——可靠性最高（多久进游戏都亮），资产暴露时间=游戏全程。

**诚实边界**：游戏运行期间，补丁必然以明文存在于磁盘与显存——持牌玩家想偷永远偷得到；
本方案消灭的是"游戏没跑时躺在磁盘里的那份"，挡的是顺手拷文件的平民盗版。
终极防线仍是服务端卡密（HWID 绑定 + 即时吊销），两层叠用。

## 八、对接发卡网（支付成功自动发卡）

**模型**：发卡网支付成功 → 平台回调你的 `notify_url`（验签+幂等+自动生成卡密入库）。
用户拿码三种接法（按平台能力选）：
A. **签名跳转取货**（易支付系平台支付完成跳转 return_url 时自带整组签名参数）：
   `https://你的域名/api/faka/take?trade_no=...&money=...&...&sign=平台签名`
   服务端整组验签后**惰性建单发卡**（不依赖 notify 先到）；
B. **平台API供货模式**（异次元等）：平台服务端下单时调你的供货接口，需按其协议加
   JSON 端点（把平台供货协议文档给我即可接上）；
C. **预生成导入**（任何平台都能用）：管理接口批量出卡，导入平台卡密库存。

**服务端配置（环境变量，发卡前必设）**：
```bash
set FAKA_KEY=你在发卡网后台的商户密钥
set FAKA_GOODS_MAP={"30.00": 30, "98.00": 0, "198.00": 90}
#   含义: 支付金额(两位小数) -> 有效天数; 0=永久。仅按金额精确映射(安全: 禁止按商品名回退)。
set TAKE_SECRET=一串32字符以上的强随机值      # 取货令牌密钥, 与支付密钥FAKA_KEY分离
```

**发卡网后台配置**：
- 回调/通知地址填 `https://你的域名/api/faka/notify`
- 【已废弃】旧方案"取货链接 sign=md5(订单号+商户密钥)"已移除——那是把支付密钥做成
  发给每个买家的离线爆破承诺（安全审计 critical：弱密钥被破=零元伪造回调+盗取他人卡密）。
  现在用接法 A（签名跳转取货）/ C（预生成导入）替代。

**安全机制**（已内置）：
- 回调验签（彩虹易支付系 MD5 约定，`verify_epay_sign` 单函数，平台不同只改它）；
- `trade_status` 必须为成功态；金额未在 `FAKA_GOODS_MAP` 映射=拒绝发卡（防"一分钱买永久卡"）；
- 平台订单号 `trade_no` 唯一约束=幂等，平台重试回调不会重复发卡；
- `/api/faka/take` 仅接受平台整组签名(易支付return_url)或 TAKE_SECRET 令牌；订单占位+发卡同事务幂等。

**测试（模拟一次回调）**：
```bash
# 假设 FAKA_KEY=testkey, 商品30元=30天
python -c "import hashlib;ps=f'money=30.00&name=月卡&out_trade_no=T1&trade_no=TEST001&trade_status=TRADE_SUCCESS';print(ps+'&key=testkey','=>',hashlib.md5((ps+'&key=testkey').encode()).hexdigest().upper())"
# 用得到的 sign 拼成表单 POST 到 http://127.0.0.1:5000/api/faka/notify → 应回 success
# 再用令牌取货(手动场景): 令牌=hmac_sha256(TAKE_SECRET, "take|TEST001")
python -c "import hmac,hashlib;print(hmac.new(b'<TAKE_SECRET>',b'take|TEST001',hashlib.sha256).hexdigest())"
# GET /api/faka/take?trade_no=TEST001&token=<上面的值> → 返回卡密
```

**常见平台差异速查**：
- 彩虹易支付系（含大多数聚合支付）：上表约定，POST 表单，回 `success`；
- 独角数卡：原生**没有**第三方供货回调/取货机制（其商户密钥属于支付网关配置）——
  用接法C（预生成导入平台库存）最稳；装了易支付协议转发类插件则可走 notify；
- 异次元发卡(acg-faka)：代充/接口商品走"供货API"模式（平台服务端主动调你、JSON进出）——
  把其供货协议文档给我即可加对应 JSON 端点；
- 彩虹易支付系变体：个别平台密钥直接拼接/参与sign_type/GBK中文——都只改
  `verify_epay_sign()` 一个函数。


