1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
|
# frozen_string_literal: true
require 'logger'
require_relative 'cors/resources'
require_relative 'cors/resource'
require_relative 'cors/result'
require_relative 'cors/version'
module Rack
class Cors
HTTP_ORIGIN = 'HTTP_ORIGIN'
HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'
HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'
HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
PATH_INFO = 'PATH_INFO'
REQUEST_METHOD = 'REQUEST_METHOD'
RACK_LOGGER = 'rack.logger'
RACK_CORS =
# retaining the old key for backwards compatibility
ENV_KEY = 'rack.cors'
OPTIONS = 'OPTIONS'
DEFAULT_VARY_HEADERS = ['Origin'].freeze
def initialize(app, opts = {}, &block)
@app = app
@debug_mode = !!opts[:debug]
@logger = @logger_proc = nil
logger = opts[:logger]
if logger
if logger.respond_to? :call
@logger_proc = opts[:logger]
else
@logger = logger
end
end
return unless block_given?
if block.arity == 1
block.call(self)
else
instance_eval(&block)
end
end
def debug?
@debug_mode
end
def allow(&block)
all_resources << (resources = Resources.new)
if block.arity == 1
block.call(resources)
else
resources.instance_eval(&block)
end
end
def call(env)
env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
path = evaluate_path(env)
add_headers = nil
if env[HTTP_ORIGIN]
debug(env) do
['Incoming Headers:',
" Origin: #{env[HTTP_ORIGIN]}",
" Path-Info: #{path}",
" 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 && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
return [400, {}, []] unless Rack::Utils.valid_path?(path)
headers = process_preflight(env, path)
debug(env) do
"Preflight Headers:\n" +
headers.collect { |kv| " #{kv.join(': ')}" }.join("\n")
end
return [200, headers, []]
else
add_headers = process_cors(env, path)
end
else
Result.miss(env, Result::MISS_NO_ORIGIN)
end
# This call must be done BEFORE calling the app because for some reason
# env[PATH_INFO] gets changed after that and it won't match. (At least
# in rails 4.1.6)
vary_resource = resource_for_path(path)
status, headers, body = @app.call env
if add_headers
headers = add_headers.merge(headers)
debug(env) do
add_headers.each_pair do |key, value|
headers["x-rack-cors-original-#{key}"] = value if headers.key?(key)
end
end
end
# Vary header should ALWAYS mention Origin if there's ANY chance for the
# response to be different depending on the Origin header value.
# Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
if vary_resource
vary = headers['vary']
cors_vary_headers = if vary_resource.vary_headers&.any?
vary_resource.vary_headers
else
DEFAULT_VARY_HEADERS
end
headers['vary'] = ((vary ? [vary].flatten.map { |v| v.split(/,\s*/) }.flatten : []) + cors_vary_headers).uniq.join(', ')
end
result = env[ENV_KEY]
result.append_header(headers) if debug? && result
[status, headers, body]
end
protected
def debug(env, message = nil, &block)
(@logger || select_logger(env)).debug(message, &block) if debug?
end
def select_logger(env)
@logger = if @logger_proc
logger_proc = @logger_proc
@logger_proc = nil
logger_proc.call
elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
Rails.logger
elsif env[RACK_LOGGER]
env[RACK_LOGGER]
else
::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
end
end
def evaluate_path(env)
path = env[PATH_INFO]
if path
path = Rack::Utils.unescape_path(path)
path = Rack::Utils.clean_path_info(path) if Rack::Utils.valid_path?(path)
end
path
end
def all_resources
@all_resources ||= []
end
def process_preflight(env, path)
result = Result.preflight(env)
resource, error = match_resource(path, env)
unless resource
result.miss(error)
return {}
end
resource.process_preflight(env, result)
end
def process_cors(env, path)
resource, error = match_resource(path, env)
if resource
Result.hit(env)
cors = resource.to_headers(env)
cors
else
Result.miss(env, error)
nil
end
end
def resource_for_path(path_info)
all_resources.each do |r|
found = r.resource_for_path(path_info)
return found if found
end
nil
end
def match_resource(path, env)
origin = env[HTTP_ORIGIN]
origin_matched = false
all_resources.each do |r|
next unless r.allow_origin?(origin, env)
origin_matched = true
found = r.match_resource(path, env)
return [found, nil] if found
end
[nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
end
end
end
|