Apacheフィルタによる置換(SUBSTITUTE)と圧縮(DEFLATE)と実行順
Webサイト上の全htmlページの書き換えを行うためにApacheのフィルタ設定を行ったがうまく動作しないということでお問い合わせをいただきました。
結論からいうとSUBSTITUTEフィルタとDEFLATEフィルタの実行順がおかしかったせいなのですが、この辺りを解説します。
この記事で学べること
以下の説明をします。
- mod_filterによる今風のフィルタ設定
- フィルタ種別と実行順
Apache2.4系を前提とします。
設定するフィルタ
例としてHTMLページの置換を行うSUBSTITUTEフィルタの設定とコンテンツの圧縮を行うDEFLATEフィルタの設定を行います。
SUBSTITUTEフィルタでは<head>内に<script>タグを挿入し、DEFLATEフィルタではtext/html, text/plainのコンテンツを圧縮する設定を行います。
以前のやりかた
説明のために古い設定方法から説明します。
古い設定方法ではAddOutputFilterByTypeを使います。これは、Apache2.0の頃の手法でAddOutputFilterByTypeはApache2.1で非推奨となっています。
検索すると結構ヒットするので、AddOutputFilterByTypeを使っているケースは今でもよく見かけます。
置換と圧縮を行う例
# content-typeがtext/html, text/plainのコンテンツを圧縮 AddOutputFilterByType DEFLATE text/html text/plain # htmlページの置換 AddOutputFilterByType SUBSTITUTE text/html Substitute "s!(<head(| [^>]*)>)!$1<script>alert('inserted');</script>!i"
ここで気になるのはDEFLATEフィルタ、SUBSTITUTEフィルタの順に登録しているので、動作もこの順番になってしまわないかという点です。DEFLATEで圧縮後のコンテンツに対してSUBSTITUTEフィルタを適用してもパターンに正常にマッチしないため置換できません。
これについては、https://httpd.apache.org/docs/2.4/mod/mod_deflate.html をみると以下の説明があります。
Note
The DEFLATE filter is always inserted after RESOURCE filters like PHP or SSI. It never touches internal subrequests.DEFLATEフィルタはPHPやSSIのようなRESOURCEフィルタの後に挿入される。内部のサブリクエストについては操作しない。
ここでフィルタ種別というものが出てきます。フィルタにはRESOURCE, CONTENT_SET, PROTOCOL, TRANSCODE, CONNECTION, NETWORKの種別があり、種別ごとにこの順番で実行されるようになっています(*1)。優先度のようなものと考えても差し障りありません。
DEFLATEフィルタの種別はCONTENT_SET、SUBSTITUTEフィルタはRESOURCEになっているため、フィルタ登録の順序には関係なく、SUBSTITUTEフィルタが適用されてからDEFLATEフィルタが動作するようになっています。
Apache 2.4におけるフィルタ設定
上記の古い形式のフィルタ設定は https://httpd.apache.org/docs/2.4/mod/mod_filter.html の Figure 1: The traditional filter model に該当します。
新しい形式のフィルタ設定は mod_filter により行います。Figure 2: The mod_filter model がこれに該当します。
mod_filterではFilterDeclareとそれに続くFilterProviderでフィルタを定義し、FilterChainで定義したフィルタをチェーンに挿入することで設定を行います。
設定例は以下のようになります。
mod_filterにより置換と圧縮を行う例
# 圧縮用フィルタ(deflate_filter)を定義 FilterDeclare deflate_filter CONTENT_SET FilterProvider deflate_filter DEFLATE "%{Content_Type} =~ m!^text/(html|plain)!" # 置換用フィルタ(insert_filter)を定義 FilterDeclare insert_filter FilterProvider insert_filter SUBSTITUTE "%{Content_Type} =~ m|^text/html|" Substitute "s!(<head(| [^>]*)>)!$1<script>alert('inserted');</script>!i" # フィルタをチェーンに登録 FilterChain @insert_filter FilterChain deflate_filter
各設定を個別にみていきます。
FilterDeclare deflate_filter CONTENT_SET
FilterProvider deflate_filter DEFLATE "%{Content_Type} =~ m!^text/(html|plain)!"
圧縮(DEFLATE)を行うフィルタをdeflate_filterという名前で作成しています。
ポイントはCONTENT_SETを指定している点です。 AddOutputFilterByTypeでDEFLATEフィルタを設定した時と同じ種別になるように設定しています。
下で定義する置換用のフィルタはRESOURCE種別になるので、チェーンへの登録順に依存せず、必ず置換処理を先に適用できるようになります。
FilterDeclare insert_filter FilterProvider insert_filter SUBSTITUTE "%{Content_Type} =~ m|^text/html|" Substitute "s!(<head(| [^>]*)>)!$1<script>alert('inserted');</script>!i"
<head>内に<script>タグを挿入する置換用フィルタをinsert_filterという名前で作成しています。フィルタ種別は未指定なのでデフォルトのRESOURCEになります。
ちなみに、Content_Typeのマッチングについては正規表現は使わずに以下のように書きたくなるかもしれませんが、php-fpmが返すコンテンツなどでは"text/html; charset=UTF-8"になったりするので正しく判定できません。
FilterProvider insert_filter SUBSTITUTE "%{Content_Type} = 'text/html'"
最後にフィルタをチェーンに登録します。
FilterChain @insert_filter FilterChain deflate_filter
フィルタ名の前に@があるとチェーンの先頭に登録されます。この例では@はなくても構いませんが、他にもフィルタ設定がたくさんある場合、個人的には置換処理は先に済ませておきたいので@をつけることが多いです。
deflate_filterはCONTENT_SETになっているので、insert_filterとの登録順によらずinsert_filter、deflate_filterの順で適用されます。このため、以下のような登録順でも正常に動作します。
FilterChain deflate_filter FilterChain insert_filter
deflate_filterをCONTENT_SETにしなかった場合は、両方共RESOUCE種別となりdeflate_filterが先に適用されるため置換が行われません。この場合、FilterChainの指定順を適切に管理すればいいのですが、実際の運用ではフィルタの設定が一箇所にまとめられているとは限りません。FilterDeclareで種別を適切に設定しておく方が後々トラブルは少ないと思います。
まとめ
mod_filterによるフィルタ設定の例を示しました。また、フィルタ種別によりフィルタの実行順を制御できることを説明しました。
Web上にはFilterDeclareでDEFLATEフィルタの設定を行っている例はたくさんありますが、CONTENT_SETを指定していない例も多いので、複数のフィルタを設定した時に思ったように動作しなくなることがあります。今回のお問い合わせもこれに該当しました。
DEFLATEフィルタをFilterProviderで定義する場合は、CONTENT_SETを指定しておきましょう。
(*1) 各々のフィルタの説明はApacheのソースコードのinclude/util_filter.hで確認できます。
投稿日:2021/11/23 00:12