前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >rfc7230 Message Syntax and Routing

rfc7230 Message Syntax and Routing

作者头像
charlieroro
发布2020-03-24 09:28:45
7980
发布2020-03-24 09:28:45
举报
文章被收录于专栏:charlierorocharlieroro

目录

  • rfc7230
    • 2 Architecture
    • 3 Message Format
    • 4 Transfer Codings
    • 5 Message Routing
    • 6 Connection Management
    • 8 IANA Considerations
    • 9 Security Considerations

rfc7230

2 Architecture

2.6 Protocol Versioning

HTTP使用.来标识协议版本。本规范定义的版本为"1.1"。版本号表示发送端遵从对应HTTP版本的实现需求。

HTTP的版本由消息首行中的HTTP-version字段表示。HTTP-version区分大小写。

代码语言:javascript
复制
HTTP-version  = HTTP-name "/" DIGIT "." DIGIT
HTTP-name     = %x48.54.54.50 ; "HTTP", case-sensitive

HTTP版本包含2个使用"."分割的十进制数字。第一个数字表示消息语义,第二个数字表示该major 版本下的最高minor版本,表示发送端后续通信遵从的协议版本。minor 版本宣告了发送者的通信能力(即使发送者仅使用了向后兼容的协议),因此可以在响应或后续的请求中通知接收者更多的高级特性。

当给HTTP/1.0或版本未知的接收者发送HTTP/1.1时,HTTP/1.1消息被组装为一个有效的HTTP/1.0消息,所有的新特性会被接收者忽略。本规范对接收者版本(recipient-version)处理一些新特性时设置了要求,这样遵循一致性的发送端在(通过配置或接收到的消息)确定接收者支持HTTP/1.1前仅会使用兼容的特性

相同major HTTP版本的不同minor版本下对首部字段的解析不变(虽然接收者对字段的默认行为可以改变)。除非特别指定,HTTP/1.1中定义的首部字段适用于所有版本的HTTP/1.x。特别地,Host和Connection首部字段应该在所有的HTTP/1.x中实现,无论这些实现是否与HTTP/1.1保持一致。

引用新的首部字段时无需修改协议版本(如果允许不识别该字段地接收者能够忽略这些字段)。首部字段扩展定义在3.2.1章节中。

处理HTTP消息的中间设备(非隧道)必须在转发消息时发送其自身的HTTP版本号。换言之,中间设备不允许在未确保消息中的版本号与接收和发送端保持一致的前提下盲目转发HTTP消息中的首行信息。未重写消息中的HTTP版本就进行转发,可能会在下游接收者通过发送者版本来决定后续传输的特性的情况下导致通信错误。

客户端在发送请求时应该使用其支持的最高版本(如果事先知道,major版本不能高于服务端所支持的最高版本)。客户端不能发送其不支持的版本。

客户端可能会在尝试至少一次正常的请求并从响应状态码或首部字段了解到服务端没有正确实现HTTP规范后,发送较低版本的请求。

服务端应该发送major版本小于或等于接收到的请求中的major版本,且其支持的最高minor版本的响应。服务端不能发送其不支持的版本。服务端可以在任何条件下通过发送505(HTTP Version Not Supported)响应来拒绝为客户端的major协议版本提供服务。

如果服务端知道或推测客户端没有正确实现HTTP规范来解析后续版本的响应(如客户端无法正确解析版本号或中间设备会盲目转发不遵从minor版本的HTTP版本号)时,服务端可能会发送HTTP/1.0响应。除非被特定的客户端属性触发(如请求中的一个或多个首部字段唯一地匹配到已知有错误的客户端发送的值),否则不能执行协议降级。

HTTP版本设计的目的为:major数字只能引入不兼容的消息语义时增加,minor数字只能在修改协议时影响到消息语义或发送端的其他能力时增加。然而,minor在RFC2068RFC2616转变时并没有增加,因为该版本并没有对协议进行修改。

当接收到一个实现了major版本但minor高于接收者所实现的HTTP消息时,接收者应该使用其实现的major下的最高minor版本进行处理。接收者可以假设拥有(其不支持地)更高minor版本的消息能够完全向后兼容,能够被实现了相同major的接收者处理。

3 Message Format

所有的HTTP/1.1都由一个起始行以及一系列与Internet Message Format格式类似的字节组成:0个或多个首部字段,一个表示首部结束的空行和可选的消息体。

代码语言:javascript
复制
HTTP-message   = start-line
               *( header-field CRLF )
               CRLF    #首部结束时会有一个回车换行
               [ message-body ]

正常解析HTTP消息的步骤为:将起始行读取到一个结构体中,将每个首部字段读取到对应字段名的哈希表中(直到表示首部结束的空行),然后使用解析的数据判断是否存在消息体。如果存在消息体,则将其视为字节流,并读取等同于消息体长度的字节长度(有length首部字段)或直到连接关闭。

接收者必须使用US-ASCII编码的超集来解析HTTP消息。将HTTP消息解析为unicode字符流而不考虑其特定编码,会由于字符串处理库处理包含LF (%x0A)的无效多字节符序列的各种方式而造成安全漏洞。基于字符的解析可能仅适用于协议元素已经从消息中提取出来的场景,如一个已经从消息中解析出来且已经划分了独立字段的首部字段值。

一个HTTP消息可能会被解析为用于增量处理或转发到下游的流。然而,接收者不能依赖于部分消息的增量传递,由于一些实现中会为了网络传输效率,安全校验或修改消息载体而缓存或延时转发消息。

发送者不在起始行和第一个首部字段之间发送空白字符,接收者在收到一个在起始行和第一个首部字段之间存在空白字符的消息必须将其视为无效的消息并拒绝,或跳过每个包含前置空白字符的行(即忽略本行以及后续包含前置空白字符的行,直到接收到正常格式的首部字段或到达首部的末尾)。

请求中出现空白字符可能用于尝试诱使服务端忽略该字段或将该之后的内容视为一个新的请求,无论那一种都会导致安全漏洞(如果该请求链上的其他实现将其解释为其他的结果)。同样地,在响应中出现这类空白字符可能会被一些客户端忽略或导致另外一些客户端停止解析。

3.1 Start Line

HTTP消息可能是从客户端发往服务端的请求,或从服务端发往客户端的响应。两种类型的消息在语法上只有在起始行上有所区分,即是否是请求行或响应行,以及确定消息体长度所使用的算法。

理论上,客户端也可以接收请求,服务端也可以接收响应。但在实现中,服务端实现为仅接收请求(响应定义为未知或无效的请求方法),且客户端实现为仅接收响应。

代码语言:javascript
复制
start-line     = request-line / status-line
3.1.1 Request Line

请求行由一个方法字段开始,后面跟一个空白字符(SP),请求目标,又一个空白字符(SP),协议版本以及CRLF(回车换行)。

代码语言:javascript
复制
request-line   = method SP request-target SP HTTP-version CRLF

方法字段作用于目标资源,请求方法区分大小写。

代码语言:javascript
复制
method         = token

本规范定义的请求方法和相关的HTTP方法注册机构以及定义新方法的考量都可以在RFC7231 4中找到。

请求目标(request-target)定义了作用于请求的目标资源,参见5.3章节。

接收者通常会将请求行按照空白字符切割为各个组件(3.5章节),3个组件中不能包含空白字符。不幸的是,一些用户代理不能够正确地编码或在相关的超文本中没有包含空白字符,这样会导致在请求目标中混入一些非法的字符。

接收到无效的请求行应该返回400(错误请求)或301(永久重定向)重定向(重定向到正确编码的请求目标)。接收者不应该尝试自动修正并处理请求(非重定向),无效的请求行可能是被蓄意构造来穿透请求链上的安全防护。

HTTP没有定义请求行的长度(2.5章节)。当服务端接收到大于所实现的长度的方法时应该响应501(Not Implemented)状态码,当服务端接收到大于其可以处理的URI长度的请求目标必须返回414(URI Too Long)状态码。

在实践中,请求行有各种特殊的限制。建议HTTP的发送端和接收端能够处理最少8000字节的请求行

3.1.2 Status Line

响应消息的首行为状态行,包含协议版本,一个空白字符(SP),状态码,又一个空白字符,以及一个可能存在的用于描述状态码的文本字段,最后以CRLF终结。

代码语言:javascript
复制
status-line = HTTP-version SP status-code SP reason-phrase CRLF

状态码为一个3位整数字组成的编码,描述了服务端尝试理解和满足客户端请求的结果。其他的响应消息用于按语义解析该状态码(RFC 7231 6)。

代码语言:javascript
复制
status-code    = 3DIGIT

原因短语(reason-phrase)存在的目的仅仅为了给状态码提供文本描述,通常不会遵从早期的互联网应用协议(通常用于与客户端进行文本交互)。 客户端应该忽略原因短语的内容。

代码语言:javascript
复制
reason-phrase  = *( HTAB / SP / VCHAR / obs-text )
3.2 Header Fields

每个首部字段都包含一个区分大小写的字段名称,后接":",可选的前置空白字符,字段值和可选的尾部空白字符

代码语言:javascript
复制
header-field   = field-name ":" OWS field-value OWS   #OWS(optional whitespace)

field-name     = token
field-value    = *( field-content / obs-fold )
field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
field-vchar    = VCHAR / obs-text

