专栏首页TestQA基于host的http代理--hproxy

基于host的http代理--hproxy

说到代理,大多数情况我们都会想到通过浏览器设置的正向代理,以及类似nginx的反向代理;而实际上除此之外还有一种基于host方式实现的代理。

本文主要讲述,如何实现一个基于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支持

本文分享自微信公众号 - TestQA(testqna),作者:five3

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • py-ops面向编程的关键字测试框架

    这是一款基于pytest封装,同时支持关键字和BDD,测试数据分离,面向编程,轻量级的,对上层自动化应用友好的基础测试框架。

    上帝De助手
  • 必知必会 - 使用kafka之前要掌握的知识

    消息队列是分布式系统架构中不可或缺的基础组件,它主要负责服务间的消息通信和数据传输。市面上有很多的开源消息队列服务可以选择,除了kafka,还有Activemq...

    上帝De助手
  • 如何进行“花式”HTTP接口测试

    曾经接手过一个HTTP的接口项目,主要业务逻辑是一个分仓发货的物流子系统。可以通过HTTP的POST方式发送请求,并返回一个XML格式的内容。

    上帝De助手
  • 数据脱口秀 | 埃隆·马斯克和他的13个疯狂脑洞

    大数据文摘
  • 女统计学家爆学术界丑闻,Google研究人员涉嫌性骚扰?

    作者:Bing 来源:论智 NIPS是学术界最受关注的大型会议之一,上周刚刚落下帷幕。我们应该感谢并祝贺那些为机器学习以及人工智能领域做出重大贡献的研究专家,许...

    企鹅号小编
  • redis数据库

    sofu456
  • 并发编程之master-worker模式

    一、简介 Master-Worker模式是常用的并行设计模式。它的核心思想是,系统有两个进程协议工作:Master进程和Worker进程。Master进程负责接...

    lyb-geek
  • Hexo更换为Google Fonts思源宋体!

    对于中文书籍来说,宋体一直是正文印刷的标准字体,而不是目前电子显示屏上普遍的黑体,因为宋体的衬线更适合长时间阅读。

    BessCroft
  • Master-Worker模式

    原文地址:https://www.relaxheart.cn/to/master/blog?uuid=80

    RelaxHeart网
  • 程序员进阶之算法练习(二十四)

    前言 已经有三个月未更新算法文章,大厂工作环境是步步紧逼的,使得所有的人越来越忙碌。余下的学习时间,要用于技术预研、知识面开阔、底层原理理解等等,慢慢算法只能占...

    落影

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动