rack 1.4.7 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +77 -0
  3. data/{COPYING → MIT-LICENSE} +4 -2
  4. data/README.rdoc +122 -456
  5. data/Rakefile +32 -31
  6. data/SPEC +119 -29
  7. data/bin/rackup +1 -0
  8. data/contrib/rack_logo.svg +164 -111
  9. data/example/lobster.ru +2 -0
  10. data/example/protectedlobster.rb +4 -2
  11. data/example/protectedlobster.ru +3 -1
  12. data/lib/rack/auth/abstract/handler.rb +7 -5
  13. data/lib/rack/auth/abstract/request.rb +8 -6
  14. data/lib/rack/auth/basic.rb +5 -2
  15. data/lib/rack/auth/digest/md5.rb +10 -8
  16. data/lib/rack/auth/digest/nonce.rb +6 -3
  17. data/lib/rack/auth/digest/params.rb +5 -4
  18. data/lib/rack/auth/digest/request.rb +4 -2
  19. data/lib/rack/body_proxy.rb +11 -9
  20. data/lib/rack/builder.rb +63 -20
  21. data/lib/rack/cascade.rb +10 -9
  22. data/lib/rack/chunked.rb +45 -11
  23. data/lib/rack/{commonlogger.rb → common_logger.rb} +24 -15
  24. data/lib/rack/{conditionalget.rb → conditional_get.rb} +20 -6
  25. data/lib/rack/config.rb +7 -0
  26. data/lib/rack/content_length.rb +12 -6
  27. data/lib/rack/content_type.rb +4 -2
  28. data/lib/rack/core_ext/regexp.rb +14 -0
  29. data/lib/rack/deflater.rb +73 -42
  30. data/lib/rack/directory.rb +77 -56
  31. data/lib/rack/etag.rb +25 -13
  32. data/lib/rack/events.rb +156 -0
  33. data/lib/rack/file.rb +4 -143
  34. data/lib/rack/files.rb +178 -0
  35. data/lib/rack/handler/cgi.rb +18 -17
  36. data/lib/rack/handler/fastcgi.rb +21 -17
  37. data/lib/rack/handler/lsws.rb +14 -12
  38. data/lib/rack/handler/scgi.rb +27 -21
  39. data/lib/rack/handler/thin.rb +19 -5
  40. data/lib/rack/handler/webrick.rb +66 -24
  41. data/lib/rack/handler.rb +29 -19
  42. data/lib/rack/head.rb +21 -14
  43. data/lib/rack/lint.rb +259 -65
  44. data/lib/rack/lobster.rb +17 -10
  45. data/lib/rack/lock.rb +19 -10
  46. data/lib/rack/logger.rb +4 -2
  47. data/lib/rack/media_type.rb +43 -0
  48. data/lib/rack/method_override.rb +52 -0
  49. data/lib/rack/mime.rb +43 -6
  50. data/lib/rack/mock.rb +109 -44
  51. data/lib/rack/multipart/generator.rb +11 -12
  52. data/lib/rack/multipart/parser.rb +302 -115
  53. data/lib/rack/multipart/uploaded_file.rb +4 -3
  54. data/lib/rack/multipart.rb +40 -9
  55. data/lib/rack/null_logger.rb +39 -0
  56. data/lib/rack/query_parser.rb +218 -0
  57. data/lib/rack/recursive.rb +14 -11
  58. data/lib/rack/reloader.rb +12 -5
  59. data/lib/rack/request.rb +484 -270
  60. data/lib/rack/response.rb +196 -77
  61. data/lib/rack/rewindable_input.rb +5 -14
  62. data/lib/rack/runtime.rb +13 -6
  63. data/lib/rack/sendfile.rb +44 -20
  64. data/lib/rack/server.rb +175 -61
  65. data/lib/rack/session/abstract/id.rb +276 -133
  66. data/lib/rack/session/cookie.rb +75 -40
  67. data/lib/rack/session/memcache.rb +4 -87
  68. data/lib/rack/session/pool.rb +24 -18
  69. data/lib/rack/show_exceptions.rb +392 -0
  70. data/lib/rack/{showstatus.rb → show_status.rb} +11 -9
  71. data/lib/rack/static.rb +65 -38
  72. data/lib/rack/tempfile_reaper.rb +24 -0
  73. data/lib/rack/urlmap.rb +40 -15
  74. data/lib/rack/utils.rb +316 -285
  75. data/lib/rack.rb +78 -23
  76. data/rack.gemspec +26 -19
  77. metadata +44 -209
  78. data/KNOWN-ISSUES +0 -30
  79. data/lib/rack/backports/uri/common_18.rb +0 -56
  80. data/lib/rack/backports/uri/common_192.rb +0 -52
  81. data/lib/rack/backports/uri/common_193.rb +0 -29
  82. data/lib/rack/handler/evented_mongrel.rb +0 -8
  83. data/lib/rack/handler/mongrel.rb +0 -100
  84. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  85. data/lib/rack/methodoverride.rb +0 -33
  86. data/lib/rack/nulllogger.rb +0 -18
  87. data/lib/rack/showexceptions.rb +0 -378
  88. data/test/builder/anything.rb +0 -5
  89. data/test/builder/comment.ru +0 -4
  90. data/test/builder/end.ru +0 -5
  91. data/test/builder/line.ru +0 -1
  92. data/test/builder/options.ru +0 -2
  93. data/test/cgi/assets/folder/test.js +0 -1
  94. data/test/cgi/assets/fonts/font.eot +0 -1
  95. data/test/cgi/assets/images/image.png +0 -1
  96. data/test/cgi/assets/index.html +0 -1
  97. data/test/cgi/assets/javascripts/app.js +0 -1
  98. data/test/cgi/assets/stylesheets/app.css +0 -1
  99. data/test/cgi/lighttpd.conf +0 -26
  100. data/test/cgi/lighttpd.errors +0 -1
  101. data/test/cgi/rackup_stub.rb +0 -6
  102. data/test/cgi/sample_rackup.ru +0 -5
  103. data/test/cgi/test +0 -9
  104. data/test/cgi/test+directory/test+file +0 -1
  105. data/test/cgi/test.fcgi +0 -8
  106. data/test/cgi/test.ru +0 -5
  107. data/test/gemloader.rb +0 -10
  108. data/test/multipart/bad_robots +0 -259
  109. data/test/multipart/binary +0 -0
  110. data/test/multipart/content_type_and_no_filename +0 -6
  111. data/test/multipart/empty +0 -10
  112. data/test/multipart/fail_16384_nofile +0 -814
  113. data/test/multipart/file1.txt +0 -1
  114. data/test/multipart/filename_and_modification_param +0 -7
  115. data/test/multipart/filename_with_escaped_quotes +0 -6
  116. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  117. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  118. data/test/multipart/filename_with_unescaped_percentages +0 -6
  119. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  120. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  121. data/test/multipart/filename_with_unescaped_quotes +0 -6
  122. data/test/multipart/ie +0 -6
  123. data/test/multipart/mixed_files +0 -21
  124. data/test/multipart/nested +0 -10
  125. data/test/multipart/none +0 -9
  126. data/test/multipart/semicolon +0 -6
  127. data/test/multipart/text +0 -15
  128. data/test/multipart/three_files_three_fields +0 -31
  129. data/test/multipart/webkit +0 -32
  130. data/test/rackup/config.ru +0 -31
  131. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  132. data/test/spec_auth.rb +0 -57
  133. data/test/spec_auth_basic.rb +0 -81
  134. data/test/spec_auth_digest.rb +0 -259
  135. data/test/spec_body_proxy.rb +0 -69
  136. data/test/spec_builder.rb +0 -207
  137. data/test/spec_cascade.rb +0 -61
  138. data/test/spec_cgi.rb +0 -102
  139. data/test/spec_chunked.rb +0 -87
  140. data/test/spec_commonlogger.rb +0 -57
  141. data/test/spec_conditionalget.rb +0 -102
  142. data/test/spec_config.rb +0 -22
  143. data/test/spec_content_length.rb +0 -86
  144. data/test/spec_content_type.rb +0 -45
  145. data/test/spec_deflater.rb +0 -187
  146. data/test/spec_directory.rb +0 -88
  147. data/test/spec_etag.rb +0 -98
  148. data/test/spec_fastcgi.rb +0 -107
  149. data/test/spec_file.rb +0 -200
  150. data/test/spec_handler.rb +0 -59
  151. data/test/spec_head.rb +0 -48
  152. data/test/spec_lint.rb +0 -515
  153. data/test/spec_lobster.rb +0 -58
  154. data/test/spec_lock.rb +0 -167
  155. data/test/spec_logger.rb +0 -23
  156. data/test/spec_methodoverride.rb +0 -72
  157. data/test/spec_mock.rb +0 -269
  158. data/test/spec_mongrel.rb +0 -182
  159. data/test/spec_multipart.rb +0 -479
  160. data/test/spec_nulllogger.rb +0 -23
  161. data/test/spec_recursive.rb +0 -72
  162. data/test/spec_request.rb +0 -955
  163. data/test/spec_response.rb +0 -313
  164. data/test/spec_rewindable_input.rb +0 -118
  165. data/test/spec_runtime.rb +0 -49
  166. data/test/spec_sendfile.rb +0 -90
  167. data/test/spec_server.rb +0 -121
  168. data/test/spec_session_abstract_id.rb +0 -43
  169. data/test/spec_session_cookie.rb +0 -361
  170. data/test/spec_session_memcache.rb +0 -321
  171. data/test/spec_session_pool.rb +0 -209
  172. data/test/spec_showexceptions.rb +0 -92
  173. data/test/spec_showstatus.rb +0 -84
  174. data/test/spec_static.rb +0 -145
  175. data/test/spec_thin.rb +0 -86
  176. data/test/spec_urlmap.rb +0 -213
  177. data/test/spec_utils.rb +0 -554
  178. data/test/spec_webrick.rb +0 -143
  179. data/test/static/another/index.html +0 -1
  180. data/test/static/index.html +0 -1
  181. data/test/testrequest.rb +0 -78
  182. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  183. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/utils.rb CHANGED
