针对内容拦截器的域

内容拦截器扩展在 iOS 和 Mac 上取得了巨大成功。开发者们正在利用 API 发挥创意,用户则喜爱其带来的速度和隐私优势。如果您对编写自己的内容拦截器感兴趣,我建议从介绍以及官方文档开始。内容拦截器与传统的 Safari 扩展不同。传统模式下,引擎(此处为 WebKit)会询问扩展代码如何操作,而内容拦截器扩展则是提前告知引擎其应如何操作。每个内容拦截器扩展都定义了一系列按顺序执行的规则。每条规则只有在满足特定条件时才能被激活。这些条件在每条规则的“触发器”中定义。在这篇博客文章中,我们将探讨一种特别受欢迎的触发器类型:基于域名的激活。

域名与 URL 过滤器

最受欢迎的触发器类型之一是基于匹配特定域名的激活方式。通常,作者希望阻止浏览器向这些域名泄露信息。触发器规范并没有明确的方法来匹配特定域名的请求。相反,规则中的“url-filter”允许对每个请求的完整 URL 应用正则表达式匹配。编写匹配域名正则表达式的方法有很多种。总的来说,无论表达式如何编写,运行时性能都非常出色。在后端,内容拦截器引擎会将规则编译成少量优化的有限状态机。编写相同表达式的不同方式会生成相同的状态机。有些定义比其他更通用;优化它们是引擎的工作。坏消息是,不同的正则表达式会导致编译时间大相径庭。有些扩展在几毫秒内完成编译,而另一些则需要几秒钟。我们交给优化器的工作越多,编译过程就越长。在 iOS 上,这意味着用户启用内容拦截器后,它需要更长时间才能应用于已加载的页面。在接下来的章节中,我们将了解 URL 过滤器(url-filters)的定义如何影响编译时间。本文的示例是要匹配“a-tracker.com”及其所有子域名。

通用域名匹配

在通过内容拦截器引擎之前,URL 会被规范化。匹配域名的最直接方法是匹配可能出现在有效规范化 URL 中的精确字符序列。它看起来会像这样

[
    {
        "trigger": {
            "url-filter": "^[a-z][-+.0-9a-z]*:/+([!$%&'()*+,-.0-9:;=a-z_~]*@)?([-%0-9a-z_]+\.)*a-tracker\.com[:/]"
        },
        "action": {
            "type": "block"
        }
    }
]

在最坏情况下,编译五万个此类模式大约需要 10 秒。有点长。编译此类模式耗时较长的原因是其复杂性。优化器需要花费大量时间来找出处理所有字符集的最有效方法。

我们来简化类似 HTTP URL 的表达式

URL 规范在可定义内容方面提供了很大的自由度。在大多数情况下,我们只关心匹配 HTTP 方案 URL 的子集。我们可以通过使结构更严格来改进模式!首先,在规范化 URL 中,方案和域名始终是小写。我们可以将触发器更改为

"trigger": {
    "url-filter": "^[a-z][-+.0-9a-z]*://+([!$%&'()*+,-.0-9:;=A-Za-z_~]*@)?([-%0-9a-z_]+\.)*a-tracker\.com[:/]",
    "url-filter-is-case-sensitive":true
}

这已经快了 20%!接下来,我们来简化方案定义。当编译“^[a-z][-+.0-9a-z]*://+”时,编译器会生成专门匹配此处定义字符的代码。匹配第一个字符很简单,它只需要是“a”到“z”之间的字符(含边界)即可。匹配后续字符会稍微复杂一些,因为需要测试更多的可能性。有两种方法可以使这部分变得简单。如果允许将模式限制为 HTTP/HTTPS,那么使用模式“^https?://+”是有效的。如果您需要匹配任何方案,模式“^[^:]+://”是一个不错的选择。编译器可以假设输入可以跳过,直到遇到第一个字符“:”,而不是在“a”到“z”的范围内查找字符。这是一个更简单的操作,从而使其更容易优化。我们可以使用相同的思路来匹配域名之前的任何内容。与其精确匹配用户名、密码和子域名,我们不如排除域名之后的所有内容。我们最终得到一个简化的表达式,如下所示

"trigger": {
    "url-filter": "^[^:]+://+([^:/]+\.)?a-tracker\.com[:/]",
    "url-filter-is-case-sensitive":true
}

在最坏情况下,编译五万个此类模式需要 2.7 秒,几乎是原始模式的四倍!

结论

内容拦截器编译器旨在优化内容拦截器的运行性能。此类优化可能需要时间。如果您的扩展包含数千条规则,那么值得考虑使用简单的正则表达式来减少编译时间。这样做可以确保用户在扩展激活后立即享受到其带来的好处。对于域名匹配,我的建议是使用以下形式的触发器:

"trigger": {
    "url-filter": "^[^:]+://+([^:/]+\.)?domain-to-match\.tld[:/]",
    "url-filter-is-case-sensitive":true
}

对于其他情况,请记住以下几点:

  • 如果可能,请使用“url-filter-is-case-sensitive”。它能将需要考虑的字符数量减半。
  • 不要忘记转义特殊字符。特别是“.”可以使模式比实际需要更通用。
  • 有时,定义不应匹配的字符比列出所有可能匹配的字符更简单。

我很乐意回答任何与 WebKit 中内容拦截器相关的问题。您可以在 Twitter 上找到我:@awfulbenBrian 可以回答关于在 Safari 中使用内容拦截器的问题。像往常一样,您也可以通过 @jonathandavis 提出其他任何问题。