actionpack 4.1.7 → 4.2.1

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +311 -527
  3. data/README.rdoc +7 -2
  4. data/lib/abstract_controller/base.rb +16 -6
  5. data/lib/abstract_controller/callbacks.rb +28 -51
  6. data/lib/abstract_controller/helpers.rb +11 -4
  7. data/lib/abstract_controller/railties/routes_helpers.rb +3 -3
  8. data/lib/abstract_controller/url_for.rb +1 -1
  9. data/lib/action_controller/base.rb +2 -1
  10. data/lib/action_controller/caching/fragments.rb +7 -1
  11. data/lib/action_controller/caching.rb +1 -1
  12. data/lib/action_controller/log_subscriber.rb +26 -26
  13. data/lib/action_controller/metal/conditional_get.rb +37 -12
  14. data/lib/action_controller/metal/etag_with_template_digest.rb +50 -0
  15. data/lib/action_controller/metal/exceptions.rb +1 -1
  16. data/lib/action_controller/metal/force_ssl.rb +1 -1
  17. data/lib/action_controller/metal/head.rb +7 -3
  18. data/lib/action_controller/metal/http_authentication.rb +14 -9
  19. data/lib/action_controller/metal/instrumentation.rb +8 -5
  20. data/lib/action_controller/metal/live.rb +57 -6
  21. data/lib/action_controller/metal/mime_responds.rb +23 -246
  22. data/lib/action_controller/metal/params_wrapper.rb +2 -2
  23. data/lib/action_controller/metal/rack_delegation.rb +1 -1
  24. data/lib/action_controller/metal/redirecting.rb +14 -8
  25. data/lib/action_controller/metal/renderers.rb +30 -10
  26. data/lib/action_controller/metal/rendering.rb +2 -6
  27. data/lib/action_controller/metal/request_forgery_protection.rb +78 -7
  28. data/lib/action_controller/metal/streaming.rb +1 -1
  29. data/lib/action_controller/metal/strong_parameters.rb +125 -12
  30. data/lib/action_controller/metal/url_for.rb +11 -12
  31. data/lib/action_controller/metal.rb +12 -11
  32. data/lib/action_controller/model_naming.rb +1 -1
  33. data/lib/action_controller/railtie.rb +4 -0
  34. data/lib/action_controller/test_case.rb +112 -75
  35. data/lib/action_controller.rb +1 -1
  36. data/lib/action_dispatch/http/cache.rb +5 -4
  37. data/lib/action_dispatch/http/filter_parameters.rb +2 -2
  38. data/lib/action_dispatch/http/headers.rb +43 -9
  39. data/lib/action_dispatch/http/mime_negotiation.rb +10 -3
  40. data/lib/action_dispatch/http/mime_type.rb +2 -2
  41. data/lib/action_dispatch/http/parameter_filter.rb +1 -1
  42. data/lib/action_dispatch/http/parameters.rb +11 -26
  43. data/lib/action_dispatch/http/request.rb +37 -11
  44. data/lib/action_dispatch/http/response.rb +70 -18
  45. data/lib/action_dispatch/http/upload.rb +3 -8
  46. data/lib/action_dispatch/http/url.rb +88 -69
  47. data/lib/action_dispatch/journey/formatter.rb +33 -17
  48. data/lib/action_dispatch/journey/gtg/builder.rb +3 -3
  49. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -7
  50. data/lib/action_dispatch/journey/gtg/transition_table.rb +20 -28
  51. data/lib/action_dispatch/journey/nfa/dot.rb +2 -2
  52. data/lib/action_dispatch/journey/nfa/simulator.rb +1 -1
  53. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -5
  54. data/lib/action_dispatch/journey/nodes/node.rb +4 -0
  55. data/lib/action_dispatch/journey/parser.rb +52 -60
  56. data/lib/action_dispatch/journey/parser.y +11 -10
  57. data/lib/action_dispatch/journey/path/pattern.rb +16 -19
  58. data/lib/action_dispatch/journey/route.rb +3 -18
  59. data/lib/action_dispatch/journey/router/strexp.rb +9 -6
  60. data/lib/action_dispatch/journey/router.rb +53 -77
  61. data/lib/action_dispatch/journey/scanner.rb +5 -5
  62. data/lib/action_dispatch/journey/visitors.rb +81 -92
  63. data/lib/action_dispatch/journey/visualizer/fsm.css +0 -4
  64. data/lib/action_dispatch/journey/visualizer/index.html.erb +2 -2
  65. data/lib/action_dispatch/middleware/callbacks.rb +1 -1
  66. data/lib/action_dispatch/middleware/cookies.rb +29 -29
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +15 -4
  68. data/lib/action_dispatch/middleware/exception_wrapper.rb +50 -18
  69. data/lib/action_dispatch/middleware/flash.rb +13 -7
  70. data/lib/action_dispatch/middleware/params_parser.rb +1 -1
  71. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -3
  72. data/lib/action_dispatch/middleware/remote_ip.rb +40 -54
  73. data/lib/action_dispatch/middleware/request_id.rb +1 -1
  74. data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -1
  75. data/lib/action_dispatch/middleware/show_exceptions.rb +1 -0
  76. data/lib/action_dispatch/middleware/static.rb +66 -37
  77. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +21 -19
  78. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +37 -9
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +2 -8
  80. data/lib/action_dispatch/middleware/templates/rescues/{diagnostics.erb → diagnostics.html.erb} +0 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +6 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +4 -0
  84. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +2 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -24
  86. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +0 -1
  87. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +120 -64
  88. data/lib/action_dispatch/routing/endpoint.rb +10 -0
  89. data/lib/action_dispatch/routing/inspector.rb +5 -12
  90. data/lib/action_dispatch/routing/mapper.rb +410 -281
  91. data/lib/action_dispatch/routing/polymorphic_routes.rb +191 -79
  92. data/lib/action_dispatch/routing/redirection.rb +10 -12
  93. data/lib/action_dispatch/routing/route_set.rb +297 -168
  94. data/lib/action_dispatch/routing/url_for.rb +15 -4
  95. data/lib/action_dispatch/testing/assertions/dom.rb +2 -26
  96. data/lib/action_dispatch/testing/assertions/response.rb +2 -7
  97. data/lib/action_dispatch/testing/assertions/routing.rb +22 -22
  98. data/lib/action_dispatch/testing/assertions/selector.rb +2 -429
  99. data/lib/action_dispatch/testing/assertions/tag.rb +2 -134
  100. data/lib/action_dispatch/testing/assertions.rb +11 -7
  101. data/lib/action_dispatch/testing/integration.rb +24 -19
  102. data/lib/action_dispatch/testing/test_request.rb +1 -1
  103. data/lib/action_dispatch/testing/test_response.rb +7 -0
  104. data/lib/action_pack/gem_version.rb +3 -3
  105. metadata +55 -13
  106. data/lib/action_controller/metal/responder.rb +0 -297
