版本号:
- 1.5.11 越界(规则 3 规定 patch ≤ 10)→ 修正为 1.6.0(进位到 minor、patch 归 0)
- versionCode 43 不变:43 从未对外发布,本次修正只是把命名拉回约定
合并支线(原在工作区根目录、App 仓库之外,DESIGN.md 自称"支线"):
- selfhost-source/server/ 自建歌曲源服务器(纯 Python 标准库、零 pip 依赖)
- selfhost-source/deploy/、DESIGN.md、LIBRARY.md、build_inventory.py
- selfhost-source/inventory/ 小清单(playlists.csv、song-stats.md 等)
- ⚠️ inventory 生成物(songs-*.csv / singers-top.csv,共 84MB)不入库,
已写入 .gitignore;需要时用 build_inventory.py 依据 muse.db 重新生成
同时入库的 App 侧支线改动(此前为工作树未提交状态):
- Whoami.kt(whoami 设备心跳)、DeviceIdentity.kt(设备身份伪装)
- PushDownload.kt / PushDownloadClient.kt(点歌推送)
- AdVideoFilter 带 query/fragment 的地址闸门;KTV_BRIDGE_API 11 → 13
- 修 ktv_api.js 的 global.KtvBridgeApiVersion(12 → 13,与注释及 Kotlin 两处校验一致)
- 对应单测:WhoamiTest / DeviceIdentityTest / PushDownloadTest / AdVideoFilterTest(共 39 例)
文档:
- DESIGN.md 的"在 App 仓库之外"约定改为"已并入仓库统一版本化"
- AGENTS.md 版本号行更新为 1.6.0/43,并新增支线并入说明
验证:assembleRelease + testReleaseUnitTest 全过(39 例)
449 lines
31 KiB
Markdown
449 lines
31 KiB
Markdown
# IIS 反向代理:**拆成两个站点**(API 站 + 媒体站),对外只暴露 IIS
|
||
|
||
> 适用场景:KTV 场所里有一台 **Windows + IIS** 的机器(对外有域名/固定 IP),
|
||
> 内网另有一台 **Linux** 跑本服务(默认 `192.168.100.232:6677`)。
|
||
> 对外只开放 IIS(HTTPS + 域名),由它把 App 需要的接口反代到内网服务;
|
||
> **管理端口 6678 不对公网暴露**。
|
||
|
||
## 0. 先理解四件事(不然后面所有排错都是盲猜)
|
||
|
||
1. **反代之后,后端看到的"客户端"是 IIS**。所以后端必须知道"谁才是真正的客户端":
|
||
靠 IIS 传 `X-Forwarded-For` / `X-Real-IP`,并且**只在可信前提下**采信 ——
|
||
后端配置 `trust_proxy_headers` + `trusted_proxies`(值填 **IIS 机器的内网 IP**)。
|
||
**不开这一项**的后果:`/push_download` 的限流会把全场设备算成同一个来源(一台刷满、其余全 429),
|
||
日志里也看不到是哪台设备,排查设备问题等于没日志。
|
||
2. **反代之后,后端看到的协议是 http**(内网 IIS→Python 是明文)。所以 HTTPS 站点必须靠
|
||
`X-Forwarded-Proto: https` + `trust_proxy_headers`,否则 `/music/do.php` 会把
|
||
`http://内网主机:6677/media/xxx.ts` 返回给 App(App 拒收,还泄露内网拓扑)。
|
||
3. **媒体必须支持 Range**。App 的下载器(OkDownload)靠 `206 Partial Content` 断点续传/边下边播。
|
||
所以 IIS 侧**不要开压缩/不要缓存改写**,后端已经支持 `HEAD`/`Range`(两份 `web.config` 都已关压缩)。
|
||
4. **为什么要拆两个站点**:让"批量拉媒体文件"与"调接口"在 **IIS 层**就分开 ——
|
||
各自的绑定/证书/限速/日志互不影响;后端再加一道**宿主隔离**(`media_hosts` / `api_hosts`),
|
||
于是 `md.kasugano.cn:4433` 只能下文件、`mdmuse.kasugano.cn` 不对外提供批量下载。
|
||
|
||
后端健康检查与排错接口(务必记住这两个):
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `GET /healthz` | 探活:**纯内存快照**、微秒级返回(给 IIS/负载均衡用),返回 `ok/version/uptime_sec/library/keeps/downloader` |
|
||
| `GET /api/client_ip` | 排错:服务端**认定的客户端 IP** + 是否来自可信代理 + 命中了哪个头 + **宿主判定结果**(`host` / `media_host_match` / `api_host_match`)。反代配置对不对,看一眼就知道 |
|
||
|
||
---
|
||
|
||
## 1. 准备:后端(Linux,6677)先跑起来
|
||
|
||
```bash
|
||
# 在 192.168.100.232 上
|
||
cd /opt/ktv-source && python3 run.py --config /etc/ktv-source/server-config.json
|
||
# 本机自测(不经过 IIS)
|
||
curl -s http://127.0.0.1:6677/healthz
|
||
# → {"ok":true,"version":"1.0.0","uptime_sec":12,...,"library":0,"keeps":0,"downloader":{...}}
|
||
```
|
||
|
||
改 `/etc/ktv-source/server-config.json`(或管理界面「设置」页),**反代部署必改**这些项:
|
||
|
||
| 键 | 建议值 | 为什么 |
|
||
|---|---|---|
|
||
| `trust_proxy_headers` | `true` | 不开 = 代理头一律不采信(限流会串、日志看不到设备 IP、HTTPS 地址会退化成 http、**宿主隔离判定会拿不到 `X-Forwarded-Host`**) |
|
||
| `trusted_proxies` | `["192.168.100.10"]`(**IIS 机器的内网 IP**;多个用逗号分隔,也可写 CIDR) | 只有这些地址发来的 `X-Forwarded-*` 才被采信(否则谁都能伪造) |
|
||
| `public_media_base` | `https://md.kasugano.cn:4433/media` | 兜底地址:即使代理头没生效,返回给 App 的也是**域名 + 端口 + https**(绝不能是裸 IP) |
|
||
| `public_host` | `md.kasugano.cn:4433` | 与上面保持同一个域名(含端口) |
|
||
| `media_hosts` | `["md.kasugano.cn"]` | **媒体站宿主**。非空才启用宿主隔离:`/media/*` 之外的路径来自媒体宿主 → 404;`/media/*` 来自非媒体宿主(且 `api_hosts` 非空)→ 404。**写域名即可,端口不用写**(见下方"端口容错") |
|
||
| `api_hosts` | `["mdmuse.kasugano.cn"]` | **API 站宿主**。配了它之后,媒体站的域名(以及任何未知宿主)请求 `/media/*` 一律 404 |
|
||
| `app_token_enforce` | 先 `false`,自有 App 上线后再 `true` | `false` = 只有"带了令牌才校验"(老 App 不带令牌照常可用);`true` = 受管路径必须带令牌 |
|
||
| `max_body_bytes` | `8388608`(默认 8MiB) | 请求体上限;超限后端回 413 + 中文说明 |
|
||
| `http_backlog` | `128`(默认) | 监听队列:IIS 探活 + 设备突发请求时不会被拒连 |
|
||
|
||
改完重启服务:`systemctl restart ktv-source`,然后确认日志里有:
|
||
|
||
```
|
||
反向代理头已启用:trusted_proxies=['192.168.100.10'](只有这些地址发来的 X-Forwarded-* 才被采信)
|
||
App 令牌:有效期 600 秒、绑定客户端IP=True、单次=False、持久化=False、限流=60/分钟/来源IP、强制=False、受管路径=['/music/do.php', '/push_download'](强制=false 时老 App 不带令牌照常可用)
|
||
双站点宿主隔离已启用:媒体宿主=['md.kasugano.cn']、API 宿主=['mdmuse.kasugano.cn']
|
||
```
|
||
|
||
> ⚠️ `trusted_proxies` 填的是 **IIS 机器的 IP**(即"直连后端的那台机器"),不是手机/设备的网段。
|
||
> 如果你把整个内网网段都写进去(比如 `192.168.100.0/24`),那么设备直连后端时的伪造头也会被采信 ——
|
||
> 只在"内网绝对干净、设备不可能直连 6677"时才这么写。
|
||
|
||
### 1.1 宿主判定与"端口容错"(很重要)
|
||
|
||
- 宿主判定只看两个来源,**优先可信的 `X-Forwarded-Host`**,不可信时退回 HTTP 的 `Host` 头
|
||
(见 `ktv/proxy.effective_host`)。好处:任何直连客户端自己塞
|
||
`X-Forwarded-Host: md.kasugano.cn:4433` **不会**被采信(它没经过可信代理名单)。
|
||
- ⚠️ **`X-Forwarded-Host` 常常带端口**:媒体站绑的是 **4433**(非默认端口),
|
||
IIS 的 `{HTTP_HOST}` 会是 `md.kasugano.cn:4433`。所以后端的宿主比较**对端口容错**:
|
||
配置里写 `["md.kasugano.cn"]` 能命中 `md.kasugano.cn:4433`,反过来写
|
||
`["md.kasugano.cn:4433"]` 也能命中不带端口的 `md.kasugano.cn`(两侧都按"去端口"再比一次,大小写不敏感)。
|
||
- 判定结果可以直接看:`GET /api/client_ip` 里的 `host`、`host_source`、`media_host_match`、`api_host_match`。
|
||
|
||
---
|
||
|
||
## 2. 装 IIS 模块:URL Rewrite + ARR
|
||
|
||
两个模块缺一不可:**URL Rewrite 负责改写/转发规则,ARR 负责真正发起代理请求**。
|
||
|
||
1. 打开 **Server Manager → Add Roles and Features** → 安装 **Web Server (IIS)**。
|
||
- 必须包含:`Common HTTP Features → Static Content`(否则站点根目录连静态文件都 404)。
|
||
- 建议勾掉/不装 **WebDAV Publishing**(它会在 URL Rewrite 之前拦 PUT/DELETE)。
|
||
2. 下载并安装两个模块(官方下载页):
|
||
- **URL Rewrite 2.1**:<https://www.iis.net/downloads/microsoft/url-rewrite>
|
||
- **Application Request Routing 3.0 (ARR)**:<https://www.iis.net/downloads/microsoft/application-request-routing>
|
||
3. 装完**重启 IIS 管理器**(`iisreset` 更彻底),确认左侧能看到服务器级的
|
||
**URL Rewrite** 与 **Application Request Routing Cache** 两个图标。
|
||
|
||
> 命令行(管理员 PowerShell)也可以装,但上面两个包是 MSI,用 `msiexec /i rewrite_amd64.msi` 之类即可。
|
||
> 装完再确认一次:IIS 管理器 → 你的服务器 → Features View → 应同时有 URL Rewrite 和 ARR Cache。
|
||
|
||
---
|
||
|
||
## 3. 启用 ARR 代理(Enable proxy)—— 不做这步必然 502
|
||
|
||
**做法 A(图形界面,推荐)**
|
||
|
||
1. IIS 管理器 → 选中**服务器节点**(不是站点)→ 双击 **Application Request Routing Cache**;
|
||
2. 右侧 **Server Proxy Settings...**;
|
||
3. 勾选 **Enable proxy**;
|
||
4. Time-out 建议改成 **600**(秒,10 分钟):媒体是大文件、慢客户端会拖很久,默认 120 秒不够;
|
||
同页的 Connect time-out / Send time-out 也一并给到 600;
|
||
5. 「Response buffer threshold」保持默认即可;⚠️ 若发现大文件下载**卡顿**或 IIS 内存占用高:
|
||
把它设为 **0**(不做响应缓冲,直接流式转发)。媒体是 75MB 级的 TS 文件,不需要在代理层缓存。
|
||
6. **Apply**。
|
||
|
||
**做法 B(配置文件)**:把 `applicationHost.config.snippet.xml` 里的 `<proxy ... />` 合并进
|
||
`%windir%\System32\inetsrv\config\applicationHost.config`,需要**管理员权限**;
|
||
改前先 `copy applicationHost.config applicationHost.config.bak`。
|
||
|
||
> 验证:`%windir%\System32\inetsrv\config\applicationHost.config` 里应能看到
|
||
> `<proxy enabled="true" preserveHostHeader="true" ... />`。看不到就是没生效,后面一定 502.3。
|
||
> `preserveHostHeader="true"` 是**媒体站**的关键项之一(让 ARR 把原始 Host 转给后端)。
|
||
|
||
---
|
||
|
||
## 4. 放行服务器变量(不放行 → 站点 500.19 打不开)
|
||
|
||
两份 `web.config` 里都用 `<serverVariables>` 设置 `X-Forwarded-For` 等头。IIS **默认不允许**站点设置
|
||
服务器变量,所以必须先放行(一次放行、全服通用,两个站点共用):
|
||
|
||
**做法 A(图形界面)**:IIS 管理器 → 服务器节点 → **URL Rewrite** → 右侧 **View Server Variables**
|
||
→ **Add** → 依次加入:
|
||
|
||
```
|
||
HTTP_X_FORWARDED_FOR
|
||
HTTP_X_FORWARDED_PROTO
|
||
HTTP_X_REAL_IP
|
||
HTTP_X_FORWARDED_HOST
|
||
```
|
||
|
||
**做法 B(配置文件)**:合并 snippet 里的 `<rewrite><allowedServerVariables>…`。
|
||
|
||
> 验证:随便访问一下站点,若是 **500.19**("This configuration section cannot be used at this path"),
|
||
> 就是这一段没做。也可以临时把 `web.config` 里所有 `<serverVariables>` 块删掉看站点是否恢复 ——
|
||
> 但那样代理头就传不过去了(见第 11 节 FAQ 的替代方案)。
|
||
|
||
> **关于 ARR 自带的行为**:ARR 在多数版本里**不会**自动追加 `X-Forwarded-For`(它只负责把请求转给后端),
|
||
> 所以这里用 `<serverVariables>` 显式设置。两份 `web.config` 里 XFF 的值**有意只写 `{REMOTE_ADDR}`**
|
||
> (不拼接客户端自己传来的 XFF):这样后端拿到的 XFF 只有一个值 = IIS 看到的真实客户端 IP,
|
||
> 客户端再怎么伪造也没用(后端还有 `trusted_proxies` 白名单兜底)。
|
||
> 如果你在 IIS 前面还挂了一层代理(比如又一层 nginx/CDN),才需要把值改成
|
||
> `{HTTP_X_FORWARDED_FOR},{REMOTE_ADDR}` 以保留上游链路。
|
||
|
||
---
|
||
|
||
## 5. 建两个站点、两个绑定、(两张证书或)一张 SAN 证书
|
||
|
||
### 5.1 API 站 `mdmuse.kasugano.cn`(HTTPS 443)
|
||
|
||
1. **建站点**:IIS 管理器 → Sites → **Add Website**
|
||
- Site name:`ktv-api`
|
||
- Physical path:`C:\inetpub\ktv-api`(**把 `api-site/web.config` 复制进来,并改名为 `web.config`**)
|
||
- Binding:`https` / 端口 `443` / Host name `mdmuse.kasugano.cn`
|
||
2. **应用程序池**(Application Pools → `ktv-api` → Advanced Settings):
|
||
- `.NET CLR version`:**No Managed Code**(全走 URL Rewrite + ARR,不需要 .NET)
|
||
- `Managed pipeline mode`:Integrated(默认即可)
|
||
- `Identity`:默认 `ApplicationPoolIdentity` 就行(纯反代不写磁盘)
|
||
- `Idle Time-out`:默认 20 分钟即可;担心被回收可以设为 0
|
||
3. 该站点提供:`token`(App 令牌)、`api.php`、`i.php`、`music/do.php`、
|
||
`push_download`、`push_download/status`、`healthz`;可选(默认注释)`api/client_ip`、`token/status`。
|
||
**其余路径 404**(包括 `/media/*`:文件下载只走媒体站)。
|
||
|
||
### 5.2 媒体站 `md.kasugano.cn`(HTTPS **4433**)
|
||
|
||
1. **建站点**:Add Website
|
||
- Site name:`ktv-media`
|
||
- Physical path:`C:\inetpub\ktv-media`(**把 `media-site/web.config` 复制进来,并改名为 `web.config`**)
|
||
- Binding:`https` / 端口 **`4433`** / Host name `md.kasugano.cn`(端口怎么填见 5.3)
|
||
2. 应用程序池设置同上(No Managed Code)。
|
||
3. 该站点**只有一条**反代规则:`media/<编号>.ts`;**其余路径一律 404**(接口不在这个域名上)。
|
||
|
||
### 5.3 HTTPS 端口 4433 怎么绑(IIS 站点绑定里改端口)
|
||
|
||
IIS 支持任意 HTTPS 端口,做法:
|
||
|
||
1. IIS 管理器 → 左侧 **Sites → ktv-media** → 右侧 **Bindings...** → **Add**;
|
||
2. Type 选 **https**;**Port 填 `4433`**(不是 443);IP 地址留 `All Unassigned`(或只绑内网网卡);
|
||
3. **Host name** 填 `md.kasugano.cn`;
|
||
4. **SSL certificate** 选你的证书(见 5.4);若需要 SNI,勾选 **Require Server Name Indication**;
|
||
5. OK → Close。之后访问就是 `https://md.kasugano.cn:4433/media/<编号>.ts`。
|
||
|
||
> - 端口 4433 **必须同时在网络层放行**:Windows 防火墙 `New-NetFirewallRule -DisplayName ktv-media-4433 -Direction Inbound -Protocol TCP -LocalPort 4433 -Action Allow`;
|
||
> 如果前面还有路由器/云防火墙/NAT,也要把 4433 映射出去(App 侧用的下载地址由后端 `public_media_base` / `X-Forwarded-Host` 决定,必须是域名 + 4433)。
|
||
> - 非 443 端口下 IIS 的 `{HTTP_HOST}` 会带端口(`md.kasugano.cn:4433`),并原样传进 `X-Forwarded-Host`;
|
||
> 后端对端口容错(第 1.1 节),所以 `media_hosts=["md.kasugano.cn"]` 就够。
|
||
> - 若你的环境只允许 443,也可以把媒体站建成"另一个 443 上的站点"(用不同域名 + SNI 区分),
|
||
> 此时把 `public_media_base`、`public_host`、以及 App 里的下载域名一起改成不带端口的形式即可;
|
||
> 后端配置(`media_hosts`)不用改。
|
||
|
||
### 5.4 证书:一张带 SAN 最省事
|
||
|
||
- **一张证书带 SAN(推荐)**:申请/签发时把 `md.kasugano.cn` 与 `mdmuse.kasugano.cn`
|
||
都写进 SAN,两个绑定选同一张证书。
|
||
- **两张证书**:各自签发,分别绑到两个站点。
|
||
- 自签(内网测试):PowerShell(管理员)
|
||
```powershell
|
||
New-SelfSignedCertificate -DnsName md.kasugano.cn,mdmuse.kasugano.cn -CertStoreLocation cert:\LocalMachine\My
|
||
```
|
||
自签证书要**导入到设备的信任库**,否则 App 会因证书校验失败而连不上(这也是为什么更推荐公网域名 + 正式证书)。
|
||
- Let's Encrypt 类工具(win-acme)默认只处理 443;**4433 需要手动建绑定并选证书**
|
||
(续期后证书指纹变了要重新选一次,或改用"一张 SAN 证书 + 手动导入"的方式)。
|
||
|
||
### 5.5 HTTP → HTTPS 跳转(可选)
|
||
|
||
给任一站点再加一个 http 绑定,然后在该站点的 URL Rewrite 里加一条 301 到 https 的规则。
|
||
⚠️ 若同时保留 http 与 https 绑定,`web.config` 里写死的 `HTTP_X_FORWARDED_PROTO=https`
|
||
需要按文件末尾的说明调整。
|
||
|
||
---
|
||
|
||
## 6. 把占位值换成你自己的(逐条核对)
|
||
|
||
| 位置 | 占位值 | 改成 |
|
||
|---|---|---|
|
||
| `api-site/web.config` 里 7 条规则的 `url="http://192.168.100.232:6677/{R:0}"` | `192.168.100.232:6677` | 你内网 Linux 服务的真实 `IP:端口` |
|
||
| `media-site/web.config` 里 1 条规则的 `url` | 同上 | 同上 |
|
||
| 两份配置里的 `HTTP_X_FORWARDED_PROTO` `value="https"` | `https` | 站点只用 HTTPS 就保持;同时有 HTTP 绑定就按文件末尾说明改 |
|
||
| IIS 站点绑定 | `mdmuse.kasugano.cn:443` / `md.kasugano.cn:4433` | 你的两个域名/端口 |
|
||
| 后端 `server-config.json` | `trusted_proxies` / `public_media_base` / `public_host` / `media_hosts` / `api_hosts` | IIS 机器 IP / `https://md.kasugano.cn:4433/media` / `md.kasugano.cn:4433` / `["md.kasugano.cn"]` / `["mdmuse.kasugano.cn"]` |
|
||
| 后端 `push_token` 与 App 侧 `pushToken` | `local` | 改成你自己的强随机串(可选但推荐) |
|
||
|
||
---
|
||
|
||
## 7. 验证(可直接复制)
|
||
|
||
> 下面 `mdmuse.kasugano.cn` = API 站,`md.kasugano.cn:4433` = 媒体站;
|
||
> 这些命令在**任何能访问 IIS 的机器**上跑(Windows 上可用 curl.exe / PowerShell 的 `curl` 别名)。
|
||
|
||
### 7.1 API 站
|
||
|
||
```bash
|
||
# ① 探活:必须 200 + JSON(后端是内存快照,应该"秒回")
|
||
curl -s -o /dev/null -w '%{http_code} %{time_total}s\n' https://mdmuse.kasugano.cn/healthz
|
||
curl -s https://mdmuse.kasugano.cn/healthz
|
||
# → {"ok":true,"version":"1.0.0","uptime_sec":…,"library":…,"keeps":…,"downloader":{…},"snapshot_age_sec":…}
|
||
|
||
# ② App 令牌:必须 200 + token + expires_in:600 + Cache-Control: no-store
|
||
curl -s -D - https://mdmuse.kasugano.cn/token
|
||
# → {"ok":true,"token":"<43 位随机串>","expires_in":600,"expires_at":<unix>}
|
||
# 响应头里必须有 Cache-Control: no-store
|
||
|
||
# ③ 老 App 取 token(i.php):必须 200 + code:200(**不带**我们的 App 令牌)
|
||
curl -s 'https://mdmuse.kasugano.cn/i.php?appid=ktv&mac=800b510bf4fd_2d7bb4a7d7f7&sn=2d7bb4a7d7f7&time=1&ver=2.0&vn=4.1.3.03281430&sign=x'
|
||
# → {"code":200,"msg":"ok","retry":5,...,"token":"..."}
|
||
|
||
# ④ 点歌取址(把 <编号> 换成歌曲表里"已下"的编号):返回的 data 必须是 https://md.kasugano.cn:4433/media/…
|
||
curl -s 'https://mdmuse.kasugano.cn/music/do.php?musicno=<编号>&ls=1&resolution=720&token=local'
|
||
# → {"code":200,"msg":"SUCCESS","data":"https://md.kasugano.cn:4433/media/<编号>.ts","limit":2000}
|
||
# ⚠️ 注意 token=local 是**老协议自己的**令牌(i.php 返回的那个),不是 App 令牌;
|
||
# 后端认得它,不会当成"错误的 App 令牌"(见第 8 节)
|
||
|
||
# ⑤ 推送接口:不带任何凭证必须 401
|
||
curl -s -o /dev/null -w '%{http_code}\n' -X POST 'https://mdmuse.kasugano.cn/push_download' -d 'tid=1&url=http://x/y.ts'
|
||
# → 401
|
||
# 用 push_token:
|
||
curl -s -X POST 'https://mdmuse.kasugano.cn/push_download' -H 'X-Push-Token: local' \
|
||
--data-urlencode 'url=http://cdn.example.com/480p/7300123.ts?token=abc&t=1' -d 'tid=7300123'
|
||
# → {"ok":true,...}(或 duplicate;期望 200/400,不是 401/404/502)
|
||
# 或用 App 令牌(第 8 节流程):-H "X-App-Token: <第②步拿到的 token>"
|
||
|
||
# ⑥ 未列出的路径必须 404(管理端口/管理界面不对外;媒体也不在 API 站)
|
||
curl -s -o /dev/null -w '%{http_code}\n' https://mdmuse.kasugano.cn/api/stats # → 404
|
||
curl -s -o /dev/null -w '%{http_code}\n' https://mdmuse.kasugano.cn/ # → 404
|
||
curl -s -o /dev/null -w '%{http_code}\n' https://mdmuse.kasugano.cn/media/1.ts # → 404
|
||
|
||
# ⑦ 宿主隔离(后端侧):API 域名请求 /media/* → 404;这一条"通过"说明 api_hosts 生效
|
||
```
|
||
|
||
### 7.2 媒体站
|
||
|
||
```bash
|
||
# ① 只有 /media/* 通,其余必须 404(接口不在这个域名上)
|
||
curl -s -o /dev/null -w '%{http_code}\n' 'https://md.kasugano.cn:4433/healthz' # → 404
|
||
curl -s -o /dev/null -w '%{http_code}\n' 'https://md.kasugano.cn:4433/music/do.php?musicno=1' # → 404
|
||
|
||
# ② 媒体 HEAD:必须 200 + Content-Length + Accept-Ranges: bytes
|
||
curl -sI 'https://md.kasugano.cn:4433/media/<编号>.ts' | grep -Ei 'HTTP/|content-length|accept-ranges|content-type'
|
||
|
||
# ③ Range:必须 206 + Content-Range(这一步不过 = App 断点续传/边下边播会出问题)
|
||
curl -s -D - -o /dev/null -r 0-1023 'https://md.kasugano.cn:4433/media/<编号>.ts' \
|
||
| grep -Ei 'HTTP/|content-range|content-length|accept-ranges'
|
||
# → HTTP/1.1 206 Partial Content / Content-Range: bytes 0-1023/<总长> / Content-Length: 1024
|
||
|
||
# ④ 宿主隔离(后端侧):媒体域名请求接口路径 → 404(IIS 这一层已经 404 了,这里验证的是"绕过 IIS 也 404")
|
||
```
|
||
|
||
### 7.3 后端侧自测(在 Linux 上直连 6677)
|
||
|
||
```bash
|
||
# ① 反代头与宿主判定是否按预期被采信(trusted_proxies 含 127.0.0.1 时才 via_proxy=true)
|
||
curl -s 'http://127.0.0.1:6677/api/client_ip' \
|
||
-H 'X-Forwarded-For: 203.0.113.9' -H 'X-Forwarded-Proto: https' \
|
||
-H 'X-Forwarded-Host: md.kasugano.cn:4433'
|
||
# → host=md.kasugano.cn:4433、host_source=x-forwarded-host、media_host_match=true、api_host_match=false
|
||
|
||
# ② 媒体站的宿主能下文件、不能调接口(把 media_hosts 配成 ["md.kasugano.cn"] 后)
|
||
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:6677/media/1.ts' \
|
||
-H 'X-Forwarded-For: 203.0.113.9' -H 'X-Forwarded-Host: md.kasugano.cn:4433' # → 200/404(文件不存在)
|
||
curl -s -o /dev/null -w '%{http_code}\n' 'http://127.0.0.1:6677/music/do.php?musicno=1' \
|
||
-H 'X-Forwarded-For: 203.0.113.9' -H 'X-Forwarded-Host: md.kasugano.cn:4433' # → 404(宿主隔离)
|
||
|
||
# ③ 令牌流程(App 侧将来要走的路)
|
||
TOKEN=$(curl -s http://127.0.0.1:6677/token | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
|
||
curl -s 'http://127.0.0.1:6677/music/do.php?musicno=1' -H "X-App-Token: $TOKEN" # → 200(或 code:404 未下载)
|
||
curl -s http://127.0.0.1:6677/token/status # → 令牌计数
|
||
|
||
# ④ 推送限流是否"按设备分开":从两台设备各推一首,观察 /push_download/status 的计数
|
||
curl -s 'http://127.0.0.1:6677/push_download/status'
|
||
```
|
||
|
||
### 7.4 排查"服务端到底认哪个客户端 IP / 哪个宿主"
|
||
|
||
默认**不**对外暴露 `/api/client_ip`(属于内部信息)。两种做法二选一:
|
||
|
||
- **A. 临时放通**:取消 `api-site/web.config` 里第 ⑨ 条规则的注释(排完记得注释回去),然后
|
||
`curl -s https://mdmuse.kasugano.cn/api/client_ip`
|
||
→ `{"ok":true,"client_ip":"203.0.113.9","peer_ip":"192.168.100.10","via_proxy":true,"source":"x-forwarded-for","host":"mdmuse.kasugano.cn","host_source":"x-forwarded-host","media_host_match":false,"api_host_match":true,...}`
|
||
- **B. 不改 IIS 配置**:在 **Linux 后端**上直连自测(7.3 的第 ① 条)。
|
||
|
||
判读要点:`client_ip` = 设备 IP、`peer_ip` = IIS 机器 IP、`via_proxy` = true、`ignored_reason` 为空
|
||
= 代理头生效;`host` = 判定用的宿主、`host_source` = 它是从哪个头来的。
|
||
|
||
---
|
||
|
||
## 8. App 令牌(默认 10 分钟)与 App 侧流程
|
||
|
||
**目的**:给**未来的自有 App**一个短时效随机凭证,避免地址被随意抓取/转发。
|
||
**兼容红线**:已发布的官方 App 不带我们的令牌,所以默认 `app_token_enforce=false` ——
|
||
它只"带了令牌才校验",老 App 一个字节都不用改(`/i.php`、`/music/do.php` 的参数与签名算法完全没动)。
|
||
|
||
自有 App 的流程(三步):
|
||
|
||
```bash
|
||
# ① 取令牌(GET /token,也接受 POST);响应禁止缓存(Cache-Control: no-store)
|
||
curl -s https://mdmuse.kasugano.cn/token
|
||
# → {"ok":true,"token":"…43 位随机串…","expires_in":600,"expires_at":1750000000}
|
||
|
||
# ② 带上令牌取地址(请求头 X-App-Token;过期/换 IP 都是 401)
|
||
curl -s 'https://mdmuse.kasugano.cn/music/do.php?musicno=<编号>&ls=1&resolution=720' \
|
||
-H "X-App-Token: <①的 token>"
|
||
|
||
# ③ 点歌推送同样可以只带 App 令牌(或继续用 push_token,二者任一)
|
||
curl -s -X POST https://mdmuse.kasugano.cn/push_download -H "X-App-Token: <①的 token>" \
|
||
--data-urlencode 'url=https://cdn.example.com/480p/<编号>.ts?token=abc&t=1' -d 'tid=<编号>'
|
||
```
|
||
|
||
后端口径(配置项见 `server/README.md`「App 令牌」一节):
|
||
|
||
| 键 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `app_token_ttl_sec` | 600 | 有效期(秒),钳制 30~86400 |
|
||
| `app_token_bind_ip` | true | 绑定**签发时的真实客户端 IP**(换 IP 用同一令牌 → 401) |
|
||
| `app_token_single_use` | false | 用过即失效;**媒体地址是多次调用,别开** |
|
||
| `app_token_paths` | `/music/do.php,/push_download` | 受管路径前缀(`/i.php` 是"带了就校验",默认不强制) |
|
||
| `app_token_enforce` | false | **什么时候打开**:自有 App 已上线、且确认全部客户端都会先取令牌之后 |
|
||
| `token_rate_limit_per_min` | 60 | 同一来源 IP 每分钟最多签发几个(防刷) |
|
||
|
||
> ⚠️ **老协议自己的 `token` 参数不算 App 令牌**:`/i.php` 返回的 token(默认 `local`)会出现在
|
||
> `do.php?...&token=local` 里,`/push_download` 的 body `token` 则是 `push_token` 口径。
|
||
> 后端对这两处做了区分,不会把它们当成"错误的 App 令牌"。
|
||
> **自有 App 请统一用请求头 `X-App-Token`**(没有任何歧义)。
|
||
>
|
||
> ⚠️ `app_token_enforce=true` 之后,`app_token_paths` 命中的路径(含 `/push_download/status`)
|
||
> 都要带令牌;运维临时核对可以直连内网 6677,或先 `GET /token` 再带上。
|
||
|
||
---
|
||
|
||
## 9. 管理端口(运维入口)怎么保护
|
||
|
||
管理端口(默认 **6678**)承载管理界面与管理 API:**绝对不要**做反代、不要映射到公网。
|
||
本目录的配置里**没有任何规则指向它**,两个站点都把其余路径 404,所以公网根本走不到它。
|
||
在此之上再加两道:
|
||
|
||
**做法一:IIS 侧限制(不改后端)**
|
||
- 不给 6678 建任何站点/绑定(默认就没有);
|
||
- 如果出于运维便利想通过 IIS 访问管理界面,就**单独建一个站点 + IP 限制**:
|
||
IIS 管理器 → 该站点 → **IP Address and Domain Restrictions** → Add Allow Entry
|
||
(只允许你的运维网段,如 `192.168.100.0/24`)→ 再把默认行为设为 **Deny**;
|
||
同时在绑定时**只绑内网网卡地址**(不要 `*`),这样公网连不到。
|
||
- 更简单:**干脆不做反代**,运维时用内网直连 `http://192.168.100.232:6678/`(配合下面 Linux 防火墙)。
|
||
|
||
**做法二:Linux 侧只让内网访问(推荐一起做)**
|
||
```bash
|
||
# 只允许运维网段访问 6678,其余一律拒绝
|
||
ufw allow from 192.168.100.0/24 to any port 6678 proto tcp
|
||
ufw deny 6678/tcp
|
||
# 或者把管理端口只绑内网网卡(更彻底)
|
||
# 配置里 admin_host 改成 192.168.100.232,重启服务
|
||
```
|
||
也可以给管理接口加令牌:`admin_token` 非空后,`/api/*` 需要
|
||
`X-Auth-Token: <token>`(或 `?token=<token>`)。
|
||
|
||
---
|
||
|
||
## 10. 与后端配置的对应关系(一页速查)
|
||
|
||
| 本目录配置 | 后端配置 | 说明 |
|
||
|---|---|---|
|
||
| 两份 `web.config` 的 `HTTP_X_FORWARDED_FOR` / `HTTP_X_REAL_IP` | `trusted_proxies`(IIS 机器 IP)+ `trust_proxy_headers=true` | 真实客户端 IP → 推送限流分桶 + 日志 |
|
||
| 两份 `web.config` 的 `HTTP_X_FORWARDED_PROTO` | 同上(同一个开关) | `/music/do.php` 返回 `https://域名/media/…` |
|
||
| 两份 `web.config` 的 `HTTP_X_FORWARDED_HOST` | `media_hosts` / `api_hosts` | **宿主隔离**:媒体宿主只能下文件、API 宿主不能批量下载(端口容错) |
|
||
| API 站的 7 条规则 | `app_token_paths` / `app_token_enforce` | App 令牌只在 API 站需要;`enforce=false` 时老 App 不受影响 |
|
||
| `<proxy preserveHostHeader="true">`(服务器级/snippet) | —— | 后端看到真实 Host(也可靠 `X-Forwarded-Host`) |
|
||
| `<requestLimits maxAllowedContentLength="33554432">` | `max_body_bytes`(默认 8MiB) | IIS 的限制要 **≥** 后端,否则 413 由 IIS 先返回 |
|
||
| `<serverRuntime uploadReadAheadSize>` | —— | 大请求体读入缓冲,避免上传被截断 |
|
||
| `<urlCompression …false />` | —— | 保证 Range/206 原样透传 |
|
||
| ARR Time-out = 600 | `timeout_sec`(下载用,另一回事) | 媒体是大文件,代理层超时要给足 |
|
||
| —— | `http_backlog=128` | 监听队列(IIS 探活 + 突发请求) |
|
||
|
||
---
|
||
|
||
## 11. 常见问题(现象 → 原因 → 处置)
|
||
|
||
| 现象 | 原因 | 处置 |
|
||
|---|---|---|
|
||
| 站点报 **500.19**(config error,打不开) | ① 服务器变量没放行(第 4 节);② `web.config` 放错/内容冲突(比如把旧的"三合一"`web.config` 又复制进了站点根目录) | 先做第 4 节;确认站点根目录里的 `web.config` **来自 `api-site/` 或 `media-site/`**,而不是上级目录那个只含说明的索引 |
|
||
| **502.3 / 502 Bad Gateway** | ARR 代理没启用;或后端地址/端口写错;或 Linux 侧 6677 没监听/被防火墙拦 | IIS 管理器 → ARR Cache → Server Proxy Settings → 勾 **Enable proxy**;`web.config` 里后端地址改成真实 `IP:端口`;在 IIS 机器上 `curl http://192.168.100.232:6677/healthz` 直连试一下 |
|
||
| **504 / 请求超时**(大文件下载中断) | ARR 默认 Time-out 120 秒;媒体下载会超过 | ARR → Server Proxy Settings → Time-out = **600**(秒),Connect/Send 也给 600 |
|
||
| 请求返回 **404**,但"明明配了规则" | ① 路径不在该站点的白名单里(API 站没有 `/media/*`,媒体站只有 `/media/*`);② 宿主隔离命中(媒体宿主调接口 / API 宿主拉媒体);③ 报错的是另一份配置 | 先确认打的是哪个域名/端口;再看 `GET /api/client_ip` 的 `host`/`media_host_match`/`api_host_match`;最后核对站点根目录里的 `web.config` 是哪一份 |
|
||
| **媒体站能下文件,但 API 域名拉媒体 404** | 这是**设计如此**(`api_hosts` 生效) | 正常现象;要让 API 域名也能下文件,就把 `api_hosts` 留空(但那样就没有隔离了) |
|
||
| **媒体站只能下文件,调接口 404** | 同上(媒体宿主 + `media_hosts` 生效) | 正常现象;接口请用 API 域名 |
|
||
| **`X-Forwarded-For` 没生效**:`/api/client_ip` 里 `via_proxy=false`、`source=tcp` | ① 后端 `trust_proxy_headers=false`;② `trusted_proxies` 里没有 IIS 机器的 IP;③ IIS 侧服务器变量没放行/没设 | 看 `/api/client_ip` 的 `ignored_reason`,它会直接说明卡在哪一步;后端改成 `trust_proxy_headers=true` + `trusted_proxies=["<IIS 内网 IP>"]`(改完重启) |
|
||
| **HTTPS 下 do.php 返回 `http://…`** | `X-Forwarded-Proto` 没传/没被采信,后端只能按 `public_media_base` 的 scheme 拼 | 修好代理头;同时把 `public_media_base` 改成 `https://md.kasugano.cn:4433/media` 兜底。⚠️ 绝不能留裸 IP(App 会拒收) |
|
||
| **Range 失效**(`curl -r 0-1023` 返回 200 且整文件) | 中间层把 Range 吃掉了/改写了:开了动态压缩、装了会缓存/改写的模块,或还有一条出站规则把响应改成 200 | 两份 `web.config` 里已 `<urlCompression …false />`;ARR 的 Response buffer threshold 设 0;`curl -sI` 确认 `Accept-Ranges: bytes`,`curl -r` 确认 **206 + Content-Range** |
|
||
| **`/healthz` 慢 / 超时** | 后端探活走的是**内存快照**,正常是毫秒级 | Linux 本机 `curl -w '%{time_total}' http://127.0.0.1:6677/healthz` 验证后端确实快;若本机快、经 IIS 慢,检查是否有额外重写规则/模块介入 |
|
||
| 站点根目录 `/` 返回 **404** | 这是**故意**的(未列路径全 404) | 正常现象;要加白名单就在 404 规则**之前**复制一条反代规则 |
|
||
| 推送全部 **429**("推送过于频繁") | 反代头没生效 → 所有设备共用 IIS 的 IP 一份额度 | 修 `X-Forwarded-For`;用 `/push_download/status` 与事件日志核对来源 IP |
|
||
| 取令牌返回 **429**("获取令牌过于频繁") | 同一来源 IP 每分钟签发超过 `token_rate_limit_per_min`(默认 60) | 正常防刷;确认不是"把 4433/443 后面的真实 IP 判成了代理地址",或按需调大该项 |
|
||
| 自己 App 带令牌仍然 **401「令牌无效或已过期,请重新获取」** | ① 令牌过期(默认 10 分钟);② `app_token_bind_ip=true` 但客户端 IP 变了(换了网络/NAT);③ 头名写错(必须 `X-App-Token`) | 重新 `GET /token`;确认走的是同一个来源 IP(看 `/api/client_ip`);核对头名 |
|
||
| App 提示"源地址无效/拒收" | 返回给 App 的媒体地址是**裸 IP**,或域名/端口与证书不匹配 | `public_media_base` 必须是域名(建议带 `:4433`);用 `getent hosts 你的域名` 确认内网 DNS 能解析 |
|
||
|
||
---
|
||
|
||
## 12. 只想先跑一个站点(最小验证)
|
||
|
||
1. 先只建 **API 站**(用 `api-site/web.config`),把 `public_media_base` / `public_host` 指向
|
||
`https://mdmuse.kasugano.cn`(不带 4433),此时下载地址也落在 API 域名上;
|
||
2. **后端 `media_hosts` 留空**(= 不启用宿主隔离),`api_hosts` 也留空;
|
||
3. 按第 7.1 节验证通过后,再建媒体站、配 `media_hosts=["md.kasugano.cn"]`、
|
||
`api_hosts=["mdmuse.kasugano.cn"]`,把 `public_media_base` 改成 `https://md.kasugano.cn:4433/media`,
|
||
并把 App 里的下载域名切过去 —— 分两步上线,出问题最容易定位。
|