local table_new = require "table.new" local string_char = string.char local string_upper = string.upper local string_find = string.find local string_sub = string.sub local string_byte = string.byte local string_format = string.format local tonumber = tonumber local table_concat = table.concat local ngx_re_find = ngx.re.find local ngx_re_gsub = ngx.re.gsub -- Charset: -- reserved = "!" / "*" / "'" / "(" / ")" / ";" / ":" / -- "@" / "&" / "=" / "+" / "$" / "," / "/" / "?" / "%" / "#" / "[" / "]" -- unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -- other: * (meaning any char that is not mentioned above) -- Reserved characters have special meaning in URI. Encoding/decoding it affects the semantics of the URI; -- Unreserved characters are safe to use as part of HTTP message without encoding; -- Other characters has not special meaning but may be not safe to use as part of HTTP message without encoding; -- We should not unescape or escape reserved characters; -- We should unescape but not escape unreserved characters; -- We choose to unescape when processing and escape when forwarding for other characters local RESERVED_CHARS = "!*'();:@&=+$,/?%#[]" local chars_to_decode = table_new(256, 0) do -- reserved for i = 1, #RESERVED_CHARS do chars_to_decode[string_byte(RESERVED_CHARS, i)] = true end -- unreserved and others default to nil end local ESCAPE_PATTERN = "[^!#$&'()*+,/:;=?@[\\]A-Z\\d\\-_.~%]" local TMP_OUTPUT = table_new(16, 0) local DOT = string_byte(".") local SLASH = string_byte("/") local function normalize_decode(m) local hex = m[1] local num = tonumber(hex, 16) -- from rfc3986 we should decode unreserved character -- and we choose to decode "others" if not chars_to_decode[num] then -- is not reserved(false or nil) return string_char(num) end return string_upper(m[0]) end local function percent_escape(m) return string_format("%%%02X", string_byte(m[0])) end -- This function does slightly different things from its name. -- It ensures the output to be safe to a part of HTTP message (headers or path) -- and preserve origin semantics local function escape(uri) if ngx_re_find(uri, ESCAPE_PATTERN, "joi") then return (ngx_re_gsub(uri, ESCAPE_PATTERN, percent_escape, "joi")) end return uri end local function normalize(uri, merge_slashes) -- check for simple cases and early exit if uri == "" or uri == "/" then return uri end -- check if uri needs to be percent-decoded -- (this can in some cases lead to unnecessary percent-decoding) if string_find(uri, "%", 1, true) then -- decoding percent-encoded triplets of unreserved characters uri = ngx_re_gsub(uri, "%([\\dA-F]{2})", normalize_decode, "joi") end -- check if the uri contains a dot -- (this can in some cases lead to unnecessary dot removal processing) -- notice: it's expected that /%2e./ is considered the same of /../ if string_find(uri, ".", 1, true) == nil then if not merge_slashes then return uri end if string_find(uri, "//", 1, true) == nil then return uri end end local output_n = 0 while #uri > 0 do local FIRST = string_byte(uri, 1) local SECOND = FIRST and string_byte(uri, 2) or nil local THIRD = SECOND and string_byte(uri, 3) or nil local FOURTH = THIRD and string_byte(uri, 4) or nil if uri == "/." then -- /. uri = "/" elseif uri == "/.." then -- /.. uri = "/" if output_n > 0 then output_n = output_n - 1 end elseif uri == "." or uri == ".." then uri = "" elseif FIRST == DOT and SECOND == DOT and THIRD == SLASH then -- ../ uri = string_sub(uri, 4) elseif FIRST == DOT and SECOND == SLASH then -- ./ uri = string_sub(uri, 3) elseif FIRST == SLASH and SECOND == DOT and THIRD == SLASH then -- /./ uri = string_sub(uri, 3) elseif FIRST == SLASH and SECOND == DOT and THIRD == DOT and FOURTH == SLASH then -- /../ uri = string_sub(uri, 4) if output_n > 0 then output_n = output_n - 1 end elseif merge_slashes and FIRST == SLASH and SECOND == SLASH then -- // uri = string_sub(uri, 2) else local i = string_find(uri, "/", 2, true) output_n = output_n + 1 if i then local seg = string_sub(uri, 1, i - 1) TMP_OUTPUT[output_n] = seg uri = string_sub(uri, i) else TMP_OUTPUT[output_n] = uri uri = "" end end end if output_n == 0 then return "" end if output_n == 1 then return TMP_OUTPUT[1] end return table_concat(TMP_OUTPUT, nil, 1, output_n) end return { escape = escape, normalize = normalize, }