Apacheのmod_rewriteとBフラグ

PHP のプログラムを Docker コンテナで動かそうとして、Apache の mod_rewrite の設定でちょっと苦労したのでメモ。

主題は PHP じゃないので、プログラムは簡単な例にしておく。クエリーパラメータ name を受け取って、挨拶を返すだけのプログラムだ。

<?php
$name = $_GET["name"];
echo "Hello, " . $name . "!" . PHP_EOL;
?>

Docker イメージは php:8.2-apache を使う。

takatoh@apostrophe:myphpapp$ docker run -d --rm -p 80:80 -v .:/var/www/html php:8.2-apache

これで localhost:80 で待ち受けているはず。curl コマンドで試してみる。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy
Hello, Andy!

期待通りだ。

つぎに、PHP プログラムに渡すパラメータ name を、クエリーパラメータではなく、パスの一部にしたい。つまり、http://localhost/hello/Andy という URL でアクセスしたい。

そこで、つぎのような .htaccess ファイルを書いた。

RewriteEngine on
RewriteRule ^hello/(.*)\$ hello.php?name=\$1

それから、Apache で上の .htaccess が有効になるように Dockerfile を書いた。デフォルトでは mod_rewrite 自体が有効になってないんだそうだ。

FROM php:8.2-apache
RUN a2enmod rewrite
RUN sed -ri -e 's!AllowOverride None!AllowOverride FileInfo!g' /etc/apache2/apache2.conf

mod_rewrite を有効にして、なおかつ、.htaccess ファイルでルールを設定するのを許可している。これを元に Docker イメージをビルド。

takatoh@apostrophe:myphpapp$ docker build -t myphpapp:1 .

コンテナを起動する。

takatoh@apostrophe:myphpapp$ docker run -d --rm -p 80:80 -v .:/var/www/html myphpapp:1

さっきと同じように curl で試してみよう。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy
Hello, Andy!

うまくいった!もちろんクエリーパラーメータで渡すのでも期待通りに動く。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy
Hello, Andy!

ところが、パス形式のパラーメータ部分(Andy の部分)に空白文字を使うとうまく動いてくれない(curl で URLエンコードする方法がわからなかったので、空白文字の代わりにエンコードした %20 を使っている)。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy%20Weir
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>403 Forbidden</title>
</head><body>
<h1>Forbidden</h1>
<p>You don't have permission to access this resource.</p>
<hr>
<address>Apache/2.4.57 (Debian) Server at localhost Port 80</address>
</body></html>

クエリーパラメータ形式では大丈夫。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello.php?name=Andy%20Weir
Hello, Andy Weir!

ブラウザでも試したけど同じ結果。さて、困った。

結論を先に書くと、RewiterRule の最後に B フラグをつければ解決する。Apache の公式ドキュメントにはちゃんと書いてある。が、参考にした日本語の解説ページの中には書いてあるページはなかった。探し方が悪いのかもしれないけど。

ともかく、.htaccess ファイルをつぎのように書き換えた。

RewriteEngine on
RewriteRule ^hello/(.*)\$ hello.php?name=\$1 [B]

再度試してみる。

takatoh@apostrophe:myphpapp$ curl http://localhost/hello/Andy%20Weir
Hello, Andy Weir!

OK!!!

一件落着。

後々のために、Apache のドキュメントから解説を引用しておこう(DeepL 翻訳)。

[B] フラグは、変換を適用する前に英数字以外の文字をエスケープするよう RewriteRule に指示します。

mod_rewrite は URL をマッピングする前にエスケープを解除しなければならないので、 後方参照は適用される時点でエスケープされません。B フラグを使うと、後方参照中の英数字以外の文字がエスケープされます

‘x & y/z’という検索語が与えられた場合、ブラウザはそれを’x%20%26%20y%2Fz’としてエンコードし、リクエストを’search/x%20%26%20y%2Fz’とします。Bフラグがない場合、このリライトルールは’search.php?term=x & y/z’にマップされますが、これは有効なURLではないため、search.php?term=x%20&y%2Fz=としてエンコードされ、意図されたものではありません。

この同じルールにBフラグを設定すると、パラメータは出力URLに渡される前に再エンコードされ、結果として/search.php?term=x%20%26%20%y%2Fzに正しくマッピングされます