local mymodule = {} -- Load libraries modelfunctions = require("modelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") authenticator = require("authenticator") roles = require("roles") posix = require("posix") -- Set variables local configfiles = {} local configuser local packagename = "tinydns" local processname = "tinydns" local configfile = "/etc/conf.d/" .. processname local configdir = "/etc/"..processname local descr = { prefix={ ['.']="Name server for your domain (NS + A + SOA)", ['&']="Delegate subdomain (NS + A)", ['=']="Host and reverse record (A + PTR)", ['+']="Host record (A, no PTR)", ['@']="Mail exchanger (MX)", ["'"]="Text record (TXT)", ['^']="Reverse record (PTR)", ['C']="Canonical name (CNAME)", ['Z']="SOA record (SOA)", [':']="Generic record", ['%']="Client location", ['S']="Service location (SRV)", ['N']="Naming authority pointer (NAPTR)", }, fieldlabels={ ['.']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['&']={"Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['=']={"Host", "IP address", "Time to live", "Timestamp", "Location", }, ['+']={"Host", "IP address", "Time to live", "Timestamp", "Location", }, ['@']={"Domain", "IP address", "Mail exchanger", "Distance", "Time to live", "Timestamp", "Location", }, ['\'']={"Domain", "Text Record", "Time to live", "Timestamp", "Location", }, ['^']={"PTR", "Domain name", "Time to live", "Timestamp", "Location", }, ['C']={"Domain", "Canonical name", "Time to live", "Timestamp", "Location", }, ['Z']={"Domain", "Primary name server", "Contact address", "Serial number", "Refresh time", "Retry time", "Expire time", "Minimum time", "Time to live", "Timestamp", "Location",}, [':']={"Domain", "Record type", "Record data", "Time to live", "Timestamp", "Location", }, ['%']={"Location", "IP prefix", }, ['S']={"Domain Service", "IP address", "Server", "Port", "Priority", "Weight", "Time to live", "Timestamp", }, ['N']={"Domain", "Order", "Preference", "Flags", "Service", "Regular expression", "Replacement", "Time to live", "Timestamp", }, }, } function mymodule.set_processname(p) processname = p configfile = "/etc/conf.d/" .. processname configdir = "/etc/"..processname end -- ################################################################################ -- LOCAL FUNCTIONS -- Return a table with the config-content of a file -- Commented/Blank lines are ignored local function get_value_from_file(file) local output = {} local filecontent = fs.read_file_as_array(file) or {} for i=1,table.maxn(filecontent) do local l = filecontent[i] if not (string.find ( l, "^[;#].*" )) and not (string.find (l, "^%s*$")) then table.insert(output, string.match(l,"(.-)%s*$")) end end if (#output > 0) then return true, output else return false, output end end -- Function to recursively inserts all filenames in a dir into an array local function recursedir(path, filearray) local k,v for k,v in pairs(posix.dir(path) or {}) do -- Ignore files that begins with a '.' if not string.match(v, "^%.") then local f = path .. "/" .. v -- If subfolder exists, list files in this subfolder if (posix.stat(f).type == "directory") then recursedir(f, filearray) else table.insert(filearray, f) end end end end -- Functin to split items into a table local function split_config_items(orgitem) local delimiter = ":" local output = {} output = format.string_to_table(string.sub(orgitem,2),delimiter) output.type = string.sub(orgitem,1,1) output.label = descr['prefix'][output.type] return output end local function getallowedlist(self, userid) local allowedlist = {} local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end -- also check to see if there are allowed files for this user's roles local userinfo = authenticator.get_userinfo(self, userid) -- add in the guest role userinfo.roles[#userinfo.roles + 1] = roles.guest_role for i,role in ipairs(userinfo.roles) do local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end end return allowedlist end -- Feed the configfiles table with list of all configfiles that are available and allowed -- Default to allowing all files if no userid or allowed list local function searchforconfigfiles(self, userid) if #configfiles > 0 and configuser == userid then return configfiles end local cnffile = {} recursedir(configdir, cnffile) local allowedlist = getallowedlist(self, userid) if allowedlist and #allowedlist > 0 then local reverseallowed = {} for x,name in ipairs(allowedlist) do reverseallowed[name] = x end for k,v in pairs(cnffile) do if reverseallowed[v] then table.insert(configfiles, v) end end else configfiles = cnffile end configuser = userid table.sort(configfiles) return configfiles end local function validfilename(path) for k,v in pairs(configfiles) do if (v == path) then return true end end return false, "Not a valid filename!" end -- ################################################################################ -- PUBLIC FUNCTIONS function mymodule.get_startstop(self, clientdata) return modelfunctions.get_startstop(processname) end function mymodule.startstop_service(self, startstop, action) return modelfunctions.startstop_service(startstop, action) end -- Present some general status function mymodule.getstatus() local status = modelfunctions.getstatus(processname, packagename, "TinyDNS Status") status.value.configdir = cfe({ label="Config directory", value=configdir, }) local config = mymodule.getconfig() status.value.listen = config.value.listen return status end function mymodule.getconfig() local config = {} local listenaddr = format.parse_ini_file(fs.read_file(configfile) or "","","IP") or "" config.listen = cfe({ label="IP address to listen on", value=listenaddr, }) local test, errtxt = validator.is_ipv4(config.listen.value) if not test then config.listen.errtxt = errtxt end return cfe({ type="group", value=config, label="TinyDNS Configuration" }) end function mymodule.setconfig(self, conf) local test, errtxt = validator.is_ipv4(conf.value.listen.value) if not test then conf.value.listen.errtxt = errtxt conf.errtxt = "Failed to set configuration" else modelfunctions.write_file_with_audit(self, configfile, format.update_ini_file(fs.read_file(configfile) or "","","IP",conf.value.listen.value)) end return conf end -- If you enter 'filter_type' (this should be one of the options found in local function check_signs() ) then -- the output will be filtered to only contain this type of data. function mymodule.getconfigobjects(self, file_name, userid, filter_type) configfiles = searchforconfigfiles(self, userid) local configobjects = {} --Loop through all available configfiles for i,filename in pairs(configfiles) do if not file_name or file_name == filename then local filecontent = fs.read_file_as_array(filename) or {} for linenumber,configline in ipairs(filecontent) do local domaindetails = {} local filecontent_table = split_config_items(configline) filecontent_table.configline = configline -- Use only configs that has a valid prefix -- If function is called with some filter options... then show only the filtered values if ( not (filter_type) or ((filter_type) and (filter_type == filecontent_table.type)) ) and (filecontent_table.label) then filecontent_table.filename = filename filecontent_table.linenumber = linenumber -- we're gonna add a reverse domain name to make it easier to sort local domain = {} for mt in string.gmatch(filecontent_table[1], "([^.]+)") do table.insert(domain, mt) end local reversedomain = {} for i=#domain,1,-1 do table.insert(reversedomain, domain[i]) end filecontent_table.sort = table.concat(reversedomain, ".") -- add it to the table table.insert(configobjects, filecontent_table) end end end end -- Sort the table by domain name (entry 1) table.sort(configobjects, function(a,b) if a == b then return false elseif a.sort ~= b.sort then return a.sort < b.sort elseif a.configline ~= b.configline then return a.configline < b.configline end a.errtxt = "Duplicate entry" b.errtxt = "Duplicate entry" return false end) for i,entry in ipairs(configobjects) do entry.sort = nil end return cfe({ type="structure", value=configobjects, label="DNS Entries", filename=file_name, fieldlabels=descr.fieldlabels }) end function mymodule.getfilelist(self, userid) configfiles = searchforconfigfiles(self, userid) local listed_files = {} for i,name in pairs(configfiles) do local filedetails = posix.stat(name) or {} filedetails.filename = name table.insert(listed_files, filedetails) end return cfe({ type="structure", value=listed_files, label="Config files" }) end function mymodule.get_filedetails(self, path, userid) configfiles = searchforconfigfiles(self, userid) return modelfunctions.getfiledetails(path, validfilename) end function mymodule.set_filedetails (self, filedetails, userid) configfiles = searchforconfigfiles(self, userid) return modelfunctions.setfiledetails(self, filedetails, validfilename) end function mymodule.getnewconfigfile() local options = {} options.filename = cfe({ label="File Name" }) return cfe({ type="group", value=options, label="New config file" }) end function mymodule.createconfigfile(self, configfile, userid) configfile.errtxt = "Failed to create file" local path = configfile.value.filename.value if not string.find(path, "/") then path = configdir .. "/" .. path end if validator.is_valid_filename(path,configdir) then if (posix.stat(path)) then configfile.value.filename.errtxt = "File already exists" else fs.create_file(path) configfile.errtxt = nil -- We have to add this file to the allowed list, if there is one local allowed = getallowedlist(self, userid) if #allowed > 0 then local perm = mymodule.getuserpermissions(self, userid) perm.value.allowed.value[#perm.value.allowed.value + 1] = path mymodule.setuserpermissions(self, perm) end end else configfile.value.filename.errtxt = "Invalid filename" end return configfile end function mymodule.get_remove_file(self, clientdata) local retval = {} retval.filename = cfe({ value=clientdata.filename or "", label="File Name" }) return cfe({ type="group", value=retval, label="Delete Config File" }) end function mymodule.remove_file(self, delfile, userid) local path = delfile.value.filename.value configfiles = searchforconfigfiles(self, userid) delfile.errtxt = "Failed to delete file" if not (fs.is_file(path)) then delfile.value.filename.errtxt = "File doesn't exist!" elseif (validfilename(path)) then os.remove(path) delfile.errtxt = nil else delfile.value.filename.errtxt = "Not a valid filename!" end return delfile end function mymodule.getpermissionslist(self) local auth = authenticator.get_subauth(self) local users = authenticator.list_users(self) local userlist = {} for i,user in ipairs(users) do local allowedlist = {} local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, user) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end userlist[#userlist + 1] = {id=user, allowed=allowedlist} end -- Need to check for roles as well as users local rolelist = {} local rols = roles.list_all_roles(self) for i,role in ipairs(rols) do local allowedlist = {} local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end rolelist[#rolelist + 1] = {id=role, allowed=allowedlist} end table.sort(userlist, function(a,b) return a.id < b.id end) return cfe({ type="structure", value={user=userlist, role=rolelist}, label="TinyDNS Permissions" }) end local function validateuserpermissions(self, userpermissions) local success = true local userinfo = authenticator.get_userinfo(self, userpermissions.value.userid.value) if not userinfo then userpermissions.value.userid.errtxt = "Invalid user" success = false end success = modelfunctions.validatemulti(userpermissions.value.allowed) and success return success, userpermissions end local function validaterolepermissions(self, rolepermissions) local success = false rolepermissions.value.role.errtxt = "Invalid role" local rols = roles.list_all_roles(self) for i,role in ipairs(rols) do if rolepermissions.value.role.value == role then rolepermissions.value.role.errtxt = nil success = true break end end success = modelfunctions.validatemulti(rolepermissions.value.allowed) and success return success, rolepermissions end function mymodule.getuserpermissions(self, userid) local allowedlist = {} local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userid) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end local cnffile = {} recursedir(configdir, cnffile) table.sort(cnffile) local allowed = cfe({ type="multi", value=allowedlist, label="TinyDNS Permissions", option=cnffile, descr="If no permissions are defined, then all are allowed", seq=1 }) if #cnffile == 0 then allowed.errtxt = "No domains defined" end local user = cfe({ value=userid, label="User Name", readonly=true, seq=0 }) local output = cfe({ type="group", value={userid=user, allowed=allowed}, label="TinyDNS Permissions" }) validateuserpermissions(self, output) return output end function mymodule.setuserpermissions(self, userpermissions) local success, userpermissions = validateuserpermissions(self, userpermissions) if success then local auth = authenticator.get_subauth(self) auth.write_entry(self, authenticator.usertable, self.conf.prefix..self.conf.controller, userpermissions.value.userid.value, table.concat(userpermissions.value.allowed.value, ",")) else userpermissions.errtxt = "Failed to set user permissions" end return userpermissions end function mymodule.getrolepermissions(self, role) local allowedlist = {} local auth = authenticator.get_subauth(self) local entry = auth.read_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, role) or "" for x in string.gmatch(entry, "([^,]+),?") do allowedlist[#allowedlist + 1] = x end local cnffile = {} recursedir(configdir, cnffile) table.sort(cnffile) local allowed = cfe({ type="multi", value=allowedlist, label="TinyDNS Permissions", option=cnffile, descr="If no permissions are defined, then all are allowed", seq=1 }) if #cnffile == 0 then allowed.errtxt = "No domains defined" end local rol = cfe({ value=role, label="Role", readonly=true, seq=0 }) local output = cfe({ type="group", value={role=rol, allowed=allowed}, label="TinyDNS Permissions" }) validaterolepermissions(self, output) return output end function mymodule.setrolepermissions(self, rolepermissions) local success, rolepermissions = validaterolepermissions(self, rolepermissions) if success then local auth = authenticator.get_subauth(self) auth.write_entry(self, authenticator.roletable, self.conf.prefix..self.conf.controller, rolepermissions.value.role.value, table.concat(rolepermissions.value.allowed.value, ",")) else rolepermissions.errtxt = "Failed to set role permissions" end return rolepermissions end return mymodule