それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

画像プロキシを Golang で作った話

はじめに

最近、アマゾンプライムビデオで「ミッションインポッシブル:フォールアウト」を見ていて最後に個人的にあまり好きでない3作目から登場していた元妻でなく前作でいい感じだった美人スパイとトム・クルーズがくっついて「この展開を待っていたああああああああ!!」と静かにガッツポーズしたけんつです。

この前まで Golang で並列処理の勉強をしていたけども急に忙しくなってしまい全く進んでいない現実に向きあいたくなくて今回は GolangSSL 画像プロキシを作りました。
そのことを雑に書いていこうかなと思っています。

なに作ったか

github.com

すごいもの作った感じのタイトルを書いたけど実は参考にしたというか Nodejs 製の画像プロキシを Golang で書きなおしただけ。
参考にしたのはこちら。

github.com

結構使われている画像プロキシで

Togetter とか
qiita.com

Qiita とか
qiita.com

探せば色々なサービスで使われていたりする。
Github の README 内に含まれる画像達もこれで配信されているらしい。
というのも、元々外部リソースを参照するときに HTTPS 化するときに全てのコンテンツが HTTPS で提供される必要があるのだが
Togetter のようなキュレーションサイトや Github, Qiita のような外部の画像を扱う可能性のあるものでは常にそれらが HTTPS で提供されているものを使えるというわけではなく
HTTP で提供される外部リソースをユーザが埋め込んでしまう場合がある。
そうなると、 Mix content になってしまいサービス自体を HTTPS で提供できないという問題がある。

そのような問題を上記の Camo では解決できる。具体的には何をするかというと Togetter の Qiita にわかりやすく要点が纏められていた。

実はCamoそのものはSSLプロキシ機能を提供していません。
Camoが提供する最も重要な役割は、プロキシ用URLに共通鍵から生成したダイジェストを含めて暗号化することなのです。

画像プロキシサーバを運用する上で懸念されることとして、プロキシ用URLに含まれるオリジン画像のURLを自由に書き換えられた結果、サービサーが想定していないリソースを配信してしまうことなどがあります。
共通鍵で暗号化することで、有効なプロキシ用URLを生成できるのは共通鍵を知るサービサーに限定することが可能になるのです。

これ考えた人、本当に天才ではと思う。

というわけで何か Golang で作りたくて、今回はこれの Golang 実装を作ってみた。

結構苦戦した

いくつか面倒だった部分となんとかこの機能を作るまでに色々と苦戦した部分をまとめる。

Proxy VS Reverse Proxy

最初は Golang で提供されている ReverseProxy 用の構造体を使って作ろうと思ったけど、あれを使うと Golang 側で Request と Response のハンドラーが完全に分離してしまい
'/', '/status' といった通常のレスポンスを返すルーティングを持つものが作りにくかったのでやめた。

普通に HTTP サーバを作るのと同じ要領で作り、画像プロキシのルーティングが呼ばれた時は NewRequest で新しくリクエストを作り直し得た画像情報を ResponseWriter でクライアントに返却するようにしている。

HMAC SHA1

この画像プロキシでは HMAC SHA1 で生成された 40 文字のダイジェストを元に鍵を確認しているがそこが問題だった。
SHA1 を吐けるものは Golang にも存在するがそれを使うと地味に鍵の設定がしづらい。

Go言語でSHA-1 - Misc Notes

そこで "crypto/hmac", "crypto/sha1"というパッケージを使った方が簡単だったからそうした。
参考↓
GoでHMAC SHA1を計算するサンプル · GitHub

Url Encoded Path

これは自分の勘違いのせいでめちゃくちゃ苦戦した。

camo は以下の形式でリクエストを受け取る。

http://example.org/digest?url=image-url
http://example.org/digest/image-url

この時、1つ目のパターンでは image-url を url encode された形式で渡しているが2つ目のパターンでは 16 進数に変換された image-url を受け取る必要がある。
これを両方 url encode された URL だと勘違いしてめちゃくちゃハマった。

というのも、なぜ URL Encode された情報をパスに出来ないかというと RFC にはこう書かれている。
https://tools.ietf.org/html/rfc3986#section-6.2.2.2

The percent-encoding mechanism (Section 2.1) is a frequent source of
variance among otherwise identical URIs. In addition to the case
normalization issue noted above, some URI producers percent-encode
octets that do not require percent-encoding, resulting in URIs that
are equivalent to their non-encoded counterparts. These URIs should
be normalized by decoding any percent-encoded octet that corresponds
to an unreserved character, as described in Section 2.3.

正規化されるのだ。

今回は gorilla mux をルーティングに使用したがそれを無効にするオプションがあるのになぜか上手くそれが解決されない問題があって苦戦した。
実際には2つ目の は 16進数表現されたものであるとわかってなるほどという気持ちになった。

そして2つ目のパターンは実装がめんどくさすぎるのとテストするときにわざわざ url を 16 進数に変換したものを使うひつようがあるのがまためんどくさくて実装していない。

使い方

ほとんど camo と同じ使い方でいける。
ただ先程も書いたが 2つ目のパターンはまだ実装していないから使えない。

http://example.org/digest?url=image-url:urlencoded

このパターンなら行ける。

おわりに

ミドルウェア書くの楽しい。