mod_proxy_htmlを説明してみた

# はじめに
Apache のモジュール mod_proxy_html について、日本語の記事が少ないので書いてみることにしました。
2回に渡ってmod_proxy_htmlについて書いていきます。1回目ではmod_proxy_htmlとはなんぞや・どういう仕組みで動いているのかを、2回目では実際の設定方法について載せていきます。


# mod_proxy_htmlとは
mod_proxy_html はApache2.4より新しくApache標準のモジュールとして組み込まれました。リバースプロキシとしてApacheを構築する際にproxy先サーバーからの応答コンテンツのURLを変換するために使用します。
コンテンツのURLの変換は下図のようにリバースプロキシからproxy先のサーバーへの振り分けに`仮想のURLパス`を使った構成を取る場合に必要となります。
![qiita-mod_proxy_html1.png](https://qiita-image-store.s3.amazonaws.com/0/316450/8b127aaf-ad69-e5c5-1cca-c2bd9a914f5e.png)

この図の構成はリバースプロキシへのアクセスURLパスが/ap1/ で始まっていればアプリケーション1へproxyし、/ap2/で始まっていればアプリケーション2へproxyする構成です。/ap1/、/ap2/にはアプリケーションの振り分け先の判定に使用し、リバースプロキシからアプリケーションへのアクセスの際にはこのURLパスは含まれません。
この構成ではユーザーは常に仮想のURLパス(/ap1/ または /ap2/) を付けてアクセスする必要があります。仮想URLパスが無いとリバースプロキシでどのアプリケーションサーバーに振り分けるか判断出来ないためです。

ここでアプリケーション1が返すHTMLコンテンツに自身へのリンクURLを"/から始まる絶対パス"で書いていた場合どうなるか考えてみましょう。

``` link.html
<html><body>
<a href="/top.html">トップへ</a>
</body></html>
```

リバースプロキシ経由でlink.htmlへアクセスし、「トップへ」のリンクをクリックすると `https://rp.example.co.jp/top.html` にアクセスすることになります。このアクセスはリバースプロキシでは仮想URLパスが付いていないためアプリケーション1へproxyすることが出来ず、ユーザーは画面遷移を正しく行えないことになります。

この事象の対処方法としては リンクのURLを相対パスで書いておけば解決します。しかし、すでにアプリケーションサーバーを構築済みでリンクURLを全て変更するには工数がかかってしまったり、パッケージ製品を使っているで必ずしもコンテンツ内のリンクを書き換えらない等で変更できない場合が考えられます。ここで出番となるのがmod_proxy_htmlです。

mod_proxy_html を使うことでアプリケーションが応答したHTMLコンテンツのURLをリバースプロキシ上で書き換えて、次からのアクセスもリバースプロキシ経由としアプリケーションの画面遷移を正しく行われるように構成できます。

![qiita-mod_proxy_html2.png](https://qiita-image-store.s3.amazonaws.com/0/316450/5d81fed1-1d74-2797-3958-3120e4362491.png)

いくつかの商用のシングルサインオン製品にはこのURLを変換する機能が備わっています。商用製品でリバースプロキシ構成を実現していて、オープンソースで同等の構成を実現しようとする場合はこのmod_proxy_html が役に立ちます。



# 動作の仕組み
mod_proxy_html のURLの変換は設定した`HTML のタグ（属性名、要素名）`に対して、`変換ルール`にて置換を行います。例えば上記構成の場合は

- 「a href」のHTMLタグ に対して
- 「/」 → /ap1/ 」に置換する

という設定を行います。

URLの変換はHTMLコンテンツに対して行います。Apacheの設定でmod_proxy_htmlを有効[^1]にするとコンテンツの Content-Type が`text/html`か`application/xhtml+xml`である場合にURLの変換が行われます。
mod_proxy_htmlはApacheのフィルタ[^2]であるため、mod_proxy_htmlを有効の設定を行わず、mod_filter を使って動作をコントロールすることが可能です。


[^1]: ProxyHTMLEnable を onと 設定
[^2]: 参考: http://httpd.apache.org/docs/2.4/en/filter.html

## HTMLコンテンツ以外の変換
コンテンツ内にURLが含まれるのはHTMLコンテンツだけではありません。CSS や JavaScript にも含まれます。mod_proxy_html はHTMLタグを検知してURLを変換するモジュールであるため CSS や JavaScript のURLの変換は行えません。(厳密にはExtend機能があり出来る場合もあるのですが誤検知があるのでオススメしません)
CSS や JavaScriptのリンクURLの変更は別のモジュール(mod_subsuite)を使用して行うのが良いです。mod_subsuite についてはくわしく解説しませんが、コンテンツ内の文字列の置換を行うことが出来るApacheモジュールです。

## mod_xml2enc
mod_proxy_htmlについて説明しましたが、mod_xml2encについて解説します。mod_proxy_htmlは、`mod_proxy_html`　`mod_xml2enc`と2つのモジュールがセットで動くことを想定しています。 
実際にCentOS7 のmod_proxy_htmlをインストールすると、モジュール設定ファイルでは次のように２つのモジュールが読み込まれるようになっています。

```/etc/httpd/conf.modules.d/00-proxyhtml.conf
# This file configures mod_proxy_html and mod_xml2enc:
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
```

mod_xml2enc は文字エンコーディングの変換を行うモジュールです。なぜ文字エンコーディングの変換を行うか説明します。


### 文字エンコーディングの変換
mod_proxy_htmlはHTMLコンテンツ内のタグを解釈するためlibxml2を使用します。libxml2ではドキュメントをパースするためコンテンツの文字エンコーディングがUTF-8である必要があります。アプリケーションが応答するコンテンツの文字エンコーディングをUTF-8に変換してコンテンツ内のタグを検知し、URL変換を行います。
この文字エンコーディングの変換を mod_xml2enc が行っています。

mod_proxy_htmlを有効にすると、mod_xml2enc も動作するようになっています。（mod_xml2encもApacheのフィルタであるため、mod_filterで動作させることが可能です。）


![qiita-mod_proxy_html3.png](https://qiita-image-store.s3.amazonaws.com/0/316450/e7fdfc77-2286-69cf-0652-dfe7ce3dd9f0.png)

mod_proxy_html を使うとコンテンツが一部までしか表示されない/文字が化ける事象が発生することがありますが、大体の原因は文字エンコーディングの変換に失敗しています。


# mod_proxy_htmlを使うと苦労すること
ここまでmod_proxy_htmlについて説明しました。ここからmod_proxy_htmlを使うと苦労することについて書いていきます。

## URL変換できない
mod_proxy_html はURLの変換を行うHTMLのタグを設定します。このHTMLタグの設定が不足しているとURLの変換は行われません。そしてこのHTMLのタグは実際に動かしてみないと不足が無いかどうかわかりません。設計段階では一般的に使われるHTMLタグを設定しておき、テストして漏れがないかを探します。
またHTMLタグ以外の場所にURLが記載されている場合は、mod_proxy_htmlでは変換出来ないのでmod_substitute で行う必要があります。
実際に環境を作ってアクセスして設定値を決定する、リンクを正しく辿れるかというのを確認して設定値を調整する必要があります。

## 文字化けが発生する
文字エンコーディングを変換することによる問題が発生することがあります。

### 文字エンコーディングが判別できない
文字エンコーディングを UTF-8 に変換するため、アプリケーションが応答するレスポンスヘッダーかコンテンツ内のmetaタグに文字エンコーディングがセットされている必要があります。これらの文字エンコーディングがセットされてなければ、mod_xml2enc で現在の文字エンコーディングを取得できず、文字化けが発生することがあります。
対応策としてはアプリケーションで応答するレスポンスHTTPヘッダーに正しい文字エンコーディングをセットするようにします。


### Shift_JIS/CP932問題

文字エンコーディングの Shift_JIS/CP932 問題です。mod_xml2enc では上で述べたようにコンテンツのレスポンスヘッダーやHTMLのMetaタグからコンテンツの現在の文字エンコーディングを判定します。
例えばレスポンスのHTTPヘッダーのCharset=Shift_JIS と入っていれば、Shift_JIS を UTF-8 の変換を行います。
しかし、ほとんどのケースで日本語環境のShift_JIS は、実際はCP932を使っています。これでは正しく文字エンコーディングの変換が行なえません。
文字エンコーディングの変換に失敗すると、その時点までのデータをブラウザに応答します。ユーザーには中途半端なコンテンツが返ることになります。Apacheのエラーログには以下のような謎めいたログが出力されます。

```error_log
encoding error : input conversion failed due to input error, bytes 0x87 0x40 0x87 0x4A
encoding error : input conversion failed due to input error, bytes 0x87 0x40 0x87 0x4A
I/O error : encoder error
encoding error : input conversion failed due to input error, bytes 0x87 0x40 0x87 0x4A
```

これは残念ながら設定で回避できる手段は無いと思います。Shift_JISではなくCP932として変換すれば良いだけなのですが設定ではどうにもなりませんでした。[^3]

[^3]: 弊社ではmod_xml2enc を改修して対応しました

### 文字の切れ目問題
アプリケーションのコンテンツのサイズが大きいと、mod_xml2encでは一定のバイト単位で文字エンコーディングの変換を行います。この文字エンコーディングの変換が運悪く文字の途中であった場合、正しく文字エンコーディングの変換を行うことができず文字化けが発生します。
"一定のバイト単位"はアプリケーション先から応答されるデータ転送量によるため、コントロールできません。

幸い、こちらは mod_substitute と組み合わせることで文字化けを防ぐことが可能です。具体的には `mod_substitute` → `mod_xml2enc` → `mod_proxy_html` の順序で動作するように設定します。
mod_substitute は改行コードを一つの区切りとして内部で処理するため、mod_xml2enc には改行コードで区切られたデータが渡ってきて処理されます。このため文字の切れ目で文字エンコーディングの変換が動作することがありません。

[次回](https://qiita.com/aimoto/items/edbb9c51c753c29758d4)ではこれらのモジュールの動作順序の設定を含めた具体的なmod_proxy_htmlの設定方法を解説します。