Eukleides project

from http://d.hatena.ne.jp/u5_h/

How to make a timed token path on h2o + mruby reverse proxy

前回はh2oとmruby,mruby-redis,mruby-base64を使って、proxy cache serverを作りましたが、今回はmruby-cipherとmruby-packを使って、一定時間だけ有効なpathによるアクセス制御を実装します。

使用するmrubyのパッケージは以下です

GitHub - hanachin/mruby-cipher: OpenSSL Cipher wrapper for mruby

GitHub - iij/mruby-pack: Array#pack and String#unpack for mruby

導入

$ git clone https://github.com/h2o/h2o                 
$ cd h2o
$ git submodule add https://github.com/iij/mruby-pack.git deps/mruby-pack
$ git submodule add https://github.com/hanachin/mruby-cipher.git deps/mruby-cipher
$ cmake -DWITH_MRUBY=on .
$ make
$ sudo make install

path生成スクリプト

  1 require 'openssl'
  2 
  3 date = (Time.now.to_i+600).to_s
  4 path = '/articles/2018/09/25/110000 '
  5 key = 'a'*32
  6 iv = 'i'*16
  7 
  8 keyword = date+' '+path
  9 
 10 enc = OpenSSL::Cipher.new('AES-256-CBC')
 11 enc.encrypt
 12 enc.key = key
 13 enc.iv = iv
 14 encrypted_data = (enc.update(keyword)+enc.final).unpack("H*")
 15 p encrypted_data
 16 
 17 dec = OpenSSL::Cipher.new('AES-256-CBC')
 18 dec.decrypt
 19 dec.key = key
 20 dec.iv = iv
 21 decrypted_data = dec.update(encrypted_data.pack('H*'))+dec.final
 22 puts decrypted_data

現在時刻より10分先の未来まで有効なpathを生成します。AES256CBCで”(10分後のunixtime path ”という平文を暗号化します。

$ ruby encrypt.rb 
["016d931ee21ba30f49012b31b6ba6701ead5988e1c120cacaecbd5c21f5e9cef7369f2b943b28a6cb3e21050062a6474"]
1538065794 /articles/2018/09/25/110000 

conf(ttp.conf)

  1 hosts:
  2   "localhost:8080":
  3     listen:
  4       port: 8080
  5       host: 0.0.0.0
  6     paths:
  7       "/":
  8         mruby.handler: |
  9           proxy = "hatenanews.com"
 10           scheme = "http"
 11           header = {}
 12           Proc.new do |env|
 13             path =[]
 14             path << env["PATH_INFO"].gsub("/","")
 15             cipher = Cipher.new("AES-256-CBC")
 16             cipher.decrypt
 17             cipher.key = 'a'*32
 18             cipher.iv = 'i'*16
 19             decrypted = (cipher.update(path.pack("H*"))+cipher.final).gsub("\n","")
 20             ddate = decrypted.split(" ")[0]
 21             dpath = decrypted.split(" ")[1]
 22             puts ddate
 23             puts dpath
 24             if ddate.to_i < Time.now.to_i then
 25               [410,{},[]]
 26             else
 27               headers={}
 28               headers["Host"]=proxy
 29               url = scheme+"://"+proxy+dpath
 30               puts url
 31               req = http_request(url)
 32               status, header, body = req.join
 33               [status, header, body]
 34             end
 35           end

localhost:8080/xxxxxxxxにアクセスするとhatenanewsのホストにproxyリクエストされます。その際のpathであるxxxxxxxxは復号化されてunixtimeとリクエスト用のpathが生成されます。取り出されたunixtimeは暗号化時刻の10分後で現在時刻より大きければ取り出されたpathでリクエスト、小さければ410を返します。

起動

$ /usr/local/bin/h2o -c ttp.conf 


実行結果(curlによるアクセス)

$ curl -I http://localhost:8080/016d931ee21ba30f49012b31b6ba6701ead5988e1c120cacaecbd5c21f5e9cef7369f2b943b28a6cb3e21050062a6474
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 105934
Server: h2o/2.3.0-DEV@bd64950a
age: 13
p3p: CP="OTI CUR OUR BUS STA"
via: 1.1 varnish-v4
date: Thu, 27 Sep 2018 16:24:48 GMT
vary: Accept-Encoding
vary: User-Agent, X-Forwarded-Host, X-Device-Type
server: nginx
x-cache: HIT
x-runtime: 0.035216
x-varnish: 383363411 387191218
x-dispatch: Hatena::Epic::Web::Blogs::DispatchFailed#default
x-revision: 347c03bbb5a956d8ac6e770f93ffac6f
content-type: text/html; charset=utf-8
x-page-cache: hit
accept-ranges: bytes
cache-control: private
x-frame-options: DENY
x-xss-protection: 1
x-cache-only-varnish: 1
x-content-type-options: nosniff
access-control-allow-origin: *
content-security-policy-report-only: block-all-mixed-content; report-uri https://blog.hatena.ne.jp/api/csp_report

10分以上経過後の結果

$ curl -vvv http://localhost:8080/122f99133e097f77410de091adbe4f3f62887561870fac92892ed5a8198ea4d8e6245bee271578fc5bc4f6fc4d954214
* About to connect() to localhost port 8080 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /122f99133e097f77410de091adbe4f3f62887561870fac92892ed5a8198ea4d8e6245bee271578fc5bc4f6fc4d954214 HTTP/1.1
> User-Agent: curl/7.29.0
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 410 OK
< Connection: keep-alive
< Content-Length: 0
< Server: h2o/2.3.0-DEV@bd64950a
< date: Thu, 27 Sep 2018 16:32:28 GMT
< 
* Connection #0 to host localhost left intact

用途としては、キャンペーン用のサイトや、期間限定コンテンツ配信などに使えるかと思います。クエリストリングの方が扱いやすいように思えます。

nginxにはsecure token moduleというものがあり、同様のユースケースで使用が想定されています。

GitHub - kaltura/nginx-secure-token-module

尤も、使い捨て暗号を使えばワンタイムパスなども作れそうです。