actionpack 5.2.4.rc1 → 6.0.0.rc2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -335
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -2
  5. data/lib/abstract_controller/base.rb +4 -2
  6. data/lib/abstract_controller/caching/fragments.rb +6 -22
  7. data/lib/abstract_controller/callbacks.rb +12 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +2 -2
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
  11. data/lib/abstract_controller/translation.rb +1 -0
  12. data/lib/action_controller.rb +1 -0
  13. data/lib/action_controller/api.rb +2 -1
  14. data/lib/action_controller/base.rb +2 -7
  15. data/lib/action_controller/caching.rb +1 -1
  16. data/lib/action_controller/log_subscriber.rb +8 -5
  17. data/lib/action_controller/metal.rb +1 -1
  18. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  19. data/lib/action_controller/metal/conditional_get.rb +9 -3
  20. data/lib/action_controller/metal/data_streaming.rb +5 -6
  21. data/lib/action_controller/metal/default_headers.rb +17 -0
  22. data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
  23. data/lib/action_controller/metal/exceptions.rb +22 -1
  24. data/lib/action_controller/metal/flash.rb +5 -5
  25. data/lib/action_controller/metal/force_ssl.rb +15 -56
  26. data/lib/action_controller/metal/head.rb +1 -1
  27. data/lib/action_controller/metal/helpers.rb +3 -4
  28. data/lib/action_controller/metal/http_authentication.rb +20 -21
  29. data/lib/action_controller/metal/implicit_render.rb +4 -14
  30. data/lib/action_controller/metal/instrumentation.rb +3 -5
  31. data/lib/action_controller/metal/live.rb +29 -27
  32. data/lib/action_controller/metal/mime_responds.rb +13 -2
  33. data/lib/action_controller/metal/params_wrapper.rb +17 -13
  34. data/lib/action_controller/metal/redirecting.rb +5 -5
  35. data/lib/action_controller/metal/renderers.rb +1 -1
  36. data/lib/action_controller/metal/rendering.rb +2 -2
  37. data/lib/action_controller/metal/request_forgery_protection.rb +23 -12
  38. data/lib/action_controller/metal/strong_parameters.rb +63 -44
  39. data/lib/action_controller/metal/url_for.rb +1 -1
  40. data/lib/action_controller/railties/helpers.rb +1 -1
  41. data/lib/action_controller/renderer.rb +16 -3
  42. data/lib/action_controller/template_assertions.rb +1 -1
  43. data/lib/action_controller/test_case.rb +3 -7
  44. data/lib/action_dispatch.rb +4 -1
  45. data/lib/action_dispatch/http/cache.rb +14 -10
  46. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  47. data/lib/action_dispatch/http/content_security_policy.rb +28 -16
  48. data/lib/action_dispatch/http/filter_parameters.rb +8 -6
  49. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  50. data/lib/action_dispatch/http/headers.rb +1 -1
  51. data/lib/action_dispatch/http/mime_negotiation.rb +7 -5
  52. data/lib/action_dispatch/http/mime_type.rb +14 -6
  53. data/lib/action_dispatch/http/parameter_filter.rb +5 -79
  54. data/lib/action_dispatch/http/parameters.rb +13 -3
  55. data/lib/action_dispatch/http/request.rb +10 -13
  56. data/lib/action_dispatch/http/response.rb +33 -19
  57. data/lib/action_dispatch/http/upload.rb +9 -1
  58. data/lib/action_dispatch/http/url.rb +81 -81
  59. data/lib/action_dispatch/journey/formatter.rb +2 -2
  60. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -2
  61. data/lib/action_dispatch/journey/nodes/node.rb +9 -8
  62. data/lib/action_dispatch/journey/path/pattern.rb +6 -2
  63. data/lib/action_dispatch/journey/route.rb +5 -4
  64. data/lib/action_dispatch/journey/router.rb +0 -3
  65. data/lib/action_dispatch/journey/router/utils.rb +10 -10
  66. data/lib/action_dispatch/journey/routes.rb +0 -1
  67. data/lib/action_dispatch/journey/scanner.rb +11 -4
  68. data/lib/action_dispatch/journey/visitors.rb +1 -1
  69. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  70. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  71. data/lib/action_dispatch/middleware/cookies.rb +46 -72
  72. data/lib/action_dispatch/middleware/debug_exceptions.rb +39 -59
  73. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  74. data/lib/action_dispatch/middleware/debug_view.rb +68 -0
  75. data/lib/action_dispatch/middleware/exception_wrapper.rb +49 -15
  76. data/lib/action_dispatch/middleware/flash.rb +1 -1
  77. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  78. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -2
  79. data/lib/action_dispatch/middleware/remote_ip.rb +6 -8
  80. data/lib/action_dispatch/middleware/request_id.rb +2 -2
  81. data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -6
  82. data/lib/action_dispatch/middleware/show_exceptions.rb +1 -1
  83. data/lib/action_dispatch/middleware/ssl.rb +8 -8
  84. data/lib/action_dispatch/middleware/stack.rb +33 -1
  85. data/lib/action_dispatch/middleware/static.rb +5 -6
  86. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  88. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  89. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  90. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  91. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  92. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  93. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  94. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -4
  95. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  96. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +7 -4
  97. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -2
  98. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +4 -0
  99. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  100. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  101. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  102. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  103. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  104. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +3 -0
  105. data/lib/action_dispatch/railtie.rb +3 -0
  106. data/lib/action_dispatch/request/session.rb +8 -0
  107. data/lib/action_dispatch/routing.rb +21 -20
  108. data/lib/action_dispatch/routing/inspector.rb +99 -50
  109. data/lib/action_dispatch/routing/mapper.rb +60 -38
  110. data/lib/action_dispatch/routing/polymorphic_routes.rb +3 -4
  111. data/lib/action_dispatch/routing/route_set.rb +24 -27
  112. data/lib/action_dispatch/routing/url_for.rb +1 -0
  113. data/lib/action_dispatch/system_test_case.rb +23 -2
  114. data/lib/action_dispatch/system_testing/browser.rb +38 -7
  115. data/lib/action_dispatch/system_testing/driver.rb +10 -1
  116. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +6 -5
  117. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +7 -5
  118. data/lib/action_dispatch/testing/assertions.rb +1 -1
  119. data/lib/action_dispatch/testing/assertions/response.rb +2 -3
  120. data/lib/action_dispatch/testing/assertions/routing.rb +15 -3
  121. data/lib/action_dispatch/testing/integration.rb +12 -5
  122. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  123. data/lib/action_dispatch/testing/test_process.rb +2 -2
  124. data/lib/action_dispatch/testing/test_response.rb +4 -32
  125. data/lib/action_pack.rb +1 -1
  126. data/lib/action_pack/gem_version.rb +4 -4
  127. metadata +24 -13
