rack 1.0.1 → 2.2.21

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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +829 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +355 -0
  6. data/Rakefile +64 -98
  7. data/SPEC.rdoc +292 -0
  8. data/bin/rackup +3 -174
  9. data/contrib/rack.png +0 -0
  10. data/contrib/rack.svg +150 -0
  11. data/contrib/rack_logo.svg +164 -111
  12. data/contrib/rdoc.css +412 -0
  13. data/example/lobster.ru +2 -0
  14. data/example/protectedlobster.rb +4 -2
  15. data/example/protectedlobster.ru +3 -1
  16. data/lib/rack/auth/abstract/handler.rb +7 -5
  17. data/lib/rack/auth/abstract/request.rb +12 -2
  18. data/lib/rack/auth/basic.rb +6 -4
  19. data/lib/rack/auth/digest/md5.rb +20 -13
  20. data/lib/rack/auth/digest/nonce.rb +6 -4
  21. data/lib/rack/auth/digest/params.rb +11 -12
  22. data/lib/rack/auth/digest/request.rb +15 -12
  23. data/lib/rack/body_proxy.rb +45 -0
  24. data/lib/rack/builder.rb +213 -19
  25. data/lib/rack/cascade.rb +50 -18
  26. data/lib/rack/chunked.rb +95 -27
  27. data/lib/rack/common_logger.rb +84 -0
  28. data/lib/rack/conditional_get.rb +83 -0
  29. data/lib/rack/config.rb +22 -0
  30. data/lib/rack/content_length.rb +19 -10
  31. data/lib/rack/content_type.rb +12 -5
  32. data/lib/rack/core_ext/regexp.rb +14 -0
  33. data/lib/rack/deflater.rb +90 -42
  34. data/lib/rack/directory.rb +118 -72
  35. data/lib/rack/etag.rb +77 -0
  36. data/lib/rack/events.rb +153 -0
  37. data/lib/rack/file.rb +4 -85
  38. data/lib/rack/files.rb +218 -0
  39. data/lib/rack/handler/cgi.rb +22 -24
  40. data/lib/rack/handler/fastcgi.rb +34 -24
  41. data/lib/rack/handler/lsws.rb +16 -15
  42. data/lib/rack/handler/scgi.rb +38 -29
  43. data/lib/rack/handler/thin.rb +23 -7
  44. data/lib/rack/handler/webrick.rb +95 -37
  45. data/lib/rack/handler.rb +49 -14
  46. data/lib/rack/head.rb +19 -13
  47. data/lib/rack/lint.rb +466 -217
  48. data/lib/rack/lobster.rb +19 -14
  49. data/lib/rack/lock.rb +24 -8
  50. data/lib/rack/logger.rb +20 -0
  51. data/lib/rack/media_type.rb +53 -0
  52. data/lib/rack/method_override.rb +52 -0
  53. data/lib/rack/mime.rb +652 -171
  54. data/lib/rack/mock.rb +203 -64
  55. data/lib/rack/multipart/generator.rb +97 -0
  56. data/lib/rack/multipart/parser.rb +440 -0
  57. data/lib/rack/multipart/uploaded_file.rb +41 -0
  58. data/lib/rack/multipart.rb +64 -0
  59. data/lib/rack/null_logger.rb +39 -0
  60. data/lib/rack/query_parser.rb +266 -0
  61. data/lib/rack/recursive.rb +18 -11
  62. data/lib/rack/reloader.rb +13 -5
  63. data/lib/rack/request.rb +607 -191
  64. data/lib/rack/response.rb +245 -110
  65. data/lib/rack/rewindable_input.rb +15 -21
  66. data/lib/rack/runtime.rb +34 -0
  67. data/lib/rack/sendfile.rb +192 -0
  68. data/lib/rack/server.rb +466 -0
  69. data/lib/rack/session/abstract/id.rb +449 -68
  70. data/lib/rack/session/cookie.rb +159 -47
  71. data/lib/rack/session/memcache.rb +4 -103
  72. data/lib/rack/session/pool.rb +39 -49
  73. data/lib/rack/show_exceptions.rb +390 -0
  74. data/lib/rack/{showstatus.rb → show_status.rb} +21 -14
  75. data/lib/rack/static.rb +164 -14
  76. data/lib/rack/tempfile_reaper.rb +22 -0
  77. data/lib/rack/urlmap.rb +60 -18
  78. data/lib/rack/utils.rb +508 -277
  79. data/lib/rack/version.rb +29 -0
  80. data/lib/rack.rb +85 -34
  81. data/rack.gemspec +43 -51
  82. metadata +119 -207
  83. data/KNOWN-ISSUES +0 -18
  84. data/RDOX +0 -428
  85. data/README +0 -364
  86. data/SPEC +0 -164
  87. data/lib/rack/adapter/camping.rb +0 -22
  88. data/lib/rack/auth/openid.rb +0 -480
  89. data/lib/rack/commonlogger.rb +0 -61
  90. data/lib/rack/conditionalget.rb +0 -47
  91. data/lib/rack/handler/evented_mongrel.rb +0 -8
  92. data/lib/rack/handler/mongrel.rb +0 -87
  93. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  94. data/lib/rack/methodoverride.rb +0 -27
  95. data/lib/rack/showexceptions.rb +0 -349
  96. data/test/cgi/lighttpd.conf +0 -20
  97. data/test/cgi/test +0 -9
  98. data/test/cgi/test.fcgi +0 -8
  99. data/test/cgi/test.ru +0 -7
  100. data/test/multipart/binary +0 -0
  101. data/test/multipart/empty +0 -10
  102. data/test/multipart/ie +0 -6
  103. data/test/multipart/nested +0 -10
  104. data/test/multipart/none +0 -9
  105. data/test/multipart/semicolon +0 -6
  106. data/test/multipart/text +0 -10
  107. data/test/spec_rack_auth_basic.rb +0 -73
  108. data/test/spec_rack_auth_digest.rb +0 -226
  109. data/test/spec_rack_auth_openid.rb +0 -84
  110. data/test/spec_rack_builder.rb +0 -84
  111. data/test/spec_rack_camping.rb +0 -51
  112. data/test/spec_rack_cascade.rb +0 -50
  113. data/test/spec_rack_cgi.rb +0 -89
  114. data/test/spec_rack_chunked.rb +0 -62
  115. data/test/spec_rack_commonlogger.rb +0 -32
  116. data/test/spec_rack_conditionalget.rb +0 -41
  117. data/test/spec_rack_content_length.rb +0 -43
  118. data/test/spec_rack_content_type.rb +0 -30
  119. data/test/spec_rack_deflater.rb +0 -127
  120. data/test/spec_rack_directory.rb +0 -61
  121. data/test/spec_rack_fastcgi.rb +0 -89
  122. data/test/spec_rack_file.rb +0 -75
  123. data/test/spec_rack_handler.rb +0 -43
  124. data/test/spec_rack_head.rb +0 -30
  125. data/test/spec_rack_lint.rb +0 -521
  126. data/test/spec_rack_lobster.rb +0 -45
  127. data/test/spec_rack_lock.rb +0 -38
  128. data/test/spec_rack_methodoverride.rb +0 -60
  129. data/test/spec_rack_mock.rb +0 -157
  130. data/test/spec_rack_mongrel.rb +0 -189
  131. data/test/spec_rack_recursive.rb +0 -77
  132. data/test/spec_rack_request.rb +0 -504
  133. data/test/spec_rack_response.rb +0 -218
  134. data/test/spec_rack_rewindable_input.rb +0 -118
  135. data/test/spec_rack_session_cookie.rb +0 -82
  136. data/test/spec_rack_session_memcache.rb +0 -240
  137. data/test/spec_rack_session_pool.rb +0 -172
  138. data/test/spec_rack_showexceptions.rb +0 -21
  139. data/test/spec_rack_showstatus.rb +0 -72
  140. data/test/spec_rack_static.rb +0 -37
  141. data/test/spec_rack_thin.rb +0 -91
  142. data/test/spec_rack_urlmap.rb +0 -185
  143. data/test/spec_rack_utils.rb +0 -387
  144. data/test/spec_rack_webrick.rb +0 -130
  145. data/test/testrequest.rb +0 -57
  146. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  147. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/utils.rb CHANGED
