--[[ Copyright (c) 2012-2019 Kaarle Ritvanen See LICENSE file for license details --]] --- @module aconf.model local M = {} local raise = require('aconf.error').raise local Union = require('aconf.model.combination').Union local fld = require('aconf.model.field') local String = fld.String local object = require('aconf.object') local class = object.class local super = object.super local pth = require('aconf.path') local update = require('aconf.util').update local stringy = require('stringy') local BaseIPAddress = class(String) function BaseIPAddress:abs_mask_addr(context) if self.mask_addr then return pth.join(pth.parent(context.addr), self.mask_addr) end end function BaseIPAddress:topology(context) local res = super(self, BaseIPAddress):topology(context) local maddr = self:abs_mask_addr(context) if maddr then table.insert(res, {path=context.path, addr=maddr, type=self.mask_type}) end return res end function BaseIPAddress:invalid(context) raise(context.path, 'Invalid IPv'..self.version..' address') end function BaseIPAddress:split(context, value) local comps = stringy.split(value, '/') if #comps == 1 then return value, self.length end if #comps > 2 or not self.cidr then self:invalid(context) end local mask = tonumber(comps[2]) if not mask or mask < 0 or mask > self.length then self:invalid(context) end return comps[1], mask end function BaseIPAddress:decode(context, value) local maddr = self:abs_mask_addr(context) if value and maddr then return value..'/'..(self:mask2cidr(context.txn:get(maddr)) or self.length) end return value end function BaseIPAddress:encode(context, value) local maddr = self:abs_mask_addr(context) if maddr then local cidr if value then value, cidr = self:split(context, value) end context.txn:set(maddr, cidr and self:cidr2mask(cidr)) end return value end --- IPv4 address field, inherits @{String}. -- @fclass net.IPv4Address -- @tparam boolean cidr if set to true, the field accepts a network -- address in CIDR notation -- @tparam string mask_addr if set, the network address is decomposed -- into address and netmask in the back-end. This parameter specifies -- the back-end address where the netmask is stored in dotted-quad -- format. M.IPv4Address = class(BaseIPAddress) function M.IPv4Address:init(params) super(self, M.IPv4Address):init( update(params, {version=4, length=32, mask_type='string'}) ) end function M.IPv4Address:validate(context, value) super(self, M.IPv4Address):validate(context, value) local address = self:split(context, value) local function test(...) if #{...} ~= 4 then return true end for _, octet in ipairs{...} do if tonumber(octet) > 255 then return true end end end if test(address:match('^(%d+)%.(%d+)%.(%d+)%.(%d+)$')) then self:invalid(context) end end function M.IPv4Address:mask2cidr(mask) local acc = 0 for i, comp in ipairs(stringy.split(mask, '.')) do acc = acc + math.floor(256 ^ (4 - i)) * tonumber(comp) end local res = 32 while acc % 2 == 0 do res = res - 1 assert(res > -1) acc = acc / 2 end return res end function M.IPv4Address:cidr2mask(cidr) local acc = (math.floor(2 ^ cidr) - 1) * math.floor(2 ^ (32 - cidr)) local comps = {} for i = 4,1,-1 do comps[i] = acc % 256 acc = math.floor(acc / 256) end return table.concat(comps, '.') end --- IPv6 address field, inherits @{String}. -- @fclass net.IPv6Address -- @tparam boolean cidr if set to true, the field accepts a network -- address -- @tparam string mask_addr if set, the network address is decomposed -- into address and netmask parts in the back-end. This parameter -- specifies the back-end address where the netmask length is stored. M.IPv6Address = class(BaseIPAddress) function M.IPv6Address:init(params) super(self, M.IPv6Address):init( update(params, {version=6, length=128, mask_type='number'}) ) end function M.IPv6Address:validate(context, value) super(self, M.IPv6Address):validate(context, value) local address = self:split(context, value) local function invalid() self:invalid(context) end if address == '' then invalid() end local comps = stringy.split(address, ':') if #comps < 3 then invalid() end local function collapse(i, ofs) if comps[i] > '' then return end if comps[i + ofs] > '' then invalid() end table.remove(comps, i) end collapse(1, 1) collapse(#comps, -1) if #comps > 8 then invalid() end local short = false for _, comp in ipairs(comps) do if comp == '' then if short then invalid() end short = true elseif not comp:match('^%x%x?%x?%x?$') then invalid() end end if ( short and #comps == 3 and comps[2] == '' ) or (not short and #comps < 8) then invalid() end end function M.IPv6Address:mask2cidr(mask) return mask end function M.IPv6Address:cidr2mask(cidr) return cidr end --- IP address field, inherits @{String}. Accepts both IPv4 and IPv6 -- address values. -- @fclass net.IPAddress -- @tparam boolean cidr if set to true, the field accepts a network -- address in CIDR notation M.IPAddress = class(Union) function M.IPAddress:init(params) local p = {cidr=params and params.cidr} super(self, M.IPAddress):init( update( params, { types={M.IPv4Address(p), M.IPv6Address(p)}, error='Invalid IP address' } ) ) end --- TCP or UDP port field, inherits @{Integer}. The value can be -- between 0 and 65535. -- @fclass net.Port M.Port = class(fld.Integer) function M.Port:validate(context, value) super(self, M.Port):validate(context, value) if value < 0 or value > 65535 then raise(context.path, 'Invalid port') end end local domain_pattern = '[A-Za-z%d%.%-]+%.[A-Za-z][A-Za-z]+' --- domain name field, inherits @{String}. -- @fclass net.DomainName M.DomainName = class(String) function M.DomainName:init(params) super(self, M.DomainName):init(update(params, {pattern=domain_pattern})) end M.EmailAddress = class(String) function M.EmailAddress:init(params) super(self, M.EmailAddress):init( update(params, {pattern='[A-Za-z%d%.%+%-]+@'..domain_pattern}) ) end return M