@@ -35,14 +35,25 @@ module ActionDispatch
35
35
 
36
36
  if env['action_dispatch.show_detailed_exceptions']
37
37
  request = Request.new(env)
38
+ traces = wrapper.traces
39
+
40
+ trace_to_show = 'Application Trace'
41
+ if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
42
+ trace_to_show = 'Full Trace'
43
+ end
44
+
45
+ if source_to_show = traces[trace_to_show].first
46
+ source_to_show_id = source_to_show[:id]
47
+ end
48
+
38
49
  template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
39
50
  request: request,
40
51
  exception: wrapper.exception,
41
- application_trace: wrapper.application_trace,
42
- framework_trace: wrapper.framework_trace,
43
- full_trace: wrapper.full_trace,
52
+ traces: traces,
53
+ show_source_idx: source_to_show_id,
54
+ trace_to_show: trace_to_show,
44
55
  routes_inspector: routes_inspector(exception),
45
- source_extract: wrapper.source_extract,
56
+ source_extracts: wrapper.source_extracts,
46
57
  line_number: wrapper.line_number,
47
58
  file: wrapper.file
48
59
  )
@@ -6,16 +6,17 @@ module ActionDispatch
6
6
  cattr_accessor :rescue_responses
7
7
  @@rescue_responses = Hash.new(:internal_server_error)
8
8
  @@rescue_responses.merge!(
9
- 'ActionController::RoutingError' => :not_found,
10
- 'AbstractController::ActionNotFound' => :not_found,
11
- 'ActionController::MethodNotAllowed' => :method_not_allowed,
12
- 'ActionController::UnknownHttpMethod' => :method_not_allowed,
13
- 'ActionController::NotImplemented' => :not_implemented,
14
- 'ActionController::UnknownFormat' => :not_acceptable,
15
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
16
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
17
- 'ActionController::BadRequest' => :bad_request,
18
- 'ActionController::ParameterMissing' => :bad_request
9
+ 'ActionController::RoutingError' => :not_found,
10
+ 'AbstractController::ActionNotFound' => :not_found,
11
+ 'ActionController::MethodNotAllowed' => :method_not_allowed,
12
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
13
+ 'ActionController::NotImplemented' => :not_implemented,
14
+ 'ActionController::UnknownFormat' => :not_acceptable,
15
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
16
+ 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
17
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
18
+ 'ActionController::BadRequest' => :bad_request,
19
+ 'ActionController::ParameterMissing' => :bad_request
19
20
  )
