actionpack 6.0.0

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 (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +311 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +58 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +267 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +150 -0
  10. data/lib/abstract_controller/callbacks.rb +224 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +32 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +67 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +271 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +81 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +280 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +151 -0
  32. data/lib/action_controller/metal/default_headers.rb +17 -0
  33. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  35. data/lib/action_controller/metal/exceptions.rb +74 -0
  36. data/lib/action_controller/metal/flash.rb +61 -0
  37. data/lib/action_controller/metal/force_ssl.rb +58 -0
  38. data/lib/action_controller/metal/head.rb +60 -0
  39. data/lib/action_controller/metal/helpers.rb +122 -0
  40. data/lib/action_controller/metal/http_authentication.rb +518 -0
  41. data/lib/action_controller/metal/implicit_render.rb +63 -0
  42. data/lib/action_controller/metal/instrumentation.rb +105 -0
  43. data/lib/action_controller/metal/live.rb +314 -0
  44. data/lib/action_controller/metal/mime_responds.rb +324 -0
  45. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  46. data/lib/action_controller/metal/params_wrapper.rb +297 -0
  47. data/lib/action_controller/metal/redirecting.rb +133 -0
  48. data/lib/action_controller/metal/renderers.rb +181 -0
  49. data/lib/action_controller/metal/rendering.rb +122 -0
  50. data/lib/action_controller/metal/request_forgery_protection.rb +456 -0
  51. data/lib/action_controller/metal/rescue.rb +28 -0
  52. data/lib/action_controller/metal/streaming.rb +223 -0
  53. data/lib/action_controller/metal/strong_parameters.rb +1105 -0
  54. data/lib/action_controller/metal/testing.rb +16 -0
  55. data/lib/action_controller/metal/url_for.rb +58 -0
  56. data/lib/action_controller/railtie.rb +89 -0
  57. data/lib/action_controller/railties/helpers.rb +24 -0
  58. data/lib/action_controller/renderer.rb +130 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +626 -0
  61. data/lib/action_dispatch.rb +114 -0
  62. data/lib/action_dispatch/http/cache.rb +226 -0
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +284 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +86 -0
  66. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  67. data/lib/action_dispatch/http/headers.rb +132 -0
  68. data/lib/action_dispatch/http/mime_negotiation.rb +177 -0
  69. data/lib/action_dispatch/http/mime_type.rb +350 -0
  70. data/lib/action_dispatch/http/mime_types.rb +50 -0
  71. data/lib/action_dispatch/http/parameter_filter.rb +12 -0
  72. data/lib/action_dispatch/http/parameters.rb +136 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  74. data/lib/action_dispatch/http/request.rb +427 -0
  75. data/lib/action_dispatch/http/response.rb +534 -0
  76. data/lib/action_dispatch/http/upload.rb +92 -0
  77. data/lib/action_dispatch/http/url.rb +350 -0
  78. data/lib/action_dispatch/journey.rb +7 -0
  79. data/lib/action_dispatch/journey/formatter.rb +189 -0
  80. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  81. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  82. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  83. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  84. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  85. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  86. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  87. data/lib/action_dispatch/journey/nodes/node.rb +141 -0
  88. data/lib/action_dispatch/journey/parser.rb +199 -0
  89. data/lib/action_dispatch/journey/parser.y +50 -0
  90. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +203 -0
  92. data/lib/action_dispatch/journey/route.rb +204 -0
  93. data/lib/action_dispatch/journey/router.rb +153 -0
  94. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  95. data/lib/action_dispatch/journey/routes.rb +81 -0
  96. data/lib/action_dispatch/journey/scanner.rb +71 -0
  97. data/lib/action_dispatch/journey/visitors.rb +268 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  102. data/lib/action_dispatch/middleware/callbacks.rb +34 -0
  103. data/lib/action_dispatch/middleware/cookies.rb +663 -0
  104. data/lib/action_dispatch/middleware/debug_exceptions.rb +185 -0
  105. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  106. data/lib/action_dispatch/middleware/debug_view.rb +68 -0
  107. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -0
  108. data/lib/action_dispatch/middleware/executor.rb +21 -0
  109. data/lib/action_dispatch/middleware/flash.rb +300 -0
  110. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  111. data/lib/action_dispatch/middleware/public_exceptions.rb +61 -0
  112. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  113. data/lib/action_dispatch/middleware/remote_ip.rb +181 -0
  114. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  115. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  116. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  117. data/lib/action_dispatch/middleware/session/cookie_store.rb +113 -0
  118. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  119. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  120. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  121. data/lib/action_dispatch/middleware/stack.rb +148 -0
  122. data/lib/action_dispatch/middleware/static.rb +129 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +24 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +29 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +38 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +165 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  148. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  149. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +203 -0
  150. data/lib/action_dispatch/railtie.rb +58 -0
  151. data/lib/action_dispatch/request/session.rb +242 -0
  152. data/lib/action_dispatch/request/utils.rb +78 -0
  153. data/lib/action_dispatch/routing.rb +261 -0
  154. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  155. data/lib/action_dispatch/routing/inspector.rb +274 -0
  156. data/lib/action_dispatch/routing/mapper.rb +2289 -0
  157. data/lib/action_dispatch/routing/polymorphic_routes.rb +351 -0
  158. data/lib/action_dispatch/routing/redirection.rb +201 -0
  159. data/lib/action_dispatch/routing/route_set.rb +887 -0
  160. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  161. data/lib/action_dispatch/routing/url_for.rb +237 -0
  162. data/lib/action_dispatch/system_test_case.rb +168 -0
  163. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  164. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  165. data/lib/action_dispatch/system_testing/server.rb +31 -0
  166. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  167. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +33 -0
  168. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  169. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  170. data/lib/action_dispatch/testing/assertions.rb +24 -0
  171. data/lib/action_dispatch/testing/assertions/response.rb +106 -0
  172. data/lib/action_dispatch/testing/assertions/routing.rb +234 -0
  173. data/lib/action_dispatch/testing/integration.rb +659 -0
  174. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  175. data/lib/action_dispatch/testing/test_process.rb +50 -0
  176. data/lib/action_dispatch/testing/test_request.rb +71 -0
  177. data/lib/action_dispatch/testing/test_response.rb +25 -0
  178. data/lib/action_pack.rb +26 -0
  179. data/lib/action_pack/gem_version.rb +17 -0
  180. data/lib/action_pack/version.rb +10 -0
  181. metadata +329 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/middleware/session/abstract_store"
4
+ begin
5
+ require "rack/session/dalli"
6
+ rescue LoadError => e
7
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
8
+ raise e
9
+ end
10
+
11
+ module ActionDispatch
12
+ module Session
13
+ # A session store that uses MemCache to implement storage.
14
+ #
15
+ # ==== Options
16
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
17
+ class MemCacheStore < Rack::Session::Dalli
18
+ include Compatibility
19
+ include StaleSessionCheck
20
+ include SessionObject
21
+
22
+ def initialize(app, options = {})
23
+ options[:expire_after] ||= options[:expires]
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/request"
4
+ require "action_dispatch/middleware/exception_wrapper"
5
+
6
+ module ActionDispatch
7
+ # This middleware rescues any exception returned by the application
8
+ # and calls an exceptions app that will wrap it in a format for the end user.
9
+ #
10
+ # The exceptions app should be passed as parameter on initialization
11
+ # of ShowExceptions. Every time there is an exception, ShowExceptions will
12
+ # store the exception in env["action_dispatch.exception"], rewrite the
13
+ # PATH_INFO to the exception status code and call the Rack app.
14
+ #
15
+ # If the application returns a "X-Cascade" pass response, this middleware
16
+ # will send an empty response as result with the correct status code.
17
+ # If any exception happens inside the exceptions app, this middleware
18
+ # catches the exceptions and returns a FAILSAFE_RESPONSE.
19
+ class ShowExceptions
20
+ FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
21
+ ["500 Internal Server Error\n" \
22
+ "If you are the administrator of this website, then please read this web " \
23
+ "application's log file and/or the web server's log file to find out what " \
24
+ "went wrong."]]
25
+
26
+ def initialize(app, exceptions_app)
27
+ @app = app
28
+ @exceptions_app = exceptions_app
29
+ end
30
+
31
+ def call(env)
32
+ request = ActionDispatch::Request.new env
33
+ @app.call(env)
34
+ rescue Exception => exception
35
+ if request.show_exceptions?
36
+ render_exception(request, exception)
37
+ else
38
+ raise exception
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def render_exception(request, exception)
45
+ backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
46
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
47
+ status = wrapper.status_code
48
+ request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
49
+ request.set_header "action_dispatch.original_path", request.path_info
50
+ request.path_info = "/#{status}"
51
+ response = @exceptions_app.call(request.env)
52
+ response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
53
+ rescue Exception => failsafe_error
54
+ $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
55
+ FAILSAFE_RESPONSE
56
+ end
57
+
58
+ def pass_response(status)
59
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
5
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
6
+ # requests:
7
+ #
8
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
9
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
10
+ # to modify the destination URL
11
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
12
+ # <tt>redirect: false</tt> to disable this feature.
13
+ #
14
+ # Requests can opt-out of redirection with +exclude+:
15
+ #
16
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
17
+ #
18
+ # Cookies will not be flagged as secure for excluded requests.
19
+ #
20
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
21
+ # must not be sent along with +http://+ requests. Enabled by default. Set
22
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
23
+ #
24
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
25
+ # this site as TLS-only and automatically redirect non-TLS requests.
26
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
27
+ #
28
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
29
+ #
30
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
31
+ # required to qualify for browser preload lists is 1 year. Defaults to
32
+ # 1 year (recommended).
33
+ #
34
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
35
+ # to all subdomains. This protects your cookies from interception by a
36
+ # vulnerable site on a subdomain. Defaults to +true+.
37
+ #
38
+ # * +preload+: Advertise that this site may be included in browsers'
39
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
40
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
41
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
42
+ # Go to https://hstspreload.org to submit your site for inclusion.
43
+ # Defaults to +false+.
44
+ #
45
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
46
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
47
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
48
+ # <tt>hsts: { expires: 0 }</tt>.
49
+ class SSL
50
+ # :stopdoc:
51
+
52
+ # Default to 1 year, the minimum for browser preload lists.
53
+ HSTS_EXPIRES_IN = 31536000
54
+
55
+ def self.default_hsts_options
56
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
57
+ end
58
+
59
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
60
+ @app = app
61
+
62
+ @redirect = redirect
63
+
64
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
65
+ @secure_cookies = secure_cookies
66
+
67
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
68
+ end
69
+
70
+ def call(env)
71
+ request = Request.new env
72
+
73
+ if request.ssl?
74
+ @app.call(env).tap do |status, headers, body|
75
+ set_hsts_header! headers
76
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
77
+ end
78
+ else
79
+ return redirect_to_https request unless @exclude.call(request)
80
+ @app.call(env)
81
+ end
82
+ end
83
+
84
+ private
85
+ def set_hsts_header!(headers)
86
+ headers["Strict-Transport-Security"] ||= @hsts_header
87
+ end
88
+
89
+ def normalize_hsts_options(options)
90
+ case options
91
+ # Explicitly disabling HSTS clears the existing setting from browsers
92
+ # by setting expiry to 0.
93
+ when false
94
+ self.class.default_hsts_options.merge(expires: 0)
95
+ # Default to enabled, with default options.
96
+ when nil, true
97
+ self.class.default_hsts_options
98
+ else
99
+ self.class.default_hsts_options.merge(options)
100
+ end
101
+ end
102
+
103
+ # https://tools.ietf.org/html/rfc6797#section-6.1
104
+ def build_hsts_header(hsts)
105
+ value = +"max-age=#{hsts[:expires].to_i}"
106
+ value << "; includeSubDomains" if hsts[:subdomains]
107
+ value << "; preload" if hsts[:preload]
108
+ value
109
+ end
110
+
111
+ def flag_cookies_as_secure!(headers)
112
+ if cookies = headers["Set-Cookie"]
113
+ cookies = cookies.split("\n")
114
+
115
+ headers["Set-Cookie"] = cookies.map { |cookie|
116
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
117
+ "#{cookie}; secure"
118
+ else
119
+ cookie
120
+ end
121
+ }.join("\n")
122
+ end
123
+ end
124
+
125
+ def redirect_to_https(request)
126
+ [ @redirect.fetch(:status, redirection_status(request)),
127
+ { "Content-Type" => "text/html",
128
+ "Location" => https_location_for(request) },
129
+ @redirect.fetch(:body, []) ]
130
+ end
131
+
132
+ def redirection_status(request)
133
+ if request.get? || request.head?
134
+ 301 # Issue a permanent redirect via a GET request.
135
+ else
136
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
137
+ end
138
+ end
139
+
140
+ def https_location_for(request)
141
+ host = @redirect[:host] || request.host
142
+ port = @redirect[:port] || request.port
143
+
144
+ location = +"https://#{host}"
145
+ location << ":#{port}" if port != 80 && port != 443
146
+ location << request.fullpath
147
+ location
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/inflector/methods"
4
+ require "active_support/dependencies"
5
+
6
+ module ActionDispatch
7
+ class MiddlewareStack
8
+ class Middleware
9
+ attr_reader :args, :block, :klass
10
+
11
+ def initialize(klass, args, block)
12
+ @klass = klass
13
+ @args = args
14
+ @block = block
15
+ end
16
+
17
+ def name; klass.name; end
18
+
19
+ def ==(middleware)
20
+ case middleware
21
+ when Middleware
22
+ klass == middleware.klass
23
+ when Class
24
+ klass == middleware
25
+ end
26
+ end
27
+
28
+ def inspect
29
+ if klass.is_a?(Class)
30
+ klass.to_s
31
+ else
32
+ klass.class.to_s
33
+ end
34
+ end
35
+
36
+ def build(app)
37
+ klass.new(app, *args, &block)
38
+ end
39
+
40
+ def build_instrumented(app)
41
+ InstrumentationProxy.new(build(app), inspect)
42
+ end
43
+ end
44
+
45
+ # This class is used to instrument the execution of a single middleware.
46
+ # It proxies the `call` method transparently and instruments the method
47
+ # call.
48
+ class InstrumentationProxy
49
+ EVENT_NAME = "process_middleware.action_dispatch"
50
+
51
+ def initialize(middleware, class_name)
52
+ @middleware = middleware
53
+
54
+ @payload = {
55
+ middleware: class_name,
56
+ }
57
+ end
58
+
59
+ def call(env)
60
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
61
+ @middleware.call(env)
62
+ end
63
+ end
64
+ end
65
+
66
+ include Enumerable
67
+
68
+ attr_accessor :middlewares
69
+
70
+ def initialize(*args)
71
+ @middlewares = []
72
+ yield(self) if block_given?
73
+ end
74
+
75
+ def each
76
+ @middlewares.each { |x| yield x }
77
+ end
78
+
79
+ def size
80
+ middlewares.size
81
+ end
82
+
83
+ def last
84
+ middlewares.last
85
+ end
86
+
87
+ def [](i)
88
+ middlewares[i]
89
+ end
90
+
91
+ def unshift(klass, *args, &block)
92
+ middlewares.unshift(build_middleware(klass, args, block))
93
+ end
94
+
95
+ def initialize_copy(other)
96
+ self.middlewares = other.middlewares.dup
97
+ end
98
+
99
+ def insert(index, klass, *args, &block)
100
+ index = assert_index(index, :before)
101
+ middlewares.insert(index, build_middleware(klass, args, block))
102
+ end
103
+
104
+ alias_method :insert_before, :insert
105
+
106
+ def insert_after(index, *args, &block)
107
+ index = assert_index(index, :after)
108
+ insert(index + 1, *args, &block)
109
+ end
110
+
111
+ def swap(target, *args, &block)
112
+ index = assert_index(target, :before)
113
+ insert(index, *args, &block)
114
+ middlewares.delete_at(index + 1)
115
+ end
116
+
117
+ def delete(target)
118
+ middlewares.delete_if { |m| m.klass == target }
119
+ end
120
+
121
+ def use(klass, *args, &block)
122
+ middlewares.push(build_middleware(klass, args, block))
123
+ end
124
+
125
+ def build(app = nil, &block)
126
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
127
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
128
+ if instrumenting
129
+ e.build_instrumented(a)
130
+ else
131
+ e.build(a)
132
+ end
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def assert_index(index, where)
139
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
140
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
141
+ i
142
+ end
143
+
144
+ def build_middleware(klass, args, block)
145
+ Middleware.new(klass, args, block)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "active_support/core_ext/uri"
5
+
6
+ module ActionDispatch
7
+ # This middleware returns a file's contents from disk in the body response.
8
+ # When initialized, it can accept optional HTTP headers, which will be set
9
+ # when a response containing a file's contents is delivered.
10
+ #
11
+ # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
12
+ # where the base path is in the +root+ directory. For example, if the +root+
13
+ # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
14
+ # +assets/application.js+ will return a response with the contents of a file
15
+ # located at +public/assets/application.js+ if the file exists. If the file
16
+ # does not exist, a 404 "File not Found" response will be returned.
17
+ class FileHandler
18
+ def initialize(root, index: "index", headers: {})
19
+ @root = root.chomp("/").b
20
+ @file_server = ::Rack::File.new(@root, headers)
21
+ @index = index
22
+ end
23
+
24
+ # Takes a path to a file. If the file is found, has valid encoding, and has
25
+ # correct read permissions, the return value is a URI-escaped string
26
+ # representing the filename. Otherwise, false is returned.
27
+ #
28
+ # Used by the +Static+ class to check the existence of a valid file
29
+ # in the server's +public/+ directory (see Static#call).
30
+ def match?(path)
31
+ path = ::Rack::Utils.unescape_path path
32
+ return false unless ::Rack::Utils.valid_path? path
33
+ path = ::Rack::Utils.clean_path_info path
34
+
35
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
36
+
37
+ if match = paths.detect { |p|
38
+ path = File.join(@root, p.b)
39
+ begin
40
+ File.file?(path) && File.readable?(path)
41
+ rescue SystemCallError
42
+ false
43
+ end
44
+ }
45
+ return ::Rack::Utils.escape_path(match).b
46
+ end
47
+ end
48
+
49
+ def call(env)
50
+ serve(Rack::Request.new(env))
51
+ end
52
+
53
+ def serve(request)
54
+ path = request.path_info
55
+ gzip_path = gzip_file_path(path)
56
+
57
+ if gzip_path && gzip_encoding_accepted?(request)
58
+ request.path_info = gzip_path
59
+ status, headers, body = @file_server.call(request.env)
60
+ if status == 304
61
+ return [status, headers, body]
62
+ end
63
+ headers["Content-Encoding"] = "gzip"
64
+ headers["Content-Type"] = content_type(path)
65
+ else
66
+ status, headers, body = @file_server.call(request.env)
67
+ end
68
+
69
+ headers["Vary"] = "Accept-Encoding" if gzip_path
70
+
71
+ [status, headers, body]
72
+ ensure
73
+ request.path_info = path
74
+ end
75
+
76
+ private
77
+ def ext
78
+ ::ActionController::Base.default_static_extension
79
+ end
80
+
81
+ def content_type(path)
82
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
83
+ end
84
+
85
+ def gzip_encoding_accepted?(request)
86
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
87
+ end
88
+
89
+ def gzip_file_path(path)
90
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
91
+ gzip_path = "#{path}.gz"
92
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
93
+ gzip_path
94
+ else
95
+ false
96
+ end
97
+ end
98
+ end
99
+
100
+ # This middleware will attempt to return the contents of a file's body from
101
+ # disk in the response. If a file is not found on disk, the request will be
102
+ # delegated to the application stack. This middleware is commonly initialized
103
+ # to serve assets from a server's +public/+ directory.
104
+ #
105
+ # This middleware verifies the path to ensure that only files
106
+ # living in the root directory can be rendered. A request cannot
107
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
108
+ # requests will result in a file being returned.
109
+ class Static
110
+ def initialize(app, path, index: "index", headers: {})
111
+ @app = app
112
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
113
+ end
114
+
115
+ def call(env)
116
+ req = Rack::Request.new env
117
+
118
+ if req.get? || req.head?
119
+ path = req.path_info.chomp("/")
120
+ if match = @file_handler.match?(path)
121
+ req.path_info = match
122
+ return @file_handler.serve(req)
123
+ end
124
+ end
125
+
126
+ @app.call(req.env)
127
+ end
128
+ end
129
+ end