# 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**: - **Application Request Routing 3.0 (ARR)**: 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` 里的 `` 合并进 `%windir%\System32\inetsrv\config\applicationHost.config`,需要**管理员权限**; 改前先 `copy applicationHost.config applicationHost.config.bak`。 > 验证:`%windir%\System32\inetsrv\config\applicationHost.config` 里应能看到 > ``。看不到就是没生效,后面一定 502.3。 > `preserveHostHeader="true"` 是**媒体站**的关键项之一(让 ARR 把原始 Host 转给后端)。 --- ## 4. 放行服务器变量(不放行 → 站点 500.19 打不开) 两份 `web.config` 里都用 `` 设置 `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 里的 `…`。 > 验证:随便访问一下站点,若是 **500.19**("This configuration section cannot be used at this path"), > 就是这一段没做。也可以临时把 `web.config` 里所有 `` 块删掉看站点是否恢复 —— > 但那样代理头就传不过去了(见第 11 节 FAQ 的替代方案)。 > **关于 ARR 自带的行为**:ARR 在多数版本里**不会**自动追加 `X-Forwarded-For`(它只负责把请求转给后端), > 所以这里用 `` 显式设置。两份 `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":} # 响应头里必须有 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=`)。 --- ## 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 不受影响 | | ``(服务器级/snippet) | —— | 后端看到真实 Host(也可靠 `X-Forwarded-Host`) | | `` | `max_body_bytes`(默认 8MiB) | IIS 的限制要 **≥** 后端,否则 413 由 IIS 先返回 | | `` | —— | 大请求体读入缓冲,避免上传被截断 | | `` | —— | 保证 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=[""]`(改完重启) | | **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` 里已 ``;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 里的下载域名切过去 —— 分两步上线,出问题最容易定位。