Firebase HostingにIPでアクセス制限をかける。そして少し妥協する。

本記事は[LIFULL Advent Calendar 2017](https://qiita.com/advent-calendar/2017/lifull)の16日目の記事です。

昨日は@watanataの「[passenger-statusを監視するMackerelのプラグイン](https://qiita.com/watanata/items/a05c57fc5c2dccd9b558)」でした。

## [Firebase Hosting](https://firebase.google.com/docs/hosting/)にアクセス制限かけたい
ご存知の方も多いと思いますが、FirebaseにはWEB Hostingの機能が無料で公開されています。
デプロイの手順もCLIツールでかんたんにでき、公開されたWEBページはhttpsになっているという大変ありがたい機能です。サクッと作ったWEBを公開するのにもってこいですね。

しかしこのサービスにはアクセス制限の機能がなく、一度deployしてしまうと全世界に配信されてしまいます。
もちろん本番サイトであればそれで良いのですが、開発環境や社内ツールとして利用したい場合[^2] にはアクセスを制限できないのは少し困ります。

[^2]: 本記事はLIFULL Advent Calendarですが、今回の実装を社内ツールとして利用しているわけではありません。

そこで、今回は今年のGoogle I/Oで発表されたCloud FunctionsとFirebase Hostingを組み合わせた動的コンテンツ配信機能を利用して、IPアドレスによるホワイトリスト形式のアクセス制限をかけてみます。

## プロジェクトのセットアップ
Firebase HostingとCloud Functionsの設定をしていきます。
とはいえ、firebaseはCLIツールがとても使いやすく、ほとんど[公式ドキュメントに書いてあるとおり](https://firebase.google.com/docs/hosting/functions)にすすめていけば問題なく導入できますので、ここではhistoryだけ貼っておきます。

```console
# firebaseのcliツールをnpm経由でインストール
$ npm install -g firebase-tools

# 初期化
$ firebase init
:
略
:

? Which Firebase CLI features do you want to setup for this folder? Press Space to select features, then Enter to confirm your choices. Functions: Configure and deploy Cloud Functions, Hosting: Configure
and deploy Firebase Hosting sites

=== Project Setup

:
略
:

? Select a default Firebase project for this directory: [create a new project]

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use TSLint to catch probable bugs and enforce style? Yes

:
略
:

? Do you want to install dependencies with npm now? Yes

:
略
:

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
:
略
:

# 作ったプロジェクトを利用するように宣言
$ firebase use --add your-project
```

## IP制限をかける
準備が終わったらさっそく制限をかけていきましょう。
`functions/src/index.ts` を編集していきます。JSを設定した人はいい感じに読み替えてください。

### 実装
```ts:functions/src/index.ts
import * as functions from 'firebase-functions';
import * as requestIp from 'request-ip';
import * as fs from 'fs';

exports.filter = functions.https.onRequest((req, res) => {
  const clientIp   = requestIp.getClientIp(req);

  const isAllowed  = ['172.0.0.1'].indexOf(clientIp) !== -1;
  const statusCode = isAllowed ? 200 : 400;
  const filename   = isAllowed ? 'index' : '400';

  const html = fs.readFileSync(`./templates/${filename}.html`).toString();

  res.status(statusCode).send(html);
});
```

これだけです。かんたんですね。
かんたんすぎてTypeScriptっぽいところがほとんどない。

`[172.0.0.1]`の部分を許可したいIPにすることで無事IP制限が実現できます。
`templates/${filename}.html`の部分はいい感じのHTMLを置いておきましょう。

#### 解説
`functions.https.onRequest`は[express](https://github.com/expressjs/express)フレンドリーに設計されており[^1]、 expressに対応しているライブラリであればほとんどそのまま利用が可能になっています。
そこで今回はexpressのRequestオブジェクトからIPアドレスを取得してくれる[request-ip](https://github.com/pbojinov/request-ip)を利用してアクセスしてきたIPを取得しています。

[^1]: というか、内部的にexpressが利用されていそうです。`functions.https.onRequest`の返り値はexpressにおけるミドルウェアという立ち位置です。コードを見る限り。

あとはとってきたIPを使ってステータスコードと表示するhtmlファイルを切り替えるだけでおしまい。


### 設定ファイル
実装が終わったら設定ファイルの変更です。
Hostingされたサイトにリクエストがきたら今回作成したCloud Functionsを呼び出すようにしていきます。
私と同じように初期化した人は下記のように修正をすればOKです。

```diff:firebase.json
{
  "functions": {
    "predeploy": "npm --prefix functions run build"
  },
  "hosting": {
    "public": "public",
+    "rewrites": [ {
+      "source": "**", "function": "filter"
+    } ],
+    "redirects": [ {
+      "source" : "/",
+      "destination" : "/app",
+      "type" : 301
    }],
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}
```

#### 解説
`rewrites`の設定により、すべてのリクエストを今回作成した`filter`の結果を返すように書き換えます。
(ドキュメントにあるサンプルとほぼおなじなので解説不要だと思いますが)
これでもう設定は終わりかと思いきや、実はこの設定には一点問題が有ります。

それが `/` へのリクエストです。
ここに関しては`rewrites`が働かず、必ず`public/index.html`を返すようになっています。
つまり`/`へのアクセスはそもそも動的コンテンツを返せないのです。

悲しい。

そこで`redirects`の修正が必要になります。ここが妥協点です。
`/`にアクセスがきたとき、どこでもいいので(この例では`/app`)リダイレクトをかけてやることで絶対に`/`にアクセスできないようにします。
せっかくなので恒久的リダイレクトにしてやりましょう。
こうすることで実質的にはすべてのページにアクセス制限をかけることが可能になります。
かけられたということにしてほしい。

## デプロイ
```console
$ firebase deploy
:
略
:
Hosting URL: https://{your-project}.firebaseapp.com
```
これで作成したアプリケーションがWeb Hostingに公開されます。

せっかくなので、今回僕が作成したサンプルアプリケーションを公開しておきます。
よろしければ参考にしてください。

https://access-policy-sample.firebaseapp.com/

/appにリダイレクトされて、「Access Denined」と表示されましたか？
僕にはindexページが表示されています。
どうか、参考にしてください。

もしみなさんにAccess Denined以外のページが見えていたら、僕は怖いです。
どうか、どうか参考にしてください。

----
明日は@hanaisの記事です。お楽しみに。