chunked编码格式引发的问题

1、问题

在适配某家的cas时,票据校验时一直失败,通常是以下几个问题引发的问题
1、网关与cas服务器的网络不通,导致票校验的请求发送不出去
2、cas服务器的票据校验接口,配置了域名,但是在nginx没有配置dns,导致域名无法解析,请求无法发送

3、配置了ip,但是cas服务端限制了host,禁用了ip访问

4、票据校验的请求有问题,service参数组装不正确

经过排查后,发现都不是上面的原因,因此抓了一个包,发现cas服务端是正常把响应返回回来的

2、代码分析

2.1、定位

既然cas服务端正常返回了响应,那就是网关侧有异常,代码打印状态码和body,发现状态码确实200,但是body是nil,也就是没有获取到body。我们采用了agentzh” Yichun“ 写的lua-resty-http模块(https://github.com/liseen/lua-resty-http)

1
2
3
4
5
6
7
8
local  xxx   =  hc:request{
url = cas_validate_uri,
method = "GET",
}

--结果调用的是

function request(self, reqt)

其中接收响应body的代码是

1
2
3
4
5
6
7
8
9
10
11
-- receive body
if shouldreceivebody(nreqt, code) then
body, err = receivebody(sock, headers, nreqt) --接收body
if err then
sock:close()
if code == 200 then
return 1, code, headers, status, nil
end
return nil, "read body failed " .. err
end
end

所以我们重点分析receivebody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
local function receivebody(sock, headers, nreqt)
-- 定义一个名为 receivebody 的本地函数,接收三个参数:
-- sock:表示连接的套接字对象,用于读取数据。
-- headers:表示请求的头部信息。
-- nreqt:包含配置参数的表格,例如最大允许的主体大小和回调函数。

local t = headers["transfer-encoding"] -- 获取 "transfer-encoding" 头部的值并存储在变量 t 中
local body = {} -- 用于存储响应体的数据块的表格
local callback = nreqt.body_callback -- 获取 nreqt 中的 body_callback 函数

if not callback then
-- 如果没有提供回调函数

local function bc(data, chunked_header, ...)
-- 定义一个本地回调函数 bc,用于处理接收到的数据块
if chunked_header then return end
-- 如果存在 chunked_header,则返回,不处理数据块
body[#body+1] = data
-- 将数据块添加到 body 表格中
end

callback = bc
-- 将本地定义的回调函数赋值给 callback
end

if t and t ~= "identity" then
-- 如果 "transfer-encoding" 头部存在且其值不等于 "identity":
-- 表示响应体是分块传输编码(chunked)

while true do
-- 开始一个无限循环,处理每个块

local chunk_header = sock:receiveuntil("\r\n")
-- 调用 sock 对象的 receiveuntil 方法,读取直到 "\r\n" 为止的数据,表示读取块头部信息。

local data, err, partial = chunk_header()
-- 调用 chunk_header 函数以获取数据块头部。如果成功,将数据块头部内容存储在 data 中。

if not data then
return nil, err
-- 如果 data 为 nil,表示读取失败,返回错误信息。
else
if data == "0" then
-- 如果读取到的块头部为 "0",表示传输结束。

return table.concat(body) -- 将 body 中的数据块合并成一个字符串并返回
else
local length = tonumber(data, 16)
-- 否则,将块头部内容转换为十六进制表示的长度。

-- TODO check nreqt.max_body_size !!
-- 注释:需要检查块的大小是否超过最大允许的主体大小。

local ok, err = read_body_data(sock, length, nreqt.fetch_size, callback)
-- 调用 read_body_data 函数从套接字读取指定长度的数据,并处理它。
-- sock:表示连接的套接字对象。
-- length:数据块的长度。
-- nreqt.fetch_size:每次读取数据的大小。
-- callback:读取后的回调函数。

if err then
return nil, err
-- 如果读取失败,返回错误信息。
end
end
end
end
elseif headers["content-length"] ~= nil and tonumber(headers["content-length"]) >= 0 then
-- 如果 "transfer-encoding" 不存在或等于 "identity",且存在 "content-length" 头部且其值为非负数:
-- 表示响应体的传输长度是固定的。

local length = tonumber(headers["content-length"])
-- 将 "content-length" 的值转换为数值。

if length > nreqt.max_body_size then
-- 如果内容长度大于最大允许的主体大小:

ngx.log(ngx.INFO, 'content-length > nreqt.max_body_size !! Tail it !')
-- 记录日志提示该情况。

length = nreqt.max_body_size
-- 将长度截取为最大允许的大小。
end

local ok, err = read_body_data(sock, length, nreqt.fetch_size, callback)
-- 调用 read_body_data 函数读取指定长度的数据,并处理它。

if not ok then
return nil, err
-- 如果读取失败,返回错误信息。
end
else
-- 如果既没有 "transfer-encoding" 头部,也没有 "content-length" 头部:
-- 假设响应体会在连接关闭时结束。

local ok, err = read_body_data(sock, nreqt.max_body_size, nreqt.fetch_size, callback)
-- 调用 read_body_data 函数读取直到最大允许大小的数据,并处理它。

if not ok then
return nil, err
-- 如果读取失败,返回错误信息。
end
end

return table.concat(body)
-- 将 body 中的数据块合并成一个字符串并返回
end

抓包查看,响应的编码格式是 Transfer-Encoding:chunked,因此确认是走到了上述代码解析chunked的逻辑,在关键代码打印日志后,发现这里返回了错误,读取body失败

1
2
if not data then
return nil,err

那么为什么读取会失败?

2.2、什么是chunked类型的数据?

HTTP 中的 Chunked 传输编码是一种在不预先知道响应数据大小的情况下进行数据传输的方式。通常在 HTTP 响应中,服务器会在头部发送 Content-Length 字段,指定即将发送的数据的总长度。然而,有时候服务器在生成响应内容时并不知道最终的内容长度,比如当内容是动态生成的。这时候,服务器可以使用 Chunked 传输编码。

Chunked 传输编码的基本工作原理

Chunked 传输编码中,响应体被分割成一系列块(chunks),每个块可以有不同的大小。每个块由两部分组成:

  1. 块头部(Chunk Header): 该部分指定了块的大小(以16进制表示),后面紧跟着一个回车换行符(\r\n)。
  2. 数据块(Chunk Data): 紧跟在块头部之后的实际数据,数据块的长度由块头部指定。块数据后面也跟着一个回车换行符(\r\n)。

响应的最后一个块是一个特殊的块,它的大小为 0,表示数据传输的结束。

客户端解析 Chunked 数据

客户端接收到 Chunked 编码的响应时,会逐块解析数据,直到遇到大小为 0 的块。具体的解析步骤如下:

  1. 读取块头部,确定当前块的大小。
  2. 读取指定大小的数据块。
  3. 继续读取下一个块,重复步骤 1 和 2。
  4. 当遇到大小为 0 的块时,停止读取。

常规的内容传输

在通常情况下,当服务器要发送响应时,会先计算好整个响应体的大小,并在 HTTP 响应头的 Content-Length 字段中告知客户端。例如,服务器在发送一个 HTML 页面时,可能会先生成整个页面内容,并且计算出它的大小,然后在响应头中设置 Content-Length,再将整个内容一次性发送给客户端。

这种方式的缺点是:

  1. 延迟高:服务器必须等待整个内容生成完毕,才能开始发送。这增加了初始延迟。
  2. 不适合动态生成的内容:如果内容是逐步生成的(例如,来自数据库的查询结果或通过流处理的内容),服务器需要等到所有内容都生成后才能计算总大小并发送。

Chunked 传输编码的优势

使用 Chunked 传输编码,服务器不需要预先知道响应内容的总大小。相反,它可以在生成内容的同时逐步将内容以一块块的形式发送给客户端。

2.3、分析

了解了原理,那我们来看下服务端返回的数据是否正常,使用tcp追踪流展示数据

chunked-01

十六进制:

chunked-02

数据很明显有问题,让我们回到代码,只有读到一个0,才会认为数据结束了,也就是已经接收到了完整的数据,但是现在不是一个0,是0000,从十六进制看也很明显,我们需要读取到 30 od oa才会认为数据结束,但是现在是30 30 30 30 od oa。因此客户端会一直读数据,而从请求的响应头connection:close 可以知道,这个连接在传输完数据后会关闭。因此当连接关闭时, local data, err, partial = chunk_header()最后会报错,err其实是一个close。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local chunk_header = sock:receiveuntil("\r\n")
local data, err, partial = chunk_header()
if not data then
return nil,err
else
if data == "0" then --只有读到一个0才会认为数据结束了
return table.concat(body) -- end of chunk
else
local length = tonumber(data, 16)

-- TODO check nreqt.max_body_size !!

local ok, err = read_body_data(sock,length, nreqt.fetch_size, callback)
if err then
return nil,err
end
end
end

所以到这里,问题其实可以确认是cas返回的数据有问题

3、问题解决

3.1、本地验证

联系了cas的服务商,被告知这个cas是拿开源来用的,掌握程度一般,沟通下来比较困难🙄,刚好我对这个问题比较感兴趣,因此拿对应开源版本做一个验证
cas地址:https://github.com/apereo/cas/tree/5.3.x?tab=readme-ov-file
安装参考文档:https://www.cnblogs.com/hellxz/p/15740935.html
tomact:https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.93/bin/
cas的war包:https://repo1.maven.org/maven2/org/apereo/cas/cas-server-webapp-tomcat/

简单运行后,成功对接我们的网关,那么抓包看一下返回的数据

chunked-03

数据格式非常正确!

3.2、最终的结论

那么现在有问题的场景,还是cas做了一定的改动?cas是用java实现的,这块对我有一定的研究成本,研究了下源码,最后追踪到了视图,但是没找到具体哪里可以设置传输编码,后面有时间再研究一下,还是先把问题还给服务商。


chunked编码格式引发的问题
https://zjfans.github.io/2024/08/13/chunked编码格式引发的问题/
作者
张三疯
发布于
2024年8月13日
许可协议