Elixir の OSS を読んでみよう ~ Plug.CSRFProtection を例に ~

この記事は、[fukuoka.ex Elixir／Phoenix Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fukuokaex) 12日目です。
前日は、 @tomoaki-kimura さんの [細かい話は置いといて、とりあえず触ってみたい人のための Elixir](https://qiita.com/tomoaki-kimura) でした

# OSS を読む
月1で [Elixir Digitalization Implementors](https://fukuokaex.connpass.com/event/197857/) というElixirの情報交換やつくったものの発表を行うイベントがあるんですが、その中で「ElixirのOSSは実は読みやすい」という話を小耳に挟みました
最近仕事では１０万コミットを超える巨大なPerlコードを読み解いて運営・改修する仕事をしているので、コードリーディング能力がどれくらい伸びたかを試してみよう！というモチベーションもありました

ということでこの記事では実際に読んでみます！

Elixir のライブラリとして有名なものとしてRailsライクなWebフレームワークのPhoenixがあると思いますが、この中で頻繁に使われている [Plug](https://hex.pm/packages/plug/1.11.0) というモジュールがあります
詳しくは Elixir School の記事がわかりやすいかと
https://elixirschool.com/ja/lessons/specifics/plug/

Plugモジュールはさらにいろんなモジュールで構成されているので、その中の何読もうかなーと思ったんですが、せっかく読むしちょっと難しいのにしようと思ったので、Phoenixのボイラープレートの中にデフォルトで定義されている `:protect_from_forgery` （CSRFトークンの発行と検証） を読んでみます

```elixir
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
```

## ちなみに読み方のコツ
読む前に、読み方のコツを書いておきます
コツは基本的にいろんなファイルを横断することになるので、検索しにくい github 上で読まずに、手元にcloneしてみなさんが普段使い慣れているエディタ上で読むことをオススメします
すでに mix deps.get などでダウンロードしたことあるライブラリであれば、 clone せずに deps ディレクトリ以下の対象ライブラリを直接エディタで開くという手もあります笑

僕は普段VsCodeの高速な全検索機能にお世話になっているので、こちらをフル活用していきます！

# 読んでみる
まず `:protect_from_forgery` のドキュメントを読みます
https://hexdocs.pm/phoenix/Phoenix.Controller.html#protect_from_forgery/2

CSRF攻撃を防ぐplugっぽいですね。 `Plug.CSRFProtection` のラッパーとのこと
ちなみにCSRF攻撃の説明はこちらです。
https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA

Elixirはドキュメントが充実しているので大変良いですね。右上の `</>` マークをクリックするとgithub上のコードにジャンプできるので大変便利・・・！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/b06add18-3d06-4e28-7bb2-d0c7497f818a.png)

ということでコードを読んでみます。
https://github.com/phoenixframework/phoenix/blob/e05af0ad5527c9a192de122f3e43d7ed4c7a4c9f/lib/phoenix/controller.ex#L1082-L1084

```elixir
  def protect_from_forgery(conn, opts \\ []) do
    Plug.CSRFProtection.call(conn, Plug.CSRFProtection.init(opts))
  end
```

本当にそのままですね笑
ということで次に `Plug.CSRFProtection` を読みます
https://hexdocs.pm/plug/Plug.CSRFProtection.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/094c3edb-907a-1eba-7833-e05411f22361.png)

コードはこちら。ドキュメント込みで４５０行程度（実装は３００行くらい）と意外とコンパクトですね。
https://github.com/elixir-plug/plug/blob/v1.11.0/lib/plug/csrf_protection.ex#L1

## Plug部分を読む
Plug は必ずビヘイビアが要求する init/1 と call/2 の関数を満たすので、そこから追っていきます。
https://github.com/elixir-plug/plug/blob/692655393a090fbae544f5cd10255d4d600e7bb0/lib/plug/csrf_protection.ex#L289-L317

```elixir
  def init(opts) do
    session_key = Keyword.get(opts, :session_key, "_csrf_token")
    mode = Keyword.get(opts, :with, :exception)
    allow_hosts = Keyword.get(opts, :allow_hosts, [])
    {session_key, mode, allow_hosts}
  end


  def call(conn, {session_key, mode, allow_hosts}) do
    csrf_token = dump_state_from_session(get_session(conn, session_key))
    load_state(conn.secret_key_base, csrf_token)


    conn =
      cond do
        verified_request?(conn, csrf_token, allow_hosts) ->
          conn


        mode == :clear_session ->
          conn |> configure_session(ignore: true) |> clear_session()


        mode == :exception ->
          raise InvalidCSRFTokenError


        true ->
          raise ArgumentError,
                "option :with should be one of :exception or :clear_session, got #{inspect(mode)}"
      end


    register_before_send(conn, &ensure_same_origin_and_csrf_token!(&1, session_key, csrf_token))
  end
```

init/1 をみると色々オプションあるみたいですね。ドキュメントに書いてるものと一致してそうです。
call/2 をみるとざっくりセッションからcsrf_tokenを読み出して、validateしてから `register_before_send` してそうです。

validate の定義は以下です。 `@unprotected_methods` に定義されたメソッドじゃないとそもそも必ずtrueになるのでそれ以上の判定をしないようですね（CSRF攻撃の性質からこれは妥当な対応です）

```elixir
@unprotected_methods ~w(HEAD GET OPTIONS)

...
  defp verified_request?(conn, csrf_token, allow_hosts) do
    conn.method in @unprotected_methods ||
      valid_csrf_token?(conn, csrf_token, body_csrf_token(conn), allow_hosts) ||
      valid_csrf_token?(conn, csrf_token, header_csrf_token(conn), allow_hosts) ||
      skip_csrf_protection?(conn)
  end
```


## 疑問を設定してみる
あとはこんな感じでエディタ検索を駆使すれば大体の概要は読めそうです（細かい挙動を追うのは大変ですが、概要理解できるだけでも十分価値があります）

実際にOSS読みたい場合は知りたい挙動があることが多いので、今回も疑問を設定してみます
今回は**「どのタイミングでどこにtokenは記録されるのか？」**を明らかにしてみます
先ほどのコードリーディングではtokenの検証はしてそうでしたが、発行はしてなさそうでした
ということで深掘りしてみます。

### どのタイミングで記録するか
まず発行したtokenをどのタイミングで記録しているかを調べましょう

`register_before_send(conn, &ensure_same_origin_and_csrf_token!(&1, session_key, csrf_token))`

が怪しそうです。 `register_before_send` を全検索してみると、これは大元の `Plug.Conn` に定義されてそうでした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/6840221a-b0bd-9b2d-4f76-e18fc80d0491.png)

```elixir
  def register_before_send(conn, callback)

  def register_before_send(%Conn{state: state}, _callback)
      when not (state in @unsent) do
    raise AlreadySentError
  end

  def register_before_send(%Conn{before_send: before_send} = conn, callback)
      when is_function(callback, 1) do
    %{conn | before_send: [callback | before_send]}
  end
```

どうもこれを呼び出すと、 `conn` （Plug.Conn型の変数）の `:before_send` キーにコールバック関数を登録してくれてそうです。
みた感じ色んなWebフレームワークでおなじみの `end_hook` みたいなイメージですね

もう少し追ってみると `run_before_send` という関数の中で登録されたコールバック関数をひたすら適用してそうです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/0a6a3984-ca7c-af48-3a9b-3ba6b53878bc.png)

