使用Tornado实现http代理
0x00 http代理
http代理的用处很多,市面上也有公开的代理,但是有时候为了工作需要,比如分析应用层流量、做数据访问控制、甚至做监控等等。Tornado提供了一些很方便的环境和API,我们可以基于Tornado轻松实现一个http代理。
0x01 实现原理
http代理主要做客户端和web服务器之间的转发,这是大家都熟悉的场景,但只限于http协议的情形。对于https的情况,这时候代理只作为TCP中继进行信息中转,需要单独处理。
0x02 Tornado实现
基于Tornado可以实现一个异步的http代理,性能优越,实现也简单,主要使用的类是AsyncHTTPClient,IOStream。阅读过Tornado源码的同学可能对这两个类并不陌生。这里还是简单说下,AsyncHTTPClient顾名思义,是用来做异步HTTP客户端请求的,而IOStream是对socket的一层封装。
AsyncHTTPClient就是用来处理普通的http请求的,RequestHandler获取客户端请求之后,proxy需要解析客户端的请求并使用这个类来请求服务器,拿到response,然后写给客户端,打完收工。
对于proxy作为TCP中继的时候,其实完全可以使用原生的socket两头儿读写数据,不过太麻烦了。Tornado提供了一个IOStream类,这个类可以看做是socket的包装类,用起来比socket简单许多,并且socket是异步非阻塞
的。
Talk is cheap, show me the code
,不多说,看代码好了,这里由于一些原因,我只能贴出关键部分的代码,希望阅读此文的同学可以自己写一个出来用,其实也不难。
处理http请求
@tornado.web.asynchronous
def get(self):
# 获取请求体
body = self.request.body
if not body:
body = None
try:
# 代理发送请求
render_request(
self.request.uri,
callback=self.on_response,
method=self.request.method,
body=body,
headers=self.request.headers,
follow_redirects=False,
allow_nonstandard_methods=True)
except tornado.httpclient.HTTPError as httperror:
if hasattr(httperror, ‘response‘) and httperror.response:
self.on_response(httperror.response)
else:
self.set_status(500)
self.write(‘Internal server error:\n‘ + str(httperror))
self.finish()
没啥好说的,接到客户端请求,直接去请求服务器就行了。异步回调函数是on_response,这个函数里就处理proxy和client的交互就行了,self.write(response.body)
你懂的。
这里有个坑,就是写headers的时候,把response的headers照搬设置一遍是会出错的,造成访问失败。这里我的处理方法是只写RequestHandler中self._headers
存在的头即可。
TCP中继实现
对于443端口或者浏览器的connect请求,代理只能从TCP层入手,转发整个HTTP报文,这里使用的是http协议中的connect方法,在RequestHandler中实现这个方法就行了。
顺便说下connect方法,这个方法被调用的时候,代理不用关系http层请求的具体内容,而是直接从TCP层转发这个报文给服务器。收到时,也是同样的转发给客户端。
CONNECT www.web-tinker.com:80 HTTP/1.1
Host: www.web-tinker.com:80
Proxy-Connection: Keep-Alive
Proxy-Authorization: Basic *
Content-Length: 0
具体实现的代码如下:
@tornado.web.asynchronous
def connect(self):
‘‘‘
对于HTTPS连接,代理应当作为TCP中继
‘‘‘
def req_close(data):
if conn_stream.closed():
return
else:
conn_stream.write(data)
def write_to_server(data):
conn_stream.write(data)
def proxy_close(data):
if req_stream.closed():
return
else:
req_stream.close(data)
def write_to_client(data):
req_stream.write(data)
def on_connect():
‘‘‘
创建TCP中继的回调
‘‘‘
req_stream.read_until_close(req_close, write_to_server)
conn_stream.read_until_close(proxy_close, write_to_client)
req_stream.write(b‘HTTP/1.0 200 Connection established\r\n\r\n‘)
print ‘Starting Conntect to %s‘ % self.request.uri
# 获取request的socket
req_stream = self.request.connection.stream
# 找到主机端口,一般为443
host, port = (None, 443)
netloc = self.request.uri.split(‘:‘)
if len(netloc) == 2:
host, port = netloc
elif len(netloc) == 1:
host = netloc[0]
# 创建iostream
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
conn_stream = tornado.iostream.IOStream(s)
conn_stream.connect((host, port), on_connect)
我解释下这两句:
req_stream.read_until_close(req_close, write_to_server)
conn_stream.read_until_close(proxy_close, write_to_client)
短短两行代码,加上4个回调函数,就完成了数据的中转。
首先,req_stream是proxy和client之间的socket,可以通过HTTPRequest获取到对应的iostream,proxy和server之间的socket就要自己创建了,这里是conn_stream。
read_until_close方法是iostream中提供的,作用是一直读数据,直到socket关闭了。
第一行的作用就是从client和proxy之间的socket中读数据,读出来之后,写入到proxy和server之间的socket中,由proxy转发。
第二行的作用就是将服务器数据写到客户端socket中了,和上面一样,没啥好说的,写入的功能就在四个回调函数中。
有人奇怪为啥read_until_close有两个回调函数,我的理解是第一个回调在关闭的时候调用,第二个回调在不停读出数据的时候调用。
写出来用的效果还行:
版权声明:本文为博主原创文章,未经博主允许不得转载。