Network.requestIntercepted for Redirect
在写关于以Chrome Headless 为框架的爬虫时候。确实遇到了不少的坑,因为没有东西可以借鉴,只能去踩了。没办法遇到实在不能解释的情况只能去看Chromium的source code 了。:)
其中一个坑是关于,Network 模块拦截请求的过程。首先需要设置拦截什么?其过滤器设置方法为 Network.setRequestInterception
其中你需要做的是指定urlPattern
请求匹配表达式 ,resourceType
拦截类型,以及interceptionStage
拦截时间。
对于拦截请求的思路,我们思路肯定是放第一个 Page.navigate
的document 类型的请求过去,在一次检测过程中,我们只能放任一个navigate 类型的document过去,保证页面是稳定的。
刚开始单一页面检测,你只需要让interceptionId
为 id-1
的Network.requestIntercepted
的事件让它过去就行。其余的都丢弃就行。
而后多站点同站点并发为1的检测,你会发现interceptionId
数值在不同tab下都是同一个数值递增的。如果还是用老办法是无法实现的。所以在事件监听的handler里面多加了一个变量Lastmethod
用来保存每次向devtools 输出的方法,这样只要值为Page.navigate
就放这次请求过去。
但是检测的过程中遇到了一个情况,当Page.navigate
遇到是是一个redirect
页面。你第一次给它放了,但是第二次因为是location
也是document
,但是按流程给它丢了。会导致页面直接about:blank
,整个程序被阻塞了。
这个问题确实困扰了我一段时间。我仔细又看了一下 Network.requestIntercepted
的描述。看看有没有能标识拦截是redirect
,发现确实有一个可选项 redirectUrl
//Redirect location, only sent if a redirect was intercepted.
从描述来看,只要这个重定向被拦截到了,就会被输出这个可选项。但是事实并不是。我本地弄了一个location.php 1
2
3
header('location: http://127.0.0.1/location.php');
die();Network.requestIntercepted
带有redirectUrl
可选项的,必须得找到一个能标志是redirect请求的事件才行。其过程还有一个Network.requestWillBeSent
事件在进行重定向的时候,其中会返回一个redirectResponse
属性。唯一能标志这个请求的事件了,但是我又不想在为这个事件再写一个callbackfunction 来判断每一个requests,这样会造成性能的浪费。
我认为既然有Network.requestIntercepted
事件官方文档既然有redirectUrl 这个字段,那就肯定有情况输出的地方。Google 无获,Stackflow,google-group提问至今无获。我决定直接去看chromuim的里面到底时怎么处理。
花了一天的时候把Network的handler
处理过程理顺了。首先注册NetworkHandle,每一个拦截请求都会创建一个Interceptedjob
其中 src/content/browser/devtools/protocol/network_handler.cc
包含是Netwrok模块中最底层的操作。
首先来看注册拦截对象的过程 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
DispatchResponse NetworkHandler::SetRequestInterception(
std::unique_ptr<protocol::Array<protocol::Network::RequestPattern>>
patterns) {
if (!patterns->length()) { //首先判断传进的匹配对象数组的长度
interception_handle_.reset();
if (url_loader_interceptor_) {
url_loader_interceptor_.reset();
update_loader_factories_callback_.Run();
}
return Response::OK();
}
std::vector<DevToolsNetworkInterceptor::Pattern> interceptor_patterns; //定义初始一个完整的匹配模式
for (size_t i = 0; i < patterns->length(); ++i) { //通过遍历传进来的匹配对象数组,以此传入 interceptor_patterns;
base::flat_set<ResourceType> resource_types;
std::string resource_type = patterns->get(i)->GetResourceType(""); //默认资源类型为空代表所有类型
if (!resource_type.empty()) {
if (!AddInterceptedResourceType(resource_type, &resource_types)) {
return Response::InvalidParams(base::StringPrintf(
"Cannot intercept resources of type '%s'", resource_type.c_str()));
}
}
interceptor_patterns.push_back(DevToolsNetworkInterceptor::Pattern(
patterns->get(i)->GetUrlPattern("*"), std::move(resource_types),//默认匹配时所有请求
ToInterceptorStage(patterns->get(i)->GetInterceptionStage(
protocol::Network::InterceptionStageEnum::Request))));// 拦截时间默认都是request要发送的时候
}
if (!host_)
return Response::InternalError();
if (base::FeatureList::IsEnabled(network::features::kNetworkService)) { //这里NetworkService是一个chrome的新特性,可以在启动的--enable-feature 时开启 ,这里不是本文影响重点。所以这里我开启了
if (!url_loader_interceptor_) {
url_loader_interceptor_ = std::make_unique<DevToolsURLLoaderInterceptor>(
base::BindRepeating(&NetworkHandler::RequestIntercepted,
weak_factory_.GetWeakPtr()));
url_loader_interceptor_->SetPatterns(interceptor_patterns, true); //新定义了一个url_loader_interceptor并设置了完整的匹配模式
update_loader_factories_callback_.Run();
} else {
url_loader_interceptor_->SetPatterns(interceptor_patterns, true);
}
return Response::OK();
}
最终调用到了 Impl::SetPatterns
在/src/content/browser/devtools/devtools_url_loader_interceptor.
中
1 | void SetPatterns(std::vector<DevToolsNetworkInterceptor::Pattern> patterns, bool handle_auth) { |
拦截对象直接赋值给了 patterns
。这里我们可以直接看看什么时候用到这个变量的 1
2
3
4
5
6
7
8
9
10
11InterceptionStage GetInterceptionStage(const GURL& url,ResourceType resource_type) const {
InterceptionStage stage = InterceptionStage::DONT_INTERCEPT;
std::string unused;
std::string url_str =
protocol::NetworkHandler::ExtractFragment(url, &unused);
for (const auto& pattern : patterns_) {
if (pattern.Matches(url_str, resource_type))
stage |= pattern.interception_stage;
}
return stage;
}
src/content/browser/devtools/devtools_url_loader_interceptor.cc
中
1 | bool InterceptionJob::StartJobAndMaybeNotify() { |
InterceptionJob::StartJobAndMaybeNotify()
中调用了这个方法,其中这个NotifyClient
就是devtools 中来发送event的操作。可以看到他是有判断的,当stage
为request
的时候才会发送这个拦截的请求。默认情况下我们设置的拦截时间都是在请求发送的时候,就是request
。再去找什么地方调用的StartJobAndMaybeNotify()
。
两个地方: InterceptionJob
的构造函数和InterceptionJob::FollowRedirect
前面说到,对于拦截每一个请求的过程中,都会创建一个InterceptionJob
,对于其构造函数里面调用StartJobAndMaybeNotify
可以想到是用来在request
请求刚刚发起的时候,这是正常情况,我想要知道的东西在第二个地方,followRedirect
,在处理重定向的时候,它是怎样向client 发送这个拦截事件的。
1 | if (interceptor_) { |
可以看到处理重定向的时候,先去掉了发起重定向的第一个请求的Job,然后发送拦截事件。其实这里我是多余跟到这里了。
在StartJobAndMaybeNotify()
中最后会NotifyClient(BuildRequestInfo(nullptr));
,其中BuildRequestInfo()
是用来构造InterceptedRequestInfo
传进去的是个空指针,来看看是怎么构造的 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15std::unique_ptr<InterceptedRequestInfo> InterceptionJob::BuildRequestInfo(
const network::ResourceResponseHead* head) {
auto result = std::make_unique<InterceptedRequestInfo>();
result->interception_id = current_id_;
result->frame_id = frame_token_;
ResourceType resource_type =
static_cast<ResourceType>(create_loader_params_->request.resource_type);
result->resource_type = resource_type;
result->is_navigation = resource_type == RESOURCE_TYPE_MAIN_FRAME ||
resource_type == RESOURCE_TYPE_SUB_FRAME;
if (head && head->headers)
result->response_headers = head->headers;
return result;
}interception_id
,frame_id
,resource_type
,is_navigation
,因为传进去的是空指针是没有response_headers
赋值的。没有我们想要的redirectUrl
再来看看NotifyClient
是怎么发送事件的, NotifyClient
->NotifyClientWithCookies
1
2
3
4
5
6request_info->network_request =
protocol::NetworkHandler::CreateRequestFromResourceRequest(
create_loader_params_->request, cookie_line)
...
base::BindOnce(interceptor_->request_intercepted_callback_,
std::move(request_info))interceptor_->request_intercepted_callback_
这个callback 是在NetworkHandler::SetRequestInterception
设置拦截对象时,同时指定为NetworkHandler::SetRequestInterception
1 | frontend_->RequestIntercepted( |
std::move(info->redirect_url)
,这里在输出的时候info
里面没有redirectUrl
这个值,前面已经分析过了。
到这里断了,得换一个思路在找,这时候我想看看哪里会调用到NotifyClient
,即发送拦截时间的时候。
发现其实还有三个地方: 1
2
3InterceptionJob::OnReceiveResponse
InterceptionJob::OnReceiveRedirect
InterceptionJob::OnAuthRequestInterceptionJob::OnReceiveRedirect
, 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void InterceptionJob::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& head) {
DCHECK_EQ(State::kRequestSent, state_);
state_ = State::kRedirectReceived;
response_metadata_ = std::make_unique<ResponseMetadata>(head);
response_metadata_->redirect_info =
std::make_unique<net::RedirectInfo>(redirect_info);
if (!(stage_ & InterceptionStage::RESPONSE)) {
client_->OnReceiveRedirect(redirect_info, head);
return;
}
std::unique_ptr<InterceptedRequestInfo> request_info =
BuildRequestInfo(&head);
request_info->redirect_url = redirect_info.new_url.spec();
NotifyClient(std::move(request_info));
} stage
等于 response
。即返回的时候拦截并发送事件。
1 | request_info->redirect_url = redirect_info.new_url.spec(); |
也指定了redircetUrl
参数项。这里我们可以知道了在调用 Network.requestIntercepted
的时候,需要指定在请求收到的时候拦截才行。这里我们需要同时指定在请求发起 和 请求回复的 都拦截才行。会不会造成资源浪费呢,其实并不会浪费多少,因为只有第一个请求过去了,它才有response 这里才会被拦截并发送事件。
所以这里在handler 里面还需要定义一个变量用来判断下一个拦截请求是不是重定向。而后我向chromuim 的开发组意见,这里为什么不在followRedirect的时候同样也指定这是一个Redirect请求呢,╮(╯▽╰)╭ 如果有chromium的源码我真想自己改了。能省不少事,能hook 底层,就可以少写很多冗余的调用的代码。下一步我想搞一套chromium的源码,试着去改然后编译,想想就很有趣。
永远不说放弃,努力再努力,终会如愿以偿 2019年03月30日18:13:50 maple