rack-wwwhisper 1.0.3.pre → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rack/wwwhisper.rb +49 -68
- data/test/test_wwwhisper.rb +61 -26
- metadata +9 -6
data/lib/rack/wwwhisper.rb
CHANGED
@@ -10,72 +10,55 @@ require 'rack/utils'
|
|
10
10
|
|
11
11
|
module Rack
|
12
12
|
|
13
|
+
class NoPublicCache
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
status, headers, body = @app.call(env)
|
20
|
+
if cache_control = headers['Cache-Control']
|
21
|
+
# If caching is enabled, make sure it is private.
|
22
|
+
if (not cache_control.gsub!(/public/, 'private') and
|
23
|
+
cache_control.index(/max-age\s*=\s*0*[1-9]/))
|
24
|
+
cache_control.insert(0, 'private, ')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
[status, headers, body]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
13
31
|
class WWWhisper
|
14
|
-
@@DEFAULT_ASSETS_URL = 'https://c693db817dca7e162673-39ba3573e09a1fa9bea151a745461b70.ssl.cf1.rackcdn.com'
|
15
32
|
@@WWWHISPER_PREFIX = '/wwwhisper/'
|
16
|
-
|
17
|
-
|
33
|
+
@@AUTH_COOKIES_PREFIX = 'wwwhisper'
|
34
|
+
@@FORWARDED_HEADERS = ['Accept', 'Accept-Language', 'Cookie', 'X-CSRFToken',
|
35
|
+
'X-Requested-With']
|
18
36
|
@@DEFAULT_IFRAME = \
|
19
|
-
%Q[<iframe id="wwwhisper-iframe" src="%s"
|
20
|
-
width="340" height="29" frameborder="0" scrolling="no"
|
37
|
+
%Q[<iframe id="wwwhisper-iframe" src="%s" width="340" height="29"
|
21
38
|
style="position:fixed; overflow:hidden; border:0px; bottom:0px;
|
22
39
|
right:0px; z-index:11235; background-color:transparent;"> </iframe>
|
23
40
|
]
|
24
41
|
|
25
42
|
def initialize(app)
|
26
43
|
@app = app
|
27
|
-
|
28
44
|
if ENV['WWWHISPER_DISABLE'] == "1"
|
29
45
|
def self.call(env)
|
30
46
|
@app.call(env)
|
31
47
|
end
|
32
48
|
return
|
33
49
|
end
|
50
|
+
@app = NoPublicCache.new(app)
|
34
51
|
|
35
52
|
if not ENV['WWWHISPER_URL']
|
36
53
|
raise StandardError, 'WWWHISPER_URL environment variable not set'
|
37
54
|
end
|
38
55
|
|
39
|
-
|
40
|
-
wwwhisper_uri = parse_uri(ENV['WWWHISPER_URL'])
|
56
|
+
@http = http_init('wwwhisper')
|
57
|
+
@wwwhisper_uri = parse_uri(ENV['WWWHISPER_URL'])
|
41
58
|
|
42
59
|
@wwwhisper_iframe = ENV['WWWHISPER_IFRAME'] ||
|
43
60
|
sprintf(@@DEFAULT_IFRAME, wwwhisper_path('auth/overlay.html'))
|
44
61
|
@wwwhisper_iframe_bytesize = Rack::Utils::bytesize(@wwwhisper_iframe)
|
45
|
-
|
46
|
-
@request_config = {
|
47
|
-
# TODO: probably now auth can be removed.
|
48
|
-
:auth => {
|
49
|
-
:forwarded_headers => ['Accept', 'Accept-Language', 'Cookie'],
|
50
|
-
:http => wwwhisper_http,
|
51
|
-
:uri => wwwhisper_uri,
|
52
|
-
:send_site_url => true,
|
53
|
-
},
|
54
|
-
:api => {
|
55
|
-
:forwarded_headers => ['Accept', 'Accept-Language', 'Cookie',
|
56
|
-
'X-CSRFToken', 'X-Requested-With'],
|
57
|
-
:http => wwwhisper_http,
|
58
|
-
:uri => wwwhisper_uri,
|
59
|
-
:send_site_url => true,
|
60
|
-
},
|
61
|
-
:assets => {
|
62
|
-
# Don't pass Accept-Encoding to get uncompressed response (so
|
63
|
-
# iframe can be inserted to it).
|
64
|
-
:forwarded_headers => ['Accept', 'Accept-Language'],
|
65
|
-
:http => http_init('wwwhisper-assets'),
|
66
|
-
:uri => parse_uri(ENV['WWWHISPER_ASSETS_URL'] || @@DEFAULT_ASSETS_URL),
|
67
|
-
:send_site_url => false,
|
68
|
-
},
|
69
|
-
}
|
70
|
-
|
71
|
-
@aliases = {}
|
72
|
-
{
|
73
|
-
'auth/login' => 'auth/login.html',
|
74
|
-
'auth/logout' => 'auth/logout.html',
|
75
|
-
'admin/' => 'admin/index.html',
|
76
|
-
}.each do |k, v|
|
77
|
-
@aliases[wwwhisper_path(k)] = wwwhisper_path(v)
|
78
|
-
end
|
79
62
|
end
|
80
63
|
|
81
64
|
def wwwhisper_path(suffix)
|
@@ -89,7 +72,7 @@ class WWWhisper
|
|
89
72
|
def call(env)
|
90
73
|
req = Request.new(env)
|
91
74
|
|
92
|
-
if req.path =~ %r{^#{
|
75
|
+
if req.path =~ %r{^#{wwwhisper_path('auth')}}
|
93
76
|
# Requests to /@@WWWHISPER_PREFIX/auth/ should not be authorized,
|
94
77
|
# every visitor can access login pages.
|
95
78
|
return dispatch(req)
|
@@ -128,12 +111,17 @@ class WWWhisper
|
|
128
111
|
env['HTTP_X_FORWARDED_PROTO'] || 'http'
|
129
112
|
end
|
130
113
|
|
131
|
-
def
|
114
|
+
def host_with_port
|
132
115
|
env['HTTP_HOST']
|
133
116
|
end
|
134
117
|
|
118
|
+
def host
|
119
|
+
host_with_port.to_s.gsub(/:\d+\z/, '')
|
120
|
+
end
|
121
|
+
|
135
122
|
def port
|
136
|
-
env['HTTP_X_FORWARDED_PORT'] ||
|
123
|
+
env['HTTP_X_FORWARDED_PORT'] || host_with_port.split(/:/)[1] ||
|
124
|
+
default_port(scheme)
|
137
125
|
end
|
138
126
|
|
139
127
|
def site_url
|
@@ -182,24 +170,25 @@ class WWWhisper
|
|
182
170
|
return http
|
183
171
|
end
|
184
172
|
|
185
|
-
def sub_request_init(
|
186
|
-
path = @aliases[path] || path
|
173
|
+
def sub_request_init(rack_req, method, path)
|
187
174
|
sub_req = Net::HTTP.const_get(method).new(path)
|
188
|
-
copy_headers(
|
189
|
-
sub_req['Site-Url'] = rack_req.site_url
|
190
|
-
|
191
|
-
|
175
|
+
copy_headers(@@FORWARDED_HEADERS, rack_req.env, sub_req)
|
176
|
+
sub_req['Site-Url'] = rack_req.site_url
|
177
|
+
if @wwwhisper_uri.user and @wwwhisper_uri.password
|
178
|
+
sub_req.basic_auth(@wwwhisper_uri.user, @wwwhisper_uri.password)
|
179
|
+
end
|
192
180
|
sub_req
|
193
181
|
end
|
194
182
|
|
195
|
-
def has_value(dict, key)
|
196
|
-
dict[key] != nil and !dict[key].empty?
|
197
|
-
end
|
198
|
-
|
199
183
|
def copy_headers(headers_names, env, sub_req)
|
200
184
|
headers_names.each do |header|
|
201
185
|
key = "HTTP_#{header.upcase}".gsub(/-/, '_')
|
202
|
-
|
186
|
+
value = env[key]
|
187
|
+
if value and key == 'HTTP_COOKIE'
|
188
|
+
# Pass only wwwhisper's cookies to the wwwhisper service.
|
189
|
+
value = value.scan(/#{@@AUTH_COOKIES_PREFIX}-[^;]*(?:;|$)/).join(' ')
|
190
|
+
end
|
191
|
+
sub_req[header] = value if value and not value.empty?
|
203
192
|
end
|
204
193
|
end
|
205
194
|
|
@@ -240,9 +229,8 @@ class WWWhisper
|
|
240
229
|
end
|
241
230
|
|
242
231
|
def wwwhisper_auth_request(req)
|
243
|
-
|
244
|
-
|
245
|
-
config[:http].request(config[:uri], auth_req)
|
232
|
+
auth_req = sub_request_init(req, 'Get', auth_query(req.path))
|
233
|
+
@http.request(@wwwhisper_uri, auth_req)
|
246
234
|
end
|
247
235
|
|
248
236
|
def should_inject_iframe(status, headers)
|
@@ -277,18 +265,11 @@ class WWWhisper
|
|
277
265
|
if orig_req.path =~ %r{^#{@@WWWHISPER_PREFIX}}
|
278
266
|
debug orig_req, "passing request to wwwhisper service #{orig_req.path}"
|
279
267
|
|
280
|
-
config =
|
281
|
-
if orig_req.path =~ %r{^#{@@WWWHISPER_PREFIX}(auth|admin)/api/}
|
282
|
-
@request_config[:api]
|
283
|
-
else
|
284
|
-
@request_config[:assets]
|
285
|
-
end
|
286
|
-
|
287
268
|
method = orig_req.request_method.capitalize
|
288
|
-
sub_req = sub_request_init(
|
269
|
+
sub_req = sub_request_init(orig_req, method, orig_req.fullpath)
|
289
270
|
copy_body(orig_req, sub_req)
|
290
271
|
|
291
|
-
sub_resp =
|
272
|
+
sub_resp = @http.request(@wwwhisper_uri, sub_req)
|
292
273
|
sub_response_to_rack(orig_req, sub_resp)
|
293
274
|
else
|
294
275
|
debug orig_req, 'passing request to Rack stack'
|
data/test/test_wwwhisper.rb
CHANGED
@@ -27,12 +27,16 @@ class MockBackend
|
|
27
27
|
assert_equal @expected_email, env['REMOTE_USER']
|
28
28
|
@response
|
29
29
|
end
|
30
|
+
|
31
|
+
def add_response_header(key, value)
|
32
|
+
@response[1][key] = value
|
33
|
+
end
|
34
|
+
|
30
35
|
end
|
31
36
|
|
32
37
|
class TestWWWhisper < Test::Unit::TestCase
|
33
38
|
include Rack::Test::Methods
|
34
39
|
WWWHISPER_URL = 'https://example.com'
|
35
|
-
WWWHISPER_ASSETS_URL = 'https://assets.example.com'
|
36
40
|
SITE_PROTO = 'https'
|
37
41
|
SITE_HOST = 'bar.io'
|
38
42
|
SITE_PORT = 443
|
@@ -41,7 +45,6 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
41
45
|
@backend = MockBackend.new(TEST_USER)
|
42
46
|
ENV.delete('WWWHISPER_DISABLE')
|
43
47
|
ENV['WWWHISPER_URL'] = WWWHISPER_URL
|
44
|
-
ENV['WWWHISPER_ASSETS_URL'] = WWWHISPER_ASSETS_URL
|
45
48
|
@wwwhisper = Rack::WWWhisper.new(@backend)
|
46
49
|
end
|
47
50
|
|
@@ -53,10 +56,6 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
53
56
|
"#{WWWHISPER_URL}#{path}"
|
54
57
|
end
|
55
58
|
|
56
|
-
def full_assets_url(path)
|
57
|
-
"#{WWWHISPER_ASSETS_URL}#{path}"
|
58
|
-
end
|
59
|
-
|
60
59
|
def test_wwwhisper_url_required
|
61
60
|
ENV.delete('WWWHISPER_URL')
|
62
61
|
assert_raise(StandardError) {
|
@@ -169,11 +168,29 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
169
168
|
def test_auth_cookies_passed_to_wwwhisper()
|
170
169
|
path = '/foo/bar'
|
171
170
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
172
|
-
with(:headers => {
|
171
|
+
with(:headers => {
|
172
|
+
'Cookie' => /wwwhisper-auth=xyz; wwwhisper-csrftoken=abc/
|
173
|
+
}).
|
173
174
|
to_return(granted())
|
174
175
|
|
175
176
|
get(path, {},
|
176
|
-
{'HTTP_COOKIE' => '
|
177
|
+
{'HTTP_COOKIE' => 'wwwhisper-auth=xyz; wwwhisper-csrftoken=abc'})
|
178
|
+
assert last_response.ok?
|
179
|
+
assert_equal 'Hello World', last_response.body
|
180
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_non_wwwhisper_cookies_not_passed_to_wwwhisper()
|
184
|
+
path = '/foo/bar'
|
185
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
186
|
+
with(:headers => {
|
187
|
+
'Cookie' => /wwwhisper-auth=xyz; wwwhisper-csrftoken=abc/
|
188
|
+
}).
|
189
|
+
to_return(granted())
|
190
|
+
|
191
|
+
get(path, {},
|
192
|
+
{'HTTP_COOKIE' => 'session=123; wwwhisper-auth=xyz;' +
|
193
|
+
'settings=foobar; wwwhisper-csrftoken=abc'})
|
177
194
|
assert last_response.ok?
|
178
195
|
assert_equal 'Hello World', last_response.body
|
179
196
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
@@ -246,13 +263,12 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
246
263
|
def test_site_url
|
247
264
|
path = '/wwwhisper/admin/index.html'
|
248
265
|
|
249
|
-
# Site-Url header should be sent to wwwhisper backend
|
250
|
-
# assets server.
|
266
|
+
# Site-Url header should be sent to wwwhisper backend.
|
251
267
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
252
268
|
with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
|
253
269
|
to_return(granted())
|
254
|
-
stub_request(:get,
|
255
|
-
with {
|
270
|
+
stub_request(:get, full_url(path)).
|
271
|
+
with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{SITE_HOST}"}).
|
256
272
|
to_return(:status => 200, :body => 'Admin page', :headers => {})
|
257
273
|
|
258
274
|
get path
|
@@ -260,7 +276,21 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
260
276
|
assert_equal 200, last_response.status
|
261
277
|
assert_equal 'Admin page', last_response.body
|
262
278
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
263
|
-
assert_requested :get,
|
279
|
+
assert_requested :get, full_url(path)
|
280
|
+
end
|
281
|
+
|
282
|
+
def test_host_with_port
|
283
|
+
path = '/foo'
|
284
|
+
host = 'localhost:5000'
|
285
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
286
|
+
# Test makes sure that port is not repeated in Site-Url.
|
287
|
+
with(:headers => {'Site-Url' => "#{SITE_PROTO}://#{host}"}).
|
288
|
+
to_return(:status => 401, :body => 'Login required', :headers => {})
|
289
|
+
|
290
|
+
# TODO: start here
|
291
|
+
get(path, {}, {'HTTP_HOST' => host, 'HTTP_X_FORWARDED_PORT' => '5000'})
|
292
|
+
assert_equal 401, last_response.status
|
293
|
+
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
264
294
|
end
|
265
295
|
|
266
296
|
def test_site_url_with_non_default_port
|
@@ -278,22 +308,11 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
278
308
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
279
309
|
end
|
280
310
|
|
281
|
-
def test_aliases
|
282
|
-
requested_path = '/wwwhisper/auth/login'
|
283
|
-
expected_path = requested_path + '.html'
|
284
|
-
stub_request(:get, full_assets_url(expected_path)).
|
285
|
-
to_return(:status => 200, :body => 'Login', :headers => {})
|
286
|
-
|
287
|
-
get requested_path
|
288
|
-
assert last_response.ok?
|
289
|
-
assert_equal 'Login', last_response.body
|
290
|
-
end
|
291
|
-
|
292
311
|
def test_redirects
|
293
312
|
path = '/wwwhisper/admin/index.html'
|
294
313
|
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
295
314
|
to_return(granted())
|
296
|
-
stub_request(:get,
|
315
|
+
stub_request(:get, full_url(path)).
|
297
316
|
to_return(:status => 303, :body => 'Admin page moved',
|
298
317
|
:headers => {'Location' => 'https://new.location/foo/bar'})
|
299
318
|
|
@@ -304,7 +323,7 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
304
323
|
assert_equal("#{SITE_PROTO}://#{SITE_HOST}:#{SITE_PORT}/foo/bar",
|
305
324
|
last_response['Location'])
|
306
325
|
assert_requested :get, full_url(@wwwhisper.auth_query(path))
|
307
|
-
assert_requested :get,
|
326
|
+
assert_requested :get, full_url(path)
|
308
327
|
end
|
309
328
|
|
310
329
|
def test_disable_wwwhisper
|
@@ -331,4 +350,20 @@ class TestWWWhisper < Test::Unit::TestCase
|
|
331
350
|
assert_not_nil last_response['Content-Length']
|
332
351
|
end
|
333
352
|
|
353
|
+
def test_public_caching_disabled()
|
354
|
+
path = '/foo/bar'
|
355
|
+
stub_request(:get, full_url(@wwwhisper.auth_query(path))).
|
356
|
+
to_return(granted())
|
357
|
+
@backend.add_response_header('Cache-Control', 'public, max-age=60')
|
358
|
+
|
359
|
+
get path
|
360
|
+
assert last_response.ok?
|
361
|
+
assert_equal 'private, max-age=60', last_response['Cache-Control']
|
362
|
+
|
363
|
+
@backend.add_response_header('Cache-Control', 'max-age=60')
|
364
|
+
get path
|
365
|
+
assert last_response.ok?
|
366
|
+
assert_equal 'private, max-age=60', last_response['Cache-Control']
|
367
|
+
end
|
368
|
+
|
334
369
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-wwwhisper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
5
|
-
prerelease:
|
4
|
+
version: 1.0.4
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Jan Wrobel
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -131,13 +131,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
131
131
|
version: '0'
|
132
132
|
segments:
|
133
133
|
- 0
|
134
|
-
hash:
|
134
|
+
hash: -4405127786806447004
|
135
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
136
|
none: false
|
137
137
|
requirements:
|
138
|
-
- - ! '
|
138
|
+
- - ! '>='
|
139
139
|
- !ruby/object:Gem::Version
|
140
|
-
version:
|
140
|
+
version: '0'
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
hash: -4405127786806447004
|
141
144
|
requirements: []
|
142
145
|
rubyforge_project:
|
143
146
|
rubygems_version: 1.8.24
|