actionpack 4.2.8 → 5.2.4.2

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 (166) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +285 -444
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller.rb +12 -5
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +45 -49
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  10. data/lib/abstract_controller/callbacks.rb +47 -31
  11. data/lib/abstract_controller/collector.rb +8 -11
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +25 -25
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  16. data/lib/abstract_controller/rendering.rb +42 -41
  17. data/lib/abstract_controller/translation.rb +10 -7
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +29 -21
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +27 -19
  23. data/lib/action_controller/caching.rb +14 -57
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +10 -15
  26. data/lib/action_controller/metal.rb +98 -83
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +118 -44
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +3 -3
  31. data/lib/action_controller/metal/data_streaming.rb +27 -46
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  34. data/lib/action_controller/metal/exceptions.rb +8 -14
  35. data/lib/action_controller/metal/flash.rb +4 -3
  36. data/lib/action_controller/metal/force_ssl.rb +23 -21
  37. data/lib/action_controller/metal/head.rb +21 -19
  38. data/lib/action_controller/metal/helpers.rb +24 -14
  39. data/lib/action_controller/metal/http_authentication.rb +64 -57
  40. data/lib/action_controller/metal/implicit_render.rb +62 -8
  41. data/lib/action_controller/metal/instrumentation.rb +19 -21
  42. data/lib/action_controller/metal/live.rb +90 -106
  43. data/lib/action_controller/metal/mime_responds.rb +33 -46
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  46. data/lib/action_controller/metal/redirecting.rb +49 -28
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +72 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  50. data/lib/action_controller/metal/rescue.rb +9 -16
  51. data/lib/action_controller/metal/streaming.rb +12 -10
  52. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +2 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +280 -411
  60. data/lib/action_dispatch.rb +27 -19
  61. data/lib/action_dispatch/http/cache.rb +93 -47
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  64. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  65. data/lib/action_dispatch/http/headers.rb +55 -22
  66. data/lib/action_dispatch/http/mime_negotiation.rb +60 -41
  67. data/lib/action_dispatch/http/mime_type.rb +134 -121
  68. data/lib/action_dispatch/http/mime_types.rb +20 -6
  69. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  70. data/lib/action_dispatch/http/parameters.rb +98 -39
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +200 -118
  73. data/lib/action_dispatch/http/response.rb +225 -110
  74. data/lib/action_dispatch/http/upload.rb +12 -6
  75. data/lib/action_dispatch/http/url.rb +110 -28
  76. data/lib/action_dispatch/journey.rb +7 -5
  77. data/lib/action_dispatch/journey/formatter.rb +55 -32
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  85. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  90. data/lib/action_dispatch/journey/route.rb +106 -28
  91. data/lib/action_dispatch/journey/router.rb +35 -23
  92. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  93. data/lib/action_dispatch/journey/routes.rb +18 -16
  94. data/lib/action_dispatch/journey/scanner.rb +18 -15
  95. data/lib/action_dispatch/journey/visitors.rb +99 -52
  96. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  97. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  101. data/lib/action_dispatch/middleware/executor.rb +21 -0
  102. data/lib/action_dispatch/middleware/flash.rb +78 -54
  103. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  104. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  105. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  106. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  107. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  108. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  109. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  110. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  111. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  112. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  113. data/lib/action_dispatch/middleware/stack.rb +31 -44
  114. data/lib/action_dispatch/middleware/static.rb +57 -50
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  116. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  124. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  125. data/lib/action_dispatch/railtie.rb +19 -11
  126. data/lib/action_dispatch/request/session.rb +106 -59
  127. data/lib/action_dispatch/request/utils.rb +67 -24
  128. data/lib/action_dispatch/routing.rb +17 -18
  129. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  130. data/lib/action_dispatch/routing/inspector.rb +58 -67
  131. data/lib/action_dispatch/routing/mapper.rb +734 -447
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  133. data/lib/action_dispatch/routing/redirection.rb +36 -26
  134. data/lib/action_dispatch/routing/route_set.rb +321 -291
  135. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  136. data/lib/action_dispatch/routing/url_for.rb +65 -25
  137. data/lib/action_dispatch/system_test_case.rb +147 -0
  138. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  139. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  140. data/lib/action_dispatch/system_testing/server.rb +31 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  143. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  145. data/lib/action_dispatch/testing/assertions.rb +6 -4
  146. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  147. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  148. data/lib/action_dispatch/testing/integration.rb +347 -209
  149. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  150. data/lib/action_dispatch/testing/test_process.rb +28 -22
  151. data/lib/action_dispatch/testing/test_request.rb +27 -34
  152. data/lib/action_dispatch/testing/test_response.rb +35 -7
  153. data/lib/action_pack.rb +4 -2
  154. data/lib/action_pack/gem_version.rb +5 -3
  155. data/lib/action_pack/version.rb +3 -1
  156. metadata +56 -39
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,72 +1,150 @@
1
+ # frozen_string_literal: true
2
+
1
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>.
2
49
  class SSL