obs-fold       = CRLF 1*( SP / HTAB )
               ; obsolete line folding
               ; see Section 3.2.4

字段名称对应使用相同语义的字段值,如Date(RFC 7231 7.1.1.2)字段包含消息出现的地区时间戳。

3.2.1 Field Extensibility

首部字段时完全可扩展的:没有限制新的字段名称(每个对应一个新的语义),且没有限制给定消息中的首部字段的数目。本规范定义了现有的字段,其他规范定义的不在本文范围内。

新的首部字段需要满足:接收者能够理解该字段,可能用于覆盖或增强先前定义的首部字段,定义请求中的前提条件,或改进响应的含义。

除非一个字段位于Connection(6.1章节)首部字段中,或明确配置不能转发或修改该字段,否则代理必须转发未识别的首部字段。其他接收者应该忽略未识别的首部字段。这种需求可以增强HTTP的功能,而无需升级协议或部署中间设备。

所有的首部字段都应该通过IANA的"Message Headers" 注册中心进行注册(RFC 7231 8.3)

3.2.2 Field Order

不同名称的首部字段的的接收顺序无关紧要,然而,通常会首先发送控制数据,如请求中的Host和响应中的Date,因此在实现中可以根据需要决定需要优先处理哪些内容。服务端不能在接收到完整的请求之前去操作目标资源,因为后续的首部字段可能会包含一些限制条件,如身份认证,或恶意修改的重复的首部字段。