@@ -1,166 +1,407 @@
1
1
  # -*- encoding: binary -*-
2
+ # frozen_string_literal: true
2
3
 
4
+ require 'uri'
5
+ require 'fileutils'
3
6
  require 'set'
4
7
  require 'tempfile'
8
+ require 'time'
9
+
10
+ require_relative 'query_parser'
5
11
 
6
12
  module Rack
7
13
  # Rack::Utils contains a grab-bag of useful methods for writing web
8
14
  # applications adopted from all kinds of Ruby libraries.
9
15
 
10
16
  module Utils
11
- # Performs URI escaping so that you can construct proper
12
- # query strings faster. Use this rather than the cgi.rb
13
- # version since it's faster. (Stolen from Camping).
17
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
18
+
19
+ ParameterTypeError = QueryParser::ParameterTypeError
20
+ InvalidParameterError = QueryParser::InvalidParameterError
21
+ DEFAULT_SEP = QueryParser::DEFAULT_SEP
22
+ COMMON_SEP = QueryParser::COMMON_SEP
23
+ KeySpaceConstrainedParams = QueryParser::Params
24
+
25
+ RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
26
+ RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
27
+ RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
28
+
29
+ class << self
30
+ attr_accessor :default_query_parser
31
+ end
32
+ # The default number of bytes to allow parameter keys to take up.
33
+ # This helps prevent a rogue client from flooding a Request.
34
+ self.default_query_parser = QueryParser.make_default(65536, 100)
35
+
36
+ module_function
37
+
38
+ # URI escapes. (CGI style space to +)
14
39
  def escape(s)