@@ -1,26 +1,35 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
2
5
  require 'fileutils'
3
6
  require 'set'
4
7
  require 'tempfile'
5
- require 'rack/multipart'
6
-
7
- major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
8
-
9
- if major == 1 && minor < 9
10
- require 'rack/backports/uri/common_18'
11
- elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 320 && RUBY_ENGINE != 'jruby'
12
- require 'rack/backports/uri/common_192'
13
- elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
14
- require 'rack/backports/uri/common_193'
15
- else
16
- require 'uri/common'
17
- end
8
+ require 'rack/query_parser'
9
+ require 'time'
10
+
11
+ require_relative 'core_ext/regexp'
18
12
 
19
13
  module Rack
20
14
  # Rack::Utils contains a grab-bag of useful methods for writing web
21
15
  # applications adopted from all kinds of Ruby libraries.
22
16
 
23
17
  module Utils
18
+ using ::Rack::RegexpExtensions
19
+
20
+ ParameterTypeError = QueryParser::ParameterTypeError
21
+ InvalidParameterError = QueryParser::InvalidParameterError
22
+ DEFAULT_SEP = QueryParser::DEFAULT_SEP
23
+ COMMON_SEP = QueryParser::COMMON_SEP
24
+ KeySpaceConstrainedParams = QueryParser::Params
25
+
26
+ class << self
27
+ attr_accessor :default_query_parser
28
+ end
29
+ # The default number of bytes to allow parameter keys to take up.
30
+ # This helps prevent a rogue client from flooding a Request.
31
+ self.default_query_parser = QueryParser.make_default(65536, 100)
32
+
24
33
  # URI escapes. (CGI style space to +)
