【Lighttpd】解説 mod_proxy

light_logo

はじめに

今回の記事はこの記事の続きです。
mod_proxyのソースコードリーディングをしてみます。mod_proxyはリクエストを振り分けるリバースプロキシだけでなく、フォワードプロキシとしても動かすことができます。

全体の流れ

lighttpd-x.x -> src -> mod_xxxとあるのがプラグイン群です。

まず,mod_proxyのイベントハンドラから見てみます。ハンドラはどのプラグインでも必須になります。

int mod_proxy_plugin_init(plugin *p) {
	p->version      = LIGHTTPD_VERSION_ID;
	p->name         = buffer_init_string("proxy");

	p->init         = mod_proxy_init;
	p->cleanup      = mod_proxy_free;
	p->set_defaults = mod_proxy_set_defaults;
	p->connection_reset        = mod_proxy_connection_close_callback; /* end of req-resp cycle */
	p->handle_connection_close = mod_proxy_connection_close_callback; /* end of client connection */
	p->handle_uri_clean        = mod_proxy_check_extension;
	p->handle_subrequest       = mod_proxy_handle_subrequest;
	p->handle_trigger          = mod_proxy_trigger;

	p->data         = NULL;

	return 0;
}

SUBREQUEST_FUNCエントリから見ていきます。このフックエントリはReadPostが終了した時に呼ばれます。

SUBREQUEST_FUNC(mod_proxy_handle_subrequest) {
   ...
	switch(proxy_write_request(srv, hctx)) {
        ...
        }
   ...
}

hctxというのは接続先のファイルディスクリプタや状態、バッファへのポインタを管理している構造体です。

typedef struct {
	proxy_connection_state_t state;
	time_t state_timestamp;

	data_proxy *host;

	buffer *response;
	buffer *response_header;

	chunkqueue *wb;

	int fd; /* fd to the proxy process */
	int fde_ndx; /* index into the fd-event buffer */

	size_t path_info_offset; /* start of path_info in uri.path */

	connection *remote_conn;  /* dump pointer */
	plugin_data *plugin_data; /* dump pointer */
} handler_ctx;

リクエストが完了した時にmod_proxy_check_extensionが呼ばれhandler_ctxを初期化しています。

static handler_ctx * handler_ctx_init(void) {
	handler_ctx * hctx;


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

	hctx->state = PROXY_STATE_INIT;
	hctx->host = NULL;

	hctx->response = buffer_init();
	hctx->response_header = buffer_init();

	hctx->wb = chunkqueue_init();

	hctx->fd = -1;
	hctx->fde_ndx = -1;

	return hctx;
}

話を戻しますが、SUBREQUEST_FUNCエントリの中でproxy_write_requestを呼んでいます。

socket関数でソケットを作成しています。* IPv4とIPv6で分岐

次にhctx構造体の状態を管理するstateで分岐します。

初期状態はPROXY_STATE_INITなのでこの分岐に入り、proxy_establish_connectionを呼んでいます。
sockaddr構造体にポートやアドレスを設定しproxy接続先のアドレスに接続しにいきます。

connect(proxy_fd, proxy_addr, servlen) (int connect(int sock, const struct sockaddr *addr, socket_t addrlen);)

connect関数の戻り値が失敗で errno 115 の場合は接続中ということでPROXY_STATE_CONNECTに遷移します。

switch (proxy_establish_connection(srv, hctx)) {
		case 1:
			proxy_set_state(srv, hctx, PROXY_STATE_CONNECT);

			/* connection is in progress, wait for an event and call getsockopt() below */

			fdevent_event_set(srv->ev, &(hctx->fde_ndx), hctx->fd, FDEVENT_OUT);

			return HANDLER_WAIT_FOR_EVENT;
		case -1:
			/* if ECONNREFUSED choose another connection -> FIXME */
			hctx->fde_ndx = -1;

			return HANDLER_ERROR;
		default:
			/* everything is ok, go on */
			proxy_set_state(srv, hctx, PROXY_STATE_PREPARE_WRITE);
			break;
}

connectで成功していればhctxのstateはPROXY_STATE_PREPARE_WRITEに遷移します。

static int proxy_create_env(server *srv, handler_ctx *hctx) が呼ばれHTTPヘッダを作成しています。

proxy_append_header(con, "X-Forwarded-For", (char *)inet_ntop_cache_get_ip(srv, &(con->dst_addr)));
proxy_set_header(con, "X-Forwarded-Proto", con->conf.is_ssl ? "https" : "http");

続いてPOST時はmessage-bodyが一定サイズ以上は一時ファイル、それ以外はメモリに格納しています。
con->request_content_queueでmessage-bodyを見てhctx->wb->bytes_inに入れています。

if (con->request.content_length) {
		chunkqueue *req_cq = con->request_content_queue;
		chunk *req_c;
		off_t offset;

		/* something to send ? */
		for (offset = 0, req_c = req_cq->first; offset != req_cq->bytes_in; req_c = req_c->next) {
			off_t weWant = req_cq->bytes_in - offset;
			off_t weHave = 0;

			/* we announce toWrite octects
			 * now take all the request_content chunk that we need to fill this request
			 * */

			switch (req_c->type) {
			case FILE_CHUNK:
				weHave = req_c->file.length - req_c->offset;

				if (weHave > weWant) weHave = weWant;

				chunkqueue_append_file(hctx->wb, req_c->file.name, req_c->offset, weHave);

				req_c->offset += weHave;
				req_cq->bytes_out += weHave;

				hctx->wb->bytes_in += weHave;

				break;
			case MEM_CHUNK:
				/* append to the buffer */
				weHave = req_c->mem->used - 1 - req_c->offset;

				if (weHave > weWant) weHave = weWant;

				b = chunkqueue_get_append_buffer(hctx->wb);
				buffer_append_memory(b, req_c->mem->ptr + req_c->offset, weHave);
				b->used++; /* add virtual \0 */

				req_c->offset += weHave;
				req_cq->bytes_out += weHave;

				hctx->wb->bytes_in += weHave;

				break;
			default:
				break;
			}

			offset += weHave;
		}

	}

最後に, case PROXY_STATE_WRITEです。

static handler_t proxy_write_request(server *srv, handler_ctx *hctx) に繋がりますが、ここでsrv->network_backend_writeを呼んでいます。

ret = srv->network_backend_write(srv, con, hctx->fd, hctx->wb, MAX_WRITE_LIMIT);
chunkqueue_remove_finished_chunks(hctx->wb);

これが,write/kqueueなどmake時に決定したプラットフォームに合ったシステムコールでhctx->fdに書き込みを行います。
例えば、linux2.6以降ならwritevを使うことができます。

この後、PROXY_STATE_READに遷移します。

if (hctx->wb->bytes_out == hctx->wb->bytes_in) {
	proxy_set_state(srv, hctx, PROXY_STATE_READ);
	fdevent_event_del(srv->ev, &(hctx->fde_ndx), hctx->fd);
	fdevent_event_set(srv->ev, &(hctx->fde_ndx), hctx->fd, FDEVENT_IN);
} else {
	fdevent_event_set(srv->ev, &(hctx->fde_ndx), hctx->fd, FDEVENT_OUT);

	return HANDLER_WAIT_FOR_EVENT;
}

続いて、PROXY_STATE_INITに戻ります。
ここでは、proxy_handle_fdevent->proxy_demux_response->proxy_response_parse と繋がります。

static handler_t proxy_handle_fdevent(server *srv, void *ctx, int revents)

proxy_handle_fdeventはfdevent_register(srv->ev, hctx->fd, proxy_handle_fdevent, hctx)で接続しているhostServerを登録することで何かイベントを検出したときに通知が来ます。

void *ctxにはhandler_ctxが入っているので、これがFDEVENT_INかFDEVENT_OUTかによって分岐します。

次にproxy_demux_responseに処理が移り,readでhostServerからptrを読み込んでいます。

static int proxy_demux_response(server *srv, handler_ctx *hctx) {

ioctlでhostServerのバッファバイト数を読み取ります。

if (ioctl(hctx->fd, FIONREAD, &b)) {
	log_error_write(srv, __FILE__, __LINE__, "sd",
			"ioctl failed: ",
			proxy_fd);
	return -1;
}

バッファバイトがある場合にreadでhostServerからレスポンスを読み取ります。

if (-1 == (r = read(hctx->fd, hctx->response->ptr + hctx->response->used - 1, b))) {
	if (errno == EAGAIN) return 0;
	log_error_write(srv, __FILE__, __LINE__, "sds",
			"unexpected end-of-file (perhaps the proxy process died):",
			proxy_fd, strerror(errno));
	return -1;
}

readで読みこんだhostServerのレスポンスをparseします。

static int proxy_response_parse(server *srv, connection *con, plugin_data *p, buffer *in) 

そしてrevents & FDEVENT_OUTの結果、writeで接続元にレスポンスを返す処理に入ります。
hostServerのレスポンスを接続元に返すためにメモリに格納します。


http_chunk_append_mem(srv, con, hctx->response->ptr, hctx->response->used);

http_chunk_append_memはhttp_chunk.cで宣言されています。

int http_chunk_append_mem(server *srv, connection *con, const char * mem, size_t len) {
	chunkqueue *cq;

	if (!con) return -1;

	cq = con->write_queue;

	if (len == 0) {
		if (con->response.transfer_encoding & HTTP_TRANSFER_ENCODING_CHUNKED) {
			chunkqueue_append_mem(cq, "0\r\n\r\n", 5 + 1);
		} else {
			chunkqueue_append_mem(cq, "", 1);
		}
		return 0;
	}

	if (con->response.transfer_encoding & HTTP_TRANSFER_ENCODING_CHUNKED) {
		http_chunk_append_len(srv, con, len - 1);
	}

	chunkqueue_append_mem(cq, mem, len);

	if (con->response.transfer_encoding & HTTP_TRANSFER_ENCODING_CHUNKED) {
		chunkqueue_append_mem(cq, "\r\n", 2 + 1);
	}

	return 0;
}

chunqueue *cq = con->write_queue に対して、chunkqueue_append_mem(cq, mem, len)でデータをセットしています。

最後に接続がサーバ内でリセットされた場合にはconnection_reset、接続がクライアントに切られた場合にはhandle_connection_closeで共に、mod_proxy_connection_close_callbackが呼ばれます。

ここでfdevent_event_del,fdevent_unregisterが行われfdeventを消去し,handler_ctx_freeで接続先のために確保したメモリを解放しています。

以上が一連の流れです。

lighttpd.confの設定取得

lighttpd.confの設定を掴んでいるのはSUBREQUEST_FUNC,mod_proxy_check_extensionです。ここで、mod_proxy_patch_connection関数を呼び設定を取得しています。

plugin_config *s = p->config_storage[0];

buffer_is_equal_stringでkeyとproxy.serverなどのchar比較をしてplugin_dataに取得した設定を入れています。

p->conf.x = s->x;

特徴のある設計なので掴むまで時間がかかりました。