strelka-cors 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,175 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'strelka/httpresponse'
5
+
6
+
7
+ # CORS-related extensions for Strelka HTTP response objects.
8
+ module Strelka::HTTPResponse::CORS
9
+ extend Strelka::MethodUtilities
10
+
11
+
12
+ ### Add some instance variables to the request object.
13
+ def initialize( * ) # :notnew:
14
+ @exposed_headers = []
15
+ @allowed_headers = []
16
+ @allowed_methods = []
17
+ @allowed_origin = nil
18
+ @credentials_allowed = false
19
+ @access_control_max_age = nil
20
+ super
21
+ end
22
+
23
+
24
+ ######
25
+ public
26
+ ######
27
+
28
+ ##
29
+ # The Array of raw header names that should be exposed on the request.
30
+ attr_accessor :exposed_headers
31
+
32
+ ##
33
+ # The Array of raw header names that should be allowed on a preflighted request
34
+ attr_accessor :allowed_headers
35
+
36
+ ##
37
+ # The Array of raw HTTP verb names that should be allowed on a preflighted request
38
+ attr_accessor :allowed_methods
39
+
40
+ ##
41
+ # The origin that should be allowed by the response.
42
+ attr_reader :allowed_origin
43
+
44
+ ##
45
+ # The number of seconds a preflight request can be cached
46
+ attr_accessor :access_control_max_age
47
+
48
+
49
+ ##
50
+ # Whether or not credentials are allowed in the preflighted request
51
+ attr_predicate_accessor :credentials_allowed
52
+
53
+
54
+ ### Set the allowed origin for the response.
55
+ def allow_origin( new_origin )
56
+ @allowed_origin = new_origin
57
+ end
58
+
59
+
60
+ ### Set the headers of the response to indicate that any Origin is allowed.
61
+ def allow_any_origin
62
+ self.allow_origin( '*' )
63
+ end
64
+
65
+
66
+ ### Add +header_names+ to the list of headers that should be exposed in the
67
+ ### response.
68
+ def expose_headers( *header_names )
69
+ self.exposed_headers ||= []
70
+ self.exposed_headers += header_names
71
+ end
72
+ alias_method :expose_header, :expose_headers
73
+
74
+
75
+ ### Add +header_names+ to the list of headers that should be allowed in a
76
+ ### preflighted request.
77
+ def allow_headers( *header_names )
78
+ self.allowed_headers ||= []
79
+ self.allowed_headers += header_names
80
+ end
81
+ alias_method :allow_header, :allow_headers
82
+
83
+
84
+ ### Add +verbs+ to the list of HTTP methods that should be allowed in a
85
+ ### preflighted request.
86
+ def allow_methods( *verbs )
87
+ self.allowed_methods ||= []
88
+ self.allowed_methods += verbs
89
+ end
90
+ alias_method :allow_method, :allow_methods
91
+
92
+
93
+ ### Allow credentials in a preflighted request.
94
+ def allow_credentials
95
+ self.credentials_allowed = true
96
+ end
97
+ alias_method :allow_cookies, :allow_credentials
98
+
99
+
100
+ ### Add any CORS headers which have been set up to the receiving response.
101
+ def add_cors_headers
102
+ origin = self.allowed_origin || self.request.origin.to_s
103
+ if self.set_header_if_present( :allow_origin, origin ) && origin != '*'
104
+ if (( current_vary = self.header.vary ))
105
+ self.header.vary = [current_vary, 'origin'].join( ', ' )
106
+ else
107
+ self.header.vary = 'origin'
108
+ end
109
+ end
110
+
111
+ self.set_header_if_present( :allow_credentials, self.credentials_allowed? )
112
+
113
+ if self.request.is_preflight?
114
+ self.log.debug "Preflight response; adding -Allow- headers"
115
+ self.set_header_if_present( :allow_headers, self.allow_headers_header )
116
+ self.set_header_if_present( :allow_methods, self.allow_methods_header )
117
+ self.set_header_if_present( :max_age, self.access_control_max_age_header )
118
+ else
119
+ self.log.debug "Regular response; adding -Expose- headers"
120
+ self.header.access_control_expose_headers = self.expose_headers_header
121
+ end
122
+ end
123
+
124
+
125
+ #########
126
+ protected
127
+ #########
128
+
129
+ ### If +value+ is not nil or empty, set the access control header with the
130
+ ### specified +name+ to it.
131
+ def set_header_if_present( name, value )
132
+ return unless value && !value.to_s.empty?
133
+ header_name = "access_control_%s" % [ name ]
134
+ self.header[ header_name ] = value.to_s
135
+
136
+ return value
137
+ end
138
+
139
+
140
+ ### Return the value that should be set on the Access-Control-Expose-Headers
141
+ ### header according to the response's #exposed_headers.
142
+ def expose_headers_header
143
+ return nil unless self.exposed_headers && !self.exposed_headers.empty?
144
+ return self.exposed_headers.map do |header_name|
145
+ header_name.to_s.split( /[\-_]+/ ).map( &:capitalize ).join( '-' )
146
+ end.sort.uniq.join( ' ' )
147
+ end
148
+
149
+
150
+ ### Return the value that should be set on the Access-Control-Allow-Headers
151
+ ### header according to the response's #allowed_headers.
152
+ def allow_headers_header
153
+ return nil unless self.allowed_headers && !self.allowed_headers.empty?
154
+ return self.allowed_headers.map do |header_name|
155
+ header_name.to_s.split( /[\-_]+/ ).map( &:capitalize ).join( '-' )
156
+ end.sort.uniq.join( ' ' )
157
+ end
158
+
159
+
160
+ ### Return the value that should be set on the Access-Control-Allow-Methods
161
+ ### header according to the response's #allowed_methods.
162
+ def allow_methods_header
163
+ return nil unless self.allowed_methods && !self.allowed_methods.empty?
164
+ return self.allowed_methods.map( &:to_s ).sort.uniq.join( ' ' )
165
+ end
166
+
167
+
168
+ ### Return the value that should be set on the Access-Control-Max-Age header
169
+ ### according to the responses #access_control_max_age
170
+ def access_control_max_age_header
171
+ max_age = self.access_control_max_age or return nil
172
+ return max_age.to_i.to_s
173
+ end
174
+
175
+ end # module Strelka::HTTPResponse::CORS
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/ruby
2
+ # coding: utf-8
3
+
4
+ BEGIN {
5
+ require 'pathname'
6
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent
7
+
8
+ srcdir = basedir.parent
9
+ strelkadir = srcdir + 'Strelka'
10
+ strelkalibdir = strelkadir + 'lib'
11
+
12
+ $LOAD_PATH.unshift( strelkalibdir.to_s ) unless $LOAD_PATH.include?( strelkalibdir.to_s )
13
+ }
14
+
15
+ # SimpleCov test coverage reporting; enable this using the :coverage rake task
16
+ if ENV['COVERAGE']
17
+ $stderr.puts "\n\n>>> Enabling coverage report.\n\n"
18
+ require 'simplecov'
19
+ SimpleCov.start do
20
+ add_filter 'spec'
21
+ add_group "Needing tests" do |file|
22
+ file.covered_percent < 90
23
+ end
24
+ end
25
+ end
26
+
27
+ require 'loggability'
28
+ require 'loggability/spechelpers'
29
+ require 'configurability'
30
+
31
+ require 'rspec'
32
+ require 'mongrel2'
33
+ require 'mongrel2/testing'
34
+
35
+ require 'strelka'
36
+ require 'strelka/testing'
37
+
38
+
39
+ Loggability.format_with( :color ) if $stdout.tty?
40
+
41
+
42
+ ### RSpec helper functions.
43
+ module Strelka::CORSSpecHelpers
44
+
45
+ # Send and receive specs for the test app
46
+ TEST_SEND_SPEC = 'tcp://127.0.0.1:9997'
47
+ TEST_RECV_SPEC = 'tcp://127.0.0.1:9996'
48
+
49
+ end # Strelka::MetriksSpecHelpers
50
+
51
+
52
+ abort "You need a version of RSpec >= 2.6.0" unless defined?( RSpec )
53
+
54
+ ### Mock with RSpec
55
+ RSpec.configure do |config|
56
+ include Strelka::Constants
57
+ include Strelka::CORSSpecHelpers
58
+
59
+ config.run_all_when_everything_filtered = true
60
+ config.filter_run :focus
61
+ config.order = 'random'
62
+ config.mock_with( :rspec ) do |mock|
63
+ mock.syntax = :expect
64
+ end
65
+
66
+ config.include( Loggability::SpecHelpers )
67
+ config.include( Mongrel2::SpecHelpers )
68
+ config.include( Strelka::Constants )
69
+ config.include( Strelka::Testing )
70
+ config.include( Strelka::CORSSpecHelpers )
71
+ end
72
+
73
+ # vim: set nosta noet ts=4 sw=4:
74
+
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env rspec -cfd -b
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require 'rspec'
6
+
7
+ require 'strelka'
8
+ require 'mongrel2/testing'
9
+ require 'strelka/testing'
10
+ require 'strelka/behavior/plugin'
11
+
12
+ require 'strelka/app/cors'
13
+
14
+
15
+ describe Strelka::App::CORS do
16
+
17
+ before( :all ) do
18
+ @request_factory = Mongrel2::RequestFactory.new(
19
+ host: 'acme.com',
20
+ port: 80,
21
+ route: '/api/v1',
22
+ headers: {
23
+ origin: 'https://acme.com/'
24
+ }
25
+ )
26
+ end
27
+
28
+
29
+ it_should_behave_like( "A Strelka Plugin" )
30
+
31
+
32
+ it "adds a method to applications to declare access control rules" do
33
+ app = Class.new( Strelka::App ) do
34
+ plugins :cors
35
+ end
36
+
37
+ expect( app ).to respond_to( :access_controls )
38
+ expect( app ).to respond_to( :cors_access_control )
39
+ end
40
+
41
+
42
+ it "adds the CORS mixin to the request class" do
43
+ app = Class.new( Strelka::App ) do
44
+ plugins :cors
45
+ end
46
+ app.install_plugins
47
+
48
+ response = @request_factory.get( '/api/v1/verify' )
49
+
50
+ expect( response ).to respond_to( :cross_origin? )
51
+ end
52
+
53
+
54
+ it "adds the CORS mixin to the response class" do
55
+ app = Class.new( Strelka::App ) do
56
+ plugins :cors
57
+ end
58
+ app.install_plugins
59
+
60
+ response = @request_factory.get( '/api/v1/verify' ).response
61
+
62
+ expect( response ).to respond_to( :credentials_allowed? )
63
+ end
64
+
65
+
66
+ describe "in an app" do
67
+
68
+ let( :appclass ) do
69
+ Class.new( Strelka::App ) do
70
+ plugins :cors
71
+
72
+ def initialize( appid='cors-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
73
+ super
74
+ end
75
+
76
+ def handle_request( req )
77
+ super do
78
+ res = req.response
79
+ res.status = HTTP::OK
80
+ res.content_type = 'text/plain'
81
+ res.puts "Ran successfully."
82
+
83
+ res
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+
90
+ context "handling a regular request" do
91
+
92
+ it "sets a default Access-Control-Allow-Origin header on responses" do
93
+ request = @request_factory.get( '/api/v1/verify' )
94
+
95
+ response = appclass.new.handle( request )
96
+
97
+ expect( response.headers ).to include( :access_control_allow_origin )
98
+ expect( response.headers.access_control_allow_origin ).to eq( request.origin.to_s )
99
+ end
100
+
101
+ end
102
+
103
+
104
+
105
+ context "handles a pre-flight request" do
106
+
107
+ it "runs access controls blocks that match the request's path" do
108
+ request = @request_factory.options( '/api/v1/verify',
109
+ access_control_request_method: 'POST'
110
+ )
111
+
112
+ appclass.access_control( '/verify' ) do |req, res|
113
+ res.allow_origin( '*' )
114
+ res.allow_headers( 'Content-Type', 'X-Object-Owner' )
115
+ end
116
+ response = appclass.new.handle( request )
117
+
118
+ expect( response.headers ).to include(
119
+ :access_control_allow_origin,
120
+ :access_control_allow_headers
121
+ )
122
+ expect( response.headers.access_control_allow_origin ).to eq( '*' )
123
+ expect( response.headers.access_control_allow_headers ).
124
+ to eq( 'Content-Type X-Object-Owner' )
125
+ end
126
+
127
+
128
+ it "runs access controls blocks that match the request's path as a Regexp" do
129
+ request = @request_factory.options( '/api/v1/verify',
130
+ access_control_request_method: 'POST'
131
+ )
132
+
133
+ appclass.access_control( %r{\A/(verify|concede|command)} ) do |req, res|
134
+ res.allow_origin( '*' )
135
+ res.allow_headers( 'Content-Type', 'X-Object-Owner' )
136
+ end
137
+ response = appclass.new.handle( request )
138
+
139
+ expect( response.headers ).to include(
140
+ :access_control_allow_origin,
141
+ :access_control_allow_headers
142
+ )
143
+ expect( response.headers.access_control_allow_origin ).to eq( '*' )
144
+ expect( response.headers.access_control_allow_headers ).
145
+ to eq( 'Content-Type X-Object-Owner' )
146
+ end
147
+
148
+
149
+ it "runs access controls blocks that don't specify a path" do
150
+ request = @request_factory.options( '/api/v1/verify',
151
+ access_control_request_method: 'POST'
152
+ )
153
+
154
+ appclass.access_control do |req, res|
155
+ res.allow_origin( 'https://acme.com/' )
156
+ res.allow_methods( :GET, :HEAD, :POST )
157
+ end
158
+ response = appclass.new.handle( request )
159
+
160
+ expect( response.headers ).to include(
161
+ :access_control_allow_origin,
162
+ :access_control_allow_methods,
163
+ :vary
164
+ )
165
+ expect( response.headers.access_control_allow_origin ).
166
+ to eq( 'https://acme.com/' )
167
+ expect( response.headers.vary.downcase.split(/\s*,\s*/) ).to include( 'origin' )
168
+ expect( response.headers.access_control_allow_methods ).
169
+ to eq( 'GET HEAD POST' )
170
+ end
171
+
172
+
173
+ it "doesn't run access controls blocks that don't match the request's path" do
174
+ request = @request_factory.options( '/api/v1/verify',
175
+ access_control_request_method: 'POST'
176
+ )
177
+
178
+ appclass.access_control( 'optimise' ) do |req, res|
179
+ res.allow_origin( '*' )
180
+ res.allow_headers( 'Content-Type', 'X-Object-Owner' )
181
+ end
182
+ response = appclass.new.handle( request )
183
+
184
+ expect( response.headers ).to_not include( :access_control_allow_headers )
185
+ expect( response.headers.access_control_allow_origin ).to eq( request.origin.to_s )
186
+ end
187
+
188
+ end
189
+
190
+ end
191
+
192
+ end
193
+
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'strelka/cors'
6
+
7
+
8
+ describe Strelka::CORS do
9
+
10
+ it "knows what version of the library it is" do
11
+ expect( described_class::VERSION ).to be_a( String ).and( match(/\A\d+\.\d+\.\d+\z/) )
12
+ end
13
+
14
+ end
15
+