rack-cors 0.1.1 → 0.2.0

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.

Potentially problematic release.


This version of rack-cors might be problematic. Click here for more details.

@@ -19,14 +19,18 @@ Install the gem:
19
19
 
20
20
  You configure Rack::Cors by passing a block to the <tt>use</tt> command:
21
21
 
22
- use Rack::Cors do |config|
23
- config.allow do |allow|
22
+ use Rack::Cors do |cfg|
23
+ cfg.allow do |allow|
24
24
  allow.origins 'localhost:3000', '127.0.0.1:3000'
25
25
 
26
26
  allow.resource '/file/list_all/', :headers => 'x-domain-token'
27
-
28
27
  allow.resource '/file/at/*',
29
28
  :methods => [:get, :post, :put, :delete],
30
29
  :headers => 'x-domain-token'
31
30
  end
31
+
32
+ cfg.allow do |allow|
33
+ allow.origins '*'
34
+ allow.resource '/public/*', :headers => :any, :methods => :get
35
+ end
32
36
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.2.0
@@ -1,8 +1,10 @@
1
+ require 'logger'
2
+
1
3
  module Rack
2
4
  class Cors
3
-
4
- def initialize(app)
5
+ def initialize(app, opts={})
5
6
  @app = app
7
+ @logger = opts[:logger]
6
8
  yield self if block_given?
7
9
  end
8
10
 
@@ -14,11 +16,24 @@ module Rack
14
16
  def call(env)
15
17
  cors_headers = nil
16
18
  if env['HTTP_ORIGIN']
19
+ debug(env) do
20
+ [ 'Incoming Headers:',
21
+ " Origin: #{env['HTTP_ORIGIN']}",
22
+ " Access-Control-Request-Method: #{env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']}",
23
+ " Access-Control-Request-Headers: #{env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"
24
+ ].join("\n")
25
+ end
17
26
  if env['REQUEST_METHOD'] == 'OPTIONS'
18
- headers = process_preflight(env)
19
- return [200, headers, []] if headers
27
+ if headers = process_preflight(env)
28
+ debug(env) do
29
+ "Preflight Headers:\n" +
30
+ headers.collect{|kv| " #{kv.join(': ')}"}.join("\n")
31
+ end
32
+ return [200, headers, []]
33
+ end
34
+ else
35
+ cors_headers = process_cors(env)
20
36
  end
21
- cors_headers = process_cors(env)
22
37
  end
23
38
  status, headers, body = @app.call env
24
39
  headers = headers.merge(cors_headers) if cors_headers
@@ -26,13 +41,20 @@ module Rack
26
41
  end
27
42
 
28
43
  protected
44
+ def debug(env, message = nil, &block)
45
+ logger = @logger || env['rack.logger'] || begin
46
+ @logger = ::Logger.new(STDOUT).tap {|logger| logger.level = ::Logger::Severity::DEBUG}
47
+ end
48
+ logger.debug(message, &block)
49
+ end
50
+
29
51
  def all_resources
30
52
  @all_resources ||= []
31
53
  end
32
54
 
33
55
  def process_preflight(env)
34
56
  resource = find_resource(env['HTTP_ORIGIN'], env['PATH_INFO'])
35
- resource.to_preflight_headers(env) if resource
57
+ resource && resource.process_preflight(env)
36
58
  end
37
59
 
38
60
  def process_cors(env)
@@ -47,20 +69,34 @@ module Rack
47
69
 
48
70
  class Resources
49
71
  def initialize
50
- @origins = []
72
+ @origins = []
51
73
  @resources = []
74
+ @public_resources = false
52
75
  end
53
76
 
54
77
  def origins(*args)
