Tornado 非同步特性使其成為實作 HTTP 反向代理的優秀選擇。本文實作一個通用代理處理器,支援 GET/POST 透傳、Header 重寫、重導向控制、錯誤碼映射,適用於微服務閘道、API 聚合、跨域代理等場景。
# -*- coding: utf-8 -*-
import logging
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.httpclient
from tornado.web import HTTPError, asynchronous
from tornado.httpclient import HTTPRequest
from tornado.options import define, options
try:
from tornado.curl_httpclient import CurlAsyncHTTPClient as AsyncHTTPClient
except ImportError:
from tornado.simple_httpclient import SimpleAsyncHTTPClient as AsyncHTTPClient
define("port", default=8888, help="run on the given port", type=int)
define("api_protocol", default="http")
define("api_host", default="feilong.me")
define("api_port", default="80")
define("debug", default=True, type=bool)
class ProxyHandler(tornado.web.RequestHandler):
@asynchronous
def get(self):
if options.debug:
return self.post()
else:
raise HTTPError(405)
@asynchronous
def post(self):
protocol = options.api_protocol
host = options.api_host
port = "" if options.api_port == "80" else ":%s" % options.api_port
uri = self.request.uri
url = "%s://%s%s%s" % (protocol, host, port, uri)
# 重寫 Host Header 指向上游
headers = dict(self.request.headers)
headers["Host"] = host
try:
AsyncHTTPClient().fetch(
HTTPRequest(
url=url,
method="POST",
body=self.request.body,
headers=headers,
follow_redirects=False
),
self._on_proxy
)
except tornado.httpclient.HTTPError as x:
if hasattr(x, "response") and x.response:
self._on_proxy(x.response)
else:
logging.error("Tornado signalled HTTPError %s", x)
def _on_proxy(self, response):
if response.error and not isinstance(response.error, tornado.httpclient.HTTPError):
raise HTTPError(500)
else:
self.set_status(response.code)
for header in ("Date", "Cache-Control", "Server", "Content-Type", "Location"):
v = response.headers.get(header)
if v:
self.set_header(header, v)
if response.body:
self.write(response.body)
self.finish()
def main():
tornado.options.parse_command_line()
application = tornado.web.Application([
(r"/.*", ProxyHandler),
])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
python proxy.py --port=8888 --api_host=feilong.me
# 訪問 https://localhost:8888/ 顯示 feilong.me 首頁
| 功能 | 實作方式 |
|---|---|
| 非同步代理 | @asynchronous + AsyncHTTPClient.fetch() 回調 |
| Header 透傳 | 複製 self.request.headers,覆寫 Host |
| Body 透傳 | self.request.body 直接轉發 |
| 重導向控制 | follow_redirects=False 避免循環 |
| 錯誤處理 | 捕獲 HTTPError,有回應則代理回應,否則 500 |
| 回應 Header 選擇性複製 | 僅複製 Date、Cache-Control、Server、Content-Type、Location |
/api/v1/* → 不同上游)