rack-cors (1.0.2)のコードリーディングをしました。
その名の通りRackミドルウェアになっていてRack::Corsを明示的にミドルウェアとして設定する必要があります。
module YourApp
class Application < Rails::Application
# ...
# Rails 5
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :options]
end
end
ということでRack::Corsの中身を見ていきます。lib/rack/cors.rbの1ファイルで全て構成されているので分割して説明します。
まず初期化時にblockを渡すとRack::Corsのコンテキストでinstance_evalされます。
module Rack
class Cors
def initialize(app, opts={}, &block)
@app = app
@debug_mode = !!opts[:debug]
@logger = @logger_proc = nil
if logger = opts[:logger]
if logger.respond_to? :call
@logger_proc = opts[:logger]
else
@logger = logger
end
end
if block_given?
if block.arity == 1
block.call(self)
else
instance_eval(&block)
end
end
end
arityなしの場合、allowを呼び出すとResourcesオブジェクトのコンテキストでinstance_evalします。
def allow(&block)
all_resources << (resources = Resources.new)
if block.arity == 1
block.call(resources)
else
resources.instance_eval(&block)
end
end
Resources#originsは以下のように定義されています。例えば example.com
を定義した場合は@originsに /^[a-z][a-z0-9.+-]*:\/\/example\.com$/
がセットされます。
class Resources
# ...
def origins(*args, &blk)
@origins = args.flatten.reject{ |s| s == '' }.map do |n|
case n
when Proc,
Regexp,
/^https?:\/\//,
'file://' then n
when '*' then @public_resources = true; n
else Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
end
end.flatten
@origins.push(blk) if blk
end
def resource(path, opts={})
@resources << Resource.new(public_resources?, path, opts)
end
#resourceはResourceのオブジェクトを@resourcesに追加しています。設定は以上になります。
次にHTTPリクエスト時のミドルウェアの動きを追っていきます。Originヘッダの中身はenv[HTTP_ORIGIN]にセットされます。リクエストメソッドがOPTIONSでAccess-Control-Request-Methodヘッダが設定されている場合(=preflight request)、process_preflightでレスポンスヘッダを生成してステータスコード200で返します。
def call(env)
env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
add_headers = nil
if env[HTTP_ORIGIN]
debug(env) do
[ 'Incoming Headers:',
" Origin: #{env[HTTP_ORIGIN]}",
" Access-Control-Request-Method: #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]}",
" Access-Control-Request-Headers: #{env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]}"
].join("\n")
end
if env[REQUEST_METHOD] == OPTIONS and env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
headers = process_preflight(env)
debug(env) do
"Preflight Headers:\n" +
headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
end
return [200, headers, []]
else
add_headers = process_cors(env)
end
else
Result.miss(env, Result::MISS_NO_ORIGIN)
end
#process_preflightは#match_resourceでResourceのインスタンスを取得し、Resource#process_preflightを呼び出します。
def process_preflight(env)
result = Result.preflight(env)
resource, error = match_resource(env)
unless resource
result.miss(error)
return {}
end
return resource.process_preflight(env, result)
end
Resource#match_resouceは設定されているResourcesのインスタンスに対してResources#allow_origin?を呼び出し、trueであればさらにResources#match_resourceを呼び出します。
def match_resource(env)
path = env[PATH_INFO]
origin = env[HTTP_ORIGIN]
origin_matched = false
all_resources.each do |r|
if r.allow_origin?(origin, env)
origin_matched = true
if found = r.match_resource(path, env)
return [found, nil]
end
end
end
[nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
end
originに*
が設定されている場合はpublic_resources? = trueになるので、戻り値としてtrueを返します。そうでなければoriginsの各値とHTTP_ORIGINの値を===
で比較します。ドメインを指定した場合は、ドメインに対応する正規表現との比較になります。
def allow_origin?(source,env = {})
return true if public_resources?
return !! @origins.detect do |origin|
if origin.is_a?(Proc)
origin.call(source,env)
else
origin === source
end
end
end
Resources#match_resourceは各リソースに対してResource#match?を呼び出します。
def matches_path?(path)
pattern =~ path
end
def match?(path, env)
matches_path?(path) && (if_proc.nil? || if_proc.call(env))
end
patternはResourceのpathから生成された正規表現になります。例えば /hoge
のパスを指定した場合は/^\/hoge$/
が正規表現として設定されます。よって正規表現のマッチングによってCORSに対応したパスかどうかを判定します。
CORに対応したオリジン、パスの場合はさらにResource#process_preflightが呼ばれ、preflightリクエストのレスポンスヘッダを生成します。
def process_preflight(env, result)
headers = {CONTENT_TYPE => TEXT_PLAIN}
request_method = env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
if request_method.nil?
result.miss(Result::MISS_NO_METHOD) and return headers
end
if !methods.include?(request_method.downcase)
result.miss(Result::MISS_DENY_METHOD) and return headers
end
request_headers = env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
if request_headers && !allow_headers?(request_headers)
result.miss(Result::MISS_DENY_HEADER) and return headers
end
result.hit = true
headers.merge(to_preflight_headers(env))
end
methods.include?でメソッドが対応しているかや、Access-Control-Request-Headersのヘッダが許可されているかを判定します。どちらも対応している場合は#to_preflight_headersがレスポンスヘッダに追加されて返されます。
#to_preflight_headersは#to_headersを呼び出しています。以下のようなハッシュを返しており、これがpreflight requestのレスポンスヘッダになります
def to_headers(env)
h = {
'Access-Control-Allow-Origin' => origin_for_response_header(env[HTTP_ORIGIN]),
'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
'Access-Control-Expose-Headers' => expose.nil? ? '' : expose.join(', '),
'Access-Control-Max-Age' => max_age.to_s }
h['Access-Control-Allow-Credentials'] = 'true' if credentials
h
end
def to_preflight_headers(env)
h = to_headers(env)
if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
h.merge!('Access-Control-Allow-Headers' => env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS])
end
h
end
preflight requestでないリクエストの場合はRack::Cors#process_corsが呼ばれます。#match_resouceでResourceを検索し、マッチした場合はResource#to_headersによってレスポンスヘッダが追加されます。
def process_cors(env)
resource, error = match_resource(env)
if resource
Result.hit(env)
cors = resource.to_headers(env)
cors
else
Result.miss(env, error)
nil
end
end