Files
2026-07-13 12:32:21 +08:00

174 lines
4.6 KiB
Lua

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,
}