文 / Mark Nottingham

译 / 孟舒贤

审校 / 蒋默邱泽

原文 / https://www.fastly.com/blog/improve-http-structured-headers

HTTP标头有什么问题?

大多数Web开发人员都熟悉HTTP标头;如Content-Length、Cache-Control和Cookie之类。它们会携带请求和响应的元数据,通常,这部分数据是消息发送者由于某种原因无法放入正文内容的信息,或者是消息接收者无需查看正文内容即可获得的信息。

因为标头需要由许多不同的客户端和服务器,代理服务和CDN处理(通常在消息的生存期内不止一次),所以大家希望它们易于处理,高效解析并且定义明确句法。

HTTP将头值(更恰当的说是字段值,因为它们也可能出现在主体后面的尾部字段中)定义为一个约束很少的“八进制序列”(即字节),尽管建议是ASCII字节。它还建议在ABNF中定义标题,如果用逗号分隔字段的值,则可以将同名的多个字段组合在同一行上。

因此,每个标题字段都有自己的唯一定义,需要知道它才能解析值。一些领域作者使用ABNF来做到这一点;另一些人使用示例。有些只是让你根据你以前看到的价值观来猜测。

例如,考虑年龄年限标题。它是核心HTTP规范的一部分,所以它应该是定义明确的,而且它只是一个简单的整数。

Age: 42

由此ABNF指定:

Age = delta-secondsdelta-seconds = 1*DIGITDIGIT = %x30-39 ; 0-9

起初这似乎很简单——0到9之间数字的一个到多个实例。但在实际考虑中,如果一个实现遇到这些现实标题中的任何一个,它应该做什么:

Age: 0, 60Age: 60, 0Age: 50mAge: abc234Age: 60;ms=212

它不是那么简单,因为测试真正的缓存需要用年限显示。

因此,当同一个人正在编写生成和消耗消息头的代码而没有其他人时,示例或ABNF可能是一个足够的定义,但如果有多个实现生成和解析值,则互操作性是很糟的。

每个标题作者都必须记住要解决一个问题列表,这些问题涉及如何处理重复值、案例规范化、无论是单个项目还是列表等等。通常,他们不会处理这些问题,这意味着开发者通常以不同的方式自行选择。

未充分指定的消息头也是安全问题的来源;如果实现解析消息头的方式不同,它们的行为可能会不同,从而导致Response Splitting这类的攻击。

浏览器供应商已经足够关注这些问题,开始像CSP算法那样定义头。也就是说,他们费力地定义解析和序列化算法,然后创建测试用例。这种方法对字段语法的模糊性较小,实现之间的差异较小。然而,它仍然是一次性的;它只有助于澄清特定标题的算法。对于规范的作者来说,去努力并确保它是正确的也是很累的-所以大多数标题作者都不会费心。它还为实现者创建了大量繁忙的工作,因为他们需要分别实现每个新的头的解析器。

引入结构化领域

HTTP工作组已经非常清楚了这类问题,几年前我们开始尝试定义一些更好的方法,使人们可以使用这些东西来创建新的字段。经过几次尝试后,我们确定了一种最初称为结构化标题的方法,但我们现在(更正确地)称之为“结构化字段”。

结构化字段是一个定义良好的数据类型库,在HTTP头和拖车中可能有用,包括字符串、Tokens、布尔值、整数、小数和字节序列作为原子“Item”类型,以及这些项的列表和字典。重要的是,它定义了每种类型的精确解析和序列化算法,以及错误处理和详细的测试套件-所有这些都有助于确保互操作性。

这允许新头字段的作者根据这些类型定义它。例如,他们可以说“这是一个字符串列表”,人们将知道如何使用一个现成的库来明确地解析和生成标头,而不是编写特定于头的代码。

Example-Header: "blue", "sort of red", "green"

每个项目也可以有参数,或键/值对的额外信息。参数是一种重要的可扩展性机制,它允许消息头随着时间的推移而演变。

Example-Header: "blue"; websafe, "sort of red"; author="sue", "green"

递归的形式也很有限;列表和字典值也可以包含列表,例如:

Example-Header: people=(joanna stacy), places=("new york" "rome")

内部列表中的每个项目以及内部列表本身都可以进行参数化。

你可能会注意到,这些消息头看起来很像许多现有的HTTP字段。这是通过设计实现的;不仅对开发人员来说是舒适的,它还允许通过结构化字段实现生成许多现有字段,并且通常它们也可以被解析。例如,许多Cache-Control报头都是有效的“结构化字段”,即使它没有定义为一个:

Cache-Control: max-age=3600, immutable

