omg-actionpack 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
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