Module:IP
Template:Module rating Template:Used in system Module:IP is a library for working with IP addresses and subnets. It can handle both IPv4 and IPv6. The library exports four classes, IPAddress, Subnet, IPv4Collection, and IPv6Collection.
Loading the library
<syntaxhighlight lang="lua"> local IP = require('Module:IP') local IPAddress = IP.IPAddress local Subnet = IP.Subnet </syntaxhighlight>
IPAddress
The IPAddress class is used to work with single IP addresses. To create a new IPAddress object:
<syntaxhighlight lang="lua"> local ipAddress = IPAddress.new(ipString) </syntaxhighlight>
The ipString variable can be a valid IPv4 or IPv6 address.
Examples:
<syntaxhighlight lang="lua"> local ipv4Address = IPAddress.new('1.2.3.4') local ipv6Address = IPAddress.new('2001:db8::ff00:12:3456') </syntaxhighlight>
IPAddress objects can be compared with relational operators:
<syntaxhighlight lang="lua"> -- Equality IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.4') -- true IPAddress.new('1.2.3.4') == IPAddress.new('1.2.3.5') -- false
-- Less than / greater than IPAddress.new('1.2.3.4') < IPAddress.new('1.2.3.5') -- true IPAddress.new('1.2.3.4') > IPAddress.new('1.2.3.5') -- false IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.5') -- true IPAddress.new('1.2.3.4') <= IPAddress.new('1.2.3.4') -- true </syntaxhighlight>
You can use tostring on them (this is equivalent to using getIP):
<syntaxhighlight lang="lua"> tostring(IPAddress.new('1.2.3.4')) -- "1.2.3.4" tostring(IPAddress.new('2001:db8::ff00:12:3456')) -- "2001:db8::ff00:12:3456"
-- Expanded IPv6 addresses are abbreviated: tostring(IPAddress.new('2001:db8:0:0:0:0:0:0')) -- "2001:db8::" </syntaxhighlight>
You can also concatenate them:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4') .. ' foo' -- "1.2.3.4 foo" IPAddress.new('1.2.3.4') .. IPAddress.new('5.6.7.8') -- "1.2.3.45.6.7.8" </syntaxhighlight>
IPAddress objects have several methods, outlined below.
getIP
<syntaxhighlight lang="lua"> ipAddress:getIP() </syntaxhighlight>
Returns a string representation of the IP address. IPv6 addresses are abbreviated if possible.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):getIP() -- "1.2.3.4" IPAddress.new('2001:db8::ff00:12:3456'):getIP() -- "2001:db8::ff00:12:3456" IPAddress.new('2001:db8:0:0:0:0:0:0'):getIP() -- "2001:db8::" </syntaxhighlight>
getVersion
<syntaxhighlight lang="lua"> ipAddress:getVersion() </syntaxhighlight>
Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):getVersion() -- "IPv4" IPAddress.new('2001:db8::ff00:12:3456'):getVersion() -- "IPv6" </syntaxhighlight>
isIPv4
<syntaxhighlight lang="lua"> ipAddress:isIPv4() </syntaxhighlight>
Returns true if the IP address is an IPv4 address, and false otherwise.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):isIPv4() -- true IPAddress.new('2001:db8::ff00:12:3456'):isIPv4() -- false </syntaxhighlight>
isIPv6
<syntaxhighlight lang="lua"> ipAddress:isIPv6() </syntaxhighlight>
Returns true if the IP address is an IPv6 address, and false otherwise.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):isIPv6() -- false IPAddress.new('2001:db8::ff00:12:3456'):isIPv6() -- true </syntaxhighlight>
isInSubnet
<syntaxhighlight lang="lua"> ipAddress:isInSubnet(subnet) </syntaxhighlight>
Returns true if the IP address is in the subnet subnet, and false otherwise. subnet may be a Subnet object or a CIDR string.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):isInSubnet('1.2.3.0/24') -- true IPAddress.new('1.2.3.4'):isInSubnet('1.2.4.0/24') -- false IPAddress.new('1.2.3.4'):isInSubnet(Subnet.new('1.2.3.0/24')) -- true IPAddress.new('2001:db8::ff00:12:3456'):isInSubnet('2001:db8::ff00:12:0/112') -- true </syntaxhighlight>
getSubnet
<syntaxhighlight lang="lua"> ipAddress:getSubnet(bitLength) </syntaxhighlight>
Returns a Subnet object for the subnet with a bit length of bitLength which contains the current IP. The bitLength parameter must be an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):getSubnet(24) -- Equivalent to Subnet.new('1.2.3.0/24') </syntaxhighlight>
getNextIP
<syntaxhighlight lang="lua"> ipAddress:getNextIP() </syntaxhighlight>
Returns a new IPAddress object equivalent to the current IP address incremented by one. The IPv4 address "255.255.255.255" rolls around to "0.0.0.0", and the IPv6 address "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" rolls around to "::".
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):getNextIP() -- Equivalent to IPAddress.new('1.2.3.5') IPAddress.new('2001:db8::ff00:12:3456'):getNextIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3457') IPAddress.new('255.255.255.255'):getNextIP() -- Equivalent to IPAddress.new('0.0.0.0') </syntaxhighlight>
getPreviousIP
<syntaxhighlight lang="lua"> ipAddress:getPreviousIP() </syntaxhighlight>
Returns a new IPAddress object equivalent to the current IP address decremented by one. The IPv4 address "0.0.0.0" rolls around to "255.255.255.255", and the IPv6 address "::" rolls around to "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff".
Examples:
<syntaxhighlight lang="lua"> IPAddress.new('1.2.3.4'):getPreviousIP() -- Equivalent to IPAddress.new('1.2.3.3') IPAddress.new('2001:db8::ff00:12:3456'):getPreviousIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:3455') IPAddress.new('0.0.0.0'):getPreviousIP() -- Equivalent to IPAddress.new('255.255.255.255') </syntaxhighlight>
Subnet
The Subnet class is used to work with subnetworks of IPv4 or IPv6 addresses. To create a new Subnet object:
<syntaxhighlight lang="lua"> local subnet = Subnet.new(cidrString) </syntaxhighlight>
cidrString must be a valid IPv4 or IPv6 CIDR string.
Subnet objects can be compared for equality:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/24') -- true Subnet.new('1.2.3.0/24') == Subnet.new('1.2.3.0/25') -- false Subnet.new('1.2.3.0/24') == Subnet.new('2001:db8::ff00:12:0/112') -- false Subnet.new('2001:db8::ff00:12:0/112') == Subnet.new('2001:db8::ff00:12:0/112') -- true Subnet.new('2001:db8:0:0:0:0:0:0/112') == Subnet.new('2001:db8::/112') -- true </syntaxhighlight>
You can use tostring on them (this is equivalent to getCIDR): <syntaxhighlight lang="lua"> tostring(Subnet.new('1.2.3.0/24')) -- "1.2.3.0/24" tostring(Subnet.new('2001:db8::ff00:12:0/112')) -- "2001:db8::ff00:12:0/112" tostring(Subnet.new('2001:db8:0:0:0:0:0:0/112')) -- "2001:db8::/112" </syntaxhighlight>
You can also concatenate them: <syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24') .. ' foo' -- "1.2.3.0/24 foo" Subnet.new('1.2.3.0/24') .. Subnet.new('4.5.6.0/24') -- "1.2.3.0/244.5.6.0/24" </syntaxhighlight>
Subnet objects have several methods, outlined below.
getPrefix
<syntaxhighlight lang="lua"> subnet:getPrefix() </syntaxhighlight>
Returns an IPAddress object for the lowest IP address in the subnet.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):getPrefix() -- Equivalent to IPAddress.new('1.2.3.0') Subnet.new('2001:db8::ff00:12:0/112'):getPrefix() -- Equivalent to IPAddress.new('2001:db8::ff00:12:0') </syntaxhighlight>
getHighestIP
<syntaxhighlight lang="lua"> subnet:getHighestIP() </syntaxhighlight>
Returns an IPAddress object for the highest IP address in the subnet.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):getHighestIP() -- Equivalent to IPAddress.new('1.2.3.255') Subnet.new('2001:db8::ff00:12:0/112'):getHighestIP() -- Equivalent to IPAddress.new('2001:db8::ff00:12:ffff') </syntaxhighlight>
getBitLength
<syntaxhighlight lang="lua"> subnet:getBitLength() </syntaxhighlight>
Returns the bit length of the subnet. This is an integer between 0 and 32 for IPv4 addresses, or an integer between 0 and 128 for IPv6 addresses.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):getBitLength() -- 24 Subnet.new('2001:db8::ff00:12:0/112'):getBitLength() -- 112 </syntaxhighlight>
getCIDR
<syntaxhighlight lang="lua"> subnet:getCIDR() </syntaxhighlight>
Returns a CIDR string representation of the subnet.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):getCIDR() -- "1.2.3.0/24" Subnet.new('2001:db8::ff00:12:0/112'):getCIDR() -- "2001:db8::ff00:12:0/112" Subnet.new('2001:db8:0:0:0:0:0:0/112'):getCIDR() -- "2001:db8::/112" </syntaxhighlight>
getVersion
<syntaxhighlight lang="lua"> subnet:getVersion() </syntaxhighlight>
Returns the version of the IP protocol being used. This is "IPv4" for IPv4 addresses, and "IPv6" for IPv6 addresses.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):getVersion() -- "IPv4" Subnet.new('2001:db8::ff00:12:0/112'):getVersion() -- "IPv6" </syntaxhighlight>
isIPv4
<syntaxhighlight lang="lua"> subnet:isIPv4() </syntaxhighlight>
Returns true if the subnet is using IPv4, and false otherwise.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):isIPv4() -- true Subnet.new('2001:db8::ff00:12:0/112'):isIPv4() -- false </syntaxhighlight>
isIPv6
<syntaxhighlight lang="lua"> subnet:isIPv6() </syntaxhighlight>
Returns true if the subnet is using IPv6, and false otherwise.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):isIPv6() -- false Subnet.new('2001:db8::ff00:12:0/112'):isIPv6() -- true </syntaxhighlight>
containsIP
<syntaxhighlight lang="lua"> subnet:containsIP(ip) </syntaxhighlight>
Returns true if the subnet contains the IP address ip, and false otherwise. ip can be an IP address string, or an IPAddress object.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):containsIP('1.2.3.4') -- true Subnet.new('1.2.3.0/24'):containsIP('1.2.4.4') -- false Subnet.new('1.2.3.0/24'):containsIP(IPAddress.new('1.2.3.4')) -- true Subnet.new('2001:db8::ff00:12:0/112'):containsIP('2001:db8::ff00:12:3456') -- true </syntaxhighlight>
overlapsSubnet
<syntaxhighlight lang="lua"> subnet:overlapsSubnet(subnet) </syntaxhighlight>
Returns true if the current subnet overlaps with subnet, and false otherwise. subnet can be a CIDR string or a subnet object.
Examples:
<syntaxhighlight lang="lua"> Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.0.0/16') -- true Subnet.new('1.2.3.0/24'):overlapsSubnet('1.2.12.0/22') -- false Subnet.new('1.2.3.0/24'):overlapsSubnet(Subnet.new('1.2.0.0/16')) -- true Subnet.new('2001:db8::ff00:12:0/112'):overlapsSubnet('2001:db8::ff00:0:0/96') -- true </syntaxhighlight>
walk
<syntaxhighlight lang="lua"> subnet:walk() </syntaxhighlight>
The walk method iterates over all of the IPAddress objects in the subnet.
Examples:
<syntaxhighlight lang="lua"> for ipAddress in Subnet.new('192.168.0.0/30'):walk() do mw.log(tostring(ipAddress)) end -- 192.168.0.0 -- 192.168.0.1 -- 192.168.0.2 -- 192.168.0.3 </syntaxhighlight>
IPv4Collection
The IPv4Collection class is used to work with several different IPv4 addresses and IPv4 subnets. To create a new IPv4Collection object:
<syntaxhighlight lang="lua"> local collection = IPv4Collection.new() </syntaxhighlight>
IPv4Collection objects have several methods, outlined below.
getVersion
<syntaxhighlight lang="lua"> collection:getVersion() </syntaxhighlight>
Returns the string "IPv4".
addIP
<syntaxhighlight lang="lua"> collection:addIP(ip) </syntaxhighlight>
Adds an IP to the collection. The IP can be either a string or an IPAddress object.
Examples:
<syntaxhighlight lang="lua"> collection:addIP('1.2.3.4') collection:addIP(IPAddress.new('1.2.3.4')) </syntaxhighlight>
This method is chainable:
<syntaxhighlight lang="lua"> collection:addIP('1.2.3.4'):addIP('5.6.7.8') </syntaxhighlight>
addSubnet
<syntaxhighlight lang="lua"> collection:addSubnet(subnet) </syntaxhighlight>
Adds a subnet to the collection. The subnet can be either a CIDR string or a Subnet object.
Examples:
<syntaxhighlight lang="lua"> collection:addSubnet('1.2.3.0/24') collection:addSubnet(Subnet.new('1.2.3.0/24')) </syntaxhighlight>
This method is chainable:
<syntaxhighlight lang="lua"> collection:addSubnet('1.2.0.0/24'):addSubnet('1.2.1.0/24') </syntaxhighlight>
addFromString
<syntaxhighlight lang="lua"> collection:addFromString(str) </syntaxhighlight>
Extracts any IPv4 addresses and IPv4 CIDR subnets from str and adds them to the collection. Any text that is not an IPv4 address or CIDR subnet is ignored.
Examples:
<syntaxhighlight lang="lua"> collection:addFromString('Add some IPs and subnets: 1.2.3.4 1.2.3.5 2001:0::f foo 1.2.4.0/24') </syntaxhighlight>
This method is chainable:
<syntaxhighlight lang="lua"> collection:addFromString('foo 1.2.3.4'):addFromString('bar 5.6.7.8') </syntaxhighlight>
containsIP
<syntaxhighlight lang="lua"> collection:containsIP(ip) </syntaxhighlight>
Returns true if the collection contains the specified IP; otherwise returns false. The ip parameter can be a string or an IPAddress object.
Examples:
<syntaxhighlight lang="lua"> collection:containsIP('1.2.3.4') collection:containsIP(IPAddress.new('1.2.3.4')) </syntaxhighlight>
getRanges
<syntaxhighlight lang="lua"> collection:getRanges() </syntaxhighlight>
Returns a sorted array of IP pairs equivalent to the collection. Each IP pair is an array representing a contiguous range of IP addresses from pair[1] to pair[2] inclusive. pair[1] and pair[2] are IPAddress objects.
Examples:
<syntaxhighlight lang="lua"> collection:addSubnet('1.2.0.0/24') collection:addSubnet('1.2.1.0/24') collection:addSubnet('1.2.10.0/24') mw.logObject(collection:getRanges()) -- Logs the following: -- table#1 { -- table#2 { -- 1.2.0.0, -- 1.2.1.255, -- }, -- table#3 { -- 1.2.10.0, -- 1.2.10.255, -- }, -- } </syntaxhighlight>
overlapsSubnet
<syntaxhighlight lang="lua"> collection:overlapsSubnet(subnet) </syntaxhighlight>
Returns true, obj if subnet overlaps this collection, where obj is the first IPAddress or Subnet object overlapping the subnet. Otherwise, returns false. subnet can be a CIDR string or a Subnet object.
Examples:
<syntaxhighlight lang="lua"> collection:addIP('1.2.3.4') collection:overlapsSubnet('1.2.3.0/24') -- true, IPAddress.new('1.2.3.4') collection:overlapsSubnet('1.2.4.0/24') -- false </syntaxhighlight>
IPv6Collection
The IPv6Collection class is used to work with several different IPv6 addresses and IPv6 subnets. IPv6Collection objects are directly analogous to IPv4Collection objects: they contain the same methods and work the same way, but all IP addresses and subnets added to it must be IPv6, not IPv4.
To create a new IPv6Collection object:
<syntaxhighlight lang="lua"> local collection = IPv6Collection.new() </syntaxhighlight>
-- 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 local makeCheckSelfFunction = libraryUtil.makeCheckSelfFunction -- Constants local V4 = 'IPv4' local V6 = 'IPv6' -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function makeValidationFunction(className, isObjectFunc) -- Make a function for validating a specific object. return function (methodName, argIdx, arg) if not isObjectFunc(arg) then error(string.format( "bad argument #%d to '%s' (not a valid %s object)", argIdx, methodName, className ), 3) end end end -------------------------------------------------------------------------------- -- Collection class -- This is a table used to hold items. -------------------------------------------------------------------------------- local Collection = {} Collection.__index = Collection function Collection:add(item) if item ~= nil then self.n = self.n + 1 self[self.n] = item end end function Collection:join(sep) return table.concat(self, sep) end function Collection:remove(pos) if self.n > 0 and (pos == nil or (0 < pos and pos <= self.n)) then self.n = self.n - 1 return table.remove(self, pos) end end function Collection:sort(comp) table.sort(self, comp) end function Collection:deobjectify() -- Turns the collection into a plain array without any special properties -- or methods. self.n = nil setmetatable(self, nil) end function Collection.new() return setmetatable({n = 0}, Collection) 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 -- 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.new() 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.new() 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 = ipStr:gsub('::', string.rep(':', 9 - n)) end local parts = Collection.new() 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 if s:match('^%x+$') then local num = tonumber(s, 16) if num and 0 <= num and num <= 65535 then parts[i] = num else return nil end 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:getBitLength() -- Return the bit length of the IP address. return self.n * 16 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.new() 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.new() 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.new() 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 zeroes to be displayed as "::" local zstart, zcount for i = 1, 9 do -- Find left-most occurrence of longest run of two or more zeroes. 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.new() 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.new() 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 -------------------------------------------------------------------------------- -- Initialize private methods available to IPAddress and Subnet -------------------------------------------------------------------------------- -- 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 -- Objects need to be able to validate other objects that they are passed -- as input, so initialize those functions here as well. local validateCollection, validateIPAddress, validateSubnet -------------------------------------------------------------------------------- -- 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 validateIPAddress = makeValidationFunction('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 -- A function to check whether methods are called with a valid self -- parameter. local checkSelf = makeCheckSelfFunction( 'IP', 'ipAddress', obj, 'IPAddress object' ) -- Public methods function obj:getIP() checkSelf(self, 'getIP') return tostring(data.rawIP) end function obj:getVersion() checkSelf(self, 'getVersion') return data.rawIP:getVersion() end function obj:isIPv4() checkSelf(self, 'isIPv4') return data.rawIP:isIPv4() end function obj:isIPv6() checkSelf(self, 'isIPv6') return data.rawIP:isIPv6() end function obj:isInCollection(collection) checkSelf(self, 'isInCollection') validateCollection('isInCollection', 1, collection) return collection:containsIP(self) end function obj:isInSubnet(subnet) checkSelf(self, 'isInSubnet') local tp = type(subnet) if tp == 'string' then subnet = makeSubnet(subnet) elseif tp == 'table' then validateSubnet('isInSubnet', 1, subnet) else checkTypeMulti('isInSubnet', 1, subnet, {'string', 'table'}) end return subnet:containsIP(self) end function obj:getSubnet(bitLength) checkSelf(self, 'getSubnet') checkType('getSubnet', 1, bitLength, 'number') if bitLength < 0 or bitLength > data.rawIP:getBitLength() or bitLength ~= math.floor(bitLength) then error(string.format( "bad argument #1 to 'getSubnet' (must be an integer between 0 and %d)", data.rawIP:getBitLength() ), 2) end return makeSubnetFromRaw(data.rawIP, bitLength) end function obj:getNextIP() checkSelf(self, 'getNextIP') return makeIPAddressFromRaw(data.rawIP:getAdjacent()) end function obj:getPreviousIP() checkSelf(self, 'getPreviousIP') 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, __metatable = false, -- don't allow access to the metatable }) end makeIPAddress = function (ip) local rawIP = RawIP.newFromIP(ip) if not rawIP then error(string.format("'%s' is an invalid IP address", 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 -- uniqueKey is a unique, private key used to test whether a given object -- is a Subnet object. local uniqueKey = {} -- Metatable local mt = { __index = function (self, key) if key == uniqueKey then return true end end, __eq = function (self, obj) return self:getCIDR() == obj:getCIDR() end, __concat = function (self, obj) return tostring(self) .. tostring(obj) end, __tostring = function (self) return self:getCIDR() end, __metatable = false } -- Private static methods local function isSubnetObject(val) -- Return true if val is a Subnet object, and false otherwise. return type(val) == 'table' and val[uniqueKey] ~= nil end -- Function to validate subnet objects. -- Params: -- methodName (string) - the name of the method being validated -- argIdx (number) - the position of the argument in the argument list -- arg - the argument to be validated validateSubnet = makeValidationFunction('Subnet', isSubnetObject) -- Constructors makeSubnetFromRaw = function (rawIP, bitLength) -- Set up structure local obj = setmetatable({}, mt) local data = { rawIP = rawIP, bitLength = bitLength, } -- A function to check whether methods are called with a valid self -- parameter. local checkSelf = makeCheckSelfFunction( 'IP', 'subnet', obj, 'Subnet object' ) -- Public methods function obj:getPrefix() checkSelf(self, 'getPrefix') if not data.prefix then data.prefix = makeIPAddressFromRaw( data.rawIP:getPrefix(data.bitLength) ) end return data.prefix end function obj:getHighestIP() checkSelf(self, 'getHighestIP') if not data.highestIP then data.highestIP = makeIPAddressFromRaw( data.rawIP:getHighestHost(data.bitLength) ) end return data.highestIP end function obj:getBitLength() checkSelf(self, 'getBitLength') return data.bitLength end function obj:getCIDR() checkSelf(self, 'getCIDR') return string.format( '%s/%d', tostring(self:getPrefix()), self:getBitLength() ) end function obj:getVersion() checkSelf(self, 'getVersion') return data.rawIP:getVersion() end function obj:isIPv4() checkSelf(self, 'isIPv4') return data.rawIP:isIPv4() end function obj:isIPv6() checkSelf(self, 'isIPv6') return data.rawIP:isIPv6() end function obj:containsIP(ip) checkSelf(self, 'containsIP') local tp = type(ip) if tp == 'string' then ip = makeIPAddress(ip) elseif tp == 'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip, {'string', 'table'}) end if self:getVersion() == ip:getVersion() then return self:getPrefix() <= ip and ip <= self:getHighestIP() end return false end function obj:overlapsCollection(collection) checkSelf(self, 'overlapsCollection') validateCollection('overlapsCollection', 1, collection) return collection:overlapsSubnet(self) end function obj:overlapsSubnet(subnet) checkSelf(self, 'overlapsSubnet') local tp = type(subnet) if tp == 'string' then subnet = makeSubnet(subnet) elseif tp == 'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet, {'string', 'table'}) end if self:getVersion() == subnet:getVersion() then return ( subnet:getHighestIP() >= self:getPrefix() and subnet:getPrefix() <= self:getHighestIP() ) end return false end function obj:walk() checkSelf(self, '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) -- Return a Subnet object from a CIDR string. If the CIDR string is -- invalid, throw an error. 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 and (n == 0 or not rhs:find('^0')) then -- The right-hand side is a number between 0 and 32 (for IPv4) -- or 0 and 128 (for IPv6) and doesn't have any leading zeroes. local base = RawIP.newFromIP(lhs) if base then -- The left-hand side is a valid IP address. local prefix = base:getPrefix(n) if base == prefix then -- The left-hand side is the lowest IP in the subnet. return makeSubnetFromRaw(prefix, n) end 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 end -------------------------------------------------------------------------------- -- Ranges class -- Holds a list of IPAdress pairs representing contiguous IP ranges. -------------------------------------------------------------------------------- local Ranges = Collection.new() Ranges.__index = Ranges function Ranges.new() return setmetatable({}, Ranges) end function Ranges:add(ip1, ip2) validateIPAddress('add', 1, ip1) if ip2 ~= nil then validateIPAddress('add', 2, ip2) if ip1 > ip2 then error('The first IP must be less than or equal to the second', 2) end end Collection.add(self, {ip1, ip2 or ip1}) end function Ranges:merge() self:sort( function (lhs, rhs) -- Sort by second value, then first. if lhs[2] == rhs[2] then return lhs[1] < rhs[1] end return lhs[2] < rhs[2] end ) local pos = self.n while pos > 1 do for i = pos - 1, 1, -1 do local ip1 = self[i][2] local ip2 = ip1:getNextIP() if ip2 < ip1 then ip2 = ip1 -- don't wrap around end if self[pos][1] > ip2 then break end ip1 = self[i][1] ip2 = self[pos][1] self[i] = {ip1 > ip2 and ip2 or ip1, self[pos][2]} self:remove(pos) pos = pos - 1 if pos <= 1 then break end end pos = pos - 1 end end -------------------------------------------------------------------------------- -- IPCollection class -- Holds a list of IP addresses/subnets. Used internally. -- Each address/subnet has the same version (either IPv4 or IPv6). -------------------------------------------------------------------------------- local IPCollection = {} IPCollection.__index = IPCollection function IPCollection.new(version) assert( version == V4 or version == V6, 'IPCollection.new called with an invalid version' ) local obj = { version = version, -- V4 or V6 addresses = Collection.new(), -- valid IP addresses subnets = Collection.new(), -- valid subnets omitted = Collection.new(), -- not-quite valid strings } return obj end function IPCollection:getVersion() -- Return a string with the IP version of addresses in this collection. return self.version end function IPCollection:_store(hit, stripColons) local maker, location if hit:find('/', 1, true) then maker = Subnet.new location = self.subnets else maker = IPAddress.new location = self.addresses end local success, obj = pcall(maker, hit) if success then location:add(obj) else if stripColons then local colons, hit = hit:match('^(:*)(.*)') if colons ~= '' then self:_store(hit) return end end self.omitted:add(hit) end end function IPCollection:_assertVersion(version, msg) if self.version ~= version then error(msg, 3) end end function IPCollection:addIP(ip) local tp = type(ip) if tp == 'string' then ip = makeIPAddress(ip) elseif tp == 'table' then validateIPAddress('addIP', 1, ip) else checkTypeMulti('addIP', 1, ip, {'string', 'table'}) end self:_assertVersion(ip:getVersion(), 'addIP called with incorrect IP version') self.addresses:add(ip) return self end function IPCollection:addSubnet(subnet) local tp = type(subnet) if tp == 'string' then subnet = makeSubnet(subnet) elseif tp == 'table' then validateSubnet('addSubnet', 1, subnet) else checkTypeMulti('addSubnet', 1, subnet, {'string', 'table'}) end self:_assertVersion(subnet:getVersion(), 'addSubnet called with incorrect subnet version') self.subnets:add(subnet) return self end function IPCollection:containsIP(ip) -- Return true, obj if ip is in this collection, -- where obj is the first IPAddress or Subnet with the ip. -- Otherwise, return false. local tp = type(ip) if tp == 'string' then ip = makeIPAddress(ip) elseif tp == 'table' then validateIPAddress('containsIP', 1, ip) else checkTypeMulti('containsIP', 1, ip, {'string', 'table'}) end if self:getVersion() == ip:getVersion() then for _, item in ipairs(self.addresses) do if item == ip then return true, item end end for _, item in ipairs(self.subnets) do if item:containsIP(ip) then return true, item end end end return false end function IPCollection:getRanges() -- Return a sorted table of IP pairs equivalent to the collection. -- Each IP pair is a table representing a contiguous range of -- IP addresses from pair[1] to pair[2] inclusive (IPAddress objects). local ranges = Ranges.new() for _, item in ipairs(self.addresses) do ranges:add(item) end for _, item in ipairs(self.subnets) do ranges:add(item:getPrefix(), item:getHighestIP()) end ranges:merge() ranges:deobjectify() return ranges end function IPCollection:overlapsSubnet(subnet) -- Return true, obj if subnet overlaps this collection, -- where obj is the first IPAddress or Subnet overlapping the subnet. -- Otherwise, return false. local tp = type(subnet) if tp == 'string' then subnet = makeSubnet(subnet) elseif tp == 'table' then validateSubnet('overlapsSubnet', 1, subnet) else checkTypeMulti('overlapsSubnet', 1, subnet, {'string', 'table'}) end if self:getVersion() == subnet:getVersion() then for _, item in ipairs(self.addresses) do if subnet:containsIP(item) then return true, item end end for _, item in ipairs(self.subnets) do if subnet:overlapsSubnet(item) then return true, item end end end return false end -------------------------------------------------------------------------------- -- IPv4Collection class -- Holds a list of IPv4 addresses/subnets. -------------------------------------------------------------------------------- local IPv4Collection = setmetatable({}, IPCollection) IPv4Collection.__index = IPv4Collection function IPv4Collection.new() return setmetatable(IPCollection.new(V4), IPv4Collection) end function IPv4Collection:addFromString(text) -- Extract any IPv4 addresses or CIDR subnets from given text. checkType('addFromString', 1, text, 'string') text = text:gsub('[:!"#&\'()+,%-;<=>?[%]_{|}]', ' ') for hit in text:gmatch('%S+') do if hit:match('^%d+%.%d+[%.%d/]+$') then local _, n = hit:gsub('%.', '.') if n >= 3 then self:_store(hit) end end end return self end -------------------------------------------------------------------------------- -- IPv6Collection class -- Holds a list of IPv6 addresses/subnets. -------------------------------------------------------------------------------- local IPv6Collection = setmetatable({}, IPCollection) IPv6Collection.__index = IPv6Collection do -- Private static methods local function isCollectionObject(val) -- Return true if val is probably derived from an IPCollection object, -- otherwise return false. if type(val) == 'table' then local mt = getmetatable(val) if mt == IPv4Collection or mt == IPv6Collection then return true end end return false end validateCollection = makeValidationFunction('IPCollection', isCollectionObject) function IPv6Collection.new() return setmetatable(IPCollection.new(V6), IPv6Collection) end function IPv6Collection:addFromString(text) -- Extract any IPv6 addresses or CIDR subnets from given text. -- Want to accept all valid IPv6 despite the fact that addresses used -- are unlikely to start with ':'. -- Also want to be able to parse arbitrary wikitext which might use -- colons for indenting. -- Therefore, if an address at the start of a line is valid, use it; -- otherwise strip any leading colons and try again. checkType('addFromString', 1, text, 'string') for line in string.gmatch(text .. '\n', '[\t ]*(.-)[\t\r ]*\n') do line = line:gsub('[!"#&\'()+,%-;<=>?[%]_{|}]', ' ') for position, hit in line:gmatch('()(%S+)') do local ip = hit:match('^([:%x]+)/?%d*$') if ip then local _, n = ip:gsub(':', ':') if n >= 2 then self:_store(hit, position == 1) end end end end return self end end return { IPAddress = IPAddress, Subnet = Subnet, IPv4Collection = IPv4Collection, IPv6Collection = IPv6Collection, }