rack-cors 1.1.1 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f879bc8ea95eac0ca9360c3a553084961d02944255f6ad380b64e855653b8b6
4
- data.tar.gz: bd9478603340a1785324ab4f1db9517a8943fdcc1be13193e4d6b83b184fa032
3
+ metadata.gz: c4c9bf1e801f0bb5f4c5abb111fbe9a0dcfbd1bcf0da1b2070fb1575de5bcdd7
4
+ data.tar.gz: 9a78208ad501ffa452ac0721eeb29ccf5fc00ad392bfe2be06c8d0c6a4a1c5c0
5
5
  SHA512:
6
- metadata.gz: 12d13e99acef13b159595487b3c0198bc1a355371bdb149241d11e1d0715148e0749085d0f0c362d4defdec2e325b416b1e93aeb28be2d421516d4db8185fdac
7
- data.tar.gz: a1f373194a95094f337c545e7751eac2c4d8500dfd607088a88e55774b755fa0ec659710319a2f7997e579231b7de806e63f2ce4a1639cdb732eed5bcbb743b9
6
+ metadata.gz: ef9c38f1c3f6c13609afcded7d3bd0c40e22e6a098fe7f565d5d6351c1ed0e3f199752912163bd1963005b3ed4d5ae20216b6f8c94736b5f941d9e038e089829
7
+ data.tar.gz: 633bf8887b580a52cd0f50359d8febffd609190cbb5084ae5e02233126c14a70d019bdfd2feddba7cf078cf5d820e86856ad867fcbc1ce90bd3ecb9c75b08f64
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ ---
2
+
3
+ AllCops:
4
+ Exclude:
5
+ - 'examples/**/*'
6
+
7
+ # Disables
8
+ Layout/LineLength:
9
+ Enabled: false
10
+ Style/Documentation:
11
+ Enabled: false
12
+ Metrics/ClassLength:
13
+ Enabled: false
14
+ Metrics/MethodLength:
15
+ Enabled: false
16
+ Metrics/BlockLength:
17
+ Enabled: false
18
+ Style/HashEachMethods:
19
+ Enabled: false
20
+ Style/HashTransformKeys:
21
+ Enabled: false
22
+ Style/HashTransformValues:
23
+ Enabled: false
24
+ Style/DoubleNegation:
25
+ Enabled: false
26
+ Metrics/CyclomaticComplexity:
27
+ Enabled: false
28
+ Metrics/PerceivedComplexity:
29
+ Enabled: false
30
+ Metrics/AbcSize:
31
+ Enabled: false
data/.travis.yml CHANGED
@@ -1,8 +1,13 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.2
5
4
  - 2.3
6
5
  - 2.4
7
6
  - 2.5
8
7
  - 2.6
8
+ - 2.7
9
+ - truffleruby-head
10
+
11
+ script:
12
+ - bundle exec rubocop
13
+ - bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Change Log
2
2
  All notable changes to this project will be documented in this file.
3
3
 
4
+ ## 2.0.0 - 2022-09-11
5
+ ### Changed
6
+ - Refactored codebase
7
+ - Support declaring custom protocols in origin
8
+ - Lowercased header names as defined by Rack spec
9
+
4
10
  ## 1.1.1 - 2019-12-29
5
11
  ### Changed
