更多>>关于我们
西安鲲之鹏网络信息技术有限公司从2010年开始专注于Web(网站)数据抓取领域。致力于为广大中国客户提供准确、快捷的数据采集相关服务。我们采用分布式系统架构,日采集网页数千万。我们拥有海量稳定高匿HTTP代理IP地址池,可以有效获取互联网任何公开可见信息。
您只需告诉我们您想抓取的网站是什么,您感兴趣的字段有哪些,你需要的数据是哪种格式,我们将为您做所有的工作,最后把数据(或程序)交付给你。
数据的格式可以是CSV、JSON、XML、ACCESS、SQLITE、MSSQL、MYSQL等等。
更多>>技术文章
HPACK解码避坑经验
发布时间:2025-03-14
某APP和服务端通信采用了自定义协议(非传统的HTTPS),具体做法是把HTTP协议包装进了自定义的应用层协议中。
客户端对服务端的请求是这样的:
(1)将url、querystring、request-headers进行HPACK压缩(编码)。
(2)将request-body进行gzip压缩。
(3)将上述两部分用AES加密后填充到应用层协议字段中,然后用TCP协议发出。
服务端对客户端的应答协议也是类似:
(1)将response-headers进行HPACK压缩(编码)。
(2)将response-body进行gzip压缩。
(3)将上述两部分用AES加密后填充到应用层协议中发回。
我们在对应答数据response-headers的解析上遇到了走了些弯路,下面分享一下经验,大家可以避坑。
通过抓包我们拿到了一段应答数据,经过AES解密后,再对response-body进行gzip解压缩,也成功看到明文的应答body。但是对response-headers进行HPACK解码时却陷入了僵局。解码使用的是Python的hpak库,decode的时候一直报"InvalidTableIndex"异常。
刚开始怀疑APP可能用了非标准的HPACK算法,而hapck库用的是标准算法,不兼容造成的。于是尝试hook它的Decoder类,却发现了一个更诡异的情况:对于同一个Decoder类、同一个类函数、同样的输入参数,APP内部调用(正常发包)时候正常,但是我们主动hook调用(创建Decoder类实例,主动调用decode函数)时却出现了"illegal index value"异常。猜测可能是类的静态变量在其它地方被修改过,查看源码也没发现。
在百思不得其解一筹莫展的情况下,我们决定再深入了解下这个HPACK压缩的算法。或许是忽略掉了什么。
这里先介绍一下HTTP头压缩的目的。我们知道,HTTP头部有一些要重复发送的字段,如 User-Agent、Accept、Content-Type,每次请求都需发送,导致带宽浪费(比如,一个User-Agent有时候就可能有近百字节)。如果客户端和服务端约定一下,将这些重复出现的信息用较少的字节来替代,双方收到后再查表将其还原,这样就能减少传输的数据量,达到降低带宽消耗和提高传输效率的目的。头部压缩是HTTP/2之后才有的特性。
HPACK是为HTTP/2设计的头部压缩算法,它通过使用霍夫曼编码(哈夫曼编码)、静态和动态表来进行压缩。
霍夫曼编码是一种无损数据压缩算法,其核心思想是:高频率字符使用短码,低频率字符使用长码。这样可以使整体编码后的平均码长达到最小,实现数据压缩的目的。
静态表好理解,就是把一些常用的HTTP头事先编号,后面只要发送这些编号(索引)即可,减少了网络传输,对方收到后再根据变化将其还原。如下图所示,是hpack.table模块中定义的静态表的部分条目。它是在RFC7541中被定义的规范,目前一共有61条。
那么动态表是干什么用的呢?隐约感觉离真相越来越近了。静态表是预定义的一组常用 header 字段及其值,固定不变。而动态表顾名思义,是动态建立的,它是用来存储在当前会话中实际出现过的 header 字段。也就是会话(连接)建立之初,动态表是空的。动态表会根据会话期间发送的header顺序动态构建和更新,这样如果同样的 header在后续请求中重复出现,就可以仅传输该header在动态表中的索引,从而大大减少需要传输的数据量。它主要功能是“记忆”当前会话中出现过的 header 信息,通过维护一个动态更新的字典来实现。
现在清楚问题所在了,原来是我们缺少了必要的动态表!!! 即便我们通过Wireshark 捕获了数据包并提取 HPACK 压缩的头部(字节码),但缺少正确的动态表状态,那么也无法正确解码出明文headers,索引找不到,所以会报"InvalidTableIndex"异常。要想成功解码,必须要从会话一开始就维护这个动态表。
hpack库在内部实现了动态表机制,即在编码和解码过程中会自动管理动态表的状态,无需用户手动干预。在编码时,当新的header被处理后,库会根据 HPACK 规范自动将相应的条目加入动态表,在解码时,接收到的 header 块也会触发动态表的同步更新,从而保证发送端和接收端动态表的一致性。对于一个会话,编码要使用同一个hpack.encoder实例,解码也使用同一个hpack.decoder实例。
如何APP使用了扩展静态表,比如,在预定义的61个基础上又增加了几个,用hpack该怎么处理呢?我们手动将这几个条目按APP同样的顺序预先加入到动态表的最前面即可,详见下面的示例代码。
- #!pip install hpack===4.1.0
- from hpack import Encoder, Decoder
- # 定义自定义的静态表,注意:这里只是示例,实际条目请确保符合 HPACK 规范
- custom_static_table = [
- (b":m-whale-time-out", b""),
- ]
- class CustomHpackEncoder(Encoder):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- for (name, value) in custom_static_table:
- self.header_table.add(name, value)
- class CustomHpackDecoder(Decoder):
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- for (name, value) in custom_static_table:
- self.header_table.add(name, value)
- # 要编码的 HTTP/2 头部列表(每个元素为 (name, value) 元组)
- headers = [
- (b":method", b"GET"),
- (b":path", b"/"),
- (b":scheme", b"https"),
- (b":authority", b"example.com"),
- (b":m-swhale-time-out", b"")
- ]
- original_size = 0
- for item in headers:
- original_size += len(item[0]) + len(item[1])
- print("Original size:", original_size)
- # 对头部进行编码,返回的结果为 bytes 类型
- encoder = CustomHpackEncoder()
- encoded_headers = encoder.encode(headers)
- print("Encoded headers:", encoded_headers)
- print("Encoded size:", len(encoded_headers))
- print()
- # 对编码后的头部进行解码,返回的结果为解码后的头部列表
- decoder = CustomHpackDecoder()
- decoded_headers = decoder.decode(encoded_headers)
- print("Decoded headers:", decoded_headers)
- print(decoder.header_table)
特别说明:本文旨在技术交流,请勿将涉及的技术用于非法用途,否则一切后果自负。如果您觉得我们侵犯了您的合法权益,请联系我们予以处理。
☹ Disqus被Qiang了,之前所有的评论内容都看不到了。如果您有爬虫相关技术方面的问题,欢迎发到我们的问答平台:http://spider.site-digger.com/