前回はlighttpdのビルド~cgi作成までを紹介しました。
今回はプラグインの導入を紹介します。最初はサンプルも一緒に紹介しつつ説明しようと思ったのですが、長くなったので今回は導入方法の説明だけ。別途サンプルはこちらで紹介します。
環境は相変わらずUbuntu 18.04 デスクトップ、lighttpdのバージョンは1.4.48です。
lighttpdプラグインの仕組み 概要
一言でいうと、決まったルールで作成した共有ライブラリを作成すると、lighttpdが動的に必要な時にAPIを呼んでくれる仕組みになっています。
この共有ライブラリのことをここではプラグインと呼んでいきます。
作成・導入方法概要
- lighttpdが提供しているプラグイン用の関数フォーマット(ここではプラグインAPIと呼ぶことにします)に合わせて必要な関数を実装する。
-
プラグイン名_init
関数を実装する。 - プラグインをlighttpdの設定ファイルに記載する。
これでlighttpdを起動すると、勝手にlighttpdがプラグインをロードしてくれます。
1. プラグインAPI定義
こちらplugin.hでこんな感じに定義されています。沢山あるので抜粋。
typedef struct {
size_t version;
buffer *name; /* name of the plugin */
void *(* init) ();
handler_t (* priv_defaults) (server *srv, void *p_d);
handler_t (* set_defaults) (server *srv, void *p_d);
handler_t (* cleanup) (server *srv, void *p_d);
/* is called ... */
//...
handler_t (* handle_uri_clean) (server *srv, connection *con, void *p_d); /* after uri is set */
//...
handler_t (* handle_request_done) (server *srv, connection *con, void *p_d); /* at the end of a request */
//...
handler_t (* handle_connection_close) (server *srv, connection *con, void *p_d); /* before close() of socket */
//...
handler_t (* connection_reset) (server *srv, connection *con, void *p_d); /* after request done or request abort */
void *data;
} plugin;
よく使いそうな所だけざっくり紹介。
プラグインAPI名 | 用途 | 備考 |
---|---|---|
init | プラグイン初期化用 | 起動時のみ実行 |
set_defaults | 設定ファイル読み込み用 | 起動時のみ実行 |
cleanup | プラグイン終了用 | 終了時のみ実行 |
handle_uri_clean | HTTPリクエストメイン処理用 | |
handle_request_done | HTTPレスポンス送信後処理用 | |
connection_reset | HTTPレスポンス送信後処理 or HTTPレスポンスアボート用 | |
handle_connection_close | TCPセッションクローズ用 |
※connection_resetはTCPセッションクローズとHTTPレスポンス送信後の処理が同じ場合に使うのかな?
この辺の仕様は把握できてないです。
※handle_uri_cleanはHTTPリクエストをパースし終えて、内部データを構築し終えた後に呼ばれるAPIなので、敢えてメイン処理と記載させていただきました。
タイミングを微妙に速くしたい時はhandle_uri_raw等他のAPIを使います。
また、handler_tの値に意味があり、APIの戻り値でその後の動作が変わります。
handler_t値 | 意味合い | 備考 |
---|---|---|
HANDLER_GO_ON | HTTPレスポンスを返さず次のプラグインAPIに処理を渡す。 | |
HANDLER_FINISHED | 処理終了、HTTPレスポンスを返す。 | |
HANDLER_WAIT_FOR_EVENT | HTTPレスポンス作成中 |
2. プラグイン名_init関数
例えばスケルトンコードではこんな感じ。mod_skeleton.soとしてビルドすればlighttpdで利用可能に。
int mod_skeleton_plugin_init(plugin *p) {
p->version = LIGHTTPD_VERSION_ID;
p->name = buffer_init_string("skeleton");
p->init = mod_skeleton_init;
p->handle_uri_clean = mod_skeleton_uri_handler;
p->set_defaults = mod_skeleton_set_defaults;
p->cleanup = mod_skeleton_free;
p->data = NULL;
return 0;
}
3. lighttpdの設定ファイルへの記載
作成したmod_XXX.so
の.soを除いて記載します。mod_skeleton.soならこんな感じ
## cgi.assign
server.modules += ( "mod_skeleton" )
プラグインAPIは記載したプラグインの上から順に呼ばれます。
lighttpdプラグインの作成方法
以下、スケルトンコードを例にとりざっくり解説していきます。
プラグイン初期化/解放(init/cleanup)
まずは設定ファイル用のplugin_config
と自プラグイン用のplugin_data
という名前の構造体を定義します。
typedef struct {
array *match;
} plugin_config;
typedef struct {
PLUGIN_DATA;
buffer *match_buf;
plugin_config **config_storage;
plugin_config conf;
} plugin_data;
array
, buffer
等はlighttpdが用意しているデータ構造体。これらを利用して設定を読んだりHTTPレスポンスデータを作成するAPIが用意されているので、極力同じ型を利用しましょう。
出来たらinit用の関数で初期化処理を書きます。これがlighttpd起動後、プラグインが読み込まれた時に実行されます。ここで作成したplugin_data
は各プラグインAPIで利用できます。
また、どのプラグインAPIと対応するかが見やすいように(?)関数定義用のマクロを用意しています。これだと引数がわからないけど。
//static handler_t mod_loopcgi_init(void)と同じ
INIT_FUNC(mod_skeleton_init) {
plugin_data *p;
p = calloc(1, sizeof(*p));
force_assert(p);
p->match_buf = buffer_init();
return p;
}
lighttpd側のマクロ定義はこんなです。
//set_defaultsで利用
# define SERVER_FUNC(x) \
static handler_t x(server *srv, void *p_d)
//その他、HTTP接続後のAPIで利用
# define CONNECTION_FUNC(x) \
static handler_t x(server *srv, connection *con, void *p_d)
//initで利用
# define INIT_FUNC(x) \
static void *x(void)
合わせてcleanup関数も定義。initに対してcleanupが長いのはset_defaultsで読み込んだ設定も開放しているからです。
ちなみに各プラグインAPIの引数p_d
がINIT_FUNCで作成したplugin_dataになります。
/* destroy the plugin data */
FREE_FUNC(mod_skeleton_free) {
plugin_data *p = p_d;
//...中略
if (p->config_storage) {
size_t i;
for (i = 0; i < srv->config_context->used; i++) {
plugin_config *s = p->config_storage[i];
if (NULL == s) continue;
array_free(s->match);
free(s);
}
free(p->config_storage);
}
buffer_free(p->match_buf);
free(p);
return HANDLER_GO_ON;
}
後はプラグイン名_init関数で登録しておけばOKです。
設定値の読み込み (set_defaults)
個人的に一番癖があるのがここ。ざっというとこんな感じ。
-
srv->config_context
に設定ファイル情報が入っているから、パースしてplugin_dataに必要な設定を保持してね!
ただこの設定、list管理されているので欲しい設定がどこにあるかぱっとわからない模様。なのでスケルトンコードではconfig_storage
というplugin_configのリストを用意しています。
「とりあえず片っ端から候補(空含む)をplugin_configに詰めるから、使うときにconfig_storageをパースしてね!」って感じ。
利用する際はmod_skeleton_patch_connection
で毎回config_storage
をパースしています。
SETDEFAULTS_FUNC(mod_skeleton_set_defaults) {
plugin_data *p = p_d;
size_t i = 0;
config_values_t cv[] = {
{ "skeleton.array", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
};
if (!p) return HANDLER_ERROR;
p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
force_assert(p->config_storage);
for (i = 0; i < srv->config_context->used; i++) {
data_config const* config = (data_config const*)srv->config_context->data[i];
plugin_config *s = calloc(1, sizeof(plugin_config));
force_assert(s);
s->match = array_init();
cv[0].destination = s->match;
p->config_storage[i] = s;
if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
return HANDLER_ERROR;
}
if (!array_is_vlist(s->match)) {
log_error_write(srv, __FILE__, __LINE__, "s",
"unexpected value for skeleton.array; expected list of \"urlpath\"");
return HANDLER_ERROR;
}
}
return HANDLER_GO_ON;
}
以下ポイントの抜粋です。
この例では、skeleton.arrayというオリジナルの設定値を読み込んでいます。ここで読み込み処理を実装すれば、専用設定を利用できるのは便利ですね。
//ここで読み込みたい設定値を記載していく。
config_values_t cv[] = {
{ "skeleton.array", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
{ NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
};
設定を読みこむためのconfig_insert_values_global。destinationにデータ領域を設定すると、config_insert_values_globalで該当設定があればデータを詰めてくれます。
型の指定はconfig_values_tに書いているT_CONFIG_ARRAYです。
//destinationにデータ領域を設定すると、config_insert_values_globalで該当設定があれば読んでくれます。
cv[0].destination = s->match;
if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
return HANDLER_ERROR;
}
これで必要な設定が読み込めました。
※個人的には滅茶苦茶わかりにくいし作るのが大変だったので、別記事で紹介するサンプルではconfig_storage
を利用しないようにしました。
HTTPリクエストに対するメイン処理 (handle_uri_clean)
HTTPリクエストに対するメイン処理。スケルトンコードでは自分当てのURIの時だけ応答を返すようにしています。
どのAPIもそうですが、前のプラグインが応答していない全HTTPリクエストでhandle_uri_cleanが呼ばれるのでuriのパースのような自分が処理するべきかの判定が必要です。
そのための設定値。
URIHANDLER_FUNC(mod_skeleton_uri_handler) {
plugin_data *p = p_d;
size_t s_len;
size_t k;
UNUSED(srv);
if (con->mode != DIRECT) return HANDLER_GO_ON;
s_len = buffer_string_length(con->uri.path);
if (0 == s_len) return HANDLER_GO_ON;
mod_skeleton_patch_connection(srv, con, p);
//ここでURIをパースして、自分宛てかどうかを確認している。
for (k = 0; k < p->conf.match->used; k++) {
data_string *ds = (data_string *)p->conf.match->data[k];
size_t ct_len = buffer_string_length(ds->value);
if (ct_len > s_len) continue;
if (ct_len == 0) continue;
//自分宛てなら403を返す。
if (0 == strncmp(con->uri.path->ptr + s_len - ct_len, ds->value->ptr, ct_len)) {
con->http_status = 403;
return HANDLER_FINISHED;
}
}
/* not found */
return HANDLER_GO_ON;
}
以下ポイントの抜粋です。
自分と関係ないリクエスト⇒HANDLER_GO_ONを返します
if (con->mode != DIRECT) return HANDLER_GO_ON;
///...
/* not found */
return HANDLER_GO_ON;
}
自分が応答を返す⇒http_statusを指定し、HANDLER_FINISHEDを返します
HTTPレスポンスボディが必要な場合はcon->write_queue
にデータを詰めます。こちらはサンプルで紹介します。
//ここでURIをパースして、自分宛てかどうかを確認している。
for (k = 0; k < p->conf.match->used; k++) {
///中略
//自分宛てなら403を返す。
if (0 == strncmp(con->uri.path->ptr + s_len - ct_len, ds->value->ptr, ct_len)) {
con->http_status = 403;
return HANDLER_FINISHED;
}
}
また、スケルトンコードにはない例ですが、時間がかかる処理の場合は一旦HANDLER_WAIT_FOR_EVENTを返し、別のトリガーで処理が終わってからHANDLER_FINISHEDを返す方法もあります。こちらもサンプルの記事で詳細説明します。
HTTPレスポンス後処理 (handle_connection_close/handle_request_done/connection_reset)
HTTPレスポンス後の処理です。ここではコードは省略。用途に合わせて接続時に作成したデータの解放にお使いください。
注意点としては、これらは重複して呼ばれる可能性があるので、二重解放にお気を付けください。
lighttpd-1.4.48
でサンプルを動かした際の動作はこうでした。
- handle_request_done⇒HTTPレスポンス送信完了後、TCPセッション切断後の2回呼ばれる。
- handle_connection_close⇒TCPセッション切断後の呼ばれる。
keep-aliveでないHTTPリクエストだと、
handle_request_done⇒handle_connection_close⇒handle_request_doneと呼ばれていました。
lighttpd, 結構バンバンリファクタをしていて、結果こういう細かい仕様はバージョンによって変わってたりする印象があるんですよね。なので仕様はこうだ!っていうのも言い切れなくて。プラグイン作成の際はご注意ください。
共通のポイント
- lighttpdはシングルスレッドなので、各処理に時間がかかる場合は、本体が止まらないような実装が必要です。時間がかかる処理用の仕組みもあります。
- lighttpdはChainOfResponsibilityのような仕組みで動作しています。自分あてのURL以外のアクセスでもプラグインAPIが実行されるため、自分宛か判別する必要があります。
- 戻り値handler_tで自分がHTTPレスポンスを返すかコントールします。
- 設定値を利用者が自由に追加できます。この辺りの拡張性は便利ですね。
注意点
例えば~1.4.39まであったhandle_joblistが削除されていたり、HTTPレスポンス後処理でも触れたように微妙に終了シーケンスが変わったりとバージョンで微妙にAPIやその仕様が違う可能性があります。
違うバージョンのものに触れるとあれ?思っていた動作と違う?なんてことも。ドキュメントを整備したり利用できるAPIをちゃんと提示してなかったりするので、プラグインはあまり公開情報って考えではなく"とりあえずソースを読んで使いたきゃ使え!"ってスタンスなのかな?
プラグイン導入の敷居が高いのが広まらない要因だったりして
参考
抜粋したソースコード(lighttpd-1.4.48/src/mod_skeleton.c他)
ligttpd公式ページの1.4.48