Skip to content

Instantly share code, notes, and snippets.

@ykst
Last active February 7, 2024 15:17
Show Gist options
  • Star 41 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ykst/52205d4d4968298137ea0050c4569170 to your computer and use it in GitHub Desktop.
Save ykst/52205d4d4968298137ea0050c4569170 to your computer and use it in GitHub Desktop.
逆引きlua-nginx-module

逆引きlua-nginx-module

(WIP)

検証環境

nginx

nginx version: nginx/1.10.1
built by gcc 4.4.7 20120313 (Red Hat 4.4.7-4) (GCC) 
configure arguments: --with-openssl=/usr/local/src/openssl --with-ld-opt=-Wl,-E,-rpath,/usr/local/lib --add-module=/usr/local/src/ngx_devel_kit --add-module=/usr/local/src/lua-nginx-module

lua-nginx-module

https://github.com/openresty/lua-nginx-module.git 7d242ff79a63bae15c8f5f68999dfe5f4da7cf67 (v0.10.6)

ngx_devel_kit

https://github.com/simpl/ngx_devel_kit.git e443262071e759c047492be60ec7e2d73c5b57ec

luajit

要make install

http://luajit.org/git/luajit-2.0.git 3e4a196777450f7db11067e93a17655ba3ee0d53 (tag: v2.1.0-beta2)

基本編

hello, world!

何をされてもhello worldで応答するhttpサーバー。

http {
    server {
        listen 80;
        location / {
            content_by_lua_block {
                ngx.say("hello, world!")
            }
        }
    }
}

nginxログに出力する

error.logにnoticeレベルで出力するにはprintを使用する。

print("hello, world!")

個別にログレベルを指定したい場合はngx.logを使用する。

ngx.log(ngx.INFO, "foobar")

第一引数に渡すログレベルには以下のものが用意されている。

   ngx.STDERR
   ngx.EMERG
   ngx.ALERT
   ngx.CRIT
   ngx.ERR
   ngx.WARN
   ngx.NOTICE
   ngx.INFO
   ngx.DEBUG

