Added lua IP library

This commit is contained in:
Filip Znachor 2022-04-14 01:54:40 +02:00
parent b38ab01247
commit 4a15a1c897

504
DynDNS/ip.lua Normal file
View file

@ -0,0 +1,504 @@
--
-- IP address manipulation library in Lua
--
-- @author leite (xico@simbio.se)
-- @license MIT
-- @copyright Simbiose 2015
local math, string, table, bit_available, bit =
require [[math]], require [[string]], require [[table]], pcall(require, 'bit')
-- yey! Lua 5.3 bitwise keywords available
if not bit_available and _VERSION == 'Lua 5.3' then
bit = assert(load([[return {
band = function (a, b) return a & b end,
bor = function (a, b) return a | b end,
rshift = function (a, b) return a >> b end,
lshift = function (a, b) return a << b end
}]]))()
end
local ip, modf, len, match, find, format, concat, insert, band, bor, rshift, lshift, type,
assert, error, tonumber, pcall, setmetatable =
{}, math.modf, string.len, string.match, string.find, string.format, table.concat, table.insert,
bit.band, bit.bor, bit.rshift, bit.lshift, type, assert, error, tonumber, pcall, setmetatable
local _octets, _octet, _part, _parts_with_octets, EMPTY, COLON, ZERO =
'^((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)%.((0?[xX]?)[%da-fA-F]+)/?(%d*)$',
'^((0?[xX]?)[%da-fA-F]+)((/?)(%d*))$', '(:?)([^:/$]*)(:?)',
'^([:%dxXa-fA-F]-::?)(([%dxXa-fA-F]*)[%.%dxXa-fA-F]*)/?(%d*)$', '', ':', '0'
math, string, bit, table = nil, nil, nil, nil
-- IP special ranges
local special_ranges = {
ipv4 = {
{'unspecified', octets={0, 0, 0, 0}, _cidr=8},
{'broadcast', octets={255, 255, 255, 255}, _cidr=32},
{'multicast', octets={224, 0, 0, 0}, _cidr=4},
{'linkLocal', octets={169, 254, 0, 0}, _cidr=16},
{'loopback', octets={127, 0, 0, 0}, _cidr=8},
{
'private', {octets={10, 0, 0, 0}, _cidr=8}, {octets={172, 16, 0, 0}, _cidr=12},
{octets={192, 168, 0, 0}, _cidr=16}
}, {
'reserved', {octets={192, 0, 0, 0}, _cidr=24}, {octets={192, 0, 2, 0}, _cidr=24},
{octets={192, 88, 99, 0}, _cidr=24}, {octets={198, 51, 100, 0}, _cidr=24},
{octets={203, 0, 113, 0}, _cidr=24}, {octets={240, 0, 0, 0}, _cidr=4}
}
}, ipv6 = {
{'unspecified', parts={0, 0, 0, 0, 0, 0, 0, 0}, _cidr=128},
{'linkLocal', parts={0xfe80, 0, 0, 0, 0, 0, 0, 0}, _cidr=10},
{'multicast', parts={0xff00, 0, 0, 0, 0, 0, 0, 0}, _cidr=8},
{'loopback', parts={0, 0, 0, 0, 0, 0, 0, 1}, _cidr=128},
{'uniqueLocal', parts={0xfc00, 0, 0, 0, 0, 0, 0, 0}, _cidr=7},
{'ipv4Mapped', parts={0, 0, 0, 0, 0, 0xffff, 0, 0}, _cidr=96},
{'rfc6145', parts={0, 0, 0, 0, 0xffff, 0, 0, 0}, _cidr=96},
{'rfc6052', parts={0x64, 0xff9b, 0, 0, 0, 0, 0, 0}, _cidr=96},
{'6to4', parts={0x2002, 0, 0, 0, 0, 0, 0, 0}, _cidr=16},
{'teredo', parts={0x2001, 0, 0, 0, 0, 0, 0, 0}, _cidr=32},
{'reserved', parts={0x2001, 0xdb8, 0, 0, 0, 0, 0, 0}, _cidr=32}
}
}
-- assert ipv4 octets
--
-- @table octets
-- @return boolean, [string]
local function assert_ipv4 (octets)
if not(octets and type(octets) == 'table') then
return false, 'octets should be a table'
end
if not(#octets == 4) then
return false, 'ipv4 octet count should be 4'
end
if not((-1 < octets[1] and 256 > octets[1]) and (-1 < octets[2] and 256 > octets[2]) and
(-1 < octets[3] and 256 > octets[3]) and (-1 < octets[4] and 256 > octets[4])) then
return false, 'ipv4 octet is a byte'
end
return true
end
-- assert ipv6 parts
--
-- @table parts
-- @return boolean, [string]
local function assert_ipv6 (parts)
if not(parts and type(parts) == 'table') then
return false, 'parts should be a table'
end
if not(#parts == 8) then
return false, 'ipv6 part count should be 8'
end
if not((-1 < parts[1] and 0x10000 > parts[1]) and (-1 < parts[2] and 0x10000 > parts[2]) and
(-1 < parts[3] and 0x10000 > parts[3]) and (-1 < parts[4] and 0x10000 > parts[4]) and
(-1 < parts[5] and 0x10000 > parts[5]) and (-1 < parts[6] and 0x10000 > parts[6]) and
(-1 < parts[7] and 0x10000 > parts[7]) and (-1 < parts[8] and 0x10000 > parts[8])) then
return false, 'ipv6 part should fit to two octets'
end
return true
end
-- generic CIDR matcher
--
-- @table first
-- @table second
-- @number part_size
-- @number cidr_bits
-- @return boolean
local function match_cidr(first, second, part_size, cidr_bits)
assert(#first == #second, 'cannot match CIDR for objects with different lengths')
local part, shift = 0, 0
while cidr_bits > 0 do
part = part + 1
shift = part_size - cidr_bits
shift = shift < 0 and 0 or shift
if rshift(first[part], shift) ~= rshift(second[part], shift) then
return false
end
cidr_bits = cidr_bits - part_size
end
return true
end
-- funct address named range matching
--
-- @table address
-- @table range_list
-- @string default_name
-- @return string
local function subnet_match(address, range_list, default_name)
local subnet = {}
for i = 1, #range_list do
subnet = range_list[i]
if #subnet == 1 then
if address:match(subnet) then
return subnet[1]
end
else
for j = 2, #subnet do
if address:match(subnet[j]) then
return subnet[1]
end
end
end
end
return default_name or 'unicast'
end
-- parse IP version 4
--
-- @string string
-- @table octets
-- @number cidr
-- @return boolean, [string]
local function parse_v4(string, octets, cidr)
local value, hex, _, __, _cidr = match(string, _octet)
if value then
value = tonumber(value, hex == ZERO and 8 or nil)
if value > 0xffffffff or value < 0 then
return false, 'address outside defined range'
end
octets[1], octets[2], octets[3], octets[4] =
band(rshift(value, 24), 0xff), band(rshift(value, 16), 0xff),
band(rshift(value, 8), 0xff), band(value, 0xff)
return tonumber(_cidr == EMPTY and 32 or _cidr)
end
local st, _st, nd, _nd, rd, _rd, th, _th, _cidr = match(string, _octets)
if not(st) then
return false, 'invalid ip address'
end
octets[1], octets[2], octets[3], octets[4] =
tonumber(st, _st == ZERO and 8 or nil), tonumber(nd, _nd == ZERO and 8 or nil),
tonumber(rd, _rd == ZERO and 8 or nil), tonumber(th, _th == ZERO and 8 or nil)
return tonumber(_cidr == EMPTY and (cidr and cidr or 32) or _cidr)
end
-- parse IP version 6
--
-- @string string
-- @table parts
-- @table octets
-- @number cidr
-- @return boolean, [string]
local function parse_v6(string, parts, octets, cidr)
local nd_sep, part, l_sep, count, double, length, index, last, string, octets_st, sep, _cidr =
'', '', false, 1, 0, 0, 0, 0, match(string, _parts_with_octets)
if not string or EMPTY == string then
return false, 'invalid ipv6 format'
end
if #octets_st == #sep then
string = string .. sep
else
local err, message = parse_v4(octets_st, octets)
if not err then
return err, message
end
end
_cidr, length, index, last, sep, part, nd_sep =
tonumber(_cidr==EMPTY and (cidr and cidr or 128) or _cidr), len(string), find(string, _part)
while index and index <= length do
if sep == COLON and nd_sep == COLON then
if part == EMPTY or l_sep then
if double > 0 then
return false, 'string is not formatted like ip address'
end
double = count
end
elseif sep == COLON or nd_sep == COLON then
if l_sep and sep == COLON then
if double > 0 then
return false, 'string is not formatted like ip address'
end
double = count
end
end
insert(parts, tonumber(part == EMPTY and '0' or part, 16))
l_sep, count, index, last, sep, part, nd_sep =
nd_sep == COLON, count + 1, find(string, _part, last + 1)
end
if #octets > 0 then
insert(parts, bor(lshift(octets[1], 8), octets[2]))
insert(parts, bor(lshift(octets[3], 8), octets[4]))
length = 7
else
length = 9
end
for index = 1, (length - count) do
insert(parts, double, 0)
end
return _cidr
end
-- ip metatable
local ip_metatable = {
-- set CIDR
--
-- @number cidr
-- @return metatable
cidr = function(self, cidr)
self._cidr = cidr
return self
end,
-- get address named range
--
-- @return string
range = function(self)
return subnet_match(self, special_ranges[self:kind()])
end,
-- get or match address kind
--
-- @string [kind]
-- @return string|boolean
kind = function(self, kind)
local _kind = #self.parts > 0 and 'ipv6' or (#self.octets > 0 and 'ipv4' or EMPTY)
if kind then
return kind == _kind
end
return _kind
end,
-- match two addresses
--
-- @table address
-- @number cidr
-- @return boolean
match = function(self, address, cidr)
if cidr and address._cidr then
address._cidr = cidr
end
return self.__eq(self, address)
end,
-- converts ipv4 to ipv4-mapped ipv6 address
--
-- @return string|nil
ipv4_mapped_address = function (self)
return self:kind('ipv4') and ip.parsev6('::ffff:' .. self:__tostring()) or nil
end,
-- check if it's a ipv4 mapped address
--
-- @return boolean
is_ipv4_mapped = function (self)
return self:range() == 'ipv4Mapped'
end,
-- converts ipv6 ipv4-mapped address to ipv4 address
--
-- @return metatable
ipv4_address = function (self)
assert(self:is_ipv4_mapped(), 'trying to convert a generic ipv6 address to ipv4')
local high, low = self.parts[7], self.parts[8]
return ip.v4({rshift(high, 8), band(high, 0xff), rshift(low, 8), band(low, 0xff)})
end,
-- IP table to string
--
-- @return string
__tostring = function(self)
if self:kind('ipv4') then
return concat(self.octets, '.')
end
local part, state, size, output = '', 0, #self.parts, {}
for i = 1, size do
part = format('%x', self.parts[i])
if 0 == state then
insert(output, (ZERO == part and EMPTY or part))
state = 1
elseif 1 == state then
if ZERO == part then
state = 2
else
insert(output, part)
end
elseif 2 == state then
if ZERO ~= part then
insert(output, EMPTY)
insert(output, part)
state = 3
end
else
insert(output, part)
end
end
if 2 == state then
insert(output, COLON)
end
return concat(output, COLON)
end,
-- compare two IP addresses
--
-- @table value
-- @return boolean
__eq = function(self, value)
if #self.parts > 0 then
assert(value.parts and #value.parts > 0, 'cannot match different address version')
return match_cidr(self.parts, value.parts, 16, value._cidr)
end
assert(value.octets and #value.octets > 0, 'cannot match different address version')
return match_cidr(self.octets, value.octets, 8, value._cidr)
end
}
ip_metatable.__index = ip_metatable
-- create new IP metatable
--
-- @table parts
-- @table octets
-- @number cidr
-- @return metatable
local function new (parts, octets, cidr)
return setmetatable({octets=octets, parts=parts, _cidr=cidr}, ip_metatable)
end
-- assert IP version 4 octets and create it's metatable
--
-- @table octets
-- @number cidr
-- @return metatable
function ip.v4 (octets, cidr)
local err, message = assert_ipv4(octets)
assert(err, message)
return new({}, octets, cidr or 32)
end
-- assert IP version 6 parts and create it's metatable
--
-- @table parts
-- @number cidr
-- @table [octets]
-- @return metatable
function ip.v6 (parts, cidr, octets)
local err, message = assert_ipv6(parts)
assert(err, message)
if octets and #octets > 0 then
err, message = assert_ipv4(octets)
assert(err, message)
end
return new(parts, octets or {}, cidr or 128)
end
-- parse string to IP version 4 metatable
--
-- @string string
-- @number [cidr]
-- @return metatable
function ip.parsev4 (string, cidr)
local octets, message = {}, ''
cidr, message = parse_v4(string, octets, cidr)
assert(cidr ~= false, message)
return ip.v4(octets, cidr)
end
-- parse string to IP version 6 metatable
--
-- @string string
-- @number [cidr]
-- @return metatable
function ip.parsev6 (string, cidr)
local parts, octets, message = {}, {}, ''
cidr, message = parse_v6(string, parts, octets, cidr)
assert(cidr ~= false, message)
return ip.v6(parts, cidr, octets)
end
-- check and parse string to IP metatable
--
-- @string string
-- @return metatable
function ip.parse (string)
if ip.isv6(string) then
return ip.parsev6(string)
elseif ip.isv4(string) then
return ip.parsev4(string)
end
error('the address has neither IPv6 nor IPv4 format')
end
-- check if string is a IP version 4 address
--
-- @string string
-- @boolean validate
-- @return boolean
function ip.isv4 (string, validate)
if validate then
local octets = {}
return parse_v4(string, octets) ~= false and assert_ipv4(octets)
end
return find(string, _octet) ~= nil or find(string, _octets) ~= nil
end
-- check if string is a IP version 6 address
--
-- @string string
-- @boolean validate
-- @return boolean
function ip.isv6 (string, validate)
if validate then
local octets, parts = {}, {}
return parse_v6(string, parts, octets) ~= false and assert_ipv6(parts)
end
return find(string, _parts_with_octets) ~= nil
end
-- check if IP address is valid
--
-- @string string
-- @return boolean
function ip.valid (string)
return pcall(ip.parse, string)
end
return ip