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/CPlusCompile & 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:281.3系〜2.2系
Apache2: HTTP Daemon Routine
モジュールの 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
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ステートメントデータタイプ
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]