rack-cors 1.1.1 → 2.0.0.rc1

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,20 +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
80
 
80
81
  if env[REQUEST_METHOD] == OPTIONS && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
81
82
  return [400, {}, []] unless Rack::Utils.valid_path?(path)
83
+
82
84
  headers = process_preflight(env, path)
83
85
  debug(env) do
84
86
  "Preflight Headers:\n" +
85
- headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
87
+ headers.collect { |kv| " #{kv.join(': ')}" }.join("\n")
86
88
  end
87
89
  return [200, headers, []]
88
90
  else
@@ -103,9 +105,7 @@ module Rack
103
105
  headers = add_headers.merge(headers)
104
106
  debug(env) do
105
107
  add_headers.each_pair do |key, value|
106
- if headers.has_key?(key)
107
- headers["X-Rack-CORS-Original-#{key}"] = value
108
- end
108
+ headers["x-rack-cors-original-#{key}"] = value if headers.key?(key)
109
109
  end
110
110
  end
111
111
  end
@@ -114,359 +114,106 @@ module Rack
114
114
  # response to be different depending on the Origin header value.
115
115
  # Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
116
116
  if vary_resource
117
- vary = headers[VARY]
118
- cors_vary_headers = if vary_resource.vary_headers && 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(', ')
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(', ')
124
124
  end
125
125
 
126
- if debug? && result = env[RACK_CORS]
127
- result.append_header(headers)
128
- end
126
+ result = env[ENV_KEY]
127
+ result.append_header(headers) if debug? && result
129
128
 
130
129
  [status, headers, body]
131
130
  end
132
131
 
133
132
  protected
134
- def debug(env, message = nil, &block)
135
- (@logger || select_logger(env)).debug(message, &block) if debug?
136
- end
137
133
 
138
- def select_logger(env)
139
- @logger = if @logger_proc
140
- logger_proc = @logger_proc
141
- @logger_proc = nil
142
- logger_proc.call
134
+ def debug(env, message = nil, &block)
135
+ (@logger || select_logger(env)).debug(message, &block) if debug?
136
+ end
143
137
 
144
- elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
145
- Rails.logger
138
+ def select_logger(env)
139
+ @logger = if @logger_proc
140
+ logger_proc = @logger_proc
141
+ @logger_proc = nil
142
+ logger_proc.call
146
143
 
147
- elsif env[RACK_LOGGER]
148
- env[RACK_LOGGER]
144
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
145
+ Rails.logger
149
146
 
150
- else
151
- ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
152
- end
153
- end
147
+ elsif env[RACK_LOGGER]
148
+ env[RACK_LOGGER]
154
149
 
155
- def evaluate_path(env)
156
- path = env[PATH_INFO]
150
+ else
151
+ ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
152
+ end
153
+ end
157
154
 
158
- if path
159
- path = Rack::Utils.unescape_path(path)
155
+ def evaluate_path(env)
156
+ path = env[PATH_INFO]
160
157
 
161
- if Rack::Utils.valid_path?(path)
162
- path = Rack::Utils.clean_path_info(path)
163
- end
164
- end
158
+ if path
159
+ path = Rack::Utils.unescape_path(path)
165
160
 
166
- path
161
+ path = Rack::Utils.clean_path_info(path) if Rack::Utils.valid_path?(path)
167
162
  end
168
163
 
169
- def all_resources
170
- @all_resources ||= []
171
- end
164
+ path
165
+ end
172
166
 
173
- def process_preflight(env, path)
174
- result = Result.preflight(env)
167
+ def all_resources
168
+ @all_resources ||= []
169
+ end
175
170
 
176
- resource, error = match_resource(path, env)
177
- unless resource
178
- result.miss(error)
179
- return {}
180
- end
171
+ def process_preflight(env, path)
172
+ result = Result.preflight(env)
181
173
 
182
- return resource.process_preflight(env, result)
174
+ resource, error = match_resource(path, env)
175
+ unless resource
176
+ result.miss(error)
177
+ return {}
183
178
  end
184
179
 
185
- def process_cors(env, path)
186
- resource, error = match_resource(path, env)
187
- if resource
188
- Result.hit(env)
189
- cors = resource.to_headers(env)
190
- cors
180
+ resource.process_preflight(env, result)
181
+ end
191
182
 
