.NET - 在CEF中实现请求拦截并修改响应内容

最近在实现NanUI拦截请求和修改响应的功能,尽管作为一个WinForm的界面框架,这么多年来我都觉得拦截请求修改响应这个需求不应该被一个纯粹的界面框架所实现,无奈这个功能在使用者的呼声里很高,所以最终还是决定实现这个功能。

经常进行CEF开发的朋友都知道,要实现请求拦截修改响应这类功能的话需要在CEF的CefRequestHandler接口中进行。 这个接口中有一个GetResourceRequestHandler方法,需要返回抽象类CefResourceRequestHandler的实例,这个类主要用于CEF的请求拦截、Web资源筛选等等操作,而其中最适合用于拦截请求并修改响应的是GetResourceResponseFilter方法,在此方法中提供了CefRequestCefResponse两个参数为我们拦截请求修改响应提供了必要条件。通过使用CefRequest参数中提供的各个属性,我们能够精准地实现对某个特殊资源的拦截,在这个方法中虽然提供了CefResponse参数,但是按照文档的说法,是不能对此进行直接修改的,需要在方法中返回抽象类CefResponseFilter的派生类并实现其中的Filter方法来修改响应内容。

以下示例,我以拦截https://www.qq.com/的首页html内容为例, 拦截后实现在页面标记前注入一段弹窗代码。我将使用CEF的.NET实现CefGlue来进行编码和演示,其他语言或者框架原理上是相通的。同时为了节省读者的时间,以下仅展示CefResourceRequestHandlerGetResourceResponseFilter
方法及返回的CefResponseFilter这部分对网络资源进行拦截及替换的核心代码。

首先实现CefResourceRequestHandler并在GetResourceResponseFilter方法中使用参数里提供的各项属性和方法实现对目标URL的页面拦截。

class CustomResourceRequestHandler : CefResourceRequestHandler
{
    protected override CefCookieAccessFilter GetCookieAccessFilter(CefBrowser browser, CefFrame frame, CefRequest request)
    {
        // 根据实际情况返回一个自定义的 Cookie 访问过滤器
        return null!;
    }

    protected override CefResponseFilter? GetResourceResponseFilter(CefBrowser browser, CefFrame frame, CefRequest request, CefResponse response)
    {
        // 这里可以根据请求的 URL 和响应的内容类型来决定是否使用自定义的响应过滤器
        // 示例:如果请求的 URL 是 https://www.qq.com,并且响应的内容类型是 text/html,则使用自定义的响应过滤器
        if (request.Url.StartsWith("https://www.qq.com") && response.GetHeaderMap()["content-type"] is var contentType && contentType is not null && contentType.Contains("text/html", StringComparison.InvariantCultureIgnoreCase))
        {
            return new CustomResponseFilter();
        }
        return base.GetResourceResponseFilter(browser, frame, request, response);
    }
}

然后就需要实现自定义的CefResponseFilter,其中的抽象方法Filter为我们提供了获取到内容并对其进行修改的途径。由于站点可能启用了分块传输(Transfer-Encoding:chunked),因此Filter方法可能被执行多次,所以需要先将所有内容保存到一个缓存当中,在确保拦截的请求内容完全获取后再执行替换操作,并再次将修改后的响应内容返回到流中给到浏览器。

class CustomResponseFilter : CefResponseFilter
{
    protected override bool InitFilter()
    {
        return true;
    }


    bool _isContentHandled = false;
    List<byte> _buffer = new();
    protected override CefResponseFilterStatus Filter(UnmanagedMemoryStream dataIn, long dataInSize, out long dataInRead, UnmanagedMemoryStream dataOut, long dataOutSize, out long dataOutWritten)
    {
        // 由于站点可能启用了分块传输(Transfer-Encoding:chunked),因此需要确保内容被全部读取。
        // 确认的方法就是检查 dataIn 是否为null 或者 dataInSize 是否为 0。
        if (dataIn is null || dataInSize == 0)
        {
            // 此处是为了确保内容只在第一次读取时被处理,后面如上述所说的分块传输会导致多次调用此方法。
            if (_isContentHandled)
            {
                var responseHtml = Encoding.UTF8.GetString(_buffer.ToArray());

                // 这里可以对 responseHtml 进行处理,比如替换某些内容
                responseHtml = responseHtml.Replace("</body>", "<script>alert('We scripted the QQ.com!');</script></body>");

                // 将处理后的内容转换为字节数组
                var responseBytes = Encoding.UTF8.GetBytes(responseHtml);

                // 清空 _buffer 并将处理后的内容添加到 _buffer 中,后面输出给浏览器的响应就是被我们替换过的内容了。
                _buffer.Clear();
                _buffer.AddRange(responseBytes);

                // 标记内容已经被处理过了,避免重复处理出现 bug
                _isContentHandled = true;
            }

            // 这里不会再有来自 dataIn 的数据了,所以 dataInRead 为 0。
            dataInRead = 0;

            // 根据 _buffer 的大小来判断是否需要继续读取数据。
            // dataOutSize 是 CEF 要求的输出缓冲区的大小,因此每次返回的数据不能超过这个大小。
            // 如果 _buffer 的大小大于 dataOutSize,则需要将数据分批写入 dataOut。
            if (_buffer.Count > dataOutSize)
            {
                dataOutWritten = dataOutSize;
                
                var writtenBuff = _buffer.GetRange(0,(int)dataOutWritten).ToArray();

                dataOut.Write(writtenBuff, 0, writtenBuff.Length);

                _buffer.RemoveRange(0, writtenBuff.Length);

                return CefResponseFilterStatus.NeedMoreData;
            }
            else
            {

                dataOutWritten = _buffer.Count;
                dataOut.Write(_buffer.ToArray(), 0, _buffer.Count);
                return CefResponseFilterStatus.Done;
            }
        }


        // 读取数据的大小,这里按照实际读取的大小来处理。
        dataInRead = dataInSize;
        // 这里让 dataOutWritten 为 0,表示没有写入数据。数据的将在响应修改后进行,这里只对数据流进行缓存。
        dataOutWritten = 0;

        // 读取数据到缓冲区,并将其添加到 _buffer 中。
        Span<byte> buff = stackalloc byte[(int)dataInSize];
        var retval = dataIn.Read(buff);

        _buffer.AddRange(buff.ToArray());

        // 持续返回 NeedMoreData,直到数据全部读取完毕后将出发方法前段 dataIn 和 dataInSize 为 0 的情况。
        return CefResponseFilterStatus.NeedMoreData;

    }
}

至此,通过GetResourceResponseFilter方法中的拦截及自定义的CefResponseFilter类里对内容的修改我们就完成了本文开头所述的拦截和替换的需求。对代码进行编译并运行后,可以看到被访问的html页面被成功拦截,我们自定义的内容也成功注入到了响应当中。

posted @ 2025-05-27 16:34  林选臣  阅读(146)  评论(0)    收藏  举报