3
- YEAR = 31536000
50
+ # :stopdoc:
51
+
52
+ # Default to 1 year, the minimum for browser preload lists.
53
+ HSTS_EXPIRES_IN = 31536000
4
54
 
5
55
  def self.default_hsts_options
6
- { :expires => YEAR, :subdomains => false }
56
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
7
57
  end
8
58
 
9
- def initialize(app, options = {})
59
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
10
60
  @app = app
11
61
 
12
- @hsts = options.fetch(:hsts, {})
13
- @hsts = {} if @hsts == true
14
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
62
+ @redirect = redirect
63
+
64
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
65
+ @secure_cookies = secure_cookies
15
66
 
16
- @host = options[:host]
17
- @port = options[:port]
67
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
18
68
  end
19
69
 
20
70
  def call(env)
21
- request = Request.new(env)
71
+ request = Request.new env
22
72
 
23
73
  if request.ssl?
24
- status, headers, body = @app.call(env)
25
- headers.reverse_merge!(hsts_headers)
26
- flag_cookies_as_secure!(headers)
27
- [status, headers, body]
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
28
78
  else
29
- redirect_to_https(request)
79
+ return redirect_to_https request unless @exclude.call(request)
80
+ @app.call(env)
30
81
  end
31
82
  end
32
83
 
33
84
  private
34
- def redirect_to_https(request)
35
- host = @host || request.host
36
- port = @port || request.port
37
-
38
- location = "https://#{host}"
39
- location << ":#{port}" if port != 80
40
- location << request.fullpath
41
-
42
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
43
-
44
- [301, headers, []]
85
+ def set_hsts_header!(headers)
86
+ headers["Strict-Transport-Security".freeze] ||= @hsts_header
45
87
  end
46
88
 
47
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
48
- def hsts_headers
49
- if @hsts
50
- value = "max-age=#{@hsts[:expires].to_i}"
51
- value += "; includeSubDomains" if @hsts[:subdomains]
52
- { 'Strict-Transport-Security' => value }
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
53
98
  else
54
- {}
99
+ self.class.default_hsts_options.merge(options)
55
100
  end
56
101
  end
57
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}".dup
106
+ value << "; includeSubDomains" if hsts[:subdomains]
107
+ value << "; preload" if hsts[:preload]
108
+ value
109
+ end
110
+
58
111
  def flag_cookies_as_secure!(headers)
59
- if cookies = headers['Set-Cookie']
60
- cookies = cookies.split("\n")
112
+ if cookies = headers["Set-Cookie".freeze]
113
+ cookies = cookies.split("\n".freeze)
61
114
 
62
- headers['Set-Cookie'] = cookies.map { |cookie|
115
+ headers["Set-Cookie".freeze] = cookies.map { |cookie|
63
116
  if cookie !~ /;\s*secure\s*(;|$)/i
64
117
  "#{cookie}; secure"
65
118
  else
66
119
  cookie
67
120
  end
68
- }.join("\n")
121
+ }.join("\n".freeze)
69
122
  end
70
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}".dup
145
+ location << ":#{port}" if port != 80 && port != 443
146
+ location << request.fullpath
147
+ location
148
+ end
71
149
  end
72
150
  end
@@ -1,28 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/inflector/methods"
2
4
  require "active_support/dependencies"
3
5
 
4
6
  module ActionDispatch
5
7
  class MiddlewareStack
6
8
  class Middleware
7
- attr_reader :args, :block, :name, :classcache
8
-
9
- def initialize(klass_or_name, *args, &block)
10
- @klass = nil
11
-
12
- if klass_or_name.respond_to?(:name)
13
- @klass = klass_or_name
14
- @name = @klass.name
15
- else
16
- @name = klass_or_name.to_s
17
- end
9
+ attr_reader :args, :block, :klass
18
10
 
19
- @classcache = ActiveSupport::Dependencies::Reference
20
- @args, @block = args, block
11
+ def initialize(klass, args, block)
12
+ @klass = klass
13
+ @args = args
14
+ @block = block
21
15
  end
22
16
 
