rack-canonical-host 0.2.1 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +18 -0
- data/lib/rack/canonical_host/redirect.rb +5 -2
- data/lib/rack/canonical_host/version.rb +1 -1
- data/spec/rack/canonical_host_spec.rb +104 -57
- data/spec/support/matchers/have_header.rb +56 -0
- data/spec/support/matchers/{be_redirect.rb → redirect_to.rb} +34 -20
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: afbfedb96e0996ded4ce9b2d21532dcbda77bded
|
4
|
+
data.tar.gz: 47d412e723522d44bf530af57fa03107fef9b745
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83e23130511d87ee922ea34ed3f87384179fde266f818c04aace1c0d19a98b389380fcc250db824a06894cb53921eb3c4fd9dc079c48058ead99d61b7d3c56ce
|
7
|
+
data.tar.gz: 0c8d2c995d2f341bd63d1b51e8ffc6d5d7161236263ee2eda932b95edd847ee59beb6af002c805b08e3c3a5e8d66333bb5599efe1489c3937058f030e22dca29
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.2.2 (2016-05-17)
|
4
|
+
|
5
|
+
* Add `:cache_control` option ([Pete Nicholls][Aupajo])
|
6
|
+
|
3
7
|
## 0.2.1 (2016-03-28)
|
4
8
|
|
5
9
|
* Relax Rack dependency to allow for Rack 2 ([Tyler Ewing][zoso10])
|
@@ -57,6 +61,7 @@
|
|
57
61
|
|
58
62
|
* Initial release ([Tyler Hunt][tylerhunt])
|
59
63
|
|
64
|
+
[Aupajo]: http://github.com/Aupajo
|
60
65
|
[finack]: http://github.com/finack
|
61
66
|
[firedev]: http://github.com/firedev
|
62
67
|
[jcarbo]: http://github.com/jcarbo
|
data/README.md
CHANGED
@@ -95,6 +95,23 @@ use Rack::CanonicalHost, 'example.com', if: /.*\.example\.com/
|
|
95
95
|
use Rack::CanonicalHost, 'example.ru', if: /.*\.example\.ru/
|
96
96
|
```
|
97
97
|
|
98
|
+
### Cache-Control
|
99
|
+
|
100
|
+
To avoid browsers indefinitely caching a `301` redirect, it’s a sensible idea
|
101
|
+
to set an expiry on each redirect, to hedge against the chance you may need to
|
102
|
+
change that redirect in the future.
|
103
|
+
|
104
|
+
``` ruby
|
105
|
+
# Leave caching up to the browser (which could cache it indefinitely):
|
106
|
+
use Rack::CanonicalHost, 'example.com'
|
107
|
+
|
108
|
+
# Cache the redirect for up to an hour:
|
109
|
+
use Rack::CanonicalHost, 'example.com', cache_control: 'max-age=3600'
|
110
|
+
|
111
|
+
# Prevent caching of redirects:
|
112
|
+
use Rack::CanonicalHost, 'example.com', cache_control: 'no-cache'
|
113
|
+
```
|
114
|
+
|
98
115
|
## Contributing
|
99
116
|
|
100
117
|
1. Fork it
|
@@ -109,6 +126,7 @@ use Rack::CanonicalHost, 'example.ru', if: /.*\.example\.ru/
|
|
109
126
|
Thanks to the following people who have contributed patches or helpful
|
110
127
|
suggestions:
|
111
128
|
|
129
|
+
* [Pete Nicholls](https://github.com/Aupajo)
|
112
130
|
* [Tyler Ewing](https://github.com/zoso10)
|
113
131
|
* [Thomas Maurer](https://github.com/tma)
|
114
132
|
* [Jeff Carbonella](https://github.com/jcarbo)
|
@@ -20,6 +20,7 @@ module Rack
|
|
20
20
|
self.host = host
|
21
21
|
self.ignore = Array(options[:ignore])
|
22
22
|
self.conditions = Array(options[:if])
|
23
|
+
self.cache_control = options[:cache_control]
|
23
24
|
end
|
24
25
|
|
25
26
|
def canonical?
|
@@ -37,6 +38,7 @@ module Rack
|
|
37
38
|
attr_accessor :host
|
38
39
|
attr_accessor :ignore
|
39
40
|
attr_accessor :conditions
|
41
|
+
attr_accessor :cache_control
|
40
42
|
|
41
43
|
private
|
42
44
|
|
@@ -46,9 +48,10 @@ module Rack
|
|
46
48
|
|
47
49
|
def headers
|
48
50
|
{
|
49
|
-
'
|
51
|
+
'Cache-Control' => cache_control,
|
50
52
|
'Content-Type' => 'text/html',
|
51
|
-
|
53
|
+
'Location' => new_url,
|
54
|
+
}.reject { |_, value| !value }
|
52
55
|
end
|
53
56
|
|
54
57
|
def enabled?
|
@@ -1,9 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
describe Rack::CanonicalHost do
|
4
|
-
let(:response) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
|
1
|
+
RSpec.describe Rack::CanonicalHost do
|
2
|
+
let(:app_response) { [200, { 'Content-Type' => 'text/plain' }, %w(OK)] }
|
5
3
|
let(:inner_app) { lambda { |env| response } }
|
6
4
|
|
5
|
+
before do
|
6
|
+
allow(inner_app)
|
7
|
+
.to receive(:call)
|
8
|
+
.with(env)
|
9
|
+
.and_return(app_response)
|
10
|
+
end
|
11
|
+
|
7
12
|
def build_app(host=nil, options={}, inner_app=inner_app(), &block)
|
8
13
|
Rack::Builder.new do
|
9
14
|
use Rack::Lint
|
@@ -19,8 +24,8 @@ describe Rack::CanonicalHost do
|
|
19
24
|
it { should_not be_redirect }
|
20
25
|
|
21
26
|
it 'calls the inner app' do
|
22
|
-
expect(inner_app).to receive(:call).with(env)
|
23
|
-
|
27
|
+
expect(inner_app).to receive(:call).with(env)
|
28
|
+
call_app
|
24
29
|
end
|
25
30
|
end
|
26
31
|
end
|
@@ -29,12 +34,14 @@ describe Rack::CanonicalHost do
|
|
29
34
|
context 'with a request to a non-matching host' do
|
30
35
|
let(:url) { 'http://www.example.com/full/path' }
|
31
36
|
|
32
|
-
it { should
|
37
|
+
it { should redirect_to('http://example.com/full/path') }
|
33
38
|
|
34
39
|
it 'does not call the inner app' do
|
35
40
|
expect(inner_app).to_not receive(:call)
|
36
|
-
|
41
|
+
call_app
|
37
42
|
end
|
43
|
+
|
44
|
+
it { expect(response).to_not have_header('Cache-Control') }
|
38
45
|
end
|
39
46
|
end
|
40
47
|
|
@@ -44,51 +51,70 @@ describe Rack::CanonicalHost do
|
|
44
51
|
end
|
45
52
|
|
46
53
|
context '#call' do
|
47
|
-
let(:
|
54
|
+
let(:headers) { {} }
|
48
55
|
|
49
|
-
|
56
|
+
let(:app) { build_app('example.com') }
|
57
|
+
let(:env) { Rack::MockRequest.env_for(url, headers) }
|
50
58
|
|
51
|
-
|
52
|
-
|
59
|
+
def call_app
|
60
|
+
app.call(env)
|
61
|
+
end
|
53
62
|
|
54
|
-
|
63
|
+
subject(:response) { call_app }
|
64
|
+
|
65
|
+
include_context 'a matching request'
|
66
|
+
include_context 'a non-matching request'
|
67
|
+
|
68
|
+
context 'when the request has a pipe in the URL' do
|
69
|
+
let(:url) { 'https://example.com/full/path?value=withPIPE' }
|
70
|
+
|
71
|
+
before { env['QUERY_STRING'].sub!('PIPE', '|') }
|
72
|
+
|
73
|
+
it { expect { call_app }.to_not raise_error }
|
55
74
|
end
|
56
75
|
|
57
|
-
context '
|
76
|
+
context 'when the request has JavaScript in the URL' do
|
77
|
+
let(:url) { 'http://www.example.com/full/path' }
|
78
|
+
|
79
|
+
let(:headers) {
|
80
|
+
{ 'QUERY_STRING' => '"><script>alert(73541);</script>' }
|
81
|
+
}
|
82
|
+
|
58
83
|
let(:app) { build_app('example.com') }
|
59
|
-
|
84
|
+
|
85
|
+
it 'escapes the JavaScript' do
|
86
|
+
expect(response)
|
87
|
+
.to redirect_to('http://example.com/full/path?%22%3E%3Cscript%3Ealert(73541)%3B%3C/script%3E')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with an X-Forwarded-Host' do
|
92
|
+
let(:url) { 'http://proxy.test/full/path' }
|
60
93
|
|
61
94
|
context 'which matches the canonical host' do
|
62
|
-
let(:
|
95
|
+
let(:headers) { { 'HTTP_X_FORWARDED_HOST' => 'example.com:80' } }
|
63
96
|
|
64
97
|
include_context 'a matching request'
|
65
98
|
end
|
66
99
|
|
67
100
|
context 'which does not match the canonical host' do
|
68
|
-
let(:
|
101
|
+
let(:headers) { { 'HTTP_X_FORWARDED_HOST' => 'www.example.com:80' } }
|
69
102
|
|
70
103
|
include_context 'a non-matching request'
|
71
104
|
end
|
72
105
|
end
|
73
106
|
|
74
|
-
context 'without
|
75
|
-
let(:app) { build_app(
|
76
|
-
|
77
|
-
include_context 'matching and non-matching requests'
|
78
|
-
|
79
|
-
context 'when the request has a pipe in the URL' do
|
80
|
-
let(:url) { 'https://example.com/full/path?value=withPIPE' }
|
81
|
-
|
82
|
-
before { env['QUERY_STRING'].sub!('PIPE', '|') }
|
107
|
+
context 'without a host' do
|
108
|
+
let(:app) { build_app(nil) }
|
83
109
|
|
84
|
-
|
85
|
-
end
|
110
|
+
include_context 'a matching request'
|
86
111
|
end
|
87
112
|
|
88
113
|
context 'with :ignore option' do
|
89
114
|
let(:app) { build_app('example.com', :ignore => 'example.net') }
|
90
115
|
|
91
|
-
include_context '
|
116
|
+
include_context 'a matching request'
|
117
|
+
include_context 'a non-matching request'
|
92
118
|
|
93
119
|
context 'with a request to an ignored host' do
|
94
120
|
let(:url) { 'http://example.net/full/path' }
|
@@ -96,67 +122,88 @@ describe Rack::CanonicalHost do
|
|
96
122
|
it { should_not be_redirect }
|
97
123
|
|
98
124
|
it 'calls the inner app' do
|
99
|
-
expect(inner_app).to receive(:call).with(env)
|
100
|
-
|
125
|
+
expect(inner_app).to receive(:call).with(env)
|
126
|
+
call_app
|
101
127
|
end
|
102
128
|
end
|
103
129
|
end
|
104
130
|
|
105
131
|
context 'with :if option' do
|
106
|
-
|
107
132
|
let(:app) { build_app('example.com', :if => 'www.example.net') }
|
108
133
|
|
109
|
-
context 'with a request to a
|
134
|
+
context 'with a request to a matching host' do
|
110
135
|
let(:url) { 'http://www.example.net/full/path' }
|
111
|
-
|
136
|
+
|
137
|
+
it { should redirect_to('http://example.com/full/path') }
|
112
138
|
end
|
113
139
|
|
114
|
-
context 'with a request to a
|
115
|
-
let(:url) { 'http://www.
|
140
|
+
context 'with a request to a non-matching host' do
|
141
|
+
let(:url) { 'http://www.example.com/full/path' }
|
142
|
+
|
116
143
|
it { should_not be_redirect }
|
117
144
|
end
|
118
|
-
|
119
145
|
end
|
120
146
|
|
121
|
-
context 'with
|
122
|
-
|
147
|
+
context 'with a regular expression :if option' do
|
123
148
|
let(:app) { build_app('example.com', :if => /.*\.example\.net/) }
|
124
149
|
|
125
|
-
context 'with a request to a
|
150
|
+
context 'with a request to a matching host' do
|
126
151
|
let(:url) { 'http://subdomain.example.net/full/path' }
|
127
|
-
|
152
|
+
|
153
|
+
it { should redirect_to('http://example.com/full/path') }
|
128
154
|
end
|
129
155
|
|
130
|
-
context 'with a request to a
|
156
|
+
context 'with a request to a non-matching host' do
|
131
157
|
let(:url) { 'http://example.net/full/path' }
|
158
|
+
|
132
159
|
it { should_not be_redirect }
|
133
160
|
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'with a :cache_control option' do
|
164
|
+
let(:url) { 'http://subdomain.example.net/full/path' }
|
165
|
+
|
166
|
+
context 'with a max-age value' do
|
167
|
+
let(:app) {
|
168
|
+
build_app('example.com', :cache_control => 'max-age=3600')
|
169
|
+
}
|
170
|
+
|
171
|
+
it {
|
172
|
+
expect(response).to have_header('Cache-Control').with('max-age=3600')
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'with a no-cache value' do
|
177
|
+
let(:app) { build_app('example.com', :cache_control => 'no-cache') }
|
178
|
+
|
179
|
+
it { expect(subject).to have_header('Cache-Control').with('no-cache') }
|
180
|
+
end
|
134
181
|
|
182
|
+
context 'with a false value' do
|
183
|
+
let(:app) { build_app('example.com', :cache_control => false) }
|
184
|
+
|
185
|
+
it { expect(subject).to_not have_header('Cache-Control') }
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'with a nil value' do
|
189
|
+
let(:app) { build_app('example.com', :cache_control => false) }
|
190
|
+
|
191
|
+
it { expect(subject).to_not have_header('Cache-Control') }
|
192
|
+
end
|
135
193
|
end
|
136
194
|
|
137
195
|
context 'with a block' do
|
138
196
|
let(:app) { build_app { 'example.com' } }
|
139
197
|
|
140
|
-
include_context '
|
198
|
+
include_context 'a matching request'
|
199
|
+
include_context 'a non-matching request'
|
141
200
|
|
142
201
|
context 'that returns nil' do
|
143
202
|
let(:app) { build_app('example.com') { nil } }
|
144
203
|
|
145
|
-
include_context '
|
146
|
-
|
147
|
-
end
|
148
|
-
|
149
|
-
context 'with URL containing JavaScript XSS' do
|
150
|
-
let(:url) { 'http://subdomain.example.net/full/path' }
|
151
|
-
let(:env) do
|
152
|
-
Rack::MockRequest.env_for(url).tap do |env|
|
153
|
-
env['QUERY_STRING'] = '"><script>alert(73541);</script>'
|
154
|
-
end
|
204
|
+
include_context 'a matching request'
|
205
|
+
include_context 'a non-matching request'
|
155
206
|
end
|
156
|
-
|
157
|
-
let(:app) { build_app('example.com') }
|
158
|
-
|
159
|
-
it { should be_redirect.to('http://example.com/full/path?%22%3E%3Cscript%3Ealert(73541)%3B%3C/script%3E') }
|
160
207
|
end
|
161
208
|
end
|
162
209
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module HaveHeader
|
2
|
+
class Matcher
|
3
|
+
def initialize(expected_header)
|
4
|
+
self.expected_header = expected_header
|
5
|
+
end
|
6
|
+
|
7
|
+
def matches?(response)
|
8
|
+
_, self.actual_headers, _ = response
|
9
|
+
|
10
|
+
if expected_value
|
11
|
+
actual_header == expected_value
|
12
|
+
else
|
13
|
+
actual_header
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def with(expected_value)
|
18
|
+
self.expected_value = expected_value
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def description
|
23
|
+
sentence = "have header #{expected_header.inspect}"
|
24
|
+
sentence << " with #{expected_value.inspect}" if expected_value
|
25
|
+
sentence
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure_message
|
29
|
+
"Expected response to #{description}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def failure_message_when_negated
|
33
|
+
"Did not expect response to #{description}"
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
attr_accessor :actual_headers
|
39
|
+
attr_accessor :expected_header
|
40
|
+
attr_accessor :expected_value
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def actual_header
|
45
|
+
actual_headers[expected_header]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def have_header(name)
|
50
|
+
Matcher.new(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
RSpec.configure do |config|
|
55
|
+
config.include HaveHeader
|
56
|
+
end
|
@@ -1,36 +1,30 @@
|
|
1
|
-
module
|
1
|
+
module RedirectTo
|
2
2
|
class Matcher
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
def initialize(expected_location)
|
4
|
+
self.expected_location = expected_location
|
5
|
+
self.expected_status_code = STATUS
|
6
|
+
end
|
7
7
|
|
8
8
|
def matches?(response)
|
9
|
-
|
10
|
-
@actual_location = headers['Location']
|
9
|
+
self.actual_status_code, self.actual_headers, _ = response
|
11
10
|
|
12
11
|
status_code_matches? && location_matches?
|
13
12
|
end
|
14
13
|
|
15
14
|
def via(expected_status_code)
|
16
|
-
|
17
|
-
self
|
18
|
-
end
|
19
|
-
|
20
|
-
def to(expected_location)
|
21
|
-
@expected_location = expected_location
|
15
|
+
self.expected_status_code = expected_status_code
|
22
16
|
self
|
23
17
|
end
|
24
18
|
|
25
19
|
def description
|
26
20
|
if expected_status_code && expected_location
|
27
|
-
"redirect
|
21
|
+
"redirect to #{expected_location.inspect} via #{expected_status_code}"
|
28
22
|
elsif expected_status_code
|
29
23
|
"redirect via #{expected_status_code}"
|
30
24
|
elsif expected_location
|
31
25
|
"redirect to #{expected_location.inspect}"
|
32
26
|
else
|
33
|
-
|
27
|
+
'be a redirect'
|
34
28
|
end
|
35
29
|
end
|
36
30
|
|
@@ -42,6 +36,22 @@ module BeRedirect
|
|
42
36
|
"Did not expect response to #{description}"
|
43
37
|
end
|
44
38
|
|
39
|
+
protected
|
40
|
+
|
41
|
+
attr_accessor :actual_headers
|
42
|
+
attr_accessor :actual_status_code
|
43
|
+
attr_accessor :expected_location
|
44
|
+
attr_accessor :expected_status_code
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
LOCATION = 'Location'
|
49
|
+
STATUS = 301
|
50
|
+
|
51
|
+
def actual_location
|
52
|
+
actual_headers[LOCATION]
|
53
|
+
end
|
54
|
+
|
45
55
|
def status_code_matches?
|
46
56
|
if expected_status_code
|
47
57
|
actual_status_code == expected_status_code
|
@@ -49,19 +59,23 @@ module BeRedirect
|
|
49
59
|
actual_status_code.to_s =~ /^30[1237]$/
|
50
60
|
end
|
51
61
|
end
|
52
|
-
private :status_code_matches?
|
53
62
|
|
54
63
|
def location_matches?
|
55
|
-
|
64
|
+
if expected_location
|
65
|
+
expected_location == actual_location
|
66
|
+
end
|
56
67
|
end
|
57
|
-
|
68
|
+
end
|
69
|
+
|
70
|
+
def redirect_to(location)
|
71
|
+
Matcher.new(location)
|
58
72
|
end
|
59
73
|
|
60
74
|
def be_redirect
|
61
|
-
Matcher.new
|
75
|
+
Matcher.new(nil).via(nil)
|
62
76
|
end
|
63
77
|
end
|
64
78
|
|
65
79
|
RSpec.configure do |config|
|
66
|
-
config.include
|
80
|
+
config.include RedirectTo
|
67
81
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-canonical-host
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tyler Hunt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: addressable
|
@@ -99,7 +99,8 @@ files:
|
|
99
99
|
- rack-canonical-host.gemspec
|
100
100
|
- spec/rack/canonical_host_spec.rb
|
101
101
|
- spec/spec_helper.rb
|
102
|
-
- spec/support/matchers/
|
102
|
+
- spec/support/matchers/have_header.rb
|
103
|
+
- spec/support/matchers/redirect_to.rb
|
103
104
|
homepage: http://github.com/tylerhunt/rack-canonical-host
|
104
105
|
licenses: []
|
105
106
|
metadata: {}
|
@@ -126,4 +127,5 @@ summary: Rack middleware for defining a canonical host name.
|
|
126
127
|
test_files:
|
127
128
|
- spec/rack/canonical_host_spec.rb
|
128
129
|
- spec/spec_helper.rb
|
129
|
-
- spec/support/matchers/
|
130
|
+
- spec/support/matchers/have_header.rb
|
131
|
+
- spec/support/matchers/redirect_to.rb
|