rack-cors 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.

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