ApacheのRewriteでパラメータのURLへリダイレクトしたい

# 概要
Apache HTTP Server（以下Apache）だけで、あるURLパスにアクセスされた際に、付与されたパラメータで指定されたURLへリダイレクトする必要があり、試行錯誤したときの話です。

# 環境 
* CentOS7
* Apache Httpd Server 2.4.6

# 要件
  * URLパス`/redirect`へアクセスされたときに、パラメータ`redirect`で指定されたURLへリダイレクトする  
  
`http://example.com/redirect?redirect=http://other.example.com`
　↓　リダイレクト
`http://other.example.com`

# 試行錯誤
バックエンドへプロキシせずにApacheの処理でリダイレクトを行う必要があったため、Rewrite設定で対応することにしました。
*※ 設定はRewriteの部分のみに、パラメータの値取得もredirectのみの想定と簡略化しています*
*※ 正規表現は厳密性よりも可読性を重視しています*

## 試行
単純にパラメータを抽出して、条件に合っている場合にリダイレクトすればOK！と考えて以下のような設定にしました。

``` conf:ダメだった設定1
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値を設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:%1]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
```

無事に`http://other.example.com`へリダイレクトしました。よし！終了。

## 錯誤

パラメータってURLエンコードされるよね。以下のような場合は大丈夫？
`http://example.com/redirect?redirect=http%3A%2F%2Fother%2Eexample%2Ecom`

試してみる。むむっ
`http://example.com/http%253A%252F%252Fother%252Eexample%252Ecom`
にリダイレクトされてしまいました。
そのままRewriteしたところ、パスとして扱われる結果に。。。  
二重にエンコードされているので、Rewriteフラグ`NE`をつけてみることにします。

``` conf:ダメだった設定2
# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値を設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:%1]
RewriteCond %{ENV:RED_PARAM  ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD,NE]
```

`http://example.com/http%3A%2F%2Fother%2Eexample%2Ecom`
二重エンコードは解消したが、やはりだめでした。  

## mod_rewriteの動作
[mod_rewriteの説明](http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewriterule)では、RewriteRuleの置換文字列には、システムファイルパス・URLパス・絶対URL・ダッシュ(-)を指定することができると記載されています。
mod_rewriteのソースを確認したところ、絶対URLと見なされるためには、置換文字列中に`://`が含まれていることが条件で、上記の要件の場合、パラメータがURLエンコードされているときはデコードする必要がありました。

そこで引数の文字列をURLデコードして標準出力するシェルスクリプト(`decode.sh`)を作成、何とか呼び出せないかと調査を開始しました。

## RewriteMapディレクティブを発見
ReweriteMapで外部プログラムを呼び出すことができるとの情報を見つけたので、さっそくデコードするシェルスクリプトを呼び出してみます。
プログラムは、標準入力を待ち受けて、標準出力へ改行で終わる文字列を出力するものでなければいけないため、引数から標準入力を待ち受けるようシェルスクリプトを修正しました。
RewriteMapでマップ名`decode`に、プログラム呼び出し`prg`を書いてコロン区切りでプログラムファイル`conf/decode.sh`を指定する以下の設定にしました。  

``` conf:うまくいったが外部プログラムが必要な設定
# decodeというマップ名に外部プログラムconf/decode.shを設定
RewriteMap decode prg:conf/decode.sh

# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値をデコードして設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:${decode:%1}]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
```

ちゃんと`http://other.example.com`へリダイレクトすることができました！
  
しかし、よくよくRewriteMapについて調べると外部プログラム以外にもマッピング種別があり、escape(エンコード)・unescape(デコード)・uppercase・lowercaseを内部ファンクションとして呼び出せることが判明！
試しにRewriteMapの設定を変更して、内部ファンクションのunescapeを指定してみます。

``` conf:一番スマートと思われる設定
# decodeというマップ名に内部ファンクションunescapeを設定
RewriteMap decode int:unescape

# /redirectへのアクセスでredirectパラメータが存在する場合、RED_PARAM環境変数に値をデコードして設定。
# /redirectへのアクセスで、RED_PARAMに値が設定されている場合にRED_PARAMの値へリダイレクトする。
RewriteCond %{QUERY_STRING} ^redirect=(.*)$
RewriteRule ^/redirect$ - [E=RED_PARAM:${decode:%1}]
RewriteCond %{ENV:RED_PARAM} ^.+$
RewriteRule ^/redirect$ %{ENV:RED_PARAM} [L,QSD]
```

外部プログラムをRewriteMapで呼び出したときと同様、期待通りに動作しました！

今回お世話になったRewriteMapについて調べたことを紹介します。

