关于Chrome Devtools Protocol中 Network模块对Redirect的处理

Network.requestIntercepted for Redirect

在写关于以Chrome Headless 为框架的爬虫时候。确实遇到了不少的坑,因为没有东西可以借鉴,只能去踩了。没办法遇到实在不能解释的情况只能去看Chromium的source code 了。:)

其中一个坑是关于,Network 模块拦截请求的过程。首先需要设置拦截什么?其过滤器设置方法为 Network.setRequestInterception 其中你需要做的是指定urlPattern 请求匹配表达式 ,resourceType 拦截类型,以及interceptionStage 拦截时间。

对于拦截请求的思路,我们思路肯定是放第一个 Page.navigate 的document 类型的请求过去,在一次检测过程中,我们只能放任一个navigate 类型的document过去,保证页面是稳定的。

刚开始单一页面检测,你只需要让interceptionIdid-1Network.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
<?php
header('location: http://127.0.0.1/location.php');
die();
chrome 默认 重定向次数不能 超过20,在这20次拦截里面没有一个 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
2
3
4
void SetPatterns(std::vector<DevToolsNetworkInterceptor::Pattern> patterns, bool handle_auth) {
patterns_ = std::move(patterns);
handle_auth_ = handle_auth;
}

拦截对象直接赋值给了 patterns 。这里我们可以直接看看什么时候用到这个变量的

1
2
3
4
5
6
7
8
9
10
11
InterceptionStage 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;
}
根据请求的url 和 资源类型 返回什么时候拦截。接下来是找什么地方调用这个方法的地方

src/content/browser/devtools/devtools_url_loader_interceptor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool InterceptionJob::StartJobAndMaybeNotify() {
start_ticks_ = base::TimeTicks::Now();
start_time_ = base::Time::Now();

current_id_ = id_prefix_ + base::StringPrintf(".%d", redirect_count_);
interceptor_->AddJob(current_id_, this);

const network::ResourceRequest& request = create_loader_params_->request;
stage_ = interceptor_->GetInterceptionStage(
request.url, static_cast<ResourceType>(request.resource_type));

if (!(stage_ & InterceptionStage::REQUEST))
return false;

if (state_ == State::kRedirectReceived)
state_ = State::kFollowRedirect;
else
DCHECK_EQ(State::kNotStarted, state_);
NotifyClient(BuildRequestInfo(nullptr));
return true;
}

InterceptionJob::StartJobAndMaybeNotify() 中调用了这个方法,其中这个NotifyClient 就是devtools 中来发送event的操作。可以看到他是有判断的,当stagerequest的时候才会发送这个拦截的请求。默认情况下我们设置的拦截时间都是在请求发送的时候,就是request。再去找什么地方调用的StartJobAndMaybeNotify()

两个地方: InterceptionJob的构造函数和InterceptionJob::FollowRedirect

前面说到,对于拦截每一个请求的过程中,都会创建一个InterceptionJob,对于其构造函数里面调用StartJobAndMaybeNotify可以想到是用来在request请求刚刚发起的时候,这是正常情况,我想要知道的东西在第二个地方,followRedirect,在处理重定向的时候,它是怎样向client 发送这个拦截事件的。

1
2
3
4
5
6
if (interceptor_) {	
interceptor_->RemoveJob(current_id_);
redirect_count_++;
if (StartJobAndMaybeNotify())
return;
}

可以看到处理重定向的时候,先去掉了发起重定向的第一个请求的Job,然后发送拦截事件。其实这里我是多余跟到这里了。

StartJobAndMaybeNotify()中最后会NotifyClient(BuildRequestInfo(nullptr)); ,其中BuildRequestInfo()是用来构造InterceptedRequestInfo 传进去的是个空指针,来看看是怎么构造的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::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_idframe_idresource_typeis_navigation,因为传进去的是空指针是没有response_headers赋值的。没有我们想要的redirectUrl

再来看看NotifyClient 是怎么发送事件的, NotifyClient ->NotifyClientWithCookies

1
2
3
4
5
6
request_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
2
3
4
5
6
7
8
frontend_->RequestIntercepted(
info->interception_id, std::move(info->network_request),
info->frame_id.ToString(), ResourceTypeToString(info->resource_type),
info->is_navigation, std::move(info->is_download),
std::move(info->redirect_url), std::move(auth_challenge),
std::move(error_reason), std::move(status_code),
std::move(response_headers));
}

std::move(info->redirect_url),这里在输出的时候info 里面没有redirectUrl这个值,前面已经分析过了。

到这里断了,得换一个思路在找,这时候我想看看哪里会调用到NotifyClient,即发送拦截时间的时候。

发现其实还有三个地方:

1
2
3
InterceptionJob::OnReceiveResponse
InterceptionJob::OnReceiveRedirect
InterceptionJob::OnAuthRequest
感兴趣的肯定是InterceptionJob::OnReceiveRedirect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void 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