Content-Encoding: gzip + Transfer-Encoding: chunked与gzip/zlib导致头部检查错误

1 投票
2 回答
4583 浏览
提问于 2025-04-17 20:56

如何处理使用gzip编码的分块数据?我有一个服务器,它以以下方式发送数据:

HTTP/1.1 200 OK\r\n
...
Transfer-Encoding: chunked\r\n
Content-Encoding: gzip\r\n
\r\n
1f50\r\n\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03\xec}\xebr\xdb\xb8\xd2\xe0\xef\xb8\xea\xbc\x03\xa2\xcc\x17\xd9\xc7\xba\xfa\x1e\xc9r*\x93\xcbL\xf6\xcc\x9c\xcc7\xf1\x9c\xf9\xb6r\xb2.H ... L\x9aFs\xe7d\xe3\xff\x01\x00\x00\xff\xff\x03\x00H\x9c\xf6\xe93\x00\x01\x00\r\n0\r\n\r\n

我尝试过几种不同的方法,但总觉得有些东西忘记了。

data = b''
depleted = False
while not depleted:
    depleted = True
    for fd, event in poller.poll(2.0):
        depleted = False
        if event == select.EPOLLIN:
            tmp = sock.recv(8192)
            data += zlib.decompress(tmp, 15 + 32)

尝试解码数据时(当然是从\r\n\r\n之后开始):
zlib.error: Error -3 while decompressing data: incorrect header check

所以我想,数据应该在完整接收后再进行解压。

        ...
        if event == select.EPOLLIN:
            data += sock.recv(8192)
data = zlib.decompress(data.split(b'\r\n\r\n',1)[1], 15 + 32)

但还是出现同样的错误。我还尝试过解压data[:-7],因为数据最后有一个块ID,还有data[2:-7]和其他各种组合,但结果还是一样的错误。

我也尝试过使用gzip模块:

with gzip.GzipFile(fileobj=Bytes(data), 'rb') as fh:
    fh.read()

但它告诉我“不是一个gzipped文件”。

即使我把服务器接收到的数据(包括头部和数据)记录到一个文件中,然后在80端口创建一个服务器套接字,把数据(保持原样)提供给浏览器,浏览器也能正常显示,所以数据是完整的。我把这些数据的头部去掉(只去掉头部),然后尝试在这个文件上使用gzip:

enter image description here

感谢@mark-adler,我写出了以下代码来处理分块数据:

unchunked = b''
pos = 0
while pos <= len(data):
    chunkLen = int(binascii.hexlify(data[pos:pos+2]), 16)
    unchunked += data[pos+2:pos+2+chunkLen]
    pos += 2+len('\r\n')+chunkLen

with gzip.GzipFile(fileobj=BytesIO(data[:-7])) as fh:
    data = fh.read()

这段代码产生了OSError: CRC check failed 0x70a18ee9 != 0x5666e236,这算是更进一步。简单来说,我根据这四个部分来剪切数据:

  • <chunk length o' X bytes> \r\n <chunk> \r\n

我可能快到了,但还不够近。

附注: 是的,套接字的实现远非最佳,但看起来是这样的,因为我觉得没有从套接字获取到所有数据,所以我设置了一个很长的超时,并尝试用depleted来做一个安全措施 :)

2 个回答

1

@mark-adler 给了我一些关于 HTML 协议中分块 模式 的好建议,除此之外,我还尝试了不同的方法来 解压 数据。

  1. 你需要把这些小块拼接成一个大块
  2. 你应该使用 gzip 而不是 zlib
  3. 你只能对拼接好的大块进行 解压,分开解压是行不通的

以上三个 问题 的解决方案如下:

unchunked = b''
pos = 0
while pos <= len(data):
    chunkNumLen = data.find(b'\r\n', pos)-pos
#   print('Chunk length found between:',(pos, pos+chunkNumLen))
    chunkLen=int(data[pos:pos+chunkNumLen], 16)
#   print('This is the chunk length:', chunkLen)
    if chunkLen == 0:
#       print('The length was 0, we have reached the end of all chunks')
        break
    chunk = data[pos+chunkNumLen+len('\r\n'):pos+chunkNumLen+len('\r\n')+chunkLen]
#   print('This is the chunk (Skipping',pos+chunkNumLen+len('\r\n'),', grabing',len(chunk),'bytes):', [data[pos+chunkNumLen+len('\r\n'):pos+chunkNumLen+len('\r\n')+chunkLen]],'...',[data[pos+chunkNumLen+len('\r\n')+chunkLen:pos+chunkNumLen+len('\r\n')+chunkLen+4]])
    unchunked += chunk
    pos += chunkNumLen+len('\r\n')+chunkLen+len('\r\n')

with gzip.GzipFile(fileobj=BytesIO(unchunked)) as fh:
    unzipped = fh.read()

return unzipped

我把调试输出留在里面,但注释掉是有原因的。
虽然看起来有点乱,但这对我来说非常有用,因为它能帮助我了解我到底在尝试解压哪些数据,哪些部分是从哪里获取的,以及每个计算带来了什么值。

这段代码会处理分块数据,格式如下:
<chunk length o' X bytes> \r\n <chunk> \r\n

在提取 X bytes 时需要小心,因为它们是以 1f50 的形式出现的,我首先需要用 binascii.hexlify(data[0:4]) 转换一下,然后再放入 int()。不太明白为什么现在不需要这样做了,因为之前我需要这样才能得到大约 8000 的长度,但后来突然给了我一个非常大的数字,这不太合理,尽管我没有给它其他数据……无论如何。之后,只需确保数字正确,然后把所有小块合并成一大堆 gzip 数据,最后输入到 .GzipFile(...) 中。

三年后编辑:

我知道这最开始是一个客户端的问题,但这里有一个 服务器端 的函数,可以发送一个相对可用的测试:

def http_gzip(data):
    compressed = gzip.compress(data)

    # format(49, 'x') returns `31` which is `\x31` but without the `\x` notation.
    # basically the same as `hex(49)` but ment for these kind of things.
    return bytes(format(len(compressed), 'x')),'UTF-8') + b'\r\n' + compressed + b'\r\n0\r\n\r\n'
3

你不能直接用 \r\n 来分割数据,因为压缩后的数据可能会包含这个序列,而且如果数据足够长,肯定会有这个序列。你需要先进行“去块处理”,也就是根据提供的长度(比如第一个长度 1f50)来处理数据,然后把得到的块传给解压缩程序。压缩数据的开头是 \x1f\x8b

这个“块”的格式是:十六进制数字,换行符,接着是指定字节数的块,换行符,再来一个十六进制数字,换行符,再来一个块,换行符,依此类推,最后一个块是零长度的,可能还有一些头信息,最后是换行符。

撰写回答