module(..., package.seeall)
-- Load libraries
require("procps")
require("getopts")
require("fs")
require("format")
require("processinfo")
require("daemoncontrol")
require("validator")
-- 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 startstop_service ( self, action )
local cmd = action.value
local cmdresult,cmdmessage,cmderror,cmdaction = daemoncontrol.daemoncontrol(processname, cmd)
action.descr=cmdmessage
action.errtxt=cmderror
return cmdresult,action
end
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 "",
})
config.baseurl = cfe({
name = "baseurl",
label="Baseurl for configfiles",
value=baseurl,
})
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
function createconfigfile (self, path)
local validfilepath, filepatherror = validator.is_valid_filename(path,baseurl)
if (fs.is_file(path)) then
return false,"File already exists"
end
if (validfilepath) then
fs.write_file(path, "")
return true,nil
else
return false, filepatherror
end
return false, "Something went wrong!"
end
function remove_file(self, path)
if not (fs.is_file(path)) then
return false,"File doesn't exist!"
end
if (validfilename(path)) then
local cmd, errors = io.popen( "/bin/rm " .. path, r )
local cmdoutput = cmd:read("*a")
cmd:close()
return true, cmdoutput
else
return false, "Not a valid filename!"
end
return false, "Something went wrong!"
end