发送端不能在一个消息中生成相同名称的首部字段,除非该首部字段的字段值使用逗号分割(如,#(values))或首部字段是知名字段(见下文)。

接收者可能会将具有多个相同名称的首部字段按照该字段名称的接收顺序组合为一个"字段名称":"字段值"。按照接收顺序进行组合对接收者如何解析组合字段值至关重要,代理在转发消息时不能修改这些字段值的顺序。

注:实现中Set-Cookie首部字段经常会在响应消息中出现多次,且不会使用列表,这种方式违背了上述具有多个相同首部字段名称的需求。由于它不能被组合为一个单独的字段值,接收者在处理首部字段时应该将Set-Cookie视为例外。

3.2.3 Whitespace

本规范使用3种规则来定义如何使用线性的空白字符:OWS (optional whitespace), RWS (required whitespace), 和BWS ("bad" whitespace)

OWS规则用于0个或多个线性空白字节。对于使用OWS来提高可读性的协议来说,发送端应该使用单个SP作为OWS,否则发送端不能生成OWS(除非需要在本地过滤中清除无效或不需要的协议元素)

RWS规则用于使用至少一个线性空白字符来分开各个字段。发送端不应该使用SP作为RWS

BWS规则用于由于历史原因所允许的OWS,发送端不能在消息中生成BWS。接收端必须将其解析为错误的空白并在解析协议元素前移除。

代码语言:javascript
复制
OWS            = *( SP / HTAB )
               ; optional whitespace
RWS            = 1*( SP / HTAB )
               ; required whitespace
BWS            = OWS
               ; "bad" whitespace
3.2.4 Field Parsing

消息的解析采用通用的算法,且不同的首部字段名称对应不同的解析算法。字段的内容在后续的消息解析前(通常在消息的整个首部处理完前)不会被解析。因此本规范没有使用之前版本中的ABNF规则来定义每个"字段名称:字段值"对,相反,本规范使用ABNF规则来规定每个已经注册的字段名称,并定义了字段名称对应的字段值的语法。

首部字段名称和冒号之间不允许有空白字符。在过去,对这类空白字符的不同处理可能会导致请求路由或响应处理中的安全漏洞。服务端必须拒绝任何在首部字段名称和冒号之间包含空白字符的请求消息,并返回400响应。代理不能在转发消息到下游前移除任何响应消息中的这类空白字符。

一个字段值可能会包含前置空白字符(OWS)或尾部空白字符;字段值的单个前置SP用于提供一致的可读性;不包含前置和尾部空白字符的字段值:如果空白字符出现在一个字段值的首个非空白字符前或出现在一个字段值的最后一个非空白字符后,接收者在解析首部字段时应该移除该字段值。

历史上,HTTP首部字段可以被扩展到多行,额外的行首至少包含一个空格或水平制表符(obs-fold)。本规范废除了除message/http media(8.3.1章节)类型外的行折叠(即包含任一匹配obs-fold规则的字段值)。发送端不能生成非message/http media类型的行折叠。

当服务端接收到消息中包含非message/http的obs-fold的请求时,必须拒绝该请求,并返回400响应,最好给出不可接受废弃的行折叠的解释;或者使用一个或多个SP字节替换接收到的obs-fold,并进行解析或转发到下游。

当代理或网关接收到消息中包含非message/http的obs-fold的请求时,必须丢弃该消息,并发送502(Bad Gateway)响应,最好给出不可接受废弃的行折叠的解释;或者使用一个或多个SP字节替换接收到的obs-fold,并进行解析或转发到下游。

当用户代理接收到消息中包含非message/http的obs-fold的请求时,许使用一个或多个SP字节替换接收到的obs-fold,并进行解析。

历史上,HTTP允许content 字段包含ISO-8859-1字符集的文本,支持的其他字符集定义在RFC2047中。实现中,大多数HTTP首部字段值仅仅使用了US-ASCII字符集的子集。新定义的首部字段应该限制其字段值使用US-ASCII字符。接收者应该将其他content (obs-text)字段中的字节视为不透明数据(opaque data)。

3.2.5 Field Limits

如2.5章节中说明,HTTP并没有实现限制每一个首部字段长度,也没有限制整个首部的长度。但在实现中对特定的首部字段有各种各样的限制,具体限制取决于特定的字段语义。

当服务端接收到长度大于其期望值的某个或某些请求首部字段时,必须返回一个合适的4xx(Client Error)状态码。忽略这类首部可能会导致服务端遭受夹带攻击(9.5章节)。

当客户端接收到长度大于其期望的首部字段时,可能会丢弃或截断(如果该字段的语法允许在不修改消息响应语义的前提下忽略丢弃的值)。

3.2.6 Field Value Components

大多数HTTPs首部字段值采用通用的句法组件(token, quoted-string, 和comment),使用空白字符或指定的字符分割。分割字符选自US-ASCII字符集(DQUOTE 和"(),/:;<=>?@[]{}"),且不能被token所采用。

代码语言:javascript
复制
token          = 1*tchar

tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
               / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
               / DIGIT / ALPHA
               ; any VCHAR, except delimiters

如果文本字符串使用双引号引用,它将被解析为一个单独的数值。

代码语言:javascript
复制
quoted-string  = DQUOTE *( qdtext / quoted-pair ) DQUOTE
qdtext         = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
obs-text       = %x80-FF

某些HTTP首部字段可以包含使用圆括号包围的注释文本(Comments )。Comments仅能用于使用"comment"作为字段值的一部分的字段中。

代码语言:javascript
复制
comment        = "(" *( ctext / quoted-pair / comment ) ")"
ctext          = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text

反斜杠可以在quoted-string和comment结构中作为单字节引用机制。接收者在处理quoted-string的值时必须处理quoted-pair,就好像quoted-pair被反斜杠后面的八位字节所取代(反斜杠的引用只需要一个斜杠即可,此时quoted-pair就是一个斜杠)。

代码语言:javascript
复制
quoted-pair    = "\" ( HTAB / SP / VCHAR / obs-text )

除非需要使用引用引号和字符串中出现反斜杠这两种情况,否则发送者不应该在quoted-string中生成quoted-pair;除非需要引用圆括号["(" 和")"]和comment中出现反斜杠这两种情况,否则发送者不应该在comment中生成quoted-pair。

3.3 Message Body

HTTP消息体用来携带请求或响应的有效载体。除非使用了transfer coding(3.3.1章节),否则消息体就等同于有效载体。

代码语言:javascript
复制
message-body = *OCTET

请求和响应中的消息体的规则不相同。

请求中是否出现消息体由 Content-Length 或Transfer-Encoding header首部字段标识。请求的消息框架独立于使用的方法(method )语义,即使方法中没有定义消息体的用途。

响应中是否出现消息体由需要响应的请求方法和响应状态码决定。对HEAD方法的响应不能包含消息体,因为响应中的首部字段(如Transfer-Encoding, Content-Length等)仅表示服务端的这些字段的值。对Connect请求方法(RFC7231 4.3.1)发送2xx响应会切换到隧道模式,此时不会携带消息体。所有的1xx204304响应都不会携带消息体。除此之外的响应都会携带消息体,即使消息体长度为0。

3.3.1 Transfer-Encoding

Transfer-Encoding 首部字段按顺序列出了已经或后续应用于有效载体的传输编码名称。传输编码定义在第4章节.

代码语言:javascript
复制
Transfer-Encoding = 1#transfer-coding

Transfer-Encoding与MIME格式的Content-Transfer-Encoding字段相似,Content-Transfer-Encoding用于在7-bit传输服务上进行二进制数据的安全传输,然而安全传输在8bit-clean传输协议下会有所不同。在HTTP场景下,Transfer-Encoding主要用于精确界定动态生成的载体(payload),以及区分载体编码(这类编码通过对字符的处理来提升传输效率或传输安全)。

接受方必须能够解析chunked(4.1章节)类型的Transfer-Encoding,因为chunked在消息载体长度未事先指定时起到了至关重要的作用。发送放不能在一个消息体中发送多个chunked(即不能对一个已经经过chunked处理的消息再次进行chunked处理)。如果对一个响应载体使用了除chunked类型外的Transfer-Encoding,那么发送方必须将chunked放到Transfer-Encoding的最后一个,或通过断开连接终止消息发送。如:

代码语言:javascript
复制
Transfer-Encoding: gzip, chunked

表示消息载体使用gzip压缩,然后使用chunked编码消息体。

与Content-Encoding不同,Transfer-Encoding属于消息的一部分,而非对消息的描述。任何请求/响应链的接收者都可以解码接收到的数据,或对消息体增加新的Transfer-Encoding。编码参数的额外信息可以定义在首部的其他字段(本规范未定义此功能)。

Transfer-Encoding可以用于HEAD请求的响应或GET请求的304响应(Section 4.1 of [RFC7232])中(这两种响应都不包含消息体),表示如果请求使用了无长度限制的GET方法,server端会对消息体进行编码传输。这种通知方式不是必要的,原因是响应链的接收者可能去除掉它们不需要的传输编码。

server不能在状态码为1xx(Informational)或204(No Content)的响应中使用Transfer-Encoding,且不能在对CONNECT请求的所有2xx(Successful)响应中使用Transfer-Encoding(Section 4.3.6 of [RFC7231])

Transfer-Encoding在HTTP/1.1中加入。HTTP/1.0无法处理传输编码载体。除非client实现知道server可以处理HTTP/1.1的请求,否则请求中不能包含Transfer-Encodig;除非server端接收到的请求指定了HTTP/1.1(或以上版本),否则不能发送包含Transfer-Encoding的响应。

server端接收到的请求中包含无法处理的传输编码时,应该返回501响应。

3.3.2 Content-Length

当一个消息没有Transfer-Encoding首部字段时,可以使用Content-Length来提供有效载体的长度,为8个字节的十进制数字。对于包含有效载体的消息,Content-Length字段值提供了消息体结束的信息;对于不包含有效载体的消息,Content-Length表示使用的representation 的长度,如:

代码语言:javascript
复制
Content-Length: 3495

当发送端的消息包含Transfer-Encoding首部字段时,不能包含Content-Length。

当发送端不使用Transfer-Encoding且请求方法表示其封装了有效载体时,用户代理应该在请求消息中发送Content-Length。例如Content-Length首部字段通常在POST请求中发送,值为0(表示没有有效载体)。当请求消息不包含有效载体,且请求方法本身不期望包含载体时,用户代理不应该发送Content-Length首部字段。

server可能在HEAD请求的响应中包含Content-Length首部字段。如果同一请求使用了GET方法,除非响应中Content-Length的字段值等于响应的有效载体的长度,否则server不能在响应中发送Content-Length。

server可能在带条件的GET请求的304响应中包含Content-Length首部字段;除非响应中Content-Length的字段值等于(对同一请求的)状态码为200的响应的有效载体的长度,否则server不能在响应中发送Content-Length。

server不能在状态码为1xx或204的响应中使用Content-Length首部字段。server不能在对CONNECT请求的任何状态码为2xx的响应中使用Content-Length首部字段。

除了上述场景外,当不存在Transfer-Encoding时,如果原始server端在发送完整的首部前知晓有效载体长度,则应该包含Content-Length首部字段,这样允许下游接收者衡量传输进度,知道什么时候接收完一个消息以及是否需要重用现有连接。

任何大于或等于0的Content-Length字段都是有效的。由于没有对载体的长度进行事先约束,接收者必须能够处理可能导致整型溢出的长度值。

如果接收到的消息有多个Content-Length首部字段包含相同的数值,或一个Content-Length首部字段包含多个数值(如:"Content-Length: 42, 42"),表示生成了多个Content-Length首部字段或合并了上游消息处理,这种情况下,接收者要么拒绝该消息,要么在确定消息体长度前使用有效的Content-Length字段替换重复的字段,要么直接转发该消息。

Content-Length值大于实际数据长度时,会导致服务端/客户端读取到消息结尾后, 一直等待下一个字节直至超时.

3.3.3 Message Body Length

消息体的长度取决于下面的某一规则(优先级降序):

  1. 任何对HEAD请求的响应,状态码为1xx,204或304的响应都在首部字段的空行后结束,无论消息中是否出现了首部字段,这类消息不能带消息体。
  2. 任何对CONNECT请求的2xx响应表示会在首部之后将连接切换到隧道模式。客户端必须忽略接收到的消息中的Content-Length或Transfer-Encoding首部字段。
  3. 如果出现了Transfer-Encoding首部字段,且chunked传输编码为最后一个编码,消息体的长度由解码的chunked数据决定(接收端会读取并解析,直到表示消息结束的chunked数据)。 如果响应中出现了Transfer-Encoding首部字段,且chunked传输编码不是最后一个编码,接收端会一直读取数据,直到连接关闭。如果请求中出现了Transfer-Encoding首部字段,但chunked传输编码不是最后一个编码,此时无法确定消息体的长度,服务度必须响应400状态码并关闭连接。 如果消息中同时出现了Transfer-Encoding和Content-Length首部字段,Transfer-Encoding会覆盖Content-Length。这类消息可能会尝试夹带请求(9.5章节)或分裂响应(9.4章节),应该被当成错误处理。发送端不能在将消息转发到下游前移除接收到的Content-Length字段。
  4. 如果接收到的消息中没有Transfer-Encoding,且包含多个不同字段值或一个非法字段值的Content-Length首部字段,则该消息是无效的,接收者应该将其视为不可恢复的错误。如果这是一个请求消息,服务端必须返回400状态码并关闭连接。如果这是一个代理接收到的响应,则代理必须关闭到server端的连接,丢弃接收到的响应,并给客户端返回502(Bad Gateway)响应。如果这是一个用户代理接收到的响应,用户代理必须关闭连接并丢弃接收到的响应。
  5. 如果首部字段仅出现了Content-Length(无Transfer-Encoding),则其十进制数值表示了消息体的字节长度。如果发送端关闭连接或接收端在接收到确定长度的数据前超时,则接收端必须将该消息视为未完成的消息,并关闭连接。
  6. 如果是一个请求消息,且不符合上面的任一条件,则消息体长度为0。
  7. 此外,如果是一个不带声明消息长度的响应,则消息体的长度以服务端关闭连接前接收到的数据为准。

由于没有办法确定由网络失败等原因导致接收到的(close-delimited)消息是否完整,服务端在生成消息时应该尽量采用编码或指定消息长度。close-delimited特性主要用于向后兼容HTTP/1.0。

服务端可能会拒绝包含消息体,但没有Content-Length的消息,并返回411响应(Length Required)。

除非使用了非chunked的传输编码,否则客户端在发送带消息体的请求时应该使用有效的Content-Length(如果事先知道消息长度),而非直接使用chunked传输编码,因为一些现有的服务会对chunked返回411状态码(即使这些服务能够识别chunked传输编码)。通常是因为这些服务需要通过要求带有content-length的网关进行调用,且服务端不能或不期望在处理前缓存整个请求。

用户代理在发送包含消息体的请求时必须发送有效的Content-Length首部字段(如果不知道服务端是否能够处理HTTP/1.1或之后的版本,服务端的版本可以通过指定用户配置或从之前的响应中获得)。

如果接收到线路上最后一个请求对应的响应,且后续需要读取一些额外的数据,用户代理可能会丢弃这些额外的数据或尝试确定这些数据是否属于前面的响应的一部分(可能前面消息中的Content-Length不正确)。客户端不能处理,缓存或将这些额外的数据作为独立的响应进行转发,这种行为可能会导致缓存中毒(cache poisoning )。

3.4 Handling Incomplete Messages

服务端在遇到由于请求取消或异常超时导致的未完成的请求消息时,因该在关闭连接前发送一个错误响应。

客户端在接收到由于连接过早关闭或解码chunked传输编码失败造成的未完成的响应消息时,必须将该消息标记为未完成的消息。对未完成的响应的缓存定义在RFC7234 3中。

如果一个响应在首部中间断开,且状态码依赖首部字段来传达完整的响应意图,此时客户端不能假设这种消息传达了正确的意图,客户端可能会重复先前的请求来决定后续的动作。

如果没有接收到用于结束编码的0长度(CRLF)的chunk,则使用chunked传输编码的消息会被视为未完成的消息。如果接收到的数据小于Content-Length的值,则使用Content-Length的消息被视为未完成的消息。没有使用chunked传输编码且没有使用Content-Length的响应长度取决于连接的关闭,即如果接收到的首部是完整的,则将接收到的消息视为完整的消息。

3.5 Message Parsing Robustness

老的HTTP/1.0用户代理可能会在POST请求之后发送额外的CRLF,作为一种对某些早期应用无法读取使用非行尾结束(line-ending)的消息内容的解决方法。HTTP/1.1用户代理不能在请求之后添加CRLF。如果需要使用行尾结束请求消息体,则用户代理必须将结束的CRLF作为消息体长度的一部分。

为了健壮性,服务端在接收并解析请求行时应该忽略至少一个请求行之前的空行(CRLF)。

虽然起始行和首部字段的行终结标记为CRLF,但接收者可能会将LF作为行终结,并忽略CR。

虽然请求行和状态行的语法规则要求每个组件元素由一个单独的SP字节分开,但接收者可能会解析除CRLF外的以空格分割的单词,将任何格式的空白字符视为SP分割器,并忽略前置和尾部空白字符。这类空白字符包含一个或多个下面字节:SP,HTAB,VT(%x0B),FF(%x0C),或只有CR。然而,相对宽容的解析可能在消息有多个接收者,且每个接收者都有其独特的健壮性解析方式时造成安全漏洞(9.5章节)。

当一个服务端仅接受HTTP请求消息或从起始行推断出为HTTP请求的消息,且其接收到与HTTP消息语法不匹配的一系列字节时(非上述罗列的健壮性异常)时,应该返回400响应。

4 Transfer Codings

Transfer Codings是一种用于消息载体在网络上进行安全传输的编码。它不同于用于消息本身编码而非描述传输的的content coding。

代码语言:javascript
复制
transfer-coding    = "chunked" ; Section 4.1
                   / "compress" ; Section 4.2.1
                   / "deflate" ; Section 4.2.2
                   / "gzip" ; Section 4.2.3
                   / transfer-extension
transfer-extension = token *( OWS ";" OWS transfer-parameter )

参数格式为name或name=value对

代码语言:javascript
复制
transfer-parameter = token BWS "=" BWS ( token / quoted-string )

所有的transfer-coding都区分大小写,并使用8.4章节中定义的HTTP Transfer Coding registry进行注册,并在TE(4.3章节)或Transfer-Encoding(3.3.1章节)首部字段中使用。

4.1 Chunked Transfer Coding

下面是是个chunked的例子,tailer的实体位于最后一个chunked消息的后面。

代码语言:javascript
复制
HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked
Trailer: Expires

7\r\n 
Mozilla\r\n 
9\r\n 
Developer\r\n 
7\r\n 
Network\r\n 
0\r\n 
Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n
\r\n

Chunked 传输编码封装了消息有效载体,使之能够以一系列的chunked方式进行传输,每个chunk都有自身的长度指示符。Chunked方式能够传输未知长度的消息,并使发送端能够保持长连接以及接收端能够知道何时接受完一个完整的消息。

下面的trailer是可选的

代码语言:javascript
复制
chunked-body   = *chunk
               last-chunk
               trailer-part
               CRLF

chunk          = chunk-size [ chunk-ext ] CRLF
               chunk-data CRLF
chunk-size     = 1*HEXDIG
last-chunk     = 1*("0") [ chunk-ext ] CRLF

chunk-size是一个16进制的数字,表示chunk-data的字节数,当接收到的chunked传输编码的chunk-size为0(可能跟在trailer之后)且以空行结束时,表示接收到一个完整的Chunked 传输编码。

接收者必须能够解析chunked 传输编码。

4.1.1 Chunk Extensions

chunked编码方式允许在chunk-size之后携带一个或多个用于支持chunk metadata(如签名或哈希),中间消息控制信息或任意长度的消息体的chunk扩展。

代码语言:javascript
复制
chunk-ext      = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )

chunk-ext-name = token
chunk-ext-val  = token / quoted-string

chunked编码与特定的连接相关,可能在接收者(或中间设备)移交上层应用处理该扩展前被移除或记录。因此chunk扩展通常局限于特定使用"长轮询"(即客户端和服务端可以互相知道所使用的chunk扩展的意图)的HTTP服务或附加在端到端的安全连接上。

接收者必须忽略未识别的chunk扩展。服务端应该(当接收到一个合理的针对大量服务的请求)限制接收到的请求中chunk扩展的总长度。同时应该限制其他消息字段的长度和超时时间,并在超过阈值之后返回4xx的响应。

4.1.2 Chunked Trailer Part

Trailer 允许发送者在最后一个chunked消息之后包含一些额外的字段,用于支持在消息发送时可能自动生成的metadata,如消息完整性校验,数字签名或处理结束的状态等。Trailer字段与首部字段相同,但它位于chunked trailer而非消息首部中。

代码语言:javascript
复制
trailer-part   = *( header-field CRLF )

发送端不能生成包含消息框架(如Transfer-Encoding和Content-Length),请求修正(如controls和conditionals rfc7231 5),响应控制数据(rfc7231 7.1)或决定如何处理载体(如Content-Encoding, Content-Type, Content-Range, 和Trailer)的railer

当接收到一个携带非空trailer的chunked消息时,接收者可能会像处理消息首部一样处理这些字段。接收者必须忽略(或认为是一个错误)禁止发送在trailer中的消息(这类消息可能会通过外部安全过滤泄露出去) 。

除非请求通过TE首部字段指明某些trailers是允许的,否则服务端不能生成它认为可以为用户代理使用的trailer字段。如果TE不包含trailers,服务端应该假设丢弃发送到用户代理的trailer字段。该需求允许中间设备向HTTP/1.0的接收端转发解码(chunked)的消息(而无需缓存整个响应)。

4.1.3 Decoding Chunked

解析chunked transfer coding的过程可以使用如下伪代码表示:

代码语言:javascript
复制
length := 0
read chunk-size, chunk-ext (if any), and CRLF
while (chunk-size > 0) {
    read chunk-data and CRLF
    append chunk-data to decoded-body
    length := length + chunk-size
    read chunk-size, chunk-ext (if any), and CRLF
}
read trailer field
while (trailer field is not empty) {
    if (trailer field is allowed to be sent in a trailer) {
        append trailer field to existing header fields
    }
    read trailer-field
}
Content-Length := length
Remove "chunked" from Transfer-Encoding
Remove Trailer from existing header fields
4.2 Compression Codings

下面定义的编码方式可以用于压缩消息载体

4.2.1 Compress Coding

compress编码是一种Lempel-Ziv-Welch (LZW)编码方式,通常由UNIX文件压缩程序compress生成。接收者应该将x-compress等同于compress

4.2.2 Deflate Coding

Deflate编码是一种包含deflate压缩数据流(rfc1951)的zlib数据格式(rfc1950),zlib数据格式使用了Lempel-Ziv (LZ77)压缩算法和Huffman编码。

注意:一些非一致性的实现会发送非zlib封装的deflate压缩数据。

4.2.3 Gzip Coding

gzip编码是一种带有32位CRC校验的LZ77 编码,通常由gzip文件压缩程序生成。接收者应该将x-gzip等同于gzip。

4.3 TE

请求中的TE首部字段用于指明客户端期望响应使用的除chunked外的传输编码,以及客户端期望接收到的chunked传输编码中的trailers字段。

TE的字段的值包含使用逗号分割的传输编码名称,每个值含可选的参数和/或关键字"trailers"。客户端不能在TE中发送chunked传输编码名称,因为所有的HTTP/1.1接收者都实现了chunked。

代码语言:javascript
复制
TE        = #t-codings
t-codings = "trailers" / ( transfer-coding [ t-ranking ] )
t-ranking = OWS ";" OWS "q=" rank
rank      = ( "0" [ "." 0*3DIGIT ] )
          / ( "1" [ "." 0*3("0") ] )

TE的使用例子如下:

代码语言:javascript
复制
TE: deflate
TE:
TE: trailers, deflate;q=0.5

出现"trailers"关键字表示客户端以及所有下游客户端允许接收到带trailer字段的chunked传输编码。对于中间设备,其表示:a) 所有的下游客户端都可以接收转发响应中的trailer字段;b)中间设备将尝试缓存下游接收者方向的响应。HTTP/1.1没有规定任何chunked响应的长度,因此中间设备应该保证缓存了整条响应。

当允许多种传输编码时,客户端可能会使用"q"(区分大小写)参数来按照优先级排序。排序值从0到1,如0.01最低优先级,而1位最高优先级,0则表示不可接受。

如果TE字段值为空,或没有出现TE字段,唯一可接受的传输编码为chunked。不携带传输编码的消息总是可接受的。

由于TE字段仅用于直连,发送TE的一段必须在Connection首部字段中发送TE连接选项,用于防止TE首部被不支持该语法的中间设备转发。

4.4 Trailer

当一个消息体使用chunked传输编码,且发送者期望在chunked消息末尾的trailer字段中携带metadata信息时,发送者应该在消息体前生成Trailer首部字段(说明了Trailer字段的位置)。这样在消息被接收到后,接收者可以在处理消息体前处理这些chunked metadata,特别是在消息以流方式传输,且接收者期望使用trailer中的metadata进行消息完整性校验时非常有用。

代码语言:javascript
复制
Trailer = 1#field-name

5 Message Routing

HTTP请求消息的路由由各个客户端基于目标资源而决定,如客户端的代理配置,建链或重用一个入站连接。对应的响应会通过相同的链路返回到客户端。

5.1 Identifying a Target Resource

HTTP广泛用于在多种应用中,从通用计算机到家庭应用。有些场景下,客户端的配置中的通信选项会被硬编码。然而,大多数HTTP客户端依赖于资源的类型和通用web浏览器的配置。

HTTP的通信由用户代理处于某种目的发起,这些目的通过请求语义(RFC 7231)和目标资源来表达。通常使用URI(2.7章节)代表目标资源,用户代理将会通过解析成绝对URI来获得目标URI。URI不包含相关的分片组件,因为分片组件仅用于客户端的处理(如一个分片组件可能表示某个网页文章的某一章节,它是由浏览器处理的)

