strelka-cors 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|