192
- else
193
- Result.miss(env, error)
194
- nil
195
- end
196
- end
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
197
189
 
198
- def resource_for_path(path_info)
199
- all_resources.each do |r|
200
- if found = r.resource_for_path(path_info)
201
- return found
202
- end
203
- end
190
+ else
191
+ Result.miss(env, error)
204
192
  nil
205
193
  end
194
+ end
206
195
 
207
- def match_resource(path, env)
208
- origin = env[HTTP_ORIGIN]
209
-
210
- origin_matched = false
211
- all_resources.each do |r|
212
- if r.allow_origin?(origin, env)
213
- origin_matched = true
214
- if found = r.match_resource(path, env)
215
- return [found, nil]
216
- end
217
- end
218
- end
219
-
220
- [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
221
- end
222
-
223
- class Result
224
- HEADER_KEY = 'X-Rack-CORS'.freeze
225
-
226
- MISS_NO_ORIGIN = 'no-origin'.freeze
227
- MISS_NO_PATH = 'no-path'.freeze
228
-
229
- MISS_NO_METHOD = 'no-method'.freeze
230
- MISS_DENY_METHOD = 'deny-method'.freeze
231
- MISS_DENY_HEADER = 'deny-header'.freeze
232
-
233
- attr_accessor :preflight, :hit, :miss_reason
234
-
235
- def hit?
236
- !!hit
237
- end
238
-
239
- def preflight?
240
- !!preflight
241
- end
242
-
243
- def miss(reason)
244
- self.hit = false
245
- self.miss_reason = reason
246
- end
247
-
248
- def self.hit(env)
249
- r = Result.new
250
- r.preflight = false
251
- r.hit = true
252
- env[RACK_CORS] = r
253
- end
254
-
255
- def self.miss(env, reason)
256
- r = Result.new
257
- r.preflight = false
258
- r.hit = false
259
- r.miss_reason = reason
260
- env[RACK_CORS] = r
261
- end
262
-
263
- def self.preflight(env)
264
- r = Result.new
265
- r.preflight = true
266
- env[RACK_CORS] = r
267
- end
268
-
269
-
270
- def append_header(headers)
271
- headers[HEADER_KEY] = if hit?
272
- preflight? ? 'preflight-hit' : 'hit'
273
- else
274
- [
275
- (preflight? ? 'preflight-miss' : 'miss'),
276
- miss_reason
277
- ].join('; ')
278
- end
279
- end
280
- end
281
-
282
- class Resources
283
-
284
- attr_reader :resources
285
-
286
- def initialize
287
- @origins = []
288
- @resources = []
289
- @public_resources = false
290
- end
291
-
292
- def origins(*args, &blk)
293
- @origins = args.flatten.reject{ |s| s == '' }.map do |n|
294
- case n
295
- when Proc,
296
- Regexp,
297
- /^https?:\/\//,
298
- 'file://' then n
299
- when '*' then @public_resources = true; n
300
- else Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
301
- end
302
- end.flatten
303
- @origins.push(blk) if blk
304
- end
305
-
306
- def resource(path, opts={})
307
- @resources << Resource.new(public_resources?, path, opts)
308
- end
309
-
310
- def public_resources?
311
- @public_resources
312
- end
313
-
314
- def allow_origin?(source,env = {})
315
- return true if public_resources?
316
-
317
- return !! @origins.detect do |origin|
318
- if origin.is_a?(Proc)
319
- origin.call(source,env)
320
- else
321
- origin === source
322
- end
323
- end
324
- end
325
-
326
- def match_resource(path, env)
327
- @resources.detect { |r| r.match?(path, env) }
328
- end
329
-
330
- def resource_for_path(path)
331
- @resources.detect { |r| r.matches_path?(path) }
332
- end
333
-
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
334
200
  end
201
+ nil
202
+ end
335
203
 
336
- class Resource
337
- class CorsMisconfigurationError < StandardError
338
- def message
339
- "Allowing credentials for wildcard origins is insecure."\
340
- " Please specify more restrictive origins or set 'credentials' to false in your CORS configuration."
341
- end
342
- end
343
-
344
- attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern, :if_proc, :vary_headers
345
-
346
- def initialize(public_resource, path, opts={})
347
- raise CorsMisconfigurationError if public_resource && opts[:credentials] == true
348
-
349
- self.path = path
350
- self.credentials = public_resource ? false : (opts[:credentials] == true)
351
- self.max_age = opts[:max_age] || 7200
352
- self.pattern = compile(path)
353
- self.if_proc = opts[:if]
354
- self.vary_headers = opts[:vary] && [opts[:vary]].flatten
355
- @public_resource = public_resource
356
-
357
- self.headers = case opts[:headers]
358
- when :any then :any
359
- when nil then nil
360
- else
361
- [opts[:headers]].flatten.collect{|h| h.downcase}
362
- end
363
-
364
- self.methods = case opts[:methods]
365
- when :any then [:get, :head, :post, :put, :patch, :delete, :options]
366
- else
367
- ensure_enum(opts[:methods]) || [:get]
368
- end.map{|e| e.to_s }
369
-
370
- self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
371
- end
372
-
373
- def matches_path?(path)
374
- pattern =~ path
375
- end
376
-
377
- def match?(path, env)
378
- matches_path?(path) && (if_proc.nil? || if_proc.call(env))
379
- end
380
-
381
- def process_preflight(env, result)
382
- headers = {}
383
-
384
- request_method = env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
385
- if request_method.nil?
386
- result.miss(Result::MISS_NO_METHOD) and return headers
387
- end
388
- if !methods.include?(request_method.downcase)
389
- result.miss(Result::MISS_DENY_METHOD) and return headers
390
- end
391
-
392
- request_headers = env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
393
- if request_headers && !allow_headers?(request_headers)
394
- result.miss(Result::MISS_DENY_HEADER) and return headers
395
- end
396
-
397
- result.hit = true
398
- headers.merge(to_preflight_headers(env))
399
- end
400
-
401
- def to_headers(env)
402
- h = {
403
- 'Access-Control-Allow-Origin' => origin_for_response_header(env[HTTP_ORIGIN]),
404
- 'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
405
- 'Access-Control-Expose-Headers' => expose.nil? ? '' : expose.join(', '),
406
- 'Access-Control-Max-Age' => max_age.to_s }
407
- h['Access-Control-Allow-Credentials'] = 'true' if credentials
408
- h
409
- end
410
-
411
- protected
412
- def public_resource?
413
- @public_resource
414
- end
415
-
416
- def origin_for_response_header(origin)
417
- return '*' if public_resource?
418
- origin
419
- end
420
-
421
- def to_preflight_headers(env)
422
- h = to_headers(env)
423
- if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
424
- h.merge!('Access-Control-Allow-Headers' => env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS])
425
- end
426
- h
427
- end
428
-
429
- def allow_headers?(request_headers)
430
- headers = self.headers || []
431
- if headers == :any
432
- return true
433
- end
434
- request_headers = request_headers.split(/,\s*/) if request_headers.kind_of?(String)
435
- request_headers.all? do |header|
436
- header = header.downcase
437
- CORS_SIMPLE_HEADERS.include?(header) || headers.include?(header)
438
- end
439
- end
204
+ def match_resource(path, env)
205
+ origin = env[HTTP_ORIGIN]
440
206
 