5.2 Connecting Inbound

一旦确定URI后,客户端需要决定是否发送网络请求,以及请求的目的地。

如果客户端有满足请求的缓存,通过会将该请求直接发送到缓存所指的地址。

如果缓存不满足请求的条件,通常客户端会检查配置来决定是否有代理满足该请求。代理配置依赖其具体实现,但通过会通过URI前缀和/或认证进行匹配,代理本身通常也使用"http"或"https" URI标识。如果使用了代理,客户端会通过代理来建立或重用入站连接。

如果不使用代理,典型的客户端会启用handler规则,该规则通常会针对目标URI的scheme(http或https,ftp等),到对应目标资源的授权组件(authority)的连接,该实现由目标URI scheme相关的规范规定,类似于本规范的"http"和"https"。

HTTP的连接管理定义在第6章节。

5.3 Request Target

一旦建立了入站连接,客户端会发送带有(从URI衍生的)request-target的HTTP请求消息。当前有4种不同的request-target,取决于请求的方法和是否使用代理。request-target主要就是目标主机的地址+端口

代码语言:javascript
复制
request-target = origin-form
               / absolute-form
               / authority-form
               / asterisk-form
5.3.1 origin-form

最常使用的request-target就是orgin-form

代码语言:javascript
复制
origin-form    = absolute-path [ "?" query ]

直接(非通过CONNECT或server-wide OPTIONS )向源服务端发起请求时,客户端仅能使用绝对路径和目标URI的查询组件作为request-target。如果目标URI的路径组件为空,客户端必须在origin-form的request-target中使用"/"作为路径,同时也会发送Host首部字段。

例如,客户端期望检索服务端资源的如下:

代码语言:javascript
复制
http://www.example.org/where?q=now

直接访问源服务端会打开一个80的TCP端口,主机名为"www.example.org",发送的请求行为

代码语言:javascript
复制
GET /where?q=now HTTP/1.1
Host: www.example.org

后续为剩下的请求消息。

适用于直接请求服务端场景

5.3.2 absolute-form

客户端向代理发送请求(非通过CONNECT或server-wide OPTIONS )时,客户端必须使用absolute-form格式的request-target发送目标URI。

代码语言:javascript
复制
absolute-form  = absolute-URI

代理需要使用有效的缓存处理请求,或以客户端身份向下一个入站代理或直接连接到request-target指定的源服务端来执行相同的请求。转发消息的相关内容参见5.7章节。

使用absolute-form的request-line的例子如下:

代码语言:javascript
复制
GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1

为了允许在未来的某个版本的HTTP中将所有的请求转换为absolute-form格式,服务端必须接受absolute-form的请求,尽管HTTP/1.1的客户端仅会将其发送到代理。

适用于代理场景,此时GET之后需要完整的URI。但未来的所有请求可能都会采用这种规则

5.3.3 authority-form

authority-form仅用于CONNECT请求中(rfc7231 4.3.6)

代码语言:javascript
复制
authority-form = authority

当客户端使用CONNECT请求通过一个或多个代理发起建立隧道时,必须仅将目标URI的授权组件(不包含任何用户信息和"@"分隔符)作为request-target,如:

代码语言:javascript
复制
CONNECT www.example.com:80 HTTP/1.1

仅适用于通过CONNECT与服务端建立隧道时使用

5.3.4 asterisk-form

asterisk-form仅用于server-wide(整个服务范围内)的OPTIONS请求(RFC 7231 4.3.7)。

代码语言:javascript
复制
asterisk-form  = "*"

当一个客户端期望使用OPTION请求整个服务端,而非指定服务端某个特定名字的资源时,客户端必须使用"*"作为request-target,如:

代码语言:javascript
复制
OPTIONS * HTTP/1.1

如果代理接收到absolute-form的OPTION请求,且请求的URI路径为空,没有查询组件,那么请求链上的最后一个代理必须在转发该请求到指定源服务端时发送"*"的request-target。

如请求为:

代码语言:javascript
复制
OPTIONS http://www.example.org:8001 HTTP/1.1

最后一个代理在转发时为

代码语言:javascript
复制
OPTIONS * HTTP/1.1
Host: www.example.org:8001

后续会连接到主机"www.example.org"的8001端口。

仅用于OPTION请求,通常用于测试场景

5.4 Host

请求中的"Host"字段提供了目标URI的host和port信息,为server在单一IP地址上配置多个Host名称时区分请求来源。

代码语言:javascript
复制
Host = uri-host [ ":" port ] ;

client必须在所有的HTTP/1.1请求中包含该首部字段。如果目标URI包含认证组件,client发送的Host的值必须为认证组件的值(去除用户子组件以及"@"界定符)。如果缺少认证组件或未定义目标URI,则client发送的Host值必须为空。

由于Host字段对于处理请求至关重要,用户代理应该将该字段作为请求行的首个字段。

如从<http://www.example.org/pub/WWW/>发送GET请求时,请求行为:

代码语言:javascript
复制
GET /pub/WWW/ HTTP/1.1
Host: www.example.org

client必须在HTTP/1.1的请求中包含Host首部字段,即使request-target格式为absolute-form,这样会允许未实现Host首部处理的HTTP/1.0代理转发Host信息。

当一个代理接收到absolute-form的request-target,该代理必须忽略接收到的Host首部字段,并使用request-target中的host信息替换。代理转发这种请求前必须基于接收到的request-target生成一个新的Host字段值,而非直接转发接收的Host字段值。

由于Host首部字段扮演了应用层的路由机制,通常会被恶意软件攻击共享缓存或被重定向到非法的server。依赖于Host字段值或使用共享缓存的缓存key(而不校验拦截的连接是否对应有效的IP地址)的拦截代理(重定向请求到内部server)会比较薄弱。

server必须对所有缺少Host首部字段或包含多个Host首部字段以及包含非法Host首部字段的HTTP/1.1请求返回400状态码响应。

5.5 Effective Request URI

由于request-target通常仅包含用户代理的目标URI的一部分,服务端会将请求的目标重建为一个"effective request URI"来服务请求。该重建过程包含服务端的本地配置以及request-target中的信息,Host首部字段和连接上下文。

对于一个用户代理,effective request URI就是目标URI。

如果request-target是absolute-form,effective request URI就等同于request-target。否则effective request URI按照如下进行构建:

  • 如果服务端的配置提供了固定的URI scheme,则使用该scheme作为effective request URI。否则,如果在使用TLS的TCP连接上接收到请求,effective request URI的scheme为"https";如果没有使用TLS,则scheme为"http"
  • 如果服务端的配置提供了固定的认证组件,该认证将会用于effective request URI。如果没有提供固定的认证组件,如果request-target为authority-form,则effective request URI的认证组件与request-target相同。如果request-target非authority-form,且Host首部字段的值非空,则认证组件与Host的值相同。否则,认证组件使用与服务端配置的默认名称,如果连接的TCP接收端口不同于effective request URI对应的scheme的默认端口,此时会使用冒号":"将接收端口附加在认证组件之后。
  • 如果request-target为authority-form或asterisk-form,则effective request URI的组合路径和查询组件为空。否则其组合路径和查询组件与request-target相同。
  • 一旦按照上述条件确定了effective request URI的组件,会将绝对URI和scheme通过"://"进行组合,后续跟上组合路径和查询组件。

例1:下面为从一个非安全的TCP连接上接收到的消息:

代码语言:javascript
复制
GET /pub/WWW/TheProject.html HTTP/1.1
Host: www.example.org:8080

effective request URI为:

代码语言:javascript
复制
http://www.example.org:8080/pub/WWW/TheProject.html

例2:下面为从使用TLS的TCP连接上接收到的消息:

代码语言:javascript
复制
OPTIONS * HTTP/1.1
Host: www.example.org

effective request URI为:

代码语言:javascript
复制
https://www.example.org

接收到缺少Host首部字段的HTTP/1.0的请求时可能需要进行启发式搜索(如,针对某些特殊host进行校验)来猜测effective request URI的认证组件。

一旦构建了effective request URI,源服务端需要决定是否为接收到请求的链路上的URI提供服务。例如,请求可能被错误处理,恶意修改或意外破坏,这种情况下,接收到的request-target或Host字段可能不同于建立连接的主机或端口。 如果连接建来自一个可信的网关,则可以确定预期的不一致的场景,否则可能会导致穿透安全过滤,传输非公共内容或缓存中毒等。参见第9章。

5.6 Associating a Response to a Request

HTTP并没有为请求标记其对应的一个或多个响应。因此,它依赖于与请求相同的连接上的响应的接收顺序。一个请求对应多个响应消息的场景发生在一个或多个1xx响应(informational responses,RFC 7231 6.2)的场景下(在针对相同请求的最终响应前发送的响应)。

当客户端在一条连接上有多个待发送的请求时,必须按照发送顺序维护待发送的请求列表,且必须将每个接收到的响应消息与该连接上未接收到最终(非1xx)响应的(最高排序的)请求进行关联。

5.7 Message Forwarding

中间设备可以在处理HTTP请求和响应时扮演多种角色。一些中间设备可以用于提升性能和可靠性,其他可能用于接入控制和内容过滤。由于一个HTTP流类似于管道过滤(pipe-and-filter)架构,因此没有限制中间设备针对HTTP流的扩展功能。

不能作为隧道的中间设备必须实现Connection首部字段(6.1章节),并在转发时移除这些字段。

除非用于环路保护,否则中间设备不能将消息转发给自身。一个中间设备通常能够识别自身的服务名,包含别名。本地变量,或字符型IP地址,并直接响应达到自身的请求。

5.7.1 Via

Via首部字段用于给出用户代理和(请求的)服务端或源服务端和(响应的)客户端之间出现的中间协议和接收者,类似email(RFC 5322 3.6.7)的"Received"首部字段。Via可以用于跟踪转发的消息,避免请求环路,并在请求/响应链上指明发送者的协议功能。

代码语言:javascript
复制
Via = 1#( received-protocol RWS received-by [ RWS comment ] )

received-protocol = [ protocol-name "/" ] protocol-version
                         ; see Section 6.7
received-by       = ( uri-host [ ":" port ] ) / pseudonym
pseudonym         = token

多个Via字段值表示转发该消息的每个代理或网关。每个中间接收者都会附加自身关于如何接收消息的信息,这样最终信息会按照转发者的顺序展示。

代理必须按照下面的规范为每个转发的消息附加合适的Via首部字段。HTTP到HTTP的网关必须在每个入站请求中附加合适的首部字段,有可能在转发的响应中附加Via首部字段。

对于每个中间设备,received-protocol表示上游消息发送者使用的协议版本。因此,Via字段记录并为下游接收者提供了请求/响应链上所宣告的协议,该功能可以为响应或后续的请求检测出可以安全使用哪些向后不兼容的特性。为了简洁,当接收到的协议为HTTP时会忽略protocol-name字段。

received-by通常是后续转发该消息的接收服务端或客户端的主机和可选端口。然而,如果真实的主机被认为是敏感信息时,发送端可能会使用假名代替。如果没有提供端口,接收者可能会认为其使用了默认的TCP端口(如果received-protocol中有TCP的话)。

发送端可能会在Via首部字段中生成一些有关接收者使用的软件,User-Agent和Server首部字段的注释。然而提供Via字段的所有注释都是可选的,接收者可能在转发消息前移除这些信息。

如,当请求通过HTTP/1.0的用户代理发送到一个名为"fred"的内部代理,该代理使用HTTP/1.1转发请求到公共代理"p.example.net",该公共代理会将请求最终转发到源服务端"www.example.com","www.example.com"接收到的请求中会有如下Via首部字段:

代码语言:javascript
复制
Via: 1.0 fred, 1.1 p.example.net

作为门户的中间设备如果使用了网络防火墙,除非明确要求,否则不应该转发防火墙域的主机的名称和端口。如果没有明确要求,这类型中间设备应该使用合适的假名替代防火墙内的主机信息。

如果Via首部字段中的多个表项使用的相同的接受协议,中间设备可能会将其组合成单一表项,如

代码语言:javascript
复制
Via: 1.0 ricky, 1.1 ethel, 1.1 fred, 1.0 lucy

可能被折叠为

代码语言:javascript
复制
Via: 1.0 ricky, 1.1 mertz, 1.0 lucy

除非所有的表项由相同的组织控制且所有的主机已经替换为合适的假名,否则发送端不应该组合Via中的表项。发送端不能组合使用不同received-protocol的表项。

5.7.2 Transformations

一些中间设备能够改变消息和以及消息体。例如,一个代理可能会为了节省缓存空间或减少低度链路上的流量而转变图片格式。然而当一些严格的应用执行这类转换时可能会导致隐藏的问题,如医学图片或科学数据研究等,特别是使用完整性检查或数字签名来校验接收到的数据的完整性时。

如果一个HTTP到HTTP的代理使用某种语义方式来修改消息(如以某种对源发送端或下游接收者至关重要的方式修改消息),则该代理被称为"transforming proxy"。例如,一个transforming proxy可能作为注释服务器(通过修改响应来添加本地注释数据库的信息),中间过滤器,格式转换器或私有过滤器。这类代理可能会被某些客户(或组织)使用。

如果一个代理接收到的request-target中的host name不是一个完全合格的域名,它可能会在转发该请求时,在host name中添加自身的域名。如果request-target包含完全合格的域名,则代理不能修改host name。

代理在转发到下一个入站服务器前不能修改接收到的request-target中的"absolute-path"和"query"(除可以使用"/"或"*"替换空的路径外)

代理可能通过应用修改消息或移除Transfer Coding(第4章节)。

代理不能修改包含no-transform cache-control(RFC 72345 5.2)指令的消息的载体(RFC 7231 3.3)。

代理可能会修改不带no-transform cache-control指令的消息载体。代理在修改载体后,必须在首部字段中添加warn-code为214("Transformation Applied")的首部字段 (如果不存在),这样在一个代理修改200的响应载体后,会通过将协议状态码为203(Non-Authoritative Information, RFC 7231 6.3.4)来通知下游接收者该消息已被修改。

除非定义的字段允许修改或由于隐私或安全的需要,否则代理不应该修改通信链上与终端相关的首部字段,如资源状态或所选择的表达(非载体)等。

6 Connection Management

HTTP消息独立于底层传输或session层的连接协议。HTTP假设其允许在可靠的传输之上,能够按顺序传输请求或响应。

5.2章节中所描述的,HTTP使用的连接协议由客户端以及目标URI决定,例如"http"的URL scheme表示其默认使用TCP/IP协议,默认端口80。客户端可以配置使用代理,其他协议和端口等。

HTTP的实现中增加了连接管理相关的操作,包括维护当前连接状态,建立新连接以及重用现有连接,处理接收到的消息,检测连接失败以及关闭两端连接等。大多数客户端会并行维护多条连接,其中可能有多个连接对应同一个服务。大多数服务在设计时会同时支持数千条连接,通过控制请求队列来实现合理利用以及检测DOS攻击。

6.1 Connection

"Connection"首部字段允许发送端设置当前连接的控制选项。为了避免下游接收者,当代理或网关接收到任何连接选项时,在转发前必须删除该字段。

当首部字段中除"Connection"还有其他字段用于支持控制当前当前连接,那么发送者必须在"Connection"首部字段中罗列对应的字段名称。proxy或gateway必须能够在转发一个消息前解析该消息中的每个connection-option,并移除连接选项中对应的首部字段以及"Connection"本身(或替换为转发者本身的连接选项)。

因此,"Connection"首部字段提供了一种区分中间接收者(逐跳)和(端到端)链路上所有接收者的声明方式,使得消息能够自描述,并允许未来部署特定连接的扩展时不必担心被老的中间设备盲目转发。

"Connection"首部字段的语法如下:

代码语言:javascript
复制
Connection        = 1#connection-option
connection-option = token

"Connection"选项区分大小写。

发送端不能在用于给所有有效载体接收者使用的首部字段中填加连接选项。如Cache-Control不能用于连接选项。

如果一个连接选项不需要参数,那么它就不需要对应一个connection-specific首部字段,因此连接选项不总是对应消息中的某个首部字段。相反,如果接收到的connection-specific首部字段没有对应的连接选项,通常表示该字段被中间设备错误转发了,接收者应该忽略该字段。

当定义一个新的连接选项时,设计者应该调查现有的首部字段名称并保证新连接选项不会与已有的首部字段名称冲突。定义一个新的连接选项时应该预留字段名称来携带与连接选项相关的额外信息。

"close"连接选项表示发送者希望在接收到响应后关闭连接,如:

代码语言:javascript
复制
Connection: close

请求或响应中的该字段都会表面发送者在完成请求/响应之后会关闭连接(6.6 章节)。

不支持长连接的客户端必须在每次请求消息中携带"close"连接选项。

不支持长连接的服务端必须在每次(状态码为非1xx的)响应中携带"close"连接选项。

6.2 Establishment

如何基于多种传输或session-layer建立连接不在讨论范围内。每条连接仅适用于一条传输链路。

6.3 Persistence

HTTP/1.1默认使用长连接,允许在一条连接上传输多个请求和响应。"close"连接选项表示在当前请求/响应之后断开连接。HTTP实现应该支持长连接。

接收方通过最新接收到的消息的协议版本以及"Connection"首部字段来决定是否建立长连接:

  • 如果出现了"close"连接选项,则在完成当前的响应之后关闭连接;
  • 如果接收到的协议为HTTP/1.1(或更新),则在当前响应之后保持连接;
  • 如果接收到的协议为HTTP/1.0,且出现了"keep-alive"连接选项,接收端不是代理且能够处理"keep-alive"选项,则在当前响应之后保持连接;
  • 连接在当前响应之后关闭

客户端可以在长连接上发送额外的请求信息,直到其接收到"close"连接选项或接收到不带"keep-alive"连接选项的HTTP/1.0响应。

为了保持长连接,该连接上所有的消息必须需要包含一个自定义的消息长度(即,非连接选项定义的)。服务端必须能够读取整个请求消息体(读取完所有请求)或在发送响应之后关闭连接(长连接上未读取完的数据可能被认为是下一个请求)。相应地,如果客户端需要在后续的请求中重用该连接,则其必须能够读取整个响应消息体。

代理服务器不能在HTTP/1.0之上维护长连接。

