从 HTTP Range 请求谈标准是如何被破坏的
如今 cdn 已成为互联网上的基础设施,会与形形色色的公司打交道。其中既有一些拿着政府资金,结果只让你草草布一个 Nginx 代理就完事的;也有现在那些中国最强势的互联网公司。这里只说互联网公司,为什么它们强势呢?一方面,它们拥有着巨大的流量,中国的 cdn 市场竞争又如此激烈,一个不爽,把量切走,反正排队等着为我服务的 cdn 厂商多的很;另一方面,它们本身的技术积累都很强,所以提起需求来是毫不手软,底气十足。
但这些有着深厚技术积累的公司,有时候提出来的需求只能让人苦笑。
背景
A 是中国目前最大的互联网公司之一,它的视频当然流量也很大。按照业内惯例,它会把内容同时让多家 cdn 厂商服务,然后定期进行打分,排名。参与打分的指标很多,最典型的就是一些 4XX 的错误请求。所以,cdn 厂商的运维及管理人员,会很重视这些指标。
最近,运维发现服务器上有很多 416 请求。416 是 Range 错误的响应码,除去一些正常的 416 外,运维还发现有一类 416 请求的 Range 头是:
Range: byptes=XXXX-0
XXXX 是个非 0 的数。很明显,这是一个错误的 Range 请求,之所以会出现这样的 Range,是因为对方的播放器 bug 导致。但 A 公司要求,把 XXX-0
按照 xxx-
来处理。
我们知道,在 HTTP 协议中规定的是,一个 Range 头可以有如下形式(假设文件大小为1000个字节):
bytes=0-499(前500个字节)
bytes=500-999(第二个500个字节)
bytes=-500(最后500个字节)
bytes=9500-(与上同,最后500个字节)
bytes=0-0,-1(第一个字节和最后一个字节)
那么到底有没有 bytes=500-0
这样的 range 呢?我们从标准和实现两方面来考虑。
标准
参考 RFC2616 14.35.1节:
If the last-byte-pos value is present, it MUST be greater than or equal to the first-byte-pos in that byte-range-spec, or the byte-range-spec is syntactically invalid. The recipient of a byte-range-set that includes one or more syntactically invalid byte-range-spec values MUST ignore the header field that includes that byte-range-set.
所以,理论上,bytes=500-0
这样的 range 段,是错误的。
实现
以 Nginx 为例,我们来看看代码中是如何处理的。
static ngx_int_t
ngx_http_range_parse(ngx_http_request_t *r, ngx_http_range_filter_ctx_t *ctx,
ngx_uint_t ranges)
{
u_char *p;
off_t start, end, size, content_length;
ngx_uint_t suffix;
ngx_http_range_t *range;
p = r->headers_in.range->value.data + 6;
size = 0;
content_length = r->headers_out.content_length_n;
for ( ;; ) {
start = 0;
end = 0;
suffix = 0;
while (*p == ' ') { p++; }
if (*p != '-') {
if (*p < '0' || *p > '9') {
return NGX_HTTP_RANGE_NOT_SATISFIABLE;
}
while (*p >= '0' && *p <= '9') {
start = start * 10 + *p++ - '0';
}
while (*p == ' ') { p++; }
if (*p++ != '-') {
return NGX_HTTP_RANGE_NOT_SATISFIABLE;
}
while (*p == ' ') { p++; }
if (*p == ',' || *p == '\0') {
end = content_length;
goto found;
}
} else {
suffix = 1;
p++;
}
if (*p < '0' || *p > '9') {
return NGX_HTTP_RANGE_NOT_SATISFIABLE;
}
while (*p >= '0' && *p <= '9') {
end = end * 10 + *p++ - '0';
}
while (*p == ' ') { p++; }
if (*p != ',' && *p != '\0') {
return NGX_HTTP_RANGE_NOT_SATISFIABLE;
}
if (suffix) {
start = content_length - end;
end = content_length - 1;
}
if (end >= content_length) {
end = content_length;
} else {
end++;
}
found:
if (start < end) {
range = ngx_array_push(&ctx->ranges);
if (range == NULL) {
return NGX_ERROR;
}
range->start = start;
range->end = end;
size += end - start;
if (ranges-- == 0) {
return NGX_DECLINED;
}
}
if (*p++ != ',') {
break;
}
}
if (ctx->ranges.nelts == 0) {
return NGX_HTTP_RANGE_NOT_SATISFIABLE;
}
if (size > content_length) {
return NGX_DECLINED;
}
return NGX_OK;
}
其中,
found:
if (start < end) {
range = ngx_array_push(&ctx->ranges);
if (range == NULL) {
return NGX_ERROR;
}
range->start = start;
range->end = end;
size += end - start;
if (ranges-- == 0) {
return NGX_DECLINED;
}
}
表明,只有 start<end 的,才会后续进行 range 处理。这里的 start,是指 bytes=a-b 中的 a,end 指的是 b+1。
所以,在 HTTP 服务器的典型实现上,Range: bytes=500-0
也是错误的请求。
现实世界
好了,从理论和典型实现上讨论了 Range 头之后,我们回到第三次元,现实世界。
可以猜想,出现这个问题的原因是 A 公司的播放器开发人员代码的一处 bug 导致。但由于它们是甲方,所以我们应该予以配合。结果是我们在服务器端将 Range: bytes=500-0
这类的请求与 Range: bytes=500-
等同处理。
解决起来很简单,非常简单。但是,好好的标准就这么被破坏了。可以想象,A 是 Top 级的互联网大佬,某家 cdn 帮他们解决这种 Range 错误了,它肯定会对其他 cdn 商家说,这个很简单,那谁都是这么处理的。然后,中国所有的 cdn 商家都这么处理了。再然后,没人记的这东西是否遵循标准了,即使有人拿标准说起,也会被老人们这么教育:“标准只是理想情况,现实中的实现,是复杂多变的…”
更没人会记得,罪魁祸首是某个不合格的播放器开发程序员。