rack-cors 1.0.5 → 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,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
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", ">= 1.6.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
  });