23
- def klass
24
- @klass || classcache[@name]
25
- end
17
+ def name; klass.name; end
26
18
 
27
19
  def ==(middleware)
28
20
  case middleware
@@ -30,24 +22,20 @@ module ActionDispatch
30
22
  klass == middleware.klass
31
23
  when Class
32
24
  klass == middleware
33
- else
34
- normalize(@name) == normalize(middleware)
35
25
  end
36
26
  end
37
27
 
38
28
  def inspect
39
- klass.to_s
29
+ if klass.is_a?(Class)
30
+ klass.to_s
31
+ else
32
+ klass.class.to_s
33
+ end
40
34
  end
41
35
 
42
36
  def build(app)
43
37
  klass.new(app, *args, &block)
44
38
  end
45
-
46
- private
47
-
48
- def normalize(object)
49
- object.to_s.strip.sub(/^::/, '')
50
- end
51
39
  end
52
40
 
53
41
  include Enumerable
@@ -75,19 +63,17 @@ module ActionDispatch
75
63
  middlewares[i]
76
64
  end
77
65
 
78
- def unshift(*args, &block)
79
- middleware = self.class::Middleware.new(*args, &block)
80
- middlewares.unshift(middleware)
66
+ def unshift(klass, *args, &block)
67
+ middlewares.unshift(build_middleware(klass, args, block))
81
68
  end
82
69
 
83
70
  def initialize_copy(other)
84
71
  self.middlewares = other.middlewares.dup
85
72
  end
86
73
 
87
- def insert(index, *args, &block)
74
+ def insert(index, klass, *args, &block)
88
75
  index = assert_index(index, :before)
89
- middleware = self.class::Middleware.new(*args, &block)
90
- middlewares.insert(index, middleware)
76
+ middlewares.insert(index, build_middleware(klass, args, block))
91
77
  end
92
78
 
93
79
  alias_method :insert_before, :insert
@@ -104,26 +90,27 @@ module ActionDispatch
104
90
  end
105
91
 
106
92
  def delete(target)
107
- middlewares.delete target
93
+ middlewares.delete_if { |m| m.klass == target }
108
94
  end
109
95
 
110
- def use(*args, &block)
111
- middleware = self.class::Middleware.new(*args, &block)
112
- middlewares.push(middleware)
96
+ def use(klass, *args, &block)
97
+ middlewares.push(build_middleware(klass, args, block))
113
98
  end
114
99
 
115
100
  def build(app = nil, &block)
116
- app ||= block
117
- raise "MiddlewareStack#build requires an app" unless app
118
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
101
+ middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
119
102
  end
120
103
 
121
- protected
104
+ private
122
105
 
123
- def assert_index(index, where)
124
- i = index.is_a?(Integer) ? index : middlewares.index(index)
125
- raise "No such middleware to insert #{where}: #{index.inspect}" unless i
126
- i
127
- end
106
+ def assert_index(index, where)
107
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
108
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
109
+ i
110
+ end
111
+
112
+ def build_middleware(klass, args, block)
113
+ Middleware.new(klass, args, block)
114
+ end
128
115
  end
129
116
  end
@@ -1,35 +1,41 @@
1
- require 'rack/utils'
2
- require 'active_support/core_ext/uri'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "active_support/core_ext/uri"
3
5
 
4
6
  module ActionDispatch
5
7
  # This middleware returns a file's contents from disk in the body response.
6
- # When initialized it can accept an optional 'Cache-Control' header which
7
- # will be set when a response containing a file's contents is delivered.
8
+ # When initialized, it can accept optional HTTP headers, which will be set
9
+ # when a response containing a file's contents is delivered.
8
10
  #
9
- # This middleware will render the file specified in `env["PATH_INFO"]`
10
- # where the base path is in the +root+ directory. For example if the +root+
11
- # is set to `public/` then a request with `env["PATH_INFO"]` of
12
- # `assets/application.js` will return a response with contents of a file
13
- # located at `public/assets/application.js` if the file exists. If the file
14
- # does not exist a 404 "File not Found" response will be returned.
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.
15
17
  class FileHandler
16
- def initialize(root, cache_control)
17
- @root = root.chomp('/')
18
- @compiled_root = /^#{Regexp.escape(root)}/
19
- headers = cache_control && { 'Cache-Control' => cache_control }
20
- @file_server = ::Rack::File.new(@root, headers)
18
+ def initialize(root, index: "index", headers: {})
19
+ @root = root.chomp("/").b
20
+ @file_server = ::Rack::File.new(@root, headers)
21
+ @index = index
21
22
  end
