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 +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 [](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
|
|