1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
|
# frozen_string_literal: true
module Rack
class QueryParser
(require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
DEFAULT_SEP = /[&;] */n
COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
# ParameterTypeError is the error that is raised when incoming structural
# parameters (parsed by parse_nested_query) contain conflicting types.
class ParameterTypeError < TypeError; end
# InvalidParameterError is the error that is raised when incoming structural
# parameters (parsed by parse_nested_query) contain invalid format or byte
# sequence.
class InvalidParameterError < ArgumentError; end
# QueryLimitError is for errors raised when the query provided exceeds one
# of the query parser limits.
class QueryLimitError < RangeError
end
# ParamsTooDeepError is the old name for the error that is raised when params
# are recursively nested over the specified limit. Make it the same as
# as QueryLimitError, so that code that rescues ParamsTooDeepError error
# to handle bad query strings also now handles other limits.
ParamsTooDeepError = QueryLimitError
def self.make_default(key_space_limit, param_depth_limit, **options)
new(Params, key_space_limit, param_depth_limit, **options)
end
attr_reader :key_space_limit, :param_depth_limit
env_int = lambda do |key, val|
if str_val = ENV[key]
begin
val = Integer(str_val, 10)
rescue ArgumentError
raise ArgumentError, "non-integer value provided for environment variable #{key}"
end
end
val
end
BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
private_constant :BYTESIZE_LIMIT
PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
private_constant :PARAMS_LIMIT
attr_reader :bytesize_limit
def initialize(params_class, key_space_limit, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
@params_class = params_class
@key_space_limit = key_space_limit
@param_depth_limit = param_depth_limit
@bytesize_limit = bytesize_limit
@params_limit = params_limit
end
# Stolen from Mongrel, with some small modifications:
# Parses a query string by breaking it up at the '&'
# and ';' characters. You can also use this to parse
# cookies by changing the characters used in the second
# parameter (which defaults to '&;').
def parse_query(qs, d = nil, &unescaper)
unescaper ||= method(:unescape)
params = make_params
check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
next if p.empty?
k, v = p.split('=', 2).map!(&unescaper)
if cur = params[k]
if cur.class == Array
params[k] << v
else
params[k] = [cur, v]
end
else
params[k] = v
end
end
return params.to_h
end
# parse_nested_query expands a query string into structural types. Supported
# types are Arrays, Hashes and basic value types. It is possible to supply
# query strings with parameters of conflicting types, in this case a
# ParameterTypeError is raised. Users are encouraged to return a 400 in this
# case.
def parse_nested_query(qs, d = nil)
params = make_params
unless qs.nil? || qs.empty?
check_query_string(qs, d).split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
k, v = p.split('=', 2).map! { |s| unescape(s) }
normalize_params(params, k, v, param_depth_limit)
end
end
return params.to_h
rescue ArgumentError => e
raise InvalidParameterError, e.message, e.backtrace
end
# normalize_params recursively expands parameters into structural types. If
# the structural types represented by two different parameter names are in
# conflict, a ParameterTypeError is raised.
def normalize_params(params, name, v, depth)
raise ParamsTooDeepError if depth <= 0
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''
if k.empty?
if !v.nil? && name == "[]"
return Array(v)
else
return
end
end
if after == ''
params[k] = v
elsif after == "["
params[name] = v
elsif after == "[]"
params[k] ||= []
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
normalize_params(params[k].last, child_key, v, depth - 1)
else
params[k] << normalize_params(make_params, child_key, v, depth - 1)
end
else
params[k] ||= make_params
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
params[k] = normalize_params(params[k], after, v, depth - 1)
end
params
end
def make_params
@params_class.new @key_space_limit
end
def new_space_limit(key_space_limit)
self.class.new @params_class, key_space_limit, param_depth_limit
end
def new_depth_limit(param_depth_limit)
self.class.new @params_class, key_space_limit, param_depth_limit
end
private
def params_hash_type?(obj)
obj.kind_of?(@params_class)
end
def params_hash_has_key?(hash, key)
return false if /\[\]/.match?(key)
key.split(/[\[\]]+/).inject(hash) do |h, part|
next h if part == ''
return false unless params_hash_type?(h) && h.key?(part)
h[part]
end
true
end
def check_query_string(qs, sep)
if qs
if qs.bytesize > @bytesize_limit
raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
end
if (param_count = qs.count(sep.is_a?(String) ? sep : '&;')) >= @params_limit
raise QueryLimitError, "total number of query parameters (#{param_count+1}) exceeds limit (#{@params_limit})"
end
qs
else
''
end
end
def unescape(string)
Utils.unescape(string)
end
class Params
def initialize(limit)
@limit = limit
@size = 0
@params = {}
end
def [](key)
@params[key]
end
def []=(key, value)
@size += key.size if key && !@params.key?(key)
raise ParamsTooDeepError, 'exceeded available parameter key space' if @size > @limit
@params[key] = value
end
def key?(key)
@params.key?(key)
end
# Recursively unwraps nested `Params` objects and constructs an object
# of the same shape, but using the objects' internal representations
# (Ruby hashes) in place of the objects. The result is a hash consisting
# purely of Ruby primitives.
#
# Mutation warning!
#
# 1. This method mutates the internal representation of the `Params`
# objects in order to save object allocations.
#
# 2. The value you get back is a reference to the internal hash
# representation, not a copy.
#
# 3. Because the `Params` object's internal representation is mutable
# through the `#[]=` method, it is not thread safe. The result of
# getting the hash representation while another thread is adding a
# key to it is non-deterministic.
#
def to_h
@params.each do |key, value|
case value
when self
# Handle circular references gracefully.
@params[key] = @params
when Params
@params[key] = value.to_h
when Array
value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
else
# Ignore anything that is not a `Params` object or
# a collection that can contain one.
end
end
@params
end
alias_method :to_params_hash, :to_h
end
end
end
|