rack-cors 1.0.5 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rack/cors.rb CHANGED
@@ -1,36 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
4
+ require_relative 'cors/resources'
5
+ require_relative 'cors/resource'
6
+ require_relative 'cors/result'
7
+ require_relative 'cors/version'
2
8
 
3
9
  module Rack
4
10
  class Cors
5
- HTTP_ORIGIN = 'HTTP_ORIGIN'.freeze
6
- HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'.freeze
11
+ HTTP_ORIGIN = 'HTTP_ORIGIN'
12
+ HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'
7
13
 
8
- HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'.freeze
9
- HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'.freeze
14
+ HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'
15
+ HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
10
16
 
11
- PATH_INFO = 'PATH_INFO'.freeze
12
- REQUEST_METHOD = 'REQUEST_METHOD'.freeze
17
+ PATH_INFO = 'PATH_INFO'
18
+ REQUEST_METHOD = 'REQUEST_METHOD'
13
19
 
14
- RACK_LOGGER = 'rack.logger'.freeze
20
+ RACK_LOGGER = 'rack.logger'
15
21
  RACK_CORS =
16
- # retaining the old key for backwards compatibility
17
- ENV_KEY = 'rack.cors'.freeze
22
+ # retaining the old key for backwards compatibility
23
+ ENV_KEY = 'rack.cors'
18
24
 
19
- OPTIONS = 'OPTIONS'.freeze
20
- VARY = 'Vary'.freeze
25
+ OPTIONS = 'OPTIONS'
21
26
 
22
27
  DEFAULT_VARY_HEADERS = ['Origin'].freeze
23
28
 
24
- # All CORS routes need to accept CORS simple headers at all times
25
- # {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers}
26
- CORS_SIMPLE_HEADERS = ['accept', 'accept-language', 'content-language', 'content-type'].freeze
27
-
28
- def initialize(app, opts={}, &block)
29
+ def initialize(app, opts = {}, &block)
29
30
  @app = app
30
31
  @debug_mode = !!opts[:debug]
31
32
  @logger = @logger_proc = nil
32
33
 
33
- if logger = opts[:logger]
34
+ logger = opts[:logger]
35
+ if logger
34
36
  if logger.respond_to? :call
35
37
  @logger_proc = opts[:logger]
36
38
  else
@@ -38,12 +40,12 @@ module Rack
38
40
  end
39
41
  end
40
42
 
41
- if block_given?
42
- if block.arity == 1
43
- block.call(self)
44
- else
45
- instance_eval(&block)
46
- end
43
+ return unless block_given?
44
+
45
+ if block.arity == 1
46
+ block.call(self)
47
+ else
48
+ instance_eval(&block)
47
49
  end
48
50
  end
49
51
 
@@ -69,18 +71,20 @@ module Rack
69
71
  add_headers = nil
70
72
  if env[HTTP_ORIGIN]
71
73
  debug(env) do
72
- [ 'Incoming Headers:',
73
- " Origin: #{env[HTTP_ORIGIN]}",
74
- " Path-Info: #{path}",
75
- " Access-Control-Request-Method: #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]}",
76
- " Access-Control-Request-Headers: #{env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]}"
77
- ].join("\n")
74
+ ['Incoming Headers:',
75
+ " Origin: #{env[HTTP_ORIGIN]}",
76
+ " Path-Info: #{path}",
77
+ " Access-Control-Request-Method: #{env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]}",
78
+ " Access-Control-Request-Headers: #{env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]}"].join("\n")
78
79
  end
79
- if env[REQUEST_METHOD] == OPTIONS and env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
80
+
81
+ if env[REQUEST_METHOD] == OPTIONS && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
82
+ return [400, {}, []] unless Rack::Utils.valid_path?(path)
83
+
80
84
  headers = process_preflight(env, path)
81
85
  debug(env) do
82
86
  "Preflight Headers:\n" +
83
- headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
87
+ headers.collect { |kv| " #{kv.join(': ')}" }.join("\n")
84
88
  end
85
89
  return [200, headers, []]
86
90
  else
@@ -101,9 +105,7 @@ module Rack
101
105
  headers = add_headers.merge(headers)
102
106
  debug(env) do
103
107
  add_headers.each_pair do |key, value|
104
- if headers.has_key?(key)
105
- headers["X-Rack-CORS-Original-#{key}"] = value
106
- end
108
+ headers["x-rack-cors-original-#{key}"] = value if headers.key?(key)
107
109
  end
108
110
  end
109
111
  end
@@ -112,349 +114,106 @@ module Rack
112
114
  # response to be different depending on the Origin header value.
113
115
  # Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
114
116
  if vary_resource
115
- vary = headers[VARY]
116
- cors_vary_headers = if vary_resource.vary_headers && vary_resource.vary_headers.any?
117
- vary_resource.vary_headers
118
- else
119
- DEFAULT_VARY_HEADERS
120
- end
121
- headers[VARY] = ((vary ? ([vary].flatten.map { |v| v.split(/,\s*/) }.flatten) : []) + cors_vary_headers).uniq.join(', ')
117
+ vary = headers['vary']
118
+ cors_vary_headers = if vary_resource.vary_headers&.any?
119
+ vary_resource.vary_headers
120
+ else
121
+ DEFAULT_VARY_HEADERS
122
+ end
123
+ headers['vary'] = ((vary ? [vary].flatten.map { |v| v.split(/,\s*/) }.flatten : []) + cors_vary_headers).uniq.join(', ')
122
124
  end
123
125
 
124
- if debug? && result = env[RACK_CORS]
125
- result.append_header(headers)
126
- end
126
+ result = env[ENV_KEY]
127
+ result.append_header(headers) if debug? && result
127
128
 
128
129
  [status, headers, body]
129
130
  end
130
131
 
131
132
  protected
132
- def debug(env, message = nil, &block)
133
- (@logger || select_logger(env)).debug(message, &block) if debug?
134
- end
135
-
136
- def select_logger(env)
137
- @logger = if @logger_proc
138
- logger_proc = @logger_proc
139
- @logger_proc = nil
140
- logger_proc.call
141
133
 
142
- elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
143
- Rails.logger
144
-
145
- elsif env[RACK_LOGGER]
146
- env[RACK_LOGGER]
147
-
148
- else
149
- ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
150
- end
151
- end
134
+ def debug(env, message = nil, &block)
135
+ (@logger || select_logger(env)).debug(message, &block) if debug?
136
+ end
152
137
 
153
- def evaluate_path(env)
154
- path = env[PATH_INFO]
155
- path = Rack::Utils.clean_path_info(Rack::Utils.unescape_path(path)) if path
156
- path
157
- end
138
+ def select_logger(env)
139
+ @logger = if @logger_proc
140
+ logger_proc = @logger_proc
141
+ @logger_proc = nil
142
+ logger_proc.call
158
143
 
159
- def all_resources
160
- @all_resources ||= []
161
- end
144
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
145
+ Rails.logger
162
146
 
163
- def process_preflight(env, path)
164
- result = Result.preflight(env)
147
+ elsif env[RACK_LOGGER]
148
+ env[RACK_LOGGER]
165
149
 
166
- resource, error = match_resource(path, env)
167
- unless resource
168
- result.miss(error)
169
- return {}
170
- end
150
+ else
151
+ ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
152
+ end
153
+ end
171
154
 
172
- return resource.process_preflight(env, result)
173
- end
155
+ def evaluate_path(env)
156
+ path = env[PATH_INFO]
174
157
 
175
- def process_cors(env, path)
176
- resource, error = match_resource(path, env)
177
- if resource
178
- Result.hit(env)
179
- cors = resource.to_headers(env)
180
- cors
158
+ if path
159
+ path = Rack::Utils.unescape_path(path)
181
160
 
182
- else
183
- Result.miss(env, error)
184
- nil
185
- end
161
+ path = Rack::Utils.clean_path_info(path) if Rack::Utils.valid_path?(path)
186
162
  end
187
163
 
188
- def resource_for_path(path_info)
189
- all_resources.each do |r|
190
- if found = r.resource_for_path(path_info)
191
- return found
192
- end
193
- end
194
- nil
195
- end
164
+ path
165
+ end
196
166
 
197
- def match_resource(path, env)
198
- origin = env[HTTP_ORIGIN]
167
+ def all_resources
168
+ @all_resources ||= []
169
+ end
199
170
 
