1. 背景
本文将介绍 chrome 扩展(chrome插件)webRequest
模块的内容,介绍其对于实际应用中的使用场景,以及如何使用的简单引导,适用于对 chrome 扩展开发不熟悉或者对webRequest
模块不熟悉的朋友。
本文中的内容,绝大部分是对官方文档的翻译,其次就是笔者在 Chrome 扩展的开发过程中,对该模块的理解。如果想了解更多信息,请阅读官方文档,本文仅起抛砖引玉的作用。
chrome 一直在更新版本,新版本发布时,模块的使用方法、参数等或许会发生变化,如果在使用过程中,部分 api 出现使用错误的情况,请读者及时查看最新的官方文档。
2. 应用场景
- 嗅探分析网络请求
- 屏蔽广告
- 禁用跨域
- 反向代理调试线上项目
- 修改请求头
- …
3. 知识背景
3.1 权限相关
要使用网络请求 API,必须要在扩展程序的清单文件中声明 “webRequest” 权限,以及需要访问网络请求的所有主机的主机权限。如果需要以阻塞方式使用网络请求 API,还需要另外请求 “webRequestBlocking” 权限。
3.2 请求的生命周期
网络请求 API 定义了一系列事件,在一次网络请求的生命周期内产生,可以使用这些事件监控和分析流量。某些同步事件还允许使用者截获、阻止或者修改请求。
- onBeforeRequest(可以为同步)
当请求即将发出时产生。这一事件在 TCP 连接建立前发送,可以用来取消或重定向请求。
- onBeforeSendHeaders(可以为同步)
当请求即将发出并且初始标头已经准备好时产生。在发送 HTTP 请求前,一旦请求标头可用就产生这一事件。这一事件可能在与服务器的 TCP 连接建立后产生,但是确保在发送任何 HTTP 数据前产生。这一事件是为了使扩展程序能够添加、修改和删除请求标头。onBeforeSendHeaders 事件将传递给所有订阅者,所以不同的订阅者都可以尝试修改请求。有关具体如何处理的细节,请参见实现细节部分。这一事件可以用来取消请求。
- onSendHeaders
当所有扩展程序已经修改完请求标头并且展现最终版本时产生。当请求即将发送至服务器的前一刻产生(前面 onBeforeSendHeaders 事件处理函数作出的修改可以在这一事件产生时体现)。这一事件在标头发送至网络前触发,仅用于提供信息,并且以异步方式处理,不允许修改或取消请求。
- onHeadersReceived(可以为同步)
每当接收到 HTTP(S) 响应标头时产生。由于重定向以及认证请求,对于每次请求这一事件可以多次产生。这一事件是为了使扩展程序能够添加、修改和删除响应标头,例如传入的 Set-Cookie 标头。缓存指示是在该事件触发前处理的,所以修改 Cache-Control 之类的标头不会影响浏览器的缓存。它还允许重定向请求。
- onAuthRequired(可以为同步)
当请求需要用户认证时产生。当接收到认证失败时产生。事件处理函数有三种选择:提供认证凭据、取消请求并显示错误页面、不采取任何行动。如果提供了错误的用户凭据,可能会为同一请求重复调用事件处理函数。这一事件可以同步处理,提供认证凭据。注意,扩展程序提供的凭据可能无效,注意不要重复提供无效凭据,陷入无限循环。
- onBeforeRedirect
当重定向即将执行时产生,重定向可以由 HTTP 响应代码或扩展程序触发。这一事件仅用于提供信息,并以异步方式处理,不允许修改或取消请求。
- onResponseStarted
当接收到响应正文的第一个字节时产生。对于 HTTP 请求,这意味着状态行和响应标头已经可用。这一事件仅用于提供信息,并以异步方式处理,不允许修改或取消请求。
- onCompleted
当请求成功处理后产生。
- onErrorOccurred
当请求不能成功处理时产生。
关于“同步事件”的含义将在下文中描述
3.3 备注
3.3.1 最终事件
网络请求 API 保证对于每一个请求,onCompleted 或 onErrorOccurred 是最终产生的事件,除了如下例外:如果请求重定向至data://URL
,onBeforeRedirect 将是最后报告的事件。
3.3.2 特殊 request header
当前不提供给 onBeforeSendHeaders 事件的标头列表,这一列表不保证是完整的或者不会变化:
- Authorization
- Cache-Control
- Connection
- Content-Length
- Host
- If-Modified-Since
- If-None-Match
- If-Range
- Partial-Data
- Pragma
- Proxy-Authorization
- Proxy-Connection
- Transfer-Encoding
3.3.3 支持哪些协议
只有扩展程序具有相应的主机权限时,网络请求 API 才会暴露相关的请求。此外,只能访问下列协议的请求:http://、https://、ftp://、file:// 或 chrome-extension://。 此外,某些请求的 URL 即使使用了以上某种协议也会被隐藏。例如,chrome-extension://other_extension_id,其中 other_extension_id 不是处理该请求的扩展程序标识符。
3.3.4 关于 CORS
从 Chrome79 开始,请求标头修改会影响跨源资源共享(CORS)检查。如果为跨源请求修改的标头不符合条件,将导致发送 CORS 预检请求,以询问服务器是否可以接受这些标头。如果真的需要修改标头以违反 CORS 协议,那么需要在 opt_extraInfoSpec 中指定“extraHeaders”。另一方面,响应标头的修改并不能欺骗 CORS 检查。如果需要欺骗 CORS 协议,还需要为响应修改指定“extraHeaders”。
从 Chrome79 开始,web 请求 API 在默认情况下不会拦截 CORS 飞前请求和响应。如果在 opt_extraInfoSpec 中为请求 URL 指定了“extraHeaders”的侦听器,则扩展可以看到请求 URL 的 CORS 预检请求。
3.4 注册事件监听器
enum ResourceType {
main_frame: 'main_frame',
sub_frame: 'sub_frame',
stylesheet: 'stylesheet',
script: 'script',
image: 'image',
font: 'font',
object: 'object',
xmlhttprequest: 'xmlhttprequest',
ping: 'ping',
csp_report: 'csp_report',
media: 'media',
websocket: 'websocket',
other: 'other',
}
interface RequestFilter {
tabId: number
types: ResourceType[]
urls: string[]
windowId: number
}
interface Header {
name: string
value?: string
binaryValue?: any
}
interface Credential {
username: string
password: string
}
interface BlockingResponse {
// 仅用于 onBeforeRequest
cancel: Boolean
// 仅用于 onBeforeRequest 和 onHeadersReceived
redirectUrl: string
// 仅用于 onBeforeSendHeaders,如:[ { name: 'pragma', value: 'no-cache' }]
requestHeaders: Header[]
// 仅用于 onHeadersReceived,如:[ { name: 'content-type', value: 'text/html' }]
responseHeaders: Header[]
// 仅用于 onAuthRequired
authCredentials: Credential
}
enum Option {
blocking: 'blocking',
extraHeaders: 'extraHeaders',
requestBody: 'requestBody',
requestHeaders: 'requestHeaders',
responseHeaders: 'responseHeaders',
}
const callback = details => {
this.callback(details, 'onBeforeRequest')
const response: BlockingResponse = {
cancel: true
}
return response
}
const filter: RequestFilter = { urls: ['<all_urls>'] }
// 详情请看官方文档
const options: Option[] = ['blocking']
chrome.webRequest.onBeforeRequest.addListener(callback, filter, options)
-
callback
即事件触发时的回调,该回调默认传入一个参数(details),details 就是请求的详情。
-
filter
Object 类型,限制事件回调 callback 触发的过滤器。filter 有四个属性可以指定,分别为 ①urls(包含指定 url 的数组)、②types(请求的类型,共 8 种)、③tabId(标签页 id)、④windowId(窗口 id)。
-
options
包含"blocking",意味着要求请求同步,基于此才可以修改消息头。
4. 如何编写代码
4.1 声明权限
{
// ....
"permissions": [
// 声明模块权限
"webRequest",
// 声明模块权限
"webRequestBlocking",
// 声明主机权限
"<all_urls>"
// ....
]
// ....
}
4.2 DEMO
4.2.1 入门 DEMO
const callback = details => {}
const filter = { urls: ['<all_urls>'] }
const extraInfoSpec = []
chrome.webRequest.onBeforeRequest.addListener(callback, filter, extraInfoSpec)
每一个 addListener() 调用必须传递回调函数,作为第一个参数。将向这一回调函数传递包含当前 URL 请求详情的字典数据,字典数据中的信息取决于具体事件类型以及 extraInfoSpec 的内容。
webRequest.RequestFilter 类型的 filter 参数允许通过不同的方式限制为哪些请求产生事件:
- urls
URL 匹配表达式,例如 😕/www.google.com/foobar。
- types
请求类型,例如 “main_frame”(为顶层框架加载的文档)、“sub_frame”(为内嵌框架加载的文档)和 “image”(网站上的图片)。请参见 webRequest.RequestFilter。
- tabId
某个标签页的 id。
- windowId
某个窗口的 id。
extraInfoSpec 取决于所监听的事件类型,可以在 extraInfoSpec 中指定字符串,获取有关请求的附加信息。这样是为了仅在明确请求时才提供请求数据的有关详情。
4.2.2 阻塞请求的 DEMO
chrome.webRequest.onBeforeRequest.addListener(
function (details) {
return { cancel: details.url.includes('https://www.baidu.com') }
},
{ urls: ['<all_urls>'] },
['blocking']
)
chrome.webRequest.onBeforeRequest.addListener(
function (details) {
return { cancel: true }
},
{ urls: ['*://www.baidu.com/*'] },
['blocking']
)
示例二的效率更高,因为不发送至 www.baidu.com 的请求不必传递给扩展程序。
4.2.3 禁用跨域的 DEMO
笔者只是介绍如何禁用跨域,若不是非常清楚该行为的副作用,请不要添加该行为。
class WebRequestUtils {
static getHeaderValueByName (headers, name) {
for (let i = 0; i < headers.length; i++) {
const header = headers[i]
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value
}
}
return ''
}
static modifyHeaderValue (headers, name, value) {
for (let i = 0; i < headers.length; i++) {
const header = headers[i]
if (header.name.toLowerCase() === name) {
headers.splice(i, 1)
}
}
headers.push({ name, value })
}
}
const callback = async details => {
let { initiator, responseHeaders, url } = details
const originalOrigin = WebRequestUtils.getHeaderValueByName(
responseHeaders,
'Access-Control-Allow-Origin'
)
if (
initiator &&
!initiator.startsWith(originalOrigin) &&
originalOrigin !== '*'
) {
console.log('%cCORS', 'color: red;', `pageUrl: ${initiator} url: ${url}`)
WebRequestUtils.modifyHeaderValue(
responseHeaders,
'Access-Control-Allow-Origin',
'*'
)
WebRequestUtils.modifyHeaderValue(
responseHeaders,
'Access-Control-Allow-Credentials',
'false'
)
WebRequestUtils.modifyHeaderValue(
responseHeaders,
'Access-Control-Allow-Methods',
'*'
)
WebRequestUtils.modifyHeaderValue(
responseHeaders,
'Access-Control-Allow-Headers',
'*'
)
}
return { responseHeaders }
}
chrome.webRequest.onHeadersReceived.addListener(
callback,
{ urls: ['<all_urls>'] },
['blocking', 'responseHeaders', 'extraHeaders']
)
4.3 同步事件
如果可选的 extraInfoSpec 数组包含 ‘blocking’ 字符串(仅允许用于特定事件,具体请查看上文中的生命周期),回调函数将以同步方式处理。这意味着请求将阻塞,直到回调函数返回。在这一种情况下,回调函数可以返回 webRequest.BlockingResponse 对象,确定这一请求下一步的生命周期。取决于当前上下文,这一响应允许取消或重定向某个请求(OnBeforeRequest),取消请求或修改标头(onBeforeSendHeaders),或者提供认证凭据(onAuthRequired)。
5. webRequest
实现细节
5.1 冲突的解决
在网络请求 API 的当前实现中,如果至少一个扩展程序要求取消请求,则请求将被取消。如果一个扩展程序取消了一个请求,所有扩展程序都会收到 onErrorOccurred 事件的通知。一次只有一个扩展程序允许重定向请求或者修改标头。如果多于一个扩展程序尝试修改请求,最近安装的扩展程序具有优先级,而忽略所有其他扩展程序。如果扩展程序修改或重定向的要求被忽略,也不会收到通知。
5.2 缓存
Chrome 浏览器使用两种缓存:磁盘缓存和十分快速的内存缓存。内存缓存的生命周期与渲染器进程(大致与每个标签页对应)的生命周期相关,通过内存缓存响应的请求对网络请求 API 不可见。如果请求处理函数更改了它的行为(例如根据哪些请求被阻止而作出的行为),简单的页面刷新不一定能够体现这一更改的行为。要确保行为更改生效,请调用handlerBehaviorChanged()
来清洗内存缓存。然而不要经常调用,清洗缓存是一项开销非常大的操作。
5.3 时间戳
网络请求事件的 timeStamp 属性仅保证内部的一致性,将两个事件的时间相比较会得到它们之间正确的时间差,但是与扩展程序内的当前时间(例如通过 (new Date()).getTime())比较则可能会导致不可预料的结果。(关于这一点,建议直接阅读官方文档)
5.4 错误处理
如果使用无效的参数注册事件,会引发 JavaScript 错误,事件处理程序也不会注册。如果事件处理的过程中产生错误,或者事件处理程序返回无效的 BlockingResponse 对象,扩展程序的控制台中会记录错误消息,同时忽略对应请求的处理程序。
6. 局限
6.1 无法修改响应数据
出于安全考虑,只允许监听请求、修改消息头,不允许直接修改响应数据。
7. API
笔者只在此处列出部分 API,详细的 API 文档请查看官方资料。
7.1 RequestFilter
描述应用于网络请求事件的过滤器对象。
属性 | 类型 | 描述 |
---|---|---|
urls | array of string | URL 或 URL 匹配表达式列表,不匹配任何 URL 的请求会被过滤出去。 |
types(可选) | array of enum of “main_frame”, “sub_frame”, “stylesheet”, “script”, “image”, “object”, “xmlhttprequest”, or “other” | 请求类型的列表,不匹配任何一种类型的请求会被过滤出去。 |
tabId(可选) | integer | 标签页的 id |
windowId(可选) | integer | 窗口的 id |
7.2 HttpHeaders
包含 HTTP 标头的数组,每一项标头都通过词典表示,包含 name 属性,以及 value 或 binaryValue 中的某一属性。
7.3 BlockingResponse
用于在 extraInfoSpec 参数中指定 “blocking” 的事件处理函数的返回值,允许事件处理函数修改网络请求。
属性 | 类型 | 描述 |
---|---|---|
cancel(可选) | boolean | 如果为 true,则取消请求。在 onBeforeRequest 事件中使用,用来阻止请求的发送。 |
redirectUrl(可选) | string | 仅用于 onBeforeRequest 和 onHeadersReceived 事件的返回值。如果设置了该属性,就会阻止原始请求的发送/完成,并重定向至指定的 URL。允许重定向至非 HTTP 协议的 URL,例如 data:。重定向操作产生的重定向通常使用原来的请求方法,以下情况例外:如果在 onHeadersReceived 阶段产生了重定向,重定向请求将使用 GET 方法发出。 |
requestHeaders(可选) | HttpHeaders | 仅用于 onBeforeSendHeaders 事件的返回值。如果设置了这一属性,请求将改用这些标头发出。 |
responseHeaders(可选) | HttpHeaders | 仅用于 onHeadersReceived 事件的返回值。如果设置了这一属性,则假定服务器返回了这些响应标头。为了限制冲突数目(对于每一个请求,只有一个扩展程序可以修改 responseHeaders),只有有当确实需要修改标头时才应该返回 responseHeaders。 |
authCredentials(可选) | object | 仅用于 onAuthRequired 事件的返回值。如果设置了这一属性,发出的请求将使用提供的凭据,数据结构:{username: ‘’, password: ‘’} |
7.4 handlerBehaviorChanged
chrome.webRequest.handlerBehaviorChanged(() => {})
当网络请求处理函数的行为发生更改时,为了避免缓存导致的不正确处理,需要调用这一函数。调用这一函数的开销比较大,不要经常调用。
8. 资源
8.1 DEMO项目
DEMO包含该模块全部事件的监听方法,解压之后,直接安装到chrome浏览器,即可使用插件,下载地址。