rack-cors 1.1.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack-cors might be problematic. Click here for more details.

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
  });