200
- origin_matched = false
201
- all_resources.each do |r|
202
- if r.allow_origin?(origin, env)
203
- origin_matched = true
204
- if found = r.match_resource(path, env)
205
- return [found, nil]
206
- end
207
- end
208
- end
171
+ def process_preflight(env, path)
172
+ result = Result.preflight(env)
209
173
 
210
- [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
174
+ resource, error = match_resource(path, env)
175
+ unless resource
176
+ result.miss(error)
177
+ return {}
211
178
  end
212
179
 
213
- class Result
214
- HEADER_KEY = 'X-Rack-CORS'.freeze
215
-
216
- MISS_NO_ORIGIN = 'no-origin'.freeze
217
- MISS_NO_PATH = 'no-path'.freeze
218
-
219
- MISS_NO_METHOD = 'no-method'.freeze
220
- MISS_DENY_METHOD = 'deny-method'.freeze
221
- MISS_DENY_HEADER = 'deny-header'.freeze
222
-
223
- attr_accessor :preflight, :hit, :miss_reason
224
-
225
- def hit?
226
- !!hit
227
- end
228
-
229
- def preflight?
230
- !!preflight
231
- end
232
-
233
- def miss(reason)
234
- self.hit = false
235
- self.miss_reason = reason
236
- end
237
-
238
- def self.hit(env)
239
- r = Result.new
240
- r.preflight = false
241
- r.hit = true
242
- env[RACK_CORS] = r
243
- end
244
-
245
- def self.miss(env, reason)
246
- r = Result.new
247
- r.preflight = false
248
- r.hit = false
249
- r.miss_reason = reason
250
- env[RACK_CORS] = r
251
- end
252
-
253
- def self.preflight(env)
254
- r = Result.new
255
- r.preflight = true
256
- env[RACK_CORS] = r
257
- end
180
+ resource.process_preflight(env, result)
181
+ end
258
182
 
183
+ def process_cors(env, path)
184
+ resource, error = match_resource(path, env)
185
+ if resource
186
+ Result.hit(env)
187
+ cors = resource.to_headers(env)
188
+ cors
259
189
 
260
- def append_header(headers)
261
- headers[HEADER_KEY] = if hit?
262
- preflight? ? 'preflight-hit' : 'hit'
263
- else
264
- [
265
- (preflight? ? 'preflight-miss' : 'miss'),
266
- miss_reason
267
- ].join('; ')
268
- end
269
- end
190
+ else
191
+ Result.miss(env, error)
192
+ nil
270
193
  end
194
+ end
271
195
 
272
- class Resources
273
-
274
- attr_reader :resources
275
-
276
- def initialize
277
- @origins = []
278
- @resources = []
279
- @public_resources = false
280
- end
281
-
282
- def origins(*args, &blk)
283
- @origins = args.flatten.reject{ |s| s == '' }.map do |n|
284
- case n
285
- when Proc,
286
- Regexp,
287
- /^https?:\/\//,
288
- 'file://' then n
289
- when '*' then @public_resources = true; n
290
- else Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
291
- end
292
- end.flatten
293
- @origins.push(blk) if blk
294
- end
295
-
296
- def resource(path, opts={})
297
- @resources << Resource.new(public_resources?, path, opts)
298
- end
299
-
300
- def public_resources?
301
- @public_resources
302
- end
303
-
304
- def allow_origin?(source,env = {})
305
- return true if public_resources?
306
-
307
- return !! @origins.detect do |origin|
308
- if origin.is_a?(Proc)
309
- origin.call(source,env)
310
- else
311
- origin === source
312
- end
313
- end
314
- end
315
-
316
- def match_resource(path, env)
317
- @resources.detect { |r| r.match?(path, env) }
318
- end
319
-
320
- def resource_for_path(path)
321
- @resources.detect { |r| r.matches_path?(path) }
322
- end
323
-
196
+ def resource_for_path(path_info)
197
+ all_resources.each do |r|
198
+ found = r.resource_for_path(path_info)
199
+ return found if found
324
200
  end
201
+ nil
202
+ end
325
203
 
326
- class Resource
327
- class CorsMisconfigurationError < StandardError
328
- def message
329
- "Allowing credentials for wildcard origins is insecure."\
330
- " Please specify more restrictive origins or set 'credentials' to false in your CORS configuration."
331
- end
332
- end
333
-
334
- attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern, :if_proc, :vary_headers
335
-
336
- def initialize(public_resource, path, opts={})
337
- raise CorsMisconfigurationError if public_resource && opts[:credentials] == true
338
-
339
- self.path = path
340
- self.credentials = public_resource ? false : (opts[:credentials] == true)
341
- self.max_age = opts[:max_age] || 7200
342
- self.pattern = compile(path)
343
- self.if_proc = opts[:if]
344
- self.vary_headers = opts[:vary] && [opts[:vary]].flatten
345
- @public_resource = public_resource
346
-
347
- self.headers = case opts[:headers]
348
- when :any then :any
349
- when nil then nil
350
- else
351
- [opts[:headers]].flatten.collect{|h| h.downcase}
352
- end
353
-
354
- self.methods = case opts[:methods]
355
- when :any then [:get, :head, :post, :put, :patch, :delete, :options]
356
- else
357
- ensure_enum(opts[:methods]) || [:get]
358
- end.map{|e| e.to_s }
359
-
360
- self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
361
- end
362
-
363
- def matches_path?(path)
364
- pattern =~ path
365
- end
366
-
367
- def match?(path, env)
368
- matches_path?(path) && (if_proc.nil? || if_proc.call(env))
369
- end
370
-
371
- def process_preflight(env, result)
372
- headers = {}
373
-
374
- request_method = env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
375
- if request_method.nil?
376
- result.miss(Result::MISS_NO_METHOD) and return headers
377
- end
378
- if !methods.include?(request_method.downcase)
379
- result.miss(Result::MISS_DENY_METHOD) and return headers
380
- end
381
-
382
- request_headers = env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
383
- if request_headers && !allow_headers?(request_headers)
384
- result.miss(Result::MISS_DENY_HEADER) and return headers
385
- end
386
-
387
- result.hit = true
388
- headers.merge(to_preflight_headers(env))
389
- end
390
-
391
- def to_headers(env)
392
- h = {
393
- 'Access-Control-Allow-Origin' => origin_for_response_header(env[HTTP_ORIGIN]),
394
- 'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
395
- 'Access-Control-Expose-Headers' => expose.nil? ? '' : expose.join(', '),
396
- 'Access-Control-Max-Age' => max_age.to_s }
397
- h['Access-Control-Allow-Credentials'] = 'true' if credentials
398
- h
399
- end
400
-
401
- protected
402
- def public_resource?
403
- @public_resource
404
- end
405
-
406
- def origin_for_response_header(origin)
407
- return '*' if public_resource?
408
- origin
409
- end
410
-
411
- def to_preflight_headers(env)
412
- h = to_headers(env)
413
- if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
414
- h.merge!('Access-Control-Allow-Headers' => env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS])
415
- end
416
- h
417
- end
418
-
419
- def allow_headers?(request_headers)
420
- headers = self.headers || []
421
- if headers == :any
422
- return true
423
- end
424
- request_headers = request_headers.split(/,\s*/) if request_headers.kind_of?(String)
425
- request_headers.all? do |header|
426
- header = header.downcase
427
- CORS_SIMPLE_HEADERS.include?(header) || headers.include?(header)
428
- end
429
- end
204
+ def match_resource(path, env)
205
+ origin = env[HTTP_ORIGIN]
430
206
 
431
- def ensure_enum(v)
432
- return nil if v.nil?
433
- [v].flatten
434
- end
207
+ origin_matched = false
208
+ all_resources.each do |r|
209
+ next unless r.allow_origin?(origin, env)
435
210
 
436
- def compile(path)
437
- if path.respond_to? :to_str
438
- special_chars = %w{. + ( )}
439
- pattern =
440
- path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
441
- case match
442
- when "*"
443
- "(.*?)"
444
- when *special_chars
445
- Regexp.escape(match)
446
- else
447
- "([^/?&#]+)"
448
- end
449
- end
450
- /^#{pattern}$/
451
- elsif path.respond_to? :match
452
- path
453
- else
454
- raise TypeError, path
455
- end
456
- end
211
+ origin_matched = true
212
+ found = r.match_resource(path, env)
213
+ return [found, nil] if found
457
214
  end
458
215
 
216
+ [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
217
+ end
459
218
  end
460
219
  end