Module:IP

-- IP library -- This library contains classes for working with IP addresses and IP ranges.

-- Load modules require('Module:No globals') local bit32 = require('bit32') local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType local checkTypeMulti = libraryUtil.checkTypeMulti

-- Constants local V4 = 'IPv4' local V6 = 'IPv6'

-- Helper functions

local function makeCheckSelfFunc(className, objectName, isSelfFunc) -- Makes a function for classes to check that their methods have a valid -- self parameter. return function (methodName, selfObject) if not isSelfFunc(selfObject) then error(string.format( 'invalid %s object. Did you call %s with a dot instead of a colon, i.e. %s.%s instead of %s:%s?', className, methodName, objectName, methodName, objectName, methodName ), 3)		end end end

-- RawIP class -- Numeric representation of an IPv4 or IPv6 address. Used internally. -- A RawIP object is constructed by adding data to a Collection object and -- then giving it a new metatable. This is to avoid the memory overhead of -- copying the data to a new table.

local RawIP = {} RawIP.__index = RawIP

do -- Collection class. -- This is a table used to hold items. local Collection do local mt = { __index = { add = function (self, item) self.n = self.n + 1 self[self.n] = item end, join = function (self, sep) return table.concat(self, sep) end, }		}		Collection = function return setmetatable({n = 0}, mt) end end

-- Constructors function RawIP.newFromIPv4(ipStr) -- Return a RawIP object if ipStr is a valid IPv4 string. Otherwise, -- return nil. -- This representation is for compatibility with IPv6 addresses. local octets = Collection local s = ipStr:match('^%s*(.-)%s*$') .. '.'		for item in s:gmatch('(.-)%.') do			octets:add(item) end if octets.n == 4 then for i, s in ipairs(octets) do				if s:match('^%d+$') then local num = tonumber(s) if 0 <= num and num <= 255 then if num > 0 and s:match('^0') then -- A redundant leading zero is for an IP in octal. return nil end octets[i] = num else return nil end else return nil end end local parts = Collection for i = 1, 3, 2 do				parts:add(octets[i] * 256 + octets[i+1]) end return setmetatable(parts, RawIP) end return nil end

function RawIP.newFromIPv6(ipStr) -- Return a RawIP object if ipStr is a valid IPv6 string. Otherwise, -- return nil. ipStr = ipStr:match('^%s*(.-)%s*$') local _, n = ipStr:gsub(':', ':') if n < 7 then ipStr, n = ipStr:gsub('::', string.rep(':', 9 - n)) end local parts = Collection for item in (ipStr .. ':'):gmatch('(.-):') do			parts:add(item) end if parts.n == 8 then for i, s in ipairs(parts) do				if s == '' then parts[i] = 0 else local num = tonumber(s, 16) if num and 0 <= num and num <= 65535 then parts[i] = num else return nil end end end return setmetatable(parts, RawIP) end return nil end

function RawIP.newFromIP(ipStr) -- Return a new RawIP object from either an IPv4 string or an IPv6 -- string. If ipStr is not a valid IPv4 or IPv6 string, then return -- nil. return RawIP.newFromIPv4(ipStr) or RawIP.newFromIPv6(ipStr) end

-- Methods function RawIP:getVersion -- Return a string with the version of the IP protocol we are using. return self.n == 2 and V4 or V6	end

function RawIP:isIPv4 -- Return true if this is an IPv4 representation, and false otherwise. return self.n == 2 end

function RawIP:isIPv6 -- Return true if this is an IPv6 representation, and false otherwise. return self.n == 8 end

function RawIP:getAdjacent(previous) -- Return a RawIP object for an adjacent IP address. If previous is true -- then the previous IP is returned; otherwise the next IP is returned. -- Will wraparound: --  next      255.255.255.255 → 0.0.0.0 --            ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff → :: --  previous  0.0.0.0 → 255.255.255.255 --            :: → ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff local result = Collection result.n = self.n		local carry = previous and 0xffff or 1 for i = self.n, 1, -1 do			local sum = self[i] + carry if sum >= 0x10000 then carry = previous and 0x10000 or 1 sum = sum - 0x10000 else carry = previous and 0xffff or 0 end result[i] = sum end return setmetatable(result, RawIP) end