15
- s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
16
- '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
17
- }.tr(' ', '+')
18
- end
19
- module_function :escape
20
-
21
- # Unescapes a URI escaped string. (Stolen from Camping).
22
- def unescape(s)
23
- s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
24
- [$1.delete('%')].pack('H*')
25
- }
26
- end
27
- module_function :unescape
28
-
29
- DEFAULT_SEP = /[&;] */n
30
-
31
- # Stolen from Mongrel, with some small modifications:
32
- # Parses a query string by breaking it up at the '&'
33
- # and ';' characters. You can also use this to parse
34
- # cookies by changing the characters used in the second
35
- # parameter (which defaults to '&;').
36
- def parse_query(qs, d = nil)
37
- params = {}
38
-
39
- (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
40
- k, v = p.split('=', 2).map { |x| unescape(x) }
41
-
42
- if cur = params[k]
43
- if cur.class == Array
44
- params[k] << v
45
- else
46
- params[k] = [cur, v]
47
- end
48
- else
49
- params[k] = v
50
- end
51
- end
40
+ URI.encode_www_form_component(s)
41
+ end
52
42
 
53
- return params
43
+ # Like URI escaping, but with %20 instead of +. Strictly speaking this is
44
+ # true URI escaping.
45
+ def escape_path(s)
46
+ RFC2396_PARSER.escape s
54
47
  end
55
- module_function :parse_query
56
48
 
57
- def parse_nested_query(qs, d = nil)
58
- params = {}
49
+ # Unescapes the **path** component of a URI. See Rack::Utils.unescape for
50
+ # unescaping query parameters or form components.
51
+ def unescape_path(s)
52
+ RFC2396_PARSER.unescape s
53
+ end
59
54
 
60
- (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
61
- k, v = unescape(p).split('=', 2)
62
- normalize_params(params, k, v)
63
- end
55
+ # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
56
+ # target encoding of the string returned, and it defaults to UTF-8
57
+ def unescape(s, encoding = Encoding::UTF_8)
58
+ URI.decode_www_form_component(s, encoding)
59
+ end
64
60
 
65
- return params
66
- end
67
- module_function :parse_nested_query
68
-
69
- def normalize_params(params, name, v = nil)
70
- name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
71
- k = $1 || ''
72
- after = $' || ''
73
-
74
- return if k.empty?
75
-
76
- if after == ""
77
- params[k] = v
78
- elsif after == "[]"
79
- params[k] ||= []
80
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
81
- params[k] << v
82
- elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
83
- child_key = $1
84
- params[k] ||= []
85
- raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
86
- if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
87
- normalize_params(params[k].last, child_key, v)
88
- else
89
- params[k] << normalize_params({}, child_key, v)
90
- end
91
- else
92
- params[k] ||= {}
93
- raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
94
- params[k] = normalize_params(params[k], after, v)
61
+ class << self
62
+ attr_accessor :multipart_total_part_limit
63
+
64
+ attr_accessor :multipart_file_limit
65
+
66
+ # multipart_part_limit is the original name of multipart_file_limit, but
67
+ # the limit only counts parts with filenames.
68
+ alias multipart_part_limit multipart_file_limit
69
+ alias multipart_part_limit= multipart_file_limit=
70
+ end
71
+
72
+ # The maximum number of file parts a request can contain. Accepting too
73
+ # many parts can lead to the server running out of file handles.
74
+ # Set to `0` for no limit.
75
+ self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
76
+
77
+ # The maximum total number of parts a request can contain. Accepting too
78
+ # many can lead to excessive memory use and parsing time.
79
+ self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
80
+
81
+ def self.param_depth_limit
82
+ default_query_parser.param_depth_limit
83
+ end
84
+
85
+ def self.param_depth_limit=(v)
86
+ self.default_query_parser = self.default_query_parser.new_depth_limit(v)
87
+ end
88
+
89
+ def self.key_space_limit
90
+ default_query_parser.key_space_limit
91
+ end
92
+
93
+ def self.key_space_limit=(v)
94
+ self.default_query_parser = self.default_query_parser.new_space_limit(v)
95
+ end
96
+
97
+ if defined?(Process::CLOCK_MONOTONIC)
98
+ def clock_time
99
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
95
100
  end
101
+ else
102
+ # :nocov:
103
+ def clock_time
104
+ Time.now.to_f
105
+ end
106
+ # :nocov:
107
+ end
96
108
 
97
- return params
109
+ def parse_query(qs, d = nil, &unescaper)
110
+ Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
111
+ end
112
+
113
+ def parse_nested_query(qs, d = nil)
114
+ Rack::Utils.default_query_parser.parse_nested_query(qs, d)
98
115
  end
99
- module_function :normalize_params
100
116
 
101
117
  def build_query(params)
102
118
  params.map { |k, v|
103
119
  if v.class == Array
104
120
  build_query(v.map { |x| [k, x] })
105
121
  else
106
- "#{escape(k)}=#{escape(v)}"
122
+ v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
107
123
  end
108
124
  }.join("&")
109
125
  end
110
- module_function :build_query
126
+
127
+ def build_nested_query(value, prefix = nil)
128
+ case value
129
+ when Array
130
+ value.map { |v|
131
+ build_nested_query(v, "#{prefix}[]")
132
+ }.join("&")
133
+ when Hash
134
+ value.map { |k, v|
135
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
136
+ }.delete_if(&:empty?).join('&')
137
+ when nil
138
+ prefix
139
+ else
140
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
141
+ "#{prefix}=#{escape(value)}"
142
+ end
143
+ end
144
+
145
+ def q_values(q_value_header)
146
+ q_value_header.to_s.split(',').map do |part|
147
+ value, parameters = part.split(';', 2).map(&:strip)
148
+ quality = 1.0
149
+ if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
150
+ quality = md[1].to_f
151
+ end
152
+ [value, quality]
153
+ end
154
+ end
155
+
156
+ # Return best accept value to use, based on the algorithm
157
+ # in RFC 2616 Section 14. If there are multiple best
158
+ # matches (same specificity and quality), the value returned
159
+ # is arbitrary.
160
+ def best_q_match(q_value_header, available_mimes)
161
+ values = q_values(q_value_header)
162
+
163
+ matches = values.map do |req_mime, quality|
164
+ match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
165
+ next unless match
166
+ [match, quality]
167
+ end.compact.sort_by do |match, quality|
168
+ (match.split('/', 2).count('*') * -10) + quality
169
+ end.last
170
+ matches && matches.first
171
+ end
172
+
173
+ ESCAPE_HTML = {
174
+ "&" => "&amp;",
175
+ "<" => "&lt;",
176
+ ">" => "&gt;",
177
+ "'" => "&#x27;",
178
+ '"' => "&quot;",
179
+ "/" => "&#x2F;"
180
+ }
181
+
182
+ ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
111
183
 
112
184
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
113
185
  def escape_html(string)
114
- string.to_s.gsub("&", "&amp;").
115
- gsub("<", "&lt;").
116
- gsub(">", "&gt;").
117
- gsub("'", "&#39;").
118
- gsub('"', "&quot;")
186
+ string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
119
187
  end
120
- module_function :escape_html
121
188
 
122
189
  def select_best_encoding(available_encodings, accept_encoding)
123
190
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
124
191
 
125
- expanded_accept_encoding =
126
- accept_encoding.map { |m, q|
127
- if m == "*"
128
- (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
129
- else
130
- [[m, q]]
192
+ expanded_accept_encoding = []
193
+
194
+ accept_encoding.each do |m, q|
195
+ preference = available_encodings.index(m) || available_encodings.size
196
+
197
+ if m == "*"
198
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
199
+ expanded_accept_encoding << [m2, q, preference]
131
200
  end
132
- }.inject([]) { |mem, list|
133
- mem + list
134
- }
201
+ else
202
+ expanded_accept_encoding << [m, q, preference]
203
+ end
204
+ end
135
205
 
136
- encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
206
+ encoding_candidates = expanded_accept_encoding
207
+ .sort_by { |_, q, p| [-q, p] }
208
+ .map!(&:first)
137
209
 
138
210
  unless encoding_candidates.include?("identity")
139
211
  encoding_candidates.push("identity")
140
212
  end
141
213
 
142
- expanded_accept_encoding.find_all { |m, q|
143
- q == 0.0
144
- }.each { |m, _|
145
- encoding_candidates.delete(m)
146
- }
214
+ expanded_accept_encoding.each do |m, q|
215
+ encoding_candidates.delete(m) if q == 0.0
216
+ end
147
217
 
148
- return (encoding_candidates & available_encodings)[0]
218
+ (encoding_candidates & available_encodings)[0]
149
219
  end
150
- module_function :select_best_encoding
151
220
 
152
- # Return the bytesize of String; uses String#length under Ruby 1.8 and
153
- # String#bytesize under 1.9.
154
- if ''.respond_to?(:bytesize)
155
- def bytesize(string)
156
- string.bytesize
221
+ def parse_cookies(env)
222
+ parse_cookies_header env[HTTP_COOKIE]
223
+ end
224
+
225
+ def parse_cookies_header(header)
226
+ # According to RFC 6265:
227
+ # The syntax for cookie headers only supports semicolons
228
+ # User Agent -> Server ==
229
+ # Cookie: SID=31d4d96e407aad42; lang=en-US
230
+ return {} unless header
231
+ header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
232
+ next if cookie.empty?
233
+ key, value = cookie.split('=', 2)
234
+ cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
157
235
  end
158
- else
159
- def bytesize(string)
160
- string.size
236
+ end
237
+
238
+ def add_cookie_to_header(header, key, value)
239
+ case value
240
+ when Hash
241
+ domain = "; domain=#{value[:domain]}" if value[:domain]
242
+ path = "; path=#{value[:path]}" if value[:path]
243
+ max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
244
+ expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
245
+ secure = "; secure" if value[:secure]
246
+ httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
247
+ same_site =
248
+ case value[:same_site]
249
+ when false, nil
250
+ nil
251
+ when :none, 'None', :None
252
+ '; SameSite=None'
253
+ when :lax, 'Lax', :Lax
254
+ '; SameSite=Lax'
255
+ when true, :strict, 'Strict', :Strict
256
+ '; SameSite=Strict'
257
+ else
258
+ raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
259
+ end
260
+ value = value[:value]
261
+ end
262
+ value = [value] unless Array === value
263
+
264
+ cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
265
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
266
+
267
+ case header
268
+ when nil, ''
269
+ cookie
270
+ when String
271
+ [header, cookie].join("\n")
272
+ when Array
273
+ (header + [cookie]).join("\n")
274
+ else
275
+ raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
276
+ end
277
+ end
278
+
279
+ def set_cookie_header!(header, key, value)
280
+ header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
281
+ nil
282
+ end
283
+
284
+ def make_delete_cookie_header(header, key, value)
285
+ case header
286
+ when nil, ''
287
+ cookies = []
288
+ when String
289
+ cookies = header.split("\n")
290
+ when Array
291
+ cookies = header
292
+ end
293
+
294
+ key = escape(key)
295
+ domain = value[:domain]
296
+ path = value[:path]
297
+ regexp = if domain
298
+ if path
299
+ /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
300
+ else
301
+ /\A#{key}=.*domain=#{domain}(?:;|$)/
302
+ end
303
+ elsif path
304
+ /\A#{key}=.*path=#{path}(?:;|$)/
305
+ else
306
+ /\A#{key}=/
307
+ end
308
+
309
+ cookies.reject! { |cookie| regexp.match? cookie }
310
+
311
+ cookies.join("\n")
312
+ end
313
+
314
+ def delete_cookie_header!(header, key, value = {})
315
+ header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
316
+ nil
317
+ end
318
+
319
+ # Adds a cookie that will *remove* a cookie from the client. Hence the
320
+ # strange method name.
321
+ def add_remove_cookie_to_header(header, key, value = {})
322
+ new_header = make_delete_cookie_header(header, key, value)
323
+
324
+ add_cookie_to_header(new_header, key,
325
+ { value: '', path: nil, domain: nil,
326
+ max_age: '0',
327
+ expires: Time.at(0) }.merge(value))
328
+
329
+ end
330
+
331
+ def rfc2822(time)
332
+ time.rfc2822
333
+ end
334
+
335
+ # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
336
+ # of '% %b %Y'.
337
+ # It assumes that the time is in GMT to comply to the RFC 2109.
338
+ #
339
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
340
+ # that I'm certain someone implemented only that option.
341
+ # Do not use %a and %b from Time.strptime, it would use localized names for
342
+ # weekday and month.
343
+ #
344
+ def rfc2109(time)
345
+ wday = RFC2822_DAY_NAME[time.wday]
346
+ mon = RFC2822_MONTH_NAME[time.mon - 1]
347
+ time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
348
+ end
349
+
350
+ # Parses the "Range:" header, if present, into an array of Range objects.
351
+ # Returns nil if the header is missing or syntactically invalid.
352
+ # Returns an empty array if none of the ranges are satisfiable.
353
+ def byte_ranges(env, size)
354
+ warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
355
+ get_byte_ranges env['HTTP_RANGE'], size
356
+ end
357
+
358
+ def get_byte_ranges(http_range, size)
359
+ # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
360
+ return nil unless http_range && http_range =~ /bytes=([^;]+)/
361
+ ranges = []
362
+ $1.split(/,\s*/).each do |range_spec|
363
+ return nil unless range_spec.include?('-')
364
+ range = range_spec.split('-')
365
+ r0, r1 = range[0], range[1]
366
+ if r0.nil? || r0.empty?
367
+ return nil if r1.nil?
368
+ # suffix-byte-range-spec, represents trailing suffix of file
369
+ r0 = size - r1.to_i
370
+ r0 = 0 if r0 < 0
371
+ r1 = size - 1
372
+ else
373
+ r0 = r0.to_i
374
+ if r1.nil?
375
+ r1 = size - 1
376
+ else
377
+ r1 = r1.to_i
378
+ return nil if r1 < r0 # backwards range is syntactically invalid
379
+ r1 = size - 1 if r1 >= size
380
+ end
381
+ end
382
+ ranges << (r0..r1) if r0 <= r1
161
383
  end
384
+
385
+ return [] if ranges.map(&:size).inject(0, :+) > size
386
+
387
+ ranges
388
+ end
389
+
390
+ # Constant time string comparison.
391
+ #
392
+ # NOTE: the values compared should be of fixed length, such as strings
393
+ # that have already been processed by HMAC. This should not be used
394
+ # on variable length plaintext strings because it could leak length info
395
+ # via timing attacks.
396
+ def secure_compare(a, b)
397
+ return false unless a.bytesize == b.bytesize
398
+
399
+ l = a.unpack("C*")
400
+
401
+ r, i = 0, -1
402
+ b.each_byte { |v| r |= v ^ l[i += 1] }
403
+ r == 0
162
404
  end
163
- module_function :bytesize
164
405
 
165
406
  # Context allows the use of a compatible middleware at different points
166
407
  # in a request handling stack. A compatible middleware must define
@@ -183,46 +424,73 @@ module Rack
183
424
  self.class.new(@for, app)
184
425
  end
185
426
 
186
- def context(env, app=@app)
427
+ def context(env, app = @app)
187
428
  recontext(app).call(env)
188
429
  end
189
430
  end
190
431
 
191
432
  # A case-insensitive Hash that preserves the original case of a
192
433
  # header when set.
193
- class HeaderHash < Hash
194
- def initialize(hash={})
434
+ #
435
+ # @api private
436
+ class HeaderHash < Hash # :nodoc:
437
+ def self.[](headers)
438
+ if headers.is_a?(HeaderHash) && !headers.frozen?
439
+ return headers
440
+ else
441
+ return self.new(headers)
442
+ end
443
+ end
444
+
445
+ def initialize(hash = {})
446
+ super()
195
447
  @names = {}
196
448
  hash.each { |k, v| self[k] = v }
197
449
  end
198
450
 
199
- def to_hash
200
- inject({}) do |hash, (k,v)|
201
- if v.respond_to? :to_ary
202
- hash[k] = v.to_ary.join("\n")
203
- else
204
- hash[k] = v
205
- end
206
- hash
451
+ # on dup/clone, we need to duplicate @names hash
452
+ def initialize_copy(other)
453
+ super
454
+ @names = other.names.dup
455
+ end
456
+
457
+ # on clear, we need to clear @names hash
458
+ def clear
459
+ super
460
+ @names.clear
461
+ end
462
+
463
+ def each
464
+ super do |k, v|
465
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
207
466
  end
208
467
  end
209
468
 
469
+ def to_hash
470
+ hash = {}
471
+ each { |k, v| hash[k] = v }
472
+ hash
473
+ end
474
+
210
475
  def [](k)
211
- super @names[k.downcase]
476
+ super(k) || super(@names[k.downcase])
212
477
  end
213
478
 
214
479
  def []=(k, v)
215
- delete k
216
- @names[k.downcase] = k
480
+ canonical = k.downcase.freeze
481
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
482
+ @names[canonical] = k
217
483
  super k, v
218
484
  end
219
485
 
220
486
  def delete(k)
221
- super @names.delete(k.downcase)
487
+ canonical = k.downcase
488
+ result = super @names.delete(canonical)
489
+ result
222
490
  end
223
491
 
224
492
  def include?(k)
225
- @names.has_key? k.downcase
493
+ super || @names.include?(k.downcase)
226
494
  end
227
495
 
228
496
  alias_method :has_key?, :include?
@@ -238,164 +506,127 @@ module Rack
238
506
  hash = dup
239
507
  hash.merge! other
240
508
  end
509
+
510
+ def replace(other)
511
+ clear
512
+ other.each { |k, v| self[k] = v }
513
+ self
514
+ end
515
+
516
+ protected
517
+ def names
518
+ @names
519
+ end
241
520
  end
242
521
 
243
522
  # Every standard HTTP code mapped to the appropriate message.
244
- # Stolen from Mongrel.
523
+ # Generated with:
524
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
525
+ # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
526
+ # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
245
527
  HTTP_STATUS_CODES = {
246
- 100 => 'Continue',
247
- 101 => 'Switching Protocols',
248
- 200 => 'OK',
249
- 201 => 'Created',
250
- 202 => 'Accepted',
251
- 203 => 'Non-Authoritative Information',
252
- 204 => 'No Content',
253
- 205 => 'Reset Content',
254
- 206 => 'Partial Content',
255
- 300 => 'Multiple Choices',
256
- 301 => 'Moved Permanently',
257
- 302 => 'Found',
258
- 303 => 'See Other',
259
- 304 => 'Not Modified',
260
- 305 => 'Use Proxy',
261
- 307 => 'Temporary Redirect',
262
- 400 => 'Bad Request',
263
- 401 => 'Unauthorized',
264
- 402 => 'Payment Required',
265
- 403 => 'Forbidden',
266
- 404 => 'Not Found',
267
- 405 => 'Method Not Allowed',
268
- 406 => 'Not Acceptable',
269
- 407 => 'Proxy Authentication Required',
270
- 408 => 'Request Timeout',
271
- 409 => 'Conflict',
272
- 410 => 'Gone',
273
- 411 => 'Length Required',
274
- 412 => 'Precondition Failed',
275
- 413 => 'Request Entity Too Large',
276
- 414 => 'Request-URI Too Large',
277
- 415 => 'Unsupported Media Type',
278
- 416 => 'Requested Range Not Satisfiable',
279
- 417 => 'Expectation Failed',
280
- 500 => 'Internal Server Error',
281
- 501 => 'Not Implemented',
282
- 502 => 'Bad Gateway',
283
- 503 => 'Service Unavailable',
284
- 504 => 'Gateway Timeout',
285
- 505 => 'HTTP Version Not Supported'
528
+ 100 => 'Continue',
529
+ 101 => 'Switching Protocols',
530
+ 102 => 'Processing',
531
+ 103 => 'Early Hints',
532
+ 200 => 'OK',
533
+ 201 => 'Created',
534
+ 202 => 'Accepted',
535
+ 203 => 'Non-Authoritative Information',
536
+ 204 => 'No Content',
537
+ 205 => 'Reset Content',
538
+ 206 => 'Partial Content',
539
+ 207 => 'Multi-Status',
540
+ 208 => 'Already Reported',
541
+ 226 => 'IM Used',
542
+ 300 => 'Multiple Choices',
543
+ 301 => 'Moved Permanently',
544
+ 302 => 'Found',
545
+ 303 => 'See Other',
546
+ 304 => 'Not Modified',
547
+ 305 => 'Use Proxy',
548
+ 306 => '(Unused)',
549
+ 307 => 'Temporary Redirect',
550
+ 308 => 'Permanent Redirect',
551
+ 400 => 'Bad Request',
552
+ 401 => 'Unauthorized',
553
+ 402 => 'Payment Required',
554
+ 403 => 'Forbidden',
555
+ 404 => 'Not Found',
556
+ 405 => 'Method Not Allowed',
557
+ 406 => 'Not Acceptable',
558
+ 407 => 'Proxy Authentication Required',
559
+ 408 => 'Request Timeout',
560
+ 409 => 'Conflict',
561
+ 410 => 'Gone',
562
+ 411 => 'Length Required',
563
+ 412 => 'Precondition Failed',
564
+ 413 => 'Payload Too Large',
565
+ 414 => 'URI Too Long',
566
+ 415 => 'Unsupported Media Type',
567
+ 416 => 'Range Not Satisfiable',
568
+ 417 => 'Expectation Failed',
569
+ 421 => 'Misdirected Request',
570
+ 422 => 'Unprocessable Entity',
571
+ 423 => 'Locked',
572
+ 424 => 'Failed Dependency',
573
+ 425 => 'Too Early',
574
+ 426 => 'Upgrade Required',
575
+ 428 => 'Precondition Required',
576
+ 429 => 'Too Many Requests',
577
+ 431 => 'Request Header Fields Too Large',
578
+ 451 => 'Unavailable for Legal Reasons',
579
+ 500 => 'Internal Server Error',
580
+ 501 => 'Not Implemented',
581
+ 502 => 'Bad Gateway',
582
+ 503 => 'Service Unavailable',
583
+ 504 => 'Gateway Timeout',
584
+ 505 => 'HTTP Version Not Supported',
585
+ 506 => 'Variant Also Negotiates',
586
+ 507 => 'Insufficient Storage',
587
+ 508 => 'Loop Detected',
588
+ 509 => 'Bandwidth Limit Exceeded',
589
+ 510 => 'Not Extended',
590
+ 511 => 'Network Authentication Required'
286
591
  }
287
592
 
288
593
  # Responses with HTTP status codes that should not have an entity body
289
- STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
594
+ STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
290
595
 
291
- # A multipart form data parser, adapted from IOWA.
292
- #
293
- # Usually, Rack::Request#POST takes care of calling this.
596
+ SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
597
+ [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
598
+ }.flatten]
294
599
 
295
- module Multipart
296
- EOL = "\r\n"
600
+ def status_code(status)
601
+ if status.is_a?(Symbol)
602
+ SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
603
+ else
604
+ status.to_i
605
+ end
606
+ end
297
607
 
298
- def self.parse_multipart(env)
299
- unless env['CONTENT_TYPE'] =~
300
- %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
301
- nil
302
- else
303
- boundary = "--#{$1}"
304
-
305
- params = {}
306
- buf = ""
307
- content_length = env['CONTENT_LENGTH'].to_i
308
- input = env['rack.input']
309
- input.rewind
310
-
311
- boundary_size = Utils.bytesize(boundary) + EOL.size
312
- bufsize = 16384
313
-
314
- content_length -= boundary_size
315
-
316
- read_buffer = ''
317
-
318
- status = input.read(boundary_size, read_buffer)
319
- raise EOFError, "bad content body" unless status == boundary + EOL
320
-
321
- rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
322
-
323
- loop {
324
- head = nil
325
- body = ''
326
- filename = content_type = name = nil
327
-
328
- until head && buf =~ rx
329
- if !head && i = buf.index(EOL+EOL)
330
- head = buf.slice!(0, i+2) # First \r\n
331
- buf.slice!(0, 2) # Second \r\n
332
-
333
- filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1]
334
- content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
335
- name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
336
-
337
- if content_type || filename
338
- body = Tempfile.new("RackMultipart")
339
- body.binmode if body.respond_to?(:binmode)
340
- end
341
-
342
- next
343
- end
344
-
345
- # Save the read body part.
346
- if head && (boundary_size+4 < buf.size)
347
- body << buf.slice!(0, buf.size - (boundary_size+4))
348
- end
349
-
350
- c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
351
- raise EOFError, "bad content body" if c.nil? || c.empty?
352
- buf << c
353
- content_length -= c.size
354
- end
355
-
356
- # Save the rest.
357
- if i = buf.index(rx)
358
- body << buf.slice!(0, i)
359
- buf.slice!(0, boundary_size+2)
360
-
361
- content_length = -1 if $1 == "--"
362
- end
363
-
364
- if filename == ""
365
- # filename is blank which means no file has been selected
366
- data = nil
367
- elsif filename
368
- body.rewind
369
-
370
- # Take the basename of the upload's original filename.
371
- # This handles the full Windows paths given by Internet Explorer
372
- # (and perhaps other broken user agents) without affecting
373
- # those which give the lone filename.
374
- filename =~ /^(?:.*[:\\\/])?(.*)/m
375
- filename = $1
376
-
377
- data = {:filename => filename, :type => content_type,
378
- :name => name, :tempfile => body, :head => head}
379
- elsif !filename && content_type
380
- body.rewind
381
-
382
- # Generic multipart cases, not coming from a form
383
- data = {:type => content_type,
384
- :name => name, :tempfile => body, :head => head}
385
- else
386
- data = body
387
- end
388
-
389
- Utils.normalize_params(params, name, data) unless data.nil?
390
-
391
- break if buf.empty? || content_length == -1
392
- }
393
-
394
- input.rewind
395
-
396
- params
397
- end
608
+ PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
609
+
610
+ def clean_path_info(path_info)
611
+ parts = path_info.split PATH_SEPS
612
+
613
+ clean = []
614
+
615
+ parts.each do |part|
616
+ next if part.empty? || part == '.'
617
+ part == '..' ? clean.pop : clean << part
398
618
  end
619
+
620
+ clean_path = clean.join(::File::SEPARATOR)
621
+ clean_path.prepend("/") if parts.empty? || parts.first.empty?
622
+ clean_path
399
623
  end
624
+
625
+ NULL_BYTE = "\0"
626
+
627
+ def valid_path?(path)
628
+ path.valid_encoding? && !path.include?(NULL_BYTE)
629
+ end
630
+
400
631
  end
401
632
  end