22
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).
23
30
  def match?(path)
24
- path = URI.parser.unescape(path)
25
- return false unless valid_path?(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
26
34
 
27
- paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v|
28
- Rack::Utils.clean_path_info v
29
- }
35
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
30
36
 
31
37
  if match = paths.detect { |p|
32
- path = File.join(@root, p.force_encoding('UTF-8'))
38
+ path = File.join(@root, p.b)
33
39
  begin
34
40
  File.file?(path) && File.readable?(path)
35
41
  rescue SystemCallError
@@ -37,31 +43,35 @@ module ActionDispatch
37
43
  end
38
44
 
39
45
  }
40
- return ::Rack::Utils.escape(match)
46
+ return ::Rack::Utils.escape_path(match).b
41
47
  end
42
48
  end
43
49
 
44
50
  def call(env)
45
- path = env['PATH_INFO']
51
+ serve(Rack::Request.new(env))
52
+ end
53
+
54
+ def serve(request)
55
+ path = request.path_info
46
56
  gzip_path = gzip_file_path(path)
47
57
 
48
- if gzip_path && gzip_encoding_accepted?(env)
49
- env['PATH_INFO'] = gzip_path
50
- status, headers, body = @file_server.call(env)
58
+ if gzip_path && gzip_encoding_accepted?(request)
59
+ request.path_info = gzip_path
60
+ status, headers, body = @file_server.call(request.env)
51
61
  if status == 304
52
62
  return [status, headers, body]
53
63
  end
54
- headers['Content-Encoding'] = 'gzip'
55
- headers['Content-Type'] = content_type(path)
64
+ headers["Content-Encoding"] = "gzip"
65
+ headers["Content-Type"] = content_type(path)
56
66
  else
57
- status, headers, body = @file_server.call(env)
67
+ status, headers, body = @file_server.call(request.env)
58
68
  end
59
69
 
60
- headers['Vary'] = 'Accept-Encoding' if gzip_path
70
+ headers["Vary"] = "Accept-Encoding" if gzip_path
61
71
 
62
72
  return [status, headers, body]
63
73
  ensure
64
- env['PATH_INFO'] = path
74
+ request.path_info = path
65
75
  end
66
76
 
67
77
  private
@@ -70,54 +80,51 @@ module ActionDispatch
70
80
  end
71
81
 
72
82
  def content_type(path)
73
- ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
83
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
74
84
  end
75
85
 
76
- def gzip_encoding_accepted?(env)
77
- env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
86
+ def gzip_encoding_accepted?(request)
87
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
78
88
  end
79
89
 
80
90
  def gzip_file_path(path)
81
91
  can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
82
92
  gzip_path = "#{path}.gz"
83
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
84
- gzip_path
93
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path).b))
94
+ gzip_path.b
85
95
  else
86
96
  false
87
97
  end
88
98
  end
89
-
90
- def valid_path?(path)
91
- path.valid_encoding? && !path.include?("\0")
92
- end
93
99
  end
94
100
 
95
101
  # This middleware will attempt to return the contents of a file's body from
96
- # disk in the response. If a file is not found on disk, the request will be
102
+ # disk in the response. If a file is not found on disk, the request will be
97
103
  # delegated to the application stack. This middleware is commonly initialized
98
- # to serve assets from a server's `public/` directory.
104
+ # to serve assets from a server's +public/+ directory.
99
105
  #
100
106
  # This middleware verifies the path to ensure that only files
101
107
  # living in the root directory can be rendered. A request cannot
102
108
  # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
103
109
  # requests will result in a file being returned.
104
110
  class Static
105
- def initialize(app, path, cache_control=nil)
111
+ def initialize(app, path, index: "index", headers: {})
106
112
  @app = app
107
- @file_handler = FileHandler.new(path, cache_control)
113
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
108
114
  end
109
115
 
110
116
  def call(env)
111
- case env['REQUEST_METHOD']
112
- when 'GET', 'HEAD'
113
- path = env['PATH_INFO'].chomp('/')
117
+ req = Rack::Request.new env
118
+
119
+ if req.get? || req.head?
120
+ path = req.path_info.chomp("/".freeze)
114
121
  if match = @file_handler.match?(path)
115
- env["PATH_INFO"] = match
116
- return @file_handler.call(env)
122
+ req.path_info = match
123
+ return @file_handler.serve(req)
117
124
  end
118
125
  end
119
126
 
120
- @app.call(env)
127
+ @app.call(req.env)
121
128
  end
122
129
  end
123
130
  end