Use lower case normalisation for cookie attributes. (#1849)
[rack.git] / lib / rack / utils.rb
blobb31d3799731d23f5a026561f135a6ddfa4bc0345
1 # -*- encoding: binary -*-
2 # frozen_string_literal: true
4 require 'uri'
5 require 'fileutils'
6 require 'set'
7 require 'tempfile'
8 require 'time'
10 require_relative 'query_parser'
11 require_relative 'mime'
12 require_relative 'headers'
14 module Rack
15   # Rack::Utils contains a grab-bag of useful methods for writing web
16   # applications adopted from all kinds of Ruby libraries.
18   module Utils
19     ParameterTypeError = QueryParser::ParameterTypeError
20     InvalidParameterError = QueryParser::InvalidParameterError
21     DEFAULT_SEP = QueryParser::DEFAULT_SEP
22     COMMON_SEP = QueryParser::COMMON_SEP
23     KeySpaceConstrainedParams = QueryParser::Params
25     class << self
26       attr_accessor :default_query_parser
27     end
28     # The default amount of nesting to allowed by hash parameters.
29     # This helps prevent a rogue client from triggering a possible stack overflow
30     # when parsing parameters.
31     self.default_query_parser = QueryParser.make_default(32)
33     module_function
35     # URI escapes. (CGI style space to +)
36     def escape(s)
37       URI.encode_www_form_component(s)
38     end
40     # Like URI escaping, but with %20 instead of +. Strictly speaking this is
41     # true URI escaping.
42     def escape_path(s)
43       ::URI::DEFAULT_PARSER.escape s
44     end
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
52     # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
53     # target encoding of the string returned, and it defaults to UTF-8
54     def unescape(s, encoding = Encoding::UTF_8)
55       URI.decode_www_form_component(s, encoding)
56     end
58     class << self
59       attr_accessor :multipart_part_limit
60     end
62     # The maximum number of parts a request can contain. Accepting too many part
63     # can lead to the server running out of file handles.
64     # Set to `0` for no limit.
65     self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
67     def self.param_depth_limit
68       default_query_parser.param_depth_limit
69     end
71     def self.param_depth_limit=(v)
72       self.default_query_parser = self.default_query_parser.new_depth_limit(v)
73     end
75     def self.key_space_limit
76       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)
77       65536
78     end
80     def self.key_space_limit=(v)
81       warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
82     end
84     if defined?(Process::CLOCK_MONOTONIC)
85       def clock_time
86         Process.clock_gettime(Process::CLOCK_MONOTONIC)
87       end
88     else
89       # :nocov:
90       def clock_time
91         Time.now.to_f
92       end
93       # :nocov:
94     end
96     def parse_query(qs, d = nil, &unescaper)
97       Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
98     end
100     def parse_nested_query(qs, d = nil)
101       Rack::Utils.default_query_parser.parse_nested_query(qs, d)
102     end
104     def build_query(params)
105       params.map { |k, v|
106         if v.class == Array
107           build_query(v.map { |x| [k, x] })
108         else
109           v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
110         end
111       }.join("&")
112     end
114     def build_nested_query(value, prefix = nil)
115       case value
116       when Array
117         value.map { |v|
118           build_nested_query(v, "#{prefix}[]")
119         }.join("&")
120       when Hash
121         value.map { |k, v|
122           build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
123         }.delete_if(&:empty?).join('&')
124       when nil
125         prefix
126       else
127         raise ArgumentError, "value must be a Hash" if prefix.nil?
128         "#{prefix}=#{escape(value)}"
129       end
130     end
132     def q_values(q_value_header)
133       q_value_header.to_s.split(/\s*,\s*/).map do |part|
134         value, parameters = part.split(/\s*;\s*/, 2)
135         quality = 1.0
136         if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
137           quality = md[1].to_f
138         end
139         [value, quality]
140       end
141     end
143     def forwarded_values(forwarded_header)
144       return nil unless forwarded_header
145       forwarded_header = forwarded_header.to_s.gsub("\n", ";")
147       forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
148         field.split(/\s*,\s*/).each do |pair|
149           return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
150           (values[$1.downcase.to_sym] ||= []) << $2
151         end
152       end
153     end
154     module_function :forwarded_values
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)
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&.first
171     end
173     ESCAPE_HTML = {
174       "&" => "&amp;",
175       "<" => "&lt;",
176       ">" => "&gt;",
177       "'" => "&#x27;",
178       '"' => "&quot;",
179       "/" => "&#x2F;"
180     }
182     ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
184     # Escape ampersands, brackets and quotes to their HTML/XML entities.
185     def escape_html(string)
186       string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
187     end
189     def select_best_encoding(available_encodings, accept_encoding)
190       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
192       expanded_accept_encoding = []
194       accept_encoding.each do |m, q|
195         preference = available_encodings.index(m) || available_encodings.size
197         if m == "*"
198           (available_encodings - accept_encoding.map(&:first)).each do |m2|
199             expanded_accept_encoding << [m2, q, preference]
200           end
201         else
202           expanded_accept_encoding << [m, q, preference]
203         end
204       end
206       encoding_candidates = expanded_accept_encoding
207         .sort_by { |_, q, p| [-q, p] }
208         .map!(&:first)
210       unless encoding_candidates.include?("identity")
211         encoding_candidates.push("identity")
212       end
214       expanded_accept_encoding.each do |m, q|
215         encoding_candidates.delete(m) if q == 0.0
216       end
218       (encoding_candidates & available_encodings)[0]
219     end
221     def parse_cookies(env)
222       parse_cookies_header env[HTTP_COOKIE]
223     end
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)
235       end
236     end
238     def add_cookie_to_header(header, key, value)
239       warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
241       case header
242       when nil, ''
243         return set_cookie_header(key, value)
244       when String
245         [header, set_cookie_header(key, value)]
246       when Array
247         header + [set_cookie_header(key, value)]
248       else
249         raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
250       end
251     end
253     def set_cookie_header(key, value)
254       case value
255       when Hash
256         key = escape(key) unless value[:escape_key] == false
257         domain  = "; domain=#{value[:domain]}"   if value[:domain]
258         path    = "; path=#{value[:path]}"       if value[:path]
259         max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
260         expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
261         secure = "; secure"  if value[:secure]
262         httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
263         same_site =
264           case value[:same_site]
265           when false, nil
266             nil
267           when :none, 'None', :None
268             '; SameSite=None'
269           when :lax, 'Lax', :Lax
270             '; SameSite=Lax'
271           when true, :strict, 'Strict', :Strict
272             '; SameSite=Strict'
273           else
274             raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
275           end
276         value = value[:value]
277       else
278         key = escape(key)
279       end
280       value = [value] unless Array === value
282       return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
283         "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
284     end
286     def set_cookie_header!(headers, key, value)
287       if header = headers[SET_COOKIE]
288         if header.is_a?(Array)
289           header << set_cookie_header(key, value)
290         else
291           headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
292         end
293       else
294         headers[SET_COOKIE] = set_cookie_header(key, value)
295       end
296     end
298     # Adds a cookie that will *remove* a cookie from the client.  Hence the
299     # strange method name.
300     def delete_set_cookie_header(key, value = {})
301       set_cookie_header(key, {
302         value: '', path: nil, domain: nil,
303         max_age: '0',
304         expires: Time.at(0)
305       }.merge(value))
306     end
308     def make_delete_cookie_header(header, key, value)
309       warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
311       delete_set_cookie_header!(header, key, value)
312     end
314     def delete_cookie_header!(headers, key, value = {})
315       headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
317       return nil
318     end
320     def add_remove_cookie_to_header(header, key, value = {})
321       warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
323       delete_set_cookie_header!(header, key, value)
324     end
326     def delete_set_cookie_header!(header, key, value = {})
327       if header
328         header = Array(header)
329         header << delete_set_cookie_header(key, value)
330       else
331         header = delete_set_cookie_header(key, value)
332       end
334       return header
335     end
337     def rfc2822(time)
338       time.rfc2822
339     end
341     # Parses the "Range:" header, if present, into an array of Range objects.
342     # Returns nil if the header is missing or syntactically invalid.
343     # Returns an empty array if none of the ranges are satisfiable.
344     def byte_ranges(env, size)
345       warn("`byte_ranges` is deprecated and will be removed in Rack 3.1, please use `get_byte_ranges`", uplevel: 1)
346       get_byte_ranges env['HTTP_RANGE'], size
347     end
349     def get_byte_ranges(http_range, size)
350       # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
351       return nil unless http_range && http_range =~ /bytes=([^;]+)/
352       ranges = []
353       $1.split(/,\s*/).each do |range_spec|
354         return nil  unless range_spec =~ /(\d*)-(\d*)/
355         r0, r1 = $1, $2
356         if r0.empty?
357           return nil  if r1.empty?
358           # suffix-byte-range-spec, represents trailing suffix of file
359           r0 = size - r1.to_i
360           r0 = 0  if r0 < 0
361           r1 = size - 1
362         else
363           r0 = r0.to_i
364           if r1.empty?
365             r1 = size - 1
366           else
367             r1 = r1.to_i
368             return nil  if r1 < r0  # backwards range is syntactically invalid
369             r1 = size - 1  if r1 >= size
370           end
371         end
372         ranges << (r0..r1)  if r0 <= r1
373       end
374       ranges
375     end
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.
383     if defined?(OpenSSL.fixed_length_secure_compare)
384       def secure_compare(a, b)
385         return false unless a.bytesize == b.bytesize
387         OpenSSL.fixed_length_secure_compare(a, b)
388       end
389     else
390       def secure_compare(a, b)
391         return false unless a.bytesize == b.bytesize
393         l = a.unpack("C*")
395         r, i = 0, -1
396         b.each_byte { |v| r |= v ^ l[i += 1] }
397         r == 0
398       end
399     end
401     # Context allows the use of a compatible middleware at different points
402     # in a request handling stack. A compatible middleware must define
403     # #context which should take the arguments env and app. The first of which
404     # would be the request environment. The second of which would be the rack
405     # application that the request would be forwarded to.
406     class Context
407       attr_reader :for, :app
409       def initialize(app_f, app_r)
410         raise 'running context does not respond to #context' unless app_f.respond_to? :context
411         @for, @app = app_f, app_r
412       end
414       def call(env)
415         @for.context(env, @app)
416       end
418       def recontext(app)
419         self.class.new(@for, app)
420       end
422       def context(env, app = @app)
423         recontext(app).call(env)
424       end
425     end
427     # A wrapper around Headers
428     # header when set.
429     #
430     # @api private
431     class HeaderHash < Hash # :nodoc:
432       def self.[](headers)
433         warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
434         if headers.is_a?(Headers) && !headers.frozen?
435           return headers
436         end
438         new_headers = Headers.new
439         headers.each{|k,v| new_headers[k] = v}
440         new_headers
441       end
443       def self.new(hash = {})
444         warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
445         headers = Headers.new
446         hash.each{|k,v| headers[k] = v}
447         headers
448       end
450       def allocate
451         raise TypeError, "cannot allocate HeaderHash"
452       end
453     end
455     # Every standard HTTP code mapped to the appropriate message.
456     # Generated with:
457     #   curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
458     #     ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
459     #               puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
460     HTTP_STATUS_CODES = {
461       100 => 'Continue',
462       101 => 'Switching Protocols',
463       102 => 'Processing',
464       103 => 'Early Hints',
465       200 => 'OK',
466       201 => 'Created',
467       202 => 'Accepted',
468       203 => 'Non-Authoritative Information',
469       204 => 'No Content',
470       205 => 'Reset Content',
471       206 => 'Partial Content',
472       207 => 'Multi-Status',
473       208 => 'Already Reported',
474       226 => 'IM Used',
475       300 => 'Multiple Choices',
476       301 => 'Moved Permanently',
477       302 => 'Found',
478       303 => 'See Other',
479       304 => 'Not Modified',
480       305 => 'Use Proxy',
481       306 => '(Unused)',
482       307 => 'Temporary Redirect',
483       308 => 'Permanent Redirect',
484       400 => 'Bad Request',
485       401 => 'Unauthorized',
486       402 => 'Payment Required',
487       403 => 'Forbidden',
488       404 => 'Not Found',
489       405 => 'Method Not Allowed',
490       406 => 'Not Acceptable',
491       407 => 'Proxy Authentication Required',
492       408 => 'Request Timeout',
493       409 => 'Conflict',
494       410 => 'Gone',
495       411 => 'Length Required',
496       412 => 'Precondition Failed',
497       413 => 'Payload Too Large',
498       414 => 'URI Too Long',
499       415 => 'Unsupported Media Type',
500       416 => 'Range Not Satisfiable',
501       417 => 'Expectation Failed',
502       421 => 'Misdirected Request',
503       422 => 'Unprocessable Entity',
504       423 => 'Locked',
505       424 => 'Failed Dependency',
506       425 => 'Too Early',
507       426 => 'Upgrade Required',
508       428 => 'Precondition Required',
509       429 => 'Too Many Requests',
510       431 => 'Request Header Fields Too Large',
511       451 => 'Unavailable for Legal Reasons',
512       500 => 'Internal Server Error',
513       501 => 'Not Implemented',
514       502 => 'Bad Gateway',
515       503 => 'Service Unavailable',
516       504 => 'Gateway Timeout',
517       505 => 'HTTP Version Not Supported',
518       506 => 'Variant Also Negotiates',
519       507 => 'Insufficient Storage',
520       508 => 'Loop Detected',
521       509 => 'Bandwidth Limit Exceeded',
522       510 => 'Not Extended',
523       511 => 'Network Authentication Required'
524     }
526     # Responses with HTTP status codes that should not have an entity body
527     STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
529     SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
530       [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
531     }.flatten]
533     def status_code(status)
534       if status.is_a?(Symbol)
535         SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
536       else
537         status.to_i
538       end
539     end
541     PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
543     def clean_path_info(path_info)
544       parts = path_info.split PATH_SEPS
546       clean = []
548       parts.each do |part|
549         next if part.empty? || part == '.'
550         part == '..' ? clean.pop : clean << part
551       end
553       clean_path = clean.join(::File::SEPARATOR)
554       clean_path.prepend("/") if parts.empty? || parts.first.empty?
555       clean_path
556     end
558     NULL_BYTE = "\0"
560     def valid_path?(path)
561       path.valid_encoding? && !path.include?(NULL_BYTE)
562     end
564   end