20
21
 
21
22
  cattr_accessor :rescue_templates
@@ -56,21 +57,52 @@ module ActionDispatch
56
57
  clean_backtrace(:all)
57
58
  end
58
59
 
60
+ def traces
61
+ appplication_trace_with_ids = []
62
+ framework_trace_with_ids = []
63
+ full_trace_with_ids = []
64
+
65
+ full_trace.each_with_index do |trace, idx|
66
+ trace_with_id = { id: idx, trace: trace }
67
+
68
+ if application_trace.include?(trace)
69
+ appplication_trace_with_ids << trace_with_id
70
+ else
71
+ framework_trace_with_ids << trace_with_id
72
+ end
73
+
74
+ full_trace_with_ids << trace_with_id
75
+ end
76
+
77
+ {
78
+ "Application Trace" => appplication_trace_with_ids,
79
+ "Framework Trace" => framework_trace_with_ids,
80
+ "Full Trace" => full_trace_with_ids
81
+ }
82
+ end
83
+
59
84
  def self.status_code_for_exception(class_name)
60
85
  Rack::Utils.status_code(@@rescue_responses[class_name])
61
86
  end
62
87
 
63
- def source_extract
64
- if application_trace && trace = application_trace.first
65
- file, line, _ = trace.split(":")
66
- @file = file
67
- @line_number = line.to_i
68
- source_fragment(@file, @line_number)
88
+ def source_extracts
89
+ backtrace.map do |trace|
90
+ file, line = trace.split(":")
91
+ line_number = line.to_i
92
+
93
+ {
94
+ code: source_fragment(file, line_number),
95
+ line_number: line_number
96
+ }
69
97
  end
70
98
  end
71
99
 
72
100
  private
73
101
 
102
+ def backtrace
103
+ Array(@exception.backtrace)
104
+ end
105
+
74
106
  def original_exception(exception)
75
107
  if registered_original_exception?(exception)
76
108
  exception.original_exception
@@ -85,9 +117,9 @@ module ActionDispatch
85
117
 
86
118
  def clean_backtrace(*args)
87
119
  if backtrace_cleaner
88
- backtrace_cleaner.clean(@exception.backtrace, *args)
120
+ backtrace_cleaner.clean(backtrace, *args)
89
121
  else
90
- @exception.backtrace
122
+ backtrace
91
123
  end
92
124
  end
93
125
 
@@ -10,7 +10,7 @@ module ActionDispatch
10
10
  end
11
11
  end
12
12
 
13
- # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
13
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
14
14
  # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
15
15
  # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
16
16
  # then expose the flash to its template. Actually, that exposure is automatically done.
@@ -37,8 +37,11 @@ module ActionDispatch
37
37
  # flash.alert = "You must be logged in"
38
38
  # flash.notice = "Post successfully created"
39
39
  #
40
- # This example just places a string in the flash, but you can put any object in there. And of course, you can put as
41
- # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
40
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
41
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
42
+ # use sanitize helper.
43
+ #
44
+ # Just remember: They'll be gone by the time the next action has been performed.
42
45
  #
43
46
  # See docs on the FlashHash class for more details about the flash.
44
47
  class Flash
@@ -76,7 +79,7 @@ module ActionDispatch
76
79
  class FlashHash
77
80
  include Enumerable
78
81
 
79
- def self.from_session_value(value)
82
+ def self.from_session_value(value) #:nodoc:
80
83
  flash = case value
81
84
  when FlashHash # Rails 3.1, 3.2
82
85
  new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
@@ -88,8 +91,11 @@ module ActionDispatch
88
91
 
89
92
  flash.tap(&:sweep)
90
93
  end
91
-
92
- def to_session_value
94
+
95
+ # Builds a hash containing the discarded values and the hashes
96
+ # representing the flashes.
97
+ # If there are no values in @flashes, returns nil.
98
+ def to_session_value #:nodoc:
93
99
  return nil if empty?
