65.9K
CodeProject 正在变化。 阅读更多。
Home

Webbrowser 控件的 HTTP 监视器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (16投票s)

2011年3月1日

CPOL

7分钟阅读

viewsIcon

272415

downloadIcon

24612

捕获单个 Webbrowser 控件请求的 ATL COM DLL

Demo

介绍 

有各种工具可以监视从不同进程发送和接收的 HTTP 流量。Fiddler 就是一个很好的例子。所有这些程序都会打开一个端口并根据进程 ID 过滤 HTTP 流量。但是,如果一个 C# 应用程序包含多个浏览器,它们就无法识别哪个请求是由哪个浏览器发送的。

C# 浏览器控件只提供导航中已导航事件,并且不提供关于它发送的请求(例如加载图像等)的任何信息。

本文提供了一个 ATL COM DLL,可以监视单个浏览器的 HTTP 流量。

背景

在我进行一个需要此功能的项目时,我偶然发现了 Igor TandetnikPassThruApp

csExWBDLMan.dll(来自 最完整的 C# Webbrowser 包装器控件 的“csExWB COM 库”)是 PassThru App 的一个实现,它提供了请求,但没有提供请求中重定向、接收到的数据、Cookie 等的详细信息。因此,我决定编写自定义代码,仅基于 PassThru App 和 csExWBDLMan.dll 来监视 HTTP 流量。

关于 PassThru App

“它是一个实现 URL moniker-to-APP 通信双方的对象,即它同时实现了 IInternetProtocol IInternetProtocolSink / IInternetBindInfo 。我们将它注册为标准协议(如 HTTP)的临时处理程序。现在,每当需要发送 HTTP 请求时,URL moniker 就会创建我们 pAPP 的一个实例并要求它完成工作。然后,pAPP 会为相关协议创建一个标准 APP 的实例(我称之为目标 APP,或 tAPP...)并充当它的客户端。此时,我们的 pAPP 就成了字面意义上的中间人。在最简单的情况下,URL Moniker 对 pAPP 的任何方法调用都会转发给 tAPP,而 tAPP 对 pAPP 的任何方法调用都会转发回 URL Moniker。pAPP 可以观察(并在需要时修改)往返于 moniker 和 tAPP 的与此请求相关的所有信息。QED” - Igor Tandetnik

代码

该代码扩展了 PassThru App 提供的类。主要有两个类:

  1. MonitorSink - MonitorSink 扩展了实现 IInternetProtocolSinkPassthroughAPP::CInternetProtocolSinkWithSP
  2. CTestAPP - CTestAPP 扩展了实现 IInternetProtocolPassthroughAPP::CInternetProtocol
class MonitorSink :
public PassthroughAPP::CInternetProtocolSinkWithSP<MonitorSink>,
    public IHttpNegotiate
{.. 
class CTestAPP :
	public PassthroughAPP::CInternetProtocol<TestStartPolicy>
{..

现在我们可以通过以下方式拦截请求:

  1. 请求 - MonitorSink::BeginningTransaction
  2. 响应 - MonitorSink::OnResponse
  3. 重定向、传输的 Cookie、错误、加载的缓存等 - MonitorSink::ReportProgress,当 IInternetProtocolSink->ReportProgress(ulStatusCode...) 中的 ulStatusCodeBINDSTATUS_REDIRECTING 等时,具体取决于所需的信息。
  4. 接收到的数据 - CTestAPP::Read

但问题是我们使用的是异步可插入协议,并且所有请求都是异步进行的。因此,我们获得所有信息,但无法确定哪个响应属于哪个请求。此外,数据是异步以块的形式接收的。

最好的解决方案是,如果我们能获得一个唯一的事务 ID(即,与请求、响应和接收到的数据相关联的唯一 ID),那么我们就能将异步调用重新编织在一起。在这里,我们很幸运。

  1. 请求的 IInternetBindInfo 大多数情况下是唯一的,并且在所有方法中都可用。但有时,浏览器会重用它。
  2. 如果请求是从 iframe 发送的,则 IHTMLWindow2 存在并且是唯一的。
  3. 请求的目标 Url 也很有可能唯一。

因此,如果我们从所有这些中创建一个 ID,我们将获得一个唯一的事务 ID。现在我们不局限于此。如果页面上有多个 iframe,那么我们可以遍历页面,根据引用者告诉我们哪个 iframe 发送了哪个请求。此外,每个 iframe 在导航时都会触发 BeforeNavigate NavigationComplete 事件。iframe 的对象可从 InternetExplorer 接口获得。如果您将所有可用信息关联起来,您就可以勾勒出一个完整的图景:

  • 页面上 iframe 的层次结构是什么
  • 哪个 iframe 导航到了哪些 URL
  • 某个特定 iframe 发送了哪些请求
  • 哪些 URL 加载失败
  • 哪些 URL 使用了本地计算机上的文件(及其实际文件位置)
  • 发送和接收了哪些标头、Cookie
  • 每个请求花了多长时间等等。

限制

  1. 如果您在原生 COM 控件中设置 Silent=true ,或者在 .NET WebBrowser 中设置 ScriptErrorsSuppressed=true ,则上述代码将停止工作。
  2. 基于 iframe 过滤请求的假设是,页面上的每个 iframe 都有不同的位置。
  3. 另一个假设是,来自特定 URL 的 flash 只会在一个浏览器控件(如果存在多个控件)和一个页面中加载。否则,从 flash 控件发送的请求就会混淆。原因是 flash 发送的请求的引用者是 flash 对象而不是页面 URL,我们无法确定是哪个 flash(来自哪个控件)实际发送了请求。

Using the Code

当您将浏览器连接到 HttpMonitor.dll 时,对于每个请求、响应等,都会触发一个带有所有必需参数的事件。有十二个加一个事件可用。

  • OnRequest(int id, int containerId, string url, 
    	string headers, string method, object postData)
  • OnRedirect(int id, int containerId, int redirectedId, string url, 
    	string redirectedUrl, string responseHeaders, string requestHeaders)
  • OnResponse(int id, int containerId, string url, int responseCode, string headers)
  • OnDataRecieved(int id, int containerId, string url, object data, bool isComplete)
  • OnCookieSent(int id, int containerId, string url, string cookies)
  • OnCookieRecieved(int id, int containerId, string url, string cookies)
  • OnMimeTypeAvailable(int id, int containerId, string url, string type)
  • OnCacheLoaded(int id, int containerId, string url, string location)
  • OnP3PHeaderRecieved(int id, int containerId, string url, string p3PHeader)
  • OnError(int id, int containerId, string url, int result, int errorCode)
  • OnProgress(int id, int containerId, string url, 
    	int grfBSCF, uint progress, uint progressMax)
  • GetIServiceProviderOnStart(int id, int containerId, string url, int ptr)

还有一个事件可用。

  • ConfirmRequest(int id, int containerId, string url, 
    	int totalInstances, ref bool itsMine)

如果请求是 flash 对象或者从 flash 对象发送的,那么 DLL 就无法确定是哪个浏览器发送的。它会触发上述事件,并要求您查看您的请求日志,并根据容器 ID 建议它是否属于您的浏览器。演示应用程序中提供了示例实现。如果您默认设置 itsMine=true ,您可以跟踪当前进程发出的所有请求。

一个常见的错误是认为 CHttpMon 一次只用于一个事务。如果 CHttpMon 包含私有变量,它将在不同的请求中共享,我们无法将其用于存储任何一个请求的数据。另外,flash 对象发出的请求的引用者是 flash 对象的位置而不是页面。因此,我们需要跟踪这些 flash 对象。无论如何,所有这些对象都像 Microsoft 最初设计的那样工作,而不是像我们希望的那样工作。其中大部分是未公开的,我们需要进行 A/B 测试来找出它们实际上是如何工作的。

IHTMLWindow2 可用于 iframe 发出的请求。这也可以进一步利用(例如,像新的 OnPageRequest 这样的事件)。我曾希望在所有请求中都能收到这个,并使用 IHtmlWindow2 和引用者来创建 containerId ,这样它将是完全唯一的,但看起来那是不可能的:(

您首先需要导航到 about:blank,以便“Internet Explorer_Server”窗口可用。然后,使用“Internet Explorer_Server 窗口”的句柄附加浏览器。

if (monitor == null)
{
     monitor = new HttpMonitorLib.HttpMonClass();
     monitor.IEWindow = GetTopWindow(GetTopWindow
		(GetTopWindow(webBrowser1.Handle))).ToInt32();
     monitor.OnRequest += new HttpMonitorLib._IHttpMonEvents_OnRequestEventHandler
			(monitor_OnRequest);
.
.

例如,当浏览器发送请求时,以下函数将被执行:

private void monitor_OnRequest(int id, int containerId, string url,
	string headers, string method, object postData)
{
//code here
}   

id 指定了与该特定 HTTP 事务相关联的唯一 ID,containerId 是发送请求的 iframe uniqueId。基本上,它是 iframe 当前位置的哈希值。

有什么新内容

  1. 在上一个版本中,并非所有请求标头都在 OnRequest 事件中报告,因为所有标头仅在 BINDSTATUS_SENDINGREQUEST 之后才可用。
  2. 添加了 OnProgress GetIServiceProviderOnStart 事件。
  3. 修改了演示应用程序实现,以便 iframe 的请求属于其页面实例而不是其父页面的实例。
  4. 对演示应用程序和 httpmonitor DLL 的代码进行了重构/优化。

演示应用

演示应用程序提供了 HttpMonitor 的实现,用于检测完整的页面导航。主类页面接受“Internet Explorer_Server”作为参数。它可以被视为 InternetExplorer 接口的包装器。该类具有以下属性:

  • Children - 子页面的列表
  • Entries - 此页面/iframe 发送的请求/响应列表
  • Navigations - 此页面或 iframe 执行的导航
  • AllIEWithNavigations - 所有 InternetExplorer 接口实例及其导航
  • Webrowser - 当前页面的 InternetExplorer 接口的实际实例
  • AllEntries - 当前控件发送的所有请求/响应
  • AllPages - 所有 Page 对象的实例

我将 containerId 设置为整数而不是字符串(因为 containerId 是引用 URL 的哈希值)的原因是,我认为我将能够以某种方式获取 IHTMLWindow2 指针,并将其作为 containerId 传递。到目前为止,我一直不成功。如果有人能弄清楚如何做到这一点,请告诉我。

有一些粗糙的方法可以实现这一点,例如,在 BeforeNavigate2 事件中,将 Cancel=true 并重新导航到
url + "IHTMLWindow2=<pointer to IHTMLWindow2>" 在查询字符串中
或者使用 "IHTMLWindow2:<pointer to IHTMLWindow2>" 在标头中导航
并解析请求以获取值。但首先,这会破坏常规导航,其次 flash 对象的请求仍然不会保留这些值。

我要感谢 Igor Tandetnik 提供的精彩 PassThruApp,它使这一切成为可能。

© . All rights reserved.