rack 2.0.9.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +808 -0
  3. data/CONTRIBUTING.md +142 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.md +293 -0
  6. data/SPEC.rdoc +340 -0
  7. data/lib/rack/auth/abstract/handler.rb +6 -2
  8. data/lib/rack/auth/abstract/request.rb +4 -2
  9. data/lib/rack/auth/basic.rb +7 -4
  10. data/lib/rack/auth/digest/md5.rb +1 -129
  11. data/lib/rack/auth/digest/nonce.rb +1 -51
  12. data/lib/rack/auth/digest/params.rb +1 -52
  13. data/lib/rack/auth/digest/request.rb +1 -41
  14. data/lib/rack/auth/digest.rb +256 -0
  15. data/lib/rack/body_proxy.rb +18 -15
  16. data/lib/rack/builder.rb +151 -40
  17. data/lib/rack/cascade.rb +30 -12
  18. data/lib/rack/chunked.rb +74 -23
  19. data/lib/rack/common_logger.rb +49 -36
  20. data/lib/rack/conditional_get.rb +33 -26
  21. data/lib/rack/config.rb +2 -0
  22. data/lib/rack/constants.rb +63 -0
  23. data/lib/rack/content_length.rb +13 -16
  24. data/lib/rack/content_type.rb +12 -8
  25. data/lib/rack/deflater.rb +84 -45
  26. data/lib/rack/directory.rb +90 -64
  27. data/lib/rack/etag.rb +17 -23
  28. data/lib/rack/events.rb +23 -20
  29. data/lib/rack/file.rb +5 -172
  30. data/lib/rack/files.rb +216 -0
  31. data/lib/rack/head.rb +10 -9
  32. data/lib/rack/headers.rb +154 -0
  33. data/lib/rack/lint.rb +786 -645
  34. data/lib/rack/lock.rb +4 -6
  35. data/lib/rack/logger.rb +4 -0
  36. data/lib/rack/media_type.rb +10 -5
  37. data/lib/rack/method_override.rb +8 -2
  38. data/lib/rack/mime.rb +17 -1
  39. data/lib/rack/mock.rb +2 -195
  40. data/lib/rack/mock_request.rb +166 -0
  41. data/lib/rack/mock_response.rb +126 -0
  42. data/lib/rack/multipart/generator.rb +21 -15
  43. data/lib/rack/multipart/parser.rb +161 -118
  44. data/lib/rack/multipart/uploaded_file.rb +19 -7
  45. data/lib/rack/multipart.rb +23 -41
  46. data/lib/rack/null_logger.rb +11 -0
  47. data/lib/rack/query_parser.rb +126 -65
  48. data/lib/rack/recursive.rb +9 -5
  49. data/lib/rack/reloader.rb +6 -4
  50. data/lib/rack/request.rb +331 -74
  51. data/lib/rack/response.rb +223 -70
  52. data/lib/rack/rewindable_input.rb +28 -8
  53. data/lib/rack/runtime.rb +11 -8
  54. data/lib/rack/sendfile.rb +42 -33
  55. data/lib/rack/show_exceptions.rb +35 -18
  56. data/lib/rack/show_status.rb +25 -15
  57. data/lib/rack/static.rb +30 -18
  58. data/lib/rack/tempfile_reaper.rb +16 -5
  59. data/lib/rack/urlmap.rb +14 -6
  60. data/lib/rack/utils.rb +268 -260
  61. data/lib/rack/version.rb +34 -0
  62. data/lib/rack.rb +15 -92
  63. metadata +44 -207
  64. data/HISTORY.md +0 -520
  65. data/README.rdoc +0 -316
  66. data/Rakefile +0 -116
  67. data/SPEC +0 -263
  68. data/bin/rackup +0 -4
  69. data/contrib/rack.png +0 -0
  70. data/contrib/rack.svg +0 -150
  71. data/contrib/rack_logo.svg +0 -164
  72. data/contrib/rdoc.css +0 -412
  73. data/example/lobster.ru +0 -4
  74. data/example/protectedlobster.rb +0 -14
  75. data/example/protectedlobster.ru +0 -8
  76. data/lib/rack/handler/cgi.rb +0 -60
  77. data/lib/rack/handler/fastcgi.rb +0 -100
  78. data/lib/rack/handler/lsws.rb +0 -61
  79. data/lib/rack/handler/scgi.rb +0 -70
  80. data/lib/rack/handler/thin.rb +0 -36
  81. data/lib/rack/handler/webrick.rb +0 -120
  82. data/lib/rack/handler.rb +0 -99
  83. data/lib/rack/lobster.rb +0 -70
  84. data/lib/rack/server.rb +0 -395
  85. data/lib/rack/session/abstract/id.rb +0 -510
  86. data/lib/rack/session/cookie.rb +0 -204
  87. data/lib/rack/session/memcache.rb +0 -99
  88. data/lib/rack/session/pool.rb +0 -83
  89. data/rack.gemspec +0 -34
  90. data/test/builder/an_underscore_app.rb +0 -5
  91. data/test/builder/anything.rb +0 -5
  92. data/test/builder/comment.ru +0 -4
  93. data/test/builder/end.ru +0 -5
  94. data/test/builder/line.ru +0 -1
  95. data/test/builder/options.ru +0 -2
  96. data/test/cgi/assets/folder/test.js +0 -1
  97. data/test/cgi/assets/fonts/font.eot +0 -1
  98. data/test/cgi/assets/images/image.png +0 -1
  99. data/test/cgi/assets/index.html +0 -1
  100. data/test/cgi/assets/javascripts/app.js +0 -1
  101. data/test/cgi/assets/stylesheets/app.css +0 -1
  102. data/test/cgi/lighttpd.conf +0 -26
  103. data/test/cgi/rackup_stub.rb +0 -6
  104. data/test/cgi/sample_rackup.ru +0 -5
  105. data/test/cgi/test +0 -9
  106. data/test/cgi/test+directory/test+file +0 -1
  107. data/test/cgi/test.fcgi +0 -9
  108. data/test/cgi/test.gz +0 -0
  109. data/test/cgi/test.ru +0 -5
  110. data/test/gemloader.rb +0 -10
  111. data/test/helper.rb +0 -34
  112. data/test/multipart/bad_robots +0 -259
  113. data/test/multipart/binary +0 -0
  114. data/test/multipart/content_type_and_no_filename +0 -6
  115. data/test/multipart/empty +0 -10
  116. data/test/multipart/fail_16384_nofile +0 -814
  117. data/test/multipart/file1.txt +0 -1
  118. data/test/multipart/filename_and_modification_param +0 -7
  119. data/test/multipart/filename_and_no_name +0 -6
  120. data/test/multipart/filename_with_encoded_words +0 -7
  121. data/test/multipart/filename_with_escaped_quotes +0 -6
  122. data/test/multipart/filename_with_escaped_quotes_and_modification_param +0 -7
  123. data/test/multipart/filename_with_null_byte +0 -7
  124. data/test/multipart/filename_with_percent_escaped_quotes +0 -6
  125. data/test/multipart/filename_with_single_quote +0 -7
  126. data/test/multipart/filename_with_unescaped_percentages +0 -6
  127. data/test/multipart/filename_with_unescaped_percentages2 +0 -6
  128. data/test/multipart/filename_with_unescaped_percentages3 +0 -6
  129. data/test/multipart/filename_with_unescaped_quotes +0 -6
  130. data/test/multipart/ie +0 -6
  131. data/test/multipart/invalid_character +0 -6
  132. data/test/multipart/mixed_files +0 -21
  133. data/test/multipart/nested +0 -10
  134. data/test/multipart/none +0 -9
  135. data/test/multipart/quoted +0 -15
  136. data/test/multipart/rack-logo.png +0 -0
  137. data/test/multipart/semicolon +0 -6
  138. data/test/multipart/text +0 -15
  139. data/test/multipart/three_files_three_fields +0 -31
  140. data/test/multipart/unity3d_wwwform +0 -11
  141. data/test/multipart/webkit +0 -32
  142. data/test/rackup/config.ru +0 -31
  143. data/test/registering_handler/rack/handler/registering_myself.rb +0 -8
  144. data/test/spec_auth_basic.rb +0 -89
  145. data/test/spec_auth_digest.rb +0 -260
  146. data/test/spec_body_proxy.rb +0 -85
  147. data/test/spec_builder.rb +0 -233
  148. data/test/spec_cascade.rb +0 -63
  149. data/test/spec_cgi.rb +0 -84
  150. data/test/spec_chunked.rb +0 -103
  151. data/test/spec_common_logger.rb +0 -107
  152. data/test/spec_conditional_get.rb +0 -103
  153. data/test/spec_config.rb +0 -23
  154. data/test/spec_content_length.rb +0 -86
  155. data/test/spec_content_type.rb +0 -46
  156. data/test/spec_deflater.rb +0 -375
  157. data/test/spec_directory.rb +0 -148
  158. data/test/spec_etag.rb +0 -108
  159. data/test/spec_events.rb +0 -133
  160. data/test/spec_fastcgi.rb +0 -85
  161. data/test/spec_file.rb +0 -264
  162. data/test/spec_handler.rb +0 -57
  163. data/test/spec_head.rb +0 -46
  164. data/test/spec_lint.rb +0 -520
  165. data/test/spec_lobster.rb +0 -59
  166. data/test/spec_lock.rb +0 -204
  167. data/test/spec_logger.rb +0 -24
  168. data/test/spec_media_type.rb +0 -42
  169. data/test/spec_method_override.rb +0 -110
  170. data/test/spec_mime.rb +0 -51
  171. data/test/spec_mock.rb +0 -359
  172. data/test/spec_multipart.rb +0 -721
  173. data/test/spec_null_logger.rb +0 -21
  174. data/test/spec_recursive.rb +0 -75
  175. data/test/spec_request.rb +0 -1423
  176. data/test/spec_response.rb +0 -528
  177. data/test/spec_rewindable_input.rb +0 -128
  178. data/test/spec_runtime.rb +0 -50
  179. data/test/spec_sendfile.rb +0 -125
  180. data/test/spec_server.rb +0 -193
  181. data/test/spec_session_abstract_id.rb +0 -31
  182. data/test/spec_session_abstract_session_hash.rb +0 -45
  183. data/test/spec_session_cookie.rb +0 -442
  184. data/test/spec_session_memcache.rb +0 -357
  185. data/test/spec_session_persisted_secure_secure_session_hash.rb +0 -73
  186. data/test/spec_session_pool.rb +0 -247
  187. data/test/spec_show_exceptions.rb +0 -93
  188. data/test/spec_show_status.rb +0 -104
  189. data/test/spec_static.rb +0 -184
  190. data/test/spec_tempfile_reaper.rb +0 -64
  191. data/test/spec_thin.rb +0 -96
  192. data/test/spec_urlmap.rb +0 -237
  193. data/test/spec_utils.rb +0 -742
  194. data/test/spec_version.rb +0 -11
  195. data/test/spec_webrick.rb +0 -206
  196. data/test/static/another/index.html +0 -1
  197. data/test/static/foo.html +0 -1
  198. data/test/static/index.html +0 -1
  199. data/test/testrequest.rb +0 -78
  200. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  201. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/utils.rb CHANGED
