mirror of https://github.com/sipwise/lua-uri.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
508 lines
15 KiB
508 lines
15 KiB
local M = { _NAME = "uri", VERSION = "1.0" }
|
|
M.__index = M
|
|
|
|
local Util = require "uri._util"
|
|
|
|
local _UNRESERVED = "A-Za-z0-9%-._~"
|
|
local _GEN_DELIMS = ":/?#%[%]@"
|
|
local _SUB_DELIMS = "!$&'()*+,;="
|
|
local _RESERVED = _GEN_DELIMS .. _SUB_DELIMS
|
|
local _USERINFO = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":]*$"
|
|
local _REG_NAME = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. "]*$"
|
|
local _IP_FUTURE_LITERAL = "^v[0-9A-Fa-f]+%." ..
|
|
"[" .. _UNRESERVED .. _SUB_DELIMS .. "]+$"
|
|
local _QUERY_OR_FRAG = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?]*$"
|
|
local _PATH_CHARS = "^[" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/]*$"
|
|
|
|
local function _normalize_percent_encoding (s)
|
|
if s:find("%%$") or s:find("%%.$") then
|
|
error("unfinished percent encoding at end of URI '" .. s .. "'", 3)
|
|
end
|
|
|
|
return s:gsub("%%(..)", function (hex)
|
|
if not hex:find("^[0-9A-Fa-f][0-9A-Fa-f]$") then
|
|
error("invalid percent encoding '%" .. hex ..
|
|
"' in URI '" .. s .. "'", 5)
|
|
end
|
|
|
|
-- Never percent-encode unreserved characters, and always use uppercase
|
|
-- hexadecimal for percent encoding. RFC 3986 section 6.2.2.2.
|
|
local char = string.char(tonumber("0x" .. hex))
|
|
return char:find("^[" .. _UNRESERVED .. "]") and char or "%" .. hex:upper()
|
|
end)
|
|
end
|
|
|
|
local function _is_ip4_literal (s)
|
|
if not s:find("^[0-9]+%.[0-9]+%.[0-9]+%.[0-9]+$") then return false end
|
|
|
|
for dec_octet in s:gmatch("[0-9]+") do
|
|
if dec_octet:len() > 3 or dec_octet:find("^0.") or
|
|
tonumber(dec_octet) > 255 then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function _is_ip6_literal (s)
|
|
local had_elipsis = false -- true when '::' found
|
|
local num_chunks = 0
|
|
while s ~= "" do
|
|
num_chunks = num_chunks + 1
|
|
local p1, p2 = s:find("::?")
|
|
local chunk
|
|
if p1 then
|
|
chunk = s:sub(1, p1 - 1)
|
|
s = s:sub(p2 + 1)
|
|
if p2 ~= p1 then -- found '::'
|
|
if had_elipsis then return false end -- two of '::'
|
|
had_elipsis = true
|
|
if chunk == "" then num_chunks = num_chunks - 1 end
|
|
else
|
|
if chunk == "" then return false end -- ':' at start
|
|
if s == "" then return false end -- ':' at end
|
|
end
|
|
else
|
|
chunk = s
|
|
s = ""
|
|
end
|
|
|
|
-- Chunk is neither 4-digit hex num, nor IPv4address in last chunk.
|
|
if (not chunk:find("^[0-9a-f]+$") or chunk:len() > 4) and
|
|
(s ~= "" or not _is_ip4_literal(chunk)) and
|
|
chunk ~= "" then
|
|
return false
|
|
end
|
|
|
|
-- IPv4address in last position counts for two chunks of hex digits.
|
|
if chunk:len() > 4 then num_chunks = num_chunks + 1 end
|
|
end
|
|
|
|
if had_elipsis then
|
|
if num_chunks > 7 then return false end
|
|
else
|
|
if num_chunks ~= 8 then return false end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function _is_valid_host (host)
|
|
if host:find("^%[.*%]$") then
|
|
local ip_literal = host:sub(2, -2)
|
|
if ip_literal:find("^v") then
|
|
if not ip_literal:find(_IP_FUTURE_LITERAL) then
|
|
return "invalid IPvFuture literal '" .. ip_literal .. "'"
|
|
end
|
|
else
|
|
if not _is_ip6_literal(ip_literal) then
|
|
return "invalid IPv6 address '" .. ip_literal .. "'"
|
|
end
|
|
end
|
|
elseif not _is_ip4_literal(host) and not host:find(_REG_NAME) then
|
|
return "invalid host value '" .. host .. "'"
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function _normalize_and_check_path (s, normalize)
|
|
if not s:find(_PATH_CHARS) then return false end
|
|
if not normalize then return s end
|
|
|
|
-- Remove unnecessary percent encoding for path values.
|
|
-- TODO - I think this should be HTTP-specific (probably file also).
|
|
--s = Util.uri_decode(s, _SUB_DELIMS .. ":@")
|
|
|
|
return Util.remove_dot_segments(s)
|
|
end
|
|
|
|
function M.new (class, uri, base)
|
|
if not class or not uri then
|
|
error("usage: URI:new(uristring, [baseuri])", 2)
|
|
end
|
|
if type(uri) ~= "string" then uri = tostring(uri) end
|
|
|
|
if base then
|
|
local uri, err = M.new(class, uri)
|
|
if not uri then return nil, err end
|
|
if type(base) ~= "table" then
|
|
base, err = M.new(class, base)
|
|
if not base then return nil, "error parsing base URI: " .. err end
|
|
end
|
|
if base:is_relative() then return nil, "base URI must be absolute" end
|
|
local ok, err = pcall(uri.resolve, uri, base)
|
|
if not ok then return nil, err end
|
|
return uri
|
|
end
|
|
|
|
local s = _normalize_percent_encoding(uri)
|
|
|
|
local _, p
|
|
local scheme, authority, userinfo, host, port, path, query, fragment
|
|
|
|
_, p, scheme = s:find("^([a-zA-Z][-+.a-zA-Z0-9]*):")
|
|
if scheme then
|
|
scheme = scheme:lower()
|
|
s = s:sub(p + 1)
|
|
end
|
|
|
|
_, p, authority = s:find("^//([^/?#]*)")
|
|
if authority then
|
|
s = s:sub(p + 1)
|
|
|
|
_, p, userinfo = authority:find("^([^@]*)@")
|
|
if userinfo then
|
|
if not userinfo:find(_USERINFO) then
|
|
return nil, "invalid userinfo value '" .. userinfo .. "'"
|
|
end
|
|
authority = authority:sub(p + 1)
|
|
end
|
|
|
|
p, _, port = authority:find(":([0-9]*)$")
|
|
if port then
|
|
port = (port ~= "") and tonumber(port) or nil
|
|
authority = authority:sub(1, p - 1)
|
|
end
|
|
|
|
host = authority:lower()
|
|
local err = _is_valid_host(host)
|
|
if err then return nil, err end
|
|
end
|
|
|
|
_, p, path = s:find("^([^?#]*)")
|
|
if path ~= "" then
|
|
local normpath = _normalize_and_check_path(path, scheme)
|
|
if not normpath then return nil, "invalid path '" .. path .. "'" end
|
|
path = normpath
|
|
s = s:sub(p + 1)
|
|
end
|
|
|
|
_, p, query = s:find("^%?([^#]*)")
|
|
if query then
|
|
s = s:sub(p + 1)
|
|
if not query:find(_QUERY_OR_FRAG) then
|
|
return nil, "invalid query value '?" .. query .. "'"
|
|
end
|
|
end
|
|
|
|
_, p, fragment = s:find("^#(.*)")
|
|
if fragment then
|
|
if not fragment:find(_QUERY_OR_FRAG) then
|
|
return nil, "invalid fragment value '#" .. fragment .. "'"
|
|
end
|
|
end
|
|
|
|
local o = {
|
|
_scheme = scheme,
|
|
_userinfo = userinfo,
|
|
_host = host,
|
|
_port = port,
|
|
_path = path,
|
|
_query = query,
|
|
_fragment = fragment,
|
|
}
|
|
setmetatable(o, scheme and class or (require "uri._relative"))
|
|
|
|
return o:init()
|
|
end
|
|
|
|
function M.uri (self, ...)
|
|
local uri = self._uri
|
|
|
|
if not uri then
|
|
local scheme = self:scheme()
|
|
if scheme then
|
|
uri = scheme .. ":"
|
|
else
|
|
uri = ""
|
|
end
|
|
|
|
local host, port, userinfo = self:host(), self._port, self:userinfo()
|
|
if host or port or userinfo then
|
|
uri = uri .. "//"
|
|
if userinfo then uri = uri .. userinfo .. "@" end
|
|
if host then uri = uri .. host end
|
|
if port then uri = uri .. ":" .. port end
|
|
end
|
|
|
|
local path = self:path()
|
|
if uri == "" and path:find("^[^/]*:") then
|
|
path = "./" .. path
|
|
end
|
|
|
|
uri = uri .. path
|
|
if self:query() then uri = uri .. "?" .. self:query() end
|
|
if self:fragment() then uri = uri .. "#" .. self:fragment() end
|
|
|
|
self._uri = uri -- cache
|
|
end
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if not new then error("URI can't be set to nil", 2) end
|
|
local newuri, err = M:new(new)
|
|
if not newuri then
|
|
error("new URI string is invalid (" .. err .. ")", 2)
|
|
end
|
|
setmetatable(self, getmetatable(newuri))
|
|
for k in pairs(self) do self[k] = nil end
|
|
for k, v in pairs(newuri) do self[k] = v end
|
|
end
|
|
|
|
return uri
|
|
end
|
|
|
|
function M.__tostring (self) return self:uri() end
|
|
|
|
function M.eq (a, b)
|
|
if type(a) == "string" then a = assert(M:new(a)) end
|
|
if type(b) == "string" then b = assert(M:new(b)) end
|
|
return a:uri() == b:uri()
|
|
end
|
|
|
|
function M.scheme (self, ...)
|
|
local old = self._scheme
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if not new then error("can't remove scheme from absolute URI", 2) end
|
|
if type(new) ~= "string" then new = tostring(new) end
|
|
if not new:find("^[a-zA-Z][-+.a-zA-Z0-9]*$") then
|
|
error("invalid scheme '" .. new .. "'", 2)
|
|
end
|
|
Util.do_class_changing_change(self, M, "scheme", new,
|
|
function (uri, new) uri._scheme = new end)
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.userinfo (self, ...)
|
|
local old = self._userinfo
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if new then
|
|
if not new:find(_USERINFO) then
|
|
error("invalid userinfo value '" .. new .. "'", 2)
|
|
end
|
|
new = _normalize_percent_encoding(new)
|
|
end
|
|
self._userinfo = new
|
|
if new and not self._host then self._host = "" end
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.host (self, ...)
|
|
local old = self._host
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if new then
|
|
new = tostring(new):lower()
|
|
local err = _is_valid_host(new)
|
|
if err then error(err, 2) end
|
|
else
|
|
if self._userinfo or self._port then
|
|
error("there must be a host if there is a userinfo or port," ..
|
|
" although it can be the empty string", 2)
|
|
end
|
|
end
|
|
self._host = new
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.port (self, ...)
|
|
local old = self._port or self:default_port()
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if new then
|
|
if type(new) == "string" then new = tonumber(new) end
|
|
if not new then error("port number must be a number", 2) end
|
|
if new < 0 then error("port number must not be negative", 2) end
|
|
local newint = new - new % 1
|
|
if newint ~= new then error("port number not integer", 2) end
|
|
if new == self:default_port() then new = nil end
|
|
end
|
|
self._port = new
|
|
if new and not self._host then self._host = "" end
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.path (self, ...)
|
|
local old = self._path
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ... or ""
|
|
new = _normalize_percent_encoding(new)
|
|
new = Util.uri_encode(new, "^A-Za-z0-9%-._~%%!$&'()*+,;=:@/")
|
|
if self._host then
|
|
if new ~= "" and not new:find("^/") then
|
|
error("path must begin with '/' when there is an authority", 2)
|
|
end
|
|
else
|
|
if new:find("^//") then new = "/%2F" .. new:sub(3) end
|
|
end
|
|
self._path = new
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.query (self, ...)
|
|
local old = self._query
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if new then
|
|
new = Util.uri_encode(new, "^" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?")
|
|
end
|
|
self._query = new
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.fragment (self, ...)
|
|
local old = self._fragment
|
|
|
|
if select("#", ...) > 0 then
|
|
local new = ...
|
|
if new then
|
|
new = Util.uri_encode(new, "^" .. _UNRESERVED .. "%%" .. _SUB_DELIMS .. ":@/?")
|
|
end
|
|
self._fragment = new
|
|
self._uri = nil
|
|
end
|
|
|
|
return old
|
|
end
|
|
|
|
function M.init (self)
|
|
local scheme_class
|
|
= Util.attempt_require("uri." .. self._scheme:gsub("[-+.]", "_"))
|
|
if scheme_class then
|
|
setmetatable(self, scheme_class)
|
|
if self._port and self._port == self:default_port() then
|
|
self._port = nil
|
|
end
|
|
-- Call the subclass 'init' method, if it has its own.
|
|
if scheme_class ~= M and self.init ~= M.init then
|
|
return self:init()
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
function M.default_port () return nil end
|
|
function M.is_relative () return false end
|
|
function M.resolve () end -- only does anything in uri._relative
|
|
|
|
-- TODO - there should probably be an option or something allowing you to
|
|
-- choose between making a link relative whenever possible (always using a
|
|
-- relative path if the scheme and authority are the same as the base URI) or
|
|
-- just using a relative reference to make the link as small as possible, which
|
|
-- might meaning using a path of '/' instead if '../../../' or whatever.
|
|
-- This method's algorithm is loosely based on the one described here:
|
|
-- http://lists.w3.org/Archives/Public/uri/2007Sep/0003.html
|
|
function M.relativize (self, base)
|
|
if type(base) == "string" then base = assert(M:new(base)) end
|
|
|
|
-- Leave it alone if we can't a relative URI, or if it would be a network
|
|
-- path reference.
|
|
if self._scheme ~= base._scheme or self._host ~= base._host or
|
|
self._port ~= base._port or self._userinfo ~= base._userinfo then
|
|
return
|
|
end
|
|
|
|
local basepath = base._path
|
|
local oldpath = self._path
|
|
-- This is to avoid trying to make a URN or something relative, which
|
|
-- is likely to lead to grief.
|
|
if not basepath:find("^/") or not oldpath:find("^/") then return end
|
|
|
|
-- Turn it into a relative reference.
|
|
self._uri = nil
|
|
self._scheme = nil
|
|
self._host = nil
|
|
self._port = nil
|
|
self._userinfo = nil
|
|
setmetatable(self, require "uri._relative")
|
|
|
|
-- Use empty path if the path in the base URI is already correct.
|
|
if oldpath == basepath then
|
|
if self._query or not base._query then
|
|
self._path = ""
|
|
else
|
|
-- An empty URI reference leaves the query string in the base URI
|
|
-- unchanged, so to get a result with no query part we have to
|
|
-- have something in the relative path.
|
|
local _, _, lastseg = oldpath:find("/([^/]+)$")
|
|
if lastseg and lastseg:find(":") then lastseg = "./" .. lastseg end
|
|
self._path = lastseg or "."
|
|
end
|
|
return
|
|
end
|
|
|
|
if oldpath == "/" or basepath == "/" then return end
|
|
|
|
local basesegs = Util.split("/", basepath:sub(2))
|
|
local oldsegs = Util.split("/", oldpath:sub(2))
|
|
|
|
if oldsegs[1] ~= basesegs[1] then return end
|
|
|
|
table.remove(basesegs)
|
|
|
|
while #oldsegs > 1 and #basesegs > 0 and oldsegs[1] == basesegs[1] do
|
|
table.remove(oldsegs, 1)
|
|
table.remove(basesegs, 1)
|
|
end
|
|
|
|
local path_naked = true
|
|
local newpath = ""
|
|
while #basesegs > 0 do
|
|
table.remove(basesegs, 1)
|
|
newpath = newpath .. "../"
|
|
path_naked = false
|
|
end
|
|
|
|
if path_naked and #oldsegs == 1 and oldsegs[1] == "" then
|
|
newpath = "./"
|
|
table.remove(oldsegs)
|
|
end
|
|
|
|
while #oldsegs > 0 do
|
|
if path_naked then
|
|
if oldsegs[1]:find(":") then
|
|
newpath = newpath .. "./"
|
|
elseif #oldsegs > 1 and oldsegs[1] == "" and oldsegs[2] == "" then
|
|
newpath = newpath .. "/."
|
|
end
|
|
end
|
|
|
|
newpath = newpath .. oldsegs[1]
|
|
path_naked = false
|
|
table.remove(oldsegs, 1)
|
|
if #oldsegs > 0 then newpath = newpath .. "/" end
|
|
end
|
|
|
|
self._path = newpath
|
|
end
|
|
|
|
return M
|
|
-- vi:ts=4 sw=4 expandtab
|