(出典: https://github.com/openresty/lua-nginx-module#nginx-log-level-constants)

nginxの変数にアクセスする

ngx.varを経由してnginx.confで使用出来る変数にアクセスすることが出来る。

...
                ngx.say(ngx.var.http_host)
...

hostname/location?xxx=yyyといったクエリパラメータに依存する動的な変数も同様にngx.varから参照することが出来る。

ngx.var.arg_xxx

HTTPヘッダにアクセスする

ngx.req.get_headersを使用する

...
                ngx.say(ngx.req.get_headers()["Host"])
...

POSTデータを取得する

ngx.req.get_body_data()を叩く前にはngx.req.read_body()を一度実行する必要がある。 それをしないとnilしか返ってこない。

...
content_by_lua_block {
    ngx.req.read_body()
    ngx.print(ngx.req.get_body_data())
}
...

外部のluaモジュールを使用する

次のような外部luaファイルをnginxから呼び出してみる。

-- testlib.lua
local _M = {}

function _M:foo()
    return "bar"
end

return _M

nginx.confからライブラリへのパスを通し、requireから使用する。

http {
    lua_package_path '/absolute/path/to/lua/scripts/?.lua;;'
    ...
        content_by_lua_block {
            local lib = require("testlib")
            ngx.say(lib:foo())
        }
    ...
}

一度requireしたスクリプトの内容はlua_code_cacheを切らない限りキャッシュされるので、その後該当ファイルを編集しても変更は反映されない。lua_package_pathrequire呼び出し時のPATHとして設定されているため、外部モジュールの中でサブディレクトリにあるスクリプトを使う場合は単にrequire("foo/bar")などで、スラッシュ区切りでモジュールを呼び出せば良い。

ワーカープロセス間でデータを共有する

redisと同じようなインターフェースを持つshared dictionaryを定義してワーカー間でデータを同期して共有することが出来る。

http {
    lua_shared_dict dic 10m; # 名前'dic'のshared dictionaryを10mbyteのサイズで作成
    ...
            content_by_lua_block {
                local cnt = ngx.shared.dic:get("cnt") or 0
                ngx.say(cnt)
                ngx.shared.dic:set("cnt", cnt + 1)
            }
    ...
}

shared dictionaryの中身はnginxをrestartするまで保持される(reloadでは消えない)。

ワーカープロセス内部でデータを共有する

requireで読み出したluaモジュールはワーカープロセスにキャッシュされる。このため、モジュールの内部で定義されている変数をワーカー内部での共有データとして使うことができる。ワーカー同士でデータを共有することはできないので、read onlyのデータに対して使用するといいだろう。(参考: Data Sharing within an Nginx Worker)

luajitでnginx立ち上げ時にluaをコンパイルする

TBD. 基本的にはluajitのluacでバイトコードを使って*_by_fileで使用すればよい

動的にluaスクリプトを更新する

lua_code_cacheを切ることで外部ファイルで記述されたluaスクリプトをリクエスト単位で読み込み直すようになる。 nginx.confに直接記述されたスクリプトに関してはreloadが必要。

http {
    lua_code_cache off;
    ...
}

luaによるフックが行えるタイミング

公式ドキュメントより、以下の図を参照。 状態遷移図 (出典: https://github.com/openresty/lua-nginx-module#directives )

luaの実行時例外発生時に特定のページにリダイレクトする

proxy編

http通信をフックする

upstreamのレスポンスbodyを改竄する

body_filter_by_luaの中でngx.arg[1]にオリジナルのbodyが入っているので、変更を加えて代入することで内容を変更することが出来る。

    ...
    location / {
        proxy_pass http://mybackend;
        body_filter_by_lua_block {
            ngx.arg[1] = string.upper(ngx.arg[1])
        }
    }
    ...

websocketをフックする

lua-resty-websocketを利用してluaレベルでwebsocketサーバーを構築して中継を行う。以下はupstreamにwebsocketを設定してリバースプロクシを構成するサンプル。

        ...
        location / {
            proxy_pass http://websocket;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;

            content_by_lua_block {
                local server = require "resty/websocket/server"
                local client = require "resty/websocket/client"
                local protocol = require "resty/websocket/protocol"

                -- websocket server
                local sock_server, err = server:new()

                if not sock_server then
                    ngx.log(ngx.ERR, "failed to new websocket: ", err)
                    return ngx.exit(444)
                end

                local sock_client, err = client:new()
                -- local uri = "ws://" ..  ngx.var.http_host ..  ngx.var.uri ..  ngx.var.is_args ..  ngx.var.args
                local uri = "ws://192.168.56.102:3000" .. ngx.var.uri ..  ngx.var.is_args ..  ngx.var.args
                local ok, err = sock_client:connect(uri)

                if not ok then
                    ngx.say("failed to connect remote: " .. err)
                    ngx.exit(404)
                end

                local function ws_proxy(sock_from, sock_to, flip_masking)
                    local opcode_mapper = {
                        ["continuation"] = 0x0,
                        ["text"] = 0x1,
                        ["binary"] = 0x2,
                        ["close"] = 0x8,
                        ["ping"] = 0x9,
                        ["pong"] = 0x9,
                    }

                    while true do
                        local data, typ, err = sock_from:recv_frame(flip_masking)

                        if data == nil then
                            -- socket already closed
                            sock_to:send_close()
                            break
                        else
                            ngx.log(ngx.INFO, data .. " (" .. typ .. ")")

                            local fin = (typ ~= "continuation")

                            if typ == "close" then
                                sock_from:send_close()
                            end

                            local bytes, err = sock_to:send_frame(fin, opcode_mapper[typ], data, flip_masking)

                            if bytes == nil then
                                sock_from:send_close()
                                break
                            end
                        end

                    end
                end

                local s2c = ngx.thread.spawn(ws_proxy, sock_client, sock_server, false)
                local c2s = ngx.thread.spawn(ws_proxy, sock_server, sock_client, true)

                if not ngx.thread.wait(s2c) then
                    ngx.log(ngx.ERR, "s2c wait failed")
                end

                if not ngx.thread.wait(c2s) then
                    ngx.log(ngx.ERR, "c2s wait failed")
                end
            }
        }
        ...

https通信のペイロードを傍受する

任意のステータスコードで応答する

ngx.exitを使用して任意のステータスコードを返して処理を終了することができる。

ngx.exit(200)

サポートされている定数のリストはこちら

意図的な通信遅延を発生させる

ノンブロッキングなスリープで通信遅延をエミュレートするにはngx.sleepを秒数指定で使用する。ミリ秒精度までサポートされている。

...
        location / {
            access_by_lua_block {
                ngx.sleep(3.000) -- 3秒待機
            }
        }
...

意図的な通信切断を発生させる

HTTPヘッダを改竄する

POST内容を改竄する

upstreamの応答を改竄する

別のサーバーに同じ通信内容を送信する

スタンドアローンサーバー編

REST APIを公開してnginxの挙動を動的に変更する

Redisへ接続する

MySQLへ接続する

チャットサーバーを構築する

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment