0x00 http代理
http代理的用處非常多,市面上也有公開的代理,可是有時候為了工作須要,比方分析應用層流量、做數據訪問控制、甚至做監控等等。Tornado提供了一些非常方便的環境和API,我們能夠基於Tornado輕松實現一個http代理。
0x01 實現原理
http代理主要做client和web服務器之間的轉發。這是大家都熟悉的場景,但僅僅限於http協議的情形。對於https的情況。這時候代理僅僅作為TCP中繼進行信息中轉,須要單獨處理。
0x02 Tornado實現
基於Tornado能夠實現一個異步的http代理,性能優越,實現也簡單,主要使用的類是AsyncHTTPClient,IOStream。
閱讀過Tornado源代碼的同學可能對這兩個類並不陌生。
這里還是簡單說下,AsyncHTTPClient顧名思義,是用來做異步HTTPclient請求的。而IOStream是對socket的一層封裝。
AsyncHTTPClient就是用來處理普通的http請求的。RequestHandler獲取client請求之后,proxy須要解析client的請求並使用這個類來請求服務器,拿到response,然后寫給client。打完收工。
對於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()
沒啥好說的。接到client請求。直接去請求服務器即可了。異步回調函數是on_response,這個函數里就處理proxy和client的交互即可了。self.write(response.body)
你懂的。
這里有個坑。就是寫headers的時候。把response的headers照搬設置一遍是會出錯的,造成訪問失敗。這里我的處理方法是僅僅寫RequestHandler中self._headers
存在的頭即可。
TCP中繼實現
對於443端口或者瀏覽器的connect請求。代理僅僅能從TCP層入手。轉發整個HTTP報文。這里使用的是http協議中的connect方法,在RequestHandler中實現這種方法即可了。
這里要注意。Tornado默認是不支持http的connect方法的,所以要改動SUPPORTED_METHODS
參數才行:
這里在RequestHandler中加入一個SUPPORTED_METHODS
替換父類的即可:
SUPPORTED_METHODS.append('CONNECT')
順便說下connect方法,這種方法被調用的時候,代理不用關系http層請求的詳細內容,而是直接從TCP層轉發這個報文給服務器。
收到時,也是相同的轉發給client。
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轉發。
第二行的作用就是將服務器數據寫到clientsocket中了,和上面一樣。沒啥好說的。寫入的功能就在四個回調函數中。
有人奇怪為啥read_until_close有兩個回調函數。我的理解是第一個回調在關閉的時候調用,第二個回調在不停讀出數據的時候調用。
寫出來用的效果還行: