Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

Apache ModuleでRequest ParameterをParseしてDBからデータを取得する

Request Parameter取得とDB接続

母校の同窓会幹事代表を務めています@yutakikuchi_です。
最近C++のエントリーを書く事が多いですが、今日もApache Moduleについて書きます。


Apache ModuleでRequest Parameterを取得する際はApacheのVersionに気をつけましょう。Versionによって使える関数が異なるようです。基本的には上位互換が保たれているようですが、最新Versionのドキュメントを参照している時に、実際には古いVersionを使ってしまっていると実装時に長い時間嵌る可能性があります。また単にRequest Paramterを取得しただけでは面白くないので、Parameterに従ってDB上のデータを参照する事を行いたいと思います。Apache ModuleやCのPreparedStatementに関する日本語ドキュメントは少ないので少しでも開発者の方々へ貢献できるように今後も頑張ります。

GitHub

GitHub Path

このエントリーで使用するソースコードGitHubに置きました。
CPlus/apache_module/ps/mod_db.c at master · yutakikuchi/CPlus はてなブックマーク - CPlus/apache_module/ps/mod_db.c at master · yutakikuchi/CPlus

Compile & Install

以下のコマンドでcompile & installしてくれます。installが完了したらApacheを再起動します。

$ sudo yum install httpd-devel mysql-devel -y
$ git clone git@github.com:yutakikuchi/CPlus.git
$ cd CPlus/apache_module/ps/ 
$ sudo apxs -i -a -c -I /usr/include/mysql -L /usr/lib64/mysql -lmysqlclient mod_db.c
$ sudo /etc/init.d/httpd restart

Apache VersionとParameter取得方法

ApacheのVersion確認

開発作業前にご自身のApache Versionを確認しておきましょう。

$ httpd -v
Server version: Apache/2.2.15 (Unix)
Server built:   Aug 13 2013 17:29:28
1.3系〜2.2系

Apache2: HTTP Daemon Routine はてなブックマーク - Apache2: HTTP Daemon Routine
モジュールの Apache 1.3 から Apache 2.0 への移植 - Apache HTTP サーバ はてなブックマーク - モジュールの Apache 1.3 から Apache 2.0 への移植 - Apache HTTP サーバ
Apacheの1系はドキュメントさえあまり残っていない状況なので、1系で開発することはお勧めしませんが念のために書いておきます。2.2系までの場合ap_getword,ap_getword_nc,apr_pstrdupのいずれかの関数を使って取得します。下の例ではapr_pstrdupを使っています。これらの関数は上位互換なので、ApacheのVersionをアップしても書き換えなくそのまま利用できそうです。( 1.3系から2.2系への移行は特に何も問題無いように思います。 )


その他の手段としてはap_add_cgi_vars(r);apr_table_get(r->subprocess_env, "QUERY_STRING");を組み合わせてParameterを取得する方法がありますが、上と同じようにParse処理は自前で書かないといけないので、大差は無いかと思っています。


下のサンプルの処理としてはとても単純でパラメータを&で区切ってhashにkey,valueとして入れ、それを後から参照しているだけです。使い易いようにparse_parameterとget_parameterに分けました。Validationは特にやっていないので適宜加えてください。

/* parse parameter */
static apr_hash_t *parse_parameter(request_rec *r) {
    char *str = apr_pstrdup(r->pool, r->args);
    if( str == NULL ) {
        return NULL;
    }
    
    apr_hash_t *hash = NULL;
    const char *del = "&";
    char *items, *last, *st;
    hash = apr_hash_make(r->pool);

    // set hash
    for ( items = apr_strtok(str, del, &last); items != NULL; items = apr_strtok(NULL, del, &last) ){
        st = strchr(items, '=');
        if (st) {
            *st++ = '\0';
            ap_unescape_url(items);
            ap_unescape_url(st);
        } else {
            st = "";
            ap_unescape_url(items);
        }
        apr_hash_set( hash, items, APR_HASH_KEY_STRING, st );
    }
    return hash;
}

/* get parameter */
static char *get_parameter(request_rec *r, apr_hash_t *hash, char *find_key) {
    apr_hash_index_t *hash_index;
    char *key, *val;
    hash_index = apr_hash_first(r->pool, hash);
    while (hash_index) {
        apr_hash_this(hash_index, (const void **)&key, NULL, (void **)&val);
        if( strcmp(key, find_key) == 0 ) {
            return (char*)val;
        }
        hash_index = apr_hash_next(hash_index);
    }
    return NULL;
}

// mainで使う
apr_hash_t *hash = parse_parameter(r);
char *id = get_parameter(r, hash, "id");
ap_rprintf(r, "id = [%d]\n", atoi(id));
2.4系

Developing modules for the Apache HTTP Server 2.4 - Apache HTTP Server はてなブックマーク - Developing modules for the Apache HTTP Server 2.4 - Apache HTTP Server
2.4になるとParameterのParseを便利なap_args_to_table関数の中でやってくれます。Parseされたデータをapr_table_getで参照するだけです。2.2まででやっていたような面倒な処理は一切不要になります。ap_args_to_tableは2.2以前は存在しないので気をつけてください。

apr_table_t *GET; 
ap_args_to_table(r, &GET);
const char *id = apr_table_get(GET, "id");
ap_rprintf(r, "id = [%d]\n", atoi(id));

DB上のデータを参照

上で取得したParameterの内容と一致するデータをDBから参照できるようにします。
参照時にはPreparedStatementを利用します。

apacheの設定ファイルを用意する

DBへの接続設定はapacheのconfファイルに記述します。設定が完了したらApacheが認識する必要があるので再起動します。

$ sudo vim /etc/httpd/conf.d/db.conf

# 以下を追記
DBHost localhost
DBPort 3306
DBUser root
#DBPass
DBName test
DBTableName sample

$ sudo /etc/init.d/httpd restart

上の定義をApache Moduleのcmd_recで呼び出します。

/*
 * Set the value for the 'DBHost' attribute.
 */
static const char *set_db_host(cmd_parms *cmd, void *mconfig, const char *name)
{
    db_env *db;
    db = (db_env *) mconfig;
    db->host = ap_getword_conf(cmd->pool, &name);
    return NULL;
}

(略)

static const command_rec db_conf_cmds[] = {
    AP_INIT_TAKE1("DBHost", set_db_host, NULL, OR_FILEINFO, "db hostname"),
    AP_INIT_TAKE1("DBPort", set_db_port, NULL, OR_FILEINFO, "db port"),
    AP_INIT_TAKE1("DBUser", set_db_user, NULL, OR_FILEINFO, "db username"),
    AP_INIT_TAKE1("DBPass", set_db_pass, NULL, OR_FILEINFO, "db password"),
    AP_INIT_TAKE1("DBName", set_db_name, NULL, OR_FILEINFO, "db name"),
    AP_INIT_TAKE1("DBTableName", set_db_table, NULL, OR_FILEINFO, "db tablename"),
    {NULL}
};
参照するTable

以下のようなTableを参照します。

mysql> SELECT * FROM test.sample;
+----+---------------------+
| id | created_at          |
+----+---------------------+
|  1 | 2013-11-21 22:05:43 |
|  2 | 2013-11-21 22:05:45 |
|  3 | 2013-11-21 22:05:49 |
|  4 | 2013-11-21 22:05:51 |
+----+---------------------+
4 rows in set (0.00 sec)
PreparedStatement

CのPreparedStatementはちょっと複雑です。以下に大まかな処理の流れを書いておきます。

1. StatementのInit
2. StatementのPrepare
3. ParameterのBind
4. StatementのExecute
5. ResultのBind
6. ResultのStore
7. ResultのFetch

これをコードに置き換えると以下のような感じになります。僕もついついやってしまう例としてはMYSQL_BINDのbuffer_typeの指定やresultの領域確保の数値を間違えてsegfaultを起こしたりします。buffer_typeについては以下のマニュアルを参考にすると良いと思います。 MySQL :: MySQL 5.1 リファレンスマニュアル :: 23.2.5 準備されたC APIステートメントデータタイプ はてなブックマーク - MySQL :: MySQL 5.1 リファレンスマニュアル :: 23.2.5 準備されたC APIステートメントデータタイプ

static int getDBContents(request_rec *r, int id) {
    // connect
    MYSQL *conn;
    conn = mysql_init(NULL);
    db_env *db = ap_get_module_config(r->per_dir_config, &db_module);
    int rid;
    MYSQL_TIME ts;

    if (!mysql_real_connect(conn, db->host, db->user, db->pass, db->name, db->port, NULL, 0)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Connection Error : %s", mysql_error(conn));
        mysql_close(conn);	
        return DECLINED;
    }

    // issue query
    char query[100];
    sprintf(query, "SELECT id, created_at FROM %s.%s where id = ?", db->name, db->table_name);

    // stmt
    MYSQL_STMT *stmt = mysql_stmt_init(conn);
    if (mysql_stmt_prepare(stmt, query, strlen(query)) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Prepare Error : %s", mysql_stmt_error(stmt));
        mysql_close(conn);	
        return DECLINED;
    }

    // bind
    MYSQL_BIND bind[1];
    memset(bind, 0, sizeof(id));

    bind[0].buffer = &id;
    bind[0].buffer_type = MYSQL_TYPE_LONG;
    bind[0].buffer_length = sizeof(id);
    bind[0].is_null = 0;

    // bind_param
    if (mysql_stmt_bind_param(stmt,bind) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Bind Param Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // execute
    if (mysql_stmt_execute(stmt) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Execute Error : %s", mysql_stmt_error(stmt));
        mysql_close(conn);	
        return DECLINED; 		
    }  

    // bind_result
    MYSQL_BIND result[2];
    memset(result, 0, sizeof(result));

    result[0].buffer = &rid;
    result[0].buffer_type = MYSQL_TYPE_LONG;
    result[0].buffer_length = sizeof(rid);
    result[0].is_null = 0;

    result[1].buffer = &ts;
    result[1].buffer_type = MYSQL_TYPE_DATETIME;
    result[1].buffer_length = sizeof(ts);
    result[1].is_null = 0;

    // bind_result
    if (mysql_stmt_bind_result(stmt,result) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Bind Result Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // store_result
    if (mysql_stmt_store_result(stmt) != 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Mysql Store Result Error : %s", mysql_stmt_error(stmt));
        mysql_stmt_close(stmt);
        mysql_close(conn);
        return DECLINED;
    }

    // stmt_fetch
    while (!mysql_stmt_fetch(stmt)) {
        ap_rprintf(r, "id = [%d]\n", rid);
        char str[30];
        sprintf(str, "%04d-%02d-%02d %02d:%02d:%02d", ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second);
        ap_rprintf(r, "datetime = [%s]\n", str);
    }

    // close
    mysql_stmt_close(stmt);
    mysql_close(conn);
    return OK;
}
データ取得のテスト

curlをする際にid=2, id=3のように指定して、データが取得できる事を確認します。

$ curl -i "http://localhost/getid?id=2"
HTTP/1.1 200 OK
Date: Thu, 21 Nov 2013 16:47:39 GMT
Server: Apache/2.2.15 (CentOS)
Content-Length: 42
Connection: close
Content-Type: text/plain; charset=UTF-8

id = [2]
datetime = [2013-11-21 22:05:45]

$ curl -i "http://localhost/getid?id=3"
HTTP/1.1 200 OK
Date: Thu, 21 Nov 2013 16:48:19 GMT
Server: Apache/2.2.15 (CentOS)
Content-Length: 42
Connection: close
Content-Type: text/plain; charset=UTF-8

id = [3]
datetime = [2013-11-21 22:05:49]