目次:
前回:
次回:
1. 今回の目標
リクエストごとに phpを実行し、SQLite3 データベース /var/lib/BBASN/log.db に定義したテーブルに リクエスト時刻とIPアドレス を記録する
2. 設計
2-1. 最初に結論
リクエストごとに プログラム ipcheck にIPアドレスを渡すには、
RewriteCond ${(マップ名):%{REMOTE_ADDR}} dummy
RewriteRule ^ -
のようにすればよい。
また、SQLite3 は、データベース全体がファイルとして保持されるのが特徴であるが、
linux で SQLite3 データベースに書き込む場合、
対応するファイルに読み込み・書き込み権限、
そのファイルのあるディレクトリに書き込み・実行権限が必要となる
2-2. リクエストごとに php にIPアドレスを渡す方法
前回、
RewriteEngine On
RewriteMap ipcheck "prg:/usr/bin/sudo -u apache -g apache /usr/bin/php /usr/local/bin/BBASN/ipcheck.php"
RewriteCond ${(マップ名):} dummy
RewriteRule ^ -
のようにすることで、リクエストごとに ipcheck プログラムを呼べることを確認した。
ここでいう「プログラムを呼ぶ」というのは、(プログラムの実行ではなく、) 常駐しているプログラムに対して標準入力を渡し、プログラムによる標準出力を待つことを意味するのだった。
上記の設定では、標準入力として(空文字 + 改行)を渡しているが、 ("任意の 文字列" + 改行) を渡したい場合は
RewriteEngine On
RewriteMap ipcheck "prg:/usr/bin/sudo -u apache -g apache /usr/bin/php /usr/local/bin/BBASN/ipcheck.php"
RewriteCond ${(マップ名):任意の\ 文字列} dummy
RewriteRule ^ -
という風にすればよい。
また、mod_rewrite(Rewrite~) の公式ドキュメント1 の説明に拠れば、サーバ変数 として REMOTE_ADDR というものがあり、こちらはリモートホスト、つまり相手のコンピュータのIPアドレスが記録されている。
そしてサーバ変数には、 %{(サーバ変数名)} の形式でアクセスできるとのことだ。
なので
RewriteEngine On
RewriteMap ipcheck "prg:/usr/bin/sudo -u apache -g apache /usr/bin/php /usr/local/bin/BBASN/ipcheck.php"
RewriteCond ${(マップ名):%{REMOTE_ADDR}} dummy
RewriteRule ^ -
という風にすれば、標準入力としてプログラムに (IPアドレス + 改行) を渡せる。
蛇足: RewriteRule ^ - [E=DUMMY:${(マップ名):%{REMOTE_ADDR}}] ではダメなのか
実は、少なくとも Apache 2.4 では
RewriteCond ${(マップ名):%{REMOTE_ADDR}} dummy
RewriteRule ^ -
は
RewriteRule ^ - [E=DUMMY:${(マップ名):%{REMOTE_ADDR}}]
としても動く。
まずは、これについて解説する。
mod_rewrite 公式ドキュメント1 によれば、
RewriteRule ディレクティブは RewriteRule Pattern Substitution [flags] という構文を持つ。
よって、 [E=DUMMY:${(マップ名):%{REMOTE_ADDR}}] の部分は flags に当たる。
flags で利用可能な構文の一つとして、 E=[!]VAR[:VAL] というものがあり、
E=VAR:VAL とした場合、 VAR で指定される名前を持つ環境変数を定義して、そこに VAL を代入することを意味する。
そして RewriteRule の flags の公式ドキュメント2 に拠れば、
VALmay contain backreferences ($Nor%N) which are expanded.
(引用者訳:VALには$Nや%N形式の後方参照(変数展開)を含めることが出来ます。)
とのことである。
しかし、これは
- 
$N: N = 0~9 で、RewriteRuleのPatternパートから キャプチャした文字列を取って来れる仕組み- 例えば (ドキュメントルートで)
とすると、RewriteRule ^article/([0-9]+)/([a-z]+) ./article.php?id=$1&lang=$2 [L]https://localhost/article/1119/enへアクセスした時、
 https://localhost/article.php?id=1119&lang=enへリライトされる
 
