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 +5 -13
- data/.travis.yml +8 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +2 -0
- data/README.md +139 -0
- data/lib/rack/cors.rb +285 -74
- data/lib/rack/cors/version.rb +1 -1
- data/rack-cors.gemspec +8 -7
- data/test/cors/test.cors.coffee +26 -2
- data/test/cors/test.cors.js +45 -4
- data/test/unit/cors_test.rb +422 -95
- data/test/unit/dsl_test.rb +28 -17
- data/test/unit/insecure.ru +8 -0
- data/test/unit/non_http.ru +8 -0
- data/test/unit/test.ru +20 -5
- metadata +56 -32
- data/README.rdoc +0 -66
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZDQyMTJlMTg3MDczMmE0ZjFkMmZjOGExMGU3NDE0NmQ2MGY1YzBlZQ==
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a12cdfc5aca2abf0cf86fb1ca217619fa6b40cad19721118016e064554f46ba0
|
4
|
+
data.tar.gz: 2874199b748909fdfd3e8ec601bd8620bc0235e60c66226259a79ff2404dbaf8
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
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
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
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[
|
31
|
-
env['HTTP_ORIGIN'] ||= env['HTTP_X_ORIGIN']
|
65
|
+
env[HTTP_ORIGIN] ||= env[HTTP_X_ORIGIN] if env[HTTP_X_ORIGIN]
|
32
66
|
|
33
|
-
|
34
|
-
|
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[
|
38
|
-
"
|
39
|
-
" Access-Control-Request-
|
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[
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
83
|
-
|
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 =
|
88
|
-
|
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
|
188
|
+
def resource_for_path(path_info)
|
92
189
|
all_resources.each do |r|
|
93
|
-
if
|
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
|
108
|
-
@origins = args.flatten.
|
282
|
+
def origins(*args, &blk)
|
283
|
+
@origins = args.flatten.reject{ |s| s == '' }.map do |n|
|
109
284
|
case n
|
110
|
-
when
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
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
|
-
|
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
|
-
|
148
|
-
|
149
|
-
self.
|
150
|
-
self.
|
151
|
-
self.
|
152
|
-
|
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
|
363
|
+
def matches_path?(path)
|
165
364
|
pattern =~ path
|
166
365
|
end
|
167
366
|
|
168
|
-
def
|
169
|
-
|
170
|
-
|
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[
|
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?
|
191
|
-
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[
|
197
|
-
h.merge!('Access-Control-Allow-Headers' => env[
|
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
|
-
|
214
|
-
headers == :any
|
215
|
-
|
216
|
-
|
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
|
|