@@ -3,57 +3,28 @@
3
3
  require "action_dispatch/http/request"
4
4
  require "action_dispatch/middleware/exception_wrapper"
5
5
  require "action_dispatch/routing/inspector"
6
+
7
+ require "active_support/actionable_error"
8
+
6
9
  require "action_view"
7
10
  require "action_view/base"
8
11
 
9
- require "pp"
10
-
11
12
  module ActionDispatch
12
13
  # This middleware is responsible for logging exceptions and
13
14
  # showing a debugging page in case the request is local.
14
15
  class DebugExceptions
15
- RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
16
-
17
- class DebugView < ActionView::Base
18
- def debug_params(params)
19
- clean_params = params.clone
20
- clean_params.delete("action")
21
- clean_params.delete("controller")
22
-
23
- if clean_params.empty?
24
- "None"
25
- else
26
- PP.pp(clean_params, "".dup, 200)
27
- end
28
- end
29
-
30
- def debug_headers(headers)
31
- if headers.present?
32
- headers.inspect.gsub(",", ",\n")
33
- else
34
- "None"
35
- end
36
- end
37
-
38
- def debug_hash(object)
39
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
40
- end
41
-
42
- def render(*)
43
- logger = ActionView::Base.logger
16
+ cattr_reader :interceptors, instance_accessor: false, default: []
44
17
 
45
- if logger && logger.respond_to?(:silence)
46
- logger.silence { super }
47
- else
48
- super
49
- end
50
- end
18
+ def self.register_interceptor(object = nil, &block)
19
+ interceptor = object || block
20
+ interceptors << interceptor
51
21
  end
52
22
 
53
- def initialize(app, routes_app = nil, response_format = :default)
23
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
54
24
  @app = app