function RawIP:getPrefix(bitLength) -- Return a RawIP object for the prefix of the current IP Address with a -- bit length of bitLength. local result = Collection result.n = self.n		for i = 1, self.n do			if bitLength > 0 then if bitLength >= 16 then result[i] = self[i] bitLength = bitLength - 16 else result[i] = bit32.replace(self[i], 0, 0, 16 - bitLength) bitLength = 0 end else result[i] = 0 end end return setmetatable(result, RawIP) end

function RawIP:getHighestHost(bitLength) -- Return a RawIP object for the highest IP with the prefix of length -- bitLength. In other words, the network (the most-significant bits) -- is the same as the current IP's, but the host bits (the		-- least-significant bits) are all set to 1. local bits = self.n * 16 local width if bitLength <= 0 then width = bits elseif bitLength >= bits then width = 0 else width = bits - bitLength end local result = Collection result.n = self.n		for i = self.n, 1, -1 do			if width > 0 then if width >= 16 then result[i] = 0xffff width = width - 16 else result[i] = bit32.replace(self[i], 0xffff, 0, width) width = 0 end else result[i] = self[i] end end return setmetatable(result, RawIP) end

function RawIP:_makeIPv6String -- Return an IPv6 string representation of the object. Behavior is -- undefined if the current object is IPv4. local z1, z2 -- indices of run of zeros to be displayed as "::" local zstart, zcount for i = 1, 9 do -- Find left-most occurrence of longest run of two or more zeros. if i < 9 and self[i] == 0 then if zstart then zcount = zcount + 1 else zstart = i					zcount = 1 end else if zcount and zcount > 1 then if not z1 or zcount > z2 - z1 + 1 then z1 = zstart z2 = zstart + zcount - 1 end end zstart = nil zcount = nil end end local parts = Collection for i = 1, 8 do			if z1 and z1 <= i and i <= z2 then if i == z1 then if z1 == 1 or z2 == 8 then if z1 == 1 and z2 == 8 then return '::' end parts:add(':') else parts:add('') end end else parts:add(string.format('%x', self[i])) end end return parts:join(':') end

function RawIP:_makeIPv4String -- Return an IPv4 string representation of the object. Behavior is -- undefined if the current object is IPv6. local parts = Collection for i = 1, 2 do			local w = self[i] parts:add(math.floor(w / 256)) parts:add(w % 256) end return parts:join('.') end

function RawIP:__tostring -- Return a string equivalent to given IP address (IPv4 or IPv6). if self.n == 2 then return self:_makeIPv4String else return self:_makeIPv6String end end

function RawIP:__lt(obj) if self.n == obj.n then for i = 1, self.n do				if self[i] ~= obj[i] then return self[i] < obj[i] end end return false end return self.n < obj.n	end

function RawIP:__eq(obj) if self.n == obj.n then for i = 1, self.n do				if self[i] ~= obj[i] then return false end end return true end return false end end

-- Initialize private constructor functions -- -- Both IPAddress and Subnet need access to each others' private constructor -- functions. IPAddress must be able to make Subnet objects from CIDR strings -- and from RawIP objects, and Subnet must be able to make IPAddress objects -- from IP strings and from RawIP objects. These constructors must all be -- private to ensure correct error levels and to stop other modules from having -- to worry about RawIP objects. Because they are private, they must be -- initialized here.

local makeIPAddress, makeIPAddressFromRaw, makeSubnet, makeSubnetFromRaw

-- IPAddress class -- Represents a single IPv4 or IPv6 address.

local IPAddress = {}

do -- dataKey is a unique key to access objects' internal data. This is needed -- to access the RawIP objects contained in other IPAddress objects so that -- they can be compared with the current object's RawIP object. This data -- is not available to other classes or other modules. local dataKey = {}

-- Private static methods local function isIPAddressObject(val) return type(val) == 'table' and val[dataKey] ~= nil end

-- A function to check whether methods are called with a valid self -- parameter. local checkSelf = makeCheckSelfFunc(		'IPAddress',		'ipAddress',		isIPAddressObject	)

-- Metamethods that don't need upvalues local function ipEquals(ip1, ip2) return ip1[dataKey].rawIP == ip2[dataKey].rawIP end

local function ipLessThan(ip1, ip2) return ip1[dataKey].rawIP < ip2[dataKey].rawIP end

local function concatIP(ip, val) return tostring(ip) .. tostring(val) end

local function ipToString(ip) return ip:getIP end

-- Constructors makeIPAddressFromRaw = function (rawIP) -- Constructs a new IPAddress object from a rawIP object. This function -- is for internal use; it is called by IPAddress.new and from other -- IPAddress methods, and should be available to the Subnet class, but -- should not be available to other modules. assert(type(rawIP) == 'table', 'rawIP was type ' .. type(rawIP) .. '; expected type table')

