--[[ Copyright (c) 2012-2015 Kaarle Ritvanen See LICENSE file for license details --]] 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 address = require('aconf.path.address') 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 address.join(address.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:_load(context) local res = super(self, BaseIPAddress):_load(context) local maddr = self:abs_mask_addr(context) if res and maddr then return res..'/'..(self:mask2cidr(context.txn:get(maddr)) or self.length) end return res end function BaseIPAddress:_save(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 super(self, BaseIPAddress):_save(context, value) end 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.pow(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.pow(2, cidr) - 1) * math.pow(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 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 M.IPAddress = class(Union) function M.IPAddress:init(params) super(self, M.IPAddress):init( update( params, {types={M.IPv4Address, M.IPv6Address}, error='Invalid IP address'} ) ) end 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]+' 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