55
25
  @routes_app = routes_app
56
26
  @response_format = response_format
27
+ @interceptors = interceptors
57
28
  end
58
29
 
59
30
  def call(env)
@@ -67,19 +38,35 @@ module ActionDispatch
67
38
 
68
39
  response
69
40
  rescue Exception => exception
41
+ invoke_interceptors(request, exception)
70
42
  raise exception unless request.show_exceptions?
71
43
  render_exception(request, exception)
72
44
  end
73
45
 
74
46
  private
75
47
 
48
+ def invoke_interceptors(request, exception)
49
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
50
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
51
+
52
+ @interceptors.each do |interceptor|
53
+ interceptor.call(request, exception)
54
+ rescue Exception
55
+ log_error(request, wrapper)
56
+ end
57
+ end
58
+
76
59
  def render_exception(request, exception)
77
60
  backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
78
61
  wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
79
62
  log_error(request, wrapper)
80
63
 
81
64
  if request.get_header("action_dispatch.show_detailed_exceptions")
82
- content_type = request.formats.first
65
+ begin
66
+ content_type = request.formats.first
67
+ rescue Mime::Type::InvalidMimeType
68
+ render_for_api_request(Mime[:text], wrapper)
69
+ end
83
70
 
84
71
  if api_request?(content_type)
85
72
  render_for_api_request(content_type, wrapper)
@@ -130,23 +117,13 @@ module ActionDispatch
130
117
  end
131
118
 
132
119
  def create_template(request, wrapper)
133
- traces = wrapper.traces
134
-
135
- trace_to_show = "Application Trace"
136
- if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error"
137
- trace_to_show = "Full Trace"
138
- end
139
-
140
- if source_to_show = traces[trace_to_show].first
141
- source_to_show_id = source_to_show[:id]
142
- end
143
-
144
- DebugView.new([RESCUES_TEMPLATE_PATH],
120
+ DebugView.new(
145
121
  request: request,
122
+ exception_wrapper: wrapper,
146
123
  exception: wrapper.exception,
147
- traces: traces,
148
- show_source_idx: source_to_show_id,
149
- trace_to_show: trace_to_show,
124
+ traces: wrapper.traces,
125
+ show_source_idx: wrapper.source_to_show_id,
126
+ trace_to_show: wrapper.trace_to_show,
150
127
  routes_inspector: routes_inspector(wrapper.exception),
151
128
  source_extracts: wrapper.source_extracts,
152
129
  line_number: wrapper.line_number,
@@ -168,11 +145,14 @@ module ActionDispatch
168
145
  trace = wrapper.framework_trace if trace.empty?
169
146
 
170
147
  ActiveSupport::Deprecation.silence do
171
- logger.fatal " "
172
- logger.fatal "#{exception.class} (#{exception.message}):"
173
- log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
174
- logger.fatal " "
175
- log_array logger, trace
148
+ message = []
149
+ message << " "
150
+ message << "#{exception.class} (#{exception.message}):"
151
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
152
+ message << " "
153
+ message.concat(trace)
154
+
155
+ log_array(logger, message)
176
156
  end
177
157
  end
178
158
 
@@ -32,7 +32,7 @@ module ActionDispatch
32
32
  req = ActionDispatch::Request.new env
33
33
 
34
34
  if req.get?
35
- path = req.path_info.chomp("/".freeze)
35
+ path = req.path_info.chomp("/")
36
36
  if path == @path
37
37
  return render_details(req)
38
38
  end
@@ -63,19 +63,19 @@ module ActionDispatch
63
63
 
64
64
  str = threads.map do |thread, info|
65
65
  if info[:exclusive]
66
- lock_state = "Exclusive".dup
66
+ lock_state = +"Exclusive"
67
67
  elsif info[:sharing] > 0
68
- lock_state = "Sharing".dup
68
+ lock_state = +"Sharing"
69
69
  lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
70
70
  else
71
- lock_state = "No lock".dup
71
+ lock_state = +"No lock"
72
72
  end
73
73
 
74
74
  if info[:waiting]
75
75
  lock_state << " (yielded share)"
76
76
  end
77
77
 
78
- msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup
78
+ msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
79
79
 
80
80
  if info[:sleeper]
81
81
  msg << " Waiting in #{info[:sleeper]}"
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
4
+
5
+ require "action_view"
6
+ require "action_view/base"
7
+
8
+ module ActionDispatch
9
+ class DebugView < ActionView::Base # :nodoc:
10
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
11
+
12
+ def initialize(assigns)
13
+ paths = [RESCUES_TEMPLATE_PATH]
14
+ lookup_context = ActionView::LookupContext.new(paths)
15
+ super(lookup_context, assigns)
16
+ end
17
+
18
+ def compiled_method_container
19
+ self.class
20
+ end
21
+
22
+ def debug_params(params)
23
+ clean_params = params.clone
24
+ clean_params.delete("action")
25
+ clean_params.delete("controller")
26
+
27
+ if clean_params.empty?
28
+ "None"
29
+ else
30
+ PP.pp(clean_params, +"", 200)
31
+ end
32
+ end
33
+
34
+ def debug_headers(headers)
35
+ if headers.present?
36
+ headers.inspect.gsub(",", ",\n")
37
+ else
38
+ "None"
39
+ end
40
+ end
41
+
42
+ def debug_hash(object)
43
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
44
+ end
45
+
46
+ def render(*)
47
+ logger = ActionView::Base.logger
48
+
49
+ if logger && logger.respond_to?(:silence)
50
+ logger.silence { super }
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def protect_against_forgery?
57
+ false
58
+ end
59
+
60
+ def params_valid?
61
+ begin
62
+ @request.parameters
63
+ rescue ActionController::BadRequest
64
+ false
65
+ end
66
+ end
67
+ end
68
+ end
@@ -12,6 +12,8 @@ module ActionDispatch
12
12
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
13
13
  "ActionController::NotImplemented" => :not_implemented,
14
14
  "ActionController::UnknownFormat" => :not_acceptable,
15
+ "Mime::Type::InvalidMimeType" => :not_acceptable,
16
+ "ActionController::MissingExactTemplate" => :not_acceptable,
15
17
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
16
18
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
17
19
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
@@ -22,28 +24,42 @@ module ActionDispatch
22
24
  )
23
25
 
24
26
  cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
25
- "ActionView::MissingTemplate" => "missing_template",
26
- "ActionController::RoutingError" => "routing_error",
27
- "AbstractController::ActionNotFound" => "unknown_action",
28
- "ActiveRecord::StatementInvalid" => "invalid_statement",
29
- "ActionView::Template::Error" => "template_error"
27
+ "ActionView::MissingTemplate" => "missing_template",
28
+ "ActionController::RoutingError" => "routing_error",
29
+ "AbstractController::ActionNotFound" => "unknown_action",
30
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
31
+ "ActionView::Template::Error" => "template_error",
32
+ "ActionController::MissingExactTemplate" => "missing_exact_template",
30
33
  )
