V1.4.10: 修复广告过滤失效(gz.ac16.vip 广告被当成歌曲下载播放)
现场事故(新设备 SM-S9180 / Android 16,WebView 正常 → 走 JS 取址): 277 JS [KtvApi] URL OK: 7303235 ls=1 via http://w.w345.my 278 点歌编号=7303235 标题=情歌(HD) 请求源=http://w.w345.my 状态=成功 urlHost=gz.ac16.vip 279 PLAY id=7303235 title=情歌(HD) src=local 即 gz.ac16.vip 的广告视频被当成歌曲下载、缓存并播放。 三处漏洞叠加: 1. ktv_api.js 的 isDemoUrl 不认识广告域名;且"ts 文件名与编号比对"用的是 /\/(\d+)\.ts/,只匹配纯数字名 —— my_ad_video.ts 这类非数字名字直接放行; 2. SongOkDownloadManager.buildDownloadUrl 的**成功分支完全没有广告校验** (AdVideoFilter.isAdVideo 全项目只在"缓存地址"与"原生解析"两处被调用); 3. SongApiClient.fetchFromDb 只做 isLikelyDemoUrl(纯 IP / r2.dev),挡不住广告域名。 且**内容校验挡不住广告**: 该广告通过 SongFileValidator(≥6MB/≥60s/TS 包完整), 被 isDownloaded() 一直复用,广告会反复播。 修复(单一闸门,全链路把关): - AdVideoFilter 新增 isTrustedSongUrl(url, musicno):非 http(s)、命中广告特征 (路径 /ad_files/、my_ad_video、**域名 *.ac16.vip**)、或 URL 里 .ts 文件名不等于 点歌编号 → 一律不可信。真源恒为 /<musicno>.ts(已实测 txog.ktvsky.com 与 download.origjoy.com 两个 CDN 域共 6 首全部相符)。 - 该闸门接入全部出入口: JS isDemoUrl(同步加固 + KTV_BRIDGE_API 8→9)、 SongUrlResolver.isValidSongUrl、SongApiClient.fetchFromDb、 以及最终兜底 SongOkDownloadManager.buildDownloadUrl(成功/缓存两条分支)。 - 判定不可信时**同时删除已落盘的歌曲文件**(purgeCachedSongFile), 否则 isDownloaded() 会一直复用那个广告文件。 - 新增 SongOkDownloadManager.purgeLegacySongCacheOnce(): 升级后一次性清理历史遗留的 可疑歌曲缓存(marker: filesDir/song_cache_purged_v1)。只是让歌曲重新下载,不动用户数据。 - 新增回归测试 AdVideoFilterTest(6 例,用日志里的真实 URL 形态,含两个真源 CDN 域); 该测试当场抓到本修复自身的一个 bug:正则把扩展名也捕获进去导致真源被误杀,已修正。 版本 1.4.9(29) 到 1.4.10(30);签名证书不变。
This commit is contained in:
+2
-2
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.local.ktv"
|
||||
minSdk 21
|
||||
targetSdk 25
|
||||
versionCode 29
|
||||
versionName "1.4.9"
|
||||
versionCode 30
|
||||
versionName "1.4.10"
|
||||
ndk {
|
||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
// KTV_BRIDGE_API: 8 (Android 缓存校验标记,请勿删除)
|
||||
// KTV_BRIDGE_API: 9 (Android 缓存校验标记,请勿删除)
|
||||
|
||||
// ─── 配置 ──────────────────────────────────────────
|
||||
const CONFIG = {
|
||||
@@ -138,13 +138,19 @@
|
||||
function isDemoUrl(url, musicno) {
|
||||
var s = String(url || '');
|
||||
if (/(?:wb66|demo)/i.test(s)) return true;
|
||||
// 广告视频专用域名(含子域)。2026-09-26 事故:新设备(WebView 正常 → 走本 JS 取址)
|
||||
// 点歌拿到了 gz.ac16.vip 的广告视频,而它通过了下述所有旧规则 → 被当成歌曲下载播放。
|
||||
if (/^https?:\/\/(?:[^\/]+\.)?ac16\.vip(?::\d+)?\//i.test(s)) return true;
|
||||
if (/\/ad_files\/|my_ad_video/i.test(s)) return true;
|
||||
if (/^https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?::\d+)?\//i.test(s)) return true;
|
||||
// R2 demo bucket: pub-*.r2.dev/ss/ 通用测试资源
|
||||
if (/pub-[a-z0-9]+\.r2\.dev\/ss\//i.test(s)) return true;
|
||||
// ts 文件名是纯数字, 若与请求的 musicno 不一致则视为广告/测试资源。
|
||||
// 原版真源 URL 的 ts 文件名总是等于 musicno (如 /7120742.ts)。
|
||||
// ts 文件名必须等于 musicno。原版真源 URL 恒为 /<musicno>.ts(已实测两个 CDN 域
|
||||
// txog.ktvsky.com / download.origjoy.com 的 6 首歌全部如此)。
|
||||
// ⚠️ 旧规则只比对"纯数字文件名"(/\/(\d+)\.ts/),像 my_ad_video.ts 这种非数字名字
|
||||
// 会直接漏过 —— 这正是广告漏放的直接原因之一。
|
||||
if (musicno) {
|
||||
var m = s.match(/\/(\d+)\.ts(?:\?|$)/);
|
||||
var m = s.match(/\/([^\/?]+)\.ts(?:\?|$)/i);
|
||||
if (m && m[1] !== String(musicno)) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -435,7 +441,7 @@
|
||||
// ─── 导出 ──────────────────────────────────────────
|
||||
global.KtvApi = KtvApi;
|
||||
global.KtvApiConfig = CONFIG;
|
||||
global.KtvBridgeApiVersion = 8;
|
||||
global.KtvBridgeApiVersion = 9;
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {KtvApi: KtvApi, CONFIG: CONFIG};
|
||||
|
||||
@@ -21,6 +21,19 @@ object AdVideoFilter {
|
||||
"my_ad_video",
|
||||
)
|
||||
|
||||
/**
|
||||
* 广告视频专用域名(命中即判定为广告,含其子域)。
|
||||
*
|
||||
* 2026-09-26 现场事故:新设备(WebView 正常 → 走 JS 取址)点歌 7303235 拿到了
|
||||
* `gz.ac16.vip` 的广告视频,而且它**通过了内容校验**(≥6MB / ≥60s / TS 包完整),
|
||||
* 于是被当歌曲缓存并播放。仅靠路径片段(`/ad_files/`、`my_ad_video`)不足以拦住 ——
|
||||
* 广告方一改文件名就漏,因此必须**按域名**兜底。
|
||||
*/
|
||||
private val AD_HOSTS = listOf(
|
||||
"gz.ac16.vip",
|
||||
"ac16.vip",
|
||||
)
|
||||
|
||||
/**
|
||||
* 替换地址。为 null 时表示「屏蔽」(返回 null,由 DB 真实歌曲地址兜底);
|
||||
* 设置后,命中广告时用该地址替换原广告。
|
||||
@@ -29,14 +42,43 @@ object AdVideoFilter {
|
||||
@JvmField
|
||||
var replacementUrl: String? = null
|
||||
|
||||
/** 判断是否为广告视频地址。 */
|
||||
/** 判断是否为广告视频地址(路径特征 + 域名)。 */
|
||||
@JvmStatic
|
||||
fun isAdVideo(url: String?): Boolean {
|
||||
if (url.isNullOrEmpty()) return false
|
||||
val lower = url.lowercase()
|
||||
return AD_MARKERS.any { lower.contains(it) }
|
||||
if (AD_MARKERS.any { lower.contains(it) }) return true
|
||||
val host = hostOf(url) ?: return false
|
||||
return AD_HOSTS.any { host == it || host.endsWith(".$it") }
|
||||
}
|
||||
|
||||
/**
|
||||
* 取址链路的**统一可信校验**(所有来源的地址都必须过这一关)。
|
||||
*
|
||||
* 判定为不可信的情况:
|
||||
* 1. 非 http(s);
|
||||
* 2. 命中广告特征([isAdVideo]);
|
||||
* 3. **URL 里的 `.ts` 文件名与点歌编号不一致** —— 真源 URL 的文件名恒等于点歌编号
|
||||
* (见 `ktv_api.js` 的说明),而广告文件常叫 `my_ad_video.ts`。
|
||||
* 旧规则只处理"纯数字且与编号不同"(`/\/(\d+)\.ts/`),非数字文件名直接放行,
|
||||
* 这正是本次广告漏放的直接原因。
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isTrustedSongUrl(url: String?, musicno: String?): Boolean {
|
||||
if (url.isNullOrEmpty()) return false
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) return false
|
||||
if (isAdVideo(url)) return false
|
||||
val name = TS_BASENAME.find(url)?.groupValues?.getOrNull(1)
|
||||
if (name != null && !musicno.isNullOrEmpty() && name != musicno) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hostOf(url: String): String? =
|
||||
runCatching { java.net.URI(url).host?.lowercase() }.getOrNull()
|
||||
|
||||
/** 取 `.ts` 的**文件名主体**(不含扩展名),供与点歌编号比对。 */
|
||||
private val TS_BASENAME = Regex("/([^/?]+)\\.ts(?:\\?|$)", RegexOption.IGNORE_CASE)
|
||||
|
||||
/**
|
||||
* 处理播放地址:广告地址返回 null(屏蔽)或 [replacementUrl](替换);
|
||||
* 非广告地址原样返回。
|
||||
|
||||
@@ -335,7 +335,7 @@ class KtvJsBridge(context: Context) {
|
||||
runCatching {
|
||||
file.isFile && file.length() in 1_001..500_000 &&
|
||||
file.readText(Charsets.UTF_8).let {
|
||||
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 8")
|
||||
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 9")
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
|
||||
@@ -361,7 +361,7 @@ class KtvJsBridge(context: Context) {
|
||||
val script = String(bytes, StandardCharsets.UTF_8)
|
||||
require(
|
||||
script.length > 1000 && script.contains("KtvApi") &&
|
||||
script.contains("KTV_BRIDGE_API: 8"),
|
||||
script.contains("KTV_BRIDGE_API: 9"),
|
||||
) { "Invalid or incompatible JS file" }
|
||||
tempFile.writeText(script, Charsets.UTF_8)
|
||||
if (!tempFile.renameTo(cacheFile)) {
|
||||
|
||||
@@ -529,6 +529,10 @@ class MainActivity : AppCompatActivity() {
|
||||
initializeDatabase()
|
||||
startRemoteServer()
|
||||
updateMobileQrOverlay()
|
||||
// 一次性清理历史遗留的可疑歌曲缓存:1.4.9 及以前 JS 取址的地址没做广告校验,
|
||||
// 广告视频会被当成歌曲落盘,且能通过内容校验被 isDownloaded() 一直复用(广告反复播)。
|
||||
// 清理只是让歌曲重新下载,不影响用户数据;之后由「取址即校验」保证不再产生。
|
||||
io.execute { SongOkDownloadManager.purgeLegacySongCacheOnce(applicationContext) }
|
||||
main.postDelayed(remoteNetworkMonitor, 5000)
|
||||
// 网络状态监测: 断网提示 + 恢复后重建歌曲源会话/重试下载。
|
||||
KtvNetworkMonitor.start(applicationContext) { online, initial ->
|
||||
|
||||
@@ -112,8 +112,11 @@ object SongApiClient {
|
||||
if (db.open()) {
|
||||
val url = db.getCloudUrl(musicno)
|
||||
db.close()
|
||||
// DB 中的 cloud_url 可能也是 demo 地址, 做 URL 基本校验
|
||||
if (url != null && !isLikelyDemoUrl(url)) return url
|
||||
// DB 中的 cloud_url 也可能就是广告/demo 地址,必须过同一道可信校验
|
||||
// (2026-09-26 广告事故:这里原来只做 isLikelyDemoUrl,挡不住广告域名)。
|
||||
if (url != null && !isLikelyDemoUrl(url) && AdVideoFilter.isTrustedSongUrl(url, musicno)) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
return null
|
||||
|
||||
@@ -23,6 +23,9 @@ object SongOkDownloadManager {
|
||||
private const val MAX_RETRY_COUNT = 3
|
||||
private const val STALL_TIMEOUT_MS = 25_000L
|
||||
|
||||
/** 曲库视频目录下会被当作"已下载歌曲"的文件后缀(用于一次性清理历史广告缓存)。 */
|
||||
private val MEDIA_EXTENSIONS = setOf("ts", "ls", "mp4", "mkv")
|
||||
|
||||
interface DownloadCallback {
|
||||
fun onDownloadStart(song: Song)
|
||||
fun onDownloadProgress(song: Song, progress: Int)
|
||||
@@ -73,6 +76,41 @@ object SongOkDownloadManager {
|
||||
return downloaded
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性清理历史遗留的歌曲缓存(广告漏放期间的产物)。
|
||||
*
|
||||
* 背景(2026-09-26 事故):1.4.9 及以前,JS 取址返回的地址在 `buildDownloadUrl` 的成功分支里
|
||||
* **完全没有过广告校验**,广告视频(如 `gz.ac16.vip`)会被当成歌曲下载并落盘;它又能通过
|
||||
* [SongFileValidator] 的内容校验(≥6MB / ≥60s / TS 包完整),因此**靠内容无法识别**,
|
||||
* 而且 [isDownloaded] 会直接复用该文件、连取址都不会再发生 —— 广告会一直播下去。
|
||||
*
|
||||
* 这类遗留文件只能在升级时清一次;此后由「取址即校验」(
|
||||
* [AdVideoFilter.isTrustedSongUrl])保证不再产生。清掉只是让歌曲重新下载,不影响任何用户数据。
|
||||
*
|
||||
* @return 删除的文件数(已清理过则返回 0)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun purgeLegacySongCacheOnce(context: android.content.Context): Int {
|
||||
val marker = File(context.filesDir, "song_cache_purged_v1")
|
||||
if (marker.exists()) return 0
|
||||
var removed = 0
|
||||
runCatching {
|
||||
val dir = File("${MuseDatabase.VIDEO_ROOT}/${MuseDatabase.CLOUD_SONG_DIR}")
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile && file.extension.lowercase() in MEDIA_EXTENSIONS) {
|
||||
SongFileValidator.forget(file)
|
||||
if (file.delete()) removed++
|
||||
}
|
||||
}
|
||||
marker.writeText("purged=${System.currentTimeMillis()} removed=$removed", Charsets.UTF_8)
|
||||
}.onFailure { Log.w(TAG, "purge legacy song cache failed: ${it.message}") }
|
||||
// 首次启动才会真正有动作,之后 marker 存在会直接返回 0,不产生噪声日志。
|
||||
if (removed > 0 || !marker.exists()) {
|
||||
MiliLog.warn("SONG-AD 一次性清理历史歌曲缓存 removed=$removed(旧版本可能把广告缓存成歌曲)")
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isDownloading(song: Song): Boolean {
|
||||
val key = songKey(song)
|
||||
@@ -173,33 +211,66 @@ object SongOkDownloadManager {
|
||||
}
|
||||
|
||||
private fun buildDownloadUrl(song: Song, forceRefresh: Boolean = false): String {
|
||||
val musicNo = song.filename?.removeSuffix(".ts")?.removeSuffix(".ls") ?: song.id
|
||||
if (!forceRefresh) {
|
||||
song.downloadUrl?.takeIf(String::isNotBlank)?.let { cached ->
|
||||
if (!AdVideoFilter.isAdVideo(cached)) return cached
|
||||
// 缓存的下载地址是广告视频,清掉并重新解析(广告会被桥接层屏蔽/替换)。
|
||||
Log.w(TAG, "丢弃广告缓存地址: ${song.title}")
|
||||
if (AdVideoFilter.isTrustedSongUrl(cached, musicNo)) return cached
|
||||
// 缓存的地址不可信(广告 / 文件名与点歌编号不符):除了清地址,还要**删掉已落盘的
|
||||
// 同名文件** —— 广告视频能通过内容校验(≥6MB / ≥60s / TS 包完整),
|
||||
// 一旦被缓存成歌曲,isDownloaded() 会直接复用、连取址都不会再发生。
|
||||
Log.w(TAG, "丢弃不可信缓存地址: ${song.title} -> $cached")
|
||||
MiliLog.warn("SONG-AD 丢弃不可信缓存地址 编号=$musicNo url=$cached")
|
||||
song.downloadUrl = null
|
||||
purgeCachedSongFile(song, "untrusted-cached-url")
|
||||
}
|
||||
}
|
||||
val musicNo = song.filename?.removeSuffix(".ts")?.removeSuffix(".ls") ?: song.id
|
||||
return SongApiClient.getSongDownloadUrl(musicNo).orEmpty().also { url ->
|
||||
// 请求源 = 本次点歌实际命中的源(JS 上报); 取不到时留空。
|
||||
val srcHost = SongApiClient.lastSongSourceHost()
|
||||
if (url.isNotEmpty()) {
|
||||
// ⚠️ 广告漏放的**最终兜底防线**:不管地址来自 JS 取址、原生兜底还是 DB 回退,
|
||||
// 都必须过 AdVideoFilter.isTrustedSongUrl(2026-09-26 事故:JS 路径返回
|
||||
// gz.ac16.vip 的广告视频,因为这里的成功分支完全没做广告校验而被当成歌曲下载播放)。
|
||||
if (url.isNotEmpty() && AdVideoFilter.isTrustedSongUrl(url, musicNo)) {
|
||||
song.downloadUrl = url
|
||||
val cdnHost = runCatching { java.net.URI(url).host }.getOrNull() ?: "unknown"
|
||||
MiliLog.info(
|
||||
"点歌编号=$musicNo 标题=${song.title} 请求源=$srcHost 状态=成功 urlHost=$cdnHost",
|
||||
)
|
||||
} else if (url.isNotEmpty()) {
|
||||
Log.w(TAG, "Untrusted URL rejected for ${song.title}: $url")
|
||||
MiliLog.error(
|
||||
"点歌编号=$musicNo 标题=${song.title} 请求源=$srcHost 状态=失败 " +
|
||||
"详情=地址不可信(广告或文件名不符) url=$url",
|
||||
)
|
||||
purgeCachedSongFile(song, "untrusted-resolved-url")
|
||||
""
|
||||
} else {
|
||||
Log.w(TAG, "Unable to get download URL: ${song.title}")
|
||||
MiliLog.error(
|
||||
"点歌编号=$musicNo 标题=${song.title} 请求源=$srcHost 状态=失败 详情=无法生成下载地址",
|
||||
)
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 删除某首歌已落盘的文件(发现地址不可信/命中广告时调用,让下次点歌重新取址下载)。 */
|
||||
private fun purgeCachedSongFile(song: Song, reason: String) {
|
||||
runCatching {
|
||||
val file = getLocalFile(song)
|
||||
if (file.exists()) {
|
||||
val sizeMb = file.length() / 1024 / 1024
|
||||
SongFileValidator.forget(file)
|
||||
val deleted = file.delete()
|
||||
MiliLog.warn(
|
||||
"SONG-AD 清除可疑歌曲缓存 编号=${song.id} 原因=$reason " +
|
||||
"大小=${sizeMb}MB 已删除=$deleted",
|
||||
)
|
||||
}
|
||||
song.path = null
|
||||
}.onFailure { Log.w(TAG, "purge cached file failed: ${it.message}") }
|
||||
}
|
||||
|
||||
private fun startDownloadTask(song: Song, url: String, callback: DownloadCallback?, attempt: Int) {
|
||||
val key = songKey(song)
|
||||
val target = getLocalFile(song)
|
||||
|
||||
@@ -202,15 +202,16 @@ object SongUrlResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/** 镜像 ktv_api.js 的 `isDemoUrl`:广告视频 / 纯 IP / R2 测试桶 / ts 编号与请求不一致。 */
|
||||
/**
|
||||
* 原生路径的可信校验:先过 `AdVideoFilter.isTrustedSongUrl`(广告域名/路径 + `.ts` 文件名
|
||||
* 必须等于点歌编号),再补上 JS `isDemoUrl` 里那几条(wb66/demo、纯 IP、R2 测试桶)。
|
||||
*/
|
||||
private fun isValidSongUrl(url: String, musicno: String): Boolean {
|
||||
if (AdVideoFilter.isAdVideo(url)) return false
|
||||
if (!AdVideoFilter.isTrustedSongUrl(url, musicno)) return false
|
||||
val lower = url.lowercase()
|
||||
if (lower.contains("wb66") || lower.contains("demo")) return false
|
||||
if (IP_URL.containsMatchIn(url)) return false
|
||||
if (R2_DEMO.containsMatchIn(url)) return false
|
||||
val matched = TS_NAME.find(url)?.groupValues?.getOrNull(1)
|
||||
if (matched != null && matched != musicno) return false
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.local.ktv;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* 广告地址拦截的回归测试。
|
||||
*
|
||||
* 对应 2026-09-26 现场事故:新设备(WebView 正常 → 走 JS 取址)点歌 7303235(情歌 HD)
|
||||
* 拿到 `gz.ac16.vip` 的广告视频,被当成歌曲下载并播放。当时的漏洞有三处:
|
||||
* 1. JS 的 isDemoUrl 不认识该广告域名,且只比对"纯数字文件名",非数字名字直接放行;
|
||||
* 2. Kotlin 的 buildDownloadUrl 成功分支**完全没做广告校验**;
|
||||
* 3. DB 回退路径只做 isLikelyDemoUrl,也挡不住广告域名。
|
||||
* 这里用日志里的**真实 URL 形态**锁住入参校验行为。
|
||||
*/
|
||||
public final class AdVideoFilterTest {
|
||||
|
||||
/** 真源地址:两个 CDN 域实测文件名都严格等于点歌编号,必须放行。 */
|
||||
@Test
|
||||
public void acceptsRealSourceUrls() {
|
||||
assertTrue(AdVideoFilter.isTrustedSongUrl(
|
||||
"http://txog.ktvsky.com/264/lvbr/480/E/ts/47.1/202209220333/480p/7303235.ts?token=526e505e",
|
||||
"7303235"));
|
||||
assertTrue(AdVideoFilter.isTrustedSongUrl(
|
||||
"http://download.origjoy.com/264/lvbr/480/E/ts/39.6/202309070206/480p/7445418.ts?token=abc",
|
||||
"7445418"));
|
||||
}
|
||||
|
||||
/** 广告域名:即使文件名伪装成正确的点歌编号,也必须拦下。 */
|
||||
@Test
|
||||
public void rejectsAdHostEvenWithMatchingName() {
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://gz.ac16.vip/ad/7303235.ts", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://cdn.ac16.vip/x/7303235.ts", "7303235"));
|
||||
}
|
||||
|
||||
/** 广告文件名的经典形态:非数字名字,旧规则的正则匹配不到。 */
|
||||
@Test
|
||||
public void rejectsAdFileName() {
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://example.com/ad_files/my_ad_video.ts", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://example.com/x/my_ad_video.ts", "7303235"));
|
||||
}
|
||||
|
||||
/** .ts 文件名与点歌编号不符(即使不命中任何广告域名)也视为不可信。 */
|
||||
@Test
|
||||
public void rejectsMismatchedFileName() {
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://example.com/x/9999999.ts", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("http://example.com/x/promo.ts", "7303235"));
|
||||
}
|
||||
|
||||
/** 非 http(s) 一律不可信。 */
|
||||
@Test
|
||||
public void rejectsNonHttpSchemes() {
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("file:///sdcard/x/7303235.ts", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("ftp://example.com/7303235.ts", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl("", "7303235"));
|
||||
assertFalse(AdVideoFilter.isTrustedSongUrl(null, "7303235"));
|
||||
}
|
||||
|
||||
/** 没有 .ts 文件名时不做文件名比对(避免误杀非 ts 的真源)。 */
|
||||
@Test
|
||||
public void allowsNonTsUrlWithoutAdMarkers() {
|
||||
assertTrue(AdVideoFilter.isTrustedSongUrl("http://cdn.example.com/path/stream?token=abc", "7303235"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user