25
34
  def escape(s)
26
35
  URI.encode_www_form_component(s)
@@ -30,125 +39,70 @@ module Rack
30
39
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
31
40
  # true URI escaping.
32
41
  def escape_path(s)
33
- escape(s).gsub('+', '%20')
42
+ ::URI::DEFAULT_PARSER.escape s
34
43
  end
35
44
  module_function :escape_path
36
45
 
46
+ # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
47
+ # unescaping query parameters or form components.
48
+ def unescape_path(s)
49
+ ::URI::DEFAULT_PARSER.unescape s
50
+ end
51
+ module_function :unescape_path
52
+
53
+
37
54
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
38
55
  # target encoding of the string returned, and it defaults to UTF-8
39
- if defined?(::Encoding)
40
- def unescape(s, encoding = Encoding::UTF_8)
41
- URI.decode_www_form_component(s, encoding)
42
- end
43
- else
44
- def unescape(s, encoding = nil)
45
- URI.decode_www_form_component(s, encoding)
46
- end
56
+ def unescape(s, encoding = Encoding::UTF_8)
57
+ URI.decode_www_form_component(s, encoding)
47
58
  end
48
59
  module_function :unescape
49
60
 
50
- DEFAULT_SEP = /[&;] */n
51
-
52
61
  class << self
53
- attr_accessor :key_space_limit
54
- attr_accessor :param_depth_limit
55
62
  attr_accessor :multipart_part_limit
56
63
  end
57
64
 
58
- # The default number of bytes to allow parameter keys to take up.
59
- # This helps prevent a rogue client from flooding a Request.
60
- self.key_space_limit = 65536
61
-
62
- # Default depth at which the parameter parser will raise an exception for
63
- # being too deep. This helps prevent SystemStackErrors
64
- self.param_depth_limit = 100
65
- #
66
- # The maximum number of parts a request can contain. Accepting to many part
65
+ # The maximum number of parts a request can contain. Accepting too many part
67
66
  # can lead to the server running out of file handles.
68
67
  # Set to `0` for no limit.
69
68
  self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
70
69
 