31
34
 
32
- attr_reader :backtrace_cleaner, :exception, :line_number, :file
35
+ cattr_accessor :wrapper_exceptions, default: [
36
+ "ActionView::Template::Error"
37
+ ]
38
+
39
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
33
40
 
34
41
  def initialize(backtrace_cleaner, exception)
35
42
  @backtrace_cleaner = backtrace_cleaner
36
- @exception = original_exception(exception)
43
+ @exception = exception
44
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
37
45
 
38
46
  expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
39
47
  end
40
48
 
49
+ def unwrapped_exception
50
+ if wrapper_exceptions.include?(exception.class.to_s)
51
+ exception.cause
52
+ else
53
+ exception
54
+ end
55
+ end
56
+
41
57
  def rescue_template
42
58
  @@rescue_templates[@exception.class.name]
43
59
  end
44
60
 
45
61
  def status_code
46
- self.class.status_code_for_exception(@exception.class.name)
62
+ self.class.status_code_for_exception(unwrapped_exception.class.name)
47
63
  end
48
64
 
49
65
  def application_trace
@@ -64,7 +80,11 @@ module ActionDispatch
64
80
  full_trace_with_ids = []
65
81
 
66
82
  full_trace.each_with_index do |trace, idx|
67
- trace_with_id = { id: idx, trace: trace }
83
+ trace_with_id = {
84
+ exception_object_id: @exception.object_id,
85
+ id: idx,
86
+ trace: trace
87
+ }
68
88
 
69
89
  if application_trace.include?(trace)
