mod_proxy_htmlを使ってみた

[前回](https://qiita.com/aimoto/items/8b40a7e0f018a79598b1)はmod_proxy_htmlについて説明しました。今回はmod_proxy_htmlの具体的な設定について載せていきます。
mod_prxoy_htmlはリバースプロキシサーバーに組み込むモジュールのため、前回の説明に使った構成図の環境を作成する流れで説明します。

# システム構成
システム構成はmod_proxy_htmlがインストールされたリバースプロキシ、アプリケーション1、アプリケーション2の3つの環境です。
リバースプロキシの環境は CentOS7 を使用します。
アプリケーション1、アプリケーション2は何でも良いのでWebサーバーを動かしておきます。(本記事では割愛します。)

![qiita-mod_proxy_html1.png](https://qiita-image-store.s3.amazonaws.com/0/316450/8b127aaf-ad69-e5c5-1cca-c2bd9a914f5e.png)


# mod_proxy_htmlのインストール
mod_proxy_html を実際に試してみましょう。mod_proxy_html は CentOS7 の標準パッケージに含まれているのでyumコマンドでインストールを行うことができます。

*  mod_proxy_htmlインストール

```console
# yum install mod_proxy_html
```

yum でインストールすると`/etc/httpd/conf.modules.d/00-proxyhtml.conf`が配置されますので、Apacheの再起動によってモジュールがロードされる構成となります。

それではhttpd.conf に mod_proxy_html の設定を行います。

# httpd.confの設定
リバースプロキシサーバーのhttpd.confに設定を記載します。（本記事ではリバースプロキシに関連した設定のみ扱います。）

## URL変換対象のHTMLのタグ
URLの変換を行うHTMLタグの設定です。

```config
ProxyHTMLLinks  a               href
ProxyHTMLLinks  area            href
ProxyHTMLLinks  link            href
ProxyHTMLLinks  img             src longdesc usemap
ProxyHTMLLinks  object          classid codebase data usemap
ProxyHTMLLinks  q               cite
ProxyHTMLLinks  blockquote      cite
ProxyHTMLLinks  ins             cite
ProxyHTMLLinks  del             cite
ProxyHTMLLinks  form            action
ProxyHTMLLinks  input           src usemap
ProxyHTMLLinks  head            profile
ProxyHTMLLinks  base            href
ProxyHTMLLinks  script          src for
```

`ProxyHTMLLinks`がURL変換を行うHTMLタグの指定を行うディレクティブです。
第一引数には要素名、第二引数以降には要素に対して適用する属性名を定義します。属性名は複数設定可能です。
上記はサンプルとして設定例を載せていますが、実際の環境ではアプリケーションのURLのリンクとして使用しているHTMLタグに漏れがないか確認して、必要があれば追加します。



## 共通設定
mod_proxy_htmlを使用するにあたっての共通の設定です。

```
<Location />
  RequestHeader unset Accept-Encoding
  <IfModule mod_filter.c>
    FilterProvider iconv    xml2enc "%{CONTENT_TYPE} =~ m|^text/html|"
    FilterProvider proxyhtml   proxy-html "%{CONTENT_TYPE} =~ m|^text/html|"
    FilterProvider SUBSTITUTE SUBSTITUTE "%{CONTENT_TYPE} =~ m|^text/html|"
    FilterProvider SUBSTITUTE SUBSTITUTE "%{CONTENT_TYPE} =~ m|^text/css|"
    FilterProvider SUBSTITUTE SUBSTITUTE "%{CONTENT_TYPE} =~ m|^text/javascript|"
    FilterProvider SUBSTITUTE SUBSTITUTE "%{CONTENT_TYPE} =~ m|^application/javascript|"
    FilterChain SUBSTITUTE iconv proxyhtml
  </IfModule>
</Location>
```

`<Location />`で囲んだ中に共通設定を行っています。
`RequestHeader unset Accept-Encoding` [^4]はアプリケーションの応答コンテンツで「コンテンツ圧縮」が行われないようにするためにリクエストHTTPヘッダーから削除しています。アプリケーションからの応答コンテンツがコンテンツ圧縮されてしまうとバイナリデータであるためにmod_proxy_htmlやmod_substituteによるURLの変換が行えないためです。

[^4]: 2021/4/20訂正。Accept-Languageと記載していましたが正しくはAccept−Encodingです。

`FilterProvider`と`FilterChain`で、Apacheのinputフィルタに関する設定を行っています。対象のフィルターは`mod_proxy_html` `mod_substitute` `mod_xml2enc`です。

`mod_proxy_html` `mod_xml2enc`は、HTMLコンテンツのみに対してURLの変換を行うため、Content-Type が text/html のみ動作する設定を行います。
`mod_substitute` はmod_proxy_htmlでURLの変換が行えない場合に補完する役割で使用し、JavaScript や CSS 等のコンテンツに対しても URL の変換を行います。そのため、これらのContent-Type を設定しています。環境によっては他にもxmlファイル等必要な Content-Typeを設定する必要があります。

`FilterChain` では、[前回の記事](https://qiita.com/aimoto/items/8b40a7e0f018a79598b1#%E6%96%87%E5%AD%97%E3%81%AE%E5%88%87%E3%82%8C%E7%9B%AE%E5%95%8F%E9%A1%8C)で触れたように `mod_substitute` `mod_xml2enc`　`mod_proxy_html` の順番でフィルタが動作するようにしています。


## Proxy先アプリケーション単位の設定
最後にアプリケーションサーバー単位の設定です。

```
ProxyPass /ap1/ https://ap1.example.co.jp/
<Location /ap1/>
  ProxyPassReverse https://ap1.example.co.jp/
  ProxyPassReverseCookiePath / /ap1/
  ProxyHTMLCharsetOut *
  ProxyHTMLURLMap ^/([^/])|^/$ /ap1/$1 R
  Substitute 's|//ap1.example.co.jp/|//rp.example.co.jp/ap1/|n'
</Location>

ProxyPass /ap2/ http://ap2.example.co.jp/
<Location /ap2/>
  ProxyPassReverse https://ap2.example.co.jp/
  ProxyPassReverseCookiePath / /ap2/
  ProxyHTMLCharsetOut *
  ProxyHTMLURLMap ^/([^/])|^/$ /ap2/$1 R
  Substitute 's|//ap2.example.co.jp/|//rp.example.co.jp/ap2/|n'
</Location>
```

ProxyPass系 の設定はmod_proxy_htmlの設定ではなく、アプリケーションサーバーへproxyするリバースプロキシとしての設定です。下記３つの設定を行っています。

 - `ProxyPass` で接続先のサーバーの設定
 - `ProxyPassReverse` では、アプリケーションが応答するLocationヘッダーの書き換え設定
 - `ProxyPassReverseCookiePath` では、アプリケーションが発行するSet-CookieヘッダーのPath属性の書き換え設定

`ProxyHTMLCharsetOut` は、URLの変換後の文字エンコーディングの指定の設定です。`*`と設定することで、mod_proxy_htmlで URL の変換を行った後で元のコンテンツと同じ文字エンコーディングに戻ります。この設定がなければユーザーに返るコンテンツの文字エンコーディングはUTF-8となります。

`ProxyHTMLURLMap` で、URLの変換ルールを設定をしています。
`//【FQDN】/` のようにスキームが省略されたリンクに対してURLの変換が行われないように正規表現の変換ルールを書いています。
`/ /ap1/` と書く記載例が見受けられますが、この設定方法だと `//【FQDN】/` のリンクに対しても URL 変換されてしまいます。ただし、正規表現の記述は一見するとわかりづらい設定であり、コンテンツ内に`//【FQDN】/`と書くリンクがなければ `/ /ap1/` と設定しても良いと思います。

`Substitute` は、mod_proxy_htmlではなくmod_substituteの設定です。mod_proxy_htmlで変換できない箇所のURLの変換を行うために設定します。


以上で設定が完了しました。Apacheを再起動して設定値を反映させます。

# 動作確認
アプリケーション1に link.html を配置して、`https://rp.example.co.jp/ap1/link.html`へアクセスしてリンクが正しく辿れるか確認してみましょう。 link.html の文字エンコーディングは UTF-8 で作成してください。

``` link.html
<html><head>
<title>AP1リンク</title>
</head><body>
<a href="/top.html">トップへ(/から始まる絶対パス)</a><br>
<a href="./top.html">トップへ(相対パス)</a><br>
<a href="//ap1.example.co.jp/top.html">トップへ(//から始まる絶対パス)</a><br>
<a href="/">/だけのリンク</a><br>
<a href="https://ap1.example.co.jp/top.html">httpから始まる絶対パスのリンク</a>
</body></html>
```

表示されたlink.htmlのリンクをクリックすると`https://rp.example.co.jp/ap1/〜`となればmod_proxy_htmlの設定がうまく出来ています。


## 文字化け?
`https://rp.example.co.jp/ap1/link.html`へのアクセスで文字化けした方はいらっしゃいませんか？
もしアプリケーション1のWebサーバー等で Content-Type に 文字エンコーディングがセットされていなければ[^1]文字化けが発生します。

 [^1]: WebサーバーがApacheであれば`AddDefaultCharset`を`off`と設定


![qiita_mod_proxy_html_1204_1.png](https://qiita-image-store.s3.amazonaws.com/0/316450/4883989c-01bf-b16b-0400-c135b27586d6.png)

これはmod_xml2encの処理でコンテンツの文字エンコーディングが何か判別できなかったため、デフォルトの`ISO-8859-1`で文字エンコーディング変換が行われたためです。

``` mod_xml2enc.c
    if (!HAVE_ENCODING(ctx->xml2enc)) {
        cfg = ap_get_module_config(r->per_dir_config, &xml2enc_module);
        if (!ctx->encoding) {
            ctx->encoding = cfg->default_charset?cfg->default_charset:"ISO-8859-1";
        }

```

文字エンコーディングが何か判別できなった場合のデフォルト値はディレクティブが用意されているため、設定により `ISO-8859-1` から変更することが可能です。
文字化けが発生した環境でhttpd.confに以下の設定を追加すると文字化けしなくなります。

```
xml2EncDefault UTF-8
```

一般的なWebアプリケーションであれば「文字エンコーディングの指定が無い」なんて事はないと思いますが、このように設定で対処することができます。
もちろん設定を行わずとも、WebサーバーでレスポンスHTTPヘッダーに文字エンコーディングをセットする[^2]、HTMLのMETAタグで文字エンコーディングをセットする[^3]でも文字化けを回避することができます。

 [^2]: WebサーバーがApacheであれば`AddDefaultCharset`を`UTF-8`と設定



## ProxyHTMLEnable は使わない
本記事の設定では mod_proxy_htmlを有効にするための`ProxyHTMLEnable on`の設定を行っていません。mod_filterによってmod_proxy_htmlを動作させています。これは意図したもので理由は２つあります。

 1. inputフィルターで動作するフィルタの順番をコントロールしたい
 2. 不要な Content-Type で mod_xml2enc が動作するのを防ぎたい

「1」についてはすでに説明済みですので「２」の理由を説明します。
`ProxyHTMLEnable on`とすると自動的に mod_xml2enc も動作するよう Apache の inputフィルタに登録されます。そしてmod_xml2encのソースコードを見ると、mod_xml2encは Content-Type が text/ で始まるか、xmlという文字列が含まれると動作します。

``` mod_xml2enc.c
    /* only act if starts-with "text/" or contains "xml" */
    if (strncmp(ctype, "text/", 5) && !strstr(ctype, "xml"))  {
        ap_remove_output_filter(f);
        return ap_pass_brigade(f->next, bb) ;
    }
```

URLの変換はHTMLのコンテンツに対してのみ行うため、mod_xml2encが動作するのはtext/htmlだけで問題ありません。しかし、`ProxyHTMLEnable on`とすると本来必要の無いコンテンツに対しても文字エンコーディングの変換処理が実行されてしまうためこれを防ぐために`ProxyHTMLEnable on`の設定を行わないようにしています。

# 終わりに
リバースプロキシの環境でmod_proxy_htmlを使用する設定例を説明しました。
もし実際の環境でmod_proxy_htmlを使う場合は、`必要なHTMLタグの設定`、`mod_substituteの動作対象Content-Type` や`置換文字列の設定`など調整が必要です。リバースプロキシとアプリケーションを構築したあとで接続試験を行って URL の変換に漏れがないかの確認を行ってください。構築後に調整が必要となるのでテストの期間を十分に確保するようスケジュールを立てることが望ましいです。

最後にmod_proxy_htmlはどうしても使わなければならない場合のみ使いましょう。この記事を読んでいただければわかりますが、mod_proxy_htmlを使いこなすのは大変です。
もしこれからシステムを設計する状態であるのならば、mod_proxy_htmlを使わない選択肢を取れないか考慮することをオススメします。



 [^3]: link.htmlに`<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">`の記述を追加