很不幸你还不能将结构化字段用于现有的标头,也无法仅通过查看它来判断给定字段是否是结构化字段;你必须知道它的定义值,因为结构化字段至少在现在才用于新字段。

使用结构化字段获得更好的性能

指定新字段更容易,并使它们更安全和更可互操作,这对HTTP来说是一个显著的改进。如果结构化字段也能帮助HTTP性能呢?他们有两种方法可以帮助你。显然,这些都是投机性的好处,但它们仍然是有趣的谈论。

首先是解析效率。由于传统的HTTP消息头是文本形式的,解析器必须接触字符串中的每个字节,有时需要多次,有时会将其复制并重新复制到内存的不同部分。这是一个固有的低效过程,HTTP/2和HTTP/3是二进制协议而不是文本协议的原因之一。

在结构化字段之前,我们对此无能为力,因为HTTP消息头的定义非常松散。结构化字段中定义良好的数据类型会改变这一点。现在,我们可以定义一个新的,二进制序列化的任何头使用他们。

二元结构化字段是定义这种序列化的草案建议,以定义这样的序列化。它使用HTTP/2(和/3)SETTINGS机制来协商对替代序列化的支持,并利用结构化字段与许多现有标题字段的语法的相似性将其返回到一组已经广泛使用的标题字段上,如果它们无法解析,则返回到不透明的文本。

二进制序列化将帮助性能多少?由于预期会减少CPU负载,因此它应该减少请求处理的延迟并提高可伸缩性。我们还没有真实的统计,但是如果你考虑许多标题所采取的路径-从JavaScript到浏览器,然后再到CDN,通过多个CDN节点到源服务器,再到应用程序代码本身。累积节省的潜力是有吸引力的。

结构化字段可能有助于性能的第二种方法是通过提高压缩效。HTTP/2为头和拖车字段引入了HPACK压缩。虽然它的前身SPDY使用GZIP,但由于CRIME攻击,它被发现是不安全的。因此,HPACK(及其继承者QPACK)通过引用整个字段值来压缩字段;如果它的任何一部分发生变化,它就不能使用以前的引用(有时会对压缩效率产生令人惊人的影响)。

之所以选择整值粒度,是因为通用解析器无法理解字段值的结构;为了安全起见,我们必须确保攻击者无法通过猜测部分字段值来探测加密。

对于结构化字段,现在有一种潜在的方法可以使压缩算法对字段中的单个数据类型而不是整个值进行操作。

Cache-Control: max-age=3600, s-maxage=7200, must-revalidate

例如,考虑以下Cache-Control字段:

使用HPACK和QPACK,整个字段值存储在动态表中,并且只能由具有完全相同值的未来消息引用。如果我们将其解析为结构化字段并存储单个数据类型,我们可以存储:

lmax-age

l3600

ls-maxage

l7200

lmust-revalidate

这些变量中的每一个都可以在将来的标头中出现时分别引用,从而使压缩算法更精细,而且效率更高。

早期的原型表明,使用这种技术的提升对于Web浏览器连接来说效率非常低的,因为它们的标题往往是高度重复的,用多个字节(字段值中的每种类型都有一个字节)替换HPACK中的1字节引用实际上会造成伤害。

对于从多个客户端携带流量的连接——例如反向代理和源服务器上游的CDN所看到的流量——好处可能更明显;需要更多的实验。

长期改善HTTP

如果上面描述的反向导入技术被捕获,未来版本的HTTP(或HTTP/2和HTTP/3的扩展)可以大大减少使用中的非结构化消息头的数量。

二进制结构化字段草案描述了两种实现方法。如果字段的语法与结构化字段兼容-至少在大多数情况下-它可以作为一个发送,当失败时返回到明文标题。

没有兼容语法的标题需要另一种方法。例如,Date、Last-Modified、Expires和类似的消息头永远不可能是有效的结构化字段。但是,可以将日期表示为整数,结构化字段可以传递整数。

所以,就有这样的标题:

Date: Thu, 09 Apr 2020 09:06:50 GMT

可能会在适当的转译跳点上表示为:

SF-Date: 1586423210

这为我们提供了一种方法,可以将所有通用消息头和额外的元信息作为结构化字段发送。

立即使用结构化字段

结构化字段规范正处于标准化的最后阶段,这意味着它很快会成为一个RFC。目前我们已经有多个实例,包括在Chrome中,许多新的安全头(例如Fetch元数据)都是结构化的。

同时,可以通过具体实现来了解它们是如何工作的。例如,Python http_sfv库允许从命令行解析它们。

如果你定义了新的消息头(无论它们是针对整个的Web还是仅针对HTTP API)都可以在RFC发布后开始使用结构化字段。

03-26 08:12