70
90
  application_trace_with_ids << trace_with_id
@@ -97,18 +117,32 @@ module ActionDispatch
97
117
  end
98
118
  end
99
119
 
120
+ def trace_to_show
121
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
122
+ "Full Trace"
123
+ else
124
+ "Application Trace"
125
+ end
126
+ end
127
+
128
+ def source_to_show_id
129
+ (traces[trace_to_show].first || {})[:id]
130
+ end
131
+
100
132
  private
101
133
 
102
134
  def backtrace
103
135
  Array(@exception.backtrace)
104
136
  end
105
137
 
106
- def original_exception(exception)
107
- if @@rescue_responses.has_key?(exception.cause.class.name)
108
- exception.cause
109
- else
110
- exception
111
- end
138
+ def causes_for(exception)
139
+ return enum_for(__method__, exception) unless block_given?
140
+
141
+ yield exception while exception = exception.cause
142
+ end
143
+
144
+ def wrapped_causes_for(exception, backtrace_cleaner)
145
+ causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
112
146
  end
113
147
 
114
148
  def clean_backtrace(*args)
@@ -38,7 +38,7 @@ module ActionDispatch
38
38
  #
39
39
  # See docs on the FlashHash class for more details about the flash.
40
40
  class Flash
41
- KEY = "action_dispatch.request.flash_hash".freeze
41
+ KEY = "action_dispatch.request.flash_hash"
42
42
 
43
43
  module RequestMethods
44
44
  # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/request"
4
+
5
+ module ActionDispatch
6
+ # This middleware guards from DNS rebinding attacks by explicitly permitting
7
+ # the hosts a request can be sent to.
8
+ #
9
+ # When a request comes to an unauthorized host, the +response_app+
10
+ # application will be executed and rendered. If no +response_app+ is given, a
11
+ # default one will run, which responds with +403 Forbidden+.
12
+ class HostAuthorization
13
+ class Permissions # :nodoc:
14
+ def initialize(hosts)
15
+ @hosts = sanitize_hosts(hosts)
16
+ end
17
+
18
+ def empty?
19
+ @hosts.empty?
20
+ end
21
+
22
+ def allows?(host)
23
+ @hosts.any? do |allowed|
24
+ allowed === host
25
+ rescue
26
+ # IPAddr#=== raises an error if you give it a hostname instead of
27
+ # IP. Treat similar errors as blocked access.
28
+ false
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def sanitize_hosts(hosts)
35
+ Array(hosts).map do |host|
36
+ case host
37
+ when Regexp then sanitize_regexp(host)
38
+ when String then sanitize_string(host)
39
+ else host
40
+ end
41
+ end
42
+ end
43
+
44
+ def sanitize_regexp(host)
45
+ /\A#{host}\z/
46
+ end
47
+
48
+ def sanitize_string(host)
49
+ if host.start_with?(".")
50
+ /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
51
+ else
52
+ host
53
+ end
54
+ end
55
+ end
56
+
57
+ DEFAULT_RESPONSE_APP = -> env do
58
+ request = Request.new(env)
59
+
60
+ format = request.xhr? ? "text/plain" : "text/html"
61
+ template = DebugView.new(host: request.host)
62
+ body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
63
+
64
+ [403, {
65
+ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
66
+ "Content-Length" => body.bytesize.to_s,
67
+ }, [body]]
68
+ end
69
+
70
+ def initialize(app, hosts, response_app = nil)
71
+ @app = app
72
+ @permissions = Permissions.new(hosts)
73
+ @response_app = response_app || DEFAULT_RESPONSE_APP
74
+ end
75
+
76
+ def call(env)
77
+ return @app.call(env) if @permissions.empty?
78
+
79
+ request = Request.new(env)
80
+
81
+ if authorized?(request)
82
+ mark_as_authorized(request)
83
+ @app.call(env)
84
+ else
85
+ @response_app.call(env)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def authorized?(request)
92
+ origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
93
+ forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
94
+
95
+ @permissions.allows?(origin_host) &&
96
+ (forwarded_host.blank? || @permissions.allows?(forwarded_host))
97
+ end
98
+
99
+ def mark_as_authorized(request)
100
+ request.set_header("action_dispatch.authorized_host", request.host)
101
+ end
102
+ end
103
+ end