module(..., package.seeall) -- Load libraries require("procps") require("getopts") require("fs") require("format") require("processinfo") require("daemoncontrol") require("validator") -- Set variables local configfiles = {} 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)", ['&']="Deletegate subdomain (NS + A)", ['=']="Host (A + PTR)", ['+']="Alias (A, no PTR)", ['@']="Mail exchanger (MX)", ["'"]="Text record (TXT)", ['^']="Reverse record (PTR)", ['C']="Canonical Name (CNAME)", ['Z']="SOA record (SOA)", [':']="Generic record", ['%']="Client location", }, 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", }, ['+']={"Alias", "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", }, }, } -- ################################################################################ -- 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) 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] or "unknown" return output end -- Feed the configfiles table with list of all availage configfiles local function searchforconfigfiles() local cnffile = {} recursedir(configdir, cnffile) for k,v in pairs(cnffile) do local configcontent = get_value_from_file(v) if (configcontent) then table.insert(configfiles, v) end end end searchforconfigfiles() local function validfilename(path) for k,v in pairs(getfilelist().value) do if (v == path) then return true end end return false, "Not a valid filename!" end -- ################################################################################ -- PUBLIC FUNCTIONS function startstop_service ( self, action ) -- action is validated in daemoncontrol local cmdresult,cmdmessage,cmderror,cmdaction = daemoncontrol.daemoncontrol(processname, action) return cfe({ type="boolean", value=cmdresult, descr=cmdmessage, errtxt=cmderror, label="Start/Stop result" }) end -- Present some general status function getstatus() local status = {} local value, errtxt = processinfo.package_version(packagename) status.version = cfe({ label="Program version", value=value, errtxt=errtxt, }) status.status = cfe({ label="Program status", value=procps.pidof(processname), }) if (#status.status.value > 0) then status.status.value = "Enabled" else status.status.value = "Disabled" end status.configdir = cfe({ label="Config directory", value=configdir, }) status.configfiles = cfe({ type="list", label="Config files", value=configfiles, }) local autostart_sequense, autostart_errtxt = processinfo.process_botsequence(processname) status.autostart = cfe({ label="Autostart sequence", value=autostart_sequense, errtxt=autostart_errtxt, }) local config = getconfig() status.listen = config.value.listen return cfe({ type="group", value=status, label="DNS Status" }) end function getconfig() local config = {} local listenaddr = getopts.getoptsfromfile(configfile,"","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 setconfig(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 getopts.setoptsinfile(configfile,"","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 getconfigobjects(self, filter_type) local configobjects = {} --Loop through all available configfiles for i,filename in pairs(configfiles) do local filecontent, fileresult fileresult, filecontent = get_value_from_file(filename) for j,configline in pairs(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 local entry = {} for i,value in ipairs(filecontent_table) do entry[i] = value end -- we're gonna add a reverse domain name to make it easier to sort local domain = {} for mt in string.gmatch(entry[1], "([^.]+)") do table.insert(domain, mt) end local reversedomain = {} for i=#domain,1,-1 do table.insert(reversedomain, domain[i]) end entry.sort = table.concat(reversedomain, ".") -- add it to the table if not configobjects[filecontent_table.type] then configobjects[filecontent_table.type] = {label=filecontent_table.label, fieldlabels=descr.fieldlabels[filecontent_table.type]} end table.insert(configobjects[filecontent_table.type], entry) end end end -- Sort each of the tables by domain name (entry 1) for type,entries in pairs(configobjects) do table.sort(entries, function(a,b) if a == b then return false; elseif a.sort ~= b.sort then return a.sort < b.sort end for i in ipairs(a) do if a[i] ~= b[i] then return a[i] < b[i] end end a.errtxt = "Duplicate entry" b.errtxt = "Duplicate entry" return false end) for i,entry in ipairs(entries) do entry.sort = nil end end return configobjects end function getfilelist () local listed_files = {} recursedir(configdir, listed_files) return cfe({ type="list", value=listed_files, label="List of config files" }) end function get_filedetails(path) local file = {} local filedetails = {} local filenameerrtxt if (path) and (fs.is_file(path)) then filedetails = fs.stat(path) else filenameerrtxt="Config file '".. tostring(path) .. "' is missing!" end file["filename"] = cfe({ label="File name", value=path, errtxt=filenameerrtxt }) file["filesize"] = cfe({ label="File size", value=filedetails.size or "0", }) file["mtime"] = cfe({ label="File date", value=filedetails.mtime or "---", }) file["filecontent"] = cfe({ type="longtext", label="File content", value=fs.read_file(path), }) return cfe({ type="group", value=file, label="Config file details" }) end function updatefilecontent (path, modifications) local success = false local errtxt if not (fs.is_file(path)) then errtxt = "Not a filename" elseif (validfilename(path)) then fs.write_file(path, format.dostounix(modifications)) success = true else errtxt = "Not a valid filename!" end return cfe({ type="boolean", value=success, label="Update file result", errtxt=errtxt }) end function getnewconfigfile() local options = {} options.filename = cfe({ value=configdir.."/", label="File Name" }) return cfe({ type="group", value=options, label="New config file" }) end function createconfigfile(configfile) configfile.errtxt = "Failed to create file" local path = configfile.value.filename.value local validfilepath, filepatherror = validator.is_valid_filename(path,configdir) if (validfilepath) then if (fs.is_file(path)) then configfile.value.filename.errtxt = "File already exists" else local file = io.open(path, "w") file:close() configfile.errtxt = nil end else configfile.value.filename.errtxt = filepatherror end return configfile end function remove_file(path) local success = false local errtxt if not (fs.is_file(path)) then errtxt = "File doesn't exist!" elseif (validfilename(path)) then local cmd, errors = io.popen( "/bin/rm " .. path, r ) local cmdoutput = cmd:read("*a") cmd:close() success = true else errtxt = "Not a valid filename!" end return cfe({ type="boolean", value=success, label="Delete config file result", errtxt=errtxt }) end