尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

猫抓Cat-Catch:一个媒体请求在浏览器安全模型中的四次转手

猫抓Cat-Catch:一个媒体请求在浏览器安全模型中的四次转手 猫抓Cat-Catch一个媒体请求在浏览器安全模型中的四次转手【免费下载链接】cat-catch猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch当用户点下播放键一个媒体请求从浏览器内核出发到最终以文件形式落盘——这中间发生了什么猫抓Cat-Catch浏览器资源嗅探扩展当前版本 2.7.2给出了一个工程化的答案它不是简单截住请求而是把一次请求拆成四个接力站每一站都对应浏览器安全模型的一道闸门。这篇文章以一次媒体请求的生命周期为叙事主线跟着一个字节走完捕获、暂存、解析、传输四个阶段每到一站会还原猫抓在面对具体约束时的取舍以及最终落地的实现。对写过多脚本、却没深入过浏览器扩展内核的开发者这是一份关于在别人画的边界里做工程的样本。第一站捕获——用两个监听器拼出一张资源身份证嗅探的第一性问题不是能不能看到请求而是看到请求后怎么判断它是不是媒体。猫抓在 js/background.js 里同时挂上了两个 webRequest 监听器分工刻意错开// onSendHeaders 阶段做正则预判拿到完整 URL 和请求头 chrome.webRequest.onSendHeaders.addListener(function (data) { G.requestHeaders.set(data.requestId, data.requestHeaders); try { findMedia(data, true); } catch (e) { console.log(e); } }, { urls: [all_urls] }, [requestHeaders, ...].filter(Boolean)); // onResponseStarted 阶段用响应头确认类型此时第一个字节已到 chrome.webRequest.onResponseStarted.addListener(function (data) { data.allRequestHeaders G.requestHeaders.get(data.requestId); G.requestHeaders.delete(data.requestId); findMedia(data); }, { urls: [all_urls] }, [responseHeaders]);为什么是两次而不是一次只监听onBeforeRequest能拿到 URL但拿不到响应头无法通过 Content-Type 判断这是音频、视频还是普通 JSON只监听onResponseStarted又太晚丢失了请求头信息referer、Cookie而这些正是后续下载重放请求时必需的。于是requestId成了两张监听器的信物前者把请求头存进 Map后者取出后立即删除保证不泄漏内存。这里还能看到一个细节——正则匹配在onSendHeaders阶段就执行命中即命中这比等响应回来再判断省掉了一轮完整的网络往返。第二站暂存——Service Worker 每五分钟死一次数据怎么活下来Chrome 的 Manifest V3 规定 service worker 空闲约 30 秒即被终止强制 5 分钟重启Chromium issue 1271154 有据可查。对一次持续数分钟甚至数小时的媒体捕获来说进程会死是常态而非异常。猫抓的处理分两层。第一层是续命。background.js 里挂着两枚定时器setInterval(chrome.runtime.getPlatformInfo, 25 * 1000)让 worker 保持活跃同时监听名为HeartBeat的 Port 连接250 秒后主动断开再等重连——用心跳把生命周期从 5 分钟拉长到可用区间。chrome.runtime.onConnect.addListener(function (Port) { if (chrome.runtime.lastError || Port.name ! HeartBeat) return; Port.postMessage(HeartBeat); const interval setInterval(function () { clearInterval(interval); Port.disconnect(); }, 250000); });第二层是抗死。捕获到一半进程被杀了怎么办chrome.alarms定时把内存里的cacheData落盘用的是这样一行兼容写法(chrome.storage.session ?? chrome.storage.local).set({ MediaData: cacheData });这里的??是精心设计过的降级链storage.session快但重启即失storage.local慢但持久。这里有个值得展开的权衡项目并没有在两者之间二选一而是让 session 优先、local 兜底——代价是重启扩展后历史捕获数据确实会丢换来的是高频写入不再触发 local 存储的 IO 错误与配额风险。被放弃的纯 local 持久化方案维护成本在于每次写入都要面对配额异常而对一个下载工具来说数据写入频率远高于配置写入稳定压倒一切。这也是为什么捕获列表的数据宁可存在会话级内存里也不追求永久保留。第三站解析——把一个页面开成四个 tab 的数据搬运工M3U8 解析是猫抓最重的模块它的架构方式非常特殊不写一个解析器而是直接复用 hls.js 的解析能力。打开 js/m3u8.js 会看到它是从零初始化一个 Hls 实例const hls new Hls({ enableWorker: false, debug: false }); const _fragments []; const keyContent new Map(); // 储存 AES-128 密钥内容 const decryptor new AESDecryptor(); // 从 hls.js 拆出的解密工具这背后的判断是M3U8 的嵌套变体、EXT-X-KEY 的 AES-128 加密、DASH/HEVC 兼容这些坑hls.js 已经趟过无数遍与其重写轮子不如寄生在成熟解析器上把精力留给浏览器扩展特有的问题。猫抓特有的问题是什么是跨页面数据搬运。MV3 下每个页面m3u8.html、downloader.html、popup.html都是独立的执行环境解析需要原始页面的 DOM、referer、密钥于是全部通过 URL 参数传递const tabId parseInt(params.get(tabid)); // 资源所在的标签页用来取密钥 const key params.get(key); // 自定义密钥 const _requestHeaders params.get(requestHeaders);解析器拿到 tabId 后向后台发getData消息取回该标签页捕获的完整媒体信息甚至还能向原页面发送getPage消息、把整页 DOM 抓回来做文件名提取。这种URL 参数 消息路由的进程间通信方案比共享 storage 实时同步简单得多代价是 URL 会很长、参数会冗余——但它是 MV3 页面隔离下最直接可靠的通道。第四站传输——六线程下载器背后的顺序纪律分片下载是性能敏感区猫抓在 js/m3u8.downloader.js 里用Downloader类实现了一个带事件系统、可插拔处理管线、可中断的并发下载器默认线程数为 6class Downloader { MAX_RETRIES 3; constructor(fragments [], thread 6) { ... } use(fn, name ) { // 注册处理步骤解密、去头、转码… this.pipeline.push({ fn, name }); return this; } sequentialPush() { // 按顺序推送 buffer保证落盘不乱序 for (; this.pushIndex this.fragments.length; this.pushIndex) { if (this.buffer[this.pushIndex]) { this.emit(sequentialPush, this.buffer[this.pushIndex]); delete this.buffer[this.pushIndex]; continue; } break; } } }并发下载最大的工程陷阱是乱序分片到达顺序与播放顺序无关直接写盘会得到一段错乱的文件。sequentialPush就是解法——每个分片按索引存入 buffer 槽位只有下一个该写的槽位有数据时才推向下游并释放内存。线程数 6 也不是拍脑袋太少喂不满带宽太多会触发 CDN 的 403 限流、把源站压垮。下载数据还要流式落盘避免几百 MB 的切片堆在内存里为此接入了 StreamSaver 的streamSaver.mitm G.streamSaverConfig.url把可写流桥接到真实文件系统。这条传输管线的整体结构如下第五站页面内插桩——捕获不到的流量就主动喂给它webRequest 能看见全部网络请求但有些资源永远不经过它加密的 MediaSource 流、被 sandbox iframe 隔离的播放器、需要重放录制而非下载的直播流。猫抓的第三层能力是往页面里注入插桩脚本 catch-script/catch.js主动改写页面行为。三个动作尤其体现功力。一是拆 sandbox。sandbox属性会让 iframe 内脚本无法被扩展访问猫抓的做法是克隆 iframe 节点、去掉 sandbox 属性再替换回去并用 MutationObserver 持续监视新插入的 iframe——这是对页面自己造隔离的反制。二是代理 MediaSource。改写MediaSource.prototype.addSourceBuffer把媒体数据的 append 过程透明记录一份这样即使是完全走 MSE 加密管道的视频也能被偷录成完整文件。三是Shadow DOM closed 录制。直播场景下catch-script/recorder.js 用attachShadow({ mode: closed })创建封闭 UI页面脚本无法探测然后用 MediaRecorder 以可选码率/帧率抓取video输出。注意这里的取舍closed模式意味着扩展自己后续也难以操作该 DOM但换来了页面无法检测到录制面板的存在——在可被反制与完全隐蔽之间作者选择了后者。同样值得注意的还有 catch-script/catch.js 里的 Trusted Types 兼容层。不少站点启用了 CSP 的require-trusted-types-for策略扩展直接写innerHTML会被拦截。猫抓的做法是先探测、失败后再尝试注册 trustedTypes 策略并把innerHTML的 setter 包一层策略转换——先按最坏情况处理再逐级降级这个模式在扩展里很典型因为扩展不知道目标站点会启用什么安全策略。被放弃方案的完整清单与代价沿着生命周期走完把途中放弃的选择集中盘点能看清作者的取舍逻辑技术点采用方案放弃方案放弃的真实代价选择理由数据持久化session 优先 local 兜底纯 local 持久化扩展重启后历史捕获列表丢失高频写入避开 local 的 IO 与配额风险请求头修改declarativeNetRequest 动态规则webRequest blocking 同步改头规则需先删除再添加有竞态窗口MV3 已移除修改 requestHeaders 的同步能力M3U8 解析内嵌 hls.js 自研解密完全自研解析器依赖第三方库的体积与行为解析坑已被社区趟平聚焦扩展特有逻辑录制 UIShadow DOM closed普通 DOM 注入扩展自身访问受限页面无法检测录制状态其中请求头修改是跨浏览器差异最刺眼的一处代码里处处是G.isFirefox分支、chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS后面跟一个.filter(Boolean)——因为 Chrome 已把改头能力移交 declarativeNetRequest而 Firefox 仍保留旧通道。一个项目要同时适配两种安全模型代码里就会长出这样的双轨制这是跨浏览器开发绕不开的税。可以迁移到任何扩展项目的四条经验把进程会死当作设计输入而非异常。凡是依赖内存状态的功能都要有一个状态可重建或可丢弃的明确边界。猫抓的 session 降级链、alarms 定时落盘、awaitG等待全局变量初始化都是在为SW 随时被杀这一前提兜底。监听器之间用 requestId 这类稳定标识做信物并保证用完即删。G.requestHeadersMap 在onErrorOccurred里同样有清理逻辑否则失败请求会累积成内存泄漏——任何跨回调传状态的代码都应配一个对称的清理钩子。复杂格式解析优先寄生成熟库把稀缺的开发精力留给扩展生态特有的问题跨页面通信、沙盒绕过、安全策略兼容。判断标准是这个问题的坑是格式本身的还是浏览器扩展独有的。并发下载必须同时解决吞吐与顺序两个问题。只调大线程数是典型的以带宽换正确性sequentialPush式的槽位归位 顺序释放才是让分片并行与文件顺序二者兼得的工程答案。浏览器扩展的开发边界每天都在变窄猫抓的价值不在于代码多精巧而在于它示范了一种态度每次安全模型收紧不是功能的终点而是下一层架构的起点。下一次当你的代码被告知这个 API 不允许了不妨先问一句这个限制背后是不是恰好藏着一个可以反过来利用的通道。【免费下载链接】cat-catch猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表