secure_escrow 0.0.1 → 0.0.2

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