Python實現的簡易HTTP代理服務器

 

本篇源碼及Ctrl+C+V的來源參考這個

使用socket編程實現代理服務器,首先它得是一個服務器,因此我們有第一篇參考代碼:

server = socket.socket()
server.bind(('127.0.0.1',8000))
server.listen(3)
conn, addr = server.accept()
data = True
while data :
    data = conn.recv(1024)
    msg = raw_input()
    if msq=="any code you mean to exit": break
    conn.sendall(msg)
conn.close()
server.close()

它做了這幾件事:

1.啓動服務,監聽端口8000,並設置爲允許3個客戶端排隊(雖然實際上只支持一個客戶端進行訪問)

2.接受請求,在連接中接收和返回數據

3.當客戶端關閉時,recv會得到空字符串,因此退出循環、結束程序

不妨就用上面的這個程序接收請求,看一看我們的代理服務器究竟要處理什麼:

GET http://www.sina.com/ HTTP/1.1
Host: www.sina.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1


 

注意到,最下面有兩個空行,這是約定,請求頭與請求體之間用\r\n\r\n來分割

爲了看的更清楚,我們可以讓它以unicode顯示

['GET http://www.sina.com/ HTTP/1.1\r\nHost: www.sina.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n']

其中的第二行Host就是我們要獲取的目標服務器地址,當然http的默認端口號是80

只要得到目標服務器的地址和端口號,我們就可以將這個請求原封不動的丟給目標服務器了,至於怎麼獲取這個目標地址,反正看起來也不難,我們可以假裝它已經實現了。

與上述服務器代碼不同,我們不需要input,也不需要循環處理數據,只需要接受完數據、把它丟給服務器就可以了,然後從目標服務器返回數據的過程恰好相反,需要從target中recv,向conn中sendall,因此:

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(3)
conn, addr = server.accept()
data = conn.recv(1024)
print data
# 假裝已經實現了getHost
host, port = getHost(data)
target = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target.connect((host, port))
target.sendall(data)

data = target.recv(1024)
print data
conn.sendall(data)

target.close()
conn.close()
server.close()

對www.sina.com的測試得到了這樣的報文:

HTTP/1.1 302 Moved Temporarily
Server: nginx
Date: Thu, 14 Mar 2019 11:25:58 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: https://www.sina.com.cn/
X-Via-CDN: f=edge,s=cmcc.shandong.ha2ts4.82.nb.sinaedge.com,c=223.72.94.28;
X-Via-Edge: 15525627584351c5e48df7d53c0784e9a7612

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

報文稱,這個網站已經搬家了,不再使用http協議進行訪問了,以後要上新浪網應該使用https://www.sina.com.cn/這個網址

顯然,這是因爲我太落伍了,在https大行其道的年代連傳統的http代理都沒學會

無論如何,這樣的結果至少表明我們正常的接收了客戶端與服務器端的響應,並且測試會發現,瀏覽器可以正常訪問到新浪網(因爲它跳轉到https協議上不再經過http代理)


至此,http代理服務器的核心代碼已經完成,接下來的任務是對這部分代碼進行優化。

首先我們假裝這個服務器啓動命令中可以接收一個整數作爲端口號,然後假裝我們的服務器可以服務於多個不同的客戶端,這意味着對於每個客戶端需要分別啓動新線程,因此:

def main(_, port=8000):
    myserver = socket.socket()
    myserver.bind(('127.0.0.1', port))
    myserver.listen(1024)
    while True:
        conn, addr = myserver.accept()
        thread_p = threading.Thread(target=thread_proxy, args=(conn, addr))
        thread_p.setDaemon(True)
        thread_p.start()

if __name__ == '__main__':
    main(*sys.argv)
    sys.exit(0)

當然了,我們的服務器很流氓,不提供退出方法,所以這是一個死循環

對每一個thread_proxy,我們需要完成三件事:1.找到目標服務器。2.轉發請求報文。3.轉發響應報文。

注意到我們在之前的簡易服務器代碼中寫的recv參數固定爲1024,這是不是意味着我們只能對請求長度小於1024的請求進行代理,超出長度概不負責?這當然是不合適的!因此我們需要將它設置的非常大循環讀取直到讀完。