441
- def ensure_enum(v)
442
- return nil if v.nil?
443
- [v].flatten
444
- end
207
+ origin_matched = false
208
+ all_resources.each do |r|
209
+ next unless r.allow_origin?(origin, env)
445
210
 
446
- def compile(path)
447
- if path.respond_to? :to_str
448
- special_chars = %w{. + ( )}
449
- pattern =
450
- path.to_str.gsub(/((:\w+)|\/\*|[\*#{special_chars.join}])/) do |match|
451
- case match
452
- when "/*"
453
- "\\/?(.*?)"
454
- when "*"
455
- "(.*?)"
456
- when *special_chars
457
- Regexp.escape(match)
458
- else
459
- "([^/?&#]+)"
460
- end
461
- end
462
- /^#{pattern}$/
463
- elsif path.respond_to? :match
464
- path
465
- else
466
- raise TypeError, path
467
- end
468
- end
211
+ origin_matched = true
212
+ found = r.match_resource(path, env)
213
+ return [found, nil] if found
469
214
  end
470
215
 
216
+ [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
217
+ end
471
218
  end
472
219
  end
data/rack-cors.gemspec CHANGED
@@ -1,27 +1,30 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'rack/cors/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "rack-cors"
8
+ spec.name = 'rack-cors'
8
9
  spec.version = Rack::Cors::VERSION
9
- spec.authors = ["Calvin Yu"]
10
- spec.email = ["me@sourcebender.com"]
11
- spec.description = %q{Middleware that will make Rack-based apps CORS compatible. Fork the project here: https://github.com/cyu/rack-cors}
12
- spec.summary = %q{Middleware for enabling Cross-Origin Resource Sharing in Rack apps}
13
- spec.homepage = "https://github.com/cyu/rack-cors"
14
- spec.license = "MIT"
10
+ spec.authors = ['Calvin Yu']
11
+ spec.email = ['me@sourcebender.com']
12
+ spec.description = 'Middleware that will make Rack-based apps CORS compatible. Fork the project here: https://github.com/cyu/rack-cors'
13
+ spec.summary = 'Middleware for enabling Cross-Origin Resource Sharing in Rack apps'
14
+ spec.homepage = 'https://github.com/cyu/rack-cors'
15
+ spec.license = 'MIT'
15
16
 
16
- spec.files = `git ls-files`.split($/).reject { |f| f == '.gitignore' or f =~ /^examples/ }
17
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject { |f| (f == '.gitignore') || f =~ /^examples/ }
17
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
+ spec.require_paths = ['lib']
20
21
 
21
- spec.add_dependency "rack", ">= 2.0.0"
22
- spec.add_development_dependency "bundler", ">= 1.16.0", '< 3'
23
- spec.add_development_dependency "rake", "~> 12.3.0"
24
- spec.add_development_dependency "minitest", "~> 5.11.0"
25
- spec.add_development_dependency "mocha", "~> 1.6.0"
26
- spec.add_development_dependency "rack-test", "~> 1.1.0"
22
+ spec.add_dependency 'rack', '>= 2.0.0'
23
+ spec.add_development_dependency 'bundler', '>= 1.16.0', '< 3'
24
+ spec.add_development_dependency 'minitest', '~> 5.11.0'
25
+ spec.add_development_dependency 'mocha', '~> 1.6.0'
26
+ spec.add_development_dependency 'pry', '~> 0.12'
27
+ spec.add_development_dependency 'rack-test', '~> 1.1.0'
28
+ spec.add_development_dependency 'rake', '~> 12.3.0'
29
+ spec.add_development_dependency 'rubocop', '~> 0.80.1'
27
30
  end
data/test/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ inherit_from: ../.rubocop.yml
3
+
4
+ # Disables
5
+ Style/ClassAndModuleChildren:
6
+ Enabled: false
7
+ Security/Eval:
8
+ Enabled: false
@@ -1,4 +1,6 @@
1
- CORS_SERVER = '127.0.0.1.xip.io:9292'
1
+ CORS_SERVER = '127.0.0.1.xip.io:3000'
2
+
3
+ mocha.setup({ignoreLeaks: true});
2
4
 
3
5
  describe 'CORS', ->
4
6
 
@@ -34,7 +36,7 @@ describe 'CORS', ->
34
36
 
35
37
  it 'should allow access to static resource', (done) ->
36
38
  $.get "http://#{CORS_SERVER}/static.txt", (data, status, xhr) ->
37
- expect($.trim(data)).to.eql("hello world")
39
+ expect($.trim(data)).to.eql("Hello world")
38
40
  done()
39
41
 
40
42
  it 'should allow post resource', (done) ->
@@ -2,7 +2,11 @@
2
2
  (function() {
3
3
  var CORS_SERVER;
4
4
 
5
- CORS_SERVER = '127.0.0.1.xip.io:9292';
5
+ CORS_SERVER = '127.0.0.1.xip.io:3000';
6
+
7
+ mocha.setup({
8
+ ignoreLeaks: true
9
+ });
6
10
 
7
11
  describe('CORS', function() {
8
12
  it('should allow access to dynamic resource', function(done) {
@@ -53,7 +57,7 @@
53
57
  });
54
58
  it('should allow access to static resource', function(done) {
55
59
  return $.get(`http://${CORS_SERVER}/static.txt`, function(data, status, xhr) {
56
- expect($.trim(data)).to.eql("hello world");
60
+ expect($.trim(data)).to.eql("Hello world");
57
61
  return done();
58
62
  });
59
63
  });