Webbrowser 控件的 HTTP 监视器






4.69/5 (16投票s)
捕获单个 Webbrowser 控件请求的 ATL COM DLL

介绍
有各种工具可以监视从不同进程发送和接收的 HTTP 流量。Fiddler 就是一个很好的例子。所有这些程序都会打开一个端口并根据进程 ID 过滤 HTTP 流量。但是,如果一个 C# 应用程序包含多个浏览器,它们就无法识别哪个请求是由哪个浏览器发送的。
C# 浏览器控件只提供导航中和已导航事件,并且不提供关于它发送的请求(例如加载图像等)的任何信息。
本文提供了一个 ATL COM DLL,可以监视单个浏览器的 HTTP 流量。
背景
在我进行一个需要此功能的项目时,我偶然发现了 Igor Tandetnik 的 PassThruApp。
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 提供的类。主要有两个类:
MonitorSink
-MonitorSink
扩展了实现IInternetProtocolSink
的PassthroughAPP::CInternetProtocolSinkWithSP
CTestAPP
-CTestAPP
扩展了实现IInternetProtocol
的PassthroughAPP::CInternetProtocol
class MonitorSink :
public PassthroughAPP::CInternetProtocolSinkWithSP<MonitorSink>,
public IHttpNegotiate
{..
class CTestAPP :
public PassthroughAPP::CInternetProtocol<TestStartPolicy>
{..
现在我们可以通过以下方式拦截请求:
- 请求 -
MonitorSink::BeginningTransaction
- 响应 -
MonitorSink::OnResponse
- 重定向、传输的 Cookie、错误、加载的缓存等 -
MonitorSink::ReportProgress
,当IInternetProtocolSink
->ReportProgress
(ulStatusCode
...) 中的ulStatusCode
是BINDSTATUS_REDIRECTING
等时,具体取决于所需的信息。 - 接收到的数据 -
CTestAPP::Read
但问题是我们使用的是异步可插入协议,并且所有请求都是异步进行的。因此,我们获得所有信息,但无法确定哪个响应属于哪个请求。此外,数据是异步以块的形式接收的。
最好的解决方案是,如果我们能获得一个唯一的事务 ID(即,与请求、响应和接收到的数据相关联的唯一 ID),那么我们就能将异步调用重新编织在一起。在这里,我们很幸运。
- 请求的
IInternetBindInfo
大多数情况下是唯一的,并且在所有方法中都可用。但有时,浏览器会重用它。 - 如果请求是从 iframe 发送的,则
IHTMLWindow2
存在并且是唯一的。 - 请求的目标
Url
也很有可能唯一。
因此,如果我们从所有这些中创建一个 ID,我们将获得一个唯一的事务 ID。现在我们不局限于此。如果页面上有多个 iframe,那么我们可以遍历页面,根据引用者告诉我们哪个 iframe 发送了哪个请求。此外,每个 iframe 在导航时都会触发 BeforeNavigate
和 NavigationComplete
事件。iframe 的对象可从 InternetExplorer
接口获得。如果您将所有可用信息关联起来,您就可以勾勒出一个完整的图景:
- 页面上 iframe 的层次结构是什么
- 哪个 iframe 导航到了哪些 URL
- 某个特定 iframe 发送了哪些请求
- 哪些 URL 加载失败
- 哪些 URL 使用了本地计算机上的文件(及其实际文件位置)
- 发送和接收了哪些标头、Cookie
- 每个请求花了多长时间等等。
限制
- 如果您在原生 COM 控件中设置
Silent=true
,或者在 .NETWebBrowser
中设置ScriptErrorsSuppressed=true
,则上述代码将停止工作。 - 基于 iframe 过滤请求的假设是,页面上的每个 iframe 都有不同的位置。
- 另一个假设是,来自特定 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
当前位置的哈希值。
有什么新内容
- 在上一个版本中,并非所有请求标头都在
OnRequest
事件中报告,因为所有标头仅在BINDSTATUS_SENDINGREQUEST
之后才可用。 - 添加了
OnProgress
和GetIServiceProviderOnStart
事件。 - 修改了演示应用程序实现,以便 iframe 的请求属于其页面实例而不是其父页面的实例。
- 对演示应用程序和 httpmonitor DLL 的代码进行了重构/优化。
演示应用
演示应用程序提供了 HttpMonitor
的实现,用于检测完整的页面导航。主类页面接受“Internet Explorer_Server
”作为参数。它可以被视为 InternetExplorer
接口的包装器。该类具有以下属性:
Children
- 子页面的列表Entries
- 此页面/iframe 发送的请求/响应列表Navigations
- 此页面或 iframe 执行的导航AllIEW
- 所有ithNavigations
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,它使这一切成为可能。