【組み込みLinux (3) 】Lighttpd について

usazan
[写真:北海道の有珠山頂(うすざん)から望む洞爺湖(とうやこ)と昭和新山]

先日、出張で訪れている札幌市内の丸善にいってきた。

3階のコンピュータ関係の本棚で「猫語の教科書」という本を発見して反射的に買ってしまった。

コンピュータと猫語という一見何の脈絡も感じない2つの言葉。

これをコンピュータ関係の本棚に並べた理由は並べた人に聞いてみないとわからない。

自分なりに思いつく理由としては
・コンピュータ好きにはネコ好きが多いから。正確にはデスクワークとペットというのは相性が良いため。
・コンピュータ言語と猫語の言語としての共通性。
・冒頭の暗号に関する話。この暗号というアプローチが、コンピュータ(軍事通信)という歴史と重なるため。

いずれにせよ、これはAmazonで本を探していては決して巡り会う事はなかったということ。

ネットの特性として知りたい情報にたどり着く手段としては優秀であるが、
意外性のある情報との出会いという点でリアルの本屋には適わない。

電子書籍が登場したころリアルの書店が潰れると言われたが、心配するほど日本では電子書籍が普及していないように思える。
WEBとリアルの書店。どちらかが完璧でないからこそ、面白いのだと思った。

本題です。

組み込みLinuxでhttpサーバ

Linuxで動いている組み込み機器は多いですが、昨今はRaspberry Piのようにhttpサーバを乗せることも増えてきています。
下位のUDP/IP,TCP/IPはOS(Linuxカーネル)の仕事なので、その辺はカーネルのソースを見てください。

Lighttpd

Apacheサーバも組み込めるかもしれませんが、今回はApacheより軽量のLighttpdを乗せるところまでを紹介します。
Light footprint + httpd = Lighttpd ということで制約の厳しい組み込みでも使えそうです。

[特徴]
・セキュリティ: BASIC、DIGEST認証
・複数ドメイン管理: バーチャルホスト機能
・帯域: コンテンツ圧縮転送
:インターフェイス: FastCGI、CGI、SCGI、SSI

まずは Lighttpd を公式サイトからDLしましょう。

現在(2013.8) は 1.4.32 のようです。

今回はクロスコンパイル環境の準備に関してはすっとばします。

$ ./configure

この時にarm-none-linux-gnueabiなど組み込み用のアーキテクチャを指定します。

インストールします。

$ make
$ make install 

設定は /etc/lighttpd.conf で行います。

Lighttpdのプラグイン開発

Lighttpd自体はpoll/epoll/selectというシステムコールを理解することが必要です。

lihgttpdの状態遷移図です。

internal-http-states

各状態の説明は以下の通りです。

・connect
waiting for a connection
・reqstart
init the read-idle timer
・read
read http-request-header from network
・reqend
parse request
・readpost
read http-request-content from network
・handlereq
handle the request internally (might result in sub-requests)
・respstart
prepare response header
・write
write response-header + content to network
・respend
cleanup environment, log request
・error
reset connection (incl. close())
・close
close connection (handle lingering close)

GET/POSTがどのように処理されているかを見てみます。
connection.cのconnection_handle_read関数でread_queueをbuffer *bに入れています。

// connection.c line 349
b = (NULL != con->read_queue->last) ? con->read_queue->last->mem : NULL;

buffer型はbuffer.hで宣言されています。
b->ptrでリクエストを取得できます。

typedef struct {
	char *ptr;

	size_t used;
	size_t size;
} buffer;

実際にpluginでGETリクエストやPOSTメッセージボディを取得したい場合は一工夫必要です。
hookのエントリポイントは以下16箇所あります。

Serverwide hooks
・init
called when the plugin is loaded
・cleanup
called when the plugin is unloaded
・set_defaults
called when the configuration has to be processed
・handle_trigger
called once a second
・handle_sighup
called when the server received a SIGHUP
・Connectionwide hooks
Most of these hooks are called in http_response_prepare() after some fields in the connection structure are set.
・handle_uri_raw
called after uri.path_raw, uri.authority and uri.scheme are set
・handle_uri_clean
called after uri.path (a clean URI without .. and %20) is set
・handle_docroot
called at the end of the logical path handle to get a docroot
・handle_subrequest_start
called if the physical path is set up and checked
・handle_subrequest
called at the end of http_response_prepare()
・handle_physical_path
called after the physical path is created and no other handler is found for this request
・handle_request_done
called when the request is done
・handle_connection_close
called if the connection has to be closed
・handle_joblist
called after the connection_state_engine is left again and plugin internal handles have to be called
・connection_reset
called if the connection structure has to be cleaned up

状態遷移図におけるreqendではGETメソッドを取得できます。
ただし、connection.cでのcon->read_queueでは取得できません。
この時点でリクエストはread_queueでなくrequest.requestに格納されています。

POSTメッセージはreqendでは取得できません。
POSTの場合、状態遷移はreadpostに遷移しメモリか一時ファイルに格納されます。
handle_subrequestでlighttpd本体ではrequest.content_lengthでPOSTメッセージがあるか判定しているようです。

プラグイン開発は実際に mod_skeleton.c を参考にしましょう。
hookエントリを追加するにはint mod_skeleton_plugin_initに追記していきます。

#include "base.h"
#include "log.h"
#include "buffer.h"

#include "plugin.h"