於是一個非常令人尷尬的問題就出現了,在某一次讀取完畢之後,我怎麼知道我讀完了呢?

一個非常直觀的想法是:如果我讀到的長度等於預設的長度,那就是沒有讀完,否則就是讀完了。然而無論是客戶端還是瀏覽器,都不知道你預設的長度是多少,因此總是存在“整倍數”的概率,而且這個概率並不太低。一旦如此,就會陷入讀阻塞。

請求頭中有一個字段【content-length】被用於描述請求體的長度,如果沒有這樣的字段,那麼約定\r\n0\r\n\r\n爲休止符

雖然網上查到的結論有些深奧,但簡單來說就是上面這句話。再加上我們之前就掌握了的\r\n\r\n分割符,形成這樣一組手段:

  1. 切取請求頭
    def splitHeader(string):
        i, l = 3, len(string)
        while i<l and (string[i] != "\n" or string[i-3:i+1] !="\r\n\r\n") : i+=1
        return string[:i-3]
  2. 從請求頭中尋找信息(host、content-length)
    def getHeader(header, name):
        name = name.upper()
        base, i, l = 0, 0, len(header)
    
        while i<l:
            # 行入口,尋找冒號
            while i<l and header[i] != ":" : i+=1
            # 判斷信息頭
            if i<l and header[base:i].strip().upper() == name:
                # 此行即爲所求,從冒號後截斷
                base = i+1
                while i<l and not(header[i] == "\n" and header[i-1] == "\r") : i+=1
                return header[base:i-1]
            else:
                # 此行非所求,跳過此行
                while i<l and not(header[i] == "\n" and header[i-1] == "\r") : i+=1
                base, i = i+1, i+1
        # 所求不存在
        return None
  3. 根據約定獲取全部報文
    def recvBody(conn, base, size):
        if size==-1:
            while base[-5:] != "\r\n0\r\n\r\n" : base += conn.recv(RECV_SIZE)
        else:
            while len(base)<size:base += conn.recv(RECV_SIZE)
        return base

有了這些給力的手段做支撐,現在可以寫thread_proxy了,爲了便捷起見,事實上很多服務器也約定,報文的頭信息不能太長,這給了我們一個保障:在指定的長度內一定能夠獲取完整的頭信息,將這個長度設置爲MAX_HEADER_SIZE,有:

def thread_proxy(client, addr):

    request = client.recv(MAX_HEADER_SIZE)
    requestHeader = splitHeader(request)
    raw_host = getHeader(requestHeader, "Host")
    host, port = transHost(raw_host)
    # body也可能是空字符串,若如此則不必處理
    if len(requestHeader) < len(request)-4:
        content_size = getHeader(requestHeader, "content-length")
        size = len(requestHeader) + 4 + int(content_size) if content_size else -1
        request = recvBody(client, request, size)

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.connect((host, port))
    server.sendall(request)

    response = server.recv(MAX_HEADER_SIZE)
    responseHeader = splitHeader(response)
    if len(responseHeader) < len(response)-4:
        content_size = getHeader(responseHeader , "content-length")
        size = len(responseHeader) + 4 + int(content_size) if content_size else -1
        response = recvBody(server, response , size)

    client.sendall(response)
    server.close()
    client.close()

其中transHost是一個異常簡單的小方法,只是處於處理默認值的方便,單獨提煉出來:

def transHost(raw_host):
    for i in range(len(raw_host)): 
        if raw_host[i] == ":" : return raw_host[:i].strip(), int(raw_host[i+1:])
    else : return raw_host.strip(), 80

len(responseHeader)+4+int(content_size)是技術不足技巧來補的解決方案,目的是實現對報文長度的控制

至此,一個基本的http代理服務器就實現了,當然,出於健壯性考慮、debug方便和其它因素,實用化的代碼會更長一點,完整的代碼點擊這裏

然而https據說會更復雜,截至目前,我連示意圖都還沒看懂。真希望有個大佬教我SSL協議?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章