@@ -1,11 +1,17 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
3
+
2
4
  require 'uri'
3
5
  require 'fileutils'
4
6
  require 'set'
5
7
  require 'tempfile'
6
- require 'rack/query_parser'
7
8
  require 'time'
8
9
 
10
+ require_relative 'query_parser'
11
+ require_relative 'mime'
12
+ require_relative 'headers'
13
+ require_relative 'constants'
14
+
9
15
  module Rack
10
16
  # Rack::Utils contains a grab-bag of useful methods for writing web
11
17
  # applications adopted from all kinds of Ruby libraries.
@@ -13,6 +19,7 @@ module Rack
13
19
  module Utils
14
20
  ParameterTypeError = QueryParser::ParameterTypeError
15
21
  InvalidParameterError = QueryParser::InvalidParameterError
22
+ ParamsTooDeepError = QueryParser::ParamsTooDeepError
16
23
  DEFAULT_SEP = QueryParser::DEFAULT_SEP
17
24
  COMMON_SEP = QueryParser::COMMON_SEP
18
25
  KeySpaceConstrainedParams = QueryParser::Params
@@ -20,57 +27,44 @@ module Rack
20
27
  class << self
21
28
  attr_accessor :default_query_parser
22
29
  end
23
- # The default number of bytes to allow parameter keys to take up.
24
- # This helps prevent a rogue client from flooding a Request.
25
- self.default_query_parser = QueryParser.make_default(65536, 100)
30
+ # The default amount of nesting to allowed by hash parameters.
31
+ # This helps prevent a rogue client from triggering a possible stack overflow
32
+ # when parsing parameters.
33
+ self.default_query_parser = QueryParser.make_default(32)
34
+
35
+ module_function
26
36
 
