WebKit 内容阻断器简介

浏览器扩展在现代浏览器中已经占据了很长一段时间的重要地位。有了扩展,每个人都可以根据自己的偏好来改变浏览器。

如今,有几种扩展浏览器模型。大多数扩展都是用 JavaScript 编写并由浏览器加载,遵循的是 Mozilla 十多年前引入的模型。WebKit 也使用该模型。这是为 OS X Safari、Web/Epiphany 和其他浏览器编写扩展的经典方式。

在 OS X 和 iOS 上,还有 App 扩展的概念,它们采用不同的安全和性能方法。它们本质上是按需启动的小型沙盒应用程序,用于扩展某些特定功能,即所谓的扩展点。

JavaScript 扩展模型在许多用例中都表现出色,但在某一类扩展中,WebKit 项目成员认为我们应该做得更好:内容阻断扩展。这类扩展是最受欢迎的类型;它们让用户决定加载什么和不加载什么、谁可以跟踪他们、页面上应该显示什么等等。

我们对基于 JavaScript 的内容阻断扩展感到不满意的原因是它们具有显著的性能缺陷。当前模型消耗大量能量,缩短了电池续航时间,并通过增加每个资源的延迟来增加页面加载时间。某些类型的扩展还会降低网页的运行时性能。有时,它们会分配大量内存,这与我们减少 WebKit 内存占用量的努力背道而驰。

在这个领域,我们希望做得更好。我们正在开发新工具,以极低的成本实现内容阻断。

我们正在开发的一项新功能允许以结构化格式提前、声明性地描述内容阻断规则,而不是在需要做出阻断决定时运行扩展提供的代码。这种模型使得 WebKit 能够将规则集编译成一种非常高效的格式,以便应用于加载和页面内容。

内容阻断器实战

在深入细节之前,我们先看看声明式扩展在新格式中是什么样子。本质上,每个内容阻断器扩展都是一个规则列表,它告诉引擎在加载资源时如何操作。这些规则以 JSON 格式编写。例如,这里有一个包含两条规则的扩展:

[
    {
        "trigger": {
            "url-filter": "evil-tracker\\.js"
        },
        "action": {
            "type": "block"
        }
    },
    {
        "trigger": {
            "url-filter": ".*",
            "resource-type": ["image", "style-sheet"]
            "unless-domain": ["reputable-content-server.com"]
        },
        "action": {
            "type": "block-cookies"
        }
    }
]

第一条规则对包含字符串“evil-tracker.js”的任何 URL 生效。当第一条规则被激活时,加载被阻断。

第二条规则对除了“reputable-content-server.com”之外的任何域上作为图像或样式表加载的任何资源生效。当规则被激活时,在将请求发送到服务器之前,所有 cookie 都将从请求中剥离。

规则由浏览器传递给引擎。在 iOS Safari 中,这是通过原生应用程序扩展机制完成的。在 OS X Safari 中,浏览器扩展可以通过新的 API 提供其规则。如果你在 WebKit 上进行开发,MiniBrowser 也允许你直接从调试菜单加载规则集。

一旦规则传递给 WebKit,它们就会被编译成高效的字节码格式。然后,引擎会对每个资源请求执行此字节码,并使用结果修改请求或注入 CSS。

字节码在网络子系统中为每个资源执行。目标是减少页面创建请求与请求实际通过网络发送之间的延迟。

内容阻断器格式

内容阻断器规则以 JSON 格式传递。顶层对象是一个数组,包含所有需要加载的规则。

内容阻断器的每条规则都是一个包含两部分的字典:激活规则的触发器,以及定义规则激活时要执行操作的动作。

典型的规则集如下所示

[
    {
        "trigger": {
            …
        },
        "action": {
            …
        }
    },
    {
        "trigger": {
            …
        },
        "action": {
            …
        }
    }
]

规则的顺序很重要。对于每个扩展,动作按顺序应用。有一个动作可以跳过当前规则之前出现的所有规则:“ignore-previous-rules”。

让我们深入了解“触发器”和“动作”对象。

触发器定义

“触发器”定义了哪些属性会激活规则。当规则被激活时,其动作被排队执行。当所有触发器都评估完毕后,动作按顺序应用。

目前,触发器基于资源加载信息:每个资源的 URL 和类型、文档的域以及资源与文档的关系。

触发器中的有效字段有

  • “url-filter”(字符串,强制):匹配资源的 URL。
  • “url-filter-is-case-sensitive”(布尔值,可选):改变“url-filter”的大小写敏感性。
  • “resource-type”(字符串数组,可选):匹配资源将被如何使用。
  • “load-type”(字符串数组,可选):匹配与主资源的关系。
  • “if-domain”/“unless-domain”(字符串数组,可选):匹配文档的域。