# RewriteMapディレクティブの書き方  
`RewriteMap  MapName  MapType:MapSource`

* `MapName` マップを呼び出すときの名称
呼び出すときは、`${MapName:LookupKey\|DefaultValue}`で指定することが可能です。
LookupKeyはマップのキー。
DefaultValueは省略可能。

* `MapType`と`MapSource`

| MapType | MapSource | 説明 |
| --- | --- | --- | 
| txt | プレーンテキスト形式のマッピングファイルのパス | KeyとValueをスペース区切りで1行に記述する形式。 |
| rnd | プレーンテキスト形式のマッピングファイルのパス | KeyとValueをスペース区切りで1行に記述する形式。Valueは複数の値をパイプで区切った形式。ランダムでいずれかの値が選択される。 |
| dbm(=DBType) | Key-Value形式のDBMファイルのパス | DBTypeはsdbm, gdbm, ndbm, dbで作成したファイルが使用可能だが、Apache HTTP Serverのユーティリティhttxt2dbmでテキストファイルから作成したdbmファイルが推奨されている。 |
| int | toupper,tolower,escape,unescape | MapSourceのいずれかの内部ファンクションを指定する。  toupper:大文字にする  tolower:小文字にする  escape:URLエンコードする  unescape:URLデコードする |
| prg | 実行するプログラムファイルのパス | プログラムはSTDINから一つの引数（Key）をとり、STDOUTで一つの改行で終了する文字列（Value）を出力する必要がある。 |
| dbd,fastdbd | クエリ | 一つの引数（Key）を与え、一つの値（Value）を返すクエリを記述する。mod_dbdでDBアクセスが行えるよう設定してあることが前提。fastdbdは、クエリキャッシュを作成するため高速。 |

# RewriteMapディレクティブの使いどころ
RewriteMapを使用することで、長くなりがちなRewrite設定を簡略化することが可能です。

``` conf:rewrite.conf
RewriteRule /foo http://foo.example.com  [L]
RewriteRule /bar http://bar.example.com [L]
RewriteRule /hoge /example/hoge [L]
...
```

以下のように置換定義を別のファイルでシンプルに定義することが可能です。
RewriteRuleが多数定義されている場合、置換定義を別のファイルで定義してApacheの設定ファイルを簡略化できるか検討したほうがよさそうです。

``` conf:rewrite.conf
RewriteMap mapname txt:conf/rewritemap.txt
RewriteRule /(.+) ${mapname:$1} [L]
```

``` txt:conf/rewritemap.txt
foo  http://foo.example.com
bar  http://bar.example.com
hoge /example/hoge

```

また、置換定義が非常に多い場合は、txtの代わりにdbmファイルを使用することを検討しましょう。
これは、LookupKeyにインデックスが作成されるためです。
Apacheに付属されている`httxt2dbm`コマンドを使用してdbmファイルを作成することができます。
`httxt2dbm -i rewritemap.txt -o rewritemap.dbm`

コマンドを実行すると、`rewtitemap.dbm.dir`と`rewritemap.dbm.pag`ファイルが作成されます。
dbmファイルを利用するときのRewriteMapの設定は以下のようになります。

``` conf:rewrite.conf
RewriteMap mapname dbm:conf/rewritemap.dbm
RewriteRule /(.+) ${mapname:$1} [L]
```

他にも`MapType`がありますが、今回はここまでとします。

# 注意
* パラメータURLへのリダイレクトでは、**予期せぬリダイレクト先にリダイレクトされることが考えられるため、パラメータのバリデーションは行いましょう**。バリデーションを行わない場合、脆弱性となる恐れがあります。記事中の設定では省略されていますが、正規表現を用いたRewriteCondなどでリダイレクト先URLのバリデーションを行い、NGの場合はエラー画面にリダイレクトするなど考慮しましょう。  
https://qiita.com/tamura__246/items/f41d2f69f1c8494493e0  
* RewriteMapの外部プログラムマッピングでは、プログラムが終了するまで待機します。応答時間の遅延の原因となる可能性がありますので注意しましょう。
* URL中に改行コードが含まれている場合は、削除する・エラーとするなど対策を行いましょう。  
https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_7.html
* その他、ディレクトリトラバーサル対策など脆弱性対策を確認・実施しましょう。

# 最後に
初めからApache HTTP Serverの調査をしっかり行うべきでした。。。
Apache HTTP Serverのモジュールはたくさんあるため、もっと良い方法があるかもしれません。
もっと良い方法があるよ、という方は教えていただけるとうれしいです。

# 参考URL
* https://httpd.apache.org/docs/trunk/rewrite/rewritemap.html
* https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html