71
- # Stolen from Mongrel, with some small modifications:
72
- # Parses a query string by breaking it up at the '&'
73
- # and ';' characters. You can also use this to parse
74
- # cookies by changing the characters used in the second
75
- # parameter (which defaults to '&;').
76
- def parse_query(qs, d = nil, &unescaper)
77
- unescaper ||= method(:unescape)
78
-
79
- params = KeySpaceConstrainedParams.new
80
-
81
- (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
82
- next if p.empty?
83
- k, v = p.split('=', 2).map(&unescaper)
84
- next unless k || v
85
-
86
- if cur = params[k]
87
- if cur.class == Array
88
- params[k] << v
89
- else
90
- params[k] = [cur, v]
91
- end
92
- else
93
- params[k] = v
94
- end
95
- end
96
-
97
- return params.to_params_hash
70
+ def self.param_depth_limit
71
+ default_query_parser.param_depth_limit
98
72
  end
99
- module_function :parse_query
100
-
101
- def parse_nested_query(qs, d = nil)
102
- params = KeySpaceConstrainedParams.new
103
73
 
104
- (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
105
- k, v = p.split('=', 2).map { |s| unescape(s) }
74
+ def self.param_depth_limit=(v)
75
+ self.default_query_parser = self.default_query_parser.new_depth_limit(v)
76
+ end
106
77
 
107
- normalize_params(params, k, v)
108
- end
78
+ def self.key_space_limit
79
+ default_query_parser.key_space_limit
80
+ end
109
81
 
110
- return params.to_params_hash
82
+ def self.key_space_limit=(v)
83
+ self.default_query_parser = self.default_query_parser.new_space_limit(v)
111
84
  end
112
- module_function :parse_nested_query
113
85
 
114
- def normalize_params(params, name, v = nil, depth = Utils.param_depth_limit)
115
- raise RangeError if depth <= 0
116
-
117
- name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
118
- k = $1 || ''
119
- after = $' || ''
120
-
121
- return if k.empty?
122
-
123
- if after == ""
124
- params[k] = v
125
- elsif after == "[]"
126
- params[k] ||= []
127
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
128
- params[k] << v
129
- elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
130
- child_key = $1
131
- params[k] ||= []
132
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
133
- if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
134
- normalize_params(params[k].last, child_key, v, depth - 1)
135
- else
136
- params[k] << normalize_params(params.class.new, child_key, v, depth - 1)
137
- end
138
- else
139
- params[k] ||= params.class.new
140
- raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
141
- params[k] = normalize_params(params[k], after, v, depth - 1)
86
+ if defined?(Process::CLOCK_MONOTONIC)
87
+ def clock_time
88
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
89
+ end
90
+ else
91
+ def clock_time
92
+ Time.now.to_f
142
93
  end
94
+ end
95
+ module_function :clock_time
143
96
 
144
- return params
97
+ def parse_query(qs, d = nil, &unescaper)
98
+ Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
145
99
  end
146
- module_function :normalize_params
100
+ module_function :parse_query
147
101
 
148
- def params_hash_type?(obj)
149
- obj.kind_of?(KeySpaceConstrainedParams) || obj.kind_of?(Hash)
102
+ def parse_nested_query(qs, d = nil)
103
+ Rack::Utils.default_query_parser.parse_nested_query(qs, d)
150
104
  end
151
- module_function :params_hash_type?
105
+ module_function :parse_nested_query
152
106
 
153
107
  def build_query(params)
154
108
  params.map { |k, v|
@@ -170,16 +124,42 @@ module Rack
170
124
  when Hash
171
125
  value.map { |k, v|
172
126
  build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
173
- }.join("&")
174
- when String
127
+ }.delete_if(&:empty?).join('&')
128
+ when nil
129
+ prefix
130
+ else
175
131
  raise ArgumentError, "value must be a Hash" if prefix.nil?
176
132
  "#{prefix}=#{escape(value)}"
177
- else
178
- prefix
179
133
  end
180
134
  end
181
135
  module_function :build_nested_query
182
136
 
137
+ def q_values(q_value_header)
138
+ q_value_header.to_s.split(/\s*,\s*/).map do |part|
139
+ value, parameters = part.split(/\s*;\s*/, 2)
140
+ quality = 1.0
141
+ if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
142
+ quality = md[1].to_f
143
+ end
144
+ [value, quality]
145
+ end
146
+ end
147
+ module_function :q_values
148
+
149
+ def best_q_match(q_value_header, available_mimes)
150
+ values = q_values(q_value_header)
151
+
152
+ matches = values.map do |req_mime, quality|
153
+ match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
154
+ next unless match
155
+ [match, quality]
156
+ end.compact.sort_by do |match, quality|
157
+ (match.split('/', 2).count('*') * -10) + quality
158
+ end.last
159
+ matches && matches.first
160
+ end
161
+ module_function :best_q_match
162
+
183
163
  ESCAPE_HTML = {
184
164
  "&" => "&amp;",
185
165
  "<" => "&lt;",
@@ -188,13 +168,8 @@ module Rack
188
168
  '"' => "&quot;",
189
169
  "/" => "&#x2F;"
190
170
  }
191
- if //.respond_to?(:encoding)
192
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
193
- else
194
- # On 1.8, there is a kcode = 'u' bug that allows for XSS otherwhise
195
- # TODO doesn't apply to jruby, so a better condition above might be preferable?
196
- ESCAPE_HTML_PATTERN = /#{Regexp.union(*ESCAPE_HTML.keys)}/n
197
- end
171
+
172
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
198
173
 
199
174
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
200
175
  def escape_html(string)
@@ -206,133 +181,177 @@ module Rack
206
181
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
207
182
 
208
183
  expanded_accept_encoding =
209
- accept_encoding.map { |m, q|
184
+ accept_encoding.each_with_object([]) do |(m, q), list|
210
185
  if m == "*"
211
- (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
186
+ (available_encodings - accept_encoding.map(&:first))
187
+ .each { |m2| list << [m2, q] }
212
188
  else
213
- [[m, q]]
189
+ list << [m, q]
214
190
  end
215
- }.inject([]) { |mem, list|
216
- mem + list
217
- }
191
+ end
218
192
 
219
- encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
193
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first)
220
194
 
221
195
  unless encoding_candidates.include?("identity")
222
196
  encoding_candidates.push("identity")
223
197
  end
224
198
 
225
- expanded_accept_encoding.find_all { |m, q|
226
- q == 0.0
227
- }.each { |m, _|
228
- encoding_candidates.delete(m)
229
- }
199
+ expanded_accept_encoding.each do |m, q|
200
+ encoding_candidates.delete(m) if q == 0.0
201
+ end
230
202
 
231
- return (encoding_candidates & available_encodings)[0]
203
+ (encoding_candidates & available_encodings)[0]
232
204
  end
233
205
  module_function :select_best_encoding
234
206
 
235
- def set_cookie_header!(header, key, value)
207
+ def parse_cookies(env)
208
+ parse_cookies_header env[HTTP_COOKIE]
209
+ end
210
+ module_function :parse_cookies
211
+
212
+ def parse_cookies_header(header)
213
+ # According to RFC 2109:
214
+ # If multiple cookies satisfy the criteria above, they are ordered in
215
+ # the Cookie header such that those with more specific Path attributes
216
+ # precede those with less specific. Ordering with respect to other
217
+ # attributes (e.g., Domain) is unspecified.
218
+ return {} unless header
219
+ header.split(/[;,] */n).each_with_object({}) do |cookie, cookies|
220
+ next if cookie.empty?
221
+ key, value = cookie.split('=', 2)
222
+ cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
223
+ end
224
+ end
225
+ module_function :parse_cookies_header
226
+
227
+ def add_cookie_to_header(header, key, value)
236
228
  case value
237
229
  when Hash
238
- domain = "; domain=" + value[:domain] if value[:domain]
239
- path = "; path=" + value[:path] if value[:path]
240
- # According to RFC 2109, we need dashes here.
241
- # N.B.: cgi.rb uses spaces...
242
- expires = "; expires=" +
243
- rfc2822(value[:expires].clone.gmtime) if value[:expires]
230
+ domain = "; domain=#{value[:domain]}" if value[:domain]
231
+ path = "; path=#{value[:path]}" if value[:path]
232
+ max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
233
+ expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
244
234
  secure = "; secure" if value[:secure]
245
- httponly = "; HttpOnly" if value[:httponly]
235
+ httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
236
+ same_site =
237
+ case value[:same_site]
238
+ when false, nil
239
+ nil
240
+ when :none, 'None', :None
241
+ '; SameSite=None'
242
+ when :lax, 'Lax', :Lax
243
+ '; SameSite=Lax'
244
+ when true, :strict, 'Strict', :Strict
245
+ '; SameSite=Strict'
246
+ else
247
+ raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
248
+ end
246
249
  value = value[:value]
247
250
  end
248
251
  value = [value] unless Array === value
249
- cookie = escape(key) + "=" +
250
- value.map { |v| escape v }.join("&") +
251
- "#{domain}#{path}#{expires}#{secure}#{httponly}"
252
252
 
253
- case header["Set-Cookie"]
253
+ cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
254
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
255
+
256
+ case header
254
257
  when nil, ''
255
- header["Set-Cookie"] = cookie
258
+ cookie
256
259
  when String
257
- header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
260
+ [header, cookie].join("\n")
258
261
  when Array
259
- header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
262
+ (header + [cookie]).join("\n")
263
+ else
264
+ raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
260
265
  end
266
+ end
267
+ module_function :add_cookie_to_header
261
268
 
269
+ def set_cookie_header!(header, key, value)
270
+ header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
262
271
  nil
263
272
  end
264
273
  module_function :set_cookie_header!
265
274
 
266
- def delete_cookie_header!(header, key, value = {})
267
- case header["Set-Cookie"]
275
+ def make_delete_cookie_header(header, key, value)
276
+ case header
268
277
  when nil, ''
269
278
  cookies = []
270
279
  when String
271
- cookies = header["Set-Cookie"].split("\n")
280
+ cookies = header.split("\n")
272
281
  when Array
273
- cookies = header["Set-Cookie"]
282
+ cookies = header
274
283
  end
275
284
 
276
- cookies.reject! { |cookie|
277
- if value[:domain]
278
- cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
279
- elsif value[:path]
280
- cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
281
- else
282
- cookie =~ /\A#{escape(key)}=/
283
- end
284
- }
285
+ regexp = if value[:domain]
286
+ /\A#{escape(key)}=.*domain=#{value[:domain]}/
287
+ elsif value[:path]
288
+ /\A#{escape(key)}=.*path=#{value[:path]}/
289
+ else
290
+ /\A#{escape(key)}=/
291
+ end
285
292
 
286
- header["Set-Cookie"] = cookies.join("\n")
293
+ cookies.reject! { |cookie| regexp.match? cookie }
287
294
 
288
- set_cookie_header!(header, key,
289
- {:value => '', :path => nil, :domain => nil,
290
- :expires => Time.at(0) }.merge(value))
295
+ cookies.join("\n")
296
+ end
297
+ module_function :make_delete_cookie_header
291
298
 
299
+ def delete_cookie_header!(header, key, value = {})
300
+ header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
292
301
  nil
293
302
  end
294
303
  module_function :delete_cookie_header!
295
304
 
296
- # Return the bytesize of String; uses String#size under Ruby 1.8 and
297
- # String#bytesize under 1.9.
298
- if ''.respond_to?(:bytesize)
299
- def bytesize(string)
300
- string.bytesize
301
- end
302
- else
303
- def bytesize(string)
304
- string.size
305
- end
305
+ # Adds a cookie that will *remove* a cookie from the client. Hence the
306
+ # strange method name.
307
+ def add_remove_cookie_to_header(header, key, value = {})
308
+ new_header = make_delete_cookie_header(header, key, value)
309
+
310
+ add_cookie_to_header(new_header, key,
311
+ { value: '', path: nil, domain: nil,
312
+ max_age: '0',
313
+ expires: Time.at(0) }.merge(value))
314
+
306
315
  end
307
- module_function :bytesize
316
+ module_function :add_remove_cookie_to_header
317
+
318
+ def rfc2822(time)
319
+ time.rfc2822
320
+ end
321
+ module_function :rfc2822
308
322
 
309
323
  # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
310
324
  # of '% %b %Y'.
311
325
  # It assumes that the time is in GMT to comply to the RFC 2109.
312
326
  #
313
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough
327
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
314
328
  # that I'm certain someone implemented only that option.
315
329
  # Do not use %a and %b from Time.strptime, it would use localized names for
316
330
  # weekday and month.
317
331
  #
318
- def rfc2822(time)
332
+ def rfc2109(time)
319
333
  wday = Time::RFC2822_DAY_NAME[time.wday]
320
334
  mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
321
335
  time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
322
336
  end
323
- module_function :rfc2822
337
+ module_function :rfc2109
324
338
 
325
339
  # Parses the "Range:" header, if present, into an array of Range objects.
326
340
  # Returns nil if the header is missing or syntactically invalid.
327
341
  # Returns an empty array if none of the ranges are satisfiable.
328
342
  def byte_ranges(env, size)
343
+ warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
344
+ get_byte_ranges env['HTTP_RANGE'], size
345
+ end
346
+ module_function :byte_ranges
347
+
348
+ def get_byte_ranges(http_range, size)
329
349
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
330
- http_range = env['HTTP_RANGE']
331
350
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
332
351
  ranges = []
333
352
  $1.split(/,\s*/).each do |range_spec|
334
353
  return nil unless range_spec =~ /(\d*)-(\d*)/
335
- r0,r1 = $1, $2
354
+ r0, r1 = $1, $2
336
355
  if r0.empty?
337
356
  return nil if r1.empty?
338
357
  # suffix-byte-range-spec, represents trailing suffix of file
@@ -346,23 +365,28 @@ module Rack
346
365
  else
347
366
  r1 = r1.to_i
348
367
  return nil if r1 < r0 # backwards range is syntactically invalid
349
- r1 = size-1 if r1 >= size
368
+ r1 = size - 1 if r1 >= size
350
369
  end
351
370
  end
352
371
  ranges << (r0..r1) if r0 <= r1
353
372
  end
354
373
  ranges
355
374
  end
356
- module_function :byte_ranges
375
+ module_function :get_byte_ranges
357
376
 
358
377
  # Constant time string comparison.
378
+ #
379
+ # NOTE: the values compared should be of fixed length, such as strings
380
+ # that have already been processed by HMAC. This should not be used
381
+ # on variable length plaintext strings because it could leak length info
382
+ # via timing attacks.
359
383
  def secure_compare(a, b)
360
- return false unless bytesize(a) == bytesize(b)
384
+ return false unless a.bytesize == b.bytesize
361
385
 
362
386
  l = a.unpack("C*")
363
387
 
364
388
  r, i = 0, -1
365
- b.each_byte { |v| r |= v ^ l[i+=1] }
389
+ b.each_byte { |v| r |= v ^ l[i += 1] }
366
390
  r == 0
367
391
  end
368
392
  module_function :secure_compare
@@ -388,24 +412,28 @@ module Rack
388
412
  self.class.new(@for, app)
389
413
  end
390
414
 
391
- def context(env, app=@app)
415
+ def context(env, app = @app)
392
416
  recontext(app).call(env)
393
417
  end
394
418
  end
395
419
 
396
420
  # A case-insensitive Hash that preserves the original case of a
397
421
  # header when set.
398
- class HeaderHash < Hash
399
- def self.new(hash={})
400
- HeaderHash === hash ? hash : super(hash)
401
- end
402
-
403
- def initialize(hash={})
422
+ #
423
+ # @api private
424
+ class HeaderHash < Hash # :nodoc:
425
+ def initialize(hash = {})
404
426
  super()
405
427
  @names = {}
406
428
  hash.each { |k, v| self[k] = v }
407
429
  end
408
430
 
431
+ # on dup/clone, we need to duplicate @names hash
432
+ def initialize_copy(other)
433
+ super
434
+ @names = other.names.dup
435
+ end
436
+
409
437
  def each
410
438
  super do |k, v|
411
439
  yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
@@ -414,7 +442,7 @@ module Rack
414
442
 
415
443
  def to_hash
416
444
  hash = {}
417
- each { |k,v| hash[k] = v }
445
+ each { |k, v| hash[k] = v }
418
446
  hash
419
447
  end
420
448
 
@@ -423,21 +451,20 @@ module Rack
423
451
  end
424
452
 
425
453
  def []=(k, v)
426
- canonical = k.downcase
454
+ canonical = k.downcase.freeze
427
455
  delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
428
- @names[k] = @names[canonical] = k
456
+ @names[canonical] = k
429
457
  super k, v
430
458
  end
431
459
 
432
460
  def delete(k)
433
461
  canonical = k.downcase
434
462
  result = super @names.delete(canonical)
435
- @names.delete_if { |name,| name.downcase == canonical }
436
463
  result
437
464
  end
438
465
 
439
466
  def include?(k)
440
- @names.include?(k) || @names.include?(k.downcase)
467
+ super || @names.include?(k.downcase)
441
468
  end
442
469
 
443
470
  alias_method :has_key?, :include?
@@ -459,120 +486,124 @@ module Rack
459
486
  other.each { |k, v| self[k] = v }
460
487
  self
461
488
  end
462
- end
463
-
464
- class KeySpaceConstrainedParams
465
- def initialize(limit = Utils.key_space_limit)
466
- @limit = limit
467
- @size = 0
468
- @params = {}
469
- end
470
-
471
- def [](key)
472
- @params[key]
473
- end
474
-
475
- def []=(key, value)
476
- @size += key.size if key && !@params.key?(key)
477
- raise RangeError, 'exceeded available parameter key space' if @size > @limit
478
- @params[key] = value
479
- end
480
489
 
481
- def key?(key)
482
- @params.key?(key)
483
- end
484
-
485
- def to_params_hash
486
- hash = @params
487
- hash.keys.each do |key|
488
- value = hash[key]
489
- if value.kind_of?(self.class)
490
- hash[key] = value.to_params_hash
491
- elsif value.kind_of?(Array)
492
- value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
493
- end
490
+ protected
491
+ def names
492
+ @names
494
493
  end
495
- hash
496
- end
497
494
  end
498
495
 
499
496
  # Every standard HTTP code mapped to the appropriate message.
500
497
  # Generated with:
501
- # curl -s http://www.iana.org/assignments/http-status-codes | \
502
- # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
503
- # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
498
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
499
+ # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
500
+ # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
504
501
  HTTP_STATUS_CODES = {
505
- 100 => 'Continue',
506
- 101 => 'Switching Protocols',
507
- 102 => 'Processing',
508
- 200 => 'OK',
509
- 201 => 'Created',
510
- 202 => 'Accepted',
511
- 203 => 'Non-Authoritative Information',
512
- 204 => 'No Content',
513
- 205 => 'Reset Content',
514
- 206 => 'Partial Content',
515
- 207 => 'Multi-Status',
516
- 226 => 'IM Used',
517
- 300 => 'Multiple Choices',
518
- 301 => 'Moved Permanently',
519
- 302 => 'Found',
520
- 303 => 'See Other',
521
- 304 => 'Not Modified',
522
- 305 => 'Use Proxy',
523
- 306 => 'Reserved',
524
- 307 => 'Temporary Redirect',
525
- 400 => 'Bad Request',
526
- 401 => 'Unauthorized',
527
- 402 => 'Payment Required',
528
- 403 => 'Forbidden',
529
- 404 => 'Not Found',
530
- 405 => 'Method Not Allowed',
531
- 406 => 'Not Acceptable',
532
- 407 => 'Proxy Authentication Required',
533
- 408 => 'Request Timeout',
534
- 409 => 'Conflict',
535
- 410 => 'Gone',
536
- 411 => 'Length Required',
537
- 412 => 'Precondition Failed',
538
- 413 => 'Request Entity Too Large',
539
- 414 => 'Request-URI Too Long',
540
- 415 => 'Unsupported Media Type',
541
- 416 => 'Requested Range Not Satisfiable',
542
- 417 => 'Expectation Failed',
543
- 418 => "I'm a Teapot",
544
- 422 => 'Unprocessable Entity',
545
- 423 => 'Locked',
546
- 424 => 'Failed Dependency',
547
- 426 => 'Upgrade Required',
548
- 500 => 'Internal Server Error',
549
- 501 => 'Not Implemented',
550
- 502 => 'Bad Gateway',
551
- 503 => 'Service Unavailable',
552
- 504 => 'Gateway Timeout',
553
- 505 => 'HTTP Version Not Supported',
554
- 506 => 'Variant Also Negotiates',
555
- 507 => 'Insufficient Storage',
556
- 510 => 'Not Extended',
502
+ 100 => 'Continue',
503
+ 101 => 'Switching Protocols',
504
+ 102 => 'Processing',
505
+ 103 => 'Early Hints',
506
+ 200 => 'OK',
507
+ 201 => 'Created',
508
+ 202 => 'Accepted',
509
+ 203 => 'Non-Authoritative Information',
510
+ 204 => 'No Content',
511
+ 205 => 'Reset Content',
512
+ 206 => 'Partial Content',
513
+ 207 => 'Multi-Status',
514
+ 208 => 'Already Reported',
515
+ 226 => 'IM Used',
516
+ 300 => 'Multiple Choices',
517
+ 301 => 'Moved Permanently',
518
+ 302 => 'Found',
519
+ 303 => 'See Other',
520
+ 304 => 'Not Modified',
521
+ 305 => 'Use Proxy',
522
+ 306 => '(Unused)',
523
+ 307 => 'Temporary Redirect',
524
+ 308 => 'Permanent Redirect',
525
+ 400 => 'Bad Request',
526
+ 401 => 'Unauthorized',
527
+ 402 => 'Payment Required',
528
+ 403 => 'Forbidden',
529
+ 404 => 'Not Found',
530
+ 405 => 'Method Not Allowed',
531
+ 406 => 'Not Acceptable',
532
+ 407 => 'Proxy Authentication Required',
533
+ 408 => 'Request Timeout',
534
+ 409 => 'Conflict',
535
+ 410 => 'Gone',
536
+ 411 => 'Length Required',
537
+ 412 => 'Precondition Failed',
538
+ 413 => 'Payload Too Large',
539
+ 414 => 'URI Too Long',
540
+ 415 => 'Unsupported Media Type',
541
+ 416 => 'Range Not Satisfiable',
542
+ 417 => 'Expectation Failed',
543
+ 421 => 'Misdirected Request',
544
+ 422 => 'Unprocessable Entity',
545
+ 423 => 'Locked',
546
+ 424 => 'Failed Dependency',
547
+ 425 => 'Too Early',
548
+ 426 => 'Upgrade Required',
549
+ 428 => 'Precondition Required',
550
+ 429 => 'Too Many Requests',
551
+ 431 => 'Request Header Fields Too Large',
552
+ 451 => 'Unavailable for Legal Reasons',
553
+ 500 => 'Internal Server Error',
554
+ 501 => 'Not Implemented',
555
+ 502 => 'Bad Gateway',
556
+ 503 => 'Service Unavailable',
557
+ 504 => 'Gateway Timeout',
558
+ 505 => 'HTTP Version Not Supported',
559
+ 506 => 'Variant Also Negotiates',
560
+ 507 => 'Insufficient Storage',
561
+ 508 => 'Loop Detected',
562
+ 509 => 'Bandwidth Limit Exceeded',
563
+ 510 => 'Not Extended',
564
+ 511 => 'Network Authentication Required'
557
565
  }
558
566
 
559
567
  # Responses with HTTP status codes that should not have an entity body
560
- STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
568
+ STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
561
569
 
562
570
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
563
- [message.downcase.gsub(/\s|-/, '_').to_sym, code]
571
+ [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
564
572
  }.flatten]
565
573
 
566
574
  def status_code(status)
567
575
  if status.is_a?(Symbol)
568
- SYMBOL_TO_STATUS_CODE[status] || 500
576
+ SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
569
577
  else
570
578
  status.to_i
571
579
  end
572
580
  end
573
581
  module_function :status_code
574
582
 
575
- Multipart = Rack::Multipart
583
+ PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
584
+
585
+ def clean_path_info(path_info)
586
+ parts = path_info.split PATH_SEPS
587
+
588
+ clean = []
589
+
590
+ parts.each do |part|
591
+ next if part.empty? || part == '.'
592
+ part == '..' ? clean.pop : clean << part
593
+ end
594
+
595
+ clean.unshift '/' if parts.empty? || parts.first.empty?
596
+
597
+ ::File.join clean
598
+ end
599
+ module_function :clean_path_info
600
+
601
+ NULL_BYTE = "\0"
602
+
603
+ def valid_path?(path)
604
+ path.valid_encoding? && !path.include?(NULL_BYTE)
605
+ end
606
+ module_function :valid_path?
576
607
 
577
608
  end
578
609
  end