按 HAR 抓包对齐原版协议: 补 api.php 取配置 + 识别业务码 403 + 按试用配额限源
依据用户提供的 HAR(e.ac19.cn, Reqable): 原版客户端是三步
1) POST /api.php action=get_config&device_id=<mac> (表单 + Dalvik UA)
2) GET /i.php 参数与 App 完全一致
3) GET /music/do.php 参数与 App 完全一致, 返回真源 CDN 地址
发现并修复三处差异:
1. 我们从未调 api.php(原版取址前必调)
→ KtvJsBridge.rawCleanPost(): 干净 POST + Content-Type 表单 + Dalvik UA
(UA 由本机 Build.VERSION.RELEASE/MODEL/ID 拼出, 与 HAR 同形态)
→ SongUrlResolver.ensureConfig(): 缓存 30 分钟, 桥不可用则跳过
→ SongApiClient 注入 POST 通道
2. 风控拒绝的真实形态是 HTTP 200 + 业务码 403
{"code":403,"msg":"请求被拒绝:检测到异常行为"} —— 不是 HTTP 403
→ i.php / do.php 两处业务码 403 都置 sawForbidden(诊断不再误判为"没有地址")
3. 无视试用配额: api.php 给出 trial_times=2 / trial_duration=5
→ 一次取址最多尝试 trial_times 个源(原先 9 个源全扫会把额度瞬间打光)
请求头分工与 HAR 一致: i.php/do.php 只有 Host+Accept 的干净 GET;
仅 api.php 带表单 Content-Type 与 Dalvik UA。
日志: 远程一条 SOURCE-CONFIG trial_enabled=… trial_times=…; 原始 JSON/卡密提示只写本地。
本机实测: api.php 返回与 HAR 相同配置; do.php 返回 code=403 检测到异常行为
(本出口 IP 处于风控/未授权: network_verification=true + 卡密机制, 额度按 IP 计)。
版本号按规则 3: 1.5.8(40) → 1.5.9(41); 按要求未打包
验证: compileReleaseKotlin + 单元测试(9 例全过)
This commit is contained in:
@@ -46,6 +46,9 @@ object SongUrlResolver {
|
||||
*/
|
||||
private const val BLOCKED_COOLDOWN_MS = 3 * 60_000L
|
||||
|
||||
/** `api.php` 配置缓存时长(原版每次启动/长时间后取一次即可)。 */
|
||||
private const val CONFIG_TTL_MS = 30 * 60_000L
|
||||
|
||||
private val DEFAULT_HOSTS = listOf(
|
||||
// 与其他入口同一后台、同一协议;2026-10 实测唯一仍下发真源 CDN 地址的入口,放首位。
|
||||
"http://gz.ac16.vip",
|
||||
@@ -65,6 +68,26 @@ object SongUrlResolver {
|
||||
|
||||
@Volatile private var fetch: ((String) -> String?)? = null
|
||||
|
||||
/** 干净 POST 通道(`api.php` 取配置用;未注入时跳过配置拉取)。 */
|
||||
@Volatile private var post: ((String, String) -> String?)? = null
|
||||
|
||||
/**
|
||||
* 源站配置(原版客户端取址前会 POST `api.php` 的 `action=get_config` 拿到)。
|
||||
*
|
||||
* 实测(HAR 2026-10-08):`{"trial_enabled":true,"trial_times":2,"trial_duration":5,
|
||||
* "network_verification":true,"heartbeat_interval":3600,"dialog_title":"请输入商家提供的正确卡密"}`
|
||||
* —— **试用额度只有 2 次 / 5 分钟**,所以取址**不能把 9 个源全扫一遍**(会把额度瞬间打光,
|
||||
* 之后就一直 `code=403 检测到异常行为`)。
|
||||
*/
|
||||
private data class SourceConfig(
|
||||
val trialTimes: Int,
|
||||
val trialDurationMin: Int,
|
||||
val networkVerification: Boolean,
|
||||
)
|
||||
|
||||
@Volatile private var config: SourceConfig? = null
|
||||
@Volatile private var configAt: Long = 0L
|
||||
|
||||
@Volatile private var sn: String = KtvJsBridge.DEVICE_SN
|
||||
|
||||
/** 本轮取址是否遇到过 HTTP 403(源站风控的直接信号)。 */
|
||||
@@ -112,6 +135,47 @@ object SongUrlResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/** 注入"干净 POST"通道(`api.php` 取配置)。 */
|
||||
fun installPost(post: (String, String) -> String?) {
|
||||
this.post = post
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取源站配置(原版客户端在取址前会调一次 `api.php`),缓存 [CONFIG_TTL_MS]。
|
||||
*
|
||||
* 用途:① 日志里给出"是否启用试用/试用次数/时长/是否需要卡密"这些**结论**;
|
||||
* ② 按 `trial_times` 限制一次取址尝试的源数量,避免把试用额度瞬间耗尽。
|
||||
*/
|
||||
private fun ensureConfig(host: String, mac: String) {
|
||||
if (System.currentTimeMillis() - configAt < CONFIG_TTL_MS) return
|
||||
val channel = post ?: return
|
||||
val form = "action=get_config&device_id=$mac"
|
||||
val raw = runCatching { channel("$host/api.php", form) }.getOrNull() ?: return
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
val status = envelope.optInt("status")
|
||||
if (status !in 200..299) {
|
||||
MiliLog.localWarn("SOURCE-CONFIG api.php HTTP $status")
|
||||
return
|
||||
}
|
||||
val body = runCatching { JSONObject(envelope.optString("body")) }.getOrNull() ?: return
|
||||
val c = body.optJSONObject("config") ?: body
|
||||
val times = c.optInt("trial_times", 0)
|
||||
val dur = c.optInt("trial_duration", 0)
|
||||
val verify = c.optBoolean("network_verification", false)
|
||||
config = SourceConfig(times, dur, verify)
|
||||
configAt = System.currentTimeMillis()
|
||||
// 远程只留一条"结论";完整 JSON 与卡密提示只写本地。
|
||||
MiliLog.info(
|
||||
"SOURCE-CONFIG trial_enabled=${c.optBoolean("trial_enabled", false)} " +
|
||||
"trial_times=$times trial_duration=${dur}min network_verification=$verify " +
|
||||
"heartbeat=${c.optInt("heartbeat_interval", 0)}s",
|
||||
)
|
||||
MiliLog.local("SOURCE-CONFIG raw=${body.toString().take(300)}")
|
||||
if (verify && c.optString("dialog_title").isNotEmpty()) {
|
||||
MiliLog.local("SOURCE-CONFIG 源站要求卡密:${c.optString("dialog_title")}")
|
||||
}
|
||||
}
|
||||
|
||||
/** 依次尝试各源,返回可用的播放地址;全部失败返回 null。 */
|
||||
fun resolve(musicno: String, resolution: String = "720", h265: Boolean = false): String? {
|
||||
if (musicno.isBlank()) return null
|
||||
@@ -131,7 +195,15 @@ object SongUrlResolver {
|
||||
sawForbidden = false
|
||||
val mac = runCatching { DeviceMac.effective() }.getOrDefault(sn)
|
||||
val startedAt = System.currentTimeMillis()
|
||||
val hosts = synchronized(hostOrder) { hostOrder.toList() }
|
||||
val allHosts = synchronized(hostOrder) { hostOrder.toList() }
|
||||
// 原版取址前会先 POST api.php 取配置;这里顺带用它限制一次取址的源数量
|
||||
// (试用额度只有 trial_times 次,扫全部源会把额度瞬间打光)。
|
||||
ensureConfig(allHosts.firstOrNull().orEmpty(), mac)
|
||||
val maxHosts = config?.trialTimes?.takeIf { it > 0 }?.coerceAtMost(allHosts.size) ?: allHosts.size
|
||||
if (maxHosts < allHosts.size) {
|
||||
MiliLog.local("NATIVE-SONG 按试用额度限制本次取址源数=$maxHosts(共 ${allHosts.size} 个)")
|
||||
}
|
||||
val hosts = allHosts.take(maxHosts)
|
||||
var lastDetail = "no-source"
|
||||
var adOnlyCount = 0 // 返回了地址但被判为广告/测试资源的源数
|
||||
var otherFailCount = 0 // token 失败 / 无地址 / 网络异常的源数
|
||||
@@ -216,7 +288,9 @@ object SongUrlResolver {
|
||||
val params = "appid=$APP_ID&mac=${mac}_$sn&sn=$sn&time=$ts&ver=$VER&vn=$VN"
|
||||
val sign = md5(params + APP_KEY)
|
||||
val json = getJson(channel, "$host/i.php?$params&sign=$sign") ?: return null
|
||||
if (json.optInt("code") != 200) {
|
||||
val tokenCode = json.optInt("code")
|
||||
if (tokenCode == 403) sawForbidden = true // 业务码 403 = 风控(见 do.php 同处注释)
|
||||
if (tokenCode != 200) {
|
||||
MiliLog.localWarn("NATIVE-SONG token 失败 源=$host code=${json.optInt("code")} msg=${json.optString("msg")}")
|
||||
return null
|
||||
}
|
||||
@@ -240,6 +314,11 @@ object SongUrlResolver {
|
||||
val json = getJson(channel, "$host/music/do.php?$params&sign=$sign") ?: return null
|
||||
val code = json.optInt("code")
|
||||
if (code != 200) {
|
||||
// ⚠️ 源站的"风控拒绝"是 **HTTP 200 + 业务码 403**(body:
|
||||
// `{"code":403,"msg":"请求被拒绝:检测到异常行为"}`,2026-10-08 实测),
|
||||
// 不是 HTTP 403 —— 以前只按 HTTP 状态判断,这类响应被当成"没有地址",
|
||||
// 日志里看不出真实原因。
|
||||
if (code == 403) sawForbidden = true
|
||||
MiliLog.localWarn("NATIVE-SONG do.php 源=$host code=$code msg=${json.optString("msg")}")
|
||||
return null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user