Content-Encoding: gzip + Transfer-Encoding: фрагментация с помощью gzip/zlib дает неправильную проверку заголовка

Как вы управляете фрагментированными данными с помощью кодировки 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 obv):
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] из-за идентификатора чанка в самом конце данных и с data[2:-7] и другими различными комбинациями, но с той же ошибкой.

Я также пробовал модуль gzip через:

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

Но это дает мне «Не сжатый файл».

Даже после записи данных, полученных серверами (заголовки + данные) в файл, а затем создания серверного сокета на порту 80, обслуживающего данные (опять же, как есть) в браузере, он отлично отображается, поэтому данные нетронутый. Я взял эти данные, удалил заголовки (и ничего больше) и попробовал gzip для файла: введите описание изображения здесь

Благодаря @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 :)


person Torxed    schedule 05.03.2014    source источник


Ответы (2)


Вы не можете разделить на \r\n, так как сжатые данные могут содержать и, если они достаточно длинные, обязательно будут содержать эту последовательность. Вам нужно сначала разархивировать фрагменты, используя предоставленную длину (например, первая длина 1f50), и передать полученные фрагменты для распаковки. Сжатые данные начинаются с \x1f\x8b.

Фрагментация представляет собой шестнадцатеричное число, crlf, чанк с таким количеством байтов, crlf, шестнадцатеричный номер, crlf, чанк, crlf, ..., последний чанк (нулевой длины), [возможно, некоторые заголовки], crlf.

person Mark Adler    schedule 05.03.2014
comment
1f50 было бы, 50 байт? или? Потому что я предполагаю, что где-то там будет еще один идентификатор длины? - person Torxed; 05.03.2014
comment
Это означает 1f50 байт. В шестнадцатеричном формате. Последний фрагмент заканчивается \r\n без длины. - person Mark Adler; 05.03.2014
comment
Это начинает становиться полезным :) ха-ха, круто! - person Torxed; 05.03.2014
comment
@Torxed: я обновил описание фрагментации, чтобы оно было более полным. - person Mark Adler; 05.03.2014
comment
Это все еще немного сбивало с толку, я не знаю, было ли это предложение chunk with that many bytes, которое меня немного оттолкнуло, или тот факт, что вы продолжили после any bytes, crlf, вероятно, должен был остановиться на этом, потому что это запутало меня с полуфабрикатом-псевдо. -синтаксис. Хотя это моя вина :) Вы одержите победу, даже если я написал полный ответ, отвечая на оба вопроса о синтаксисе, как с этим бороться и какую библиотеку/метод использовать для распаковки данных. - person Torxed; 05.03.2014

@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(...).

Редактировать 3 года спустя:

Я знаю, что сначала это была проблема на стороне клиента, но вот функция на стороне сервера для отправки функционального теста:

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'
person Torxed    schedule 05.03.2014
comment
1. не нужно сшивать куски в одну большую кучу. Это может быть удобнее, но не обязательно. 2. gzip.GzipFile is реализован поверх zlib 3. вы можете распаковать частичный контент 4. могут быть расширения блоков после размера, которые следует игнорировать, если вы их не понимаете, т.е. игнорировать необязательно ; ... до \r\n. - person jfs; 05.03.2014
comment
не используйте hexlify() здесь. Длина фрагмента представляет собой шестнадцатеричное число, например, int(b'ff', 16) == 255, и в данном случае это два байта b'f' и b'f'; ord(b'f') == 102 == 0x66 т. е. b'f' == b'\x66', поэтому binascii.hexlify(b'ff') == b'6666' (примечание: здесь ascii b'6' т. е. b'\x36'). - person jfs; 05.03.2014
comment
Вы не должны использовать find, так как если есть ошибка в передаваемых данных, find может найти \r\n в сжатых данных. Вы должны искать именно правильные данные в правильном месте. Вы должны искать шестнадцатеричные цифры. Должен быть хотя бы один. Если нет, ошибка. Ищите больше шестнадцатеричных цифр. Если первая нешестнадцатеричная цифра не \r, то ошибка. Если следующий символ не \n, ошибка. Затем вы считаете байты для куска. Если следующий байт после чанка не \r, то ошибка. И т.п. - person Mark Adler; 06.03.2014
comment
Как уже отмечалось, вам не нужно сначала собрать весь поток. Вы можете загружать декомпрессор по частям, что будет использовать намного меньше памяти. Создайте объект zlib.decompressobj и передайте ему данные. Предоставление wbits равного 15+16 приведет к распаковке формата gzip. - person Mark Adler; 06.03.2014
comment
@MarkAdler .find() в этом случае вернет только первое вхождение \r\n, равное <hex>\r\n<chunk ...>, так что это настолько безопасно, насколько это возможно. Также мне нужен способ получить полный шестнадцатеричный код, так как иногда это только один байт, а не два. Попробую распаковать каждый чанк по отдельности.. Но при попытке с zlib поначалу не очень получалось (пробовал и в одном большом чанке, и в каждом отдельном чанке).. - person Torxed; 06.03.2014
comment
@ J.F.Sebastian Я не могу объяснить, как появился этот феномен, но все, что я знаю, это то, что данные поступили в b'\xff\xff' which i couldn't input into int()`, поскольку он ожидает формат, очень похожий на 0xff или даже ff. Хотя это немного сбивает с толку, я понимаю, к чему вы клоните, и я не могу защитить свой код или вывод. Все, что я знаю, это то, что при использовании hexlify() я получил правильное значение (8091, если я не ошибаюсь) из 1f50 (который hexlify преобразовал в 0x1f0x50, что мне и было нужно) :) - person Torxed; 06.03.2014
comment
Небезопасно, если \r\n отсутствует из-за ошибки передачи. - person Mark Adler; 06.03.2014
comment
@Torxed: if b'\xff' not in string.hexdigits.encode('ascii'): raise ValueError("it is not part of the chunk length") - person jfs; 06.03.2014
comment
@MarkAdler, если \r\n отсутствует из-за ошибки передачи по TCP-соединению, я совершенно уверен, что у меня не будет никаких данных после шестнадцатеричного значения, даже если я получу шестнадцатеричное значение. Если у меня возникнут проблемы с передачей, у меня будет гораздо больше проблем, чем .find(), если у меня возникнут проблемы с передачей данных :) Но на всякий случай я буду эмулировать проблему с передачей до/в середине/после соответствующей части и посмотрю, что произойдет: ) - person Torxed; 06.03.2014
comment
@Torxed: hexlify(b'1f50') == b'31663530', int(b'1f50', 16) == 8016 Перечитай мой предыдущий комментарий, попробуй выражения в оболочке Python, поиграйся, пока не поймешь, что b'1f50' != b'\x1f\x50' и len(b'1f50') == 4 and len(b'\x1f\x50') == 2 - person jfs; 06.03.2014
comment
@ J.F.Sebastian Я благодаря вашим ошеломляющим примерам все начинает становиться на свои места! Ты чемпион! :) - person Torxed; 06.03.2014