rack-cors 0.4.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Cors
5
+ class Result
6
+ HEADER_KEY = 'x-rack-cors'
7
+
8
+ MISS_NO_ORIGIN = 'no-origin'
9
+ MISS_NO_PATH = 'no-path'
10
+
11
+ MISS_NO_METHOD = 'no-method'
12
+ MISS_DENY_METHOD = 'deny-method'
13
+ MISS_DENY_HEADER = 'deny-header'
14
+
15
+ attr_accessor :preflight, :hit, :miss_reason
16
+
17
+ def hit?
18
+ !!hit
19
+ end
20
+
21
+ def preflight?
22
+ !!preflight
23
+ end
24
+
25
+ def miss(reason)
26
+ self.hit = false
27
+ self.miss_reason = reason
28
+ end
29
+
30
+ def self.hit(env)
31
+ r = Result.new
32
+ r.preflight = false
33
+ r.hit = true
34
+ env[Rack::Cors::ENV_KEY] = r
35
+ end
36
+
37
+ def self.miss(env, reason)
38
+ r = Result.new
39
+ r.preflight = false
40
+ r.hit = false
41
+ r.miss_reason = reason
42
+ env[Rack::Cors::ENV_KEY] = r
43
+ end
44
+
45
+ def self.preflight(env)
46
+ r = Result.new
47
+ r.preflight = true
48
+ env[Rack::Cors::ENV_KEY] = r
49
+ end
50
+
51
+ def append_header(headers)
52
+ headers[HEADER_KEY] = if hit?
53
+ preflight? ? 'preflight-hit' : 'hit'
54
+ else
55
+ [
56
+ (preflight? ? 'preflight-miss' : 'miss'),
57
+ miss_reason
58
+ ].join('; ')
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Cors
3
- VERSION = "0.4.1"
5
+ VERSION = '2.0.1'
4
6
  end
5
7
  end
data/lib/rack/cors.rb CHANGED
@@ -1,21 +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
- ENV_KEY = 'rack.cors'.freeze
11
+ HTTP_ORIGIN = 'HTTP_ORIGIN'
12
+ HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'
13
+
14
+ HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'
15
+ HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'
16
+
17
+ PATH_INFO = 'PATH_INFO'
18
+ REQUEST_METHOD = 'REQUEST_METHOD'
19
+
20
+ RACK_LOGGER = 'rack.logger'
21
+ RACK_CORS =
22
+ # retaining the old key for backwards compatibility
23
+ ENV_KEY = 'rack.cors'
6
24
 
7
- ORIGIN_HEADER_KEY = 'HTTP_ORIGIN'.freeze
8
- ORIGIN_X_HEADER_KEY = 'HTTP_X_ORIGIN'.freeze
9
- PATH_INFO_HEADER_KEY = 'PATH_INFO'.freeze
10
- VARY_HEADER_KEY = 'Vary'.freeze
11
- DEFAULT_VARY_HEADERS = ['Origin'].freeze
25
+ OPTIONS = 'OPTIONS'
12
26
 
13
- def initialize(app, opts={}, &block)
27
+ DEFAULT_VARY_HEADERS = ['Origin'].freeze
28
+
29
+ def initialize(app, opts = {}, &block)
14
30
  @app = app
15
31
  @debug_mode = !!opts[:debug]
16
32
  @logger = @logger_proc = nil
17
33
 
18
- if logger = opts[:logger]
34
+ logger = opts[:logger]
35
+ if logger
19
36
  if logger.respond_to? :call
20
37
  @logger_proc = opts[:logger]
21
38
  else
@@ -23,12 +40,12 @@ module Rack
23
40
  end
24
41
  end
25
42
 
26
- if block_given?
27
- if block.arity == 1
28
- block.call(self)
29
- else
30
- instance_eval(&block)
31
- end
43
+ return unless block_given?
44
+
45
+ if block.arity == 1
46
+ block.call(self)
47
+ else
48
+ instance_eval(&block)
32
49
  end
33
50
  end
34
51
 
@@ -47,36 +64,40 @@ module Rack
47
64
  end
48
65
 
49
66
  def call(env)
50
- env[ORIGIN_HEADER_KEY] ||= env[ORIGIN_X_HEADER_KEY] if env[ORIGIN_X_HEADER_KEY]
67
+ env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
68
+
69
+ path = evaluate_path(env)
51
70
 
52
71
  add_headers = nil
53
- if env[ORIGIN_HEADER_KEY]
72
+ if env[HTTP_ORIGIN]
54
73
  debug(env) do
55
- [ 'Incoming Headers:',
56
- " Origin: #{env[ORIGIN_HEADER_KEY]}",
57
- " Access-Control-Request-Method: #{env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']}",
58
- " Access-Control-Request-Headers: #{env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"
59
- ].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")
60
79
  end
61
- if env['REQUEST_METHOD'] == 'OPTIONS' and env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
62
- if headers = process_preflight(env)
63
- debug(env) do
64
- "Preflight Headers:\n" +
65
- headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
66
- end
67
- return [200, headers, []]
80
+
81
+ if env[REQUEST_METHOD] == OPTIONS && env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
82
+ return [400, {}, []] unless Rack::Utils.valid_path?(path)
83
+
84
+ headers = process_preflight(env, path)
85
+ debug(env) do
86
+ "Preflight Headers:\n" +
87
+ headers.collect { |kv| " #{kv.join(': ')}" }.join("\n")
68
88
  end
89
+ return [200, headers, []]
69
90
  else
70
- add_headers = process_cors(env)
91
+ add_headers = process_cors(env, path)
71
92
  end
72
93
  else
73
94
  Result.miss(env, Result::MISS_NO_ORIGIN)
74
95
  end
75
96
 
76
97
  # This call must be done BEFORE calling the app because for some reason
77
- # env[PATH_INFO_HEADER_KEY] gets changed after that and it won't match.
78
- # (At least in rails 4.1.6)
79
- vary_resource = resource_for_path(env[PATH_INFO_HEADER_KEY])
98
+ # env[PATH_INFO] gets changed after that and it won't match. (At least
99
+ # in rails 4.1.6)
100
+ vary_resource = resource_for_path(path)
80
101
 
81
102
  status, headers, body = @app.call env
82
103
 
@@ -84,9 +105,7 @@ module Rack
84
105
  headers = add_headers.merge(headers)
85
106
  debug(env) do
86
107
  add_headers.each_pair do |key, value|
87
- if headers.has_key?(key)
88
- headers["X-Rack-CORS-Original-#{key}"] = value
89
- end
108
+ headers["x-rack-cors-original-#{key}"] = value if headers.key?(key)
90
109
  end
91
110
  end
92
111
  end
@@ -95,324 +114,106 @@ module Rack
95
114
  # response to be different depending on the Origin header value.
96
115
  # Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
97
116
  if vary_resource
98
- vary = headers[VARY_HEADER_KEY]
99
- cors_vary_headers = if vary_resource.vary_headers && vary_resource.vary_headers.any?
100
- vary_resource.vary_headers
101
- else
102
- DEFAULT_VARY_HEADERS
103
- end
104
- headers[VARY_HEADER_KEY] = ((vary ? vary.split(/,\s*/) : []) + 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(', ')
105
124
  end
106
125
 
107
- if debug? && result = env[ENV_KEY]
108
- result.append_header(headers)
109
- end
126
+ result = env[ENV_KEY]
127
+ result.append_header(headers) if debug? && result
110
128
 
111
129
  [status, headers, body]
112
130
  end
113
131
 
114
132
  protected
115
- def debug(env, message = nil, &block)
116
- (@logger || select_logger(env)).debug(message, &block) if debug?
117
- end
118
-
119
- def select_logger(env)
120
- @logger = if @logger_proc
121
- logger_proc = @logger_proc
122
- @logger_proc = nil
123
- logger_proc.call
124
-
125
- elsif defined?(Rails) && Rails.logger
126
- Rails.logger
127
-
128
- elsif env['rack.logger']
129
- env['rack.logger']
130
133
 
131
- else
132
- ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
133
- end
134
- end
135
-
136
- def all_resources
137
- @all_resources ||= []
138
- end
134
+ def debug(env, message = nil, &block)
135
+ (@logger || select_logger(env)).debug(message, &block) if debug?
136
+ end
139
137
 
140
- def process_preflight(env)
141
- resource, error = match_resource(env)
142
- if resource
143
- Result.preflight_hit(env)
144
- preflight = resource.process_preflight(env)
145
- preflight
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
- else
148
- Result.preflight_miss(env, error)
149
- nil
150
- end
151
- end
144
+ elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
145
+ Rails.logger
152
146
 
153
- def process_cors(env)
154
- resource, error = match_resource(env)
155
- if resource
156
- Result.hit(env)
157
- cors = resource.to_headers(env)
158
- cors
147
+ elsif env[RACK_LOGGER]
148
+ env[RACK_LOGGER]
159
149
 
160
- else
161
- Result.miss(env, error)
162
- nil
163
- end
164
- end
150
+ else
151
+ ::Logger.new(STDOUT).tap { |logger| logger.level = ::Logger::Severity::DEBUG }
152
+ end
153
+ end
165
154
 
166
- def resource_for_path(path_info)
167
- all_resources.each do |r|
168
- if found = r.resource_for_path(path_info)
169
- return found
170
- end
171
- end
172
- nil
173
- end
155
+ def evaluate_path(env)
156
+ path = env[PATH_INFO]
174
157
 
175
- def match_resource(env)
176
- path = env[PATH_INFO_HEADER_KEY]
177
- origin = env[ORIGIN_HEADER_KEY]
178
-
179
- origin_matched = false
180
- all_resources.each do |r|
181
- if r.allow_origin?(origin, env)
182
- origin_matched = true
183
- if found = r.match_resource(path, env)
184
- return [found, nil]
185
- end
186
- end
187
- end
158
+ if path
159
+ path = Rack::Utils.unescape_path(path)
188
160
 
189
- [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
161
+ path = Rack::Utils.clean_path_info(path) if Rack::Utils.valid_path?(path)
190
162
  end
191
163
 
192
- class Result
193
- HEADER_KEY = 'X-Rack-CORS'.freeze
194
-
195
- MISS_NO_ORIGIN = 'no-origin'.freeze
196
- MISS_NO_PATH = 'no-path'.freeze
197
-
198
- attr_accessor :preflight, :hit, :miss_reason
199
-
200
- def hit?
201
- !!hit
202
- end
203
-
204
- def preflight?
205
- !!preflight
206
- end
207
-
208
- def self.hit(env)
209
- r = Result.new
210
- r.preflight = false
211
- r.hit = true
212
- env[ENV_KEY] = r
213
- end
214
-
215
- def self.miss(env, reason)
216
- r = Result.new
217
- r.preflight = false
218
- r.hit = false
219
- r.miss_reason = reason
220
- env[ENV_KEY] = r
221
- end
164
+ path
165
+ end
222
166
 
223
- def self.preflight_hit(env)
224
- r = Result.new
225
- r.preflight = true
226
- r.hit = true
227
- env[ENV_KEY] = r
228
- end
167
+ def all_resources
168
+ @all_resources ||= []
169
+ end
229
170
 
230
- def self.preflight_miss(env, reason)
231
- r = Result.new
232
- r.preflight = true
233
- r.hit = false
234
- r.miss_reason = reason
235
- env[ENV_KEY] = r
236
- end
171
+ def process_preflight(env, path)
172
+ result = Result.preflight(env)
237
173
 
238
- def append_header(headers)
239
- headers[HEADER_KEY] = if hit?
240
- preflight? ? 'preflight-hit' : 'hit'
241
- else
242
- [
243
- (preflight? ? 'preflight-miss' : 'miss'),
244
- miss_reason
245
- ].join('; ')
246
- end
247
- end
174
+ resource, error = match_resource(path, env)
175
+ unless resource
176
+ result.miss(error)
177
+ return {}
248
178
  end
249
179
 
250
- class Resources
251
- def initialize
252
- @origins = []
253
- @resources = []
254
- @public_resources = false
255
- end
256
-
257
- def origins(*args, &blk)
258
- @origins = args.flatten.collect do |n|
259
- case n
260
- when Regexp,
261
- /^https?:\/\//,
262
- 'file://' then n
263
- when '*' then @public_resources = true; n
264
- else Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
265
- end
266
- end.flatten
267
- @origins.push(blk) if blk
268
- end
269
-
270
- def resource(path, opts={})
271
- @resources << Resource.new(public_resources?, path, opts)
272
- end
273
-
274
- def public_resources?
275
- @public_resources
276
- end
277
-
278
- def allow_origin?(source,env = {})
279
- return true if public_resources?
280
-
281
- effective_source = (source == 'null' ? 'file://' : source)
282
-
283
- return !! @origins.detect do |origin|
284
- if origin.is_a?(Proc)
285
- origin.call(source,env)
286
- else
287
- origin === effective_source
288
- end
289
- end
290
- end
291
-
292
- def match_resource(path, env)
293
- @resources.detect { |r| r.match?(path, env) }
294
- end
180
+ resource.process_preflight(env, result)
181
+ end
295
182
 
296
- def resource_for_path(path)
297
- @resources.detect { |r| r.matches_path?(path) }
298
- 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
299
189
 
190
+ else
191
+ Result.miss(env, error)
192
+ nil
300
193
  end
194
+ end
301
195
 
302
- class Resource
303
- attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern, :if_proc, :vary_headers
304
-
305
- def initialize(public_resource, path, opts={})
306
- self.path = path
307
- self.credentials = opts[:credentials].nil? ? true : opts[:credentials]
308
- self.max_age = opts[:max_age] || 1728000
309
- self.pattern = compile(path)
310
- self.if_proc = opts[:if]
311
- self.vary_headers = opts[:vary] && [opts[:vary]].flatten
312
- @public_resource = public_resource
313
-
314
- self.headers = case opts[:headers]
315
- when :any then :any
316
- when nil then nil
317
- else
318
- [opts[:headers]].flatten.collect{|h| h.downcase}
319
- end
320
-
321
- self.methods = case opts[:methods]
322
- when :any then [:get, :head, :post, :put, :patch, :delete, :options]
323
- else
324
- ensure_enum(opts[:methods]) || [:get]
325
- end.map{|e| e.to_s }
326
-
327
- self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
328
- end
329
-
330
- def matches_path?(path)
331
- pattern =~ path
332
- end
333
-
334
- def match?(path, env)
335
- matches_path?(path) && (if_proc.nil? || if_proc.call(env))
336
- end
337
-
338
- def process_preflight(env)
339
- return nil if invalid_method_request?(env) || invalid_headers_request?(env)
340
- {'Content-Type' => 'text/plain'}.merge(to_preflight_headers(env))
341
- end
342
-
343
- def to_headers(env)
344
- h = {
345
- 'Access-Control-Allow-Origin' => origin_for_response_header(env[ORIGIN_HEADER_KEY]),
346
- 'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
347
- 'Access-Control-Expose-Headers' => expose.nil? ? '' : expose.join(', '),
348
- 'Access-Control-Max-Age' => max_age.to_s }
349
- h['Access-Control-Allow-Credentials'] = 'true' if credentials
350
- h
351
- end
352
-
353
- protected
354
- def public_resource?
355
- @public_resource
356
- end
357
-
358
- def origin_for_response_header(origin)
359
- return '*' if public_resource? && !credentials
360
- origin
361
- end
362
-
363
- def to_preflight_headers(env)
364
- h = to_headers(env)
365
- if env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
366
- h.merge!('Access-Control-Allow-Headers' => env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])
367
- end
368
- h
369
- end
370
-
371
- def invalid_method_request?(env)
372
- request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
373
- request_method.nil? || !methods.include?(request_method.downcase)
374
- end
375
-
376
- def invalid_headers_request?(env)
377
- request_headers = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
378
- request_headers && !allow_headers?(request_headers)
379
- end
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
200
+ end
201
+ nil
202
+ end
380
203
 
381
- def allow_headers?(request_headers)
382
- return false if headers.nil?
383
- headers == :any || begin
384
- request_headers = request_headers.split(/,\s*/) if request_headers.kind_of?(String)
385
- request_headers.all?{|h| headers.include?(h.downcase)}
386
- end
387
- end
204
+ def match_resource(path, env)
205
+ origin = env[HTTP_ORIGIN]
388
206
 
389
- def ensure_enum(v)
390
- return nil if v.nil?
391
- [v].flatten
392
- end
207
+ origin_matched = false
208
+ all_resources.each do |r|
209
+ next unless r.allow_origin?(origin, env)
393
210
 
394
- def compile(path)
395
- if path.respond_to? :to_str
396
- special_chars = %w{. + ( )}
397
- pattern =
398
- path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
399
- case match
400
- when "*"
401
- "(.*?)"
402
- when *special_chars
403
- Regexp.escape(match)
404
- else
405
- "([^/?&#]+)"
406
- end
407
- end
408
- /^#{pattern}$/
409
- elsif path.respond_to? :match
410
- path
411
- else
412
- raise TypeError, path
413
- end
414
- end
211
+ origin_matched = true
212
+ found = r.match_resource(path, env)
213
+ return [found, nil] if found
415
214
  end
416
215
 
216
+ [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
217
+ end
417
218
  end
418
219
  end
data/rack-cors.gemspec CHANGED
@@ -1,26 +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. Read more here: http://blog.sourcebender.com/2010/06/09/introducin-rack-cors.html. 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_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "minitest", ">= 5.3.0"
24
- spec.add_development_dependency "mocha", ">= 0.14.0"
25
- spec.add_development_dependency "rack-test", ">= 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'
26
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