6
12
  - Allow /<resource>/* to match /<resource>/ and /<resource> paths
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in rack-cors.gemspec
data/README.md CHANGED
@@ -20,41 +20,34 @@ gem 'rack-cors'
20
20
  ## Configuration
21
21
 
22
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.
23
+ For Rails, you'll need to add this middleware on application startup. A practical way to do this is with an initializer file. For example, the following will allow GET, POST, PATCH, or PUT requests from any origin on any resource:
24
24
 
25
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
26
+ # config/initializers/cors.rb
27
+
28
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
29
+ allow do
30
+ origins '*'
31
+ resource '*', headers: :any, methods: [:get, :post, :patch, :put]
47
32
  end
48
33
  end
49
34
  ```
50
35
 
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).
36
+ 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). Basic setup examples for Rails 5 & Rails 6 can be found in the examples/ directory.
52
37
 
53
38
  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
39
 
40
+ *Note about Rails 6*: Rails 6 has support for blocking requests from unknown hosts, so origin domains will need to be added there as well.
41
+
42
+ ```ruby
43
+ Rails.application.config.hosts << "product.com"
44
+ ```
45
+
46
+ Read more about it here in the [Rails Guides](https://guides.rubyonrails.org/configuring.html#configuring-middleware)
47
+
55
48
  ### Rack Configuration
56
49
 
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.
50
+ NOTE: If you're running Rails, adding `config/initializers/cors.rb` should be enough. There is no need to update `config.ru` as well.
58
51
 
59
52
  In `config.ru`, configure `Rack::Cors` by passing a block to the `use` command:
60
53
 
@@ -96,14 +89,14 @@ end
96
89
  #### Origin
97
90
  Origins can be specified as a string, a regular expression, or as '\*' to allow all origins.
98
91
 
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`).
92
+ **\*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, like `\Ahttps:\/\/example\.com\z`.
100
93
 
101
94
  Additionally, origins can be specified dynamically via a block of the following form:
102
95
  ```ruby
103
96
  origins { |source, env| true || false }
104
97
  ```
105
98
 
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:
99
+ A Resource path can be specified as exact string match (`/path/to/file.txt`) or with a '\*' wildcard (`/all/files/in/*`). A resource can take the following options:
107
100
 
108
101
  * **methods** (string or array or `:any`): The HTTP methods allowed for the resource.
109
102
  * **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.
@@ -116,24 +109,41 @@ A Resource path can be specified as exact string match (`/path/to/file.txt`) or
116
109
 
117
110
  ## Common Gotchas
118
111
 
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.
112
+ ### Origin Matching
120
113
 
121
- Here are some common cases:
114
+ When specifying an origin, make sure that it does not have a trailing slash.
122
115
 
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.
116
+ ### Testing Postman and/or cURL
124
117
 
125
- * **Caching in the middleware.** Insert this middleware before `Rack::Cache` so that the proper CORS headers are written and not cached ones.
118
+ * Make sure you're passing in an `Origin:` header. That header is required to trigger a CORS response. Here's [a good SO post](https://stackoverflow.com/questions/12173990/how-can-you-debug-a-cors-request-with-curl) about using cURL for testing CORS.
119
+ * Make sure your origin does not have a trailing slash.
126
120
 
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`.
121
+ ### Positioning in the Middleware Stack
128
122
 
129
- To determine where to put the CORS middleware in the Rack stack, run the following command:
123
+ Positioning of `Rack::Cors` in the middleware stack is very important. In the Rails example above we put it above all other middleware which, in our experience, provides the most consistent results.
124
+
125
+ Here are some scenarios where incorrect positioning have created issues:
126
+
127
+ * **Serving static files.** Insert before `ActionDispatch::Static` so that static files are served with the proper CORS headers. **NOTE:** this might not work in production as static files are usually served from the web server (Nginx, Apache) and not the Rails container.
128
+
129
+ * **Caching in the middleware.** Insert before `Rack::Cache` so that the proper CORS headers are written and not cached ones.
130
+
131
+ * **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.
132
+
133
+ You can run the following command to see what the middleware stack looks like:
130
134
 
131
135
  ```bash
132
136
  bundle exec rake middleware
133
137
  ```
134
138
 
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:
139
+ Note that the middleware stack is different in production. For example, the `ActionDispatch::Static` middleware will not be part of the stack if `config.serve_static_assets = false`. You can run this to see what your middleware stack looks like in production:
136
140
 
137
141
  ```bash
138
142
  RAILS_ENV=production bundle exec rake middleware
139
143
  ```
144
+
145
+ ### Serving static files
146
+
147
+ If you trying to serve CORS headers on static assets (like CSS, JS, Font files), keep in mind that static files are usually served directly from web servers and never runs through the Rails container (including the middleware stack where `Rack::Cors` resides).
148
+
149
+ In Heroku, you can serve static assets through the Rails container by setting `config.serve_static_assets = true` in `production.rb`.
data/Rakefile CHANGED
@@ -1,4 +1,6 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
2
4
 
3
5
  require 'rake/testtask'
4
6
  Rake::TestTask.new(:test) do |test|
@@ -7,15 +9,14 @@ Rake::TestTask.new(:test) do |test|
7
9
  test.verbose = true
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
11
13
 
12
14
  require 'rdoc/task'
13
15
  Rake::RDocTask.new do |rdoc|
14
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
16
+ version = File.exist?('VERSION') ? File.read('VERSION') : ''
15
17
 
16
18
  rdoc.rdoc_dir = 'rdoc'
17
19
  rdoc.title = "rack-cors #{version}"
18
20
  rdoc.rdoc_files.include('README*')
19
21
  rdoc.rdoc_files.include('lib/**/*.rb')