#include 
#include 
#include 

/* plugin config for all request/connections */

typedef struct {
	array *match;
} plugin_config;

typedef struct {
	PLUGIN_DATA;

	buffer *match_buf;

	plugin_config **config_storage;

	plugin_config conf;
} plugin_data;

typedef struct {
	size_t foo;
} handler_ctx;

static handler_ctx * handler_ctx_init() {
	handler_ctx * hctx;

	hctx = calloc(1, sizeof(*hctx));

	return hctx;
}

static void handler_ctx_free(handler_ctx *hctx) {

	free(hctx);
}

/* init the plugin data */
INIT_FUNC(mod_skeleton_init) {
	plugin_data *p;

	p = calloc(1, sizeof(*p));

	p->match_buf = buffer_init();

	return p;
}

/* detroy the plugin data */
FREE_FUNC(mod_skeleton_free) {
	plugin_data *p = p_d;

	UNUSED(srv);

	if (!p) return HANDLER_GO_ON;

	if (p->config_storage) {
		size_t i;

		for (i = 0; i < srv->config_context->used; i++) {
			plugin_config *s = p->config_storage[i];

			if (!s) continue;

			array_free(s->match);

			free(s);
		}
		free(p->config_storage);
	}

	buffer_free(p->match_buf);

	free(p);

	return HANDLER_GO_ON;
}

/* handle plugin config and check values */

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(specific_config *));

	for (i = 0; i < srv->config_context->used; i++) {
		plugin_config *s;

		s = calloc(1, sizeof(plugin_config));
		s->match    = array_init();

		cv[0].destination = s->match;

		p->config_storage[i] = s;

		if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) {
			return HANDLER_ERROR;
		}
	}

	return HANDLER_GO_ON;
}

#define PATCH(x) \
	p->conf.x = s->x;
static int mod_skeleton_patch_connection(server *srv, connection *con, plugin_data *p) {
	size_t i, j;
	plugin_config *s = p->config_storage[0];

	PATCH(match);

	/* skip the first, the global context */
	for (i = 1; i < srv->config_context->used; i++) {
		data_config *dc = (data_config *)srv->config_context->data[i];
		s = p->config_storage[i];

		/* condition didn't match */
		if (!config_check_cond(srv, con, dc)) continue;

		/* merge config */
		for (j = 0; j < dc->value->used; j++) {
			data_unset *du = dc->value->data[j];

			if (buffer_is_equal_string(du->key, CONST_STR_LEN("skeleton.array"))) {
				PATCH(match);
			}
		}
	}

	return 0;
}
#undef PATCH

URIHANDLER_FUNC(mod_skeleton_uri_handler) {
	plugin_data *p = p_d;
	int s_len;
	size_t k, i;

	UNUSED(srv);

	if (con->mode != DIRECT) return HANDLER_GO_ON;

	if (con->uri.path->used == 0) return HANDLER_GO_ON;

	mod_skeleton_patch_connection(srv, con, p);

	s_len = con->uri.path->used - 1;

	for (k = 0; k < p->conf.match->used; k++) {
		data_string *ds = (data_string *)p->conf.match->data[k];
		int ct_len = ds->value->used - 1;

		if (ct_len > s_len) continue;
		if (ds->value->used == 0) continue;

		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;
}

/* this function is called at dlopen() time and inits the callbacks */

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;
}

このコードをビルドしてみます。ダイナミックリンクライブラリが作成されます。(.so .laが作成されます)
makefile.amを編集します。

lib_LTLIBRARIES += mod_counter.la
mod_counter_la_SOURCES = mod_counter.c
mod_counter_la_LDFLAGS = -module -export-dynamic -avoid-version -no-undefined
mod_counter_la_LIBADD = $(common_libadd)
$ ./configure

設定ファイル(/etc/lighttpd/lighttpd.conf)を変更します。

# Errorログ
server.errorlog = "/var/log/lighttpd/error.log"

# コンテンツ圧縮設定
compress.cache-dir = "/var/lib/lighttpd/cache/compress/"
compress.filetype  = ("text/css", "text/javascript", "text/plain", "text/html")

# mod_skeletonを追加
server.modules = (
       "mod_access",
       "mod_accesslog",
       "mod_auth",
       "mod_skeleton"
)

#フックするエントリの設定
skeleton.array = (".html")

設定したらデーモンを再起動しましょう。

httpクライアントから ローカルホストであるhttps://127.0.0.1/smp.html にアクセスしてみましょう。
443のステータスコードが返ってくれば、このプラグインは正常動作しているはずです。

サーバも組み込めたので、サーバのコンテンツを返してみましょう。

ということで次回はCGI連携などを書きます。

そういえば気づいたらSublimetext2のプラグイン管理が楽になってました。詳しくはここを参考に。

コマンドからもSublimetext2を使えるようにするためシンボリックリンクを張っておきます。

# case : mac
$ sudo ln -s /Applications/Sublime\ Text2.app/Contents/SharedSupport/bin/subl  /opt/local/bin/subl
$ subl  *.c

# case : centos 64bit /optにinstall
$ sudo mv Sublime\ Text\ 2 /opt
$ sudo ln -s /opt/Sublime\ Text\ 2/sublime_text /usr/bin/subl
$ sudo subl /usr/share/applications/sublime.desktop

Gtk-WARNING **: cannot open display とでたらユーザ間で重なっている可能性があります。