ngx_mrubyを使って簡易ファイル共有システムを作る

mod_mruby ngx_mruby Advent Calendar 2014 の22日目(12/22)の投稿です。遅ればせながら申し訳ありません…

#はじめに
仕事上でのファイルのやり取りでdr○pb○xを使用していたのですが、たまに(というか頻繁に)気を付けてないと上限に簡単に引っかかっていました。
その時一時的に手元のnginxに設定をゴニョゴニョ手動で追加して対応したのですが、

* 十分複雑なダウンロードkeyを自動で決める
* BASIC認証をかける
* IP制限をかける
* 任意の頃合いに配布を取り下げる(時限取り下げ)

というのを都度やっていくのはそこそこ面倒で、だけどアプリ化するまでもなく。ただサンプルとして「これngx_mrubyでやったら面白いかも」と、作ってみることにしました。

nginx側でアプリを書くのはどうなのか賛否あるかと思いますが、ここまで来たら一度突き抜けてしまえ！と思いを振り切って書いてみました。が！結局色々と問題や仕事に引っかかり、思っていたような仕組みにならず、もんにょりしております。
やってみるとngx_mrubyでやれるnginxの制御のやりかたも見えてきて、面白かったです。

https://github.com/takeswim/ngx_mruby_box

#目標
* POST /upload APIの作成
in:IP制限・basic認証アカウント/パスワード・取り下げ時限
out:ランダムkey(JSON)

* GET /download APIの作成
in:ランダムkey、out:ファイル
basic認証・ダウンロード取り下げの機能


フロントエンドも多分mruby側で書けるんじゃないの？とも思いましたが、一旦スルーです。

```
[POST]/upload?filename=xxx.tar.gz&allow=192.168.0.0,192.168.0.1&user=hoge&pass=moge&limit=30
response ... {"key":"xxxxxxxxxxxxxxxxxx"}

[GET]/download?key=xxxxxxxxxxxxxxxxxx
response ... filename:xxx.tar.gz
```

#/downloadを実装してみる
/downloadを先に作るのが簡単そうなので着手。
redisに以下のkeyでvalueを各々入れます。
DBX:[randkey]:filename
DBX:[randkey]:allow
DBX:[randkey]:auth

```
[takeswim@instance-1 ___]$ redis-cli 
127.0.0.1:6379> set DBX:hogehoge:filename filename.txt
OK
127.0.0.1:6379> set DBX:hogehoge:allow　192.168.0.0,192.168.0.1
OK
127.0.0.1:6379> set DBX:hogehoge:auth take:hara
OK
```

limitがついたら各レコードsetexで期限つきレコードで設定することにしますが、開発中思い出したように消えると面倒なので後回しにします。

##IP制限
allowにremote_ipが含まれているかを確認する実装をします。含まれていなかったら403エラー。

```
flg_allow = false
allowcfg = redis.get("DBX:"+v.arg_key+":allow")
if (allowcfg)
  allows = allowcfg.split(",")
  if (allows.include? c.remote_ip)
    flg_allow = true
  end
end
if (!flg_allow)
  Server::return Server::HTTP_FORBIDDEN
else
:
```

## BASIC認証
mod_mruby側でBASIC認証に関する実装方法があったのですがhttp://blog.matsumoto-r.jp/?p=3072
ngx_mrubyの方には同じ機能はありません。
恐らくヘッダを調整すれば何とかなるだろうとは思いつつ、タイミングよく@matsumotoryさんからもヒントを頂き…

<blockquote class="twitter-tweet" lang="ja"><p><a href="https://twitter.com/takeswim">@takeswim</a> mruby_access_handlerで認証通らなければ403返すようにすれば良さそうですね。</p>&mdash; MATSUMOTO, Ryosuke (@matsumotory) <a href="https://twitter.com/matsumotory/status/545065853834629120">2014, 12月 17</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

```
def CHECK_AUTH(cfg, req, salt)
  if (cfg && cfg == req)
    return true
  end
  return false
end
Server = get_server_class
userdata = Userdata.new "redis_data_key"
redis    = userdata.redis
v        = Server::Var.new
r        = Server::Request.new
flg_auth = false
r.headers_in.all.keys.each do |k|
  if (k == 'Authorization')
    auth = redis.get("DBX:"+v.arg_key+":auth")
    if (CHECK_AUTH(redis.get("DBX:"+v.arg_key+":auth"), r.headers_in['Authorization'], "SALT"))
      flg_auth = true
    end
    break
  end  
end
if (!flg_auth)
  r.headers_out['WWW-Authenticate'] = 'Basic realm="dbx"'
  Server::return Server::HTTP_UNAUTHORIZED
else
  # TROUGH
end
## EOF ##
```

以下の場合エラーを発生するように実装。

* headerにAuthoriszationが無い
* keyに対する認証情報が無い
* keyに対する認証情報が一致しない

これでブラウザでアクセスするとBASIC認証画面が表示されます。厳密にはパスワードhashした方がいいですが、時間が無いので一旦スキップ…

(12/25 0:25追記)
DBX:hogehoge:authの値とheader['Authorisation']とを比較していますが、実際にheader['Authorisation']に入るのは、
`Basic dGFrZTpoYXJh`
Basicの後に続いている文字列は「ユーザー名：パスワード」がBase64になったものです。
なので、この開発段階では後追いで

```
127.0.0.1:6379> set DBX:hogehoge:auth "Basic dGFrZTpoYXJh"
OK
```

と値をセットし直しています。

## 配信するファイルにリダイレクトさせる(つもりが…)
ここはredirectを使い直接ファイル参照させよう、と思っていたのですが…

```
open() "/usr/local/nginx/html/tmp/rdx/filename.tar.gz" failed (2: No such file or directory)
```
make時のdocument_rootが邪魔をして絶対path指定できない…

この問題もそうなのですが、/uploadでbodyにあるデータを保存するところでも引っかかり、一旦redis側にデータ本体を保存することにしました。`DBX:[randkey]:body`が追加になります。

```
127.0.0.1:6379> set DBX:hogehoge:body "this is body of filename.txt"
OK
```

ブラウザで/download?key=hogehogeでアクセス、hogehoge.txtでファイルダウンロードに成功しました。

#/upload
## bodyの読み取り
前述のとおりこれが上手くいかない。Nginx::Request.new.bodyで受け取れるかと思ったのですが…
[＠Marcyさんの5日目の情報を元に](http://qiita.com/Marcy/items/1a7ab35970c31d169bf8#post)、一旦フォームデータを受け取り保存する形で、お試し作成しました。

##randkeyの生成
ダウンロードのキーになる、十分ランダムな文字列…ということで`SecureRandom()`を使おうかな！位に思っていたが無い！そりゃそうか…一旦これもスキップし、unixtimeを取って何となく重複しそうにないもの返すものを仮に作りました。

これを元に、以下の様なキーで、各バリューをsetします。
DBX:[randkey]:filename
DBX:[randkey]:allow
DBX:[randkey]:auth
DBX:[randkey]:body

一通りできたところで/upload実行。発行されたキーを元に/download実行、で上手くいくことが確認できました。できましたが…大きいファイルでの検証等全くやっていないので、色々問題ありそうです。


# 未回収の課題一覧
## SecureRandom()
SecureRandom()…ではなくて、mkpasswdのソースを眺めつつ、それに類似する何か(random+salt的なもの)を作れるかなと思います。

## ダウンロード時限
これはredis側でsetをsetexに変えれば出来るはずです。
ただ時間切れを時間切れとしてエラー出力するようなものは無くなるので、その辺りあった方が良ければそうした方がいいのかも。

## ファイル関連の読み書き問題
* ngx_mruby側でredirectは変更できるがデフォルトdocument_rootがくっついてしまう。
* ngx_mruby側で、受け取ったbodyを読み取れない。
* uploadでnginx側でファイルを受け取り、downloadでkeyに紐づくファイルをredirect、という方針がよさそうだが…

## ダウンロード回数制限
備忘録。今書きながら「ダウンロード回数制限はすぐに出来そうだな」と思いました。

以上解決できないままタイムアップです。

##まとめ
というわけで、未解決の課題が色々残ってしまいました…ちょっと年始のリハビリに自宅で色々やってみようと思います。

作りながら思ったのは、ngx_mrubyが比較的何でもできるので、ネットワークレイヤーの中でのnginxの位置づけをどうすべきなんだろう？という所です。

「nginxはSSLとかキャッシュとかプロキシとか、サーバサイド前段の処理をこなしてくれるサーバ」だけど「ngx_mrubyを組み込むことでアプリ的な処理もできる」…

個人的にはngx_mrubyでREST APIが作れるフレームワークがあって、ビジネスロジックも書けてしかも超絶速いよ！というのがあるのも面白いかも！とも思うのですが。

明日は@udzuraさんの「mod_mruby、DockerのONBUILDを使ってビルドしたら便利」です！