基於host的http代理--hproxy
- 2019 年 10 月 4 日
- 筆記
說到代理,大多數情況我們都會想到通過瀏覽器設置的正向代理,以及類似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支援