lyyyuna 的小花园

动静中之动, by

RSS

使用 Python 实现一个简单的 HTTP 代理 - GET

发表于 2016-01

前言

HTTP 代理是位于服务器与客户端之间的中间实体,在各个端点之间来回传送 HTTP 报文。

按照用途分类,HTTP proxy 可以分为

按照代理对客户端的可见性又可以分为

本文要实现一个简单的 HTTP 非透明代理,暂时只支持 GET 请求的转发,且不追求性能和错误处理。

HTTP 非透明代理下 GET 请求的不同

除了部分请求头和 URL 之外,在非透明代理下,浏览器发给服务器和发给代理的报文是完全一样的。

  1. 首先浏览器会向非透明代理发送完整的绝对路径 URL。而在普通情况下只会发送相对路径,不需要主机名。
  2. 浏览器会用 Proxy-Connection 头部代替 Connection 头部。

为何要用绝对路径

早期的 HTTP 设计中,客户端只会与单个服务器进行通信,所以一旦 TCP 连接建立起来以后,只需要相对路径。

但代理就有问题,客户端首先和代理建立 TCP 连接,但由于传递的请求头中使用相对路径,代理就不知道使用什么 IP 和 端口来向远端的服务器建立 TCP 连接。所以,对于早期的 HTTP 1.0,强制客户端发给代理时使用完整路径,如

GET http://www.douban.com/ HTTP /1.0

较新的 HTTP 1.1 则规定了必须包含 Host 头部。所以对于 HTTP 1.1 的代理来说,完整 URL 不是必须的。但由于网络上还有大量旧版代理,Host 头部代理或许根本不识别,所以现在浏览器在使用代理时,还是会使用完整 URL。

以下是用 nc 监听 8888 端口,火狐配置 nc 8888 为代理时,所发送 GET 请求

root@yan:~# nc -lvp 8888
listening on [any] 8888 ...
Warning: forward host lookup failed for promote.cache-dns.local: Unknown host
connect to [192.168.27.128] from promote.cache-dns.local [192.168.27.1] 29798
GET http://www.douban.com/ HTTP/1.1
Host: www.douban.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: bid="UwoBYPAPsOE"; ll="118159"; __utma=30149280.478845192.1452777657.1452777657.1452777657.1; __utmz=30149280.1452777657.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _pk_id.100001.8cb4=d15426da1038582e.1452777681.1.1452777681.1452777681.
Connection: keep-alive
Cache-Control: max-age=0

可以看出使用的是完整路径。

为什么要用 Proxy-Connection 头部

Connection 头部是为了减少建立 TCP 连接的次数,复用连接产生的。默认 HTTP 1.1 是 Keepalive,但 1.0 的代理则不识别此头部。对于不认识的头部,代理会直接转发,以保持向后兼容性。

假如 Connection: Keep-alive 发给了代理,代理不识别转发给了服务器,而恰巧服务器识别此头部,便会出现严重问题。服务器和浏览器都保持连接,而代理则中断了连接。

为解决这个问题,出现了一个新的头部 Proxy-Connection。如果 1.1 的代理,代理会改写为 Connection 头部。如果 1.0 的代理,那么会直接转发此头部,服务器发现 Proxy-Connection 后,就会采用非长连接的方式。

Proxy-Connection 头部我实际实验的时候,并不是每个浏览器都会发送。同样是火狐,家里电脑直接发送了 Connection,而单位电脑则是 Proxy-Connection。

实现

我们要做的有

我采用 BaseHTTPServer 和 BaseHTTPRequestHandler 来处理浏览器发送的 GET 请求,原始套接字 socket 来转发请求给远端服务器,既没有多线程也没有 IO 复用。采用 urllib 库来解析完整路径 URL,并取出相对路径。

代码如下

    from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
    import socket
    import urllib

    class MyHandler(BaseHTTPRequestHandler):

        def do_GET(self):
            uri = self.path
            # print uri
            proto, rest = urllib.splittype(uri)
            host, rest = urllib.splithost(rest)
            # print host
            path = rest        
            host, port = urllib.splitnport(host)
            if port < 0:
                port = 80
            # print host
            host_ip = socket.gethostbyname(host)
            # print port

            del self.headers['Proxy-Connection']
            self.headers['Connection'] = 'close'

            send_data = 'GET ' + path + ' ' + self.protocol_version + '\r\n'
            head = ''
            for key, val in self.headers.items():
                head = head + "%s: %s\r\n" % (key, val)
            send_data = send_data + head + '\r\n'
            # print send_data
            so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            so.connect((host_ip, port))
            so.sendall(send_data)

            # 因为采用非长连接,所以会关闭连接, recv 会退出
            data = ''
            while True:
                tmp = so.recv(4096)
                if not tmp:
                    break
                data = data + tmp

            # socprint data
            so.close()

            self.wfile.write(data)


        # do_CONNECT = do_GET

    def main():
        try:
            server = HTTPServer(('', 8888), MyHandler)
            print 'Welcome to the machine...'
            server.serve_forever()
        except KeyboardInterrupt:
            print '^C received, shutting down server'
            server.socket.close()

    if __name__ == '__main__':
        main()