318 lines
8.9 KiB
Lua
318 lines
8.9 KiB
Lua
local ipmatcher = require "resty.ipmatcher"
|
|
|
|
|
|
|
|
local type = type
|
|
local ipairs = ipairs
|
|
local tonumber = tonumber
|
|
local tostring = tostring
|
|
local gsub = string.gsub
|
|
local fmt = string.format
|
|
local lower = string.lower
|
|
local find = string.find
|
|
local splitn = require("kong.tools.string").splitn
|
|
local split_once = require("kong.tools.string").split_once
|
|
|
|
|
|
local _M = {}
|
|
|
|
|
|
local ipv4_prefixes = {}
|
|
for i = 0, 32 do
|
|
ipv4_prefixes[tostring(i)] = i
|
|
end
|
|
|
|
|
|
local ipv6_prefixes = {}
|
|
for i = 0, 128 do
|
|
ipv6_prefixes[tostring(i)] = i
|
|
end
|
|
|
|
|
|
local function split_cidr(cidr, prefixes)
|
|
local ip, prefix = split_once(cidr, "/")
|
|
if not ip then
|
|
return
|
|
end
|
|
|
|
return ip, prefixes[prefix]
|
|
end
|
|
|
|
|
|
local function validate(input, f1, f2, prefixes)
|
|
if type(input) ~= "string" then
|
|
return false
|
|
end
|
|
|
|
if prefixes then
|
|
local ip, prefix = split_cidr(input, prefixes)
|
|
if not ip or not prefix then
|
|
return false
|
|
end
|
|
|
|
input = ip
|
|
end
|
|
|
|
if f1(input) then
|
|
return true
|
|
end
|
|
|
|
if f2 and f2(input) then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
function _M.is_valid_ipv4(ipv4)
|
|
return validate(ipv4, ipmatcher.parse_ipv4)
|
|
end
|
|
|
|
|
|
function _M.is_valid_ipv6(ipv6)
|
|
return validate(ipv6, ipmatcher.parse_ipv6)
|
|
end
|
|
|
|
|
|
function _M.is_valid_ip(ip)
|
|
return validate(ip, ipmatcher.parse_ipv4, ipmatcher.parse_ipv6)
|
|
end
|
|
|
|
|
|
function _M.is_valid_cidr_v4(cidr_v4)
|
|
return validate(cidr_v4, ipmatcher.parse_ipv4, nil, ipv4_prefixes)
|
|
end
|
|
|
|
|
|
function _M.is_valid_cidr_v6(cidr_v6)
|
|
return validate(cidr_v6, ipmatcher.parse_ipv6, nil, ipv6_prefixes)
|
|
end
|
|
|
|
|
|
function _M.is_valid_cidr(cidr)
|
|
return validate(cidr, _M.is_valid_cidr_v4, _M.is_valid_cidr_v6)
|
|
end
|
|
|
|
|
|
function _M.is_valid_ip_or_cidr_v4(ip_or_cidr_v4)
|
|
return validate(ip_or_cidr_v4, ipmatcher.parse_ipv4, _M.is_valid_cidr_v4)
|
|
end
|
|
|
|
|
|
function _M.is_valid_ip_or_cidr_v6(ip_or_cidr_v6)
|
|
return validate(ip_or_cidr_v6, ipmatcher.parse_ipv6, _M.is_valid_cidr_v6)
|
|
end
|
|
|
|
|
|
function _M.is_valid_ip_or_cidr(ip_or_cidr)
|
|
return validate(ip_or_cidr, _M.is_valid_ip, _M.is_valid_cidr)
|
|
end
|
|
|
|
|
|
--- checks the hostname type; ipv4, ipv6, or name.
|
|
-- Type is determined by exclusion, not by validation. So if it returns 'ipv6' then
|
|
-- it can only be an ipv6, but it is not necessarily a valid ipv6 address.
|
|
-- @param name the string to check (this may contain a portnumber)
|
|
-- @return string either; 'ipv4', 'ipv6', or 'name'
|
|
-- @usage hostname_type("123.123.123.123") --> "ipv4"
|
|
-- hostname_type("::1") --> "ipv6"
|
|
-- hostname_type("some::thing") --> "ipv6", but invalid...
|
|
function _M.hostname_type(name)
|
|
local remainder, colons = gsub(name, ":", "")
|
|
if colons > 1 then
|
|
return "ipv6"
|
|
end
|
|
if remainder:match("^[%d%.]+$") then
|
|
return "ipv4"
|
|
end
|
|
return "name"
|
|
end
|
|
|
|
|
|
--- parses, validates and normalizes an ipv4 address.
|
|
-- @param address the string containing the address (formats; ipv4, ipv4:port)
|
|
-- @return normalized address (string) + port (number or nil), or alternatively nil+error
|
|
function _M.normalize_ipv4(address)
|
|
local a,b,c,d,port
|
|
if address:find(":", 1, true) then
|
|
-- has port number
|
|
a,b,c,d,port = address:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?):(%d+)$")
|
|
else
|
|
-- without port number
|
|
a,b,c,d,port = address:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$")
|
|
end
|
|
if not a then
|
|
return nil, "invalid ipv4 address: " .. address
|
|
end
|
|
a,b,c,d = tonumber(a), tonumber(b), tonumber(c), tonumber(d)
|
|
if a < 0 or a > 255 or b < 0 or b > 255 or c < 0 or
|
|
c > 255 or d < 0 or d > 255 then
|
|
return nil, "invalid ipv4 address: " .. address
|
|
end
|
|
if port then
|
|
port = tonumber(port)
|
|
if port > 65535 then
|
|
return nil, "invalid port number"
|
|
end
|
|
end
|
|
|
|
return fmt("%d.%d.%d.%d",a,b,c,d), port
|
|
end
|
|
|
|
|
|
--- parses, validates and normalizes an ipv6 address.
|
|
-- @param address the string containing the address (formats; ipv6, [ipv6], [ipv6]:port)
|
|
-- @return normalized expanded address (string) + port (number or nil), or alternatively nil+error
|
|
function _M.normalize_ipv6(address)
|
|
local check, port = address:match("^(%b[])(.-)$")
|
|
if port == "" then
|
|
port = nil
|
|
end
|
|
if check then
|
|
check = check:sub(2, -2) -- drop the brackets
|
|
-- we have ipv6 in brackets, now get port if we got something left
|
|
if port then
|
|
port = port:match("^:(%d-)$")
|
|
if not port then
|
|
return nil, "invalid ipv6 address"
|
|
end
|
|
port = tonumber(port)
|
|
if port > 65535 then
|
|
return nil, "invalid port number"
|
|
end
|
|
end
|
|
else
|
|
-- no brackets, so full address only; no brackets, no port
|
|
check = address
|
|
port = nil
|
|
end
|
|
-- check ipv6 format and normalize
|
|
if check:sub(1,1) == ":" then
|
|
check = "0" .. check
|
|
end
|
|
if check:sub(-1,-1) == ":" then
|
|
check = check .. "0"
|
|
end
|
|
if check:find("::", 1, true) then
|
|
-- expand double colon
|
|
local _, count = gsub(check, ":", "")
|
|
local ins = ":" .. string.rep("0:", 8 - count)
|
|
check = gsub(check, "::", ins, 1) -- replace only 1 occurence!
|
|
end
|
|
local a,b,c,d,e,f,g,h = check:match("^(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?):(%x%x?%x?%x?)$")
|
|
if not a then
|
|
-- not a valid IPv6 address
|
|
return nil, "invalid ipv6 address: " .. address
|
|
end
|
|
local zeros = "0000"
|
|
return lower(fmt("%s:%s:%s:%s:%s:%s:%s:%s",
|
|
zeros:sub(1, 4 - #a) .. a,
|
|
zeros:sub(1, 4 - #b) .. b,
|
|
zeros:sub(1, 4 - #c) .. c,
|
|
zeros:sub(1, 4 - #d) .. d,
|
|
zeros:sub(1, 4 - #e) .. e,
|
|
zeros:sub(1, 4 - #f) .. f,
|
|
zeros:sub(1, 4 - #g) .. g,
|
|
zeros:sub(1, 4 - #h) .. h)), port
|
|
end
|
|
|
|
|
|
--- parses and validates a hostname.
|
|
-- @param address the string containing the hostname (formats; name, name:port)
|
|
-- @return hostname (string) + port (number or nil), or alternatively nil+error
|
|
function _M.check_hostname(address)
|
|
local name = address
|
|
local port = address:match(":(%d+)$")
|
|
if port then
|
|
name = name:sub(1, -(#port+2))
|
|
port = tonumber(port)
|
|
if port > 65535 then
|
|
return nil, "invalid port number"
|
|
end
|
|
end
|
|
local match = name:match("^[%d%a%-%.%_]+$")
|
|
if match == nil then
|
|
return nil, "invalid hostname: " .. address
|
|
end
|
|
|
|
-- Reject prefix/trailing dashes and dots in each segment
|
|
-- notes:
|
|
-- - punycode allows prefixed dash, if the characters before the dash are escaped
|
|
-- - FQDN can end in dots
|
|
local t, count = splitn(name, ".")
|
|
for index, segment in ipairs(t) do
|
|
if segment:match("-$") or segment:match("^%.") or segment:match("%.$") or
|
|
(segment == "" and index ~= count) then
|
|
return nil, "invalid hostname: " .. address
|
|
end
|
|
end
|
|
return name, port
|
|
end
|
|
|
|
|
|
local verify_types = {
|
|
ipv4 = _M.normalize_ipv4,
|
|
ipv6 = _M.normalize_ipv6,
|
|
name = _M.check_hostname,
|
|
}
|
|
|
|
|
|
--- verifies and normalizes ip adresses and hostnames. Supports ipv4, ipv4:port, ipv6, [ipv6]:port, name, name:port.
|
|
-- Returned ipv4 addresses will have no leading zero's, ipv6 will be fully expanded without brackets.
|
|
-- Note: a name will not be normalized!
|
|
-- @param address string containing the address
|
|
-- @return table with the following fields: `host` (string; normalized address, or name), `type` (string; 'ipv4', 'ipv6', 'name'), and `port` (number or nil), or alternatively nil+error on invalid input
|
|
function _M.normalize_ip(address)
|
|
local atype = _M.hostname_type(address)
|
|
local addr, port = verify_types[atype](address)
|
|
if not addr then
|
|
return nil, port
|
|
end
|
|
return {
|
|
type = atype,
|
|
host = addr,
|
|
port = port,
|
|
}
|
|
end
|
|
|
|
|
|
--- Formats an ip address or hostname with an (optional) port for use in urls.
|
|
-- Supports ipv4, ipv6 and names.
|
|
--
|
|
-- Explicitly accepts 'nil+error' as input, to pass through any errors from the normalizing and name checking functions.
|
|
-- @param p1 address to format, either string with name/ip, table returned from `normalize_ip`, or from the `socket.url` library.
|
|
-- @param p2 port (optional) if p1 is a table, then this port will be inserted if no port-field is in the table
|
|
-- @return formatted address or nil+error
|
|
-- @usage
|
|
-- local addr, err = format_ip(normalize_ip("001.002.003.004:123")) --> "1.2.3.4:123"
|
|
-- local addr, err = format_ip(normalize_ip("::1")) --> "[0000:0000:0000:0000:0000:0000:0000:0001]"
|
|
-- local addr, err = format_ip("::1", 80)) --> "[::1]:80"
|
|
-- local addr, err = format_ip(check_hostname("//bad .. name\\")) --> nil, "invalid hostname: ... "
|
|
function _M.format_host(p1, p2)
|
|
local t = type(p1)
|
|
if t == "nil" then
|
|
return p1, p2 -- just pass through any errors passed in
|
|
end
|
|
local host, port, typ
|
|
if t == "table" then
|
|
port = p1.port or p2
|
|
host = p1.host
|
|
typ = p1.type or _M.hostname_type(host)
|
|
elseif t == "string" then
|
|
port = p2
|
|
host = p1
|
|
typ = _M.hostname_type(host)
|
|
else
|
|
return nil, "cannot format type '" .. t .. "'"
|
|
end
|
|
if typ == "ipv6" and not find(host, "[", nil, true) then
|
|
return "[" .. _M.normalize_ipv6(host) .. "]" .. (port and ":" .. port or "")
|
|
else
|
|
return host .. (port and ":" .. port or "")
|
|
end
|
|
end
|
|
|
|
|
|
return _M
|