lib/rack/utils.rb in rack-2.1.4.4 vs lib/rack/utils.rb in rack-2.2.0
- old
+ new
@@ -3,21 +3,20 @@
require 'uri'
require 'fileutils'
require 'set'
require 'tempfile'
-require 'rack/query_parser'
require 'time'
-require_relative 'core_ext/regexp'
+require_relative 'query_parser'
module Rack
# Rack::Utils contains a grab-bag of useful methods for writing web
# applications adopted from all kinds of Ruby libraries.
module Utils
- using ::Rack::RegexpExtensions
+ (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
ParameterTypeError = QueryParser::ParameterTypeError
InvalidParameterError = QueryParser::InvalidParameterError
DEFAULT_SEP = QueryParser::DEFAULT_SEP
COMMON_SEP = QueryParser::COMMON_SEP
@@ -28,58 +27,44 @@
end
# The default number of bytes to allow parameter keys to take up.
# This helps prevent a rogue client from flooding a Request.
self.default_query_parser = QueryParser.make_default(65536, 100)
+ module_function
+
# URI escapes. (CGI style space to +)
def escape(s)
URI.encode_www_form_component(s)
end
- module_function :escape
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
# true URI escaping.
def escape_path(s)
::URI::DEFAULT_PARSER.escape s
end
- module_function :escape_path
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
# unescaping query parameters or form components.
def unescape_path(s)
::URI::DEFAULT_PARSER.unescape s
end
- module_function :unescape_path
-
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
# target encoding of the string returned, and it defaults to UTF-8
def unescape(s, encoding = Encoding::UTF_8)
URI.decode_www_form_component(s, encoding)
end
- module_function :unescape
class << self
- attr_accessor :multipart_total_part_limit
-
- attr_accessor :multipart_file_limit
-
- # multipart_part_limit is the original name of multipart_file_limit, but
- # the limit only counts parts with filenames.
- alias multipart_part_limit multipart_file_limit
- alias multipart_part_limit= multipart_file_limit=
+ attr_accessor :multipart_part_limit
end
- # The maximum number of file parts a request can contain. Accepting too
- # many parts can lead to the server running out of file handles.
+ # The maximum number of parts a request can contain. Accepting too many part
+ # can lead to the server running out of file handles.
# Set to `0` for no limit.
- self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
+ self.multipart_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i
- # The maximum total number of parts a request can contain. Accepting too
- # many can lead to excessive memory use and parsing time.
- self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
-
def self.param_depth_limit
default_query_parser.param_depth_limit
end
def self.param_depth_limit=(v)
@@ -97,36 +82,34 @@
if defined?(Process::CLOCK_MONOTONIC)
def clock_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
else
+ # :nocov:
def clock_time
Time.now.to_f
end
+ # :nocov:
end
- module_function :clock_time
def parse_query(qs, d = nil, &unescaper)
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end
- module_function :parse_query
def parse_nested_query(qs, d = nil)
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end
- module_function :parse_nested_query
def build_query(params)
params.map { |k, v|
if v.class == Array
build_query(v.map { |x| [k, x] })
else
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
end
}.join("&")
end
- module_function :build_query
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
@@ -141,24 +124,26 @@
else
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{prefix}=#{escape(value)}"
end
end
- module_function :build_nested_query
def q_values(q_value_header)
- q_value_header.to_s.split(',').map do |part|
- value, parameters = part.split(';', 2).map(&:strip)
+ q_value_header.to_s.split(/\s*,\s*/).map do |part|
+ value, parameters = part.split(/\s*;\s*/, 2)
quality = 1.0
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
quality = md[1].to_f
end
[value, quality]
end
end
- module_function :q_values
+ # Return best accept value to use, based on the algorithm
+ # in RFC 2616 Section 14. If there are multiple best
+ # matches (same specificity and quality), the value returned
+ # is arbitrary.
def best_q_match(q_value_header, available_mimes)
values = q_values(q_value_header)
matches = values.map do |req_mime, quality|
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
@@ -167,11 +152,10 @@
end.compact.sort_by do |match, quality|
(match.split('/', 2).count('*') * -10) + quality
end.last
matches && matches.first
end
- module_function :best_q_match
ESCAPE_HTML = {
"&" => "&",
"<" => "<",
">" => ">",
@@ -184,26 +168,31 @@
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end
- module_function :escape_html
def select_best_encoding(available_encodings, accept_encoding)
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- expanded_accept_encoding =
- accept_encoding.each_with_object([]) do |(m, q), list|
- if m == "*"
- (available_encodings - accept_encoding.map(&:first))
- .each { |m2| list << [m2, q] }
- else
- list << [m, q]
+ expanded_accept_encoding = []
+
+ accept_encoding.each do |m, q|
+ preference = available_encodings.index(m) || available_encodings.size
+
+ if m == "*"
+ (available_encodings - accept_encoding.map(&:first)).each do |m2|
+ expanded_accept_encoding << [m2, q, preference]
end
+ else
+ expanded_accept_encoding << [m, q, preference]
end
+ end
- encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first)
+ encoding_candidates = expanded_accept_encoding
+ .sort_by { |_, q, p| [-q, p] }
+ .map!(&:first)
unless encoding_candidates.include?("identity")
encoding_candidates.push("identity")
end
@@ -211,31 +200,23 @@
encoding_candidates.delete(m) if q == 0.0
end
(encoding_candidates & available_encodings)[0]
end
- module_function :select_best_encoding
def parse_cookies(env)
parse_cookies_header env[HTTP_COOKIE]
end
- module_function :parse_cookies
def parse_cookies_header(header)
- # According to RFC 2109:
- # If multiple cookies satisfy the criteria above, they are ordered in
- # the Cookie header such that those with more specific Path attributes
- # precede those with less specific. Ordering with respect to other
- # attributes (e.g., Domain) is unspecified.
- return {} unless header
- header.split(/[;,] */n).each_with_object({}) do |cookie, cookies|
- next if cookie.empty?
- key, value = cookie.split('=', 2)
- cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
- end
+ # According to RFC 6265:
+ # The syntax for cookie headers only supports semicolons
+ # User Agent -> Server ==
+ # Cookie: SID=31d4d96e407aad42; lang=en-US
+ cookies = parse_query(header, ';') { |s| unescape(s) rescue s }
+ cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
end
- module_function :parse_cookies_header
def add_cookie_to_header(header, key, value)
case value
when Hash
domain = "; domain=#{value[:domain]}" if value[:domain]
@@ -273,17 +254,15 @@
(header + [cookie]).join("\n")
else
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
end
end
- module_function :add_cookie_to_header
def set_cookie_header!(header, key, value)
header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
nil
end
- module_function :set_cookie_header!
def make_delete_cookie_header(header, key, value)
case header
when nil, ''
cookies = []
@@ -291,29 +270,34 @@
cookies = header.split("\n")
when Array
cookies = header
end
- regexp = if value[:domain]
- /\A#{escape(key)}=.*domain=#{value[:domain]}/
- elsif value[:path]
- /\A#{escape(key)}=.*path=#{value[:path]}/
+ key = escape(key)
+ domain = value[:domain]
+ path = value[:path]
+ regexp = if domain
+ if path
+ /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
+ else
+ /\A#{key}=.*domain=#{domain}(?:;|$)/
+ end
+ elsif path
+ /\A#{key}=.*path=#{path}(?:;|$)/
else
- /\A#{escape(key)}=/
+ /\A#{key}=/
end
cookies.reject! { |cookie| regexp.match? cookie }
cookies.join("\n")
end
- module_function :make_delete_cookie_header
def delete_cookie_header!(header, key, value = {})
header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
nil
end
- module_function :delete_cookie_header!
# Adds a cookie that will *remove* a cookie from the client. Hence the
# strange method name.
def add_remove_cookie_to_header(header, key, value = {})
new_header = make_delete_cookie_header(header, key, value)
@@ -322,16 +306,14 @@
{ value: '', path: nil, domain: nil,
max_age: '0',
expires: Time.at(0) }.merge(value))
end
- module_function :add_remove_cookie_to_header
def rfc2822(time)
time.rfc2822
end
- module_function :rfc2822
# Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
# of '% %b %Y'.
# It assumes that the time is in GMT to comply to the RFC 2109.
#
@@ -343,38 +325,35 @@
def rfc2109(time)
wday = Time::RFC2822_DAY_NAME[time.wday]
mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
end
- module_function :rfc2109
# Parses the "Range:" header, if present, into an array of Range objects.
# Returns nil if the header is missing or syntactically invalid.
# Returns an empty array if none of the ranges are satisfiable.
def byte_ranges(env, size)
warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
get_byte_ranges env['HTTP_RANGE'], size
end
- module_function :byte_ranges
def get_byte_ranges(http_range, size)
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
return nil unless http_range && http_range =~ /bytes=([^;]+)/
ranges = []
$1.split(/,\s*/).each do |range_spec|
- return nil unless range_spec.include?('-')
- range = range_spec.split('-')
- r0, r1 = range[0], range[1]
- if r0.nil? || r0.empty?
- return nil if r1.nil?
+ return nil unless range_spec =~ /(\d*)-(\d*)/
+ r0, r1 = $1, $2
+ if r0.empty?
+ return nil if r1.empty?
# suffix-byte-range-spec, represents trailing suffix of file
r0 = size - r1.to_i
r0 = 0 if r0 < 0
r1 = size - 1
else
r0 = r0.to_i
- if r1.nil?
+ if r1.empty?
r1 = size - 1
else
r1 = r1.to_i
return nil if r1 < r0 # backwards range is syntactically invalid
r1 = size - 1 if r1 >= size
@@ -382,11 +361,10 @@
end
ranges << (r0..r1) if r0 <= r1
end
ranges
end
- module_function :get_byte_ranges
# Constant time string comparison.
#
# NOTE: the values compared should be of fixed length, such as strings
# that have already been processed by HMAC. This should not be used
@@ -399,11 +377,10 @@
r, i = 0, -1
b.each_byte { |v| r |= v ^ l[i += 1] }
r == 0
end
- module_function :secure_compare
# Context allows the use of a compatible middleware at different points
# in a request handling stack. A compatible middleware must define
# #context which should take the arguments env and app. The first of which
# would be the request environment. The second of which would be the rack
@@ -432,10 +409,18 @@
# A case-insensitive Hash that preserves the original case of a
# header when set.
#
# @api private
class HeaderHash < Hash # :nodoc:
+ def self.[](headers)
+ if headers.is_a?(HeaderHash) && !headers.frozen?
+ return headers
+ else
+ return self.new(headers)
+ end
+ end
+
def initialize(hash = {})
super()
@names = {}
hash.each { |k, v| self[k] = v }
end
@@ -444,10 +429,16 @@
def initialize_copy(other)
super
@names = other.names.dup
end
+ # on clear, we need to clear @names hash
+ def clear
+ super
+ @names.clear
+ end
+
def each
super do |k, v|
yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
end
end
@@ -588,11 +579,10 @@
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
else
status.to_i
end
end
- module_function :status_code
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
def clean_path_info(path_info)
parts = path_info.split PATH_SEPS
@@ -602,20 +592,18 @@
parts.each do |part|
next if part.empty? || part == '.'
part == '..' ? clean.pop : clean << part
end
- clean.unshift '/' if parts.empty? || parts.first.empty?
-
- ::File.join clean
+ clean_path = clean.join(::File::SEPARATOR)
+ clean_path.prepend("/") if parts.empty? || parts.first.empty?
+ clean_path
end
- module_function :clean_path_info
NULL_BYTE = "\0"
def valid_path?(path)
path.valid_encoding? && !path.include?(NULL_BYTE)
end
- module_function :valid_path?
end
end