- 例えば (ドキュメントルートで)
- 
%N: N = 0~9 で、 対応するRewriteCondがある場合、(最後の)CondPatternパートからキャプチャした文字列を取って来れる仕組み- 例えば (ドキュメントルートで)
とすると、RewriteCond %{REQUEST_SCHEME} ^(https?)$ RewriteCond %{HTTP_HOST} ^([^.]+)\.localhost$ RewriteRule ^article/([0-9]+)/([a-z]+) ./article.php?subdomain=%1&id=$1&lang=$2 [L]https://news.localhost/article/1119/enへアクセスした時、
 https://news.localhost/article.php?subdomain=news&id=1119&lang=enへリライトされる
- 上記について、^(https?)$のキャプチャは、当該のRewriteCondが最後でないため、無視される。
 
- 例えば (ドキュメントルートで)
であり、 ${(マップ名):key} や %{(サーバ変数名)} とは根本的に異なる仕組みである。
なので  [E=DUMMY:★] の★の部分で ${(マップ名):(任意の文字列)} や %{(サーバ変数名)} が利用可能であることは、公式ドキュメントによる保証は一切ない。
一方、
RewriteCond ${(マップ名):%{REMOTE_ADDR}} dummy
RewriteRule ^ -
は、 RewriteCond TestString CondPattern [flags] の TestString で
${mapname:key|default} が利用できて、
かつ Using RewriteMapの公式ドキュメント3で key に 環境変数を展開する実例が紹介されていることにより正当化できる。
(蛇足 ここまで)
Apache から渡された (IPアドレス + 改行) は、 php では
<?php
while (($line = fgets(STDIN)) !== false){
    echo "OK\n";
    fflush(STDOUT);
}
のようにすれば、 $line として受け取れる。
さらに $ip = trim($line); のようにすれば、改行を取り除いて $ip として IPアドレスを受け取れる。
2-3. SQLite3 のデータベースを準備する
リクエストごとに、 SQLite3 データベース へ リクエスト時刻 と IPアドレス を記録したい場合、
まずは データベースを作り、次にそのデータベースにテーブルを定義する必要がある。
2-3-1. データベースの作成と権限
SQLite3 では、「データベースはファイルとして表現される」仕様になっている。
よって、「SQLite3 のデータベースを作る」とは、「新規ファイルを作る」ことそのものである。
例えば /var/lib/BBASN/log.db をデータベースとして新規に作りたい場合、
単に touch /var/lib/BBASN/log.db を実行すればよい。
これでデータベースそのものは完成する。
(あるいは、2-3-2節でも述べるが /var/lib/BBASN/log.db が存在しない状態で、php で new SQLite3("/var/lib/BBASN/log.db") することでも作れる)
このように簡単にデータベースが作れるのは、「データベースがファイルとして表現されている」ことの大きなメリットの一つである。
一方、デメリットもあり、その一つは「データベースへのアクセス権限を、ファイルとディレクトリの権限として考えなければいけない」ことである。
データベース /var/lib/BBASN/log.db に対する読み込みと書き込みをするには、
当然だがこのファイルへの読み込み権限と書き込み権限が必要である。
だがこれだけでは十分でなく、 データベースの格納されたディレクトリへの書き込み権限と実行権限 も必要になる。
今回の例で言えば、ディレクトリ /var/lib/BBASN への書き込み権限と実行権限が必要になる。
これは、SQLite3 がトランザクション毎に、データベースと同ディレクトリに一時ファイルを作る3ためである。
Linuxでは、ディレクトリにファイルを作るためには、書き込み権限と実行権限の両方が必要になる。
2-3-2. テーブルの作成
テーブルの作成は、 SQLite3 を OSにインストールして sqlite3 コマンドから行うこともできるが、
今回は php がインストール済みなので、 php から行うことにしてみる。
( php は、内部に SQLite3 を持っている)
次のようなコードでテーブルを作成できる。
<?php
$db = new SQLite3('/var/lib/BBASN/log.db');
if (!$db)
    exit("データベースに接続できませんでした。");
$sqls = [
    'accesses作成' => <<<SQL
        CREATE TABLE IF NOT EXISTS accesses (
            utc TEXT DEFAULT CURRENT_TIMESTAMP,
            ip  TEXT
        );
    SQL
];
foreach($sqls as $title => $sql)
    if ($db->exec($sql))
        echo "$title 成功\n";
    else
        echo "$title 失敗\n";
