secure_escrow 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +0 -2
- data/Readme.md +19 -13
- data/lib/assets/javascripts/secure_escrow.js +18 -19
- data/lib/secure_escrow/middleware.rb +75 -44
- data/lib/secure_escrow/railtie.rb +18 -12
- data/lib/secure_escrow/version.rb +1 -1
- data/secure_escrow.gemspec +1 -1
- data/spec/middleware_spec.rb +180 -49
- data/spec/mock_engine.rb +13 -10
- data/spec/mock_rack_app.rb +6 -0
- data/spec/mock_redis.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- metadata +7 -5
data/Gemfile.lock
CHANGED
@@ -2,7 +2,6 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
secure_escrow (0.0.1)
|
5
|
-
redis
|
6
5
|
|
7
6
|
GEM
|
8
7
|
remote: http://rubygems.org/
|
@@ -65,7 +64,6 @@ GEM
|
|
65
64
|
rake (0.9.2.2)
|
66
65
|
rb-appscript (0.6.1)
|
67
66
|
rb-fsevent (0.4.3.1)
|
68
|
-
redis (2.2.2)
|
69
67
|
rspec (2.7.0)
|
70
68
|
rspec-core (~> 2.7.0)
|
71
69
|
rspec-expectations (~> 2.7.0)
|
data/Readme.md
CHANGED
@@ -35,16 +35,18 @@ SecureEscrow has 5 integration points with a Rails application
|
|
35
35
|
- Forms generated by views
|
36
36
|
|
37
37
|
### Install as Middleware
|
38
|
-
Add the SecureEscrow::Middleware around your Rails application. It must be configured with
|
38
|
+
Add the SecureEscrow::Middleware around your Rails application. It must be configured with both a Rack endpoint to call, and a Rails app instance to retrieve routes and configuration information from.
|
39
39
|
In this example, the <tt>Awesome::Application.config.redis</tt> value is available after the <tt>environment</tt> file has been run.
|
40
40
|
|
41
|
-
Example <tt>config.
|
41
|
+
Example <tt>config/initializers/secure_escrow.rb</tt>:
|
42
42
|
|
43
43
|
````ruby
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
Awesome::Application.middleware.insert_before(
|
45
|
+
Rack::Lock,
|
46
|
+
SecureEscrow::Middleware,
|
47
|
+
Awesome::Application,
|
48
|
+
store: Awesome::Application.config.redis
|
49
|
+
)
|
48
50
|
````
|
49
51
|
|
50
52
|
### Configure Domain Information
|
@@ -54,12 +56,14 @@ may differ for various environments. For example, in <tt>development.rb</tt> you
|
|
54
56
|
|
55
57
|
````ruby
|
56
58
|
# = SecureEscrow config =
|
57
|
-
config.
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
59
|
+
config.secure_escrow = {
|
60
|
+
secure_domain_name: 'www.secure-awesome.devlocal',
|
61
|
+
secure_domain_protocol: 'http',
|
62
|
+
secure_domain_port: 3000,
|
63
|
+
insecure_domain_name: 'www.insecure-awesome.devlocal',
|
64
|
+
insecure_domain_protocol: 'http',
|
65
|
+
insecure_domain_port: 3000,
|
66
|
+
}
|
63
67
|
`````
|
64
68
|
|
65
69
|
### Declare Escrowed Routes
|
@@ -80,10 +84,12 @@ escrow 'signin' => 'sessions#create', as: :user_session
|
|
80
84
|
get 'signout'=> 'sessions#destroy', as: :destroy_user_session
|
81
85
|
````
|
82
86
|
|
87
|
+
Alternatively, SecureEscrow can be configured to escrow any POST action without explicitly declaring it within the application. Just provide `allow_non_escrow_routes: true` to the initial SecureEscrow configuration.
|
88
|
+
|
83
89
|
### Deliver JavaScript assets
|
84
90
|
SecureEscrow integrates in the Rails asset pipeline. Just add the following to your <tt>application.js</tt>.
|
85
91
|
|
86
|
-
````
|
92
|
+
````javascript
|
87
93
|
// SecureEscrow
|
88
94
|
// ======================
|
89
95
|
//= require secure_escrow
|
@@ -14,12 +14,18 @@
|
|
14
14
|
// Consumes iframe content and bubbles it up as an ajax response
|
15
15
|
function onLoad() {
|
16
16
|
var payload_error,
|
17
|
-
|
17
|
+
inner_text, http_status, wrapper_json, json, response;
|
18
18
|
|
19
19
|
try {
|
20
|
-
|
20
|
+
inner_text = $(iframe[0].contentWindow.document).find("#response").text();
|
21
21
|
try {
|
22
|
-
|
22
|
+
wrapper_json = JSON.parse(inner_text);
|
23
|
+
http_status = wrapper_json.status;
|
24
|
+
try {
|
25
|
+
json = JSON.parse(wrapper_json.body);
|
26
|
+
} catch(_3) {
|
27
|
+
payload_error = 'Error parsing original body';
|
28
|
+
}
|
23
29
|
} catch(_2) {
|
24
30
|
payload_error = 'Error parsing JSON response';
|
25
31
|
}
|
@@ -27,23 +33,18 @@
|
|
27
33
|
payload_error = 'Error accessing response body';
|
28
34
|
}
|
29
35
|
|
30
|
-
|
31
36
|
response = {
|
32
|
-
responseText:
|
37
|
+
responseText: wrapper_json.body
|
33
38
|
};
|
34
39
|
|
35
|
-
if (
|
36
|
-
|
37
|
-
} else {
|
38
|
-
response.responseJSON = json;
|
39
|
-
}
|
40
|
-
|
41
|
-
if (json && json.success) {
|
42
|
-
form.trigger('ajax:success', response);
|
40
|
+
if (http_status >= 200 && http_status < 300) {
|
41
|
+
form.trigger('ajax:success', [json]);
|
43
42
|
} else {
|
44
|
-
form.trigger('ajax:error', response);
|
43
|
+
form.trigger('ajax:error', [response]);
|
45
44
|
}
|
46
|
-
form.trigger('ajax:complete', response);
|
45
|
+
form.trigger('ajax:complete', [response]);
|
46
|
+
|
47
|
+
iframe.remove();
|
47
48
|
}
|
48
49
|
|
49
50
|
iframe.load(onLoad);
|
@@ -57,10 +58,8 @@
|
|
57
58
|
escrow = form.data('escrow'),
|
58
59
|
isEscrow = escrow !== undefined;
|
59
60
|
|
60
|
-
if (isEscrow) {
|
61
|
-
|
62
|
-
handleRemoteEscrowSubmission(form);
|
63
|
-
}
|
61
|
+
if (isEscrow && 'iframe' === escrow) {
|
62
|
+
handleRemoteEscrowSubmission(form);
|
64
63
|
}
|
65
64
|
|
66
65
|
});
|
@@ -9,8 +9,11 @@ module SecureEscrow
|
|
9
9
|
POST = 'POST'
|
10
10
|
GET = 'GET'
|
11
11
|
COOKIE_SEPARATOR = ';'
|
12
|
+
EXPIRE_COOKIE = Time.gm(1979, 1, 1)
|
12
13
|
RAILS_ROUTES = 'action_dispatch.routes'
|
13
14
|
LOCATION = 'Location'
|
15
|
+
CONTENT_TYPE = 'Content-Type'
|
16
|
+
JSON_CONTENT = /^application\/json/
|
14
17
|
ESCROW_MATCH = /^(.+)\.(.+)$/
|
15
18
|
TTL = 180 # Seconds until proxied response expires
|
16
19
|
NONCE = 'nonce'
|
@@ -21,9 +24,11 @@ module SecureEscrow
|
|
21
24
|
end
|
22
25
|
|
23
26
|
class Middleware
|
24
|
-
def initialize
|
25
|
-
@
|
26
|
-
@
|
27
|
+
def initialize next_app, rails_app, config
|
28
|
+
@next_app = next_app
|
29
|
+
@rails_app = rails_app
|
30
|
+
@config = config
|
31
|
+
@store = config[:store]
|
27
32
|
end
|
28
33
|
|
29
34
|
def call env
|
@@ -31,7 +36,7 @@ module SecureEscrow
|
|
31
36
|
end
|
32
37
|
|
33
38
|
def presenter env
|
34
|
-
Presenter.new @
|
39
|
+
Presenter.new @next_app, @rails_app, @config, env
|
35
40
|
end
|
36
41
|
|
37
42
|
def handle_presenter e
|
@@ -49,12 +54,14 @@ module SecureEscrow
|
|
49
54
|
class Presenter
|
50
55
|
include MiddlewareConstants
|
51
56
|
|
52
|
-
attr_reader :
|
57
|
+
attr_reader :next_app, :rails_app, :store, :config, :env
|
53
58
|
|
54
|
-
def initialize
|
55
|
-
@
|
56
|
-
@
|
57
|
-
@
|
59
|
+
def initialize next_app, rails_app, config, env
|
60
|
+
@next_app = next_app
|
61
|
+
@rails_app = rails_app
|
62
|
+
@config = config
|
63
|
+
@store = config[:store]
|
64
|
+
@env = env
|
58
65
|
end
|
59
66
|
|
60
67
|
def serve_response_from_escrow?
|
@@ -70,9 +77,11 @@ module SecureEscrow
|
|
70
77
|
end
|
71
78
|
|
72
79
|
def store_response_in_escrow?
|
73
|
-
|
74
|
-
|
75
|
-
|
80
|
+
return false unless POST == env[REQUEST_METHOD]
|
81
|
+
recognized = recognize_path
|
82
|
+
config[:allow_non_escrow_routes] ?
|
83
|
+
recognized :
|
84
|
+
recognized && recognized[:escrow]
|
76
85
|
end
|
77
86
|
|
78
87
|
def serve_response_from_escrow!
|
@@ -83,7 +92,20 @@ module SecureEscrow
|
|
83
92
|
# Destroy the stored value
|
84
93
|
store.del key
|
85
94
|
|
86
|
-
|
95
|
+
status, headers, body = value[RESPONSE]
|
96
|
+
|
97
|
+
if headers[CONTENT_TYPE] && JSON_CONTENT.match(headers[CONTENT_TYPE])
|
98
|
+
body = [
|
99
|
+
"<html><body><script id=\"response\" type=\"text/x-json\">%s</script></body></html>" %
|
100
|
+
{ status: status, body: body.join.to_s }.to_json
|
101
|
+
]
|
102
|
+
headers[CONTENT_TYPE] = "text/html; charset=utf-8"
|
103
|
+
status = 200
|
104
|
+
end
|
105
|
+
|
106
|
+
expire_cookie_token!(headers)
|
107
|
+
|
108
|
+
[ status, headers, body ]
|
87
109
|
else
|
88
110
|
# HTTP Status Code 403 - Forbidden
|
89
111
|
return [ 403, {}, [ BAD_NONCE ] ]
|
@@ -101,7 +123,10 @@ module SecureEscrow
|
|
101
123
|
id, nonce = store_in_escrow status, header, response
|
102
124
|
token = "#{id}.#{nonce}"
|
103
125
|
|
104
|
-
response_headers = {
|
126
|
+
response_headers = {
|
127
|
+
LOCATION => redirect_to_location(token),
|
128
|
+
CONTENT_TYPE => header[CONTENT_TYPE]
|
129
|
+
}
|
105
130
|
set_cookie_token!(response_headers, token) if homogenous_host_names?
|
106
131
|
|
107
132
|
# HTTP Status Code 303 - See Other
|
@@ -147,7 +172,8 @@ module SecureEscrow
|
|
147
172
|
# Serialze the nonce and Rack response triplet,
|
148
173
|
# store in Redis, and set TTL
|
149
174
|
key = escrow_key id
|
150
|
-
|
175
|
+
|
176
|
+
store.setex key, TTL, value.to_json
|
151
177
|
|
152
178
|
[ id, nonce ]
|
153
179
|
end
|
@@ -156,21 +182,15 @@ module SecureEscrow
|
|
156
182
|
[ UUID.generate, SecureRandom.hex(4) ]
|
157
183
|
end
|
158
184
|
|
159
|
-
|
160
|
-
|
161
|
-
Rack::Utils.set_cookie_header!(headers, DATA_KEY,
|
162
|
-
value: token,
|
163
|
-
httponly: true)
|
185
|
+
def call_result
|
186
|
+
@call_result ||= next_app.call env
|
164
187
|
end
|
165
188
|
|
166
189
|
def redirect_to_location token = nil
|
167
|
-
routes = app.routes
|
168
|
-
config = app.config
|
169
|
-
|
170
190
|
redirect_to_options = {
|
171
|
-
protocol:
|
172
|
-
host:
|
173
|
-
port:
|
191
|
+
protocol: rails_config[:insecure_domain_protocol] || request.protocol,
|
192
|
+
host: rails_config[:insecure_domain_name] || request.host,
|
193
|
+
port: rails_config[:insecure_domain_port] || request.port,
|
174
194
|
}
|
175
195
|
|
176
196
|
if token && !homogenous_host_names?
|
@@ -181,37 +201,50 @@ module SecureEscrow
|
|
181
201
|
recognize_path.merge(redirect_to_options))
|
182
202
|
end
|
183
203
|
|
204
|
+
private
|
205
|
+
def rails_config
|
206
|
+
@rails_config ||= rails_app.config.secure_escrow
|
207
|
+
end
|
208
|
+
|
209
|
+
def set_cookie_token! headers, token
|
210
|
+
Rack::Utils.set_cookie_header! headers, DATA_KEY,
|
211
|
+
value: token,
|
212
|
+
httponly: true
|
213
|
+
end
|
214
|
+
|
215
|
+
def expire_cookie_token! headers
|
216
|
+
Rack::Utils.set_cookie_header! headers, DATA_KEY,
|
217
|
+
value: "",
|
218
|
+
httponly: true,
|
219
|
+
expires: EXPIRE_COOKIE
|
220
|
+
end
|
221
|
+
|
184
222
|
def rewrite_location_header! header
|
185
223
|
return unless header[LOCATION]
|
186
224
|
|
187
|
-
config = app.config
|
188
|
-
routes = app.routes
|
189
|
-
|
190
225
|
# Rewrite redirect to secure domain
|
191
226
|
header[LOCATION] = routes.url_for(
|
192
227
|
routes.recognize_path(header[LOCATION]).merge(
|
193
|
-
host:
|
194
|
-
protocol:
|
195
|
-
port:
|
228
|
+
host: rails_config[:insecure_domain_name],
|
229
|
+
protocol: rails_config[:insecure_domain_protocol],
|
230
|
+
port: rails_config[:insecure_domain_port],
|
196
231
|
))
|
197
232
|
|
198
233
|
header
|
199
234
|
end
|
200
235
|
|
201
|
-
def
|
202
|
-
@
|
203
|
-
end
|
204
|
-
|
205
|
-
def rails_routes
|
206
|
-
@rails_routes ||= app.routes
|
236
|
+
def routes
|
237
|
+
@routes ||= rails_app.routes
|
207
238
|
end
|
208
239
|
|
209
240
|
# TODO: Examine the performance implications of parsing the
|
210
241
|
# Cookie / Query payload this early in the stack
|
211
242
|
def escrow_id_and_nonce
|
212
|
-
data = (homogenous_host_names? ?
|
243
|
+
data = Array((homogenous_host_names? ?
|
213
244
|
Rack::Utils.parse_query(env[HTTP_COOKIE], COOKIE_SEPARATOR) :
|
214
|
-
Rack::Utils.parse_query(env[QUERY_STRING]))[DATA_KEY]
|
245
|
+
Rack::Utils.parse_query(env[QUERY_STRING]))[DATA_KEY]).find do |e|
|
246
|
+
e.match ESCROW_MATCH
|
247
|
+
end
|
215
248
|
|
216
249
|
return unless data
|
217
250
|
match = data.match ESCROW_MATCH
|
@@ -221,18 +254,16 @@ module SecureEscrow
|
|
221
254
|
end
|
222
255
|
|
223
256
|
def homogenous_host_names?
|
224
|
-
|
225
|
-
config.secure_domain_name == config.insecure_domain_name
|
257
|
+
rails_config[:secure_domain_name] == rails_config[:insecure_domain_name]
|
226
258
|
end
|
227
259
|
|
228
260
|
def recognize_path
|
229
261
|
begin
|
230
|
-
|
262
|
+
routes.recognize_path(
|
231
263
|
env[REQUEST_PATH],
|
232
264
|
method: env[REQUEST_METHOD]
|
233
265
|
)
|
234
266
|
rescue ActionController::RoutingError
|
235
|
-
{}
|
236
267
|
end
|
237
268
|
end
|
238
269
|
|
@@ -4,44 +4,50 @@ require "action_pack"
|
|
4
4
|
module SecureEscrow
|
5
5
|
module Railtie
|
6
6
|
module Routing
|
7
|
-
def escrow
|
7
|
+
def escrow *args, &block
|
8
|
+
options = args.extract_options!
|
8
9
|
defaults = options[:defaults] || {}
|
9
10
|
defaults[:escrow] = true
|
10
|
-
|
11
|
+
options[:defaults] = defaults
|
12
|
+
args.push options
|
13
|
+
post *args, &block
|
11
14
|
end
|
12
15
|
end
|
13
16
|
|
14
17
|
module ActionViewHelper
|
15
18
|
DATA_ESCROW = 'data-escrow'
|
16
19
|
IFRAME = 'iframe'
|
17
|
-
POST = '
|
20
|
+
POST = 'POST'
|
18
21
|
|
19
22
|
def escrow_form_for record, options = {}, &proc
|
20
23
|
options[:html] ||= {}
|
21
24
|
|
22
|
-
stringy_record =
|
25
|
+
stringy_record = case record
|
26
|
+
when String, Symbol then true
|
27
|
+
else false
|
28
|
+
end
|
23
29
|
apply_form_for_options!(record, options) unless stringy_record
|
24
30
|
|
25
31
|
|
26
|
-
form_for record, escrow_options(options), &proc
|
32
|
+
form_for record, escrow_options(options, POST), &proc
|
27
33
|
end
|
28
34
|
|
29
35
|
def escrow_form_tag url_for_options = {}, options = {}, &block
|
30
|
-
form_tag url_for_options, escrow_options(options), &block
|
36
|
+
form_tag url_for_options, escrow_options(options, POST), &block
|
31
37
|
end
|
32
38
|
|
33
39
|
private
|
34
|
-
def escrow_options options
|
40
|
+
def escrow_options options, method
|
35
41
|
# Rewrite URL to point to secure domain
|
36
42
|
app = Rails.application
|
37
|
-
config = app.config
|
43
|
+
config = app.config.secure_escrow
|
38
44
|
|
39
45
|
submission_url = controller.url_for(
|
40
|
-
app.routes.recognize_path(options[:url]).
|
46
|
+
app.routes.recognize_path(options[:url], method: method).
|
41
47
|
merge(
|
42
|
-
host: config.
|
43
|
-
protocol: config.
|
44
|
-
port: config.
|
48
|
+
host: config[:secure_domain_name] || request.host,
|
49
|
+
protocol: config[:secure_domain_protocol] || request.protocol,
|
50
|
+
port: config[:secure_domain_port] || request.port,
|
45
51
|
))
|
46
52
|
|
47
53
|
options[:url] = submission_url
|
data/secure_escrow.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = SecureEscrow::VERSION
|
8
8
|
s.authors = ["Duncan Beevers"]
|
9
9
|
s.email = ["duncan@dweebd.com"]
|
10
|
-
s.homepage = ""
|
10
|
+
s.homepage = "https://github.com/duncanbeevers/SecureEscrow"
|
11
11
|
s.summary = "Secure AJAX-style actions for Rails applications"
|
12
12
|
s.description = "SecureEscrow provides a content proxy for Rails applications allowing POSTing to secure actions from insecure domains without full-page refreshes"
|
13
13
|
|
data/spec/middleware_spec.rb
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
|
2
2
|
include SecureEscrow::MiddlewareConstants
|
3
3
|
|
4
|
-
describe
|
5
|
-
let(:
|
4
|
+
describe SecureEscrow::Middleware do
|
5
|
+
let(:rack_app) { MockRackApp.new }
|
6
|
+
let(:rails_app) { MockEngine.new }
|
6
7
|
let(:store) { MockRedis.new }
|
7
|
-
let(:
|
8
|
-
let(:
|
8
|
+
let(:config) { { store: store } }
|
9
|
+
let(:middleware) { SecureEscrow::Middleware.new rack_app, rails_app, config }
|
10
|
+
let(:presenter) { SecureEscrow::Middleware::Presenter.new rack_app, rails_app, config, env }
|
9
11
|
let(:env) { {} }
|
10
12
|
|
11
13
|
context 'as a Rack application' do
|
@@ -14,10 +16,16 @@ describe 'SecureEscrow::Middleware' do
|
|
14
16
|
end
|
15
17
|
|
16
18
|
it 'should handle_presenter with wrapped environment' do
|
17
|
-
middleware.should_receive(:
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
middleware.should_receive(:handle_presenter).
|
20
|
+
with(duck_type(
|
21
|
+
:serve_response_from_escrow?,
|
22
|
+
:serve_response_from_escrow!,
|
23
|
+
:response_is_redirect?,
|
24
|
+
:redirect_to_response!,
|
25
|
+
:store_response_in_escrow?,
|
26
|
+
:store_response_in_escrow_and_redirect!,
|
27
|
+
:serve_response_from_application!
|
28
|
+
)).once
|
21
29
|
|
22
30
|
middleware.call(env)
|
23
31
|
end
|
@@ -118,21 +126,21 @@ describe 'SecureEscrow::Middleware' do
|
|
118
126
|
|
119
127
|
describe 'response_is_redirect?' do
|
120
128
|
it 'should not include status codes less than 300' do
|
121
|
-
|
129
|
+
rack_app.should_receive(:call).
|
122
130
|
once.with(env).and_return([ 299, {}, [ '' ] ])
|
123
131
|
|
124
132
|
presenter.response_is_redirect?.should be_false
|
125
133
|
end
|
126
134
|
|
127
135
|
it 'should not include status codes greater than 399' do
|
128
|
-
|
136
|
+
rack_app.should_receive(:call).
|
129
137
|
once.with(env).and_return([ 400, {}, [ '' ] ])
|
130
138
|
|
131
139
|
presenter.response_is_redirect?.should be_false
|
132
140
|
end
|
133
141
|
|
134
142
|
it 'should include 304' do
|
135
|
-
|
143
|
+
rack_app.should_receive(:call).
|
136
144
|
once.with(env).and_return([ 304, {}, [ '' ] ])
|
137
145
|
|
138
146
|
presenter.response_is_redirect?.should be_true
|
@@ -148,11 +156,8 @@ describe 'SecureEscrow::Middleware' do
|
|
148
156
|
it 'should not store non-existent routes' do
|
149
157
|
presenter.env[REQUEST_METHOD] = POST
|
150
158
|
|
151
|
-
|
152
|
-
|
153
|
-
and_raise(
|
154
|
-
ActionController::RoutingError.new("No route matches #{env[REQUEST_PATH]}")
|
155
|
-
)
|
159
|
+
rails_app.routes.stub!(:recognize_path).
|
160
|
+
and_raise(ActionController::RoutingError.new("No route matches #{env[REQUEST_PATH]}"))
|
156
161
|
|
157
162
|
presenter.store_response_in_escrow?.should be_false
|
158
163
|
end
|
@@ -160,17 +165,35 @@ describe 'SecureEscrow::Middleware' do
|
|
160
165
|
it 'should not store non-escrow routes' do
|
161
166
|
presenter.env[REQUEST_METHOD] = POST
|
162
167
|
|
163
|
-
|
168
|
+
rails_app.routes.should_receive(:recognize_path).
|
164
169
|
once.with(env[REQUEST_PATH], { method: POST }).
|
165
170
|
and_return(controller: 'session', action: 'create')
|
166
171
|
|
167
172
|
presenter.store_response_in_escrow?.should be_false
|
168
173
|
end
|
169
174
|
|
175
|
+
describe 'configured not to check for escrowable routes' do
|
176
|
+
let(:config) { { store: store, allow_non_escrow_routes: true } }
|
177
|
+
|
178
|
+
it 'should store https existent, non-escrow routes' do
|
179
|
+
presenter.env[REQUEST_METHOD] = POST
|
180
|
+
|
181
|
+
presenter.store_response_in_escrow?.should be_true
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should not store non-existent routes' do
|
185
|
+
presenter.env[REQUEST_METHOD] = POST
|
186
|
+
rails_app.routes.stub!(:recognize_path).
|
187
|
+
and_raise(ActionController::RoutingError.new("No route matches #{env[REQUEST_PATH]}"))
|
188
|
+
|
189
|
+
presenter.store_response_in_escrow?.should be_false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
170
193
|
it 'should store escrow routes' do
|
171
194
|
presenter.env[REQUEST_METHOD] = POST
|
172
195
|
|
173
|
-
|
196
|
+
rails_app.routes.should_receive(:recognize_path).
|
174
197
|
once.with(env[REQUEST_PATH], { method: POST }).
|
175
198
|
and_return(controller: 'session', action: 'create', escrow: true)
|
176
199
|
|
@@ -196,50 +219,118 @@ describe 'SecureEscrow::Middleware' do
|
|
196
219
|
presenter.serve_response_from_escrow!
|
197
220
|
end
|
198
221
|
|
199
|
-
|
200
|
-
|
201
|
-
|
222
|
+
context 'when the response type is not application/json' do
|
223
|
+
it 'should deliver the original response code' do
|
224
|
+
response = [ 403, {}, [ 'text' ] ]
|
225
|
+
store_in_escrow store, 'id', 'nonce', response
|
226
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
227
|
+
|
228
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
229
|
+
status.should eq 403
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'should deliver the content-type header' do
|
233
|
+
response = [ 403, { 'Content-Type' => 'application/x-waterbuffalo' }, [ 'text' ] ]
|
234
|
+
store_in_escrow store, 'id', 'nonce', response
|
235
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
236
|
+
|
237
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
238
|
+
headers['Content-Type'].should eq 'application/x-waterbuffalo'
|
239
|
+
end
|
240
|
+
|
241
|
+
it 'should deliver the original response body' do
|
242
|
+
response = [ 403, { 'Content-Type' => 'application/x-waterbuffalo' }, [ 'text' ] ]
|
243
|
+
store_in_escrow store, 'id', 'nonce', response
|
244
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
245
|
+
|
246
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
247
|
+
body.should eq [ 'text' ]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'when the response type is application/json; charset=utf-8' do
|
252
|
+
let(:response_headers) { { 'Content-Type' => 'application/json; charset=utf-8' } }
|
253
|
+
it 'should deliver the response with status code 200' do
|
254
|
+
response = [ 403, response_headers, [ 'text' ] ]
|
255
|
+
store_in_escrow store, 'id', 'nonce', response
|
256
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
257
|
+
|
258
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
259
|
+
status.should eq 200
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'should deliver the headers with content-type text/html; charset=utf-8' do
|
263
|
+
response = [ 403, response_headers, [ 'text' ] ]
|
264
|
+
store_in_escrow store, 'id', 'nonce', response
|
265
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
266
|
+
|
267
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
268
|
+
headers['Content-Type'].should eq "text/html; charset=utf-8"
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'should wrap the response in html and json' do
|
272
|
+
response = [ 403, response_headers, [ 'text' ] ]
|
273
|
+
store_in_escrow store, 'id', 'nonce', response
|
274
|
+
set_escrow_cookie presenter, 'id', 'nonce'
|
275
|
+
|
276
|
+
json_representation = "{\"status\":403,\"body\":\"text\"}"
|
277
|
+
|
278
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
279
|
+
body.join.should eq "<html><body><script id=\"response\" type=\"text/x-json\">#{json_representation}</script></body></html>"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'should delete the secure escrow cookie' do
|
284
|
+
store_in_escrow store, 'id', 'nonce', [
|
285
|
+
200, {
|
286
|
+
"Set-Cookie" => "_everloop_session=persists"
|
287
|
+
},
|
288
|
+
[ "" ]
|
289
|
+
]
|
290
|
+
|
202
291
|
set_escrow_cookie presenter, 'id', 'nonce'
|
203
292
|
|
204
|
-
presenter.serve_response_from_escrow
|
293
|
+
status, headers, body = presenter.serve_response_from_escrow!
|
294
|
+
headers["Set-Cookie"].should eq "_everloop_session=persists\nsecure_escrow=; expires=Mon, 01-Jan-1979 00:00:00 GMT; HttpOnly"
|
205
295
|
end
|
206
|
-
|
296
|
+
end
|
207
297
|
|
208
298
|
describe 'redirect_to_response!' do
|
209
299
|
it 'should use status code from application' do
|
210
300
|
response = [ 315, {}, [ '' ] ]
|
211
|
-
|
301
|
+
rack_app.should_receive(:call).
|
212
302
|
once.with(env).and_return(response)
|
213
303
|
|
214
304
|
presenter.redirect_to_response!.should eq response
|
215
305
|
end
|
216
306
|
|
217
307
|
it 'should rewrite location' do
|
308
|
+
config = rails_app.config.secure_escrow
|
218
309
|
original_location = "%s://%s:%s/path/" % [
|
219
|
-
|
220
|
-
|
221
|
-
|
310
|
+
config[:secure_domain_protocol],
|
311
|
+
config[:secure_domain_name],
|
312
|
+
config[:secure_domain_port],
|
222
313
|
]
|
223
314
|
expected_location = "%s://%s:%s/path/" % [
|
224
|
-
|
225
|
-
|
226
|
-
|
315
|
+
config[:insecure_domain_protocol],
|
316
|
+
config[:insecure_domain_name],
|
317
|
+
config[:insecure_domain_port],
|
227
318
|
]
|
228
319
|
|
229
320
|
original_response = [ 315, { LOCATION => original_location }, [ '' ] ]
|
230
|
-
|
321
|
+
rack_app.should_receive(:call).
|
231
322
|
once.with(env).and_return(original_response)
|
232
323
|
|
233
|
-
|
324
|
+
rails_app.routes.should_receive(:recognize_path).
|
234
325
|
once.with(original_location).
|
235
326
|
and_return(controller: 'sessions', action: 'create')
|
236
|
-
|
327
|
+
rails_app.routes.should_receive(:url_for).
|
237
328
|
once.with(
|
238
329
|
controller: 'sessions',
|
239
330
|
action: 'create',
|
240
|
-
host:
|
241
|
-
protocol:
|
242
|
-
port:
|
331
|
+
host: config[:insecure_domain_name],
|
332
|
+
protocol: config[:insecure_domain_protocol],
|
333
|
+
port: config[:insecure_domain_port],
|
243
334
|
).and_return(expected_location)
|
244
335
|
|
245
336
|
presenter.redirect_to_response![1][LOCATION].should eq expected_location
|
@@ -254,6 +345,31 @@ describe 'SecureEscrow::Middleware' do
|
|
254
345
|
presenter.store_response_in_escrow_and_redirect![0].should eq 303
|
255
346
|
end
|
256
347
|
|
348
|
+
describe 'headers' do
|
349
|
+
it 'should set Location header to redirect location' do
|
350
|
+
mock_location = mock('redirect_to_location')
|
351
|
+
presenter.should_receive(:redirect_to_location).
|
352
|
+
once.and_return(mock_location)
|
353
|
+
|
354
|
+
headers = presenter.store_response_in_escrow_and_redirect![1]
|
355
|
+
headers['Location'].should eq mock_location
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'should set Location header to redirect location' do
|
359
|
+
mock_content_type = mock('content_type')
|
360
|
+
|
361
|
+
presenter.stub!(
|
362
|
+
redirect_to_location: '/',
|
363
|
+
store_in_escrow: [ 'id', 'nonce' ])
|
364
|
+
|
365
|
+
presenter.should_receive(:call_result).
|
366
|
+
and_return([ 200, { 'Content-Type' => mock_content_type}, '' ])
|
367
|
+
|
368
|
+
headers = presenter.store_response_in_escrow_and_redirect![1]
|
369
|
+
headers['Content-Type'].should eq mock_content_type
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
257
373
|
context 'when insecure_domain_name is different from secure_domain_name' do
|
258
374
|
let(:app) {
|
259
375
|
MockEngine.new(
|
@@ -279,7 +395,7 @@ describe 'SecureEscrow::Middleware' do
|
|
279
395
|
describe 'serve_response_from_application!' do
|
280
396
|
it 'should serve response from application' do
|
281
397
|
response = [ 200, {}, [ '' ] ]
|
282
|
-
|
398
|
+
rack_app.should_receive(:call).
|
283
399
|
once.with(env).and_return(response)
|
284
400
|
presenter.serve_response_from_application!.should eq response
|
285
401
|
end
|
@@ -332,10 +448,11 @@ describe 'SecureEscrow::Middleware' do
|
|
332
448
|
end
|
333
449
|
|
334
450
|
it 'should rewrite domain of redirect to secure domain' do
|
451
|
+
config = rails_app.config.secure_escrow
|
335
452
|
original_redirect_url = "%s://%s:%s" % [
|
336
|
-
|
337
|
-
|
338
|
-
|
453
|
+
config[:secure_domain_protocol],
|
454
|
+
config[:secure_domain_name],
|
455
|
+
config[:secure_domain_port],
|
339
456
|
]
|
340
457
|
rewritten_redirect_url = 'boo'
|
341
458
|
|
@@ -344,16 +461,16 @@ describe 'SecureEscrow::Middleware' do
|
|
344
461
|
presenter.should_receive(:generate_id_and_nonce).
|
345
462
|
once.with.and_return([ 'id', 'nonce'])
|
346
463
|
|
347
|
-
|
464
|
+
rails_app.routes.should_receive(:recognize_path).
|
348
465
|
once.with(original_redirect_url).
|
349
466
|
and_return(controller: 'sessions', action: 'create')
|
350
467
|
|
351
|
-
|
468
|
+
rails_app.routes.should_receive(:url_for).
|
352
469
|
once.with(
|
353
470
|
controller: 'sessions', action: 'create',
|
354
|
-
host:
|
355
|
-
protocol:
|
356
|
-
port:
|
471
|
+
host: config[:insecure_domain_name],
|
472
|
+
protocol: config[:insecure_domain_protocol],
|
473
|
+
port: config[:insecure_domain_port],
|
357
474
|
).and_return(rewritten_redirect_url)
|
358
475
|
|
359
476
|
expected_stored_value = {
|
@@ -362,8 +479,8 @@ describe 'SecureEscrow::Middleware' do
|
|
362
479
|
}.to_json
|
363
480
|
|
364
481
|
key = presenter.escrow_key 'id'
|
365
|
-
store.should_receive(:
|
366
|
-
once.with(key, expected_stored_value)
|
482
|
+
store.should_receive(:setex).
|
483
|
+
once.with(key, TTL, expected_stored_value)
|
367
484
|
|
368
485
|
presenter.store_in_escrow(
|
369
486
|
303,
|
@@ -376,7 +493,7 @@ describe 'SecureEscrow::Middleware' do
|
|
376
493
|
|
377
494
|
describe 'escrow_id and escrow_nonce' do
|
378
495
|
context 'when insecure_domain_name is different from secure_domain_name' do
|
379
|
-
let(:
|
496
|
+
let(:rails_app) {
|
380
497
|
MockEngine.new(
|
381
498
|
secure_domain_name: 'www.ssl-example.com',
|
382
499
|
insecure_domain_name: 'www.example.com'
|
@@ -405,6 +522,13 @@ describe 'SecureEscrow::Middleware' do
|
|
405
522
|
presenter.escrow_id.should eq 'id'
|
406
523
|
presenter.escrow_nonce.should eq 'nonce'
|
407
524
|
end
|
525
|
+
|
526
|
+
it 'should select first suitable escrow key from cookie' do
|
527
|
+
set_multi_escrow_cookie presenter, "A", "B", "C", "D"
|
528
|
+
|
529
|
+
presenter.escrow_id.should eq "A"
|
530
|
+
presenter.escrow_nonce.should eq "B"
|
531
|
+
end
|
408
532
|
end
|
409
533
|
|
410
534
|
end
|
@@ -420,6 +544,13 @@ def set_escrow_cookie presenter, id = 'id', nonce = 'nonce'
|
|
420
544
|
set_escrow_env HTTP_COOKIE, presenter, id, nonce
|
421
545
|
end
|
422
546
|
|
547
|
+
def set_multi_escrow_cookie presenter, id1 = 'id1', nonce1 = 'nonce1', id2 = 'id2', nonce2 = 'nonce2'
|
548
|
+
presenter.env[HTTP_COOKIE] = "%s=%s.%s; %s=%s.%s" % [
|
549
|
+
SecureEscrow::MiddlewareConstants::DATA_KEY, id1, nonce1,
|
550
|
+
SecureEscrow::MiddlewareConstants::DATA_KEY, id2, nonce2
|
551
|
+
]
|
552
|
+
end
|
553
|
+
|
423
554
|
def set_escrow_env key, presenter, id, nonce
|
424
555
|
presenter.env[key] = "%s=%s.%s" % [
|
425
556
|
SecureEscrow::MiddlewareConstants::DATA_KEY,
|
@@ -427,7 +558,7 @@ def set_escrow_env key, presenter, id, nonce
|
|
427
558
|
]
|
428
559
|
end
|
429
560
|
|
430
|
-
def store_in_escrow store, id = 'id', nonce = 'nonce', response = []
|
561
|
+
def store_in_escrow store, id = 'id', nonce = 'nonce', response = [ 200, {}, [ "" ] ]
|
431
562
|
store.set(
|
432
563
|
presenter.escrow_key(id),
|
433
564
|
ActiveSupport::JSON.encode(
|
data/spec/mock_engine.rb
CHANGED
@@ -12,16 +12,16 @@ class MockEngine
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def config extra_config = {}
|
15
|
-
@config ||= Config.new
|
16
|
-
{
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
@config ||= Config.new.tap do |config|
|
16
|
+
config.secure_escrow = {
|
17
|
+
secure_domain_name: 'www.example.com',
|
18
|
+
secure_domain_protocol: 'https',
|
19
|
+
secure_domain_port: 443,
|
20
|
+
insecure_domain_name: 'www.example.com',
|
21
|
+
insecure_domain_protocol: 'http',
|
22
|
+
insecure_domain_port: 80
|
23
|
+
}.merge extra_config
|
24
|
+
end
|
25
25
|
end
|
26
26
|
|
27
27
|
def routes
|
@@ -32,6 +32,9 @@ class MockEngine
|
|
32
32
|
end
|
33
33
|
|
34
34
|
class Routes
|
35
|
+
def recognize_path path, options = {}
|
36
|
+
{}
|
37
|
+
end
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
data/spec/mock_redis.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: secure_escrow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2012-02-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &70114553359420 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,7 +21,7 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70114553359420
|
25
25
|
description: SecureEscrow provides a content proxy for Rails applications allowing
|
26
26
|
POSTing to secure actions from insecure domains without full-page refreshes
|
27
27
|
email:
|
@@ -47,9 +47,10 @@ files:
|
|
47
47
|
- secure_escrow.gemspec
|
48
48
|
- spec/middleware_spec.rb
|
49
49
|
- spec/mock_engine.rb
|
50
|
+
- spec/mock_rack_app.rb
|
50
51
|
- spec/mock_redis.rb
|
51
52
|
- spec/spec_helper.rb
|
52
|
-
homepage:
|
53
|
+
homepage: https://github.com/duncanbeevers/SecureEscrow
|
53
54
|
licenses: []
|
54
55
|
post_install_message:
|
55
56
|
rdoc_options: []
|
@@ -76,5 +77,6 @@ summary: Secure AJAX-style actions for Rails applications
|
|
76
77
|
test_files:
|
77
78
|
- spec/middleware_spec.rb
|
78
79
|
- spec/mock_engine.rb
|
80
|
+
- spec/mock_rack_app.rb
|
79
81
|
- spec/mock_redis.rb
|
80
82
|
- spec/spec_helper.rb
|