27
37
  # URI escapes. (CGI style space to +)
28
38
  def escape(s)
29
39
  URI.encode_www_form_component(s)
30
40
  end
31
- module_function :escape
32
41
 
33
42
  # Like URI escaping, but with %20 instead of +. Strictly speaking this is
34
43
  # true URI escaping.
35
44
  def escape_path(s)
36
45
  ::URI::DEFAULT_PARSER.escape s
37
46
  end
38
- module_function :escape_path
39
47
 
40
48
  # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
41
49
  # unescaping query parameters or form components.
42
50
  def unescape_path(s)
43
51
  ::URI::DEFAULT_PARSER.unescape s
44
52
  end
45
- module_function :unescape_path
46
-
47
53
 
48
54
  # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
49
55
  # target encoding of the string returned, and it defaults to UTF-8
50
56
  def unescape(s, encoding = Encoding::UTF_8)
51
57
  URI.decode_www_form_component(s, encoding)
52
58
  end
53
- module_function :unescape
54
59
 
55
60
  class << self
56
- attr_accessor :multipart_total_part_limit
57
-
58
- attr_accessor :multipart_file_limit
59
-
60
- # multipart_part_limit is the original name of multipart_file_limit, but
61
- # the limit only counts parts with filenames.
62
- alias multipart_part_limit multipart_file_limit
63
- alias multipart_part_limit= multipart_file_limit=
61
+ attr_accessor :multipart_part_limit
64
62
  end
65
63
 
66
- # The maximum number of file parts a request can contain. Accepting too
67
- # many parts can lead to the server running out of file handles.
64
+ # The maximum number of parts a request can contain. Accepting too many part
65
+ # can lead to the server running out of file handles.
68
66
  # Set to `0` for no limit.
69
- self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
70
-
71
- # The maximum total number of parts a request can contain. Accepting too
72
- # many can lead to excessive memory use and parsing time.
73
- self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
67
+ self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
74
68
 
