# -*- encoding: binary -*- require 'uri' require 'fileutils' require 'set' require 'tempfile' require 'rack/query_parser' require 'time' module Rack # Rack::Utils contains a grab-bag of useful methods for writing web # applications adopted from all kinds of Ruby libraries. module Utils ParameterTypeError = QueryParser::ParameterTypeError InvalidParameterError = QueryParser::InvalidParameterError DEFAULT_SEP = QueryParser::DEFAULT_SEP COMMON_SEP = QueryParser::COMMON_SEP KeySpaceConstrainedParams = QueryParser::Params class << self attr_accessor :default_query_parser 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) # 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_part_limit end # 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_part_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || 128).to_i def self.param_depth_limit default_query_parser.param_depth_limit end def self.param_depth_limit=(v) self.default_query_parser = self.default_query_parser.new_depth_limit(v) end def self.key_space_limit default_query_parser.key_space_limit end def self.key_space_limit=(v) self.default_query_parser = self.default_query_parser.new_space_limit(v) end if defined?(Process::CLOCK_MONOTONIC) def clock_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end else def clock_time Time.now.to_f end 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| build_nested_query(v, "#{prefix}[]") }.join("&") when Hash value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) }.reject(&:empty?).join('&') when nil prefix 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(/\s*,\s*/).map do |part| value, parameters = part.split(/\s*;\s*/, 2) quality = 1.0 if md = /\Aq=([\d.]+)/.match(parameters) quality = md[1].to_f end [value, quality] end end module_function :q_values 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 && matches.first end module_function :best_q_match ESCAPE_HTML = { "&" => "&", "<" => "<", ">" => ">", "'" => "'", '"' => """, "/" => "/" } ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys) # 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.map { |m, q| if m == "*" (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] } else [[m, q]] end }.inject([]) { |mem, list| mem + list } encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m } unless encoding_candidates.include?("identity") encoding_candidates.push("identity") end expanded_accept_encoding.each { |m, q| encoding_candidates.delete(m) if q == 0.0 } return (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. 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] path = "; path=#{value[:path]}" if value[:path] max_age = "; max-age=#{value[:max_age]}" if value[:max_age] # There is an RFC mess in the area of date formatting for Cookies. Not # only are there contradicting RFCs and examples within RFC text, but # there are also numerous conflicting names of fields and partially # cross-applicable specifications. # # These are best described in RFC 2616 3.3.1. This RFC text also # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a # fixed length format with space-date delimited fields. # # See also RFC 1123 section 5.2.14. # # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote # the space delimited format. These formats are compliant with RFC 2822. # # For reference, all involved RFCs are: # RFC 822 # RFC 1123 # RFC 2109 # RFC 2616 # RFC 2822 # RFC 2965 # RFC 6265 expires = "; expires=" + rfc2822(value[:expires].clone.gmtime) 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'.freeze when :lax, 'Lax', :Lax '; SameSite=Lax'.freeze when true, :strict, 'Strict', :Strict '; SameSite=Strict'.freeze else raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}" end value = value[:value] end value = [value] unless Array === value cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}" case header when nil, '' cookie when String [header, cookie].join("\n") when Array (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 = [] when String cookies = header.split("\n") when Array cookies = header end cookies.reject! { |cookie| if value[:domain] cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/ elsif value[:path] cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/ else cookie =~ /\A#{escape(key)}=/ end } 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) add_cookie_to_header(new_header, key, {: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. # # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough # that I'm certain someone implemented only that option. # Do not use %a and %b from Time.strptime, it would use localized names for # weekday and month. # 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 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? # 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 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 # on variable length plaintext strings because it could leak length info # via timing attacks. def secure_compare(a, b) return false unless a.bytesize == b.bytesize l = a.unpack("C*") 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 # application that the request would be forwarded to. class Context attr_reader :for, :app def initialize(app_f, app_r) raise 'running context does not respond to #context' unless app_f.respond_to? :context @for, @app = app_f, app_r end def call(env) @for.context(env, @app) end def recontext(app) self.class.new(@for, app) end def context(env, app=@app) recontext(app).call(env) end end # A case-insensitive Hash that preserves the original case of a # header when set. class HeaderHash < Hash def self.new(hash={}) HeaderHash === hash ? hash : super(hash) end def initialize(hash={}) super() @names = {} hash.each { |k, v| self[k] = v } end # on dup/clone, we need to duplicate @names hash def initialize_copy(other) super @names = other.names.dup end def each super do |k, v| yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v) end end def to_hash hash = {} each { |k,v| hash[k] = v } hash end def [](k) super(k) || super(@names[k.downcase]) end def []=(k, v) canonical = k.downcase.freeze delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary @names[canonical] = k super k, v end def delete(k) canonical = k.downcase result = super @names.delete(canonical) result end def include?(k) super || @names.include?(k.downcase) end alias_method :has_key?, :include? alias_method :member?, :include? alias_method :key?, :include? def merge!(other) other.each { |k, v| self[k] = v } self end def merge(other) hash = dup hash.merge! other end def replace(other) clear other.each { |k, v| self[k] = v } self end protected def names @names end end # 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 -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' HTTP_STATUS_CODES = { 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 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 => 'Payload Too Large', 414 => 'URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Range Not Satisfiable', 417 => 'Expectation Failed', 421 => 'Misdirected Request', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 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', 510 => 'Not Extended', 511 => 'Network Authentication Required' } # Responses with HTTP status codes that should not have an entity body STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304) SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message| [message.downcase.gsub(/\s|-|'/, '_').to_sym, code] }.flatten] def status_code(status) if status.is_a?(Symbol) SYMBOL_TO_STATUS_CODE[status] || 500 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 clean = [] 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) end module_function :clean_path_info NULL_BYTE = "\0".freeze def valid_path?(path) path.valid_encoding? && !path.include?(NULL_BYTE) end module_function :valid_path? end end