local mymodule = {} -- Load libraries modelfunctions = require("modelfunctions") validator = require("acf.validator") fs = require("acf.fs") posix = require("posix") format = require("acf.format") processinfo = require("acf.processinfo") -- Set variables local configfile = "/etc/tcpproxy.conf" local processname = "tcpproxy" local packagename = "tcpproxy" local smtppackagename = "rxmtp" local smtpdirectory = "/etc/rxmtp/" local keywords = { "standalone", "port", "interface", "rotate", "server", "uid", "gid", "user", "exec", "acp", "logname", "setenv", "timeout", "writefile" } local config -- ################################################################################ -- LOCAL FUNCTIONS local function parseconfigfile(file) file = file or "" local retval = {} for line in string.gmatch(file, "([^\n]+)\n?") do line = string.gsub(line, "#.*$", "") if line and line ~= "" then table.insert(retval, {}) for word in string.gmatch(line, "%S+") do table.insert(retval[#retval], word) end end end return retval end local function getsmtpconfig(config) -- parse the TCP proxy config file for smtp entries local retval = {} local port25 = false local currentint for i,entry in ipairs(config) do if entry[1] == "port" then if entry[2] == "25" then port25 = true currentint = nil else port25 = false currentint = nil end elseif port25 then if entry[1] == "interface" then currentint = entry[2] elseif entry[1] == "exec" and string.find(entry[2], "rxmtp$") then if currentint then table.insert(retval, {ipaddr=currentint, cmd=table.concat(entry, " ", 2)}) else -- bad config - exec command without interface end end end end return retval end local function setsmtpcmd(ipaddr, cmd) if cmd then cmd = "exec "..cmd end local file = fs.read_file(configfile) or "" local inport25 = false local ininterface = false local done = false local lines = {} for line in string.gmatch(file, "([^\n]*)\n?") do if not done then if ininterface and string.find(line, "^%s*exec%s") then -- We found the line, replace it line = cmd done = true elseif ininterface and string.find(line, "^%s*interface%s") then -- We're leaving the interface and we haven't written line yet if cmd then if string.find(lines[#lines], "^%s*$") then lines[#lines] = nil end lines[#lines + 1] = cmd lines[#lines + 1] = "" end done = true elseif inport25 and string.find(line, "^%s*interface%s+"..ipaddr) then ininterface = true elseif inport25 and string.find(line, "^%s*port%s") then -- we're leaving port 25 and we haven't written line yet if cmd then lines[#lines + 1] = "interface "..ipaddr lines[#lines + 1] = cmd lines[#lines + 1] = "" end done = true elseif string.find(line, "^%s*port%s+25") then inport25 = true end end lines[#lines + 1] = line end if not done and cmd then if not inport25 then lines[#lines + 1] = "port 25" end if not ininterface then lines[#lines + 1] = "interface "..ipaddr end lines[#lines + 1] = cmd end fs.write_file(configfile, string.gsub(table.concat(lines, "\n"), "\n+$", "")) end local function parsesmtpcmd(exec) -- parse the command line into a table exec = exec or "" local options = format.opts_to_table(exec) or {} local server = string.match(exec, "rxmtp.*%s(%S+)%s*$") if server and not string.match(server, "^%-") then local test = string.match(exec, "rxmtp.*%s(%S+)%s+%S+%s*$") if test then local optionswithvalues = {["-l"]=1, ["-o"]=1, ["-r"]=1, ['-s']=1, ['-t']=1, ['-x']=1, ['-X']=1} if not optionswithvalues[test] then options[""] = server end else options[""] = server end end return options end local function validatesmtpentry(entry) local success = true if string.find(entry.value.ipaddr.value, "[^%w.]") then entry.value.ipaddr.errtxt = "Invalid interface / address" success = false end if string.find(entry.value.server.value, "[^%w.-]") and not fs.is_file(entry.value.server.value) then entry.value.server.errtxt = "Cannot find server binary" success = false end if string.find(entry.value.domain.value, "[^%w.-]") then entry.value.domain.errtxt = "Invalid domain" success = false end if entry.value.optionalserver.value ~= "" then if string.find(string.match(entry.value.optionalserver.value, "^[^:]+"), "[^%w.-]") then entry.value.optionalserver.errtxt = "Invalid server" success = false elseif string.match(entry.value.optionalserver.value, ":(.*)") and not validator.is_port(string.match(entry.value.optionalserver.value, ":(.*)")) then entry.value.optionalserver.errtxt = "Invalid port" success = false elseif entry.value.domain.value == "" then entry.value.optionalserver.errtxt = "Cannot define server without Custom Domain" success = false end elseif entry.value.domain.value ~= "" then entry.value.optionalserver.errtxt = "Must define server if Custom Domain defined" success = false end success = modelfunctions.validateselect(entry.value.optionalrewritelist) and success if entry.value.domain.value == "" and entry.value.optionalrewritelist.value ~= "" then entry.value.optionalrewritelist.errtxt = "Cannot define rewrite list without Custom Domain" success = false end success = modelfunctions.validateselect(entry.value.senderlistfile) and success success = modelfunctions.validateselect(entry.value.rcptlistfile) and success return success, entry end local function getsmtpcmd(ipaddr) config = config or parseconfigfile(fs.read_file(configfile) or "") local smtpconfig = getsmtpconfig(config) local exec = "" for i,entry in ipairs(smtpconfig) do if entry.ipaddr == ipaddr then exec = entry.cmd break end end return exec end local getfilelist = function() local filelist = {} if not fs.is_dir(smtpdirectory) then fs.create_directory(smtpdirectory) end for file in posix.files(smtpdirectory) do if fs.is_file(smtpdirectory .. file) then table.insert(filelist, smtpdirectory .. file) end end return filelist 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 function mymodule.getstatus() return modelfunctions.getstatus(processname, packagename, "TCP Proxy Status") end function mymodule.getconfigfile() return modelfunctions.getfiledetails(configfile) end function mymodule.setconfigfile(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, {configfile}) end function mymodule.getsmtpstatus() local value, errtxt = processinfo.package_version(smtppackagename) local version = cfe({ value=value, label="Program version", errtxt=errtxt, name=smtppackagename }) return cfe({ type="group", value={version=version}, label="SMTP Proxy Status" }) end function mymodule.listsmtpentries(self) local entries = cfe({ type="structure", value={}, label="SMTP Command Entries" }) if self then local interfacescontroller = self:new("alpine-baselayout/interfaces") local interfaces = interfacescontroller.model:get_addresses() interfacescontroller:destroy() -- add in entries for interfaces (w/o ipaddr) local interface for i,entry in ipairs(interfaces.value) do if interface ~= entry.interface then interface = entry.interface table.insert(entries.value, {interface=interface}) end table.insert(entries.value, entry) end end local reverseaddress = {} for i,int in ipairs(entries.value) do reverseaddress[int.ipaddr or int.interface] = i end config = config or parseconfigfile(fs.read_file(configfile) or "") local smtpconfig = getsmtpconfig(config) for i,int in ipairs(smtpconfig) do local pos = reverseaddress[int.ipaddr] if pos then entries.value[pos].cmd = int.cmd else if not validator.is_ipv4(int.ipaddr) then int.interface = int.ipaddr int.ipaddr = nil end table.insert(entries.value, int) end end return entries end function mymodule.readsmtpentry(ipaddr) local exec = getsmtpcmd(ipaddr) local listfiles = getfilelist() table.insert(listfiles, 1, "") local ipaddrcfe = cfe({ value=ipaddr, label="Interface / IP Address", readonly=true, seq=0 }) local server = cfe({ label="SMTP Server", descr="Domain name / address of different machine or local binary", seq=1 }) local addressonly = cfe({ type="boolean", value=false, label="Address Only", descr="Removes everything in the from field except address", seq=2 }) local domain = cfe({ label="Custom Domain", seq=3 }) local optionalserver = cfe({ label="Custom Domain Server", descr="Use this SMTP server[:port] for e-mails to Custom Domain", seq=4 }) local optionalrewritelist = cfe({ type="select", label="Custom Domain Rewrite List", descr="File with a list for sender rewriting for Custom Domain", option=listfiles, seq=5 }) local senderlistfile = cfe({ type="select", label="Sender List File", descr="File with a list for sender rewriting (and valid senders)", option=listfiles, seq=6 }) local rcptlistfile = cfe({ type="select", label="Recipient List File", descr="File with a list for recipient rewriting (and valid recipients)", option=listfiles, seq=7 }) if exec and exec ~= "" then local options = parsesmtpcmd(exec) server.value = options[""] or server.value addressonly.value = (options["-b"] ~= nil) if options["-o"] then domain.value = string.match(options["-o"], "^[^=]*") or "" optionalserver.value = string.match(options["-o"], "=([^=]*)") or "" optionalrewritelist.value = string.match(options["-o"], "=.*=(.*)") or "" end senderlistfile.value = options["-x"] or senderlistfile.value rcptlistfile.value = options["-X"] or rcptlistfile.value end return cfe({ type="group", value={ipaddr=ipaddrcfe, server=server, addressonly=addressonly, domain=domain, optionalserver=optionalserver, optionalrewritelist=optionalrewritelist, senderlistfile=senderlistfile, rcptlistfile=rcptlistfile}, label="SMTP Proxy Entry" }) end function mymodule.updatesmtpentry(self, entry) local success, entry = validatesmtpentry(entry) if success then local options = parsesmtpcmd(getsmtpcmd(entry.value.ipaddr.value)) options[""] = entry.value.server.value if entry.value.addressonly.value then options["-b"] = "" else options["-b"] = nil end if entry.value.domain.value == "" then options["-o"] = nil else options["-o"] = entry.value.domain.value .. "=" .. entry.value.optionalserver.value end if entry.value.optionalrewritelist.value ~= "" then options["-o"] = options["-o"] .. "=" .. entry.value.optionalrewritelist.value end if entry.value.senderlistfile.value ~= "" then options["-x"] = entry.value.senderlistfile.value else options["-x"] = nil end if entry.value.rcptlistfile.value ~= "" then options["-X"] = entry.value.rcptlistfile.value else options["-X"] = nil end local exec = {"/usr/sbin/rxmtp"} for option,value in pairs(options) do if option ~= "" then if value ~= "" then exec[#exec + 1] = option .. " " .. value else exec[#exec + 1] = option end end end if options[""] ~= "" then exec[#exec + 1] = options[""] end setsmtpcmd(entry.value.ipaddr.value, table.concat(exec, " ")) else entry.errtxt = "Failed to set SMTP Proxy Entry" end return entry end function mymodule.getdelsmtpentry(self, clientdata) local retval = {} retval.ipaddr = cfe({ value=clientdata.ipaddr or "", label="IP Address" }) return cfe({ type="group", value=retval, label="Delete SMTP Proxy Entry" }) end function mymodule.delsmtpentry(self, delentry) -- TODO - validate ipaddr setsmtpcmd(delentry.value.ipaddr.value, nil) return delentry end function mymodule.listsmtpfiles() local retval = cfe({ type="structure", value={}, label="SMTP Proxy Files" }) for i,file in ipairs(getfilelist()) do local filedetails = posix.stat(file) filedetails.filename = file table.insert(retval.value, filedetails) end return retval end function mymodule.getnewsmtpfile() local filename = cfe({ label="File Name", descr="Must be in "..smtpdirectory }) return cfe({ type="group", value={filename=filename}, label="SMTP Proxy File" }) end function mymodule.createsmtpfile(self, filedetails) local success = true if not validator.is_valid_filename(filedetails.value.filename.value, smtpdirectory) then success = false filedetails.value.filename.errtxt = "Invalid filename" else if not fs.is_dir(smtpdirectory) then fs.create_directory(smtpdirectory) end if posix.stat(filedetails.value.filename.value) then success = false filedetails.value.filename.errtxt = "Filename already exists" end end if success then fs.create_file(filedetails.value.filename.value) else filedetails.errtxt = "Failed to Create File" end return filedetails end function mymodule.readsmtpfile(filename) return modelfunctions.getfiledetails(filename, function(filename) return validator.is_valid_filename(filename, smtpdirectory) end) end function mymodule.updatesmtpfile(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, function(filename) return validator.is_valid_filename(filename, smtpdirectory) end) end function mymodule.getdelsmtpfile(self, clientdata) local retval = {} retval.filename = cfe({ value=clientdata.filename or "", label="File Name" }) return cfe({ type="group", value=retval, label="Delete SMTP Proxy File" }) end function mymodule.delsmtpfile(self, delfile) local filename = delfile.value.filename.value if validator.is_valid_filename(filename, smtpdirectory) and fs.is_file(filename) then os.remove(filename) else delfile.errtxt = "Failed to delete SMTP Proxy File" delfile.value.filename.errtxt = "Invalid filename" end return delfile end return mymodule