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协议?

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