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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/ChangeLog +52 -0
- data/History.md +4 -0
- data/Manifest.txt +14 -0
- data/README.md +134 -0
- data/Rakefile +87 -0
- data/lib/strelka/app/cors.rb +111 -0
- data/lib/strelka/cors.rb +23 -0
- data/lib/strelka/httprequest/cors.rb +55 -0
- data/lib/strelka/httpresponse/cors.rb +175 -0
- data/spec/helpers.rb +74 -0
- data/spec/strelka/app/cors_spec.rb +193 -0
- data/spec/strelka/cors_spec.rb +15 -0
- data/spec/strelka/httprequest/cors_spec.rb +91 -0
- data/spec/strelka/httpresponse/cors_spec.rb +234 -0
- metadata +231 -0
- metadata.gz.sig +0 -0
@@ -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
|
data/spec/helpers.rb
ADDED
@@ -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
|
+
|