1. 程式人生 > >使用Tornado實現http代理

使用Tornado實現http代理

有時 fin support article edi cor 替換 ons async

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有兩個回調函數。我的理解是第一個回調在關閉的時候調用,第二個回調在不停讀出數據的時候調用。
寫出來用的效果還行:
技術分享

使用Tornado實現http代理