55
- @origins = args.flatten.collect{|n| "http://#{n}" unless n.match(/^https?:\/\//)}
78
+ @origins = args.flatten.collect do |n|
79
+ case n
80
+ when /^https?:\/\// then n
81
+ when '*'
82
+ @public_resources = true
83
+ n
84
+ else
85
+ "http://#{n}"
86
+ end
87
+ end
56
88
  end
57
89
 
58
90
  def resource(path, opts={})
59
- @resources << Resource.new(path, opts)
91
+ @resources << Resource.new(public_resources?, path, opts)
92
+ end
93
+
94
+ def public_resources?
95
+ @public_resources
60
96
  end
61
97
 
62
98
  def allow_origin?(source)
63
- @origins.include?(source)
99
+ public_resources? || @origins.include?(source)
64
100
  end
65
101
 
66
102
  def find_resource(path)
@@ -71,36 +107,70 @@ module Rack
71
107
  class Resource
72
108
  attr_accessor :path, :methods, :headers, :max_age, :credentials, :pattern
73
109
 
74
- def initialize(path, opts = {})
110
+ def initialize(public_resource, path, opts={})
75
111
  self.path = path
76
112
  self.methods = ensure_enum(opts[:methods]) || [:get]
77
113
  self.credentials = opts[:credentials] || true
78
- self.headers = ensure_enum(opts[:headers]) || nil
79
114
  self.max_age = opts[:max_age] || 1728000
80
115
  self.pattern = compile(path)
116
+ @public_resource = public_resource
117
+
118
+ self.headers = case opts[:headers]
119
+ when :any then :any
120
+ when nil then nil
121
+ else
122
+ [opts[:headers]].flatten.collect{|h| h.downcase}
123
+ end
81
124
  end
82
125
 
83
126
  def match?(path)
84
127
  pattern =~ path
85
128
  end
86
129
 
87
- def to_headers(env)
88
- { 'Access-Control-Allow-Origin' => env['HTTP_ORIGIN'],
89
- 'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
90
- 'Access-Control-Allow-Credentials' => credentials.to_s,
91
- 'Access-Control-Max-Age' => max_age.to_s }
130
+ def process_preflight(env)
131
+ return nil if invalid_method_request?(env) || invalid_headers_request?(env)
132
+ to_preflight_headers(env)
92
133
  end
93
134
 
94
- def to_preflight_headers(env)
95
- h = to_headers(env)
96
- h.merge!('Access-Control-Allow-Headers' => headers.join(', ')) if headers
135
+ def to_headers(env)
136
+ h = { 'Access-Control-Allow-Origin' => public_resource? ? '*' : env['HTTP_ORIGIN'],
137
+ 'Access-Control-Allow-Methods' => methods.collect{|m| m.to_s.upcase}.join(', '),
138
+ 'Access-Control-Max-Age' => max_age.to_s }
139
+ h['Access-Control-Allow-Credentials'] = 'true' if credentials
97
140
  h
98
141
  end
99
142
 
100
143
  protected
144
+ def public_resource?
145
+ @public_resource
146
+ end
147
+
148
+ def to_preflight_headers(env)
149
+ h = to_headers(env)
150
+ if env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
151
+ h.merge!('Access-Control-Allow-Headers' => env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])
152
+ end
153
+ h
154
+ end
155
+
156
+ def invalid_method_request?(env)
157
+ request_method = env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
158
+ request_method.nil? || !methods.include?(request_method.downcase.to_sym)
159
+ end
160
+
161
+ def invalid_headers_request?(env)
162
+ request_headers = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
163
+ request_headers && !allow_headers?(request_headers)
164
+ end
165
+
166
+ def allow_headers?(request_headers)
167
+ return false if headers.nil?
168
+ headers == :any || !request_headers.detect{|h| !headers.include?(h.downcase)}
169
+ end
170
+
101
171
  def ensure_enum(v)
102
172
  return nil if v.nil?
103
- [v] unless v.respond_to?(:join)
173
+ [v].flatten
104
174
  end
105
175
 
106
176
  def compile(path)
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rack-cors}
8
- s.version = "0.1.1"
8
+ s.version = "0.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Calvin Yu"]
12
- s.date = %q{2010-06-02}
12
+ s.date = %q{2010-06-08}
13
13
  s.email = %q{csyu77@gmail.com}
14
14
  s.extra_rdoc_files = [
15
15
  "README.rdoc"
@@ -20,13 +20,18 @@ Gem::Specification.new do |s|
20
20
  "Rakefile",
21
21
  "VERSION",
22
22
  "lib/rack/cors.rb",
23
- "rack-cors.gemspec"
23
+ "rack-cors.gemspec",
24
+ "test/cors_test.rb",
25
+ "test/test.ru"
24
26
  ]
