OpenResty 是支持动态请求和响应的增强版 NGINX
API7.ai
October 23, 2022
前回の紹介を経て、OpenRestyの概念とその学習方法について理解していただけたと思います。本記事では、OpenRestyがクライアントのリクエストとレスポンスをどのように処理するかについて解説します。
OpenRestyはNGINXベースのウェブサーバーですが、NGINXとは根本的に異なります。NGINXは静的な設定ファイルによって駆動されるのに対し、OpenRestyはLua APIによって駆動され、より柔軟でプログラマブルな機能を提供します。
ここでは、Lua APIの利点について説明します。
APIのカテゴリ
まず、OpenRestyのAPIは以下のようなカテゴリに分類されます。
- リクエストとレスポンスの処理
- SSL関連
- 共有ディクショナリ
- Cosocket
- 4層トラフィックの処理
- プロセスとワーカー
- NGINX変数と設定へのアクセス
- 文字列、時間、コーデックなどの一般的な機能
ここで、OpenRestyのLua APIドキュメントを開き、APIリストと照らし合わせて、このカテゴリに関連付けて確認することをお勧めします。
OpenRestyのAPIは、lua-nginx-module
プロジェクトだけでなく、lua-resty-core
プロジェクトにも存在します。例えば、ngx.ssl
、ngx.base64
、ngx.errlog
、ngx.process
、ngx.re.split
、ngx.resp.add_header
、ngx.balancer
、ngx.semaphore
、ngx.ocsp
などのAPIがあります。
lua-nginx-module
プロジェクトに含まれていないAPIを使用するには、個別にrequire
する必要があります。例えば、split
関数を使用する場合、以下のように呼び出します。
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
もちろん、これには混乱を招くかもしれません。lua-nginx-module
プロジェクトには、ngx.re.sub
、ngx.re.find
など、ngx.re
で始まるAPIがいくつかあります。なぜngx.re.split
だけがrequire
を必要とするのでしょうか?
前回のlua-resty-core
の章で述べたように、新しいOpenResty APIはlua-resty-core
リポジトリでFFIを使用して実装されているため、断片化が避けられません。将来的には、lua-nginx-module
とlua-resty-core
プロジェクトを統合することでこの問題を解決することを期待しています。
リクエスト
次に、OpenRestyがクライアントのリクエストとレスポンスをどのように処理するかを見ていきましょう。まず、リクエストを処理するAPIを見ていきますが、ngx.req
で始まるAPIは20以上あります。では、どのように始めればよいでしょうか?
HTTPリクエストメッセージは、リクエスト行、リクエストヘッダー、リクエストボディの3つの部分で構成されていることを知っています。そこで、この3つの部分に分けてAPIを紹介します。
リクエスト行
まず、リクエスト行です。HTTPのリクエスト行には、リクエストメソッド、URI、HTTPプロトコルのバージョンが含まれます。NGINXでは、組み込み変数を使用してこの値を取得できますが、OpenRestyではngx.var.*
APIに対応します。以下に2つの例を示します。
- 組み込み変数
$scheme
は、NGINXでプロトコル名を表し、http
またはhttps
です。OpenRestyでは、ngx.var.scheme
を使用して同じ値を返すことができます。 $request_method
は、GET
、POST
などのリクエストメソッドを表します。OpenRestyでは、ngx.var.request_method
を使用して同じ値を返すことができます。
NGINXの公式ドキュメントを参照して、NGINXの組み込み変数の完全なリストを取得できます: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables。
では、なぜOpenRestyはリクエスト行のデータをngx.var.*
のような変数の値を返すことで取得できるのに、リクエスト行専用のAPIを提供しているのでしょうか?
その理由はいくつかあります:
- まず、
ngx.var
を繰り返し読み取ることはパフォーマンスが悪いため推奨されません。 - 次に、プログラムの使いやすさを考慮して、
ngx.var
は文字列を返しますが、Luaオブジェクトではありません。args
を取得する際に複数の値が返される場合、処理が難しくなります。 - 最後に、柔軟性の観点から、
ngx.var
のほとんどは読み取り専用であり、$args
やlimit_rate
などの一部の変数のみが書き込み可能です。しかし、メソッド、URI、argsを変更する必要がある場合がよくあります。
そのため、OpenRestyはリクエスト行を操作するための専用APIをいくつか提供しており、リクエスト行を書き換えてリダイレクトなどの後続の操作を行うことができます。
HTTPプロトコルのバージョン番号を取得する方法を見てみましょう。OpenRestyのAPI ngx.req.http_version
は、NGINXの$server_protocol
変数と同じことを行います。ただし、このAPIの戻り値は文字列ではなく数値形式で、2.0
、1.0
、1.1
、0.9
のいずれかの値が返されます。これらの範囲外の値の場合、nil
が返されます。
リクエスト行のメソッドを取得する方法を見てみましょう。前述の通り、ngx.req.get_method
とNGINXの$request_method
変数の役割と戻り値は同じで、文字列形式です。
ただし、現在のHTTPリクエストメソッドを設定するngx.req.set_method
のパラメータ形式は文字列ではなく、組み込みの数値定数です。例えば、以下のコードはリクエストメソッドをPOSTに書き換えます。
ngx.req.set_method(ngx.HTTP_POST)
組み込み定数ngx.HTTP_POST
が実際に数値であり、文字列ではないことを確認するために、その値を出力して8が表示されるかどうかを確認できます。
resty -e 'print(ngx.HTTP_POST)'
このように、get
メソッドの戻り値は文字列ですが、set
メソッドの入力値は数値です。set
メソッドに混乱を招く値を渡しても問題ありませんが、APIがクラッシュして500
エラーを報告する可能性があります。しかし、以下のような判断ロジックでは:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- do something
end
このようなコードはエラーを報告せず、コードレビュー中でも見つけにくいです。私も以前に同様のミスを犯したことがあり、2回のコードレビューと不完全なテストケースを経ても問題を発見できませんでした。最終的には、本番環境での異常によって問題が発覚しました。
このような問題を解決する実用的な方法は、より注意を払うか、もう1層のカプセル化を追加すること以外にはありません。ビジネスAPIを設計する際には、get
とset
メソッドのパラメータ形式を一貫させることを検討し、パフォーマンスを多少犠牲にしても構わない場合があります。
さらに、リクエスト行を書き換えるメソッドの中には、URIとargsを書き換えるためのngx.req.set_uri
とngx.req.set_uri_args
という2つのAPIがあります。以下のNGINX設定を見てみましょう。
rewrite ^ /foo?a=3? break;
では、同等のLua APIでこれをどのように解決できるでしょうか?答えは以下の2行のコードです。
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
公式ドキュメントを読んだことがある場合、ngx.req.set_uri
には2番目のパラメータjump
があり、デフォルトはfalse
です。これをtrue
に設定すると、上記の例のrewrite
コマンドのフラグをbreak
ではなくlast
に設定することと同じになります。
ただし、rewrite
コマンドのフラグ設定は読みにくく、認識しにくいため、コードよりも直感的で保守性が低いと感じています。
リクエストヘッダー
HTTPリクエストヘッダーはkey : value
形式で、例えば以下のようになります。
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
OpenRestyでは、ngx.req.get_headers
を使用してリクエストヘッダーを解析し、取得できます。戻り値の型はテーブルです。
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- ここで現在のリクエストを無視するか拒否するかを選択できます
end
for k, v in pairs(h) do
...
end
デフォルトでは、最初の100ヘッダーが返されます。100を超える場合、truncated
エラーが報告され、開発者がそれをどのように処理するかを決定します。なぜこのような方法を取るのか疑問に思うかもしれませんが、これは後述するセキュリティの脆弱性に関するセクションで説明します。
ただし、OpenRestyは特定のリクエストヘッダーを取得するための専用APIを提供していないことに注意してください。つまり、ngx.req.header['host']
のような形式はありません。そのようなニーズがある場合、NGINX変数$http_xxx
に依存して実現する必要があります。OpenRestyでは、ngx.var.http_xxx
を使用して取得できます。
次に、リクエストヘッダーを書き換えたり削除したりする方法を見てみましょう。これらの操作のAPIは非常に直感的です。
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
もちろん、公式ドキュメントには、ヘッダーの値をnil
に設定するなど、他の方法でリクエストヘッダーを削除する方法も記載されています。ただし、コードの明確さのために、clear_header
を使用して統一することをお勧めします。
リクエストボディ
最後に、リクエストボディを見てみましょう。パフォーマンス上の理由から、OpenRestyはリクエストボディを積極的に読み取りません。ただし、nginx.conf
でlua_need_request_body
ディレクティブを有効にした場合を除きます。さらに、大きなリクエストボディの場合、OpenRestyはその内容をディスク上の一時ファイルに保存するため、リクエストボディを読み取るプロセスは以下のようになります。
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
local tmp_file = ngx.req.get_body_file()
-- io.open(tmp_file)
-- ...
end
このコードには、ディスクファイルを読み取るためのIOブロッキング操作が含まれています。client_body_buffer_size
(64ビットシステムではデフォルトで16KB)の設定を調整して、ブロッキング操作を最小限に抑えることができます。また、client_body_buffer_size
とclient_max_body_size
を同じ値に設定し、メモリ内で完全に処理することもできます。これは、メモリのサイズと同時リクエスト数に依存します。
さらに、リクエストボディを書き換えることもできます。ngx.req.set_body_data
とngx.req.set_body_file
という2つのAPIは、文字列とローカルディスクファイルを入力パラメータとして受け取り、リクエストボディの書き換えを行います。ただし、この種の操作はあまり一般的ではなく、詳細についてはドキュメントを参照してください。
レスポンス
リクエストが処理された後、クライアントにレスポンスを返す必要があります。リクエストメッセージと同様に、レスポンスメッセージもいくつかの部分で構成されています。ステータス行、レスポンスヘッダー、レスポンスボディです。これら3つの部分に応じて、対応するAPIを紹介します。
ステータス行
ステータス行で主に気にするのはステータスコードです。デフォルトでは、返されるHTTPステータスコードは200で、これはOpenRestyに組み込まれた定数ngx.HTTP_OK
です。しかし、コードの世界では、常に最も例外的なケースを処理するコードが存在します。
リクエストメッセージを検出して悪意のあるリクエストであることがわかった場合、リクエストを終了する必要があります。
ngx.exit(ngx.HTTP_BAD_REQUEST)
ただし、OpenRestyのHTTPステータスコードには特別な定数ngx.OK
があります。ngx.exit(ngx.OK)
の場合、リクエストは現在の処理フェーズを終了し、次の段階に進みますが、クライアントに直接返されるわけではありません。
もちろん、終了せずにステータスコードを書き換えることもできます。以下のようにngx.status
を使用します。
ngx.status = ngx.HTTP_FORBIDDEN
ステータスコード定数について詳しく知りたい場合は、ドキュメントを参照してください。
レスポンスヘッダー
レスポンスヘッダーに関しては、2つの方法で設定できます。1つ目は最もシンプルな方法です。
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- 削除
ここで、ngx.header
はレスポンスヘッダー情報を保持しており、読み取り、変更、削除が可能です。
2つ目の方法は、lua-resty-core
リポジトリのngx_resp.add_header
を使用してヘッダーメッセージを追加する方法です。
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
1つ目の方法との違いは、add_header
は同じ名前の既存のフィールドを上書きしないことです。
レスポンスボディ
最後に、レスポンスボディを見てみましょう。OpenRestyでは、ngx.say
とngx.print
を使用してレスポンスボディを出力できます。
ngx.say('hello, world')
この2つのAPIの機能は同じですが、ngx.say
は末尾に改行が追加される点が異なります。
文字列の連結による非効率を避けるために、ngx.say / ngx.print
は文字列と配列形式のパラメータをサポートしています。
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
この方法では、Luaレベルでの文字列連結をスキップし、C関数に処理を任せます。
まとめ
今日の内容を振り返りましょう。リクエストとレスポンスメッセージに関連するOpenRestyのAPIを紹介しました。ご覧の通り、OpenRestyのAPIはNGINXのディレクティブよりも柔軟で強力です。
では、HTTPリクエストを処理する際に、OpenRestyが提供するLua APIはあなたのニーズを満たすのに十分でしょうか?コメントを残して、この記事を同僚や友人と共有し、一緒にコミュニケーションと改善を行いましょう。