nginx-lua-moduleでHTTPのRange Requestsを実装する

ローカルで運用しているnginxサーバー上のmp4ファイルをchrome経由で閲覧したときに、
シークができなかったので、HTTPのRange Requestsをluaで実装した。

ローカルでの運用を考えているため、セキュリティ対策など考えられていません。
流用の際は十分ご注意ください。
記事内のsample.comは各自の環境に置き換えてください。

# 環境
* [Ubuntu server 16.04.1](https://www.ubuntu.com/download/server)
* kernel: 4.4.0-57-generic

# インストール
```
$ sudo apt-get install nginx nginx-extras
$ dpkg -l | grep nginx
ii  nginx                              1.10.0-0ubuntu0.16.04.4                   all          small, powerful, scalable web/proxy server
ii  nginx-common                       1.10.0-0ubuntu0.16.04.4                   all          small, powerful, scalable web/proxy server - common files
ii  nginx-extras                       1.10.0-0ubuntu0.16.04.4                   amd64        nginx web/proxy server (extended version)
```

# nginx-lua-moduleの動作確認
まずは、helloコマンドを実装。
## nginxのsite設定
`$ sudo vim /etc/nginx/sites-available/dl.sample.com`

```
server {
  listen  443 ssl;
  server_name dl.sample.com;
  location / {
    root  /var/www/html;
  }

  location /hello {
    content_by_lua "
      ngx.say('Hello, from inline lua')
    ";
  }

  location /hellofile {
    content_by_lua_file /etc/nginx/lua/hello.lua;
  }
}
```

`$ sudo ln -s /etc/nginx/sites-available/dl.sample.com /etc/nginx/sites-enable/`


## luaファイル作成(location /hellofileで呼び出されるもの)
```
$ sudo mkdir /etc/nginx/lua
$ sudo vim /etc/nginx/lua/hello.lua
```

```
ngx.say("hello by file")
```

## 動作確認

```
$ sudo service nginx restart
$ curl https://dl.sample.com/hello
Hello, from inline lua
$ curl https://dl.sample.com/hellofile
hello by file
```

# Range Requestsの実装
## nginxのsite設定
`$ sudo vim /etc/nginx/sites-available/dl.sample.com`

```
server {
  listen  443 ssl;
  server_name dl.sample.com;
  location / {
    root  /var/www/html;
  }

  location ~ /v0/(.*\.mp4) {
    set $video_path $1;
    content_by_lua_file /etc/nginx/lua/v0/anymp4.lua;
  }

}
```
任意のファイル名でアクセスできるように(.*\.mp4)設定。

## luaファイル作成(location /v0/(.*\.mp4) で呼び出されるもの)
```
$ sudo mkdir /etc/nginx/lua/v0
$ sudo vim /etc/nginx/lua/v0/anymp4.lua
```

```
function io.file_size(filename)
  local fh = assert(io.open(filename, "rb"))
  local len = assert(fh:seek("end"))
  fh.close()
  return len
end

function table.in_key(tbl, key)
  for k, v in pairs(tbl) do
    if k==key then return true end
  end
end

local video_path = ngx.unescape_uri(ngx.var.video_path)
local video_dir = "/mnt/mp4/"
local filename = video_dir..video_path

local h = ngx.req.get_headers()

local instance_length = io.file_size(filename)
if table.in_key(h, "range") then
  -- Range requests
  local reqRange = h["range"]
  local first_byte_pos = 0
  local last_byte_pos = instance_length - 1

  local reqFirst, reqLast = string.match(reqRange, "(%d*)-(%d*)")
  first_byte_pos = reqFirst

  if reqLast == "" then
    -- nothing to do
  else
    last_byte_pos = tonumber(reqLast)
  end

  -- Status code
  ngx.status = ngx.HTTP_PARTIAL_CONTENT

  -- Response headers
  contentLength = last_byte_pos - first_byte_pos + 1
  ngx.header["Content-Length"] = contentLength
  ngx.header["Content-Range"] = "bytes "..first_byte_pos.."-"..last_byte_pos.."/"..instance_length

  -- output stream
  local file = io.open(filename)
  file:seek("set", tonumber(reqFirst))
  local f = file:read(contentLength)
  ngx.print(f)
else
  -- Normal requests
  ngx.header["Content-Length"] = instance_length

  -- Status code
  ngx.status = ngx.HTTP_OK

  -- output stream
  local file = io.open(filename)
  local f = file:read("*all")
  ngx.print(f)
  ngx.flush(true)
end
```
RFC7233に従い、リクエスト時にHTTPヘッダ内にrangeがあれば、
206(HTTP_PARTIAL_CONTENT)と指定された範囲のデータを送信する。

## 動作確認

```
$ sudo service nginx restart
$ ls /mnt/mp4/
1.mp4
```
任意のmp4ファイル(この例では1.mp4)をanymp4.luaで指定したフォルダ(この例では/mnt/mp4/)に格納しておく。

chromeで下記のようなアドレスへアクセスすると、mp4閲覧時にシーク可能となっていることが確認できる。
`https://dl.sample.com/v0/1.mp4`

# 参考
http://techhey.hatenablog.com/entry/2014/04/17/200831
https://triple-underscore.github.io/RFC7233-ja.html