pythonのWebフレームワークTornadoでhttpのプロキシサーバを作ってみた
PythonではTornadoというWebフレームワークを使うことで簡単にノンブロッキングWebサーバの開発ができるらしい。
最近ではnode.jsなどがノンブロッキングWebサーバとして注目を集めているが、文字列操作の点でpythonの方が優れていそうだったのでとりあえずpythonで実装してみることにした。
○まずはTornadoをPythonにインストール
Tornadoのインストールはpipで簡単に行える
pip install tornado
pipを使わない場合はソースコードを落としてきて、直接インストールできる。
tar xvzf tornado-4.1.tar.gz cd tornado-4.1 python setup.py build sudo python setup.py install
○Tornadoを使ってプロキシサーバを作成
以下のコードでプロキシサーバになる
#!/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import sys import os import tornado.httpserver import tornado.ioloop import tornado.iostream import tornado.web import tornado.httpclient import ssl class ProxyHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): return self.myRequest() @tornado.web.asynchronous def post(self): return self.myRequest() def myRequest(self): #self.render("test.html") def get_response( response): if response.error and not isinstance(response.error,tornado.httpclient.HTTPError): self.set_status(500) self.write('500 error:\n' + str(response.error)) self.finish() 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: print(self.request.uri) #self.write(response.body.replace("<body".encode("utf-8"), "<script type='text/javascript'>(function(){alert('hello');})();</script><body".encode("utf-8"))) self.write(response.body) #self.render(response.body) self.finish() req = tornado.httpclient.HTTPRequest( url=self.request.uri, method=self.request.method, body=self.request.body, headers=self.request.headers, follow_redirects=False, allow_nonstandard_methods=True) client = tornado.httpclient.AsyncHTTPClient() try: #コールバック関数にhandle_responseを指定。ここにアクセスしたレスポンスが入る client.fetch(req, get_response) except tornado.httpclient.HTTPError as e: if hasattr(e, 'response') and e.response: get_response(e.response) else: self.set_status(500) self.write('500 error:\n' + str(e)) self.finish() def run_proxy(port): app = tornado.web.Application( [(r'.*', ProxyHandler),] ) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(port) print("Server is up ...") tornado.ioloop.IOLoop.instance().start() #プロキシサーバを稼働させる if __name__ == "__main__": port = 8888 if len(sys.argv) > 1: port = int(sys.argv[1]) print ("Starting cache proxy on port %d" % port) run_proxy(port) #run_ssl_proxy(8888)
やっていることとしては、ポートの8888でサーバを立てておきリクエストを受け取ったらをそのまま中継させるだけだ。
サーバを立ち上げているのは以下の部分になります。
app = tornado.web.Application(
[(r'.*', ProxyHandler),]
)
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(port)
tornado.web.Applicationの引数で受け取るリクエストと呼びだすクラスを指定しています。
また、今回は指定していませんがSSL通信を行いたい場合は以下のようにオプションとして証明書と秘密鍵の指定が必要になります。
app = tornado.web.Application( [(r'.*', ProxyHandler),], template_path=os.path.join(os.getcwd(), "static"), static_path=os.path.join(os.getcwd(), "static") ) http_server = tornado.httpserver.HTTPServer(app, ssl_options={ "certfile":"/var/pyTest/ca2.crt", "keyfile":"/var/pyTest/ca2_out.key", #ssl_version= ssl.PROTOCOL_TLSv1 })
○ブラウザのプロキシとして設定する
ブラウザからアクセスする場合は、プロキシとして通す設定が必要になる。
Firefoxの場合はオプション→詳細→ネットワーク→接続設定で行うことができる。
ブラウザ設定後リクエストを飛ばしたらGET,POSTにかかわらずmyRequest()メソッドを実行するようにしています。リクエストは以下の部分で設定を行っています。内容としては受け取ったリクエストをそのままセットしているだけです。
req = tornado.httpclient.HTTPRequest( url=self.request.uri, method=self.request.method, body=self.request.body, headers=self.request.headers, follow_redirects=False, allow_nonstandard_methods=True) client = tornado.httpclient.AsyncHTTPClient()
リクエストのコールバックはclient.fetch(req, get_response)で指定しています。
それからリクエストの送信とエラー時の処理を定義しています。
if hasattr(e, 'response') and e.response: get_response(e.response) else: self.set_status(500) self.write('500 error:\n' + str(e)) self.finish()
コールバックメソッド実行時結果が500エラーなら500 errorとクライアントに表示します。
if response.error and not isinstance(response.error,tornado.httpclient.HTTPError): self.set_status(500) self.write('500 error:\n' + str(response.error)) self.finish()
結果を受け取れた場合、レスポンスヘッダとボディにそのままの内容を書き込んでいます。
self.writeの部分で文字列操作を行うと、クライアント側の表示を書き換えることができます。今回は使っておりませんが、lxmlというサードパーティモジュールがhtmlの操作に便利そうです。
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: print(self.request.uri) #self.write(response.body.replace("<body".encode("utf-8"), "<script type='text/javascript'>(function(){alert('hello');})();</script><body".encode("utf-8"))) self.write(response.body) #self.render(response.body) self.finish()
結果ですが、httpプロトコルについてはうまくプロキシサーバとして動いておりましたがhttpsでSSL通信を行おうとしたらエラーが発生しました。自分で準備したオレオレ証明書と秘密鍵の部分でエラーが発生していたので、ちょっと調べたいと思います。
→ちょっと調べてみました。どうやらこのやり方だとクライアント→プロキシサーバ間もssl通信をする想定になっているようだが、クライアント側のFirefoxでssl通信のプロキシとして指定したらクライアント→プロキシサーバ間でssl通信を行っていないようなのでエラーになってそうな気がする。
ブラウザのプロキシに指定するのではなくhttps://localhost:8888/https://google.co.jpとかでリクエストは受け取れていたので多分そうです。
まあ、httpsの場合はプロキシサーバはキャッシュの転送だけを行うルータのように動くのが前提のプロトコルのようなので、無理そうか
HTTPSプロキシの仕組み - ソフトウェアエンジニア現役続行