zoukankan      html  css  js  c++  java
  • 使用Tornado实现http代理

    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:
    ' + 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
    
    ')
    
            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有两个回调函数。我的理解是第一个回调在关闭的时候调用,第二个回调在不停读出数据的时候调用。
    写出来用的效果还行:
    原理图

  • 相关阅读:
    sql语句性能优化
    Windows版Redis如何使用?(单机)
    redis在项目中的使用(单机版、集群版)
    在windows上搭建redis集群(redis-cluster)
    Jenkins打包Maven项目
    numpy交换列
    Linq中join多字段匹配
    SpringMVC Web项目升级为Springboot项目(二)
    SpringMVC Web项目升级为Springboot项目(一)
    springboot读取application.properties中自定义配置
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/7157815.html
Copyright © 2011-2022 走看看