HProxy -- 基于HOST的HTTP代理

OSI Approved :: MIT License
Python
ANY
上帝De助手 · 2019年08月29日 · 2315 次阅读 · 3 条评论

说到代理,大多数情况我们都会想到通过浏览器设置的正向代理,以及类似 nginx 的反向代理;而实际上除此之外还有一种基于 host 方式实现的代理,它可以解决如何录制服务端 http 接口请求信息。

本文主要讲述,如何实现一个基于 host 方式的 http 代理,以及它与普通代理之间的区别。这种方式的代理主要可以应用于哪些实际的测试场景。

与普通代理的区别

所谓的普通代理,就是我们日常会用到的那种代理,通常需要客户端本身支持,使用时对客户端进行代理信息配置。最常见的就是对浏览器、curl 等客户端配置代理,一般主要用来翻墙的!

而 host 代理则不是主流的代理方式,它的特点是通过设置 host 就能实现代理,而不需客户端本身支持,相对应用的访问更广一些。下面我们就来逐一对比下它们的具体区别:

对比项 普通代理 HOST 代理
需要客户端支持
设置方式 配置客户端 配置 HOST
支持透明代理
支持绝对路径
支持非 80 端口
实现方式 socket http
URL 路径支持 绝对路径 相对路径
代理服务与客户端同机 支持 不支持
代理配置方式 域名配置灵活 host 配置不灵活

通过对比可以发现它们都能满足基本的 HTTP 代理功能,主要区别在于适用的场景有所不同;普通代理只要客户端本身支持基本上什么请求都可以代理,HOST 代理只要请求是 80/443 端口都可以代理。

实现方式

接收请求

实现一个 HOST 代理是非常简单的,你只需要基于一个现成的 WEB 框架,比如:Flask,Tornado;再加上一个 url 请求框架即可,比如:requests。而首先你得实现一个可以接手任意 URL 路径的请求处理函数,如下:

from werkzeug.routing import BaseConverter
from flask import Flask, request, jsonify


class RegexConverter(BaseConverter):
    def __init__(self, url_map, *args):
        super(RegexConverter, self).__init__(url_map)
        self.url = url_map
        self.regex = args[0]   # 正则的匹配规则

    def to_python(self, value):
        return value


app = Flask(__name__, static_url_path='/do_not_use_this_path__')
app.url_map.converters['re'] = RegexConverter


@app.route('/<re(r".*"):path>')
def proxy(path):
    url = request.base_url
    query = request.args
    method = request.method
    headers = request.headers
    form_data = request.form
    body = request.data
    files = request.files

    payload = {
        'path': f'/{path}',
        'url': url,
        'method': method,
        'headers': dict(headers),
        'query': query,
        'form_data': form_data,
        'body': body.decode('utf-8'),
        'files': files
    }

    return jsonify(payload)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

启动这个 web 服务之后,只要在 host 文件中配置一个指向该服务所在机器的 IP 和目标域名映射即可。比如:

# host文件添加一个映射
10.0.0.1 www.baidu.com

之后,就可以在浏览器访问http://www.baidu.com这个网址了,路径内容和参数随便输,它都会完整把你请求的信息给返回来,类似一个镜像服务。效果如下:

代理请求

目前来说,我们已经完成 HTTP 代理的一半功能了,剩下的就是如何去发送获取到的 HTTP 请求,之后在把请求响应内容组装好,再发回给浏览器或客户端。首先是组装要发送的请求,样例代码如下:

class METHOD:
    GET = 'GET'
    POST = 'POST'
    PUT = 'PUT'
    DELETE = 'DELETE'
    HEAD = 'HEAD'
    OPTIONS = 'OPTIONS'

def warp_request_data(payload):
    """
    :param payload: {
        'path': path,
        'url': url,
        'method': method,
        'headers': dict(headers),
        'query': query,
        'form_data': form_data,
        'body': body.decode('utf-8'),
        'files': files
    }
    :return:
    """
    send_data = {'method': payload['method'], 'url': payload['url'],
                 'headers': payload['headers'], 'data': None, 'files': None}

    if payload['method'] in (METHOD.GET, METHOD.HEAD, METHOD.OPTIONS):
        send_data['data'] = payload['query']
    elif payload['method'] in (METHOD.POST, METHOD.DELETE, METHOD.PUT):
        if payload['query']:
            payload['url'] = f"{payload['url']}?{urllib.parse.urlencode(payload['query'])}"
        if payload['form_data']:
            ct = payload['headers'].get('Content-Type')
            if 'application/x-www-form-urlencoded' in ct:
                send_data['data'] = payload['form_data']
            elif 'multipart/form-data' in ct:
                send_data['data'] = payload['form_data']
                send_data['files'] = payload['files']
        elif payload['body']:
            send_data['data'] = payload['body']
        elif payload['files']:
            send_data['files'] = payload['files']

    return send_data

