基於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支援

Tags: