rack-cors 0.4.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.

@@ -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