75
69
  def self.param_depth_limit
76
70
  default_query_parser.param_depth_limit
@@ -81,11 +75,12 @@ module Rack
81
75
  end
82
76
 
83
77
  def self.key_space_limit
84
- default_query_parser.key_space_limit
78
+ warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
79
+ 65536
85
80
  end
86
81
 
87
82
  def self.key_space_limit=(v)
88
- self.default_query_parser = self.default_query_parser.new_space_limit(v)
83
+ warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
89
84
  end
90
85
 
91
86
  if defined?(Process::CLOCK_MONOTONIC)
@@ -93,21 +88,20 @@ module Rack
93
88
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
94
89
  end
95
90
  else
91
+ # :nocov:
96
92
  def clock_time
97
93
  Time.now.to_f
98
94
  end
95
+ # :nocov:
99
96
  end
100
- module_function :clock_time
101
97
 
102
98
  def parse_query(qs, d = nil, &unescaper)
103
99
  Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
104
100
  end
105
- module_function :parse_query
106
101
 
107
102
  def parse_nested_query(qs, d = nil)
108
103
  Rack::Utils.default_query_parser.parse_nested_query(qs, d)
109
104
  end
110
- module_function :parse_nested_query
111
105
 
112
106
  def build_query(params)
113
107
  params.map { |k, v|
@@ -118,7 +112,6 @@ module Rack
118
112
  end
119
113
  }.join("&")
120
114
  end
121
- module_function :build_query
122
115
 
123
116
  def build_nested_query(value, prefix = nil)
124
117
  case value
@@ -129,7 +122,7 @@ module Rack
129
122
  when Hash
130
123
  value.map { |k, v|
131
124
  build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
132
- }.reject(&:empty?).join('&')
125
+ }.delete_if(&:empty?).join('&')
133
126
  when nil
134
127
  prefix
135
128
  else
@@ -137,20 +130,35 @@ module Rack
137
130
  "#{prefix}=#{escape(value)}"
138
131
  end
139
132
  end
140
- module_function :build_nested_query
141
133
 
142
134
  def q_values(q_value_header)
143
135
  q_value_header.to_s.split(/\s*,\s*/).map do |part|
144
136
  value, parameters = part.split(/\s*;\s*/, 2)
145
137
  quality = 1.0
146
- if md = /\Aq=([\d.]+)/.match(parameters)
138
+ if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
147
139
  quality = md[1].to_f
148
140
  end
149
141
  [value, quality]
150
142
  end
151
143
  end
152
- module_function :q_values
153
144
 
145
+ def forwarded_values(forwarded_header)
146
+ return nil unless forwarded_header
147
+ forwarded_header = forwarded_header.to_s.gsub("\n", ";")
148
+
149
+ forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
150
+ field.split(/\s*,\s*/).each do |pair|
151
+ return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
152
+ (values[$1.downcase.to_sym] ||= []) << $2
153
+ end
154
+ end
155
+ end
156
+ module_function :forwarded_values
157
+
158
+ # Return best accept value to use, based on the algorithm
159
+ # in RFC 2616 Section 14. If there are multiple best
160
+ # matches (same specificity and quality), the value returned
161
+ # is arbitrary.
154
162
  def best_q_match(q_value_header, available_mimes)
155
163
  values = q_values(q_value_header)
156
164
 
@@ -161,9 +169,8 @@ module Rack
161
169
  end.compact.sort_by do |match, quality|
162
170
  (match.split('/', 2).count('*') * -10) + quality
163
171
  end.last
164
- matches && matches.first
172
+ matches&.first
165
173
  end
166
- module_function :best_q_match
167
174
 
