mod_rewriteでGeoIPとブラウザの言語設定から特定の国・言語のページにリダイレクトさせてみた

マルチカントリー、マルチ言語なサイトの場合、国/言語でURLを分けて別サイトとして管理されてる企業さんは多いです。
例えば、ソシャゲで有名なGREEさんのコーポレートサイトもそのようになっているようでURLは以下の形式になっています。

```
http://corp.gree.net/jp/en/
```

サイト管理に使っているCMSにもよりますが、こうすることでサイト全体は本社で管理して各国のサイトはその国の現地法人に管理させる、みたいな管理がやりやすくなります。


今回の案件もそんなお客さんだったのですが、/jp/en/ 部分がなく / でアクセスしたらIPとブラウザ言語から適切にURLを補完するようにしてほしいという要件に二つ返事ではいと答えたものの、思ったより悩んだのでqiitaにメモっておくことにします。


## 設計
設計は簡単に以下のようにした。
あと、運用するなかで現地法人や対応言語は増減する可能性があるためメンテナンス性も考慮する。
`http://hoge.com/` へのアクセスの場合
- 国名はCloudFrontのCloudFront-Viewer-Countryヘッダーをもとに補完
- 言語はAccept-Languageヘッダーをもとに補完
- 補完した結果、サイトにない組み合わせだった場合、本社サイト(/us/en/)にリダイレクト

`http://hoge.com/国/` へのアクセスの場合
- 国名はrequest urlにすでに含まれてるからそれをそのまま利用
- 言語はAccept-Languageヘッダーをもとに補完
- 補完した結果、サイトにない組み合わせだった場合、本社サイト(/us/en/)にリダイレクト


## 実装

``` rewrite.rules
## complete country and language URLs from http headers
RewriteMap lowercase int:tolower

#### case 1: /     -> /jp/ja/
RewriteCond %{REQUEST_URI} ^/?$
RewriteCond %{HTTP:Accept-Language} ^(..)
RewriteRule ^.+$ /%{HTTP:CloudFront-Viewer-Country}/%1/ [C]
RewriteRule ^(.*)$ ${lowercase:$1} [L,R=301]

#### case 2: /jp/  -> /jp/ja/
RewriteCond %{REQUEST_URI} ^/[a-zA-Z]{2}/?$
RewriteCond %{HTTP:Accept-Language} ^(..)
RewriteRule ^(.+?)/?$ $1/%1/ [C]
RewriteRule ^(.*)$ ${lowercase:$1} [L,R=301]

#### case 3: redirect /us/en/ if the url(country and language) does not exist in origin.
RewriteMap cl "txt:/etc/httpd/conf.d/rewrites/country-language.txt"
RewriteCond %{REQUEST_URI} ^/[a-zA-Z]{2}/[a-zA-Z]{2}/$
RewriteCond ${cl:%{REQUEST_URI}|none}  ^none$
RewriteRule ^.+$ /us/en/ [L,R=301]

```

``` country-language.txt
##
## country-language.txt - supported country and language list
##
/jp/en/ accept
/jp/ja/ accept
/ch/en/ accept
/us/en/ accept

```

## 説明
やることはシンプルなので設計をもとにどのようなURLのパターンがあるかを洗い出し、分類して、コンパクトに設定できるか考えた。
が、そもそもrewriteでどうやるの？っていうのが合ったのでまずは自分が困った部分について解説してみる。

### GeoIPによる国判定
GlobalIPのアドレスレンジはICANNが管理し国ごとに決められたレンジが払い出されています。なので理屈上はアドレスレンジと国の対応表がGetできればアクセス元IPから国が特定できるのですが、
1. アドレスレンジと国の対応は変わる可能性がある
2. レンジは膨大なのでマッチングにそこそこ処理がかかる。
のでWebサーバ側であまりやりたくありません。