最重要的字段,也是唯一强制的字段,是“url-filter”。在此字段中,您定义一个正则表达式,它将根据每个资源的 URL 进行评估。可以通过匹配每个字符来匹配每个 URL(例如“.*”),但通常情况下,最好尽可能精确,以避免不可预见的副作用。

正则表达式的语法是 JavaScript 正则表达式的严格子集。这将在本文后面介绍。

可以通过字段“url-filter-is-case-sensitive”来更改“url-filter”的大小写敏感性。默认情况下,匹配是不区分大小写的。

可选字段“resource-type”指定要匹配的加载类型。此字段的内容是一个数组,包含所有可以激活触发器的加载类型。可能的值有:

  • “document”
  • “image”
  • “style-sheet”
  • “script”
  • “font”
  • “raw”(任何未类型化加载,例如 XMLHttpRequest)
  • “svg-document”
  • “media”
  • “popup”

由于触发器在加载开始之前进行评估,这些类型定义了引擎打算如何使用资源,而不一定是资源本身的类型(例如,<img src=”something.css”> 被识别为图像)。

如果未指定“resource-type”,则默认匹配所有类型的资源。

字段“load-type”定义了正在加载的资源的域与文档的域之间的关系。两个可能的值是:

  • “first-party”
  • “third-party”

“first-party”加载是指 URL 与文档具有相同安全源的任何加载。所有其他情况都是“third-party”。

最后,可以使触发器依赖于主文档的 URL。在这种情况下,规则仅适用于特定域,或仅适用于特定域之外。

域过滤器是“if-domain”和“unless-domain”,这些字段是互斥的。在这些字段中,您可以为主文档提供域过滤器。

谨慎使用触发器很重要,以避免规则意外激活。由于不可能测试整个网络来验证触发器,因此建议尽可能具体。

动作定义

字典的“动作”部分定义了当资源与触发器匹配时引擎应该做什么。

目前,动作对象只有 2 个有效字段

  • “type”(字符串,强制):定义规则激活时要执行的操作。
  • “selector”(字符串,对于“css-display-none”类型是强制的):定义要应用于页面的选择器列表。

有 3 种限制资源的动作类型:“block”(阻断)、“block-cookies”(阻断 cookie)、“css-display-none”(CSS 隐藏)。还有一种额外的类型,它对资源没有影响,但会改变内容扩展的行为方式:“ignore-previous-rules”(忽略先前规则)。

动作“block”是最强大的一个。它告诉引擎中止加载资源。如果资源被缓存,缓存将被忽略,加载仍将失败。

动作“block-cookies”改变了资源通过网络请求的方式。在将请求发送到服务器之前,所有 cookie 都将从标头中剥离。Safari 有自己的隐私政策,它在此规则之上适用。只有那些本应被隐私政策接受的 cookie 才能被阻断,结合“block-cookies”和“ignore-previous-rules”仍遵循浏览器的隐私设置。

动作“css-display-none”作用于 CSS 子系统。它允许您根据选择器隐藏页面元素。当设置此动作时,应有一个名为“selector”的第二个条目,其中包含一个选择器列表。任何与选择器列表匹配的元素,其“display”属性都将设置为“none”,从而将其隐藏。

WebKit 支持的每个选择器都支持内容扩展,包括复合选择器和来自 CSS Selectors Level 4 的新选择器。例如,以下动作定义是有效的:

"action": {
    "type": "css-display-none",
    "selector": "#newsletter, :matches(.main-page, .article) .annoying-overlay"
}

最后,还有动作类型“ignore-previous-rules”。它所做的就是在触发器被激活时,忽略当前规则之前的所有规则。请注意,不可能忽略其他扩展的规则。每个扩展都与其他扩展隔离。

正则表达式格式

触发器支持基于正则表达式过滤每个资源的 URL。

“url-filter”中的所有字符串都解释为正则表达式。您必须小心转义正则表达式控制字符。通常,点号会出现在过滤器中,需要进行转义(例如,“energy-waster.com”应表示为“energy-waster\.com”)。

该格式是 JavaScript 正则表达式的严格子集。在语法上,JavaScript 支持的所有内容都已保留,但只有一部分会被解析器接受。不支持的表达式会导致解析错误。

支持以下功能

  • 使用“.”匹配任意字符。
  • 使用范围语法 [a-b] 匹配范围。
  • 使用“?”、“+”和“*”量化表达式。
  • 使用括号进行分组。

