rack-ssl-enforcer 0.2.7 → 0.2.8
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/LICENSE +20 -20
- data/README.md +300 -297
- data/lib/rack-ssl-enforcer.rb +1 -1
- data/lib/rack/ssl-enforcer.rb +204 -204
- data/lib/rack/ssl-enforcer/constraint.rb +44 -42
- data/lib/rack/ssl-enforcer/version.rb +5 -5
- metadata +22 -7
- checksums.yaml +0 -7
data/lib/rack-ssl-enforcer.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'rack/ssl-enforcer'
|
1
|
+
require 'rack/ssl-enforcer'
|
data/lib/rack/ssl-enforcer.rb
CHANGED
@@ -1,204 +1,204 @@
|
|
1
|
-
require 'rack/ssl-enforcer/constraint'
|
2
|
-
|
3
|
-
module Rack
|
4
|
-
|
5
|
-
class SslEnforcer
|
6
|
-
|
7
|
-
CONSTRAINTS_BY_TYPE = {
|
8
|
-
:hosts => [:only_hosts, :except_hosts],
|
9
|
-
:agents => [:only_agents, :except_agents],
|
10
|
-
:path => [:only, :except],
|
11
|
-
:methods => [:only_methods, :except_methods],
|
12
|
-
:environments => [:only_environments, :except_environments]
|
13
|
-
}
|
14
|
-
|
15
|
-
# Warning: If you set the option force_secure_cookies to false, make sure that your cookies
|
16
|
-
# are encoded and that you understand the consequences (see documentation)
|
17
|
-
def initialize(app, options={})
|
18
|
-
default_options = {
|
19
|
-
:redirect_to => nil,
|
20
|
-
:redirect_code => nil,
|
21
|
-
:strict => false,
|
22
|
-
:mixed => false,
|
23
|
-
:hsts => nil,
|
24
|
-
:http_port => nil,
|
25
|
-
:https_port => nil,
|
26
|
-
:force_secure_cookies => true,
|
27
|
-
:redirect_html => nil,
|
28
|
-
:before_redirect => nil
|
29
|
-
}
|
30
|
-
CONSTRAINTS_BY_TYPE.values.each do |constraints|
|
31
|
-
constraints.each { |constraint| default_options[constraint] = nil }
|
32
|
-
end
|
33
|
-
|
34
|
-
@app, @options = app, default_options.merge(options)
|
35
|
-
end
|
36
|
-
|
37
|
-
def call(env)
|
38
|
-
@request = Rack::Request.new(env)
|
39
|
-
|
40
|
-
return @app.call(env) if ignore?
|
41
|
-
|
42
|
-
@scheme = if enforce_ssl?
|
43
|
-
'https'
|
44
|
-
elsif enforce_non_ssl?
|
45
|
-
'http'
|
46
|
-
end
|
47
|
-
|
48
|
-
if redirect_required?
|
49
|
-
call_before_redirect
|
50
|
-
modify_location_and_redirect
|
51
|
-
elsif ssl_request?
|
52
|
-
status, headers, body = @app.call(env)
|
53
|
-
flag_cookies_as_secure!(headers) if @options[:force_secure_cookies]
|
54
|
-
set_hsts_headers!(headers) if @options[:hsts] && !@options[:strict]
|
55
|
-
[status, headers, body]
|
56
|
-
else
|
57
|
-
@app.call(env)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def redirect_required?
|
64
|
-
scheme_mismatch? || host_mismatch?
|
65
|
-
end
|
66
|
-
|
67
|
-
def ignore?
|
68
|
-
if @options[:ignore]
|
69
|
-
rules = [@options[:ignore]].flatten.compact
|
70
|
-
rules.any? do |rule|
|
71
|
-
SslEnforcerConstraint.new(:ignore, rule, @request).matches?
|
72
|
-
end
|
73
|
-
else
|
74
|
-
false
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def scheme_mismatch?
|
79
|
-
@scheme && @scheme != current_scheme
|
80
|
-
end
|
81
|
-
|
82
|
-
def host_mismatch?
|
83
|
-
destination_host && destination_host != @request.host
|
84
|
-
end
|
85
|
-
|
86
|
-
def call_before_redirect
|
87
|
-
@options[:before_redirect].call(@request) unless @options[:before_redirect].nil?
|
88
|
-
end
|
89
|
-
|
90
|
-
def modify_location_and_redirect
|
91
|
-
location = "#{current_scheme}://#{@request.host}#{@request.fullpath}"
|
92
|
-
location = replace_scheme(location, @scheme)
|
93
|
-
location = replace_host(location, @options[:redirect_to])
|
94
|
-
redirect_to(location)
|
95
|
-
end
|
96
|
-
|
97
|
-
def redirect_to(location)
|
98
|
-
body = []
|
99
|
-
body << "<html><body>You are being <a href=\"#{location}\">redirected</a>.</body></html>" if @options[:redirect_html].nil?
|
100
|
-
body << @options[:redirect_html] if @options[:redirect_html].is_a?(String)
|
101
|
-
body = @options[:redirect_html] if @options[:redirect_html].respond_to?('each')
|
102
|
-
|
103
|
-
[@options[:redirect_code] || 301, { 'Content-Type' => 'text/html', 'Location' => location }, body]
|
104
|
-
end
|
105
|
-
|
106
|
-
def ssl_request?
|
107
|
-
current_scheme == 'https'
|
108
|
-
end
|
109
|
-
|
110
|
-
def destination_host
|
111
|
-
if @options[:redirect_to]
|
112
|
-
host_parts = URI.split(
|
113
|
-
host_parts[2] || host_parts[5]
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
# Fixed in rack >= 1.3
|
118
|
-
def current_scheme
|
119
|
-
if @request.env['HTTPS'] == 'on' || @request.env['HTTP_X_SSL_REQUEST'] == 'on'
|
120
|
-
'https'
|
121
|
-
elsif @request.env['HTTP_X_FORWARDED_PROTO']
|
122
|
-
@request.env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
123
|
-
else
|
124
|
-
@request.scheme
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
def enforce_ssl_for?(keys)
|
129
|
-
provided_keys = keys.select { |key| @options[key] }
|
130
|
-
if provided_keys.empty?
|
131
|
-
true
|
132
|
-
else
|
133
|
-
provided_keys.all? do |key|
|
134
|
-
rules = [@options[key]].flatten.compact
|
135
|
-
rules.send([:except_hosts, :except_agents, :except_environments, :except].include?(key) ? :all? : :any?) do |rule|
|
136
|
-
SslEnforcerConstraint.new(key, rule, @request).matches?
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
def enforce_non_ssl?
|
143
|
-
@options[:strict] || @options[:mixed] && !(@request.request_method == 'PUT' || @request.request_method == 'POST')
|
144
|
-
end
|
145
|
-
|
146
|
-
def enforce_ssl?
|
147
|
-
CONSTRAINTS_BY_TYPE.inject(true) do |memo, (type, keys)|
|
148
|
-
memo && enforce_ssl_for?(keys)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def replace_scheme(uri, scheme)
|
153
|
-
return uri if not scheme_mismatch?
|
154
|
-
|
155
|
-
port = adjust_port_to(scheme)
|
156
|
-
uri_parts = URI.split(
|
157
|
-
uri_parts[3] = port unless port.nil?
|
158
|
-
uri_parts[0] = scheme
|
159
|
-
URI::HTTP.new(*uri_parts).to_s
|
160
|
-
end
|
161
|
-
|
162
|
-
def replace_host(uri, host)
|
163
|
-
return uri unless host_mismatch?
|
164
|
-
|
165
|
-
host_parts = URI.split(
|
166
|
-
new_host = host_parts[2] || host_parts[5]
|
167
|
-
uri_parts = URI.split(
|
168
|
-
uri_parts[2] = new_host
|
169
|
-
URI::HTTPS.new(*uri_parts).to_s
|
170
|
-
end
|
171
|
-
|
172
|
-
def adjust_port_to(scheme)
|
173
|
-
if scheme == 'https'
|
174
|
-
@options[:https_port] if @options[:https_port] && @options[:https_port] != URI::HTTPS.default_port
|
175
|
-
elsif scheme == 'http'
|
176
|
-
@options[:http_port] if @options[:http_port] && @options[:http_port] != URI::HTTP.default_port
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
# see http://en.wikipedia.org/wiki/HTTP_cookie#Cookie_theft_and_session_hijacking
|
181
|
-
def flag_cookies_as_secure!(headers)
|
182
|
-
if cookies = headers['Set-Cookie']
|
183
|
-
# Support Rails 2.3 / Rack 1.1 arrays as headers
|
184
|
-
unless cookies.is_a?(Array)
|
185
|
-
cookies = cookies.split("\n")
|
186
|
-
end
|
187
|
-
|
188
|
-
headers['Set-Cookie'] = cookies.map do |cookie|
|
189
|
-
cookie !~ /(^|;\s)secure($|;)/ ? "#{cookie}; secure" : cookie
|
190
|
-
end.join("\n")
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
# see http://en.wikipedia.org/wiki/Strict_Transport_Security
|
195
|
-
def set_hsts_headers!(headers)
|
196
|
-
opts = { :expires => 31536000, :subdomains => true }
|
197
|
-
opts.merge!(@options[:hsts]) if @options[:hsts].is_a? Hash
|
198
|
-
value = "max-age=#{opts[:expires]}"
|
199
|
-
value += "; includeSubDomains" if opts[:subdomains]
|
200
|
-
headers.merge!({ 'Strict-Transport-Security' => value })
|
201
|
-
end
|
202
|
-
|
203
|
-
end
|
204
|
-
end
|
1
|
+
require 'rack/ssl-enforcer/constraint'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
|
5
|
+
class SslEnforcer
|
6
|
+
|
7
|
+
CONSTRAINTS_BY_TYPE = {
|
8
|
+
:hosts => [:only_hosts, :except_hosts],
|
9
|
+
:agents => [:only_agents, :except_agents],
|
10
|
+
:path => [:only, :except],
|
11
|
+
:methods => [:only_methods, :except_methods],
|
12
|
+
:environments => [:only_environments, :except_environments]
|
13
|
+
}
|
14
|
+
|
15
|
+
# Warning: If you set the option force_secure_cookies to false, make sure that your cookies
|
16
|
+
# are encoded and that you understand the consequences (see documentation)
|
17
|
+
def initialize(app, options={})
|
18
|
+
default_options = {
|
19
|
+
:redirect_to => nil,
|
20
|
+
:redirect_code => nil,
|
21
|
+
:strict => false,
|
22
|
+
:mixed => false,
|
23
|
+
:hsts => nil,
|
24
|
+
:http_port => nil,
|
25
|
+
:https_port => nil,
|
26
|
+
:force_secure_cookies => true,
|
27
|
+
:redirect_html => nil,
|
28
|
+
:before_redirect => nil
|
29
|
+
}
|
30
|
+
CONSTRAINTS_BY_TYPE.values.each do |constraints|
|
31
|
+
constraints.each { |constraint| default_options[constraint] = nil }
|
32
|
+
end
|
33
|
+
|
34
|
+
@app, @options = app, default_options.merge(options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(env)
|
38
|
+
@request = Rack::Request.new(env)
|
39
|
+
|
40
|
+
return @app.call(env) if ignore?
|
41
|
+
|
42
|
+
@scheme = if enforce_ssl?
|
43
|
+
'https'
|
44
|
+
elsif enforce_non_ssl?
|
45
|
+
'http'
|
46
|
+
end
|
47
|
+
|
48
|
+
if redirect_required?
|
49
|
+
call_before_redirect
|
50
|
+
modify_location_and_redirect
|
51
|
+
elsif ssl_request?
|
52
|
+
status, headers, body = @app.call(env)
|
53
|
+
flag_cookies_as_secure!(headers) if @options[:force_secure_cookies]
|
54
|
+
set_hsts_headers!(headers) if @options[:hsts] && !@options[:strict]
|
55
|
+
[status, headers, body]
|
56
|
+
else
|
57
|
+
@app.call(env)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def redirect_required?
|
64
|
+
scheme_mismatch? || host_mismatch?
|
65
|
+
end
|
66
|
+
|
67
|
+
def ignore?
|
68
|
+
if @options[:ignore]
|
69
|
+
rules = [@options[:ignore]].flatten.compact
|
70
|
+
rules.any? do |rule|
|
71
|
+
SslEnforcerConstraint.new(:ignore, rule, @request).matches?
|
72
|
+
end
|
73
|
+
else
|
74
|
+
false
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def scheme_mismatch?
|
79
|
+
@scheme && @scheme != current_scheme
|
80
|
+
end
|
81
|
+
|
82
|
+
def host_mismatch?
|
83
|
+
destination_host && destination_host != @request.host
|
84
|
+
end
|
85
|
+
|
86
|
+
def call_before_redirect
|
87
|
+
@options[:before_redirect].call(@request) unless @options[:before_redirect].nil?
|
88
|
+
end
|
89
|
+
|
90
|
+
def modify_location_and_redirect
|
91
|
+
location = "#{current_scheme}://#{@request.host}#{@request.fullpath}"
|
92
|
+
location = replace_scheme(location, @scheme)
|
93
|
+
location = replace_host(location, @options[:redirect_to])
|
94
|
+
redirect_to(location)
|
95
|
+
end
|
96
|
+
|
97
|
+
def redirect_to(location)
|
98
|
+
body = []
|
99
|
+
body << "<html><body>You are being <a href=\"#{location}\">redirected</a>.</body></html>" if @options[:redirect_html].nil?
|
100
|
+
body << @options[:redirect_html] if @options[:redirect_html].is_a?(String)
|
101
|
+
body = @options[:redirect_html] if @options[:redirect_html].respond_to?('each')
|
102
|
+
|
103
|
+
[@options[:redirect_code] || 301, { 'Content-Type' => 'text/html', 'Location' => location }, body]
|
104
|
+
end
|
105
|
+
|
106
|
+
def ssl_request?
|
107
|
+
current_scheme == 'https'
|
108
|
+
end
|
109
|
+
|
110
|
+
def destination_host
|
111
|
+
if @options[:redirect_to]
|
112
|
+
host_parts = URI.split(@options[:redirect_to])
|
113
|
+
host_parts[2] || host_parts[5]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Fixed in rack >= 1.3
|
118
|
+
def current_scheme
|
119
|
+
if @request.env['HTTPS'] == 'on' || @request.env['HTTP_X_SSL_REQUEST'] == 'on'
|
120
|
+
'https'
|
121
|
+
elsif @request.env['HTTP_X_FORWARDED_PROTO']
|
122
|
+
@request.env['HTTP_X_FORWARDED_PROTO'].split(',')[0]
|
123
|
+
else
|
124
|
+
@request.scheme
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def enforce_ssl_for?(keys)
|
129
|
+
provided_keys = keys.select { |key| @options[key] }
|
130
|
+
if provided_keys.empty?
|
131
|
+
true
|
132
|
+
else
|
133
|
+
provided_keys.all? do |key|
|
134
|
+
rules = [@options[key]].flatten.compact
|
135
|
+
rules.send([:except_hosts, :except_agents, :except_environments, :except].include?(key) ? :all? : :any?) do |rule|
|
136
|
+
SslEnforcerConstraint.new(key, rule, @request).matches?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def enforce_non_ssl?
|
143
|
+
@options[:strict] || @options[:mixed] && !(@request.request_method == 'PUT' || @request.request_method == 'POST')
|
144
|
+
end
|
145
|
+
|
146
|
+
def enforce_ssl?
|
147
|
+
CONSTRAINTS_BY_TYPE.inject(true) do |memo, (type, keys)|
|
148
|
+
memo && enforce_ssl_for?(keys)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def replace_scheme(uri, scheme)
|
153
|
+
return uri if not scheme_mismatch?
|
154
|
+
|
155
|
+
port = adjust_port_to(scheme)
|
156
|
+
uri_parts = URI.split(uri)
|
157
|
+
uri_parts[3] = port unless port.nil?
|
158
|
+
uri_parts[0] = scheme
|
159
|
+
URI::HTTP.new(*uri_parts).to_s
|
160
|
+
end
|
161
|
+
|
162
|
+
def replace_host(uri, host)
|
163
|
+
return uri unless host_mismatch?
|
164
|
+
|
165
|
+
host_parts = URI.split(host)
|
166
|
+
new_host = host_parts[2] || host_parts[5]
|
167
|
+
uri_parts = URI.split(uri)
|
168
|
+
uri_parts[2] = new_host
|
169
|
+
URI::HTTPS.new(*uri_parts).to_s
|
170
|
+
end
|
171
|
+
|
172
|
+
def adjust_port_to(scheme)
|
173
|
+
if scheme == 'https'
|
174
|
+
@options[:https_port] if @options[:https_port] && @options[:https_port] != URI::HTTPS.default_port
|
175
|
+
elsif scheme == 'http'
|
176
|
+
@options[:http_port] if @options[:http_port] && @options[:http_port] != URI::HTTP.default_port
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# see http://en.wikipedia.org/wiki/HTTP_cookie#Cookie_theft_and_session_hijacking
|
181
|
+
def flag_cookies_as_secure!(headers)
|
182
|
+
if cookies = headers['Set-Cookie']
|
183
|
+
# Support Rails 2.3 / Rack 1.1 arrays as headers
|
184
|
+
unless cookies.is_a?(Array)
|
185
|
+
cookies = cookies.split("\n")
|
186
|
+
end
|
187
|
+
|
188
|
+
headers['Set-Cookie'] = cookies.map do |cookie|
|
189
|
+
cookie !~ /(^|;\s)secure($|;)/ ? "#{cookie}; secure" : cookie
|
190
|
+
end.join("\n")
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# see http://en.wikipedia.org/wiki/Strict_Transport_Security
|
195
|
+
def set_hsts_headers!(headers)
|
196
|
+
opts = { :expires => 31536000, :subdomains => true }
|
197
|
+
opts.merge!(@options[:hsts]) if @options[:hsts].is_a? Hash
|
198
|
+
value = "max-age=#{opts[:expires]}"
|
199
|
+
value += "; includeSubDomains" if opts[:subdomains]
|
200
|
+
headers.merge!({ 'Strict-Transport-Security' => value })
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
@@ -1,42 +1,44 @@
|
|
1
|
-
class SslEnforcerConstraint
|
2
|
-
def initialize(name, rule, request)
|
3
|
-
@name = name
|
4
|
-
@rule = rule
|
5
|
-
@request = request
|
6
|
-
end
|
7
|
-
|
8
|
-
def matches?
|
9
|
-
if @rule.is_a?(String) && [:only, :except].include?(@name)
|
10
|
-
result = tested_string[0, @rule.size].send(operator, @rule)
|
11
|
-
|
12
|
-
result =
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
when /
|
33
|
-
@request.
|
34
|
-
when /
|
35
|
-
|
36
|
-
when /
|
37
|
-
|
38
|
-
|
39
|
-
@request.
|
40
|
-
|
41
|
-
|
42
|
-
end
|
1
|
+
class SslEnforcerConstraint
|
2
|
+
def initialize(name, rule, request)
|
3
|
+
@name = name
|
4
|
+
@rule = rule
|
5
|
+
@request = request
|
6
|
+
end
|
7
|
+
|
8
|
+
def matches?
|
9
|
+
if @rule.is_a?(String) && [:only, :except].include?(@name)
|
10
|
+
result = tested_string[0, @rule.size].send(operator, @rule)
|
11
|
+
elsif @rule.respond_to?(:call)
|
12
|
+
result = @rule.call(@request)
|
13
|
+
else
|
14
|
+
result = tested_string.send(operator, @rule)
|
15
|
+
end
|
16
|
+
|
17
|
+
negate_result? ? !result : result
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def negate_result?
|
23
|
+
@name.to_s =~ /except/
|
24
|
+
end
|
25
|
+
|
26
|
+
def operator
|
27
|
+
@rule.is_a?(Regexp) ? "=~" : "=="
|
28
|
+
end
|
29
|
+
|
30
|
+
def tested_string
|
31
|
+
case @name.to_s
|
32
|
+
when /hosts/
|
33
|
+
@request.host
|
34
|
+
when /methods/
|
35
|
+
@request.request_method
|
36
|
+
when /environments/
|
37
|
+
ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["ENV"]
|
38
|
+
when /agents/
|
39
|
+
@request.user_agent
|
40
|
+
else
|
41
|
+
@request.path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|