rack-cors から CORS を理解する

web の仕様を理解するにはドキュメントを読み、その仕様の実装を見てみるのが良い方法だと思う。
ruby の CORS の制御を実装している rack-cors から CORS を理解する。

CORS について

  • Cross Origin Resource Sharing とは、異なるオリジン(protocol + port + domain)間で、リソースを認可するための仕様
    • 主に XMLHttpRequest または Fetch API を呼び出し時に利用されることが多い(つまり JavaScript を利用したサーバーとの通信全般)
  • この仕様の全体についてはまとめないので下のような記事を読むと良さそう
  • Simple Request と Preflight Request に関しては、自分もあまり覚えていなかったのでまとめる
  • Simple Request
    • サイト間リクエストがユーザーデータに影響を与えないようなリクエス
    • CORS プリフライトを伴わないリクエストで、 MDN のドキュメントによると以下 5 条件が満たされているリクエストのこと。
      • 許可されているメソッドのうちの一つであること
        • GET
        • HEAD
        • POST ユーザーエージェントによって自動的に設定されたヘッダーを除いて、手動で設定できるヘッダーが以下だけであること
        • Accept
        • Accept-Language
        • Content-Language
        • Content-Type (但し、下記の要件を満たすもの)
        • DPR
        • Downlink
        • Save-Data
        • Viewport-Width
        • Width
      • Content-Type ヘッダーが以下のいずれかであること
        • application/x-www-form-urlencoded
        • multipart/form-data
        • text/plain
      • リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと。
      • リクエストに ReadableStream オブジェクトが使用されていないこと。
  • Preflight Request

rack-cors とは

rack アプリケーションに CORS の制御機能を追加する rack middleware。 独自の DSL で CORS を制御できるようにする。 rack-cors の README にあるサンプル。 どのオリジンが、どのリソースに、何の操作をしていいかを定義している。

use Rack::Cors do
  allow do
    origins 'localhost:3000', '127.0.0.1:3000',
            /\Ahttp:\/\/192\.168\.0\.\d{1,3}(:\d+)?\z/
            # regular expressions can be used here

    resource '/file/list_all/', :headers => 'x-domain-token'
    resource '/file/at/*',
        methods: [:get, :post, :delete, :put, :patch, :options, :head],
        headers: 'x-domain-token',
        expose: ['Some-Custom-Response-Header'],
        max_age: 600
        # headers to expose
  end

  allow do
    origins '*'
    resource '/public/*', headers: :any, methods: :get

    # Only allow a request for a specific host
    resource '/api/v1/*',
        headers: :any,
        methods: :get,
        if: proc { |env| env['HTTP_HOST'] == 'api.example.com' }
  end
end

実際に見ていく

実際にリクエストが来た時、Simple RequestPreflight Requestで制御が異なるのでそれぞれのケースを見ていく。

Simple Request のケース

  • Simple Request を扱うケース
    • レスポンス時に付加するヘッダーを取得する
    • Rack::Cors#process_cors
      • 定義した DSL の origin・resource にリクエストが発生している origin・resource の組み合わせがあるか確認する
      • 存在していればレスポンス時に付加するヘッダーを取得する
    • レスポンス時に Access-Control-* のヘッダーを付加する
      • リクエストヘッダーの Origin と レスポンスヘッダーの Access-Control-Allow-Origin が一致していなければリソースは利用されない。
      • Simple Request の時って、Access-Control-Allow-OriginAccess-Control-Allow-Credentials 以外のヘッダーはいらないのでは?

Preflight Request のケース

  • Preflight Request
    • Method が Options でヘッダーに HTTP_ACCESS_CONTROL_REQUEST_METHOD がある時は、Preflight Request としてリクエストを処理する
    • Rack::Cors#process_preflight
      • 定義した DSL の origin・resource にリクエストが発生している origin・resource の組み合わせがあるか確認する
      • なければ Preflight Request の処理を終了する。Access-Control-* がレスポンスヘッダーに付加されないので後続のリクエストは送られない
      • リクエストヘッダーの Access-Control-Request-MethodAccess-Control-Request-Headers が事前に許可するとしたメソッドとヘッダーに含まれていなければ Preflight Request の処理を終了する。
      • Rack::Cors#to_preflight_headers
        • レスポンスヘッダーに追加する Access-Control-* のハッシュを返す
      • 内側の rack アプリケーションにはリクエストを伝搬せず、ヘッダーを付加したレスポンスを返す