94
100
  {'discard' => @discard.to_a, 'flashes' => @flashes}
95
101
  end
@@ -129,7 +135,7 @@ module ActionDispatch
129
135
  end
130
136
 
131
137
  def key?(name)
132
- @flashes.key? name
138
+ @flashes.key? name.to_s
133
139
  end
134
140
 
135
141
  def delete(key)
@@ -47,7 +47,7 @@ module ActionDispatch
47
47
  else
48
48
  false
49
49
  end
50
- rescue Exception => e # JSON or Ruby code block errors
50
+ rescue => e # JSON or Ruby code block errors
51
51
  logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
52
52
 
53
53
  raise ParseError.new(e.message, e)
@@ -1,4 +1,14 @@
1
1
  module ActionDispatch
2
+ # When called, this middleware renders an error page. By default if an HTML
3
+ # response is expected it will render static error pages from the `/public`
4
+ # directory. For example when this middleware receives a 500 response it will
5
+ # render the template found in `/public/500.html`.
6
+ # If an internationalized locale is set, this middleware will attempt to render
7
+ # the template in `/public/500.<locale>.html`. If an internationalized template
8
+ # is not found it will fall back on `/public/500.html`.
9
+ #
10
+ # When a request with a content type other than HTML is made, this middleware
11
+ # will attempt to convert error information into the appropriate response type.
2
12
  class PublicExceptions
3
13
  attr_accessor :public_path
4
14
 
@@ -32,9 +42,8 @@ module ActionDispatch
32
42
  end
33
43
 
34
44
  def render_html(status)
35
- found = false
36
- path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
37
- path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
45
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
46
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
38
47
 
39
48
  if found || File.exist?(path)
40
49
  render_format(status, 'text/html', File.read(path))
@@ -1,3 +1,5 @@
1
+ require 'ipaddr'
2
+
1
3
  module ActionDispatch
2
4
  # This middleware calculates the IP address of the remote client that is
3
5
  # making the request. It does this by checking various headers that could
@@ -11,7 +13,7 @@ module ActionDispatch
11
13
  # Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
12
14
  # requires. Some Rack servers simply drop preceding headers, and only report
13
15
  # the value that was {given in the last header}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
14
- # If you are behind multiple proxy servers (like Nginx to HAProxy to Unicorn)
16
+ # If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
15
17
  # then you should test your Rack server to make sure your data is good.
16
18
  #
17
19
  # IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
@@ -28,14 +30,14 @@ module ActionDispatch
28
30
  # guaranteed by the IP specification to be private addresses. Those will
29
31
  # not be the ultimate client IP in production, and so are discarded. See
30
32
  # http://en.wikipedia.org/wiki/Private_network for details.
31
- TRUSTED_PROXIES = %r{
32
- ^127\.0\.0\.1$ | # localhost IPv4
33
- ^::1$ | # localhost IPv6
34
- ^fc00: | # private IPv6 range fc00
35
- ^10\. | # private IPv4 range 10.x.x.x
36
- ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255
37
- ^192\.168\. # private IPv4 range 192.168.x.x
38
- }x
33
+ TRUSTED_PROXIES = [
34
+ "127.0.0.1", # localhost IPv4
35
+ "::1", # localhost IPv6
36
+ "fc00::/7", # private IPv6 range fc00::/7
37
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
38
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
39
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
40
+ ].map { |proxy| IPAddr.new(proxy) }
39
41
 
40
42
  attr_reader :check_ip, :proxies
41
43
 
@@ -47,24 +49,24 @@ module ActionDispatch
47
49
  # clients (like WAP devices), or behind proxies that set headers in an
48
50
  # incorrect or confusing way (like AWS ELB).
49
51
  #
50
- # The +custom_proxies+ argument can take a regex, which will be used
51
- # instead of +TRUSTED_PROXIES+, or a string, which will be used in addition
52
- # to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the
53
- # middle (or at the beginning) of the X-Forwarded-For list, with your proxy
54
- # servers after it. If your proxies aren't removed, pass them in via the
55
- # +custom_proxies+ parameter. That way, the middleware will ignore those
56
- # IP addresses, and return the one that you want.
52
+ # The +custom_proxies+ argument can take an Array of string, IPAddr, or
53
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
54
+ # single string, IPAddr, or Regexp object is provided, it will be used in
55
+ # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
56
+ # want in the middle (or at the beginning) of the X-Forwarded-For list,
57
+ # with your proxy servers after it. If your proxies aren't removed, pass
58
+ # them in via the +custom_proxies+ parameter. That way, the middleware will
59
+ # ignore those IP addresses, and return the one that you want.
57
60
  def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