```elixir
  defp run_before_send(%Conn{before_send: before_send} = conn, new) do
    conn = Enum.reduce(before_send, %{conn | state: new}, & &1.(&2))

    if conn.state != new do
      raise ArgumentError, "cannot send/change response from run_before_send callback"
    end

    %{conn | resp_headers: merge_headers(conn.resp_headers, conn.resp_cookies)}
  end
```

`run_before_send` は色んなところで呼ばれてますが、分かりやすいのが `send_resp` するときに呼ばれているところですね
やはり予想通り end_hook っぽい挙動のようです
こういう書き方あるんですね・・・！初めて知りました
自作Plug作るときに役立ちそうです

```elixir
  @spec send_resp(t) :: t | no_return
  def send_resp(conn)

  def send_resp(%Conn{state: :unset}) do
    raise ArgumentError, "cannot send a response that was not set"
  end

  def send_resp(%Conn{adapter: {adapter, payload}, state: :set, owner: owner} = conn) do
    conn = run_before_send(conn, :set)

    {:ok, body, payload} =
      adapter.send_resp(payload, conn.status, conn.resp_headers, conn.resp_body)

    send(owner, @already_sent)
    %{conn | adapter: {adapter, payload}, resp_body: body, state: :sent}
  end

  def send_resp(%Conn{}) do
    raise AlreadySentError
  end
```

そして `register_before_send` で登録された `ensure_same_origin_and_csrf_token` をみてみるとこんな感じです
あればセッションに記録するけど、そうでなければスルーみたいな挙動になってますね

```elixir
  defp ensure_csrf_token(conn, session_key, csrf_token) do
    Process.delete(:plug_masked_csrf_token)

    case Process.delete(:plug_unmasked_csrf_token) do
      ^csrf_token -> conn
      nil -> conn
      :delete -> delete_session(conn, session_key)
      current -> put_session(conn, session_key, current)
    end
  end
```

