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 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 access to a backing key-value store.
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.ru</tt>:
41
+ Example <tt>config/initializers/secure_escrow.rb</tt>:
42
42
 
43
43
  ````ruby
44
- # This file is used by Rack-based servers to start the application.
45
- require ::File.expand_path('../config/environment', __FILE__)
46
- use SecureEscrow::Middleware, Awesome::Application.config.redis
47
- run Awesome::Application
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.secure_domain_name = 'www.secure-awesome.devlocal'
58
- config.secure_domain_protocol = 'http'
59
- config.secure_domain_port = 3000
60
- config.insecure_domain_name = 'www.insecure-awesome.devlocal'
61
- config.insecure_domain_protocol = 'http'
62
- config.insecure_domain_port = 3000
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
- ````ruby
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
- innerText, json, response;
17
+ inner_text, http_status, wrapper_json, json, response;
18
18
 
19
19
  try {
20
- innerText = iframe[0].contentWindow.document.body.innerText;
20
+ inner_text = $(iframe[0].contentWindow.document).find("#response").text();
21
21
  try {
22
- json = JSON.parse(innerText);
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: innerText
37
+ responseText: wrapper_json.body
33
38
  };
34
39
 
35
- if (payload_error) {
36
- response.errorJSON = { error: payload_error };
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
- if ('iframe' === escrow) {
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 app, store
25
- @app = app
26
- @store = store
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 @app, @store, env
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 :app, :store, :env
57
+ attr_reader :next_app, :rails_app, :store, :config, :env
53
58
 
54
- def initialize app, store, env
55
- @app = app
56
- @store = store
57
- @env = env
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
- method = env[REQUEST_METHOD]
74
- return false unless POST == method
75
- recognize_path[:escrow]
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
- return value[RESPONSE]
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 = { LOCATION => redirect_to_location(token) }
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
- store.setex key, value.to_json, TTL
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
- private
160
- def set_cookie_token! headers, token
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: config.insecure_domain_protocol,
172
- host: config.insecure_domain_name,
173
- port: config.insecure_domain_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: config.insecure_domain_name,
194
- protocol: config.insecure_domain_protocol,
195
- port: config.insecure_domain_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 call_result
202
- @call_result ||= app.call env
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
- config = app.config
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
- rails_routes.recognize_path(
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 options, &block
7
+ def escrow *args, &block
8
+ options = args.extract_options!
8
9
  defaults = options[:defaults] || {}
9
10
  defaults[:escrow] = true
10
- post options.merge(defaults), &block
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 = 'post'
20
+ POST = 'POST'
18
21
 
19
22
  def escrow_form_for record, options = {}, &proc
20
23
  options[:html] ||= {}
21
24
 
22
- stringy_record = String === record || Symbol === 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.secure_domain_name,
43
- protocol: config.secure_domain_protocol,
44
- port: config.secure_domain_port
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
@@ -1,3 +1,3 @@
1
1
  module SecureEscrow
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
 
@@ -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 'SecureEscrow::Middleware' do
5
- let(:app) { MockEngine.new }
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(:middleware) { SecureEscrow::Middleware.new app, store }
8
- let(:presenter) { SecureEscrow::Middleware::Presenter.new app, store, env }
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(:presenter).with(env).
18
- once.and_return(presenter)
19
-
20
- middleware.should_receive(:handle_presenter).with(presenter).once
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
- app.should_receive(:call).
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
- app.should_receive(:call).
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
- app.should_receive(:call).
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
- app.routes.should_receive(:recognize_path).
152
- once.with(env[REQUEST_PATH], { method: POST }).
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
- app.routes.should_receive(:recognize_path).
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
- app.routes.should_receive(:recognize_path).
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
- it 'should return the escrowed response' do
200
- response = [ 200, {}, [ 'text' ] ]
201
- store_in_escrow store, 'id', 'nonce', response
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!.should eq response
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
- end
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
- app.should_receive(:call).
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
- app.config.secure_domain_protocol,
220
- app.config.secure_domain_name,
221
- app.config.secure_domain_port
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
- app.config.insecure_domain_protocol,
225
- app.config.insecure_domain_name,
226
- app.config.insecure_domain_port
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
- app.should_receive(:call).
321
+ rack_app.should_receive(:call).
231
322
  once.with(env).and_return(original_response)
232
323
 
233
- app.routes.should_receive(:recognize_path).
324
+ rails_app.routes.should_receive(:recognize_path).
234
325
  once.with(original_location).
235
326
  and_return(controller: 'sessions', action: 'create')
236
- app.routes.should_receive(:url_for).
327
+ rails_app.routes.should_receive(:url_for).
237
328
  once.with(
238
329
  controller: 'sessions',
239
330
  action: 'create',
240
- host: app.config.insecure_domain_name,
241
- protocol: app.config.insecure_domain_protocol,
242
- port: app.config.insecure_domain_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
- app.should_receive(:call).
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
- app.config.secure_domain_protocol,
337
- app.config.secure_domain_name,
338
- app.config.secure_domain_port
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
- app.routes.should_receive(:recognize_path).
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
- app.routes.should_receive(:url_for).
468
+ rails_app.routes.should_receive(:url_for).
352
469
  once.with(
353
470
  controller: 'sessions', action: 'create',
354
- host: app.config.insecure_domain_name,
355
- protocol: app.config.insecure_domain_protocol,
356
- port: app.config.insecure_domain_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(:set).
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(:app) {
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
- 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
- )
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
 
@@ -0,0 +1,6 @@
1
+ class MockRackApp
2
+ def call env
3
+ [ 200, {}, %w(OK!) ]
4
+ end
5
+ end
6
+
data/spec/mock_redis.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  class MockRedis
2
- def setex key, value, ttl
2
+ def setex key, ttl, value
3
3
  set key, value
4
4
  expire key, ttl
5
5
  end
data/spec/spec_helper.rb CHANGED
@@ -16,5 +16,6 @@ Spork.each_run do
16
16
  # Load mocks
17
17
  require 'mock_engine'
18
18
  require 'mock_redis'
19
+ require 'mock_rack_app'
19
20
  end
20
21
 
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.1
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: 2011-11-08 00:00:00.000000000 Z
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: &70248523590320 !ruby/object:Gem::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: *70248523590320
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