rack-cors 0.2.9 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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