-- Set up structure local obj = {} local data = {} data.rawIP = rawIP

-- Public methods function obj:getIP checkSelf('getIP', self) return tostring(data.rawIP) end

function obj:getVersion checkSelf('getVersion', self) return data.rawIP:getVersion end

function obj:isIPv4 checkSelf('isIPv4', self) return data.rawIP:isIPv4 end

function obj:isIPv6 checkSelf('isIPv6', self) return data.rawIP:isIPv6 end

function obj:isInSubnet(subnet) checkSelf('isInSubnet', self) local tp = type(subnet) if tp == 'string' then subnet = makeSubnet(subnet) elseif tp == 'table' then -- TODO: check if Subnet object else checkTypeMulti('isInSubnet', 1, subnet, {'string', 'table'}) end return subnet:containsIP(self) end

function obj:getSubnet(bitLength) checkSelf('getSubnet', self) checkType('getSubnet', 1, bitLength, 'number') return makeSubnetFromRaw(data.rawIP, bitLength) end

function obj:getNextIP checkSelf('getNextIP', self) return makeIPAddressFromRaw(data.rawIP:getAdjacent) end

function obj:getPreviousIP checkSelf('getPreviousIP', self) return makeIPAddressFromRaw(data.rawIP:getAdjacent(true)) end

-- Metamethods return setmetatable(obj, {			__eq = ipEquals,			__lt = ipLessThan,			__concat = concatIP,			__tostring = ipToString,			__index = function (self, key)				-- If any code knows the unique data key, allow it to access				-- the data table.				if key == dataKey then					return data				end			end,		}) end

makeIPAddress = function (ip) local rawIP = RawIP.newFromIP(ip) if not rawIP then error(string.format("'%s' is an invalid IP", ip), 3) end return makeIPAddressFromRaw(rawIP) end

function IPAddress.new(ip) checkType('IPAddress.new', 1, ip, 'string') return makeIPAddress(ip) end end

-- Subnet class -- Represents a block of IPv4 or IPv6 addresses.

local Subnet = {}

do -- Initialize metatable local mt = {}

-- Constructors makeSubnetFromRaw = function (rawIP, bitLength) -- Set up structure local obj = setmetatable({}, mt) local data = { rawIP = rawIP, bitLength = bitLength, }

-- Public methods function obj:getPrefix if not data.prefix then data.prefix = makeIPAddressFromRaw(					data.rawIP:getPrefix(data.bitLength)				) end return data.prefix end

function obj:getHighestIP if not data.highestIP then data.highestIP = makeIPAddressFromRaw(					data.rawIP:getHighestHost(data.bitLength)				) end return data.highestIP end

function obj:getBitLength return data.bitLength end

function obj:getCIDR return string.format(				'%s/%d',				tostring(self:getPrefix), self:getBitLength			) end

function obj:getVersion return data.rawIP:getVersion end

function obj:isIPv4 return data.rawIP:isIPv4 end

function obj:isIPv6 return data.rawIP:isIPv6 end

function obj:containsIP(ip) if self:getVersion == ip:getVersion then return self:getPrefix <= ip and ip <= self:getHighestIP end return false end

function obj:overlapsSubnet(subnet) if self:getVersion == subnet:getVersion then return (					subnet:getHighestIP >= self:getPrefix and					subnet:getPrefix <= self:getHighestIP				) end return false end

function obj:walk local started local current = self:getPrefix local highest = self:getHighestIP return function if not started then started = true return current end if current < highest then current = current:getNextIP return current end end end

return obj end

makeSubnet = function (cidr) local lhs, rhs = cidr:match('^%s*(.-)/(%d+)%s*$') if lhs then local bits = lhs:find(':', 1, true) and 128 or 32 local n = tonumber(rhs) if n and n <= bits then local base = RawIP.newFromIP(lhs) local prefix = base:getPrefix(n) if base == prefix then return makeSubnetFromRaw(prefix, n)				end end end error(string.format("'%s' is an invalid CIDR string", cidr), 3) end

function Subnet.new(cidr) checkType('Subnet.new', 1, cidr, 'string') return makeSubnet(cidr) end

-- Metamethods function mt:__eq(obj) return self:getCIDR == obj:getCIDR end

function mt:__tostring return self:getCIDR end end

return { IPAddress = IPAddress, Subnet = Subnet, }