ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CEF中拦截WSS握手请求的完整实现:C++桌面端WebSocket流量治理

CEF中拦截WSS握手请求的完整实现:C++桌面端WebSocket流量治理 简介面向 .NET 开发者的 CEF 拦截 WSS 完整代码基于 CefSharp 与 WinForm 实现解决在桌面应用中捕获、监控或自定义处理任意网站 WebSocket 安全通信的需求。通过注册自定义 Scheme Handler接管 ws/wss 协议请求进而完成握手代理、数据帧过滤与日志记录等操作适合需要调试网络流量或做数据管控的中高级开发者参考。压缩包共 21 个文件以 13 个 C# 源码文件为核心覆盖 Chrome 请求处理器、响应过滤器、主窗体与动态资源等模块另含 2 个配置文件、2 个 resx 资源文件及解决方案文件整体仅 76KB结构紧凑可直接用 Visual Studio 打开 CEF.sln 查看运行。已有 200 人学习下载。配套代码提供了完整的 Scheme Handler 注册流程、WSS 连接建立与数据拦截逻辑并包含错误处理与断开连接示例方便二次改造嵌入自身项目对理解 CefSharp 扩展机制和 WebSocket 协议均有实际帮助。 做C桌面端开发的朋友应该没少被CEF折腾。最近有个需求要把内嵌网页里所有WSS连接收进监管范围熟悉CEF的人都知道普通的OnBeforeResourceLoad能拦http和https但对WebSocket经常不生效因为WSS不是普通资源请求它走的是HTTP Upgrade协议敲完101状态码之后连接就独立出去了。这篇文章我会把我在实际项目里用的完整拦截代码拉出来讲清楚包括怎么判断一个请求是WebSocket握手怎么在握手阶段直接阻断怎么配置白名单放行以及Windows和Linux上调试时踩过的几个坑。如果你正在做广告过滤、协议过滤、内网流量审计这类纯粹的前端治理功能可以直接拿这份代码改。1. 项目背景CEF和WSS拦截到底解决什么问题1.1 WSS连接其实是“HTTP Upgrade”请求很多人第一次接触WSS拦截时会下意识去CefResourceRequestHandler里找WebSocket相关的资源类型。结果发现在CefRequest::GetResourceType()的枚举里根本没有RT_WEB_SOCKET这种值。原因是Chromium内部把WebSocket握手当成了普通HTTP请求处理客户端先发一个带有Upgrade: websocket和Sec-WebSocket-Key的GET请求服务端返回101 Switching Protocols后TCP连接才升级成独立的WebSocket通道。这意味着只要能卡住握手阶段就能决定这条WSS连接是生是死。反过来一旦101响应已经返回你再用CEF的请求回调去拦就什么都抓不到了。CEF对WebSocket的拦截能力基本就集中在“握手请求到达服务端之前”这个时间窗口。1.2 适合哪些场景劝退哪些场景我这次做的是内部客户端流量治理内嵌一个业务系统里面会连一堆wss://地址有实时消息推送也有埋点统计和一些不友好的第三方心跳。我需要搞清楚哪些域名在建立连接、哪些连接是企业里不允许的必要时直接掐掉。这套拦截思路适合这么几类场景广告脚本发起的WSS长连接想统一禁止公司内网浏览器工具只允许白名单域名建立WebSocket自动化测试时需要模拟WebSocket断连、错误等异常场景协议诊断和接口巡检想记录所有WSS目标地址。但也要劝退一类需求如果你想看到WSS收发消息的明文内容只靠CEF的原生资源请求回调是不够的。因为WSS的payload是在TLS加密隧道里流动的握手之后CEF默认并不会把每一帧解密数据暴露给C层。想做到消息级控制必须另想办法我后面第6节会讲一个能用但不够优雅的JS注入方案。1.3 方案选型为什么选择RequestHandler而不是浏览器插件CEF本身支持加载扩展但扩展的权限模型和浏览器进程隔离会让“拦截一切网站WSS”这件事变得很别扭。你要为每个WebContext装插件、维护插件生命周期还得考虑页面是否禁用扩展。相比之下CefRequestHandler::GetResourceRequestHandler是CEF官方推荐的资源拦截入口。它在浏览器进程中触发能够统一处理所有frame、所有子资源的HTTP请求也包括WebSocket握手请求。配合一个全局单例的CefResourceRequestHandler能做到“应用内加载的所有网站一视同仁被扫描一遍”。我最终的方案就是用CefResourceRequestHandler做握手阶段控制用JS注入自研消息记录做连接建立后的消息阶段治理。核心是前者代码量小、稳定、好排查。2. 准备工程CEF最小Demo跑起来2.1 依赖和编译版本以下代码基于CEF 3.x稳定版具体我用的是CEF 87对应Chromium 87内核Windows 10 Visual Studio 2019Linux Ubuntu 20.04 gcc 9。CEF的API在这几个版本之间变化不大主要回调函数名基本一致。如果你用的是更新版本记得留意CefResourceRequestHandler的签名变化。工程里需要链接CEF的libcef.lib和libcef_dll_wrapper具体编译方式CEF官方CMake模板写得已经足够清楚这里不赘述。真正影响后面代码的只有三个头文件#include include/cef_client.h #include include/cef_request_handler.h #include include/cef_parser.h2.2 拦截回调的统一入口CEF的设计是每个CefBrowser实例对应一个CefClientCEF框架在收到资源请求时会先调用CefClient::GetRequestHandler()拿到CefRequestHandler再通过CefRequestHandler::GetResourceRequestHandler()询问当前请求交给哪个CefResourceRequestHandler处理。所以要让“所有网站的WSS”都被拦关键就是让所有CefBrowser实例共用同一个CefClient。最常见的错误是一个页面new一个CefClient每个Client里各自维护自己的RequestHandler那你只能拦到那个Client对应的页面WSS换个标签页就失效了。我的做法是定义一个全局的BrowserClient在CefBrowserHost::CreateBrowser()里给所有Browser传同一个实例CefWindowInfo window_info; CefBrowserSettings settings; CefString url https://your-target-site.com; // client 是全局单例被所有标签页复用 CefRefPtrCefBrowser browser CefBrowserHost::CreateBrowserSync( window_info, client, url, settings, nullptr, nullptr);这样任何Browser中的任何页面最终都会走到同一个拦截器里。3. 完整代码拦截所有WSS握手请求3.1 核心回调实现先贴头文件这里只保留关键方法// wss_interceptor.h #pragma once #include include/cef_request_handler.h #include string #include unordered_set class WssInterceptor : public CefResourceRequestHandler { public: WssInterceptor(); // 拦截资源请求WebSocket握手也会经过这里 CefResourceRequestHandler::ReturnValue OnBeforeResourceLoad( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, CefRefPtrCefCallback callback) override; private: // 判断请求是不是WebSocket握手 bool IsWebSocketUpgrade(CefRefPtrCefRequest request); // 根据URL决定放行还是拦截 bool ShouldBlockWss(const std::string url); // 白名单域名集合 std::unordered_setstd::string allow_domains_; IMPLEMENT_REFCOUNTING(WssInterceptor); };然后是实现文件。这里最关键的是IsWebSocketUpgrade它负责从HTTP头部里识别WebSocket握手请求// wss_interceptor.cpp #include wss_interceptor.h #include include/cef_browser.h #include include/cef_request.h #include include/cef_parser.h #include iostream #include algorithm WssInterceptor::WssInterceptor() { // 演示默认放行本机测试域名其他WSS全部可拦可记 allow_domains_.insert(echo.websocket.org); } bool WssInterceptor::IsWebSocketUpgrade(CefRefPtrCefRequest request) { CefRequest::HeaderMap headers; request-GetHeaderMap(headers); bool has_upgrade false; bool has_key false; for (const auto pair : headers) { std::string name pair.first.LowerCase().ToString(); std::string value pair.second.LowerCase().ToString(); if (name upgrade value.find(websocket) ! std::string::npos) { has_upgrade true; } if (name sec-websocket-key) { has_key true; } } return has_upgrade has_key; } bool WssInterceptor::ShouldBlockWss(const std::string url) { // 解析URL拿到host CefURLParts parts; if (!CefParseURL(url, parts)) { return false; } std::string host CefString(parts.host).ToString(); std::transform(host.begin(), host.end(), host.begin(), ::tolower); // 命中白名单就放行 if (allow_domains_.count(host) 0) { return false; } // 你可以在外面写自己的策略 // 1. 全部拦截 // 2. 只拦截特定域名 // 3. 只记录不拦截返回RV_CONTINUE bool block_all true; return block_all; } CefResourceRequestHandler::ReturnValue WssInterceptor::OnBeforeResourceLoad( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, CefRefPtrCefCallback callback) { if (!IsWebSocketUpgrade(request)) { return RV_CONTINUE; } std::string url request-GetURL().ToString(); std::cout [WSS Interceptor] find WebSocket handshake: url std::endl; if (ShouldBlockWss(url)) { std::cout [WSS Interceptor] block wss: url std::endl; return RV_CANCEL; } return RV_CONTINUE; }这里的RV_CANCEL就是让CEF立刻终止这次握手请求页面里的new WebSocket()会收到error事件连接不会真的发出去。RV_CONTINUE则是放行。3.2 策略引擎放行、阻断还是记录ShouldBlockWss当前是最简单的全量拦截。实际项目里我不会写死block_all true而是改成读取配置文件提供三种动作模式行为适用场景block直接RV_CANCEL禁止的WSS连接allowRV_CONTINUE业务必需连接recordRV_CONTINUE但打日志流量审计和调试如果是record模式你不需要动ReturnValue只需把URL、host、时间、frame等数据写到日志或数据库中。这样才不会因为误伤核心业务。3.3 把拦截器挂到所有Browser上拦截器本身不挂在CefRequestHandler里而是挂在CefClient的子类中。我建了一个BrowserClient统一实现// browser_client.h #pragma once #include include/cef_client.h #include wss_interceptor.h class BrowserClient : public CefClient, public CefRequestHandler { public: BrowserClient(); // 所有Browser共用同一个RequestHandler CefRefPtrCefRequestHandler GetRequestHandler() override { return this; } // 所有请求包括WSS握手都交给同一个WssInterceptor CefRefPtrCefResourceRequestHandler GetResourceRequestHandler( CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefRequest request, bool is_navigation, bool is_download, const CefString request_initiator, bool disable_default_handling) override { return wss_interceptor_; } private: CefRefPtrWssInterceptor wss_interceptor_; IMPLEMENT_REFCOUNTING(BrowserClient); };// browser_client.cpp #include browser_client.h BrowserClient::BrowserClient() : wss_interceptor_(new WssInterceptor()) { }在创建浏览器时把所有标签页都绑定到同一个BrowserClient实例上就能做到“一切网站”的WSS拦截。如果你还需要处理弹出窗口、下载、插件权限等可以继续重写CefClient的其他回调不影响本方案。4. 验证和日志确认WSS拦截生效4.1 写一个测试页面我在本地放了一个最简单的测试页面打开一个wss://echo.websocket.org连接!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWSS Test/title /head body script try { const ws new WebSocket(wss://echo.websocket.org); ws.onopen function() { document.body.innerHTML p stylecolor:greenWSS opened/p; }; ws.onerror function() { document.body.innerHTML p stylecolor:redWSS blocked by interceptor/p; }; } catch (e) { document.body.innerHTML p stylecolor:redException: e.message /p; } /script /body /html如果拦截生效命令行和调试窗口会看到[WSS Interceptor] blocked页面里的onerror会触发不会出现onopen。4.2 日志输出与排查建议CEF默认不输出std::cout到标准输出需要你在CefSettings里启用日志CefSettings settings; settings.log_severity LOGSEVERITY_VERBOSE; settings.log_file cef_log.txt;设置之后std::cout是否进文件取决于你的工程配置。我更推荐使用CEF自带的LOG(INFO)宏在回调里加一条日志LOG(INFO) WSS request: url;这样你可以在cef_log.txt里看到所有请求记录排查起来非常方便。5. 常见的坑为什么我拦不到、拦下来后页面报错5.1 为什么拦不到WSS最典型的拦不到情况是OnBeforeResourceLoad里只判断了request-GetResourceType()发现不是RT_MAIN_FRAME、RT_XHR等常见类型就直接返回RV_CONTINUE导致WebSocket握手请求被漏掉了。正确的做法是用HTTP头判断也就是我上面写的IsWebSocketUpgrade。另一个常见问题是某个Browser的CefClient实例和别的Browser不共享导致一部分页面能拦、另一部分拦不到。你可以在拦截器里加一个static std::atomicint计数器每次进入回调加一如果切换页面后计数没有变化就说明这个页面的请求走到了另一个Handler里。还有可能是浏览器进程和渲染进程分离后有些内部请求有问题。但WSS握手发生在浏览器进程的资源请求管线上一般不会跨进程丢除非你开了一些自定义scheme或者请求被CefRequestHandler::OnBeforeBrowse提前拦截了。5.2 WSS拦下来了页面却无限重试这是我在实际项目里被坑得最惨的地方。有些业务页面会监听WebSocket的onclose和onerror然后自动重连。你只是在握手阶段返回RV_CANCEL页面收到的表现就是“连接失败”不少三方SDK会把这个当成临时网络故障每隔几秒重试一次日志里刷屏CPU和内存也跟着涨。解决思路是被拦截时不要只返回RV_CANCEL而是返回一个更明确的错误状态。不过CEF的RV_CANCEL没有细分错误码页面拿到的都是通用的error事件。更稳妥的做法是配合JS注入把重连SDK里的重试逻辑一并禁掉。但这样侵入性比较大需要根据具体SDK做适配。如果你面对的是自己公司的业务页面最好在业务层加一个“服务端协商开关”让页面知道当前客户端不允许这个WSS域名从而避免无意义的自动重连。5.3 多进程与强制关闭CEF进程CEF默认是“多进程架构”一个主进程多个renderer进程和GPU进程。调试WSS拦截时经常遇到重新编译后连不上调试器因为之前的renderer进程还占用着动态库。我早期经常用任务管理器一个个找CEF的垃圾进程后来发现直接命令行更干净# Windows替换成你的exe名 taskkill /F /IM your_app.exe但在代码里不能依赖外部强杀否则可能造成用户数据损坏。正确退出CEF的姿势是// 关闭所有Browser窗口后 CefShutdown();在CefShutdown之前确保所有CefBrowser实例都已经调用GetHost()-CloseBrowser(true)并释放引用。如果你在WSS拦截器里保存了CefBrowser指针也要在退出前清空否则会出现“循环引用导致CEF无法退出”的情况。5.4 拦截后如何兼容正常功能有些页面同时连接多个WSS有的业务消息走wss://chat.internal.com有的埋点走wss://tracker.com还有的走wss://third-party-sdk.com。如果你一口气把全部WSS都拦了业务会奇奇怪怪。建议策略引擎里增加一个“白名单前缀匹配”而不是全域名匹配例如bool WssInterceptor::ShouldBlockWss(const std::string url) { if (url.find(wss://chat.internal.com/) 0) { return false; // 业务必需 } if (url.find(wss://tracker.com/) 0) { return true; // 埋点不想让发 } return !allow_all_; // 默认按全局配置走 }这样你可以在维护业务稳定性和流量治理之间找到平衡而不是一刀切。6. 进阶从连接拦截到消息级管控6.1 JS注入的基本思路如果你的确需要看到、修改甚至阻止WSS消息内容CEF原生接口里没有直接暴露消息级Hook但可以通过提前拦截window.WebSocket来做。思路是在页面运行JavaScript前替换掉WebSocket构造函数(function() { if (window.__wssPatched) return; window.__wssPatched true; const OriginalWebSocket window.WebSocket; class WrappedWebSocket extends OriginalWebSocket { constructor(url, protocols) { console.log([WSS] opening , url); super(url, protocols); this.addEventListener(message, (event) { // 这里可以拿到消息但要看业务允不允许 console.log([WSS] message , event.data); }); this.addEventListener(error, (event) { console.log([WSS] error , url); }); } send(data) { // 这里可以拦截发送内容 console.log([WSS] send , data); return super.send(data); } } Object.defineProperty(window, WebSocket, { configurable: true, enumerable: false, writable: true, value: WrappedWebSocket }); })();这段JS必须在页面自身的WebSocket业务代码之前执行否则会被绕过。一个比较可靠的注入时机是CEF的DevTools协议Page.addScriptToEvaluateOnNewDocument它会在文档创建早期执行命中率远高于OnLoadEnd后执行ExecuteJavaScript。但要注意CEF的ExecuteJavaScript默认是在当前页面上下文执行如果页面设置了严格的CSP或者禁用了unsafe-eval有些注入会失败。这也是为什么很多CEF流量治理方案会选择在C层先拦截握手再配合JS层做消息治理而不是完全依赖JS。6.2 警惕踩红线别拿拦截做坏事WSS拦截是一把双刃剑。正常的广告过滤、内网访问控制、测试故障注入都是合理场景。但如果你把这个能力用于窃取用户聊天内容、篡改第三方服务的支付信息、或者绕过别人的风控系统那就超出了技术讨论的范畴也会给自己和公司带来法律风险。我的建议是拦截WSS的目标应该限定在你自有或者是经过授权的系统里。比如公司内部客户端、你们自己开发的自动化测试工具、或者用户明确授权安装的桌面应用。无论从技术还是合规角度都不要对个人通讯、银行、政务等敏感站点做消息级管控。最后说几句实操体会这套WSS拦截代码我前后迭代了三版第一版只想在C层拦握手结果发现业务页面自动重连刷爆了日志第二版加了白名单算是能用了第三版配合JS注入做消息级管控才算真正满足需求。我自己的体会是CEF的请求回调虽然强大但WebSocket的生命周期和普通HTTP请求不一样握手只是开始之后连接一直活着。所以做拦截功能时一定要把“连接建立前”和“连接建立后”分开设计不能只堵不疏。如果这篇文章能让你少走一两个弯路那我也没白摔那几跤。本文还有配套的精品资源点击获取
返回列表