25
27
  s.homepage = %q{http://github.com/cyu/rack-cors}
26
28
  s.rdoc_options = ["--charset=UTF-8"]
27
29
  s.require_paths = ["lib"]
28
30
  s.rubygems_version = %q{1.3.7}
29
31
  s.summary = %q{Middleware for enabling Cross-Origin Resource Sharing in Rack apps}
32
+ s.test_files = [
33
+ "test/cors_test.rb"
34
+ ]
30
35
 
31
36
  if s.respond_to? :specification_version then
32
37
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
@@ -0,0 +1,80 @@
1
+ require 'rubygems'
2
+ require 'rack/test'
3
+ require 'shoulda'
4
+
5
+ Rack::Test::Session.class_eval do
6
+ def options(uri, params = {}, env = {}, &block)
7
+ env = env_for(uri, env.merge(:method => "OPTIONS", :params => params))
8
+ process_request(uri, env, &block)
9
+ end
10
+ end
11
+
12
+ Rack::Test::Methods.class_eval do
13
+ def_delegator :current_session, :options
14
+ end
15
+
16
+ class CorsTest < Test::Unit::TestCase
17
+ include Rack::Test::Methods
18
+
19
+ def app
20
+ eval "Rack::Builder.new {( " + File.read(File.dirname(__FILE__) + '/test.ru') + "\n )}"
21
+ end
22
+
23
+ context 'preflight requests' do
24
+ should 'fail if origin is invalid' do
25
+ preflight_request('http://allyourdataarebelongtous.com', '/')
26
+ assert_preflight_failure
27
+ end
28
+
29
+ should 'fail if Access-Control-Request-Method does not exist' do
30
+ preflight_request('http://localhost:3000', '/', :method => nil)
31
+ assert_preflight_failure
32
+ end
33
+
34
+ should 'fail if Access-Control-Request-Method is not allowed' do
35
+ preflight_request('http://localhost:3000', '/get-only', :method => :post)
36
+ assert_preflight_failure
37
+ end
38
+
39
+ should 'fail if header is not allowed' do
40
+ preflight_request('http://localhost:3000', '/single_header', :headers => 'Fooey')
41
+ assert_preflight_failure
42
+ end
43
+
44
+ should 'allow any header if headers = :any' do
45
+ preflight_request('http://localhost:3000', '/', :headers => 'Fooey')
46
+ assert_preflight_success
47
+ end
48
+
49
+ should 'allow header case insensitive match' do
50
+ preflight_request('http://localhost:3000', '/single_header', :headers => 'X-Domain-Token')
51
+ assert_preflight_success
52
+ end
53
+
54
+ should '* origin should allow any origin' do
55
+ preflight_request('http://locohost:3000', '/public')
56
+ assert_preflight_success
57
+ assert_equal '*', last_response.headers['Access-Control-Allow-Origin']
58
+ end
59
+ end
60
+
61
+ protected
62
+ def preflight_request(origin, path, opts = {})
63
+ header 'Origin', origin
64
+ unless opts.key?(:method) && opts[:method].nil?
65
+ header 'Access-Control-Request-Method', opts[:method] ? opts[:method].to_s.upcase : 'GET'
66
+ end
67
+ if opts[:headers]
68
+ header 'Access-Control-Request-Headers', [opts[:headers]].flatten.join(', ')
69
+ end
70
+ options path
71
+ end
72
+
73
+ def assert_preflight_success
74
+ assert_not_nil last_response.headers['Access-Control-Allow-Origin']
75
+ end
76
+
77
+ def assert_preflight_failure
78
+ assert_nil last_response.headers['Access-Control-Allow-Origin']
79
+ end
80
+ end
@@ -0,0 +1,24 @@
1
+ require 'rack/cors'
2
+
3
+ use Rack::Cors do |cfg|
4
+ cfg.allow do |allow|
5
+ allow.origins 'localhost:3000', '127.0.0.1:3000'
6
+
7
+ allow.resource '/get-only', :methods => :get
8
+ allow.resource '/', :headers => :any
9
+ allow.resource '/single_header', :headers => 'x-domain-token'
10
+ # allow.resource '/file/at/*',
11
+ # :methods => [:get, :post, :put, :delete],
12
+ # :headers => :any,
13
+ # :max_age => 0
14
+ end
15
+
16
+ cfg.allow do |allow|
17
+ allow.origins '*'
18
+ allow.resource '/public'
19
+ end
20
+ end
21
+
22
+ map '/' do
23
+ run Proc.new { |env| [200, {'Content-Type' => 'text/html'}, ['success']] }
24
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-cors
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 1
10
- version: 0.1.1
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Calvin Yu
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-06-02 00:00:00 -04:00
18
+ date: 2010-06-08 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -34,6 +34,8 @@ files:
34
34
  - VERSION
35
35
  - lib/rack/cors.rb
36
36
  - rack-cors.gemspec
37
+ - test/cors_test.rb
38
+ - test/test.ru
37
39
  has_rdoc: true
38
40
  homepage: http://github.com/cyu/rack-cors
39
41
  licenses: []
@@ -68,5 +70,5 @@ rubygems_version: 1.3.7
68
70
  signing_key:
69
71
  specification_version: 3
70
72
  summary: Middleware for enabling Cross-Origin Resource Sharing in Rack apps
71
- test_files: []
72
-
73
+ test_files:
74
+ - test/cors_test.rb