$db->close();
まず、 $db = new SQLite3('/var/lib/BBASN/log.db'); で、
データベース '/var/lib/BBASN/log.db' に接続を試みている。
ここで当該のデータベースファイルが存在しない場合、データベースファイルを新規作成する。
その後、 $db->exec((CREATE TABLE 文)) で CREATE TABLE文を実行できる。
この時、データベースファイルへの書き込み権限、またはデータベースファイルのディレクトリへの書き込み・実行権限がないと「失敗」する。
(データベースファイルへの読み込み権限がない場合は、そもそも接続に失敗する。)
また、作成するテーブル accesses については、
- utc TEXT DEFAULT CURRENT_TIMESTAMP
- ip TEXT
のカラムがあればよいだろう。
なぜ時刻を TEXT 型で保存するのか
まずSQLite3には、「時刻型」のようなものは存在しない。
これは公式ドキュメント4にも次のように書かれている。
SQLite does not have a storage class set aside for storing dates and/or times. Instead, the built-in Date And Time Functions of SQLite are capable of storing dates and times as TEXT, REAL, or INTEGER values:
- TEXT as ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS").
- REAL as Julian day numbers, the number of days since noon in Greenwich on November 24, 4714 B.C. according to the proleptic Gregorian calendar.
- INTEGER as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC.
(引用者訳: SQLiteには日付や時刻を格納するために特別に用意された型はありません。
そこで、SQLite に組み込まれた日付・時刻関数を使い、日付や時刻を次のように TEXT, REAL, INTEGER 型で保存することが一般的です。
- TEXT: ISO 8601 形式の文字列 (
"YYYY-MM-DD HH:MM:SS.SSS")- REAL: ユリウス日数 (先発グレゴリオ暦紀元前 4714 年 11 月 24 日の正午、つまり
-4712-01-01 12:00:00 UTCからの経過日数)- INTEGER: UNIX時刻 (
1970-01-01 00:00:00 UTCからの経過秒数 ))
また、 CREATE TABLE文で DEFAULT CURRENT_TIMESTAMP のカラムを作ると、そのカラムの初期値は 文字列となることが公式ドキュメント5で次のように説明されている。
If the default value of a column is CURRENT_TIME, CURRENT_DATE or CURRENT_TIMESTAMP, then the value used in the new row is a text representation of the current UTC date and/or time. (中略) The format for CURRENT_TIMESTAMP is "YYYY-MM-DD HH:MM:SS".
(引用者訳: カラムの初期値をCURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMPのどれかにすると、新しいレコードを挿入した時の初期値は 現在のUTCでの日付・時刻を表す文字列となります。 (中略)CURRENT_TIMESTAMPのフォーマットは"YYYY-MM-DD HH:MM:SS"です。)
そこで、 CURREMT_TIMESTAMP をそのまま保存するために、 utc カラムを TEXT型とした。
(なぜ時刻を TEXT 型で保存するのか ここまで)
2-4. テーブルにレコードを挿入する
$db でデータベースに接続しており、 $ip に IPアドレスを表す文字列が入っている場合、
次のようにしてテーブルにレコードを挿入出来る。
$stmt = $db->prepare('INSERT INTO accesses(ip) VALUES (:ip)');
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$stmt->execute();
メソッド $db->exec にクエリをべた書きすることはお勧めできない。
$db->exec("INSERT INTO accesses(ip) VALUES ('$ip')");
のようにクエリに変数を入れて直接実行することもできるのだが、
これはSQLインジェクションの危険があるため、避けたほうが良い。
SQLインジェクションの例
第三者の悪意で $ip の中身が "'); (任意のSQL); SELECT ('" になった場合、
$db->exec("INSERT INTO accesses(ip) VALUES (''); (任意のSQL); SELECT ('')");
が実行されることになり、 (任意のSQL) が実行されてしまう。
(SQLインジェクションの例 ここまで)
一方、 $stmt = $db->prepare() を使うやり方では、
$stmt->bindValue の際に、 SQLインジェクションを引き起こしかねない文字をエスケープするなどして、自動で安全にしてくれる。
2-5. 簡単なビューアを用意する
/var/lib/BBASN/log.db にあるテーブル一覧を表示し、
また SELECT文 を実行してその結果を表示するための php ビューアは次のように作れる。
ビューア
<?php
// SQLite 接続 
$db = new SQLite3('/var/lib/BBASN/log.db');
$db->busyTimeout(1000);
echo "接続しました\n";
// テーブル一覧を表示 
echo "テーブル一覧:\n";
$result = $db->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
while ($record = $result->fetchArray(SQLITE3_ASSOC))
    echo "  - " . $record['name'] . "\n";