調べていたらCloudFrontのジオターゲティング機能を用いれば、`CloudFront-Viewer-Country`というカスタムヘッダーに国コードをセットしてくれることがわかったので今回はそれを使うことにしました。
Akamaiにも同様の機能があるみたいなのでCDNでは一般的な機能なのかもしれません。
参考：[Amazon CloudFrontがジオターゲティングに対応しました](https://dev.classmethod.jp/cloud/aws/cloudfront-geo-targeting/)

### URLを小文字に変換する。
mod_rewriteにはrewritemapと言って指定されたmapにしたがって変換処理する機能があります。そしてこのmapには組み込みのmapがいくつかあり、`tolower`を使うと小文字に変換できることがわかりました。

具体的にはこんな感じです。

```
RewriteMap lowercase int:tolower
RewriteRule ^(.*)$ ${lowercase:$1} [L,R=301]
```

これはtolowerという組み込みの対応表(関数と思うと理解しやすい)をlowercaseという名前で宣言しています、`${lowercase:$1}`は `$1`の値を lowercaseで変換した結果、という意味です。つまり`$1` が小文字に変換されます。で、`$1`は直前の正規表現の1番目のキャプチャ(括弧)部分です。直前の正規表現を見ると`^(.*)$`はリクエストURLの始めから最後までにマッチしますので結局リクエストURL全体が小文字に変換されることになります。

参考：[Using RewriteMap](https://httpd.apache.org/docs/2.4/ja/rewrite/rewritemap.html#int)

### RewriteCond の正規表現結果を後続のRewriteRule で再利用する方法。
これは `%N` で参照できました。

具体的にはこんな感じで、下の例ではRewriteCondでキャプチャしたものをRewriteRuleの変換先パラメータとして使用しています。そして便利なことにRewriteCond のキャプチャとRewriteRule内の正規表現のキャプチャは別物のようで `$1/%1/`のように同時に参照することができます。なんて便利！！

```
RewriteCond %{HTTP:Accept-Language} ^(..)
RewriteRule ^(.+?)/?$ $1/%1/ [C]
```

ちなみに上記は `http://hoge.com/jp/` というURLでアクセスされて、ブラウザ言語設定が日本語だった場合に`http://hoge.com/jp/ja/`というURLに補完させるロジックに使っている記述です。


### originサイトにないURLだった場合にDefault値(/us/en/)にリダレクトさせる運用しやすい設定方法
これはRewriteMapを使います。RewriteMapはmapにない場合のDefault値を定義することができ、これを利用するとシンプルに実装できました。上記例で `### case 3`と記載している箇所がそれです。
1行つづコメントします。

まず１行目、これはoriginサイトに存在するURLの表を予めcountry-language.txtにまとめておき、それをRewriteMapで`cl`というmapとして宣言しています。country-language.txtは見てもらったらわかるとおり一律 `accept` に変換されるようにしています。

```
RewriteMap cl "txt:/etc/httpd/conf.d/rewrites/country-language.txt"
```

２行目、`/jp/ja/`のように`/国/言語/`のURLに限定しています。

```
RewriteCond %{REQUEST_URI} ^/[a-zA-Z]{2}/[a-zA-Z]{2}/$
```


３行目、URLを１行目で宣言したmapに渡し戻り値がnoneかそうでないかを比較しています。
country-language.txt内にあるURLであれば`${cl:%{REQUEST_URI}|none}`は`accept`となるので`^none$`にはマッチしません。逆にcountry-language.txtに無いURLの場合は`${cl:%{REQUEST_URI}|none}`はDefault値の`none`となり`^none$`にマッチします。つまりcountry-language.txtに無いURLの場合のみ４行目のRewriteRuleが実行されます。

```
RewriteCond ${cl:%{REQUEST_URI}|none}  ^none$
```
４行目、ここは簡単です。この行が実行されるのはoriginサイトに存在しないURLの場合のみなので、`/us/en/`にリダイレクトさせるだけです。

```
RewriteRule ^.+$ /us/en/ [L,R=301]
```


## こころ残り
今回は予め`country-language.txt`に存在するURLをすべて列挙しておいて、mapに存在しないURLならDefaultURLに飛ばすという処理にしましたが、originサイトのページ有無に応じて自動で振り分けできるようにしたかったのですが良い方法が浮かびませんでした。
RewriteMap にはカスタムスクリプトを登録することができるのでそれを利用すれば実現できそうですが、Apacheの負荷とブラウザへの応答時間が心配で今回は見送りました
良い方法をご存知だったらコメントで教えてもらえると泣いて喜びます