接着,通过requests发送组装好的请求数据内容。方法如下:

from requests import request as sender

def send_request(req):
    return sender(req['method'].lower(), req['url'], headers=req['headers'],
                  data=req['data'], files=req['files'])

最后,把获取到的响应进行再次组装,便于主程序直接返回内容给浏览器或客户端。代码如下:

def warp_response_data(rep):
    body = rep.content
    if 'Transfer-Encoding' in rep.headers:      # 不支持chunk
        del rep.headers['Transfer-Encoding']
        rep.headers['Content-Length'] = len(body)
    if 'Connection' in rep.headers:             # 不支持keep-alive
        del rep.headers['Connection']
    if 'Content-Encoding' in rep.headers:       # 不支持gzip
        del rep.headers['Content-Encoding']
    rep.headers['Server'] = 'host proxy/0.1'    # 修改服务器信息

    return {
        'code': rep.status_code,
        'headers': dict(rep.headers),
        'body': body
    }

同时,需要在 http 处理的主函数中添加对 http 请求的代理操作。最后的 http 处理主函数新增内容如下:

@app.route('/<re(r".*"):path>')
def proxy(path):
    req = Action.warp_request_data(payload)
    rep = Action.send_request(req)
    ret = Action.warp_response_data(rep)

    return ret['body'], ret['code'], ret['headers']

插件机制

目前为止完成了这么多功能之后,一个 http 的代理就已经初步完成了。只是目前仅仅是做了代理,但是没有任何的作用。因为我们不能对它进行任何操作。要让它变得有意义就得添加插件机制,让用户可以对代理的请求进行处理和操作。

首先,定义一个插件类,用于注册和执行插件内容。代码如下:

class Plugins:
    def fire(self, context):        # 插件事件触发
        for func in self.events:
            func(context)

    def register(self, func):       # 插件事件注册
        self.events.append(func)


class PRE_PROXY(Plugins):
    def __init__(self):
        self.events = []


class POST_PROXY(Plugins):
    def __init__(self):
        self.events = []


pre_proxy = PRE_PROXY()
post_proxy = POST_PROXY()


def before_proxy(func):         # 注册触发代理前装饰器
    pre_proxy.register(func)
    return func


def after_proxy(func):          # 注册触发代理后装饰器
    post_proxy.register(func)
    return func

同时,也要在 http 主函数中添加插件函数的调用,修改后的代码如下:

...
    context = {}
    req = Action.warp_request_data(payload)

    context['request'] = req
    pre_proxy.fire(context)         # 触发代理前事件

    rep = Action.send_request(req)
    ret = Action.warp_response_data(rep)

    context['response'] = ret
    post_proxy.fire(context)        # 触发代理后事件
...

最后,在同目录下新建一个插件脚本文件script.py。其内容如下:

from .plugins import before_proxy, after_proxy

@before_proxy
def before(context):
    print(context)

@after_proxy
def after(context):
    print(context)

这个脚本内容非常的简单,导入 2 个注册装饰器分别用来注册代理前和代理后的事件。注册函数可以接收到一个请求和响应的上下文对象参数,这里仅仅是打印了出来。

当然,插件还可以做很多其它的事情,比如:过滤特定 url 并保存请求信息;修改请求和响应信息内容等。完整项目代码请关注公众号并回复hproxy即可!

应用场景

这类 http 代理主要应用的场景一般多为测试或者开发,日常生活中翻墙还是要是普通代理。主要可以用于辅助测试,比如:mock 系统,api 接口测试等。

对于 mock 系统,可以用来录制 mock 内容,尤其是针对服务端请求第三方接口的请求录制。比如:

  • 录制调用第三方银行的接口请求,作为 mock 内容
  • 选择性的 mock 同域名下的部分 URL 请求,其它 URL 则透传

用于 api 自动化测试,可以直接录制对应接口的 API 请求,用于快速生成自动化测试用例。当然还可以用于安全测试,篡改指定的 http 请求内容。

后期功能增强

虽然目前该 demo 程序已经可以开始用于辅助测试工作了,但是想要更加的易用,还有很多的特性需要支持。

  • 协程支持
  • 过滤功能
  • https 支持
  • websocket 支持
  • keep-alive 支持
  • chunk 支持

评论列表
ZengTester 发表于 2020年01月30日

请问在代理的过程如何解决 https 双项代理的问题

上帝De助手 发表于 2019年09月16日
几许风雨 发表于 2019年09月07日

github 地址啥时候可以公布