附录A 1.2中描述了与HTTP/1.0向后兼容的信息。

6.3.1. Retrying Requests

连接可以在任意时间被有意或无意地关闭。实现中应该预期到这种需求并能够从异步关闭事件中恢复。

当关闭一个入站连接时,客户端可能会打开一个新的连接并自动并重传被断开的请求(如果这些请求是幂等的)。代理不能自动重试非幂等的请求(实现中一般不会给代理配置哪些是幂等的哪些不是幂等,因此通常代理就不应该自动重试连接)。

除非用户代理有办法知道请求是幂等的,否则不能自动重试非幂等的请求。例如,用户代理可能通过设计或配置知道某个POST请求是安全的且可以进行自动重试。同样地,一个用户代理设计为根据版本控制执行操作,通过这种方式,用户代理可能能够通过检测目标资源版本的方式从部分失败中恢复,回滚或修复部分已经应用的修改,然后自动重试失败的请求。

客户端不应该字段重试失败的请求。

6.3.2 Pipelining

支持长连接的客户可能会"pipeline"(并行处理)请求(即不等待响应,同时发送多个请求),如果这些请求都包含安全方法,服务端在接收到之后可能会并行处理这些请求,但必须以接收到的请求的顺序发送响应报文。

如果在接收到对应的响应前连接已经关闭,"pipeline"请求的客户端应该重试这些没有获得响应的请求。在上一次连接关闭后,当client重试"pipeline"的请求时(上一次响应时服务端并没有明确关闭连接)会创建新的连接,由于上一次"pipeline"遗留的首个请求可能会再次导致连接问题,因此不能立即执行"pipeline"请求,

对"pipelining"来说幂等方法很重要,因为它会在连接失败后重试请求。除非用户代理有办法发现并从涉及"pipeline"顺序的部分失败中恢复,否则不能在非幂等方法返回请求状态码之前执行"pipeline"请求。

中间设备接收到"pipelined"请求时可能会在入站转发时"pipeline"这些请求(中间设备可以通过出站到用户代理的流量来判断请求是否被安全pipeline)。如果在接收到响应前入站连接失败,pipeline的中间设备可能会(在所有请求为幂等的条件下)顺序重试没有接收到响应的请求,否则中间设置应该立即转发所有接收到的响应并关闭相应的出站连接,这样出站的用户代理可以执行恢复操作。

6.4 Concurrency

客户端应该限制同一连接到一台服务器的连接的数目。

先前版本的HTTP协议规定了连接的最大数目,但在实现中,该限制并不适用于很多应用。因此,本规范并没有强制限制连接的特定数目,但建议客户端对打开的连接进行限制。

多条连接通常用于防止队头阻塞(head-of-line blocking)问题,即一个请求占用了服务端大量处理资源以及/或请求体过大导致阻塞相同连接上的后续请求。然而每条连接都会消耗服务端的资源,更进一步讲,使用多条连接会导致非预期的网络阻塞问题。

注意,服务端在遭受恶意或特定的队头阻塞攻击时,可能会拒绝流量,这样会导致客户端创建大量打开的连接。

6.5 Failures and Timeouts

服务端不再维护一条非活动的连接前会有一段超时时间。代理服务可能会设置更大的超时时间,因为客户端可能会通过相同的代理服务创建多条连接。使用长连接时不需要在客户端和服务端设置超时时间。

客户端或服务端使用超时时间时应该采用优雅关闭的方式关闭连接。实现时应该持续监控接收到关闭信号的(打开的)连接,并正常响应该信号。及时关闭两端的连接可以使系统回收资源。

客户端,服务端或代理可能在任意时间关闭连接。例如,当服务端关闭一个疑似"idle"状态的连接时,该连接上的客户端可能正在开始发送新的请求,在服务端看来,该"idle"状态的连接正在关闭;而在客户端看来,它的请求正在处理中。

服务端应该维持长连接,并允许使用底层传输的流控制机制来解决临时的高负载情况,而不应该关闭连接并期待通过客户端重试解决问题,这样可能会导致网络拥塞。

客户端在发送请求时应该通过监控网络连接检测到错误的响应。如果客户端收到表示服务端不期望接收消息体并关闭连接的响应,客户端应该立即停止传输消息体并关闭本端连接。

6.6 Tear-down

6.1章节提供了一个"close"连接选项,表示发送端期望在完成当前的请求/响应之后关闭连接。

客户端在发送"close"连接选项之后就不能在该连接上发送后续请求,且必须在获得到对应的响应之后关闭连接。

服务端在接收到"close"连接请求,并发送最后一个响应之后必须初始化关闭连接的操作。服务端不应该在该连接的最后一个响应中携带"close"连接选项。服务端不能处理该连接上的后续请求。

服务端在发送携带"close"的连接选项的响应之后必须初始化关闭连接操作,且不能处理该连接上的后续请求。

客户端在接收到"close"选项之后必须停止在该连接上发送请求,并在接收到携带"close"的响应之后关闭连接。如果此时已经在该连接上发送了其他(pipeline)请求,客户端不应该假设这些请求可以被服务端处理。

如果服务端立即关闭了TCP连接,此时客户端可能不会获得最后一个HTTP响应。如果服务端从一个完全关闭的连接上接收到了客户端的数据(如客户端在接收到服务端响应之前发送的请求),此时服务端的TCP栈会发送reset报文给客户端,不幸的是,该reset报文会清除客户端发送缓存中未确认的数据。

为了避免TCP reset报文造成的问题,服务端通常会采用分阶段式关闭连接。首选,服务端会通过仅关闭(读/写)连接的写端来进入TCP半关闭状态,此时服务端会继续在该连接上读取客户端的数据,直到接收到客户端发送的关闭报文或服务端的TCP栈确认接收到客户端对服务端上一次响应的确认信息(ACK报文)。最后服务端会完全关闭连接。 即通过FIN报文实现连接关闭,而非RST报文

目前不清楚reset引发的问题是否仅限于TCP协议。

6.7 Upgrade

"Upgrade"首部字段用于提供一种在同一条连接上将HTTP/1.1转变为其他协议的简单机制。客户端可能会在该字段中发送一系列的协议来让服务端在发送最终响应前按照(优先级下降的方式)切换到一个或多个协议。当服务端期望继续使用当前协议时,可能会忽略"Upgrade"首部字段。不能使用"Upgrade"来强制切换协议。

代码语言:javascript
复制
Upgrade          = 1#protocol

protocol         = protocol-name ["/" protocol-version]
protocol-name    = token
protocol-version = token

服务端在发送101(切换协议)响应时,必须在该响应中指明该连接需要切换的协议;如果需要切换多个协议,服务端必须以协议从下层到上层的顺序给出切换的协议。服务端不能切换到客户端"Upgrade"首部字段中没有声明的协议。服务端可能会忽略客户端指定的协议顺序,并依照其他因素(如请求的特征或当前服务的负载)选择需要切换的协议。

服务端发送的426(需要升级协议)响应中必须包含"Upgrade"首部字段,该首部字段指定了接受的协议,以优先级降序排序。

服务端可能在任意响应中发送"Upgrade"首部字段来宣告其支持升级到罗列的协议(优先级降序)。

下面时客户端发送的一个例子:

代码语言:javascript
复制
GET /hello.txt HTTP/1.1
Host: www.example.com
Connection: upgrade
Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11

应用层的通信能力和特性依赖于切换的新协议。然而在立即发送完101响应后,服务端会继续响应先前的请求,就好像在新协议上接收到该请求(即在协议切换后,服务端还有一个待处理的请求,因此它要继续处理未完成的请求,而无需接收到重复的请求)。

例如,如果接收到的GET请求中包含"Upgrade"首部字段,且服务端决定切换协议,此时服务端会立即发送101响应并按照新协议响应先前的GET请求。这样会使连接快速升级到新协议而无需额外的交互。除非新协议能够处理接收到的消息,否则服务端不能切换协议。任何协议可以处理"OPTION"请求。

下面是一个响应的例子:

代码语言:javascript
复制
HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: HTTP/2.0

[... data stream switches to HTTP/2.0 with an appropriate response
(as defined by new protocol) to the "GET /hello.txt" request ...]

当发送"Upgrade"时,发送端必须发送一个包含"upgrade"连接选项的"Connection"首部字段(6.1章节),用于防止被不支持所列举协议的中间设备转发。服务端在接收到HTTP/1.0的请求时必须忽略"Upgrade"首部字段。

客户端在发送完整个请求消息前不能使用切换的协议(如,客户端不能在发送一个消息中途切换协议)。如果服务端同时接收到"Upgrade"和带有"100-continue"(rfc7231 5.1.1 )的"Expect"首部字段时,服务端必须在发送101(协议切换)响应前发送100(continue)响应。完成当前请求后才能切换协议,"100-continue"和后续请求是一体的

"Upgrade"首部字段仅用于切换现有的连接上的协议,不能用于切换底层连接协议,也不能将当前连接切换为其他连接。如果要达到上述要求,可以使用3xx(重定向)响应(rfc 7231 6.4)。

本规范仅定义了名为"HTTP"的超文本传输协议。其他由IANA注册的token定义在8.6章节中。

"Upgrade"必须结合"Connection"使用

8 IANA Considerations

协议字段注册机构,本章节不作详解

