lib/qeweney/request_info.rb



# frozen_string_literal: true

require 'uri'
require 'escape_utils'

module Qeweney
  module RequestInfoMethods
    def host
      @headers['host'] || @headers[':authority']
    end
    alias_method :authority, :host

    def connection
      @headers['connection']
    end

    def upgrade_protocol
      connection == 'upgrade' && @headers['upgrade']&.downcase
    end

    def websocket_version
      headers['sec-websocket-version'].to_i
    end

    def protocol
      @protocol ||= @adapter.protocol
    end
    
    def method
      @method ||= @headers[':method'].downcase
    end
    
    def scheme
      @scheme ||= @headers[':scheme']
    end
    
    def uri
      @uri ||= URI.parse(@headers[':path'] || '')
    end
    
    def full_uri
      @full_uri = "#{scheme}://#{host}#{uri}"
    end
    
    def path
      @path ||= uri.path
    end
    
    def query_string
      @query_string ||= uri.query
    end
    
    def query
      return @query if @query
      
      @query = (q = uri.query) ? parse_query(q) : {}
    end
    
    QUERY_KV_REGEXP = /([^=]+)(?:=(.*))?/
    
    def parse_query(query)
      query.split('&').each_with_object({}) do |kv, h|
        k, v = kv.match(QUERY_KV_REGEXP)[1..2]
        # k, v = kv.split('=')
        h[k.to_sym] = v ? URI.decode_www_form_component(v) : true
      end
    end

    def request_id
      @headers['x-request-id']
    end

    def forwarded_for
      @headers['x-forwarded-for']
    end

    # TODO: should return encodings in client's order of preference (and take
    # into account q weights)
    def accept_encoding
      encoding = @headers['accept-encoding']
      return [] unless encoding

      encoding.split(',').map { |i| i.strip }
    end

    def cookies
      @cookies ||= parse_cookies(headers['cookie'])
    end

    COOKIE_RE = /^([^=]+)=(.*)$/.freeze
    SEMICOLON = ';'
  
    def parse_cookies(cookies)
      return {} unless cookies

      cookies.split(SEMICOLON).each_with_object({}) do |c, h|
        raise BadRequestError, 'Invalid cookie format' unless c.strip =~ COOKIE_RE
  
        key, value = Regexp.last_match[1..2]
        h[key] = EscapeUtils.unescape_uri(value)
      end
    end
  end

  module RequestInfoClassMethods
    def parse_form_data(body, headers)
      case (content_type = headers['content-type'])
      when /^multipart\/form\-data; boundary=([^\s]+)/
        boundary = "--#{Regexp.last_match(1)}"
        parse_multipart_form_data(body, boundary)
      when /^application\/x-www-form-urlencoded/
        parse_urlencoded_form_data(body)
      else
        raise "Unsupported form data content type: #{content_type}"
      end
    end

    def parse_multipart_form_data(body, boundary)
      parts = body.split(boundary)
      parts.each_with_object({}) do |p, h|
        next if p.empty? || p == "--\r\n"

        # remove post-boundary \r\n
        p.slice!(0, 2)
        parse_multipart_form_data_part(p, h)
      end
    end

    def parse_multipart_form_data_part(part, hash)
      body, headers = parse_multipart_form_data_part_headers(part)
      disposition = headers['content-disposition'] || ''

      name = (disposition =~ /name="([^"]+)"/) ? Regexp.last_match(1) : nil
      filename = (disposition =~ /filename="([^"]+)"/) ? Regexp.last_match(1) : nil

      if filename
        hash[name] = { filename: filename, content_type: headers['content-type'], data: body }
      else
        hash[name] = body
      end
    end

    def parse_multipart_form_data_part_headers(part)
      headers = {}
      while true
        idx = part.index("\r\n")
        break unless idx

        header = part[0, idx]
        part.slice!(0, idx + 2)
        break if header.empty?

        next unless header =~ /^([^\:]+)\:\s?(.+)$/
        
        headers[Regexp.last_match(1).downcase] = Regexp.last_match(2)
      end
      # remove trailing \r\n
      part.slice!(part.size - 2, 2)
      [part, headers]
    end

    PARAMETER_RE = /^(.+)=(.*)$/.freeze
    MAX_PARAMETER_NAME_SIZE = 256
    MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB

    def parse_urlencoded_form_data(body)
      body.force_encoding(Encoding::UTF_8) unless body.encoding == Encoding::UTF_8
      body.split('&').each_with_object({}) do |i, m|
        raise 'Invalid parameter format' unless i =~ PARAMETER_RE
  
        k = Regexp.last_match(1)
        raise 'Invalid parameter size' if k.size > MAX_PARAMETER_NAME_SIZE
  
        v = Regexp.last_match(2)
        raise 'Invalid parameter size' if v.size > MAX_PARAMETER_VALUE_SIZE
  
        m[EscapeUtils.unescape_uri(k)] = EscapeUtils.unescape_uri(v)
      end
    end
  end
end