Goreplay工具学习(3)
前两篇看了这个工具的主体架构,知道了工具是如何执行的。主要执行的流程就是通过一个个插件,来将收到的结果进行一步步的处理过滤然后传递给下一个插件。
这些天在工作中有了另一个需求,需要对录制的流量进行一些过滤操作,并将其保存下来。对于这个需求,需要用到这个工具的中间件的功能,之前有提到过,中间件的作用主要是将流量按照用户的需求做一些定制化的过滤操作。
1. 中间件的架构
当启动gorelay工具并指定中间件时,会根据所填入的命令进行初始化中间件对象。
这个NewMiddleware
方法返回的是Middleware
类型的对象。可以先看下此类型的定义
type Middleware struct {
command string
data chan *Message
Stdin io.Writer
Stdout io.Reader
commandCancel context.CancelFunc
stop chan bool // Channel used only to indicate goroutine should shutdown
closed bool
mu sync.RWMutex
}
从定义可以看出,此结构体定义了command (中间件执行命令) , Message的chan队列,输入输出流,停止标记,互斥锁等。
2. 中间件初始化
然后可以看下这个NewMiddleware
方法都做了哪些操作。
如上图,第30行到33行主要是初始化这个中间件的结构体,给一些初始值。
第35行到38行主要是将用户传入的命令(–middleware 后的参数)作为一个bash命令,创建Cmd对象。这个Cmd对象是go自带的包,里边包含了命令的运行路径,标准输入输出错误流等各种参数,与其他语言执行命令行类似。
第40,41行是将此命令的输入输出流赋值给此中间件对象所定义的输入输出流。第43行是将当前程序的标准错误流赋值给此命令的标准错误流,即如我们在命令行执行这个工具,如果执行中间件代码有错误异常,即会将错误信息打印在命令行终端上。
第45行启动一个协程,传入的参数是中间件的输出流,即执行命令的输出流,下文会专门讨论此read方法。
第47-62行主要作用是启动一个协程来运行此命令,然后监听命令是否异常结束。
最后,再看下上面第45行的read方法:
因为此方法传入的是中间件对象的输出流,因此这里主要是读取此对象的输出流,即所运行的中间件文件如果在标准输出流输出任何内容,都会在这里读取到。在读取到中间件文件的输出后,会对其进行一些的鉴别操作,以此来查看中间件是否返回合规的数据。若数据合规,会将数据转化为Message
类型的对象,然后传递给中间件的data。
这里对中间件初始化操作做一个小结,主要是启动用户配置的中间件命令,然后监听此命令的输出,并对输出进行相关检查,若无问题则发到中间件对象的data管道。
3. 中间件运行
在初始化完成后,就开始正式运行此中间件了。
可以看框住的代码,向中间件的ReadFrom方法中传入读数据的插件,即将读到的数据传入给中间件,具体的操作如下
这里的大体操作就是将读插件所读取到的数据,写到中间件的标准输入流上(line97),即给运行的cmd输入数据。这样的话,如果有写入数据,则经过中间件的处理,会将数据输出到标准输出流上,然后在初始化时候启动的读取标准输出流的协程就可以对标准输出流输出的数据进行验证并发往自身的data管道中。
4. 中间件示例
在项目中的example/middleware
目录下,有许多中间件的示例代码,这里用python的代码来讲解。因为目前实际工作中,使用python的情况还是最多的。
def process_stdin():
"""
Process STDIN and output to STDOUT
"""
for raw_line in fileinput.input():
line = raw_line.rstrip()
# Decode base64 encoded line
decoded = bytes.fromhex(line)
# Split into metadata and payload, the payload is headers + body
(raw_metadata, payload) = decoded.split(b'\n', 1)
# Split into headers and payload
headers_pos = find_end_of_headers(payload)
raw_headers = payload[:headers_pos]
raw_content = payload[headers_pos:]
log('===================================')
request_type_id = int(raw_metadata.split(b' ')[0])
log('Request type: {}'.format({
1: 'Request',
2: 'Original Response',
3: 'Replayed Response'
}[request_type_id]))
log('===================================')
log('Original data:')
log(line)
log('Decoded request:')
log(decoded)
encoded = binascii.hexlify(
raw_metadata + b'\n' + raw_headers + raw_content).decode('ascii')
log('Encoded data:')
log(encoded)
sys.stdout.write(encoded + '\n')
对于fileinput.input()是会读取标准输入流的数据,若没有数据则阻塞。在读取到数据后会对数据进行解码操作。首先第10行会将传入的16进制数据转为byte类型,然后根据解码后的数据进行区分请求头和请求体,最后再将请求头和请求体拼装成十六进制编码输出到标准输出流中。
首先流量中第一行为meta信息,即工具自己加的一些识别信息。
这个example展示了如何将数据进行解码以及转码传输。我们可以在解码后对流量进行相关条件判断,只要符合需求的流量能输出到标准输出流中,这样就可以满足过滤的需求。
在我调试这段代码时发现,每次我请求监听的端口,终端都会打印出流量,但是却不往下游写。当时也很纳闷,明明执行了sys.stdout.write()方法,但工具监听输出流的代码却没有动。如果我换成shell脚本,则不会出现这问题。最终在各种查阅资料调试下,发现了问题所在最后的sys.stdout.write()执行后,如果不调用sys.stdout.flush()方法,则输出流的数据会攒够一定长度才会真正的输出。在最后加上sys.stdout.flush()
后,中间件的read方法就立马读取到了输出流传出的数据。