Module: Rack::Utils
| Relationships & Source Files | |
| Namespace Children | |
|
Classes:
| |
| Extension / Inclusion / Inheritance Descendants | |
|
Included In:
| |
| Defined in: | lib/rack/utils.rb |
Overview
Utils contains a grab-bag of useful methods for writing web applications adopted from all kinds of Ruby libraries.
Constant Summary
-
ALLOWED_FORWARDED_PARAMS =
private
# File 'lib/rack/utils.rb', line 149%w[by for host proto].map { |name| [name, name.to_sym] }.to_h.freeze
-
COMMON_SEP =
# File 'lib/rack/utils.rb', line 25QueryParser::COMMON_SEP
-
DEFAULT_SEP =
# File 'lib/rack/utils.rb', line 24QueryParser::DEFAULT_SEP
-
HTTP_STATUS_CODES =
# File 'lib/rack/utils.rb', line 590
Every standard HTTP code mapped to the appropriate message. Generated with:
curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \ | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \ .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \ .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"{ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 103 => 'Early Hints', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 208 => 'Already Reported', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Content Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 421 => 'Misdirected Request', 422 => 'Unprocessable Content', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Too Early', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required' } -
InvalidParameterError =
# File 'lib/rack/utils.rb', line 22QueryParser::InvalidParameterError
-
KeySpaceConstrainedParams =
# File 'lib/rack/utils.rb', line 26QueryParser::Params
-
NULL_BYTE =
# File 'lib/rack/utils.rb', line 707"\0" -
OBSOLETE_SYMBOLS_TO_STATUS_CODES =
private
# File 'lib/rack/utils.rb', line 660{ payload_too_large: 413, unprocessable_entity: 422, bandwidth_limit_exceeded: 509, not_extended: 510 }.freeze -
OBSOLETE_SYMBOL_MAPPINGS =
private
# File 'lib/rack/utils.rb', line 668{ payload_too_large: :content_too_large, unprocessable_entity: :unprocessable_content }.freeze -
PATH_SEPS =
# File 'lib/rack/utils.rb', line 690Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact).freeze
-
ParameterTypeError =
# File 'lib/rack/utils.rb', line 21QueryParser::ParameterTypeError
-
ParamsTooDeepError =
# File 'lib/rack/utils.rb', line 23QueryParser::ParamsTooDeepError
-
STATUS_WITH_NO_ENTITY_BODY =
# File 'lib/rack/utils.rb', line 654
Responses with HTTP status codes that should not have an entity body
-
SYMBOL_TO_STATUS_CODE =
# File 'lib/rack/utils.rb', line 656 -
URI_PARSER =
# File 'lib/rack/utils.rb', line 27defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
-
VALID_COOKIE_KEY =
private
# File 'lib/rack/utils.rb', line 353
A valid cookie key according to RFC6265 and RFC2616. A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ “ / [ ] ? = { }.
/\A[!#$%&'*\-\.\^_`|~0-9a-zA-Z]\z/.freeze
Class Attribute Summary
- .default_query_parser rw
- .multipart_file_limit (also: .multipart_part_limit) rw
-
.multipart_part_limit
rw
Alias for .multipart_file_limit.
- .multipart_total_part_limit rw
- .param_depth_limit rw
- .param_depth_limit=(v) rw
Class Method Summary
-
.best_q_match(q_value_header, available_mimes)
mod_func
Return best accept value to use, based on the algorithm in RFC 2616 Section 14.
- .build_nested_query(value, prefix = nil) mod_func
- .build_query(params) mod_func
-
.byte_ranges(env, size, max_ranges: 100)
mod_func
Parses the “Range:” header, if present, into an array of Range objects.
- .clean_path_info(path_info) mod_func
-
.clock_time
mod_func
:nocov:
- .delete_cookie_header!(headers, key, value = {}) mod_func
-
.delete_set_cookie_header(key, value = {}) ⇒ encoded string
mod_func
Generate an encoded string based on the given
keyandvalueusing set_cookie_header for the purpose of causing the specified cookie to be deleted. -
.delete_set_cookie_header!(header, key, value = {}) ⇒ header value
mod_func
Set an expired cookie in the specified headers with the given cookie
keyandvalueusing delete_set_cookie_header. -
.escape(s)
mod_func
URI escapes.
-
.escape_html(string)
mod_func
Escape ampersands, brackets and quotes to their HTML/XML entities.
-
.escape_path(s)
mod_func
Like URI escaping, but with %20 instead of +.
- .forwarded_values(forwarded_header) mod_func
- .get_byte_ranges(http_range, size, max_ranges: 100) mod_func
-
.parse_cookies(env) ⇒ Hash
mod_func
Parse cookies from the provided request environment using parse_cookies_header.
-
.parse_cookies_header(value) ⇒ Hash
mod_func
Parse cookies from the provided header
valueaccording to RFC6265. - .parse_nested_query(qs, d = nil) mod_func
- .parse_query(qs, d = nil, &unescaper) mod_func
- .q_values(q_value_header) mod_func
- .rfc2822(time) mod_func
-
.secure_compare(a, b)
mod_func
Constant time string comparison.
-
.select_best_encoding(available_encodings, accept_encoding)
mod_func
Given an array of available encoding strings, and an array of acceptable encodings for a request, where each element of the acceptable encodings array is an array where the first element is an encoding name and the second element is the numeric priority for the encoding, return the available encoding with the highest priority.
-
.set_cookie_header(key, value) ⇒ encoded string
mod_func
Generate an encoded string using the provided
keyandvaluesuitable for theset-cookieheader according to RFC6265. -
.set_cookie_header!(headers, key, value) ⇒ header value
mod_func
Append a cookie in the specified headers with the given cookie
keyandvalueusing set_cookie_header. - .status_code(status) mod_func
-
.unescape(s, encoding = Encoding::UTF_8)
mod_func
Unescapes a URI escaped string with
encoding. -
.unescape_path(s)
mod_func
Unescapes the path component of a URI.
- .valid_path?(path) ⇒ Boolean mod_func
Class Attribute Details
.default_query_parser (rw)
[ GitHub ]# File 'lib/rack/utils.rb', line 30
attr_accessor :default_query_parser
.multipart_file_limit (rw) Also known as: .multipart_part_limit
[ GitHub ]# File 'lib/rack/utils.rb', line 65
attr_accessor :multipart_file_limit
.multipart_part_limit (rw)
Alias for .multipart_file_limit.
# File 'lib/rack/utils.rb', line 69
alias multipart_part_limit multipart_file_limit
.multipart_total_part_limit (rw)
[ GitHub ]# File 'lib/rack/utils.rb', line 63
attr_accessor :multipart_total_part_limit
.param_depth_limit (rw)
[ GitHub ]# File 'lib/rack/utils.rb', line 82
def self.param_depth_limit default_query_parser.param_depth_limit end
.param_depth_limit=(v) (rw)
[ GitHub ]# File 'lib/rack/utils.rb', line 86
def self.param_depth_limit=(v) self.default_query_parser = self.default_query_parser.new_depth_limit(v) end
Class Method Details
.best_q_match(q_value_header, available_mimes) (mod_func)
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.
# File 'lib/rack/utils.rb', line 227
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) } next unless match [match, quality] end.compact.sort_by do |match, quality| (match.split('/', 2).count('*') * -10) + quality end.last matches&.first end
.build_nested_query(value, prefix = nil) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 120
def build_nested_query(value, prefix = nil) case value when Array value.map { |v| build_nested_query(v, "#{prefix}[]") }.join("&") when Hash value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k) }.delete_if(&:empty?).join('&') when nil escape(prefix) else raise ArgumentError, "value must be a Hash" if prefix.nil? "#{escape(prefix)}=#{escape(value)}" end end
.build_query(params) (mod_func)
[ GitHub ].byte_ranges(env, size, max_ranges: 100) (mod_func)
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.
# File 'lib/rack/utils.rb', line 492
def byte_ranges(env, size, max_ranges: 100) get_byte_ranges env['HTTP_RANGE'], size, max_ranges: max_ranges end
.clean_path_info(path_info) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 692
def clean_path_info(path_info) parts = path_info.split PATH_SEPS clean = [] parts.each do |part| next if part.empty? || part == '.' part == '..' ? clean.pop : clean << part end clean_path = clean.join(::File::SEPARATOR) clean_path.prepend("/") if parts.empty? || parts.first.empty? clean_path end
.clock_time (mod_func)
:nocov:
# File 'lib/rack/utils.rb', line 96
def clock_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end
.delete_cookie_header!(headers, key, value = {}) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 450
def (headers, key, value = {}) headers[SET_COOKIE] = (headers[SET_COOKIE], key, value) return nil end
.delete_set_cookie_header(key, value = {}) ⇒ encoded string (mod_func)
Generate an encoded string based on the given key and value using set_cookie_header for the purpose of causing the specified cookie to be deleted. The value may be an instance of Hash and can include attributes as outlined by set_cookie_header. The encoded cookie will have a max_age of 0 seconds, an expires date in the past and an empty value. When used with the set-cookie header, it will cause the client to remove any matching cookie.
("myname")
# => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
# File 'lib/rack/utils.rb', line 446
def (key, value = {}) (key, value.merge(max_age: '0', expires: Time.at(0), value: '')) end
.delete_set_cookie_header!(header, key, value = {}) ⇒ header value (mod_func)
Set an expired cookie in the specified headers with the given cookie key and value using delete_set_cookie_header. This causes the client to immediately delete the specified cookie.
(nil, "mycookie")
# => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
If the header is non-nil, it will be modified in place.
header = []
(header, "mycookie")
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
header
# => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
# File 'lib/rack/utils.rb', line 474
def (header, key, value = {}) if header header = Array(header) header << (key, value) else header = (key, value) end return header end
.escape(s) (mod_func)
URI escapes. (CGI style space to +)
# File 'lib/rack/utils.rb', line 40
def escape(s) URI.encode_www_form_component(s) end
.escape_html(string) (mod_func)
Escape ampersands, brackets and quotes to their HTML/XML entities.
# File 'lib/rack/utils.rb', line 250
def escape_html(string) CGI.escapeHTML(string.to_s) end
.escape_path(s) (mod_func)
Like URI escaping, but with %20 instead of +. Strictly speaking this is true URI escaping.
# File 'lib/rack/utils.rb', line 46
def escape_path(s) URI_PARSER.escape s end
.forwarded_values(forwarded_header) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 152
def forwarded_values(forwarded_header) return unless forwarded_header header = forwarded_header.to_s.tr("\n", ";") header.sub!(/\A[\s;,]+/, '') num_params = num_escapes = 0 max_params = max_escapes = 1024 params = {} # Parse parameter list while i = header.index('=') # Only parse up to max parameters, to avoid potential denial of service num_params += 1 return if num_params > max_params # Found end of parameter name, ensure forward progress in loop param = header.slice!(0, i+1) # Remove ending equals and preceding whitespace from parameter name param.chomp!('=') param.strip! param.downcase! return unless param = ALLOWED_FORWARDED_PARAMS[param] if header[0] == '"' # Parameter value is quoted, parse it, handling backslash escapes header.slice!(0, 1) value = String.new while i = header.index(/(["\\])/) c = $1 # Append all content until ending quote or escape value << header.slice!(0, i) # Remove either backslash or ending quote, # ensures forward progress in loop header.slice!(0, 1) # stop parsing parameter value if found ending quote break if c == '"' # Only allow up to max escapes, to avoid potential denial of service num_escapes += 1 return if num_escapes > max_escapes escaped_char = header.slice!(0, 1) value << escaped_char end else if i = header.index(/[;,]/) # Parameter value unquoted (which may be invalid), value ends at comma or semicolon value = header.slice!(0, i) value.sub!(/[\s;,]+\z/, '') else # If no ending semicolon, assume remainder of line is value and stop parsing header.strip! value = header header = '' end value.lstrip! end (params[param] ||= []) << value # skip trailing semicolons/commas/whitespace, to proceed to next parameter header.sub!(/\A[\s;,]+/, '') unless header.empty? end params end
.get_byte_ranges(http_range, size, max_ranges: 100) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 496
def get_byte_ranges(http_range, size, max_ranges: 100) # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35> # Ignore Range when file size is 0 to avoid a 416 error. return nil if size.zero? return nil unless http_range && http_range =~ /bytes=([^;]+)/ byte_range = $1 return nil if byte_range.count(',') >= max_ranges ranges = [] byte_range.split(/,[ \t]*/).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? # 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? r1 = size - 1 else r1 = r1.to_i return nil if r1 < r0 # backwards range is syntactically invalid r1 = size - 1 if r1 >= size end end ranges << (r0..r1) if r0 <= r1 end return [] if ranges.map(&:size).sum > size ranges end
.parse_cookies(env) ⇒ Hash (mod_func)
Parse cookies from the provided request environment using parse_cookies_header. Returns a map of cookie key to cookie value.
({'HTTP_COOKIE' => 'myname=myvalue'})
# => {'myname' => 'myvalue'}
# File 'lib/rack/utils.rb', line 347
def (env) env[HTTP_COOKIE] end
.parse_cookies_header(value) ⇒ Hash (mod_func)
Parse cookies from the provided header value according to RFC6265. The syntax for cookie headers only supports semicolons. Returns a map of cookie key to cookie value.
('myname=myvalue; max-age=0')
# => {"myname"=>"myvalue", "max-age"=>"0"}
# File 'lib/rack/utils.rb', line 328
def (value) return {} unless value value.split(/; */n).each_with_object({}) do |, | next if .empty? key, value = .split('=', 2) [key] = (unescape(value) rescue value) unless .key?(key) end end
.parse_nested_query(qs, d = nil) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 106
def parse_nested_query(qs, d = nil) Rack::Utils.default_query_parser.parse_nested_query(qs, d) end
.parse_query(qs, d = nil, &unescaper) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 102
def parse_query(qs, d = nil, &unescaper) Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper) end
.q_values(q_value_header) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 138
def q_values(q_value_header) q_value_header.to_s.split(',').map do |part| value, parameters = part.split(';', 2).map(&:strip) quality = 1.0 if parameters && (md = /\Aq=([\d.]+)/.match(parameters)) quality = md[1].to_f end [value, quality] end end
.rfc2822(time) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 485
def rfc2822(time) time.rfc2822 end
.secure_compare(a, b) (mod_func)
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 on variable length plaintext strings because it could leak length info via timing attacks.
See additional method definition at line 540.
# File 'lib/rack/utils.rb', line 547
def secure_compare(a, b) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) end
.select_best_encoding(available_encodings, accept_encoding) (mod_func)
Given an array of available encoding strings, and an array of acceptable encodings for a request, where each element of the acceptable encodings array is an array where the first element is an encoding name and the second element is the numeric priority for the encoding, return the available encoding with the highest priority.
The accept_encoding argument is typically generated by calling Request#accept_encoding.
Example:
select_best_encoding(%w(compress gzip identity),
[["compress", 0.5], ["gzip", 1.0]])
# => "gzip"
To reduce denial of service potential, only the first 16 acceptable encodings are considered.
# File 'lib/rack/utils.rb', line 274
def select_best_encoding(available_encodings, accept_encoding) # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html # Only process the first 16 encodings accept_encoding = accept_encoding[0...16] = [] wildcard_seen = false accept_encoding.each do |m, q| preference = available_encodings.index(m) || available_encodings.size if m == "*" unless wildcard_seen (available_encodings - accept_encoding.map(&:first)).each do |m2| << [m2, q, preference] end wildcard_seen = true end else << [m, q, preference] end end encoding_candidates = .sort do |(_, q1, p1), (_, q2, p2)| if r = (q1 <=> q2).nonzero? -r else (p1 <=> p2).nonzero? || 0 end end .map!(&:first) unless encoding_candidates.include?("identity") encoding_candidates.push("identity") end .each do |m, q| encoding_candidates.delete(m) if q == 0.0 end (encoding_candidates & available_encodings)[0] end
.set_cookie_header(key, value) ⇒ encoded string (mod_func)
Generate an encoded string using the provided key and value suitable for the set-cookie header according to RFC6265. The value may be an instance of either String or Hash. If the cookie key is invalid (as defined by RFC6265), an ArgumentError will be raised.
If the cookie value is an instance of Hash, it considers the following cookie attribute keys: domain, max_age, expires (must be instance of Time), secure, http_only, same_site and value. For more details about the interpretation of these fields, consult RFC6265 Section 5.2.
("myname", "myvalue")
# => "myname=myvalue"
("myname", {value: "myvalue", max_age: 10})
# => "myname=myvalue; max-age=10"
# File 'lib/rack/utils.rb', line 376
def (key, value) unless key =~ VALID_COOKIE_KEY raise ArgumentError, "invalid cookie key: #{key.inspect}" end case value when Hash domain = "; domain=#{value[:domain]}" if value[:domain] path = "; path=#{value[:path]}" if value[:path] max_age = "; max-age=#{value[:max_age]}" if value[:max_age] expires = "; expires=#{value[:expires].httpdate}" if value[:expires] secure = "; secure" if value[:secure] httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only]) same_site = case value[:same_site] when false, nil nil when :none, 'None', :None '; samesite=none' when :lax, 'Lax', :Lax '; samesite=lax' when true, :strict, 'Strict', :Strict '; samesite=strict' else raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}" end partitioned = "; partitioned" if value[:partitioned] value = value[:value] end value = [value] unless Array === value return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}" end
.set_cookie_header!(headers, key, value) ⇒ header value (mod_func)
Append a cookie in the specified headers with the given cookie key and value using set_cookie_header.
If the headers already contains a set-cookie key, it will be converted to an Array if not already, and appended to.
# File 'lib/rack/utils.rb', line 420
def (headers, key, value) if header = headers[SET_COOKIE] if header.is_a?(Array) header << (key, value) else headers[SET_COOKIE] = [header, (key, value)] end else headers[SET_COOKIE] = (key, value) end end
.status_code(status) (mod_func)
[ GitHub ]# File 'lib/rack/utils.rb', line 674
def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE.fetch(status) do fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack." if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status] = "#{} Please use #{canonical_symbol.inspect} instead." end warn , uplevel: 3 fallback_code end else status.to_i end end
.unescape(s, encoding = Encoding::UTF_8) (mod_func)
Unescapes a URI escaped string with encoding. encoding will be the target encoding of the string returned, and it defaults to UTF-8
# File 'lib/rack/utils.rb', line 58
def unescape(s, encoding = Encoding::UTF_8) URI.decode_www_form_component(s, encoding) end
.unescape_path(s) (mod_func)
Unescapes the path component of a URI. See .unescape for unescaping query parameters or form components.
# File 'lib/rack/utils.rb', line 52
def unescape_path(s) URI_PARSER.unescape s end
.valid_path?(path) ⇒ Boolean (mod_func)
# File 'lib/rack/utils.rb', line 709
def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end