8.5 Content Coding Registration

IANA在http://www.iana.org/assignments/http-parameters维护者"HTTP Content Coding Registry"。"HTTP Content Coding Registry"注册的内容如下:

代码语言:javascript
复制
+------------+--------------------------------------+---------------+
| Name       | Description                          | Reference     |
+------------+--------------------------------------+---------------+
| compress   | UNIX "compress" data format [Welch]  | Section 4.2.1 |
| deflate    | "deflate" compressed data            | Section 4.2.2 |
|            | ([RFC1951]) inside the "zlib" data   |               |
|            | format ([RFC1950])                   |               |
| gzip       | GZIP file format [RFC1952]           | Section 4.2.3 |
| x-compress | Deprecated (alias for compress)      | Section 4.2.1 |
| x-gzip     | Deprecated (alias for gzip)          | Section 4.2.3 |
+------------+--------------------------------------+---------------+
8.6 Upgrade Token Registry

"Hypertext Transfer Protocol (HTTP) Upgrade Token Registry"定义了用于标识"Upgrade"字段中协议名称的命名空间。tokens的注册由http://www.iana.org/assignments/http-upgrade-tokens维护。

9 Security Considerations

本章节用于提醒开发人员,供应商以及用户与HTTP消息语法,解析和路由有关的安全。HTTP语义和载体相关的安全参见RFC 7231

9.1 Establishing Authority

HTTP依赖一种权威响应的概念:HTTP通过目标URI来判断一个响应(该响应给出了请求的目标资源产生响应消息时的状态)是否具有权威性。为了提高性能和可靠性,通常会使用非授权的资源(如共享缓存)来提供响应,但这种情况仅限于可信赖的源或发布的响应可以被安全地使用的情况。

不幸的是,很难去establishing authority(建立权威),例如钓鱼网站可以攻击用户权威感知,用户的权威感知可能会被内容相似的超文本诱导(如可能被添加的userinfo混淆权威组件)。用户代理可能会通过方便用户在动作前检查URL,严格区分(或拒绝)userinfo,以及在接收到来自未知或不信任的源的文档时不发送存储的证书和cookies的方式降低钓鱼攻击。

当使用权威组件注册名称时,"http" URI scheme依赖用于本地名称解析服务来决定从哪里获取权威响应。这意味着任何对用户网络主机列表,缓存名称或名称解析库的攻击都会变成对establishing authority的攻击。同样地,用户对DNS服务的选择,以及对获取解析结果的服务的等级划分,都会影响到真实的地址映射。可以采用DNSSEC(RFC 4033)来提高解析的真实性。

此外,在获取到一个IP地址后,为一个"http" URI 建立权威时仍然容易遭受IP路由的攻击。

使用"https" scheme可以避免遭受多种潜在的对establishing authority的攻击。TLS连接是安全加密的,且客户端可以将服务端的身份与目标URI的安全组件进行比对校验。

9.2 Risks of Intermediaries

HTTP中间设备作为中间人,可能会发起中间人攻击。对中间设备妥协的系统可能会导致严重的安全和资产问题。中间设备可能拥有访问安全相关的信息,如用户和组织的个人信息以及用户和内容提供商的隐私信息等。妥协于中间人或中间人实现或忽略安全和资产考虑的系统,可能会遭受大范围的可能的攻击。

包含共享缓存的中间设备特别容易遭受缓存中毒攻击(RFC 7234 8).

现实时需要在涉及和代码实现,以及配置选项中考虑隐私和安全。

用户需要注意到中间设备并不值得信任,HTTP无法解决这类问题。

9.3 Attacks via Protocol Element Length

因为HTTP使用了文本,字符分割的字段,解析者很容易遭受长(或很慢的)字节流数据的攻击,特别是实现了未事先定义长度的协议元素时。

为了提高互操性,请求行(3.1.1章节)和首部字段(3.2章节)中给出了最小长度限制的建议。这些是最小值的建议,也可以用于有限资源的实现中。但大多数实现也会选择更高的限制。

服务端可以拒绝长度过大的request-target(RFC7231 6.5.12)或请求载体过大(RFC7231 6.5.11)的消息。其余与限制相关的状态码定义在HTTP扩展中(RFC6585).

接受应该小心限制其处理的其他协议元素,包括(但不限于)请求方法,响应状态短语,首部字段名称,数值和消息块。错误地限制这类处理会导致缓存溢出,算法溢出或增加DOS攻击的可能性。

9.4 Response Splitting

响应分割攻击原理可以参见这里,文中构造一个请求对应多个响应的关键点是其请求的length为0,但在CRLF之后又跟了内容

响应分割(CRLF注入)是一种通用的Web攻击的技术,其利用了HTTP消息基于行的特性和长连接上针对请求的响应顺序。这种技术在请求经过共享缓存时特别容易遭到破坏。

响应分割利用了服务端(通常是一个应用服务)的漏洞,攻击者可以在请求的参数中发送编码的数据,这类数据后续会被解码并通过响应的首部字段进行回显。如果解码的数据经过精心编排,导致该响应看起来已经结束,后续的响应已经开始,此时响应被分割,第二个响应中的字段会被攻击者控制。攻击者后续会在长连接上发送任何请求,使得接收者(包括中间设备)相信分割的第二部分是针对第二个请求的权威应答。

例如,应用服务器可能会在request-target中读取一个参数,并在重定向中将其重用,从而导致在响应的“Location ”首部字段中回显相同的参数。如果该参数被应用解码但没有在响应字段中正确编码,攻击者可以通过发送编码的CRLF和其他内容使得应用的单个响应看起来像是一个或多个响应。

一种通用的防止响应分割的防御方式为:过滤数据(非首部)中使用CR和LF(即"%0D"和"%x0A")编码的请求。然而,这种方式假设应用服务仅处理URI解码,而不处理模糊数据的转换,如字符集转换,XML实体转换,base64解码,sprintf格式转换等。一种更有效缓解这种攻击的方式是阻止所有非服务端核心协议库在首部中发送CR或LF,即限制API输出的首部字段,过滤错误的字节并且不允许应用服务直接写入协议流。

9.5. Request Smuggling

请求夹带攻击利用各种接收者在解析协议上的不同来在一个看似无害的请求中隐藏额外的请求(可能会被代理阻止)。类似响应分割攻击,请求夹带会导致各种HTTP使用上的攻击。

本规范引用了请求解析的新的要求,特别是对消息框架的要求(3.3.3章节)来降低请求夹带攻击。

9.6 Message Integrity

HTTP没有定义校验消息完整性的机制,主要依赖底层传输协议的错误检测能力和length和chunk分割来检测完整性。其他的完整性检测机制,如hash函数或数字签名可以通过扩展的元首部字段来选择性的添加到消息中。历史上,缺乏单一的完整性校验机制被大多数HTTP通信所采纳。然而,HTTP作为流行的信息接入机制,使其对消息完整性校验越来越迫切。

鼓励用户代理通过可配置的方法来检测和报告消息的完整性,这些方法可以选择性的配置哪些环境需要校验完整性。例如,用于浏览病史和药物相互作用的浏览器需要在这些信息不完整,过期或在传输时损坏的情况下告知用户。这类机制可能会通过用户代理扩展来选择性开启,或通过响应中负责消息完整性校验的元素据来实现。至少,用户代理应该在需要时应该能够区分一个响应消息是完整的还是不完整的。

9.7 Message Confidentiality

HTTP依赖于底层传输协议来提供消息机密性。HTTP的实现独立于底层传输协议,因此能够在多种加密连接上使用,可以通过URI scheme或用户代理配置来选择这类传输协议。

https scheme可以使用加密连接来认证资源(2.7.2章节)。

9.8 Privacy of Server Log Information

服务端会随着时间记录有关用户请求的个人数据,可能用于鉴别用户的阅读模式或感兴趣的主题。特别地,当在一个中间设备上收集日志信息时,经常会包含用户代理交互的历史数据,包括大量站点信息,这类信息可以用于追踪特定的用户。

HTTP日志信息是机密的,经常受法律和规定的限制。日志信息需要被安全地保存并为分析者制定遵循的准则。可以对个人信息的独立表项进行匿名处理,但这通常不足以阻止通过其他相关的字符推断出真实的日志追踪信息。因此,对一个特定的用户来说,暴露其接入痕迹是不安全的,即使这些痕迹使用了假名。

为了减小盗窃和意外暴露的风险,应该消除日志中的个人身份信息,包含用户标识,IP地址和用户通过的请求参数(如果不需要这类信息来提供安全,审计和欺诈控制)。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-12-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • rfc7230
    • 2 Architecture
      • 3 Message Format
        • 4 Transfer Codings
          • 5 Message Routing
            • 6 Connection Management
              • 8 IANA Considerations
                • 9 Security Considerations
                相关产品与服务
                微服务引擎 TSE
                微服务引擎(Tencent Cloud Service Engine)提供开箱即用的云上全场景微服务解决方案。支持开源增强的云原生注册配置中心(Zookeeper、Nacos 和 Apollo),北极星网格(腾讯自研并开源的 PolarisMesh)、云原生 API 网关(Kong)以及微服务应用托管的弹性微服务平台。微服务引擎完全兼容开源版本的使用方式,在功能、可用性和可运维性等多个方面进行增强。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档