少し凝ったことを(簡単に)できるリバースプロキシサーバーをGoで書いてみた
リバースプロキシで少し凝ったことをする際、Nginx+Luaで頑張るなど、いくつか手法があります。しかし、(個人の意見ですが)どれも使い勝手がいいとは言えません。そこで、「使い勝手のよく、少し凝ったことのできるリバースプロキシサーバー」を作ってみました。
実装はGoで、レポジトリはここです。
名前はScript+ProxyでScriproxyです。安直ですね。
特徴
- スクリプトを用いて、アップストリームサーバー(プロキシ先)を動的に設定できるリバースプロキシサーバーです。リクエストヘッダーの値や、URLクエリの値、リクエストパスなどをスクリプト内で用いることができます。
- Tengoを使っており、Goのような直感的な文法でスクリプトを書くことができます。また、よくできた標準ライブラリもあります。
- アップストリームを指定するだけでなく、リクエストヘッダーの値やクエリの値、リクエストパスを書き換えることもできます。
インストール
$ go install github.com/kkty/scriproxy
使い方
$ scriproxy --help Usage: scriproxy [OPTIONS] Application Options: --script= The path to a tengo script for rewriting requests --libraries= The tengo libraries used in the script, separated by commas Help Options: -h, --help Show this help message
--script
コマンドライン引数で、スクリプトへのパスを指定します。スクリプトの例は、次のセクションで紹介します。
Tengoを内部で使っています. Tengoには質の良い標準ライブラリがあり、それをスクリプト内で用いることもできます。その際は、--libraries
コマンドライン引数で、使用しているライブラリを指定する必要があります。例えば、text
ライブラリとfmt
ライブラリを用いたい場合には--libraries=text,fmt
のようにします。
つまり、具体的な起動コマンドは以下のようになります。
$ scriproxy --script /path/to/script.go --libraries fmt,text
スクリプト例
(実際のシチュエーションを想定したものというよりも、)Scriproxyによって何が可能なのかがわかりやすいものを紹介します。もちろん、これらを組み合わせて更に複雑なこともできます。
スクリプト内で使える値や関数については次のセクションで補足しています。
シンプルなプロキシ
// req.urlは、プロキシ先のURLに対応します req.url.scheme = "https" req.url.host = "example.com" // req.hostは、リクエストヘッダの`Host: ...`に設定される値に対応します // 通常はこれを設定する必要があります(多くの場合、req.url.hostと同じ値になります) req.host = "example.com"
このスクリプトによって、プロキシサーバーに対して送られたリクエストはhttps://example.com
に流されます。リクエストヘッダの値やクエリパラメータの値は変更されずにプロキシ先に送られます。
クエリパラメータを用いる場合
アップストリームサーバーの選択にクエリパラメータを用いることができます。
// クエリパラメータから値を取得する req.url.host = req.url.query.get("host") req.url.scheme = req.url.query.get("scheme") // アップストリームサーバーへのリクエストにはそれらのパラメータが含まれないようにする req.url.query.del("host") req.url.query.del("scheme") // これは多くのケースで必要! req.host = req.url.host
このスクリプトを用いると、 /foo?host=example.com&scheme=http
へのリクエストはhttp://example.com/foo
に、/foo?host=example.org&scheme=https
へのリクエストはhttps://example.org/foo
へと流されます。
Hostヘッダーを用いる場合
リクエストヘッダーのHostの値を用いてプロキシ先を指定することもできます。
// `--libraries=text`をコマンドライン引数で指定する必要があることに注意! text := import("text") // "example.com.local"や"example.com.secure.local"のような値がHostヘッダーとして指定されていることを前提としている splitted := text.split(req.host, ".") l := len(splitted) if splitted[l-2] == "secure" { req.url.scheme = "https" req.url.host = text.join(splitted[:l-2], ".") } else { req.url.scheme = "http" req.url.host = text.join(splitted[:l-1], ".") } req.host = req.url.host
(Scriproxyに対するリクエストの)リクエストヘッダにHead: example.com.secure.local
が指定されていればhttps://example.com
に、example.com.local
が指定されていればhttp://example.com
にリクエストがプロキシされます。
リクエストヘッダを用いる場合
Host以外のリクエストヘッダを用いることもできます。
// `--libraries=text`をコマンドライン引数で指定する必要があることに注意! text := import("text") // "User-Agent"ではなく"user-agent"とすることも可能 // 関数の挙動は https://golang.org/pkg/net/http/#Header.Get に沿っている ua := req.header.get("user-agent") ua = text.to_lower(ua) if text.contains(ua, "iphone") { req.url.host = "example.com" } else { req.url.host = "example.org" } // ユーザーエージェントの値を上書き req.header.set("user-agent", "my-proxy") req.url.scheme = "https" req.host = req.url.host
このスクリプトを用いると、iPhoneからのリクエストはhttps://example.org
に、それ以外のリクエストはhttps://example.com
に流されます。
req.header.set("host", "...")
は機能しません。Hostヘッダーを書き換える際にはreq.host
を書き換える必要があります。これは、Goのhttp.Requestの挙動に沿っています。
補足
req.host
,req.url.scheme
,req.url.host
andreq.url.path
が値として使え、書き換えることもできます。req.url.query.get("key")
,req.url.query.set("key", "value")
,req.url.query.del("key")
の4つの関数がクエリパラメータの参照/書き換えに使用できます。req.header.get("key")
,req.header.set("key", "value")
,req.header.add("key", "value")
,req.header.del("key")
の4つの関数がリクエストヘッダの参照/書き換えに使用できます。- 以上の値/関数の挙動はGoのhttp.Requestと似せています。
- 4コアマシンで秒間数千リクエスト通せるはずです。
- ロギングの機能も入れています。