OpenResty 是支持动态请求和响应的增强版 NGINX

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

前回の紹介を経て、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.sslngx.base64ngx.errlogngx.processngx.re.splitngx.resp.add_headerngx.balancerngx.semaphorengx.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.subngx.re.findなど、ngx.reで始まるAPIがいくつかあります。なぜngx.re.splitだけがrequireを必要とするのでしょうか?

前回のlua-resty-coreの章で述べたように、新しいOpenResty APIはlua-resty-coreリポジトリでFFIを使用して実装されているため、断片化が避けられません。将来的には、lua-nginx-modulelua-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は、GETPOSTなどのリクエストメソッドを表します。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のほとんどは読み取り専用であり、$argslimit_rateなどの一部の変数のみが書き込み可能です。しかし、メソッド、URI、argsを変更する必要がある場合がよくあります。

そのため、OpenRestyはリクエスト行を操作するための専用APIをいくつか提供しており、リクエスト行を書き換えてリダイレクトなどの後続の操作を行うことができます。

HTTPプロトコルのバージョン番号を取得する方法を見てみましょう。OpenRestyのAPI ngx.req.http_versionは、NGINXの$server_protocol変数と同じことを行います。ただし、このAPIの戻り値は文字列ではなく数値形式で、2.01.01.10.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を設計する際には、getsetメソッドのパラメータ形式を一貫させることを検討し、パフォーマンスを多少犠牲にしても構わない場合があります。

さらに、リクエスト行を書き換えるメソッドの中には、URIとargsを書き換えるためのngx.req.set_uringx.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.conflua_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_sizeclient_max_body_sizeを同じ値に設定し、メモリ内で完全に処理することもできます。これは、メモリのサイズと同時リクエスト数に依存します。

さらに、リクエストボディを書き換えることもできます。ngx.req.set_body_datangx.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.sayngx.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はあなたのニーズを満たすのに十分でしょうか?コメントを残して、この記事を同僚や友人と共有し、一緒にコミュニケーションと改善を行いましょう。