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
尤も、使い捨て暗号を使えばワンタイムパスなども作れそうです。