### どこで発行されるか
次にどのタイミングで発行されるかですが、 一旦ドキュメントを読んでみましょう
コード追った後に読んでみると理解しやすいですね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/c7e80640-bc28-fbf7-bfae-7df36a305092.png)

どうも Plug.CSRFProtection が自動で生成してくれるわけではなく、 `get_csrf_token` を呼んで、なかったら自動生成しますよ、という感じみたいですね
コードみてみると確かにそんな感じっぽい

```elixir
  def get_csrf_token do
    if token = Process.get(:plug_masked_csrf_token) do
      token
    else
      token = mask(unmasked_csrf_token())
      Process.put(:plug_masked_csrf_token, token)
      token
    end
  end
```

※このように疑問点はドキュメントだけでなくコードリーディングを組み合わせることで理解が深まるのでおすすめです！

tokenってどこに保存するんだろう？（DB？）と思ってましたがここで `Process` モジュール使ってるんですね！
https://hexdocs.pm/elixir/Process.html

こういう簡単なデータ保存に使えるとはあまり思っていなかったので、新しい発見でした
`Process.get` や `Process.put` 便利ですね
乱用するとグローバル変数の嵐になりそうですが、簡単にredisみたいな使い方したいだけなら良さそうです
ただ expire の設定とかはできなさそうなので、実際に検討する場合は要件に応じて、という感じになりそうですね

#### おまけ もうちょっと深追い
`get_csrf_token` を呼んでる箇所どこかなーと探してみましたが、これは結構見つけるの大変でした笑
deps以下全検索してみたら `PhoenixHTML` モジュールがやってそうです（ `get_csrf_token_for` は内部で `get_csrf_token` を呼んでます）
これを使うことで、フォームに自動で埋め込んでくれるみたいですね

https://github.com/phoenixframework/phoenix_html/blob/070235190c5850fd52526228e89f35d7e410dfbd/lib/phoenix_html/tag.ex#L182-L218

```elixir
  def form_tag(action, opts \\ [])


  def form_tag(action, do: block) do
    form_tag(action, [], do: block)
  end


  def form_tag(action, opts) when is_list(opts) do
    {:safe, method} = html_escape(Keyword.get(opts, :method, "post"))


    {extra, opts} =
      case method do
        "get" ->
          {"", opts}


        "post" ->
          csrf_token_tag(
            action,
            Keyword.put(opts, :method, "post"),
            ""
          )


        _ ->
          csrf_token_tag(
            action,
            Keyword.put(opts, :method, "post"),
            ~s'<input name="#{@method_param}" type="hidden" value="#{method}">'
          )
      end


    opts =
      case Keyword.pop(opts, :multipart, false) do
        {false, opts} -> opts
        {true, opts} -> Keyword.put(opts, :enctype, "multipart/form-data")
      end


    html_escape([tag(:form, [action: action] ++ opts), raw(extra)])
  end

...
  defp csrf_token_tag(to, opts, extra) do
    case Keyword.pop(opts, :csrf_token, true) do
      {csrf_token, opts} when is_binary(csrf_token) ->
        {extra <> ~s'<input name="#{@csrf_param}" type="hidden" value="#{csrf_token}">', opts}

      {true, opts} ->
        csrf_token = Plug.CSRFProtection.get_csrf_token_for(to)
        {extra <> ~s'<input name="#{@csrf_param}" type="hidden" value="#{csrf_token}">', opts}

      {false, opts} ->
        {extra, opts}
    end
  end
```

逆にいうと、自前で HTML 書いたりしているけど、CSRF攻撃に対する対策したい場合は自分で `get_csrf_token` を呼ばないといけないですね！

確認してみると確かにされてそうでした
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201242/9bef5e2e-4051-aad0-afd6-731b6e7b533c.png)

## まとめ
今回読んだOSSは結構難しい方でしたが、もう少し簡単なものはいくつもあるので（[Enumモジュールとか](https://github.com/elixir-lang/elixir/blob/v1.11.2/lib/elixir/lib/enum.ex#L199)）、ぜひ自分が使っているライブラリの細かい挙動が気になったらドキュメントを読むだけでなくコードを読んでみてもいかがでしょうか？
またOSSのコードは大抵自分よりすごい人が書いていることが多いので、そこから学ぶことも多いです
自分自身、仕事でコードを読むことで成長できた面があるので、自分の成長という意味でもオススメです

あとは単にこれどうやって動いてるんだろう・・・？というのを解き明かしていくのは楽しいですね
謎解きみたいな感覚になります笑

Elixirは他の言語に比べると読みやすい方かなと思うので、上級アルケミストを目指す人はぜひチャレンジしてみてください！

明日13日目はわれらが @piacere_ex さんの「プログラミングElixir第二版でアップデートされた内容について」です！
僕も購入しましたがまだ読みきれてないので、気になりますね・・・！笑