echo "\nSQLを入力してください(例: SELECT * FROM tablename LIMIT 5;)\n";
echo "終了するには空行\n\n";
// 対話ループ
while (true) {
    $query = "";
    echo "sqlite> ";
    while (!in_array(($line = fgets(STDIN)), ["\n", "\r", "\r\n"]) && !feof(STDIN)) {
        echo "sqlite> ";
        $query .= $line;
    }
    if (feof(STDIN) || trim($query) == "") break;
    try {
        $result = $db->query($query);
        if ($result === false) {
            echo "クエリエラー: " . $db->lastErrorMsg() . "\n";
            continue;
        }
        // 結果を出力
        $cols = [];
        $record = $result->fetchArray(SQLITE3_ASSOC);
        if ($record === false) {
            echo "(結果なし)\n";
            continue;
        }
        $cols = array_keys($record);
        echo implode(" | ", $cols) . "\n";
        echo str_repeat("-", strlen(implode(" | ", $cols))) . "\n";
        echo implode(" | ", $record) . "\n";
        while ($record = $result->fetchArray(SQLITE3_ASSOC)) {
            echo implode(" | ", $record) . "\n";
        }
    } catch (Exception $e) {
        echo "例外: " . $e->getMessage() . "\n";
    }
}
echo "終了しました。\n";
$db->close();
(ビューア ここまで)
(3章 手順8 のphpプログラムと全く同じである)
利用例
[user@xxxxxxxxxxxxx ~]$ sudo php /path/to/viewer.php
接続しました
テーブル一覧:
  - accesses
SQLを入力してください(例: SELECT * FROM tablename LIMIT 5;)
終了するには空行
sqlite> SELECT * FROM accesses;
sqlite>
utc | ip
--------
2025-10-14 23:36:12 | x.x.x.x
2025-10-14 23:36:13 | x.x.x.x
2025-10-14 23:36:15 | x.x.x.x
2025-10-14 23:36:15 | x.x.x.x
2025-10-14 23:36:16 | x.x.x.x
2025-10-14 23:36:17 | x.x.x.x
2025-10-14 23:36:17 | x.x.x.x
2025-10-14 23:36:18 | x.x.x.x
2025-10-14 23:36:18 | x.x.x.x
sqlite>
終了しました。
[user@xxxxxxxxxxxxx ~]$
(利用例 ここまで)
3. 作ってみる
リクエストごとに phpを実行し、SQLite3 データベース /var/lib/BBASN/log.db に定義したテーブルに リクエスト時刻とIPアドレス を記録する仕組みを作ってみる。
- 
前回 の手順をすべて実施する
 
- 
sudo nano /etc/httpd/conf/block_by_ASN.confを実行して、/etc/httpd/conf/block_by_ASN.confの内容を次のものに書き換える/etc/httpd/conf/block_by_ASN.confRewriteEngine On RewriteMap ipcheck "prg:/usr/bin/sudo -u apache -g apache /usr/bin/php /usr/local/bin/BBASN/ipcheck.php" RewriteCond ${ipcheck:%{REMOTE_ADDR}} dummy RewriteRule ^ -- (ファイルパスは 前回 の手順10のものを使うこと)
- (変更箇所は3行目のみである)
- この書き換えで、 Apache は リクエストがある度に php に (ipアドレス + 改行) を標準入力として渡すようになる
 
 
- 
sudo nano /usr/local/bin/BBASN/ipcheck.phpを実行して、
 sudo nano /usr/local/bin/BBASN/ipcheck.phpの内容を次のものに書き換える/usr/local/bin/ipcheck.php<?php $db = new SQLite3('/var/lib/BBASN/log.db'); while (($line = fgets(STDIN)) !== false){ $ip = trim($line); $stmt = $db->prepare('INSERT INTO accesses(ip) VALUES (:ip)'); $stmt->bindValue(':ip', $ip, SQLITE3_TEXT); $stmt->execute(); echo "OK\n"; fflush(STDOUT); } $db->close();- リクエストがあるごとに SQLite3 データベースにIPアドレスを記録するようにしている。
 但し、記録先のデータベースおよびテーブルがまだ用意できていないので、手順4~7で用意する。