20
22
  end
21
-
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Cors
5
+ class Resource
6
+ # All CORS routes need to accept CORS simple headers at all times
7
+ # {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers}
8
+ CORS_SIMPLE_HEADERS = %w[accept accept-language content-language content-type].freeze
9
+
10
+ attr_accessor :path, :methods, :headers, :expose, :max_age, :credentials, :pattern, :if_proc, :vary_headers
11
+
12
+ def initialize(public_resource, path, opts = {})
13
+ raise CorsMisconfigurationError if public_resource && opts[:credentials] == true
14
+
15
+ self.path = path
16
+ self.credentials = public_resource ? false : (opts[:credentials] == true)
17
+ self.max_age = opts[:max_age] || 7200
18
+ self.pattern = compile(path)
19
+ self.if_proc = opts[:if]
20
+ self.vary_headers = opts[:vary] && [opts[:vary]].flatten
21
+ @public_resource = public_resource
22
+
23
+ self.headers = case opts[:headers]
24
+ when :any then :any
25
+ when nil then nil
26
+ else
27
+ [opts[:headers]].flatten.collect(&:downcase)
28
+ end
29
+
30
+ self.methods = case opts[:methods]
31
+ when :any then %i[get head post put patch delete options]
32
+ else
33
+ ensure_enum(opts[:methods]) || [:get]
34
+ end.map(&:to_s)
35
+
36
+ self.expose = opts[:expose] ? [opts[:expose]].flatten : nil
37
+ end
38
+
39
+ def matches_path?(path)
40
+ pattern =~ path
41
+ end
42
+
43
+ def match?(path, env)
44
+ matches_path?(path) && (if_proc.nil? || if_proc.call(env))
45
+ end
46
+
47
+ def process_preflight(env, result)
48
+ headers = {}
49
+
50
+ request_method = env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_METHOD]
51
+ result.miss(Result::MISS_NO_METHOD) && (return headers) if request_method.nil?
52
+ result.miss(Result::MISS_DENY_METHOD) && (return headers) unless methods.include?(request_method.downcase)
53
+
54
+ request_headers = env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
55
+ result.miss(Result::MISS_DENY_HEADER) && (return headers) if request_headers && !allow_headers?(request_headers)
56
+
57
+ result.hit = true
58
+ headers.merge(to_preflight_headers(env))
59
+ end
60
+
61
+ def to_headers(env)
62
+ h = {
63
+ 'access-control-allow-origin' => origin_for_response_header(env[Rack::Cors::HTTP_ORIGIN]),
64
+ 'access-control-allow-methods' => methods.collect { |m| m.to_s.upcase }.join(', '),
65
+ 'access-control-expose-headers' => expose.nil? ? '' : expose.join(', '),
66
+ 'access-control-max-age' => max_age.to_s
67
+ }
68
+ h['access-control-allow-credentials'] = 'true' if credentials
69
+ h
70
+ end
71
+
72
+ protected
73
+
74
+ def public_resource?
75
+ @public_resource
76
+ end
77
+
78
+ def origin_for_response_header(origin)
79
+ return '*' if public_resource?
80
+
81
+ origin
82
+ end
83
+
84
+ def to_preflight_headers(env)
85
+ h = to_headers(env)
86
+ h.merge!('Access-Control-Allow-Headers' => env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]) if env[Rack::Cors::HTTP_ACCESS_CONTROL_REQUEST_HEADERS]
87
+ h
88
+ end
89
+
90
+ def allow_headers?(request_headers)
91
+ headers = self.headers || []
92
+ return true if headers == :any
93
+
94
+ request_headers = request_headers.split(/,\s*/) if request_headers.is_a?(String)
95
+ request_headers.all? do |header|
96
+ header = header.downcase
97
+ CORS_SIMPLE_HEADERS.include?(header) || headers.include?(header)
98
+ end
99
+ end
100
+
101
+ def ensure_enum(var)
102
+ return nil if var.nil?
103
+
104
+ [var].flatten
105
+ end
106
+
107
+ def compile(path)
108
+ if path.respond_to? :to_str
109
+ special_chars = %w[. + ( )]
110
+ pattern =
111
+ path.to_str.gsub(%r{((:\w+)|/\*|[\*#{special_chars.join}])}) do |match|
112
+ case match
113
+ when '/*'
114
+ '\\/?(.*?)'
115
+ when '*'
116
+ '(.*?)'
117
+ when *special_chars
118
+ Regexp.escape(match)
119
+ else
120
+ '([^/?&#]+)'
121
+ end
122
+ end
123
+ /^#{pattern}$/
124
+ elsif path.respond_to? :match
125
+ path
126
+ else
127
+ raise TypeError, path
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Cors
5
+ class Resource
6
+ class CorsMisconfigurationError < StandardError
7
+ def message
8
+ 'Allowing credentials for wildcard origins is insecure.' \
9
+ " Please specify more restrictive origins or set 'credentials' to false in your CORS configuration."
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resources/cors_misconfiguration_error'
4
+
5
+ module Rack
6
+ class Cors
7
+ class Resources
8
+ attr_reader :resources
9
+
10
+ def initialize
11
+ @origins = []
12
+ @resources = []
13
+ @public_resources = false
14
+ end
15
+
16
+ def origins(*args, &blk)
17
+ @origins = args.flatten.reject { |s| s == '' }.map do |n|
18
+ case n
19
+ when Proc, Regexp, %r{^[a-z][a-z0-9.+-]*://}
20
+ n
21
+ when '*'
22
+ @public_resources = true
23
+ n
24
+ else
25
+ Regexp.compile("^[a-z][a-z0-9.+-]*:\\\/\\\/#{Regexp.quote(n)}$")
26
+ end
27
+ end.flatten
28
+ @origins.push(blk) if blk
29
+ end
30
+
31
+ def resource(path, opts = {})
32
+ @resources << Resource.new(public_resources?, path, opts)
33
+ end
34
+
35
+ def public_resources?
36
+ @public_resources
37
+ end
38
+
39
+ def allow_origin?(source, env = {})
40
+ return true if public_resources?
41
+
42
+ !!@origins.detect do |origin|
43
+ if origin.is_a?(Proc)
44
+ origin.call(source, env)
45
+ elsif origin.is_a?(Regexp)
46
+ source =~ origin
47
+ else
48
+ source == origin
49
+ end
50
+ end
51
+ end
52
+
53
+ def match_resource(path, env)
54
+ @resources.detect { |r| r.match?(path, env) }
55
+ end
56
+
57
+ def resource_for_path(path)
58
+ @resources.detect { |r| r.matches_path?(path) }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ class Cors
5
+ class Result
6
+ HEADER_KEY = 'x-rack-cors'
7
+
8
+ MISS_NO_ORIGIN = 'no-origin'
9
+ MISS_NO_PATH = 'no-path'
10
+
11
+ MISS_NO_METHOD = 'no-method'
12
+ MISS_DENY_METHOD = 'deny-method'
13
+ MISS_DENY_HEADER = 'deny-header'
14
+
15
+ attr_accessor :preflight, :hit, :miss_reason
16
+
17
+ def hit?
18
+ !!hit
19
+ end
20
+
21
+ def preflight?
22
+ !!preflight
23
+ end
24
+
25
+ def miss(reason)
26
+ self.hit = false
27
+ self.miss_reason = reason
28
+ end
29
+
30
+ def self.hit(env)
31
+ r = Result.new
32
+ r.preflight = false
33
+ r.hit = true
34
+ env[Rack::Cors::ENV_KEY] = r
35
+ end
36
+
37
+ def self.miss(env, reason)
38
+ r = Result.new
39
+ r.preflight = false
40
+ r.hit = false
41
+ r.miss_reason = reason
42
+ env[Rack::Cors::ENV_KEY] = r
43
+ end
44
+
45
+ def self.preflight(env)
46
+ r = Result.new
47
+ r.preflight = true
48
+ env[Rack::Cors::ENV_KEY] = r
49
+ end
50
+
51
+ def append_header(headers)
52
+ headers[HEADER_KEY] = if hit?
53
+ preflight? ? 'preflight-hit' : 'hit'
54
+ else
55
+ [
56
+ (preflight? ? 'preflight-miss' : 'miss'),
57
+ miss_reason
58
+ ].join('; ')
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  class Cors
3
- VERSION = "1.1.1"
5
+ VERSION = '2.0.0.rc1'
4
6
  end
5
7
  end