rack-cors 0.2.9 → 1.0.5

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.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- OTQyY2Q0OGNmOGZjMWNhOTc0OTdmOTY3ZTk0NDY0NzhmODE5YTI5Yg==
5
- data.tar.gz: !binary |-
6
- ZDQyMTJlMTg3MDczMmE0ZjFkMmZjOGExMGU3NDE0NmQ2MGY1YzBlZQ==
2
+ SHA256:
3
+ metadata.gz: a12cdfc5aca2abf0cf86fb1ca217619fa6b40cad19721118016e064554f46ba0
4
+ data.tar.gz: 2874199b748909fdfd3e8ec601bd8620bc0235e60c66226259a79ff2404dbaf8
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- NGViNTEzOTIzZjc2ZjVhNjZkZDBmODQ5NzAzNzk3ZThiYzZkMGU2YTU3MmUx
10
- NDc3ODlkMTIwNjFmNGZhOWUwZmNjMmEyN2YxZWQ1NTA3NDU3NjEzMGYyNDdi
11
- NTM4ZTI3NmQ4ZTE1MmNjOGY5ZTBlNDk2YTQyZDQ0NzA5ZTc2NTE=
12
- data.tar.gz: !binary |-
13
- NGIwN2QwMjQ1OTQwNzY3M2MzYWFiNWI0Y2RjMjVmMTEzMTk3YjE1ZGJhNjQw
14
- YzRjNzhkZTRmZGExYjU2YzU5Y2Q4OTkyNzAyYTFhODRmNGViOGI0MTQyNmNh
15
- MjAwYjUxZjRkMGZjMjRmMDJhYjRiOWU2ZmNjNTFjNmEzZTY2YWQ=
6
+ metadata.gz: 2b71fe191ad396ab85e8c1966e979fa3516ee768bae6ed93fd1d43644eada8a455dbab00990ef22440ee7f82dab16a37b283897403d4eba674547bda1f0b86f5
7
+ data.tar.gz: a31481b3f6d9d45bdc522c444e923438f7f513a57796bf2cf6eaaa665d87f7479bf5f1e5f5ea8d380ce7194f0d3690823e1a681e55c17317cab29bf87b7a7303
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ sudo: false
3
+ rvm:
4
+ - 2.2
5
+ - 2.3
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
data/CHANGELOG.md ADDED
@@ -0,0 +1,73 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ ## 1.0.5 - 2019-11-14
5
+ ### Changed
6
+ - Update Gem spec to require rack >= 1.6.0
7
+
8
+ ## 1.0.4 - 2019-11-13
9
+ ### Security
10
+ - Escape and resolve path before evaluating resource rules (thanks to Colby Morgan)
11
+
12
+ ## 1.0.3 - 2019-03-24
13
+ ### Changed
14
+ - Don't send 'Content-Type' header with pre-flight requests
15
+ - Allow ruby array for vary header config
16
+
17
+ ## 1.0.2 - 2017-10-22
18
+ ### Fixed
19
+ - Automatically allow simple headers when headers are set
20
+
21
+ ## 1.0.1 - 2017-07-18
22
+ ### Fixed
23
+ - Allow lambda origin configuration
24
+
25
+ ## 1.0.0 - 2017-07-15
26
+ ### Security
27
+ - Don't implicitly accept 'null' origins when 'file://' is specified
28
+ (https://github.com/cyu/rack-cors/pull/134)
29
+ - Ignore '' origins (https://github.com/cyu/rack-cors/issues/139)
30
+ - Default credentials option on resources to false
31
+ (https://github.com/cyu/rack-cors/issues/95)
32
+ - Don't allow credentials option to be true if '*' is specified is origin
33
+ (https://github.com/cyu/rack-cors/pull/142)
34
+ - Don't reflect Origin header when '*' is specified as origin
35
+ (https://github.com/cyu/rack-cors/pull/142)
36
+
37
+ ### Fixed
38
+ - Don't respond immediately on non-matching preflight requests instead of
39
+ sending them through the app (https://github.com/cyu/rack-cors/pull/106)
40
+
41
+ ## 0.4.1 - 2017-02-01
42
+ ### Fixed
43
+ - Return miss result in X-Rack-CORS instead of incorrectly returning
44
+ preflight-hit
45
+
46
+ ## 0.4.0 - 2015-04-15
47
+ ### Changed
48
+ - Don't set HTTP_ORIGIN with HTTP_X_ORIGIN if nil
49
+
50
+ ### Added
51
+ - Calculate vary headers for non-CORS resources
52
+ - Support custom vary headers for resource
53
+ - Support :if option for resource
54
+ - Support :any as a possible value for :methods option
55
+
56
+ ### Fixed
57
+ - Don't symbolize incoming HTTP request methods
58
+
59
+ ## 0.3.1 - 2014-12-27
60
+ ### Changed
61
+ - Changed the env key to rack.cors to avoid Rack::Lint warnings
62
+
63
+ ## 0.3.0 - 2014-10-19
64
+ ### Added
65
+ - Added support for defining a logger with a Proc
66
+ - Return a X-Rack-CORS header when in debug mode detailing how Rack::Cors
67
+ processed a request
68
+ - Added support for non HTTP/HTTPS origins when just a domain is specified
69
+
70
+ ### Changed
71
+ - Changed the log level of the fallback logger to DEBUG
72
+ - Print warning when attempting to use :any as an allowed method
73
+ - Treat incoming `Origin: null` headers as file://
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in rack-cors.gemspec
4
4
  gemspec
5
+
6
+ gem 'pry-byebug', '~> 3.6.0'
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # Rack CORS Middleware [![Build Status](https://travis-ci.org/cyu/rack-cors.svg?branch=master)](https://travis-ci.org/cyu/rack-cors)
2
+
3
+ `Rack::Cors` provides support for Cross-Origin Resource Sharing (CORS) for Rack compatible web applications.
4
+
5
+ The [CORS spec](http://www.w3.org/TR/cors/) allows web applications to make cross domain AJAX calls without using workarounds such as JSONP. See [Cross-domain Ajax with Cross-Origin Resource Sharing](http://www.nczonline.net/blog/2010/05/25/cross-domain-ajax-with-cross-origin-resource-sharing/)
6
+
7
+ ## Installation
8
+
9
+ Install the gem:
10
+
11
+ `gem install rack-cors`
12
+
13
+ Or in your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'rack-cors'
17
+ ```
18
+
19
+
20
+ ## Configuration
21
+
22
+ ### Rails Configuration
23
+ Put something like the code below in `config/application.rb` of your Rails application. For example, this will allow GET, POST or OPTIONS requests from any origin on any resource.
24
+
25
+ ```ruby
26
+ module YourApp
27
+ class Application < Rails::Application
28
+ # ...
29
+
30
+ # Rails 5
31
+
32
+ config.middleware.insert_before 0, Rack::Cors do
33
+ allow do
34
+ origins '*'
35
+ resource '*', headers: :any, methods: [:get, :post, :options]
36
+ end
37
+ end
38
+
39
+ # Rails 3/4
40
+
41
+ config.middleware.insert_before 0, "Rack::Cors" do
42
+ allow do
43
+ origins '*'
44
+ resource '*', headers: :any, methods: [:get, :post, :options]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
51
+ We use `insert_before` to make sure `Rack::Cors` runs at the beginning of the stack to make sure it isn't interfered with by other middleware (see `Rack::Cache` note in **Common Gotchas** section). Check out the [rails 4 example](https://github.com/cyu/rack-cors/tree/master/examples/rails4) and [rails 3 example](https://github.com/cyu/rack-cors/tree/master/examples/rails3).
52
+
53
+ See The [Rails Guide to Rack](http://guides.rubyonrails.org/rails_on_rack.html) for more details on rack middlewares or watch the [railscast](http://railscasts.com/episodes/151-rack-middleware).
54
+
55
+ ### Rack Configuration
56
+
57
+ NOTE: If you're running Rails, updating in `config/application.rb` should be enough. There is no need to update `config.ru` as well.
58
+
59
+ In `config.ru`, configure `Rack::Cors` by passing a block to the `use` command:
60
+
61
+ ```ruby
62
+ use Rack::Cors do
63
+ allow do
64
+ origins 'localhost:3000', '127.0.0.1:3000',
65
+ /\Ahttp:\/\/192\.168\.0\.\d{1,3}(:\d+)?\z/
66
+ # regular expressions can be used here
67
+
68
+ resource '/file/list_all/', :headers => 'x-domain-token'
69
+ resource '/file/at/*',
70
+ methods: [:get, :post, :delete, :put, :patch, :options, :head],
71
+ headers: 'x-domain-token',
72
+ expose: ['Some-Custom-Response-Header'],
73
+ max_age: 600
74
+ # headers to expose
75
+ end
76
+
77
+ allow do
78
+ origins '*'
79
+ resource '/public/*', headers: :any, methods: :get
80
+
81
+ # Only allow a request for a specific host
82
+ resource '/api/v1/*',
83
+ headers: :any,
84
+ methods: :get,
85
+ if: proc { |env| env['HTTP_HOST'] == 'api.example.com' }
86
+ end
87
+ end
88
+ ```
89
+
90
+ ### Configuration Reference
91
+
92
+ #### Middleware Options
93
+ * **debug** (boolean): Enables debug logging and `X-Rack-CORS` HTTP headers for debugging.
94
+ * **logger** (Object or Proc): Specify the logger to log to. If a proc is provided, it will be called when a logger is needed. This is helpful in cases where the logger is initialized after `Rack::Cors` is initially configured, like `Rails.logger`.
95
+
96
+ #### Origin
97
+ Origins can be specified as a string, a regular expression, or as '\*' to allow all origins.
98
+
99
+ **\*SECURITY NOTE:** Be careful when using regular expressions to not accidentally be too inclusive. For example, the expression `/https:\/\/example\.com/` will match the domain *example.com.randomdomainname.co.uk*. It is recommended that any regular expression be enclosed with start & end string anchors (`\A\z`).
100
+
101
+ Additionally, origins can be specified dynamically via a block of the following form:
102
+ ```ruby
103
+ origins { |source, env| true || false }
104
+ ```
105
+
106
+ A Resource path can be specified as exact string match (`/path/to/file.txt`) or with a '\*' wildcard (`/all/files/in/*`). To include all of a directory's files and the files in its subdirectories, use this form: `/assets/**/*`. A resource can take the following options:
107
+
108
+ * **methods** (string or array or `:any`): The HTTP methods allowed for the resource.
109
+ * **headers** (string or array or `:any`): The HTTP headers that will be allowed in the CORS resource request. Use `:any` to allow for any headers in the actual request.
110
+ * **expose** (string or array): The HTTP headers in the resource response can be exposed to the client.
111
+ * **credentials** (boolean, default: `false`): Sets the `Access-Control-Allow-Credentials` response header. **Note:** If a wildcard (`*`) origin is specified, this option cannot be set to `true`. Read this [security article](http://web-in-security.blogspot.de/2017/07/cors-misconfigurations-on-large-scale.html) for more information.
112
+ * **max_age** (number): Sets the `Access-Control-Max-Age` response header.
113
+ * **if** (Proc): If the result of the proc is true, will process the request as a valid CORS request.
114
+ * **vary** (string or array): A list of HTTP headers to add to the 'Vary' header.
115
+
116
+
117
+ ## Common Gotchas
118
+
119
+ Incorrect positioning of `Rack::Cors` in the middleware stack can produce unexpected results. The Rails example above will put it above all middleware which should cover most issues.
120
+
121
+ Here are some common cases:
122
+
123
+ * **Serving static files.** Insert this middleware before `ActionDispatch::Static` so that static files are served with the proper CORS headers (see note below for a caveat). **NOTE:** that this might not work in production environments as static files are usually served from the web server (Nginx, Apache) and not the Rails container.
124
+
125
+ * **Caching in the middleware.** Insert this middleware before `Rack::Cache` so that the proper CORS headers are written and not cached ones.
126
+
127
+ * **Authentication via Warden** Warden will return immediately if a resource that requires authentication is accessed without authentication. If `Warden::Manager`is in the stack before `Rack::Cors`, it will return without the correct CORS headers being applied, resulting in a failed CORS request. Be sure to insert this middleware before 'Warden::Manager`.
128
+
129
+ To determine where to put the CORS middleware in the Rack stack, run the following command:
130
+
131
+ ```bash
132
+ bundle exec rake middleware
133
+ ```
134
+
135
+ In many cases, the Rack stack will be different running in production environments. For example, the `ActionDispatch::Static` middleware will not be part of the stack if `config.serve_static_assets = false`. You can run the following command to see what your middleware stack looks like in production:
136
+
137
+ ```bash
138
+ RAILS_ENV=production bundle exec rake middleware
139
+ ```
data/lib/rack/cors.rb CHANGED
@@ -2,10 +2,41 @@ require 'logger'
2
2
 
3
3
  module Rack
4
4
  class Cors
5
+ HTTP_ORIGIN = 'HTTP_ORIGIN'.freeze
6
+ HTTP_X_ORIGIN = 'HTTP_X_ORIGIN'.freeze
7
+
8
+ HTTP_ACCESS_CONTROL_REQUEST_METHOD = 'HTTP_ACCESS_CONTROL_REQUEST_METHOD'.freeze
9
+ HTTP_ACCESS_CONTROL_REQUEST_HEADERS = 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS'.freeze
10
+
11
+ PATH_INFO = 'PATH_INFO'.freeze
12
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
13
+
14
+ RACK_LOGGER = 'rack.logger'.freeze
15
+ RACK_CORS =
16
+ # retaining the old key for backwards compatibility
17
+ ENV_KEY = 'rack.cors'.freeze
18
+
19
+ OPTIONS = 'OPTIONS'.freeze
20
+ VARY = 'Vary'.freeze
21
+
22
+ DEFAULT_VARY_HEADERS = ['Origin'].freeze
23
+
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
+
5
28
  def initialize(app, opts={}, &block)
6
29
  @app = app
7
- @logger = opts[:logger]
8
30
  @debug_mode = !!opts[:debug]
31
+ @logger = @logger_proc = nil
32
+
33
+ if logger = opts[:logger]
34
+ if logger.respond_to? :call
35
+ @logger_proc = opts[:logger]
36
+ else
37
+ @logger = logger
38
+ end
39
+ end
9
40
 
10
41
  if block_given?
11
42
  if block.arity == 1
@@ -16,6 +47,10 @@ module Rack
16
47
  end
17
48
  end
18
49
 
50
+ def debug?
51
+ @debug_mode
52
+ end
53
+
19
54
  def allow(&block)
20
55
  all_resources << (resources = Resources.new)
21
56
 
@@ -27,90 +62,232 @@ module Rack
27
62
  end
28
63
 
29
64
  def call(env)
30
- env['HTTP_ORIGIN'] = 'file://' if env['HTTP_ORIGIN'] == 'null'
31
- env['HTTP_ORIGIN'] ||= env['HTTP_X_ORIGIN']
65
+ env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
32
66
 
33
- cors_headers = nil
34
- if env['HTTP_ORIGIN']
67
+ path = evaluate_path(env)
68
+
69
+ add_headers = nil
70
+ if env[HTTP_ORIGIN]
35
71
  debug(env) do
36
72
  [ 'Incoming Headers:',
37
- " Origin: #{env['HTTP_ORIGIN']}",
38
- " Access-Control-Request-Method: #{env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']}",
39
- " Access-Control-Request-Headers: #{env['HTTP_ACCESS_CONTROL_REQUEST_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]}"
40
77
  ].join("\n")
41
78
  end
42
- if env['REQUEST_METHOD'] == 'OPTIONS' and env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
43
- if headers = process_preflight(env)
44
- debug(env) do
45
- "Preflight Headers:\n" +
46
- headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
47
- end
48
- return [200, headers, []]
79
+ if env[REQUEST_METHOD] == OPTIONS and env[HTTP_ACCESS_CONTROL_REQUEST_METHOD]
80
+ headers = process_preflight(env, path)
81
+ debug(env) do
82
+ "Preflight Headers:\n" +
83
+ headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
49
84
  end
85
+ return [200, headers, []]
50
86
  else
51
- cors_headers = process_cors(env)
87
+ add_headers = process_cors(env, path)
52
88
  end
89
+ else
90
+ Result.miss(env, Result::MISS_NO_ORIGIN)
53
91
  end
92
+
93
+ # This call must be done BEFORE calling the app because for some reason
94
+ # env[PATH_INFO] gets changed after that and it won't match. (At least
95
+ # in rails 4.1.6)
96
+ vary_resource = resource_for_path(path)
97
+
54
98
  status, headers, body = @app.call env
55
- if cors_headers
56
- headers = headers.merge(cors_headers)
57
99
 
58
- # http://www.w3.org/TR/cors/#resource-implementation
59
- unless headers['Access-Control-Allow-Origin'] == '*'
60
- vary = headers['Vary']
61
- headers['Vary'] = ((vary ? vary.split(/,\s*/) : []) + ['Origin']).uniq.join(', ')
100
+ if add_headers
101
+ headers = add_headers.merge(headers)
102
+ debug(env) do
103
+ add_headers.each_pair do |key, value|
104
+ if headers.has_key?(key)
105
+ headers["X-Rack-CORS-Original-#{key}"] = value
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Vary header should ALWAYS mention Origin if there's ANY chance for the
112
+ # response to be different depending on the Origin header value.
113
+ # Better explained here: http://www.fastly.com/blog/best-practices-for-using-the-vary-header/
114
+ 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
62
120
  end
121
+ headers[VARY] = ((vary ? ([vary].flatten.map { |v| v.split(/,\s*/) }.flatten) : []) + cors_vary_headers).uniq.join(', ')
63
122
  end
123
+
124
+ if debug? && result = env[RACK_CORS]
125
+ result.append_header(headers)
126
+ end
127
+
64
128
  [status, headers, body]
65
129
  end
66
130
 
67
131
  protected
68
132
  def debug(env, message = nil, &block)
69
- if @debug_mode
70
- logger = @logger || env['rack.logger'] || begin
71
- @logger = ::Logger.new(STDOUT).tap {|logger| logger.level = ::Logger::Severity::INFO}
72
- end
73
- logger.debug(message, &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
+
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 }
74
150
  end
75
151
  end
76
152
 
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
158
+
77
159
  def all_resources
78
160
  @all_resources ||= []
79
161
  end
80
162
 
81
- def process_preflight(env)
82
- resource = find_resource(env['HTTP_ORIGIN'], env['PATH_INFO'],env)
83
- resource && resource.process_preflight(env)
163
+ def process_preflight(env, path)
164
+ result = Result.preflight(env)
165
+
166
+ resource, error = match_resource(path, env)
167
+ unless resource
168
+ result.miss(error)
169
+ return {}
170
+ end
171
+
172
+ return resource.process_preflight(env, result)
84
173
  end
85
174
 
86
- def process_cors(env)
87
- resource = find_resource(env['HTTP_ORIGIN'], env['PATH_INFO'],env)
88
- resource.to_headers(env) if resource
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
181
+
182
+ else
183
+ Result.miss(env, error)
184
+ nil
185
+ end
89
186
  end
90
187
 
91
- def find_resource(origin, path, env)
188
+ def resource_for_path(path_info)
92
189
  all_resources.each do |r|
93
- if r.allow_origin?(origin, env) and found = r.find_resource(path)
190
+ if found = r.resource_for_path(path_info)
94
191
  return found
95
192
  end
96
193
  end
97
194
  nil
98
195
  end
99
196
 
197
+ def match_resource(path, env)
198
+ origin = env[HTTP_ORIGIN]
199
+
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
209
+
210
+ [nil, origin_matched ? Result::MISS_NO_PATH : Result::MISS_NO_ORIGIN]
211
+ end
212
+
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
258
+
259
+
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
270
+ end
271
+
100
272
  class Resources
273
+
274
+ attr_reader :resources
275
+
101
276
  def initialize
102
277
  @origins = []
103
278
  @resources = []
104
279
  @public_resources = false
105
280
  end
106
281
 
107
- def origins(*args,&blk)
108
- @origins = args.flatten.collect do |n|
282
+ def origins(*args, &blk)
283
+ @origins = args.flatten.reject{ |s| s == '' }.map do |n|
109
284
  case n
110
- when Regexp, /^https?:\/\// then n
111
- when 'file://' then n
112
- when '*' then @public_resources = true; n
113
- else ["http://#{n}", "https://#{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)}$")
114
291
  end
115
292
  end.flatten
116
293
  @origins.push(blk) if blk
@@ -126,6 +303,7 @@ module Rack
126
303
 
127
304
  def allow_origin?(source,env = {})
128
305
  return true if public_resources?
306
+
129
307
  return !! @origins.detect do |origin|
130
308
  if origin.is_a?(Proc)
131
309
  origin.call(source,env)
@@ -135,21 +313,36 @@ module Rack
135
313
  end
136
314
  end
137
315
 
138
- def find_resource(path)
139
- @resources.detect{|r| r.match?(path)}
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) }
140
322
  end
323
+
141
324
  end
142
325
 
143
326
  class Resource
144
- attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern
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
145
335
 
146
336
  def initialize(public_resource, path, opts={})
147
- self.path = path
148
- self.methods = ensure_enum(opts[:methods]) || [:get]
149
- self.credentials = opts[:credentials].nil? ? true : opts[:credentials]
150
- self.max_age = opts[:max_age] || 1728000
151
- self.pattern = compile(path)
152
- @public_resource = public_resource
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
153
346
 
154
347
  self.headers = case opts[:headers]
155
348
  when :any then :any
@@ -158,22 +351,46 @@ module Rack
158
351
  [opts[:headers]].flatten.collect{|h| h.downcase}
159
352
  end
160
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
+
161
360
  self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
162
361
  end
163
362
 
164
- def match?(path)
363
+ def matches_path?(path)
165
364
  pattern =~ path
166
365
  end
167
366
 
168
- def process_preflight(env)
169
- return nil if invalid_method_request?(env) || invalid_headers_request?(env)
170
- {'Content-Type' => 'text/plain'}.merge(to_preflight_headers(env))
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))
171
389
  end
172
390
 
173
391
  def to_headers(env)
174
- x_origin = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
175
392
  h = {
176
- 'Access-Control-Allow-Origin' => origin_for_response_header(env['HTTP_ORIGIN']),
393
+ 'Access-Control-Allow-Origin' => origin_for_response_header(env[HTTP_ORIGIN]),
177
394
  'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
178
395
  'Access-Control-Expose-Headers' => expose.nil? ? '' : expose.join(', '),
179
396
  'Access-Control-Max-Age' => max_age.to_s }
@@ -187,33 +404,27 @@ module Rack
187
404
  end
188
405
 
189
406
  def origin_for_response_header(origin)
190
- return '*' if public_resource? && !credentials
191
- origin == 'file://' ? 'null' : origin
407
+ return '*' if public_resource?
408
+ origin
192
409
  end
193
410
 
194
411
  def to_preflight_headers(env)
195
412
  h = to_headers(env)
196
- if env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
197
- h.merge!('Access-Control-Allow-Headers' => env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])
413
+ if env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
414
+ h.merge!('Access-Control-Allow-Headers' => env[HTTP_ACCESS_CONTROL_REQUEST_HEADERS])
198
415
  end
199
416
  h
200
417
  end
201
418
 
202
- def invalid_method_request?(env)
203
- request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
204
- request_method.nil? || !methods.include?(request_method.downcase.to_sym)
205
- end
206
-
207
- def invalid_headers_request?(env)
208
- request_headers = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
209
- request_headers && !allow_headers?(request_headers)
210
- end
211
-
212
419
  def allow_headers?(request_headers)
213
- return false if headers.nil?
214
- headers == :any || begin
215
- request_headers = request_headers.split(/,\s*/) if request_headers.kind_of?(String)
216
- request_headers.all?{|h| headers.include?(h.downcase)}
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)
217
428
  end
218
429
  end
219
430