- (エディタ nano で [ /usr/local/bin/BBASN/ipcheck.php is meant to be read-only ]などの警告が出る場合があるが、無視して読み書きできる。)- これは ユーザ(今回は sudoしているのでroot) がipcheck.phpへのアクセス権を持っていないことについての警告である。
- Linuxでは、 rootは特別に、 パーミッションに拘わらずすべてのファイルに対して読み書きが可能となっている。
 
 
- これは ユーザ(今回は 
 
- リクエストがあるごとに SQLite3 データベースにIPアドレスを記録するようにしている。
- 
sudo nano /usr/local/bin/BBASN/scheme.phpを実行し、
 次のような/usr/local/bin/BBASN/scheme.phpを作る。/usr/local/bin/BBASN/scheme.php<?php $db = new SQLite3('/var/lib/BBASN/log.db'); if (!$db) exit("データベースに接続できませんでした。"); $sqls = [ 'accesses作成' => <<<SQL CREATE TABLE IF NOT EXISTS accesses ( utc TEXT DEFAULT CURRENT_TIMESTAMP, ip TEXT ); SQL ]; foreach($sqls as $title => $sql) if ($db->exec($sql)) echo "$title 成功\n"; else echo "$title 失敗\n"; $db->close();
- 次のコマンドを実行し、/usr/local/bin/BBASN/scheme.phpの所有者とパーミッションを変更する。
 所有ユーザroot, 所有グループrootで 所有ユーザは読み書き可能 (6)、それ以外は一切のアクセスを不能(0)にする。sudo chown root:root /usr/local/bin/BBASN/scheme.php sudo chmod 600 /usr/local/bin/BBASN/scheme.php- 
scheme.phpはデータベースとテーブルの定義用のプログラムであり、sudoを使って手動で1回呼べばよいだけである。
 よってapacheによるアクセスは不要。
 
 
- 
- 次のコマンドを実行し、データベース /var/lib/BBASN/log.dbを作ってapacheで読み書き可能にする。sudo touch /var/lib/BBASN/log.db sudo chown apache:apache /var/lib/BBASN/log.db sudo chmod 600 /var/lib/BBASN/log.db- (データベースの所属ディレクトリに対して、 apacheはフルアクセス可能であるように設定されているはずである。 (前回 の手順3) )- SQLite3 のデータベースを(読み書き両方で)利用する場合、 データベースファイルに対する読み書き権限と、その所属ディレクトリに対する書き込みと実行の権限が必要なので、この確認は重要である。
 
 
- SQLite3 のデータベースを(読み書き両方で)利用する場合、 データベースファイルに対する読み書き権限と、その所属ディレクトリに対する書き込みと実行の権限が必要なので、この確認は重要である。
 
- (データベースの所属ディレクトリに対して、 
- 
sudo php /usr/local/bin/BBASN/scheme.phpを実行し、
 phpプログラム/usr/local/bin/BBASN/scheme.phpを実行する- データベース /usr/local/bin/BBASN/log.dbに、テーブルaccessesが作成される。
- 実行した結果、 accesses作成 成功と出れば成功。
 accesses作成 失敗と出た場合は権限などを見直して再試行すること。
 
 
- データベース 
- 
sudo nano /usr/local/bin/BBASN/viewer.phpを実行し、
 次のような/usr/local/bin/BBASN/viewer.phpを作る。/usr/local/bin/BBASN/viewer.php<?php // SQLite 接続 $db = new SQLite3('/var/lib/BBASN/log.db'); $db->busyTimeout(1000); echo "接続しました\n"; // テーブル一覧を表示 echo "テーブル一覧:\n"; $result = $db->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"); while ($record = $result->fetchArray(SQLITE3_ASSOC)) echo " - " . $record['name'] . "\n"; echo "\nSQLを入力してください(例: SELECT * FROM tablename LIMIT 5;)\n"; echo "終了するには空行\n\n"; // 対話ループ while (true) { $query = ""; echo "sqlite> "; while (!in_array(($line = fgets(STDIN)), ["\n", "\r", "\r\n"]) && !feof(STDIN)) { echo "sqlite> "; $query .= $line; } if (feof(STDIN) || trim($query) == "") break; try { $result = $db->query($query); if ($result === false) { echo "クエリエラー: " . $db->lastErrorMsg() . "\n"; continue; } // 結果を出力 $cols = []; $record = $result->fetchArray(SQLITE3_ASSOC); if ($record === false) { echo "(結果なし)\n"; continue; } $cols = array_keys($record); echo implode(" | ", $cols) . "\n"; echo str_repeat("-", strlen(implode(" | ", $cols))) . "\n"; echo implode(" | ", $record) . "\n"; while ($record = $result->fetchArray(SQLITE3_ASSOC)) { echo implode(" | ", $record) . "\n"; } } catch (Exception $e) { echo "例外: " . $e->getMessage() . "\n"; } } echo "終了しました。\n"; $db->close();
- 次のコマンドを実行し、/usr/local/bin/BBASN/viewer.phpの所有者とパーミッションを変更する。
 所有ユーザroot, 所有グループrootで 所有ユーザは読み書き可能 (6)、それ以外は一切のアクセスを不能(0)にする。sudo chown root:root /usr/local/bin/BBASN/viewer.php sudo chmod 600 /usr/local/bin/BBASN/viewer.php- 
viewer.phpはsudoを使って手動で利用する前提のビューアである。
 よってroot以外のアクセスは不要。
 
 
- 
- 
sudo php /usr/local/bin/BBASN/viewer.phpを実行し、次のようにして データベースにaccessesテーブルが正しく定義されたことを確認する。[user@xxxxxxxxxxxxx ~]$ sudo php /usr/local/bin/BBASN/viewer.php 接続しました テーブル一覧: - accesses SQLを入力してください(例: SELECT * FROM tablename LIMIT 5;) 終了するには空行 sqlite> PRAGMA TABLE_INFO(accesses); sqlite> cid | name | type | notnull | dflt_value | pk --------------------------------------------- 0 | utc | TEXT | 0 | CURRENT_TIMESTAMP | 0 1 | ip | TEXT | 0 | | 0 sqlite> 終了しました。 [user@xxxxxxxxxxxxx ~]$- 「テーブル一覧」に accessesがあることから、 テーブルaccessesが定義されたことが分かる
- 
PRAGMA TABLE_INFO(accesses);の結果から、テーブルaccessesのカラム定義を確認できる
 
 
- 「テーブル一覧」に 
- 
sudo systemctl restart httpdを実行し、Apache を再起動する
4. 使って見る
ブラウザや curl などでサーバにアクセスしてから、
sudo php /usr/local/bin/BBASN/viewer.php を実行し、次のようにすると、
データベーステーブル accesses に アクセス時刻と IPアドレスが記録されていることが分かる。
[user@xxxxxxxxxxxxx ~]$ sudo php /usr/local/bin/BBASN/viewer.php
接続しました
テーブル一覧:
  - accesses
SQLを入力してください(例: SELECT * FROM tablename LIMIT 5;)
終了するには空行
sqlite> SELECT * FROM accesses;
sqlite>
utc | ip
--------
2025-10-14 23:36:12 | x.x.x.x
2025-10-14 23:36:13 | x.x.x.x
2025-10-14 23:36:15 | x.x.x.x
2025-10-14 23:36:15 | x.x.x.x
2025-10-14 23:36:16 | x.x.x.x
2025-10-14 23:36:17 | x.x.x.x
2025-10-14 23:36:17 | x.x.x.x
2025-10-14 23:36:18 | x.x.x.x
2025-10-14 23:36:18 | x.x.x.x
sqlite>
終了しました。
[user@xxxxxxxxxxxxx ~]$
(x.x.x.x のところには、実際には IPv4アドレスが入っている)
目次:
前回:
次回:
