Python实现的简易HTTP代理服务器

Python实现的简易HTTP代理服务器Python实现的简易HTTP代理服务器使用socket编程实现代理服务器,首先它得是一个服务器至此,http代理服务器的核心代码已经完成于是一个非常令人尴尬的问题就出现了,在某一次读取完毕之后,我怎么知道我读完了呢?一旦如此,就会陷入读阻塞。至此,一个基本的http代理服务器就实现了,当然,出于健壮性考虑、debug方便和其它因素,实用化的代码会更长一点

大家好,欢迎来到IT知识分享网。

 

本篇源码及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协议?

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/23692.html

(0)
上一篇 2023-09-07 11:00
下一篇 2023-09-07 17:00

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信