omg-actionpack 8.0.0.alpha1

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.
Files changed (187) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +129 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller/asset_paths.rb +14 -0
  6. data/lib/abstract_controller/base.rb +299 -0
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +265 -0
  10. data/lib/abstract_controller/collector.rb +44 -0
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +243 -0
  14. data/lib/abstract_controller/logger.rb +16 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
  16. data/lib/abstract_controller/rendering.rb +126 -0
  17. data/lib/abstract_controller/translation.rb +42 -0
  18. data/lib/abstract_controller/url_for.rb +37 -0
  19. data/lib/abstract_controller.rb +36 -0
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +155 -0
  22. data/lib/action_controller/base.rb +332 -0
  23. data/lib/action_controller/caching.rb +49 -0
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +96 -0
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +341 -0
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +20 -0
  32. data/lib/action_controller/metal/data_streaming.rb +154 -0
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
  36. data/lib/action_controller/metal/exceptions.rb +106 -0
  37. data/lib/action_controller/metal/flash.rb +67 -0
  38. data/lib/action_controller/metal/head.rb +67 -0
  39. data/lib/action_controller/metal/helpers.rb +129 -0
  40. data/lib/action_controller/metal/http_authentication.rb +565 -0
  41. data/lib/action_controller/metal/implicit_render.rb +67 -0
  42. data/lib/action_controller/metal/instrumentation.rb +120 -0
  43. data/lib/action_controller/metal/live.rb +398 -0
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +337 -0
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +312 -0
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +251 -0
  51. data/lib/action_controller/metal/renderers.rb +181 -0
  52. data/lib/action_controller/metal/rendering.rb +260 -0
  53. data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
  54. data/lib/action_controller/metal/rescue.rb +33 -0
  55. data/lib/action_controller/metal/streaming.rb +183 -0
  56. data/lib/action_controller/metal/strong_parameters.rb +1546 -0
  57. data/lib/action_controller/metal/testing.rb +25 -0
  58. data/lib/action_controller/metal/url_for.rb +65 -0
  59. data/lib/action_controller/metal.rb +339 -0
  60. data/lib/action_controller/railtie.rb +149 -0
  61. data/lib/action_controller/railties/helpers.rb +26 -0
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +691 -0
  65. data/lib/action_controller.rb +80 -0
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +249 -0
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +365 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +80 -0
  72. data/lib/action_dispatch/http/filter_redirect.rb +50 -0
  73. data/lib/action_dispatch/http/headers.rb +134 -0
  74. data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
  75. data/lib/action_dispatch/http/mime_type.rb +389 -0
  76. data/lib/action_dispatch/http/mime_types.rb +54 -0
  77. data/lib/action_dispatch/http/parameters.rb +119 -0
  78. data/lib/action_dispatch/http/permissions_policy.rb +189 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +67 -0
  80. data/lib/action_dispatch/http/request.rb +498 -0
  81. data/lib/action_dispatch/http/response.rb +556 -0
  82. data/lib/action_dispatch/http/upload.rb +107 -0
  83. data/lib/action_dispatch/http/url.rb +344 -0
  84. data/lib/action_dispatch/journey/formatter.rb +226 -0
  85. data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
  88. data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +208 -0
  90. data/lib/action_dispatch/journey/parser.rb +103 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +209 -0
  92. data/lib/action_dispatch/journey/route.rb +189 -0
  93. data/lib/action_dispatch/journey/router/utils.rb +105 -0
  94. data/lib/action_dispatch/journey/router.rb +151 -0
  95. data/lib/action_dispatch/journey/routes.rb +82 -0
  96. data/lib/action_dispatch/journey/scanner.rb +70 -0
  97. data/lib/action_dispatch/journey/visitors.rb +267 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/journey.rb +7 -0
  102. data/lib/action_dispatch/log_subscriber.rb +25 -0
  103. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  104. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  105. data/lib/action_dispatch/middleware/callbacks.rb +38 -0
  106. data/lib/action_dispatch/middleware/cookies.rb +719 -0
  107. data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
  108. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  109. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  110. data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
  111. data/lib/action_dispatch/middleware/executor.rb +32 -0
  112. data/lib/action_dispatch/middleware/flash.rb +318 -0
  113. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  114. data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
  115. data/lib/action_dispatch/middleware/reloader.rb +16 -0
  116. data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
  117. data/lib/action_dispatch/middleware/request_id.rb +50 -0
  118. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  119. data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
  120. data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
  121. data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
  122. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
  123. data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
  124. data/lib/action_dispatch/middleware/ssl.rb +180 -0
  125. data/lib/action_dispatch/middleware/stack.rb +194 -0
  126. data/lib/action_dispatch/middleware/static.rb +192 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  148. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  149. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  150. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  151. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  152. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  153. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
  154. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
  155. data/lib/action_dispatch/railtie.rb +77 -0
  156. data/lib/action_dispatch/request/session.rb +283 -0
  157. data/lib/action_dispatch/request/utils.rb +109 -0
  158. data/lib/action_dispatch/routing/endpoint.rb +19 -0
  159. data/lib/action_dispatch/routing/inspector.rb +323 -0
  160. data/lib/action_dispatch/routing/mapper.rb +2372 -0
  161. data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
  162. data/lib/action_dispatch/routing/redirection.rb +218 -0
  163. data/lib/action_dispatch/routing/route_set.rb +958 -0
  164. data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
  165. data/lib/action_dispatch/routing/url_for.rb +244 -0
  166. data/lib/action_dispatch/routing.rb +262 -0
  167. data/lib/action_dispatch/system_test_case.rb +206 -0
  168. data/lib/action_dispatch/system_testing/browser.rb +75 -0
  169. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  170. data/lib/action_dispatch/system_testing/server.rb +33 -0
  171. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  172. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  173. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  174. data/lib/action_dispatch/testing/assertions/response.rb +114 -0
  175. data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
  176. data/lib/action_dispatch/testing/assertions.rb +25 -0
  177. data/lib/action_dispatch/testing/integration.rb +694 -0
  178. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  179. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  180. data/lib/action_dispatch/testing/test_process.rb +57 -0
  181. data/lib/action_dispatch/testing/test_request.rb +73 -0
  182. data/lib/action_dispatch/testing/test_response.rb +58 -0
  183. data/lib/action_dispatch.rb +147 -0
  184. data/lib/action_pack/gem_version.rb +19 -0
  185. data/lib/action_pack/version.rb +12 -0
  186. data/lib/action_pack.rb +27 -0
  187. metadata +375 -0
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ # # Action Dispatch HostAuthorization
7
+ #
8
+ # This middleware guards from DNS rebinding attacks by explicitly permitting the
9
+ # hosts a request can be sent to, and is passed the options set in
10
+ # `config.host_authorization`.
11
+ #
12
+ # Requests can opt-out of Host Authorization with `exclude`:
13
+ #
14
+ # config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
15
+ #
16
+ # When a request comes to an unauthorized host, the `response_app` application
17
+ # will be executed and rendered. If no `response_app` is given, a default one
18
+ # will run. The default response app logs blocked host info with level 'error'
19
+ # and responds with `403 Forbidden`. The body of the response contains debug
20
+ # info if `config.consider_all_requests_local` is set to true, otherwise the
21
+ # body is empty.
22
+ class HostAuthorization
23
+ ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", ".test", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
24
+ PORT_REGEX = /(?::\d+)/ # :nodoc:
25
+ SUBDOMAIN_REGEX = /(?:[a-z0-9-]+\.)/i # :nodoc:
26
+ IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
27
+ IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
28
+ IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
29
+ VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
30
+ /\A#{IPV4_HOSTNAME}\z/,
31
+ /\A#{IPV6_HOSTNAME}\z/,
32
+ /\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
33
+ )
34
+
35
+ class Permissions # :nodoc:
36
+ def initialize(hosts)
37
+ @hosts = sanitize_hosts(hosts)
38
+ end
39
+
40
+ def empty?
41
+ @hosts.empty?
42
+ end
43
+
44
+ def allows?(host)
45
+ @hosts.any? do |allowed|
46
+ if allowed.is_a?(IPAddr)
47
+ begin
48
+ allowed === extract_hostname(host)
49
+ rescue
50
+ # IPAddr#=== raises an error if you give it a hostname instead of IP. Treat
51
+ # similar errors as blocked access.
52
+ false
53
+ end
54
+ else
55
+ allowed === host
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+ def sanitize_hosts(hosts)
62
+ Array(hosts).map do |host|
63
+ case host
64
+ when Regexp then sanitize_regexp(host)
65
+ when String then sanitize_string(host)
66
+ else host
67
+ end
68
+ end
69
+ end
70
+
71
+ def sanitize_regexp(host)
72
+ /\A#{host}#{PORT_REGEX}?\z/
73
+ end
74
+
75
+ def sanitize_string(host)
76
+ if host.start_with?(".")
77
+ /\A#{SUBDOMAIN_REGEX}?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
78
+ else
79
+ /\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
80
+ end
81
+ end
82
+
83
+ def extract_hostname(host)
84
+ host.slice(VALID_IP_HOSTNAME, "host") || host
85
+ end
86
+ end
87
+
88
+ class DefaultResponseApp # :nodoc:
89
+ RESPONSE_STATUS = 403
90
+
91
+ def call(env)
92
+ request = Request.new(env)
93
+ format = request.xhr? ? "text/plain" : "text/html"
94
+
95
+ log_error(request)
96
+ response(format, response_body(request))
97
+ end
98
+
99
+ private
100
+ def response_body(request)
101
+ return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
102
+
103
+ template = DebugView.new(hosts: request.env["action_dispatch.blocked_hosts"])
104
+ template.render(template: "rescues/blocked_host", layout: "rescues/layout")
105
+ end
106
+
107
+ def response(format, body)
108
+ [RESPONSE_STATUS,
109
+ { Rack::CONTENT_TYPE => "#{format}; charset=#{Response.default_charset}",
110
+ Rack::CONTENT_LENGTH => body.bytesize.to_s },
111
+ [body]]
112
+ end
113
+
114
+ def log_error(request)
115
+ logger = available_logger(request)
116
+
117
+ return unless logger
118
+
119
+ logger.error("[#{self.class.name}] Blocked hosts: #{request.env["action_dispatch.blocked_hosts"].join(", ")}")
120
+ end
121
+
122
+ def available_logger(request)
123
+ request.logger || ActionView::Base.logger
124
+ end
125
+ end
126
+
127
+ def initialize(app, hosts, exclude: nil, response_app: nil)
128
+ @app = app
129
+ @permissions = Permissions.new(hosts)
130
+ @exclude = exclude
131
+
132
+ @response_app = response_app || DefaultResponseApp.new
133
+ end
134
+
135
+ def call(env)
136
+ return @app.call(env) if @permissions.empty?
137
+
138
+ request = Request.new(env)
139
+ hosts = blocked_hosts(request)
140
+
141
+ if hosts.empty? || excluded?(request)
142
+ mark_as_authorized(request)
143
+ @app.call(env)
144
+ else
145
+ env["action_dispatch.blocked_hosts"] = hosts
146
+ @response_app.call(env)
147
+ end
148
+ end
149
+
150
+ private
151
+ def blocked_hosts(request)
152
+ hosts = []
153
+
154
+ origin_host = request.get_header("HTTP_HOST")
155
+ hosts << origin_host unless @permissions.allows?(origin_host)
156
+
157
+ forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
158
+ hosts << forwarded_host unless forwarded_host.blank? || @permissions.allows?(forwarded_host)
159
+
160
+ hosts
161
+ end
162
+
163
+ def excluded?(request)
164
+ @exclude && @exclude.call(request)
165
+ end
166
+
167
+ def mark_as_authorized(request)
168
+ request.set_header("action_dispatch.authorized_host", request.host)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ # # Action Dispatch PublicExceptions
7
+ #
8
+ # When called, this middleware renders an error page. By default if an HTML
9
+ # response is expected it will render static error pages from the `/public`
10
+ # directory. For example when this middleware receives a 500 response it will
11
+ # render the template found in `/public/500.html`. If an internationalized
12
+ # locale is set, this middleware will attempt to render the template in
13
+ # `/public/500.<locale>.html`. If an internationalized template is not found it
14
+ # will fall back on `/public/500.html`.
15
+ #
16
+ # When a request with a content type other than HTML is made, this middleware
17
+ # will attempt to convert error information into the appropriate response type.
18
+ class PublicExceptions
19
+ attr_accessor :public_path
20
+
21
+ def initialize(public_path)
22
+ @public_path = public_path
23
+ end
24
+
25
+ def call(env)
26
+ request = ActionDispatch::Request.new(env)
27
+ status = request.path_info[1..-1].to_i
28
+ begin
29
+ content_type = request.formats.first
30
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
31
+ content_type = Mime[:text]
32
+ end
33
+ body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
34
+
35
+ render(status, content_type, body)
36
+ end
37
+
38
+ private
39
+ def render(status, content_type, body)
40
+ format = "to_#{content_type.to_sym}" if content_type
41
+ if format && body.respond_to?(format)
42
+ render_format(status, content_type, body.public_send(format))
43
+ else
44
+ render_html(status)
45
+ end
46
+ end
47
+
48
+ def render_format(status, content_type, body)
49
+ [status, { Rack::CONTENT_TYPE => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
50
+ Rack::CONTENT_LENGTH => body.bytesize.to_s }, [body]]
51
+ end
52
+
53
+ def render_html(status)
54
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
55
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
56
+
57
+ if found || File.exist?(path)
58
+ render_format(status, "text/html", File.read(path))
59
+ else
60
+ [404, { Constants::X_CASCADE => "pass" }, []]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ # # Action Dispatch Reloader
7
+ #
8
+ # ActionDispatch::Reloader wraps the request with callbacks provided by
9
+ # ActiveSupport::Reloader, intended to assist with code reloading during
10
+ # development.
11
+ #
12
+ # ActionDispatch::Reloader is included in the middleware stack only if reloading
13
+ # is enabled, which it is by the default in `development` mode.
14
+ class Reloader < Executor
15
+ end
16
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "ipaddr"
6
+
7
+ module ActionDispatch
8
+ # # Action Dispatch RemoteIp
9
+ #
10
+ # This middleware calculates the IP address of the remote client that is making
11
+ # the request. It does this by checking various headers that could contain the
12
+ # address, and then picking the last-set address that is not on the list of
13
+ # trusted IPs. This follows the precedent set by e.g. [the Tomcat
14
+ # server](https://issues.apache.org/bugzilla/show_bug.cgi?id=50453). A more
15
+ # detailed explanation of the algorithm is given at GetIp#calculate_ip.
16
+ #
17
+ # Some Rack servers concatenate repeated headers, like [HTTP RFC
18
+ # 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) requires.
19
+ # Some Rack servers simply drop preceding headers, and only report the value
20
+ # that was [given in the last
21
+ # header](https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-server
22
+ # s). If you are behind multiple proxy servers (like NGINX to HAProxy to
23
+ # Unicorn) then you should test your Rack server to make sure your data is good.
24
+ #
25
+ # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. This
26
+ # middleware assumes that there is at least one proxy sitting around and setting
27
+ # headers with the client's remote IP address. If you don't use a proxy, because
28
+ # you are hosted on e.g. Heroku without SSL, any client can claim to have any IP
29
+ # address by setting the `X-Forwarded-For` header. If you care about that, then
30
+ # you need to explicitly drop or ignore those headers sometime before this
31
+ # middleware runs. Alternatively, remove this middleware to avoid inadvertently
32
+ # relying on it.
33
+ class RemoteIp
34
+ class IpSpoofAttackError < StandardError; end
35
+
36
+ # The default trusted IPs list simply includes IP addresses that are guaranteed
37
+ # by the IP specification to be private addresses. Those will not be the
38
+ # ultimate client IP in production, and so are discarded. See
39
+ # https://en.wikipedia.org/wiki/Private_network for details.
40
+ TRUSTED_PROXIES = [
41
+ "127.0.0.0/8", # localhost IPv4 range, per RFC-3330
42
+ "::1", # localhost IPv6
43
+ "fc00::/7", # private IPv6 range fc00::/7
44
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
45
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
46
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
47
+ ].map { |proxy| IPAddr.new(proxy) }
48
+
49
+ attr_reader :check_ip, :proxies
50
+
51
+ # Create a new `RemoteIp` middleware instance.
52
+ #
53
+ # The `ip_spoofing_check` option is on by default. When on, an exception is
54
+ # raised if it looks like the client is trying to lie about its own IP address.
55
+ # It makes sense to turn off this check on sites aimed at non-IP clients (like
56
+ # WAP devices), or behind proxies that set headers in an incorrect or confusing
57
+ # way (like AWS ELB).
58
+ #
59
+ # The `custom_proxies` argument can take an enumerable which will be used
60
+ # instead of `TRUSTED_PROXIES`. Any proxy setup will put the value you want in
61
+ # the middle (or at the beginning) of the `X-Forwarded-For` list, with your
62
+ # proxy servers after it. If your proxies aren't removed, pass them in via the
63
+ # `custom_proxies` parameter. That way, the middleware will ignore those IP
64
+ # addresses, and return the one that you want.
65
+ def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
66
+ @app = app
67
+ @check_ip = ip_spoofing_check
68
+ @proxies = if custom_proxies.blank?
69
+ TRUSTED_PROXIES
70
+ elsif custom_proxies.respond_to?(:any?)
71
+ custom_proxies
72
+ else
73
+ raise(ArgumentError, <<~EOM)
74
+ Setting config.action_dispatch.trusted_proxies to a single value isn't
75
+ supported. Please set this to an enumerable instead. For
76
+ example, instead of:
77
+
78
+ config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
79
+
80
+ Wrap the value in an Array:
81
+
82
+ config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
83
+
84
+ Note that passing an enumerable will *replace* the default set of trusted proxies.
85
+ EOM
86
+ end
87
+ end
88
+
89
+ # Since the IP address may not be needed, we store the object here without
90
+ # calculating the IP to keep from slowing down the majority of requests. For
91
+ # those requests that do need to know the IP, the GetIp#calculate_ip method will
92
+ # calculate the memoized client IP address.
93
+ def call(env)
94
+ req = ActionDispatch::Request.new env
95
+ req.remote_ip = GetIp.new(req, check_ip, proxies)
96
+ @app.call(req.env)
97
+ end
98
+
99
+ # The GetIp class exists as a way to defer processing of the request data into
100
+ # an actual IP address. If the ActionDispatch::Request#remote_ip method is
101
+ # called, this class will calculate the value and then memoize it.
102
+ class GetIp
103
+ def initialize(req, check_ip, proxies)
104
+ @req = req
105
+ @check_ip = check_ip
106
+ @proxies = proxies
107
+ end
108
+
109
+ # Sort through the various IP address headers, looking for the IP most likely to
110
+ # be the address of the actual remote client making this request.
111
+ #
112
+ # REMOTE_ADDR will be correct if the request is made directly against the Ruby
113
+ # process, on e.g. Heroku. When the request is proxied by another server like
114
+ # HAProxy or NGINX, the IP address that made the original request will be put in
115
+ # an `X-Forwarded-For` header. If there are multiple proxies, that header may
116
+ # contain a list of IPs. Other proxy services set the `Client-Ip` header
117
+ # instead, so we check that too.
118
+ #
119
+ # As discussed in [this post about Rails IP
120
+ # Spoofing](https://web.archive.org/web/20170626095448/https://blog.gingerlime.c
121
+ # om/2012/rails-ip-spoofing-vulnerabilities-and-protection/), while the first IP
122
+ # in the list is likely to be the "originating" IP, it could also have been set
123
+ # by the client maliciously.
124
+ #
125
+ # In order to find the first address that is (probably) accurate, we take the
126
+ # list of IPs, remove known and trusted proxies, and then take the last address
127
+ # left, which was presumably set by one of those proxies.
128
+ def calculate_ip
129
+ # Set by the Rack web server, this is a single value.
130
+ remote_addr = ips_from(@req.remote_addr).last
131
+
132
+ # Could be a CSV list and/or repeated headers that were concatenated.
133
+ client_ips = ips_from(@req.client_ip).reverse!
134
+ forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
135
+
136
+ # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
137
+ # are both set, it means that either:
138
+ #
139
+ # 1) This request passed through two proxies with incompatible IP header
140
+ # conventions.
141
+ #
142
+ # 2) The client passed one of `Client-Ip` or `X-Forwarded-For`
143
+ # (whichever the proxy servers weren't using) themselves.
144
+ #
145
+ # Either way, there is no way for us to determine which header is the right one
146
+ # after the fact. Since we have no idea, if we are concerned about IP spoofing
147
+ # we need to give up and explode. (If you're not concerned about IP spoofing you
148
+ # can turn the `ip_spoofing_check` option off.)
149
+ should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
150
+ if should_check_ip && !forwarded_ips.include?(client_ips.last)
151
+ # We don't know which came from the proxy, and which from the user
152
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
153
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
154
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
155
+ end
156
+
157
+ # We assume these things about the IP headers:
158
+ #
159
+ # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
160
+ # - Client-Ip is propagated from the outermost proxy, or is blank
161
+ # - REMOTE_ADDR will be the IP that made the request to Rack
162
+ ips = forwarded_ips + client_ips
163
+ ips.compact!
164
+
165
+ # If every single IP option is in the trusted list, return the IP that's
166
+ # furthest away
167
+ filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
168
+ end
169
+
170
+ # Memoizes the value returned by #calculate_ip and returns it for
171
+ # ActionDispatch::Request to use.
172
+ def to_s
173
+ @ip ||= calculate_ip
174
+ end
175
+
176
+ private
177
+ def ips_from(header) # :doc:
178
+ return [] unless header
179
+ # Split the comma-separated list into an array of strings.
180
+ ips = header.strip.split(/[,\s]+/)
181
+ ips.select! do |ip|
182
+ # Only return IPs that are valid according to the IPAddr#new method.
183
+ range = IPAddr.new(ip).to_range
184
+ # We want to make sure nobody is sneaking a netmask in.
185
+ range.begin == range.end
186
+ rescue ArgumentError
187
+ nil
188
+ end
189
+ ips
190
+ end
191
+
192
+ def filter_proxies(ips) # :doc:
193
+ ips.reject do |ip|
194
+ @proxies.any? { |proxy| proxy === ip }
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "securerandom"
6
+ require "active_support/core_ext/string/access"
7
+
8
+ module ActionDispatch
9
+ # # Action Dispatch RequestId
10
+ #
11
+ # Makes a unique request id available to the `action_dispatch.request_id` env
12
+ # variable (which is then accessible through ActionDispatch::Request#request_id
13
+ # or the alias ActionDispatch::Request#uuid) and sends the same id to the client
14
+ # via the `X-Request-Id` header.
15
+ #
16
+ # The unique request id is either based on the `X-Request-Id` header in the
17
+ # request, which would typically be generated by a firewall, load balancer, or
18
+ # the web server, or, if this header is not available, a random uuid. If the
19
+ # header is accepted from the outside world, we sanitize it to a max of 255
20
+ # chars and alphanumeric and dashes only.
21
+ #
22
+ # The unique request id can be used to trace a request end-to-end and would
23
+ # typically end up being part of log files from multiple pieces of the stack.
24
+ class RequestId
25
+ def initialize(app, header:)
26
+ @app = app
27
+ @header = header
28
+ @env_header = "HTTP_#{header.upcase.tr("-", "_")}"
29
+ end
30
+
31
+ def call(env)
32
+ req = ActionDispatch::Request.new env
33
+ req.request_id = make_request_id(req.get_header(@env_header))
34
+ @app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
35
+ end
36
+
37
+ private
38
+ def make_request_id(request_id)
39
+ if request_id.presence
40
+ request_id.gsub(/[^\w\-@]/, "").first(255)
41
+ else
42
+ internal_request_id
43
+ end
44
+ end
45
+
46
+ def internal_request_id
47
+ SecureRandom.uuid
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/notifications"
6
+
7
+ module ActionDispatch
8
+ class ServerTiming
9
+ class Subscriber # :nodoc:
10
+ include Singleton
11
+ KEY = :action_dispatch_server_timing_events
12
+
13
+ def initialize
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def call(event)
18
+ if events = ActiveSupport::IsolatedExecutionState[KEY]
19
+ events << event
20
+ end
21
+ end
22
+
23
+ def collect_events
24
+ events = []
25
+ ActiveSupport::IsolatedExecutionState[KEY] = events
26
+ yield
27
+ events
28
+ ensure
29
+ ActiveSupport::IsolatedExecutionState.delete(KEY)
30
+ end
31
+
32
+ def ensure_subscribed
33
+ @mutex.synchronize do
34
+ # Subscribe to all events, except those beginning with "!" Ideally we would be
35
+ # more selective of what is being measured
36
+ @subscriber ||= ActiveSupport::Notifications.subscribe(/\A[^!]/, self)
37
+ end
38
+ end
39
+
40
+ def unsubscribe
41
+ @mutex.synchronize do
42
+ ActiveSupport::Notifications.unsubscribe @subscriber
43
+ @subscriber = nil
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.unsubscribe # :nodoc:
49
+ Subscriber.instance.unsubscribe
50
+ end
51
+
52
+ def initialize(app)
53
+ @app = app
54
+ @subscriber = Subscriber.instance
55
+ @subscriber.ensure_subscribed
56
+ end
57
+
58
+ def call(env)
59
+ response = nil
60
+ events = @subscriber.collect_events do
61
+ response = @app.call(env)
62
+ end
63
+
64
+ headers = response[1]
65
+
66
+ header_info = events.group_by(&:name).map do |event_name, events_collection|
67
+ "%s;dur=%.2f" % [event_name, events_collection.sum(&:duration)]
68
+ end
69
+
70
+ if headers[ActionDispatch::Constants::SERVER_TIMING].present?
71
+ header_info.prepend(headers[ActionDispatch::Constants::SERVER_TIMING])
72
+ end
73
+ headers[ActionDispatch::Constants::SERVER_TIMING] = header_info.join(", ")
74
+
75
+ response
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "rack/utils"
6
+ require "rack/request"
7
+ require "rack/session/abstract/id"
8
+ require "action_dispatch/middleware/cookies"
9
+ require "action_dispatch/request/session"
10
+
11
+ module ActionDispatch
12
+ module Session
13
+ class SessionRestoreError < StandardError # :nodoc:
14
+ def initialize
15
+ super("Session contains objects whose class definition isn't available.\n" \
16
+ "Remember to require the classes for all objects kept in the session.\n" \
17
+ "(Original exception: #{$!.message} [#{$!.class}])\n")
18
+ set_backtrace $!.backtrace
19
+ end
20
+ end
21
+
22
+ module Compatibility
23
+ def initialize(app, options = {})
24
+ options[:key] ||= "_session_id"
25
+ super
26
+ end
27
+
28
+ def generate_sid
29
+ sid = SecureRandom.hex(16)
30
+ sid.encode!(Encoding::UTF_8)
31
+ sid
32
+ end
33
+
34
+ private
35
+ def initialize_sid # :doc:
36
+ @default_options.delete(:sidbits)
37
+ @default_options.delete(:secure_random)
38
+ end
39
+
40
+ def make_request(env)
41
+ ActionDispatch::Request.new env
42
+ end
43
+ end
44
+
45
+ module StaleSessionCheck
46
+ def load_session(env)
47
+ stale_session_check! { super }
48
+ end
49
+
50
+ def extract_session_id(env)
51
+ stale_session_check! { super }
52
+ end
53
+
54
+ def stale_session_check!
55
+ yield
56
+ rescue ArgumentError => argument_error
57
+ if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
58
+ begin
59
+ # Note that the regexp does not allow $1 to end with a ':'.
60
+ $1.constantize
61
+ rescue LoadError, NameError
62
+ raise ActionDispatch::Session::SessionRestoreError
63
+ end
64
+ retry
65
+ else
66
+ raise
67
+ end
68
+ end
69
+ end
70
+
71
+ module SessionObject # :nodoc:
72
+ def commit_session(req, res)
73
+ req.commit_csrf_token
74
+ super(req, res)
75
+ end
76
+
77
+ def prepare_session(req)
78
+ Request::Session.create(self, req, @default_options)
79
+ end
80
+
81
+ def loaded_session?(session)
82
+ !session.is_a?(Request::Session) || session.loaded?
83
+ end
84
+ end
85
+
86
+ class AbstractStore < Rack::Session::Abstract::Persisted
87
+ include Compatibility
88
+ include StaleSessionCheck
89
+ include SessionObject
90
+
91
+ private
92
+ def set_cookie(request, response, cookie)
93
+ request.cookie_jar[key] = cookie
94
+ end
95
+ end
96
+
97
+ class AbstractSecureStore < Rack::Session::Abstract::PersistedSecure
98
+ include Compatibility
99
+ include StaleSessionCheck
100
+ include SessionObject
101
+
102
+ def generate_sid
103
+ Rack::Session::SessionId.new(super)
104
+ end
105
+
106
+ private
107
+ def set_cookie(request, response, cookie)
108
+ request.cookie_jar[key] = cookie
109
+ end
110
+ end
111
+ end
112
+ end