58
61
  @app = app
59
62
  @check_ip = check_ip_spoofing
60
- @proxies = case custom_proxies
61
- when Regexp
62
- custom_proxies
63
- when nil
64
- TRUSTED_PROXIES
65
- else
66
- Regexp.union(TRUSTED_PROXIES, custom_proxies)
67
- end
63
+ @proxies = if custom_proxies.blank?
64
+ TRUSTED_PROXIES
65
+ elsif custom_proxies.respond_to?(:any?)
66
+ custom_proxies
67
+ else
68
+ Array(custom_proxies) + TRUSTED_PROXIES
69
+ end
68
70
  end
69
71
 
70
72
  # Since the IP address may not be needed, we store the object here
@@ -80,32 +82,6 @@ module ActionDispatch
80
82
  # into an actual IP address. If the ActionDispatch::Request#remote_ip method
81
83
  # is called, this class will calculate the value and then memoize it.
82
84
  class GetIp
83
-
84
- # This constant contains a regular expression that validates every known
85
- # form of IP v4 and v6 address, with or without abbreviations, adapted
86
- # from {this gist}[https://gist.github.com/gazay/1289635].
87
- VALID_IP = %r{
88
- (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
89
- (^(
90
- (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
91
- (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
92
- (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
93
- (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
94
- (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
95
- (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
96
- (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
97
- (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
98
- (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
99
- (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
100
- (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
101
- (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
102
- (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
103
- ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
104
- (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning
105
- (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
106
- )$)
107
- }x
108
-
109
85
  def initialize(env, middleware)
110
86
  @env = env
111
87
  @check_ip = middleware.check_ip
@@ -118,7 +94,7 @@ module ActionDispatch
118
94
  #
119
95
  # REMOTE_ADDR will be correct if the request is made directly against the
120
96
  # Ruby process, on e.g. Heroku. When the request is proxied by another
121
- # server like HAProxy or Nginx, the IP address that made the original
97
+ # server like HAProxy or NGINX, the IP address that made the original
122
98
  # request will be put in an X-Forwarded-For header. If there are multiple
123
99
  # proxies, that header may contain a list of IPs. Other proxy services
124
100
  # set the Client-Ip header instead, so we check that too.
@@ -173,12 +149,22 @@ module ActionDispatch
173
149
  def ips_from(header)
174
150
  # Split the comma-separated list into an array of strings
175
151
  ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
176
- # Only return IPs that are valid according to the regex
177
- ips.select{ |ip| ip =~ VALID_IP }
152
+ ips.select do |ip|
153
+ begin
154
+ # Only return IPs that are valid according to the IPAddr#new method
155
+ range = IPAddr.new(ip).to_range
156
+ # we want to make sure nobody is sneaking a netmask in
157
+ range.begin == range.end
158
+ rescue ArgumentError
159
+ nil
160
+ end
161
+ end
178
162
  end
179
163
 
180
164
  def filter_proxies(ips)
181
- ips.reject { |ip| ip =~ @proxies }
165
+ ips.reject do |ip|
166
+ @proxies.any? { |proxy| proxy === ip }
167
+ end
182
168
  end
183
169
 
184
170
  end
@@ -5,7 +5,7 @@ module ActionDispatch
5
5
  # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
6
6
  # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
7
7
  #
8
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
8
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
9
9
  # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
10
10
  # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
11
11
  #
@@ -49,7 +49,7 @@ module ActionDispatch
49
49
  # reasonably sure that your upgrade is otherwise complete. Additionally,
50
50
  # you should take care to make sure you are not relying on the ability to
51
51
  # decode signed cookies generated by your app in external applications or
52
- # Javascript before upgrading.
52
+ # JavaScript before upgrading.
53
53
  #
54
54
  # Note that changing the secret key will invalidate all existing sessions!
55
55
  class CookieStore < Rack::Session::Abstract::ID
@@ -42,6 +42,7 @@ module ActionDispatch
42
42
  wrapper = ExceptionWrapper.new(env, exception)
43
43
  status = wrapper.status_code
44
44
  env["action_dispatch.exception"] = wrapper.exception
45
+ env["action_dispatch.original_path"] = env["PATH_INFO"]
45
46
  env["PATH_INFO"] = "/#{status}"
46
47
  response = @exceptions_app.call(env)
47
48
  response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
@@ -2,69 +2,98 @@ require 'rack/utils'
2
2
  require 'active_support/core_ext/uri'
3
3
 
4
4
  module ActionDispatch
5
+ # 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
+ #
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.
5
15
  class FileHandler
6
16
  def initialize(root, cache_control)
7
17
  @root = root.chomp('/')
8
18
  @compiled_root = /^#{Regexp.escape(root)}/
9
- headers = cache_control && { 'Cache-Control' => cache_control }
10
- @file_server = ::Rack::File.new(@root, headers)
19
+ headers = cache_control && { 'Cache-Control' => cache_control }
20
+ @file_server = ::Rack::File.new(@root, headers)
11
21
  end
12
22
 
13
23
  def match?(path)
14
- path = unescape_path(path)
24
+ path = URI.parser.unescape(path)
15
25
  return false unless path.valid_encoding?
16
26
 
17
- full_path = path.empty? ? @root : File.join(@root,
18
- clean_path_info(escape_glob_chars(path)))
19
- paths = "#{full_path}#{ext}"
27
+ paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v|
28
+ Rack::Utils.clean_path_info v
29
+ }
20
30
 
21
- matches = Dir[paths]
22
- match = matches.detect { |m| File.file?(m) }
23
- if match
24
- match.sub!(@compiled_root, '')
25
- ::Rack::Utils.escape(match)
31
+ if match = paths.detect { |p|
32
+ path = File.join(@root, p)
33
+ begin
34
+ File.file?(path) && File.readable?(path)
35
+ rescue SystemCallError
36
+ false
37
+ end
38
+
39
+ }
40
+ return ::Rack::Utils.escape(match)
26
41
  end
27
42
  end
28
43
 
29
44
  def call(env)
30
- @file_server.call(env)
31
- end
32
-
33
- def ext
34
- @ext ||= begin
35
- ext = ::ActionController::Base.default_static_extension
36
- "{,#{ext},/index#{ext}}"
45
+ path = env['PATH_INFO']
46
+ gzip_path = gzip_file_path(path)
47
+
48
+ if gzip_path && gzip_encoding_accepted?(env)
49
+ env['PATH_INFO'] = gzip_path
50
+ status, headers, body = @file_server.call(env)
51
+ headers['Content-Encoding'] = 'gzip'
52
+ headers['Content-Type'] = content_type(path)
53
+ else
54
+ status, headers, body = @file_server.call(env)
37
55
  end
38
- end
39
56
 
40
- def unescape_path(path)
41
- URI.parser.unescape(path)
42
- end
57
+ headers['Vary'] = 'Accept-Encoding' if gzip_path
43
58
 
44
- def escape_glob_chars(path)
45
- path.gsub(/[*?{}\[\]]/, "\\\\\\&")
59
+ return [status, headers, body]
60
+ ensure
61
+ env['PATH_INFO'] = path
46
62
  end
47
63
 
48
64
  private
65
+ def ext
66
+ ::ActionController::Base.default_static_extension
67
+ end
49
68
 
50
- PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
51
-
52
- def clean_path_info(path_info)
53
- parts = path_info.split PATH_SEPS
54
-
55
- clean = []
56
-
57
- parts.each do |part|
58
- next if part.empty? || part == '.'
59
- part == '..' ? clean.pop : clean << part
69
+ def content_type(path)
70
+ ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
60
71
  end
61
72
 
62
- clean.unshift '/' if parts.empty? || parts.first.empty?
73
+ def gzip_encoding_accepted?(env)
74
+ env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
75
+ end
63
76
 
64
- ::File.join(*clean)
65
- end
77
+ def gzip_file_path(path)
78
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
79
+ gzip_path = "#{path}.gz"
80
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
81
+ gzip_path
82
+ else
83
+ false
84
+ end
85
+ end
66
86
  end
67
87
 
88
+ # This middleware will attempt to return the contents of a file's body from
89
+ # disk in the response. If a file is not found on disk, the request will be
90
+ # delegated to the application stack. This middleware is commonly initialized
91
+ # to serve assets from a server's `public/` directory.
92
+ #
93
+ # This middleware verifies the path to ensure that only files
94
+ # living in the root directory can be rendered. A request cannot
95
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
96
+ # requests will result in a file being returned.
68
97
  class Static
69
98
  def initialize(app, path, cache_control=nil)
70
99
  @app = app