168
175
  ESCAPE_HTML = {
169
176
  "&" => "&amp;",
@@ -180,243 +187,293 @@ module Rack
180
187
  def escape_html(string)
181
188
  string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
182
189
  end
183
- module_function :escape_html
184
190
 
185
191
  def select_best_encoding(available_encodings, accept_encoding)
186
192
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
187
193
 
188
- expanded_accept_encoding =
189
- accept_encoding.map { |m, q|
190
- if m == "*"
191
- (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
192
- else
193
- [[m, q]]
194
+ expanded_accept_encoding = []
195
+
196
+ accept_encoding.each do |m, q|
197
+ preference = available_encodings.index(m) || available_encodings.size
198
+
199
+ if m == "*"
200
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
201
+ expanded_accept_encoding << [m2, q, preference]
194
202
  end
195
- }.inject([]) { |mem, list|
196
- mem + list
197
- }
203
+ else
204
+ expanded_accept_encoding << [m, q, preference]
205
+ end
206
+ end
198
207
 
199
- encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
208
+ encoding_candidates = expanded_accept_encoding
209
+ .sort_by { |_, q, p| [-q, p] }
210
+ .map!(&:first)
200
211
 
201
212
  unless encoding_candidates.include?("identity")
202
213
  encoding_candidates.push("identity")
203
214
  end
204
215
 
205
- expanded_accept_encoding.each { |m, q|
216
+ expanded_accept_encoding.each do |m, q|
206
217
  encoding_candidates.delete(m) if q == 0.0
207
- }
218
+ end
208
219
 
209
- return (encoding_candidates & available_encodings)[0]
220
+ (encoding_candidates & available_encodings)[0]
210
221
  end
211
- module_function :select_best_encoding
212
222
 
213
- def parse_cookies(env)
214
- parse_cookies_header env[HTTP_COOKIE]
215
- end
216
- module_function :parse_cookies
223
+ # :call-seq:
224
+ # parse_cookies_header(value) -> hash
225
+ #
226
+ # Parse cookies from the provided header +value+ according to RFC6265. The
227
+ # syntax for cookie headers only supports semicolons. Returns a map of
228
+ # cookie +key+ to cookie +value+.
229
+ #
230
+ # parse_cookies_header('myname=myvalue; max-age=0')
231
+ # # => {"myname"=>"myvalue", "max-age"=>"0"}
232
+ #
233
+ def parse_cookies_header(value)
234
+ return {} unless value
217
235
 
218
- def parse_cookies_header(header)
219
- # According to RFC 2109:
220
- # If multiple cookies satisfy the criteria above, they are ordered in
221
- # the Cookie header such that those with more specific Path attributes
222
- # precede those with less specific. Ordering with respect to other
223
- # attributes (e.g., Domain) is unspecified.
224
- cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
225
- cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v }
236
+ value.split(/; */n).each_with_object({}) do |cookie, cookies|
237
+ next if cookie.empty?
238
+ key, value = cookie.split('=', 2)
239
+ cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
240
+ end
226
241
  end
227
- module_function :parse_cookies_header
228
242
 
229
243
  def add_cookie_to_header(header, key, value)
244
+ warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
245
+
246
+ case header
247
+ when nil, ''
248
+ return set_cookie_header(key, value)
249
+ when String
250
+ [header, set_cookie_header(key, value)]
251
+ when Array
252
+ header + [set_cookie_header(key, value)]
253
+ else
254
+ raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
255
+ end
256
+ end
257
+
258
+ # :call-seq:
259
+ # parse_cookies(env) -> hash
260
+ #
261
+ # Parse cookies from the provided request environment using
262
+ # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
263
+ #
264
+ # parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
265
+ # # => {'myname' => 'myvalue'}
266
+ #
267
+ def parse_cookies(env)
268
+ parse_cookies_header env[HTTP_COOKIE]
269
+ end
270
+
271
+ # :call-seq:
272
+ # set_cookie_header(key, value) -> encoded string
273
+ #
274
+ # Generate an encoded string using the provided +key+ and +value+ suitable
275
+ # for the +set-cookie+ header according to RFC6265. The +value+ may be an
276
+ # instance of either +String+ or +Hash+.
277
+ #
278
+ # If the cookie +value+ is an instance of +Hash+, it considers the following
279
+ # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
280
+ # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
281
+ # details about the interpretation of these fields, consult
282
+ # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
283
+ #
284
+ # An extra cookie attribute +escape_key+ can be provided to control whether
285
+ # or not the cookie key is URL encoded. If explicitly set to +false+, the
286
+ # cookie key name will not be url encoded (escaped). The default is +true+.
287
+ #
288
+ # set_cookie_header("myname", "myvalue")
289
+ # # => "myname=myvalue"
290
+ #
291
+ # set_cookie_header("myname", {value: "myvalue", max_age: 10})
292
+ # # => "myname=myvalue; max-age=10"
293
+ #
294
+ def set_cookie_header(key, value)
230
295
  case value
231
296
  when Hash
297
+ key = escape(key) unless value[:escape_key] == false
232
298
  domain = "; domain=#{value[:domain]}" if value[:domain]
233
299
  path = "; path=#{value[:path]}" if value[:path]
234
300
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
235
- # There is an RFC mess in the area of date formatting for Cookies. Not
236
- # only are there contradicting RFCs and examples within RFC text, but
237
- # there are also numerous conflicting names of fields and partially
238
- # cross-applicable specifications.
239
- #
240
- # These are best described in RFC 2616 3.3.1. This RFC text also
241
- # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
242
- # fixed length format with space-date delimited fields.
243
- #
244
- # See also RFC 1123 section 5.2.14.
245
- #
246
- # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
247
- # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
248
- # the space delimited format. These formats are compliant with RFC 2822.
249
- #
250
- # For reference, all involved RFCs are:
251
- # RFC 822
252
- # RFC 1123
253
- # RFC 2109
254
- # RFC 2616
255
- # RFC 2822
256
- # RFC 2965
257
- # RFC 6265
258
- expires = "; expires=" +
259
- rfc2822(value[:expires].clone.gmtime) if value[:expires]
301
+ expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
260
302
  secure = "; secure" if value[:secure]
261
- httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
303
+ httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
262
304
  same_site =
263
305
  case value[:same_site]
264
306
  when false, nil
265
307
  nil
266
308
  when :none, 'None', :None
267
- '; SameSite=None'.freeze
309
+ '; SameSite=None'
268
310
  when :lax, 'Lax', :Lax
269
- '; SameSite=Lax'.freeze
311
+ '; SameSite=Lax'
270
312
  when true, :strict, 'Strict', :Strict
271
- '; SameSite=Strict'.freeze
313
+ '; SameSite=Strict'
272
314
  else
273
315
  raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
274
316
  end
275
317
  value = value[:value]
318
+ else
319
+ key = escape(key)
276
320
  end
321
+
277
322
  value = [value] unless Array === value
278
323
 
279
- cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
324
+ return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
280
325
  "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
326
+ end
281
327
 
282
- case header
283
- when nil, ''
284
- cookie
285
- when String
286
- [header, cookie].join("\n")
287
- when Array
288
- (header + [cookie]).join("\n")
328
+ # :call-seq:
329
+ # set_cookie_header!(headers, key, value) -> header value
330
+ #
331
+ # Append a cookie in the specified headers with the given cookie +key+ and
332
+ # +value+ using set_cookie_header.
333
+ #
334
+ # If the headers already contains a +set-cookie+ key, it will be converted
335
+ # to an +Array+ if not already, and appended to.
336
+ def set_cookie_header!(headers, key, value)
337
+ if header = headers[SET_COOKIE]
338
+ if header.is_a?(Array)
339
+ header << set_cookie_header(key, value)
340
+ else
341
+ headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
342
+ end
289
343
  else
290
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
344
+ headers[SET_COOKIE] = set_cookie_header(key, value)
291
345
  end
292
346
  end
293
- module_function :add_cookie_to_header
294
347
 
295
- def set_cookie_header!(header, key, value)
296
- header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
297
- nil
348
+ # :call-seq:
349
+ # delete_set_cookie_header(key, value = {}) -> encoded string
350
+ #
351
+ # Generate an encoded string based on the given +key+ and +value+ using
352
+ # set_cookie_header for the purpose of causing the specified cookie to be
353
+ # deleted. The +value+ may be an instance of +Hash+ and can include
354
+ # attributes as outlined by set_cookie_header. The encoded cookie will have
355
+ # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
356
+ # +value+. When used with the +set-cookie+ header, it will cause the client
357
+ # to *remove* any matching cookie.
358
+ #
359
+ # delete_set_cookie_header("myname")
360
+ # # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
361
+ #
362
+ def delete_set_cookie_header(key, value = {})
363
+ set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
298
364
  end
299
- module_function :set_cookie_header!
300
365
 
301
366
  def make_delete_cookie_header(header, key, value)
302
- case header
303
- when nil, ''
304
- cookies = []
305
- when String
306
- cookies = header.split("\n")
307
- when Array
308
- cookies = header
309
- end
310
-
311
- cookies.reject! { |cookie|
312
- if value[:domain]
313
- cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
314
- elsif value[:path]
315
- cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
316
- else
317
- cookie =~ /\A#{escape(key)}=/
318
- end
319
- }
367
+ warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
320
368
 
321
- cookies.join("\n")
369
+ delete_set_cookie_header!(header, key, value)
322
370
  end
323
- module_function :make_delete_cookie_header
324
371
 
325
- def delete_cookie_header!(header, key, value = {})
326
- header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
327
- nil
372
+ def delete_cookie_header!(headers, key, value = {})
373
+ headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
374
+
375
+ return nil
328
376
  end
329
- module_function :delete_cookie_header!
330
377
 
331
- # Adds a cookie that will *remove* a cookie from the client. Hence the
332
- # strange method name.
333
378
  def add_remove_cookie_to_header(header, key, value = {})
334
- new_header = make_delete_cookie_header(header, key, value)
379
+ warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
380
+
381
+ delete_set_cookie_header!(header, key, value)
382
+ end
335
383
 
336
- add_cookie_to_header(new_header, key,
337
- {:value => '', :path => nil, :domain => nil,
338
- :max_age => '0',
339
- :expires => Time.at(0) }.merge(value))
384
+ # :call-seq:
385
+ # delete_set_cookie_header!(header, key, value = {}) -> header value
386
+ #
387
+ # Set an expired cookie in the specified headers with the given cookie
388
+ # +key+ and +value+ using delete_set_cookie_header. This causes
389
+ # the client to immediately delete the specified cookie.
390
+ #
391
+ # delete_set_cookie_header!(nil, "mycookie")
392
+ # # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
393
+ #
394
+ # If the header is non-nil, it will be modified in place.
395
+ #
396
+ # header = []
397
+ # delete_set_cookie_header!(header, "mycookie")
398
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
399
+ # header
400
+ # # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
401
+ #
402
+ def delete_set_cookie_header!(header, key, value = {})
403
+ if header
404
+ header = Array(header)
405
+ header << delete_set_cookie_header(key, value)
406
+ else
407
+ header = delete_set_cookie_header(key, value)
408
+ end
340
409
 
410
+ return header
341
411
  end
342
- module_function :add_remove_cookie_to_header
343
412
 
344
413
  def rfc2822(time)
345
414
  time.rfc2822
346
415
  end
347
- module_function :rfc2822
348
-
349
- # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
350
- # of '% %b %Y'.
351
- # It assumes that the time is in GMT to comply to the RFC 2109.
352
- #
353
- # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
354
- # that I'm certain someone implemented only that option.
355
- # Do not use %a and %b from Time.strptime, it would use localized names for
356
- # weekday and month.
357
- #
358
- def rfc2109(time)
359
- wday = Time::RFC2822_DAY_NAME[time.wday]
360
- mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
361
- time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
362
- end
363
- module_function :rfc2109
364
416
 
365
417
  # Parses the "Range:" header, if present, into an array of Range objects.
366
418
  # Returns nil if the header is missing or syntactically invalid.
367
419
  # Returns an empty array if none of the ranges are satisfiable.
368
420
  def byte_ranges(env, size)
369
- warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
370
421
  get_byte_ranges env['HTTP_RANGE'], size
371
422
  end
372
- module_function :byte_ranges
373
423
 
374
424
  def get_byte_ranges(http_range, size)
375
425
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
376
426
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
377
427
  ranges = []
378
428
  $1.split(/,\s*/).each do |range_spec|
379
- return nil unless range_spec.include?('-')
380
- range = range_spec.split('-')
381
- r0, r1 = range[0], range[1]
382
- if r0.nil? || r0.empty?
383
- return nil if r1.nil?
429
+ return nil unless range_spec =~ /(\d*)-(\d*)/
430
+ r0, r1 = $1, $2
431
+ if r0.empty?
432
+ return nil if r1.empty?
384
433
  # suffix-byte-range-spec, represents trailing suffix of file
385
434
  r0 = size - r1.to_i
386
435
  r0 = 0 if r0 < 0
387
436
  r1 = size - 1
388
437
  else
389
438
  r0 = r0.to_i
390
- if r1.nil?
439
+ if r1.empty?
391
440
  r1 = size - 1
392
441
  else
393
442
  r1 = r1.to_i
394
443
  return nil if r1 < r0 # backwards range is syntactically invalid
395
- r1 = size-1 if r1 >= size
444
+ r1 = size - 1 if r1 >= size
396
445
  end
397
446
  end
398
447
  ranges << (r0..r1) if r0 <= r1
399
448
  end
400
449
  ranges
401
450
  end
402
- module_function :get_byte_ranges
403
451
 
404
- # Constant time string comparison.
405
- #
406
- # NOTE: the values compared should be of fixed length, such as strings
407
- # that have already been processed by HMAC. This should not be used
408
- # on variable length plaintext strings because it could leak length info
409
- # via timing attacks.
410
- def secure_compare(a, b)
411
- return false unless a.bytesize == b.bytesize
452
+ # :nocov:
453
+ if defined?(OpenSSL.fixed_length_secure_compare)
454
+ # Constant time string comparison.
455
+ #
456
+ # NOTE: the values compared should be of fixed length, such as strings
457
+ # that have already been processed by HMAC. This should not be used
458
+ # on variable length plaintext strings because it could leak length info
459
+ # via timing attacks.
460
+ def secure_compare(a, b)
461
+ return false unless a.bytesize == b.bytesize
412
462
 
413
- l = a.unpack("C*")
463
+ OpenSSL.fixed_length_secure_compare(a, b)
464
+ end
465
+ # :nocov:
466
+ else
467
+ def secure_compare(a, b)
468
+ return false unless a.bytesize == b.bytesize
414
469
 
415
- r, i = 0, -1
416
- b.each_byte { |v| r |= v ^ l[i+=1] }
417
- r == 0
470
+ l = a.unpack("C*")
471
+
472
+ r, i = 0, -1
473
+ b.each_byte { |v| r |= v ^ l[i += 1] }
474
+ r == 0
475
+ end
418
476
  end
419
- module_function :secure_compare
420
477
 
421
478
  # Context allows the use of a compatible middleware at different points
422
479
  # in a request handling stack. A compatible middleware must define
@@ -439,98 +496,49 @@ module Rack
439
496
  self.class.new(@for, app)
440
497
  end
441
498
 
442
- def context(env, app=@app)
499
+ def context(env, app = @app)
443
500
  recontext(app).call(env)
444
501
  end
445
502
  end
446
503
 
447
- # A case-insensitive Hash that preserves the original case of a
504
+ # A wrapper around Headers
448
505
  # header when set.
449
- class HeaderHash < Hash
450
- def self.new(hash={})
451
- HeaderHash === hash ? hash : super(hash)
452
- end
453
-
454
- def initialize(hash={})
455
- super()
456
- @names = {}
457
- hash.each { |k, v| self[k] = v }
458
- end
459
-
460
- # on dup/clone, we need to duplicate @names hash
461
- def initialize_copy(other)
462
- super
463
- @names = other.names.dup
464
- end
465
-
466
- def each
467
- super do |k, v|
468
- yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
506
+ #
507
+ # @api private
508
+ class HeaderHash < Hash # :nodoc:
509
+ def self.[](headers)
510
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
511
+ if headers.is_a?(Headers) && !headers.frozen?
512
+ return headers
469
513
  end
470
- end
471
514
 
472
- def to_hash
473
- hash = {}
474
- each { |k,v| hash[k] = v }
475
- hash
515
+ new_headers = Headers.new
516
+ headers.each{|k,v| new_headers[k] = v}
517
+ new_headers
476
518
  end
477
519
 
478
- def [](k)
479
- super(k) || super(@names[k.downcase])
520
+ def self.new(hash = {})
521
+ warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
522
+ headers = Headers.new
523
+ hash.each{|k,v| headers[k] = v}
524
+ headers
480
525
  end
481
526
 
482
- def []=(k, v)
483
- canonical = k.downcase.freeze
484
- delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
485
- @names[canonical] = k
486
- super k, v
527
+ def self.allocate
528
+ raise TypeError, "cannot allocate HeaderHash"
487
529
  end
488
-
489
- def delete(k)
490
- canonical = k.downcase
491
- result = super @names.delete(canonical)
492
- result
493
- end
494
-
495
- def include?(k)
496
- super || @names.include?(k.downcase)
497
- end
498
-
499
- alias_method :has_key?, :include?
500
- alias_method :member?, :include?
501
- alias_method :key?, :include?
502
-
503
- def merge!(other)
504
- other.each { |k, v| self[k] = v }
505
- self
506
- end
507
-
508
- def merge(other)
509
- hash = dup
510
- hash.merge! other
511
- end
512
-
513
- def replace(other)
514
- clear
515
- other.each { |k, v| self[k] = v }
516
- self
517
- end
518
-
519
- protected
520
- def names
521
- @names
522
- end
523
530
  end
524
531
 
525
532
  # Every standard HTTP code mapped to the appropriate message.
526
533
  # Generated with:
527
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
528
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
529
- # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
534
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
535
+ # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
536
+ # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
530
537
  HTTP_STATUS_CODES = {
531
538
  100 => 'Continue',
532
539
  101 => 'Switching Protocols',
533
540
  102 => 'Processing',
541
+ 103 => 'Early Hints',
534
542
  200 => 'OK',
535
543
  201 => 'Created',
536
544
  202 => 'Accepted',
@@ -547,6 +555,7 @@ module Rack
547
555
  303 => 'See Other',
548
556
  304 => 'Not Modified',
549
557
  305 => 'Use Proxy',
558
+ 306 => '(Unused)',
550
559
  307 => 'Temporary Redirect',
551
560
  308 => 'Permanent Redirect',
552
561
  400 => 'Bad Request',
@@ -571,6 +580,7 @@ module Rack
571
580
  422 => 'Unprocessable Entity',
572
581
  423 => 'Locked',
573
582
  424 => 'Failed Dependency',
583
+ 425 => 'Too Early',
574
584
  426 => 'Upgrade Required',
575
585
  428 => 'Precondition Required',
576
586
  429 => 'Too Many Requests',
@@ -585,12 +595,13 @@ module Rack
585
595
  506 => 'Variant Also Negotiates',
586
596
  507 => 'Insufficient Storage',
587
597
  508 => 'Loop Detected',
598
+ 509 => 'Bandwidth Limit Exceeded',
588
599
  510 => 'Not Extended',
589
600
  511 => 'Network Authentication Required'
590
601
  }
591
602
 
592
603
  # Responses with HTTP status codes that should not have an entity body
593
- STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
604
+ STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
594
605
 
595
606
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
596
607
  [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
@@ -598,12 +609,11 @@ module Rack
598
609
 
599
610
  def status_code(status)
600
611
  if status.is_a?(Symbol)
601
- SYMBOL_TO_STATUS_CODE[status] || 500
612
+ SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
602
613
  else
603
614
  status.to_i
604
615
  end
605
616
  end
606
- module_function :status_code
607
617
 
608
618
  PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
609
619
 
@@ -617,18 +627,16 @@ module Rack
617
627
  part == '..' ? clean.pop : clean << part
618
628
  end
619
629
 
620
- clean.unshift '/' if parts.empty? || parts.first.empty?
621
-
622
- ::File.join(*clean)
630
+ clean_path = clean.join(::File::SEPARATOR)
631
+ clean_path.prepend("/") if parts.empty? || parts.first.empty?
632
+ clean_path
623
633
  end
624
- module_function :clean_path_info
625
634
 
626
- NULL_BYTE = "\0".freeze
635
+ NULL_BYTE = "\0"
627
636
 
628
637
  def valid_path?(path)
629
638
  path.valid_encoding? && !path.include?(NULL_BYTE)
630
639
  end
631
- module_function :valid_path?
632
640
 
633
641
  end
634
642
  end