可以使用行首(“^”)和行尾(“$”)标记,但它们仅限于作为表达式的第一个和最后一个字符。例如,像“^bar$”这样的模式是完全有效的,而“(foo)?^bar$”则会导致语法错误。

所有 URL 匹配都针对 URL 的规范版本进行。因此,您可以预期 URL 完全是 ASCII 字符。域将已经过 punycode 编码。方案和域都已是小写。URL 的资源部分已经过百分比编码。

由于 URL 已知是 ASCII 字符,因此 url-filter 也仅限于 ASCII。包含非 ASCII 字符的模式会导致解析错误。

隐私

我们在构建这些功能时,重点是提供更好的隐私控制。我们希望启用更好的隐私过滤器,这也是推动现有功能集的原因。

有各种各样的功能可以利用内容阻断器 API,它们围绕着隐私或更好的用户体验。我们很高兴听到您关于哪些做得好、哪些需要改进以及缺少什么的反馈。

声明式内容阻断扩展模型的一个主要优点是,扩展不会看到用户浏览或请求页面的 URL 和资源。WebKit 本身不跟踪哪些规则在哪些 URL 上执行过;我们的设计理念就是不跟踪您。

所有内容都是公开开发的;欢迎所有人审计和改进代码。内容阻断器的主要部分位于 Source/WebCore/contentextensions

性能建议

此功能的一个重要重点是性能。我们正在努力实现良好的可伸缩性,同时对性能影响最小。

如果规则编译器检测到一组规则会对用户体验产生负面影响,它将拒绝加载它们并返回错误。

您的扩展中有一些参数确实会影响性能。在本节中,我将提供一些获得良好性能的一般规则。有几个最大化性能的主要主题:

  • 尽可能避免在“url-filter”中使用量词(“*”、“+”、“?”)。
  • CSS 规则最好在任何“ignore-previous-rules”之前定义。
  • 使触发器尽可能具体。
  • 将具有相似动作的规则进行分组。

最小化正则表达式中的量词

避免使用量词有助于我们在后端优化触发器。量词很有用,但它们增加了匹配的可能性,这往往会降低性能。我们发现许多现有的隐私扩展有时过度使用量词,这往往代价高昂。

一个特别糟糕的情况是量词出现在字符串中间。例如:

foo.*bar

它们往往比单独使用“foo”和“bar”要慢。使用太多它们可能会导致规则集被拒绝。

该规则的一个例外是常见前缀。例如,如果您有许多规则,例如:

https?://user-tracker.com
https?://we-follow-you.com
https?://etc.com

这些规则按前缀“https?://”分组,并且只算作一条带量词的规则。

将 CSS 规则置于 ignore-previous-rules 之前

编译规则时,只要我们确定 CSS 规则将一起使用,就会将其分组。例如,如果一组规则应用于每个页面(通过使用过滤器“.*”),则会为它们准备一个特殊的样式表,以便在页面加载后立即使用。

当出现“ignore-previous-rules”时,它会强制编译器中断样式表,因为在动作“ignore-previous-rules”之前出现的所有规则在动作被激活时都会被忽略。

使用具体触发器

好的触发器会尝试排除所有可能意外激活的事物。正则表达式应该尽可能具体,以实现您的预期目标。如果可能,请指定资源类型;如果规则只应应用于某些域,请使用域过滤器。

拥有具体的规则对于避免无意中更改页面很重要,但它也有助于性能。拥有少量要执行的动作是一个好主意。

将规则与相同动作分组

最后,将具有相似动作的规则分组可以简化执行。理想情况下,您的扩展将包含所有阻断加载的规则,然后是所有阻断 cookie 的规则等。

由于规则按顺序评估,将它们分组在一起很有用,因为匹配触发器意味着可以跳过所有具有相同动作的后续规则。

例如,如果所有“block”规则都在一起,一旦第一个被激活,所有后续的阻断规则都会被跳过,因为它们具有相同的动作。触发器评估会继续对第一个具有不同动作的后续规则进行。

我们期待您的反馈

我们一直在开发这些功能,目标是在不产生不合理性能成本的情况下实现更好的隐私。我们有意将自己限制在少数可以改进的功能上。

如果您开始构建内容阻断器扩展,如果您将 JSON 文件发送给我们,那将对我们有所帮助。拥有许多不同的用例将帮助我们更好地优化代码。

对于简短的问题,您可以在 Twitter 上联系。对于较长的问题,您可以发送电子邮件至 webkit-help提交错误报告。如果您有兴趣修改代码,请随时向Alex ChristensenBrady EidsonSam Weinig 寻求帮助。

有关 Safari 采用这些 API 的问题,请联系Brian WeinsteinJon Davis