strelka-cors 0.0.1

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.
@@ -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
+