module(..., package.seeall) -- Load libraries require("procps") require("getopts") require("fs") require("format") require("processinfo") -- Set variables local configdir local datafile local configfiles = {} local configitems = {} local packagename = "tinydns" local processname = "tinydns" local configfile = "/etc/conf.d/" .. processname local baseurl = "/etc/tinydns" local initdoptions = getopts.getoptsfromfile_onperline("/etc/init.d/" .. processname) if (initdoptions) then configdir = initdoptions.DATADIR datafile = initdoptions.ROOT .. "/data" or "/var/cache/data" else configdir = "/etc/" .. processname datafile = "/var/cache/data" end 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", }, reverse={ ['nsourdomain']=".", ['nsdomain']="&", ['host']="=", ['alias']="+", ['mx']="@", ['ptr']="^", ['cname']="C", ['soa']="Z", [':']=":", ['locations']="%", }, fieldlabels={ ['.']={"Prefix", "Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['&']={"Prefix", "Domain", "IP address", "Name server", "Time to live", "Timestamp", "Location", }, ['=']={"Prefix", "Host", "IP address", "Time to live", "Timestamp", "Location", }, ['+']={"Prefix", "Alias", "IP address", "Time to live", "Timestamp", "Location", }, ['@']={"Prefix", "Domain", "IP address", "Mail exchanger", "Distance", "Time to live", "Timestamp", "Location", }, ['^']={"Prefix", "PTR", "Domain name", "Time to live", "Timestamp", "Location", }, ['C']={"Prefix", "Domain name", "Canonical name", "Time to live", "Timestamp", "Location", }, ['Z']={"Prefix", "Unknown", "Primary name server", "Contact address", "Serial number", "Refresh time", "Retry time", "Expire time", "Minimum time", "Time to live", "Timestamp", "Location",}, [':']={"Prefix", }, ['%']={"Prefix", }, }, } --configdir = "hidden for the moment - This row is here only for debug purpose" -- ################################################################################ -- 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,1,1) .. ":" .. string.sub(orgitem,2),delimiter) output.type = descr['prefix'] output.type = output.type[string.sub(orgitem,1,1)] 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 recurseoutput(table,cnt) if not (cnt) then cnt=0 end cnt = cnt + 1 for k,v in pairs(table or {}) do if (type(v) == "string") then io.write(" ".. tostring(v) .. "
") else io.write(" ".. tostring(k) .. "
") recurseoutput(v,cnt) end end end -- Create table with doman levels local function recursedomains(t,array,maxn,currnum) if not (currnum) then currnum = maxn + 1 end currnum = currnum - 1 if not (currnum == 0) then if not (array[t[currnum]]) then array[t[currnum]] = {} end recursedomains(t,array[t[currnum]],maxn,currnum) end -- FIXME: This is a /really uggly/ hack to return the current table -- If it's fixed nicely... it would be wonderful! if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]][t[maxn-4]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]][t[maxn-4]][t[maxn-5]]) then return array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]][t[maxn-4]][t[maxn-5]] end if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]][t[maxn-4]]) then return array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]][t[maxn-4]] end if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]]) then return array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]] end if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]]) then return array[t[maxn]][t[maxn-1]][t[maxn-2]][t[maxn-3]] end if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) and (array[t[maxn]][t[maxn-1]][t[maxn-2]]) then return array[t[maxn]][t[maxn-1]][t[maxn-2]] end if (array[t[maxn]]) and (array[t[maxn]][t[maxn-1]]) then return array[t[maxn]][t[maxn-1]] end if (array[t[maxn]]) then return array[t[maxn]] end end local function validfilename(path) for k,v in pairs(getfilelist()) do if (v == path) then return true end end return false, "Not a valid filename!" end -- Example taken from PIL -- Sort by Keys local function pairsByKeys(t,f) local a = {} for n in pairs(t) do -- This is to fix some bug when next table is indexnumber instead of name if (tonumber(n) == nil) then a[#a + 1] = n end end table.sort(a,f) local i = 0 -- iterator variable return function () --iterator function i = i + 1 return a[i], t[a[i]] end end local function rebuild_table(t,domains_rebuilt) if not (type(t) == "string") then for k,v in pairs(t) do if (tonumber(k)) then table.insert(domains_rebuilt, v) else table.insert(domains_rebuilt, {label=k}) rebuild_table(v,domains_rebuilt[#domains_rebuilt]) end end table.sort(domains_rebuilt, function(a,b) return (a.label < b.label) end) end end -- This function removes all records that doesn't have the filter-value local function filter_table(t1,domains_filtered,filter) if not (type(t1) == "string") then for k1,v1 in pairs(t1) do for k2,v2 in pairs(v1) do if (v2.label) then if ( string.find(filter,v2.label) ) then table.insert(domains_filtered, v2) end end end end end end -- ################################################################################ -- PUBLIC FUNCTIONS function valid_filename(self,path) return validfilename(path) end -- This function could be used to check that valid parameters are used in different places function check_signs(sign) local output if (sign) and (descr[sign]) then output = descr[sign] end return output end -- Present some general status function getstatus() local status = {} local value, errtxt = processinfo.package_version(packagename) status.version = cfe({ name = "version", label="Program version", value=value, errtxt=errtxt, }) status.status = cfe({ name="status", label="Program status", value=procps.pidof(processname), }) status.configdir = cfe({ name="configdir", label="Config directory", value=configdir, }) status.configfiles = cfe({ name="configfiles", label="Config files", value=configfiles, }) local autostart_sequense, autostart_errtxt = processinfo.process_botsequence(processname) status.autostart = cfe({ name="autostart", label="Autostart sequence", value=autostart_sequense, errtxt=autostart_errtxt, }) return status end -- Return config-information function getlocations(self,filter_type) local config = {} local configobjects = {} local locations = {} local listenaddr = getopts.getoptsfromfile_onperline(configfile,"IP") or {} -- Loop through all available configfiles for k,v in pairs(configfiles) do local filecontent, fileresult fileresult, filecontent = get_value_from_file(v) for kk,vv in pairs(filecontent) do local domaindetails = {} local filecontent_table = split_config_items(vv) -- This is mostly for debugging -- This table contains all available configs table.insert(configobjects, cfe({ name=vv, value=vv, option=filecontent_table, })) -- Create a table with location items -- Containing all objects that start with % if (filecontent_table[1] == "%") then if not (locations[filecontent_table[2]]) then locations[filecontent_table[2]] = {} end table.insert(locations[filecontent_table[2]], filecontent_table[3]) end end end return locations end function getconfig(self,filter_type) local config = {} local listenaddr = getopts.getoptsfromfile_onperline(configfile,"IP") or {} config.listen = cfe({ name = "listen", label="IP address to listen on", value=listenaddr.IP or "", }) return config 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, filter_levels) local domains = {} --Loop through all available configfiles for k,v in pairs(configfiles) do local filecontent, fileresult fileresult, filecontent = get_value_from_file(v) for kk,vv in pairs(filecontent) do local domaindetails = {} local filecontent_table = split_config_items(vv) filecontent_table["orgrecord"] = vv -- Create domain information tables local domain -- * START * COMMONT SETTINGS **************************************** local descr=descr['prefix'] -- Use only configs that has a valid prefix -- We filter away location-definitions -- 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[1]))) and (descr[filecontent_table[1]]) and not (filecontent_table[1] == "%") then domain = format.string_to_table(filecontent_table[2], "%.") -- We rebuild the table and add previous level-information to the current level for i = table.maxn(domain),2,-1 do domain[i-1] = domain[i-1] .. "." .. domain[i] end local domainoptions = {} -- Add details to the previous object table.insert(domainoptions, cfe ({ name="type", label="Type", value=descr[filecontent_table[1]], })) -- Set values and labels for field #3 local name = "ip" local label = "IP address" -- Some configs uses third column in some other way if (filecontent_table[1] == "^") or (filecontent_table[1] == "C") then name = "pointdomain" label = "Domain" end if (filecontent_table[1] == "Z") then name = "mname" label = "Primary nameserver" end if (filecontent_table[1] == ":") then name = "rectype" label = "Type of record" end if (filecontent_table[3]) and (#filecontent_table[3]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[3], })) end -- Set values and labels for field #4 name = "ttl" label = "Time to live" -- Some configs uses third column in some other way if (filecontent_table[1] == ".") or (filecontent_table[1] == "&") then name = "ns" label = "Name server" end if (filecontent_table[1] == "@") then name = "mx" label = "Mail exchanger" end if (filecontent_table[1] == "Z") then name = "rname" label = "Contact address" end if (filecontent_table[4]) and (#filecontent_table[4]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[4], })) end -- Set values and labels for field #5 name = "timestamp" label = "Time stamp" -- Some configs uses third column in some other way if (filecontent_table[1] == ".") or (filecontent_table[1] == "&") then name = "ttl" label = "Time to live" end if (filecontent_table[1] == "@") then name = "dist" label = "Distance" end if (filecontent_table[1] == "Z") then name = "ser" label = "Serial number" end if (filecontent_table[5]) and (#filecontent_table[5]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[5], })) end -- Set values and labels for field #6 name = "lo" label = "Location" -- Some configs uses third column in some other way if (filecontent_table[1] == ".") or (filecontent_table[1] == "&") then name = "timestamp" label = "Time stamp" end if (filecontent_table[1] == "@") then name = "ttl" label = "Time to live" end if (filecontent_table[1] == "Z") then name = "ref" label = "Refresh time" end if (filecontent_table[6]) and (#filecontent_table[6]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[6], })) end -- Set values and labels for field #7 local name = nil local label = nil -- Some configs uses third column in some other way if (filecontent_table[1] == ".") or (filecontent_table[1] == "&") then name = "lo" label = "Location" end if (filecontent_table[1] == "@") then name = "timestamp" label = "Timestamp" end if (filecontent_table[1] == "Z") then name = "ret" label = "Retry time" end if (filecontent_table[7]) and (#filecontent_table[7]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[7], })) end -- Set values and labels for field #8 local name = nil local label = nil -- Some configs uses third column in some other way if (filecontent_table[1] == "@") then name = "lo" label = "Location" end if (filecontent_table[1] == "Z") then name = "exp" label = "Expire time" end if (filecontent_table[8]) and (#filecontent_table[8]> 0) and (name) then table.insert(domainoptions, cfe ({ name=name, label=label, value=filecontent_table[8], })) end -- Set values and labels for field #9-12 if (filecontent_table[1] == "Z") then if (filecontent_table[9]) and (#filecontent_table[9]> 0) then table.insert(domainoptions, cfe ({ name="min", label="Minimum time", value=filecontent_table[9], })) end if (filecontent_table[10]) and (#filecontent_table[10]> 0) then table.insert(domainoptions, cfe ({ name="ttl", label="Time to live", value=filecontent_table[10], })) end if (filecontent_table[11]) and (#filecontent_table[11]> 0) then table.insert(domainoptions, cfe ({ name="timestamp", label="Time stamp", value=filecontent_table[11], })) end if (filecontent_table[12]) and (#filecontent_table[12]> 0) then table.insert(domainoptions, cfe ({ name="location", label="Location", value=filecontent_table[12], })) end end -- This is the main information on each object domaindetails = cfe ({ name=filecontent_table[2], label=filecontent_table[2], option=domainoptions, orgrecordtable=filecontent_table, }) end -- * END * COMMONT SETTINGS ******************************************** -- Inject the previous data into the right table local value = filecontent_table[2] local currenttable if (type(domain) == "table") then currenttable = recursedomains(domain, domains, table.maxn(domain)) end ---[[ if (domaindetails.value) then table.insert (currenttable , domaindetails) end --]] end end -- TODO: Sort the domains table! -- Sorting is not possible when things is done as they are (se above) -- problem comese when we use keynames instead of [1], [2], ... -- Next we rebuild the domains table and do it so it can be sorted local domains_sorted = {} local domains_rebuilt = {} rebuild_table(domains,domains_rebuilt) -- Filter away not wanted records local domains_filtered = {} -- local filter_level2 = "" -- < This is DEBUG!!! REMOVE WHEN DONE! -- filter_table(domains_rebuilt,domains_filtered,filter_level2) domains_filtered = domains_rebuilt return domains_filtered end function getfilelist () local listed_files = {} for k,v in pairs{baseurl} do recursedir(v, listed_files) end -- table.sort(listed_files, function (a,b) return (a.name < b.name) end ) return listed_files end function get_filedetails(self,path) if not (validfilename(path)) then return false, "Not a valid filename!" end local file = {} local filedetails = {} local config = {} local filenameerrtxt if (path) and (fs.is_file(path)) then filedetails = fs.stat(path) config = getconfig(path) else config = {} config.filename = {} config["filename"]["errtxt"]="Config file '".. tostring(path) .. "' is missing!" end file["filename" .. (num or "")] = cfe({ name="filename" .. (num or ""), label="File name", value=path, errtxt=filenameerrtxt }) file["filesize" .. (num or "")] = cfe({ name="filesize" .. (num or ""), label="File size", value=filedetails.size or 0, }) file["mtime" .. (num or "")] = cfe({ name="mtime" .. (num or ""), label="File date", value=filedetails.mtime or "---", }) file["filecontent" .. (num or "")] = cfe({ type="longtext", name="filecontent" .. (num or ""), label="File content", value=fs.read_file(path), }) -- Sum all errors into one cfe local sumerrors = "" for k,v in pairs(config) do if (config[k]) and (config[k]["errtxt"]) and (config[k]["errtxt"] ~= "") then sumerrors = sumerrors .. config[k]["errtxt"] .. "\n" end end if (sumerrors ~= "") then file["sumerrors" .. (num or "")] = cfe ({ name="sumerrors" .. (num or ""), label = "Configuration errors", errtxt = string.match(sumerrors, "(.-)\n$"), }) end return file end function updatefilecontent (self, filetochange) local path = filetochange.name local modifications = filetochange.value if not (fs.is_file(path)) then return false, "Not a filename" end if (validfilename(path)) then fs.write_file(path, format.dostounix(modifications)) return true else return false, "Not a valid filename!" end return false, "Something went wrong!" end