local mymodule = {} -- Load libraries posix = require("posix") modelfunctions = require("modelfunctions") dbmodelfunctions = require("dbmodelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") db = require("acf.db") -- Set variables local processname = "kamailio" local packagename = "kamailio" local baseurl = "/etc/kamailio" local kamctlrc_file = "/etc/kamailio/kamctlrc" local dbkam local config -- ################################################################################ -- DATABASE FUNCTIONS local function assert (v, m) if not v then m = m or "Assertion failed!" error(m, 0) end return v, m end local removequotes = function(value) if value and string.find(value, '^".*"$') then return string.sub(value, 2, -2) end return value end local databasecreate = function() if not dbkam then local engine -- parse the kamctlrc file config = config or format.parse_ini_file(fs.read_file(kamctlrc_file), "") or {} if not config.DBENGINE then error("Database engine not specified, please setup one in the config script "..kamctlrc_file) end -- create environment object if config.DBENGINE == "SQLITE" then engine = db.engine.sqlite3 elseif config.DBENGINE == "MYSQL" or config.DBENGINE == "mysql" or config.DBENGINE == "MySQL" then engine = db.engine.mysql elseif config.DBENGINE == "PGSQL" or config.DBENGINE == "pgsql" or config.DBENGINE == "postgres" or config.DBENGINE == "postgresql" or config.DBENGINE == "POSTGRESQL" then engine = db.engine.postgresql elseif config.DBENGINE == "ORACLE" or config.DBENGINE == "oracle" or config.DBENGINE == "Oracle" then error("ORACLE database not supported") elseif config.DBENGINE == "DBTEXT" or config.DBENGINE == "dbtext" or config.DBENGINE == "textdb" then error("DBTEXT database not supported") elseif config.DBENGINE == "DB_BERKELEY" or config.DBENGINE == "db_berkeley" or config.DBENGINE == "BERKELEY" or config.DBENGINE == "berkeley" then error("BERKELEY database not supported") else error("Unknown database engine "..config.DBENGINE) end -- We don't want to leave the port as null, or it will ask the user each time -- But, it usually isn't specified. Let's make the proper default local port if engine == db.engine.mysql then port = "3306" elseif engine == db.engine.postgresql then port = "5432" end -- connect to data source if engine == db.engine.sqlite3 then dbkam = db.create(engine, removequotes(config.DB_PATH) or "", "", "", "", "") else dbkam = db.create(engine, removequotes(config.DBNAME) or "kamailio", removequotes(config.DBRWUSER) or "kamailio", removequotes(config.DBRWPW) or "kamailiorw", removequotes(config.DBHOST) or "127.0.0.1", removequotes(config.DBPORT) or port) end end return dbkam end local databaseconnect = function() if not dbkam then databasecreate() end return dbkam.databaseconnect() end local databasedisconnect = function() local result = dbkam.databasedisconnect() dbkam = nil return result end local runsqlcommand = function(sql) mymodule.logevent(sql) return dbkam.runsqlcommand(sql) end local getselectresponse = function(sql) mymodule.logevent(sql) return dbkam.getselectresponse(sql) end -- ################################################################################ -- LOCAL FUNCTIONS local is_valid_filename = function(filename) local dirname = posix.dirname(filename) return validator.is_valid_filename(filename) and string.match(dirname, baseurl) and not string.match(dirname, "%.%.") end local function validate_user(user) local success = true if user.value.username.value == "" then user.value.username.errtxt = "Cannot be empty" success = false elseif string.find(user.value.username.value, "@") then user.value.username.errtxt = "Contains invalid character" success = false end if user.value.domain.value == "" then user.value.domain.errtxt = "Cannot be empty" success = false end -- password will not be present for deleteuser if user.value.password and user.value.password.value == "" then user.value.password.errtxt = "Cannot be empty" success = false end if user.value.password and user.value.password.value ~= user.value.password_confirm.value then user.value.password_confirm.errtxt = "Must match password" success = false end return success, user 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, "Kamailio Status") end function mymodule.get_filedetails(self, clientdata) return modelfunctions.getfiledetails(clientdata.filename, is_valid_filename) end function mymodule.update_filedetails(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, is_valid_filename) end function mymodule.list_files() local retval = {} for file in fs.find(null, baseurl) do local details = posix.stat(file) if details.type == "regular" then details.filename = file table.insert(retval, details) end end table.sort(retval, function(a,b) return a.filename < b.filename end) return cfe({ type="structure", value=retval, label="List of Kamailio files" }) end local function parse_db_show(table) -- These settings work for Postgres, MYSQL, and DBTEXT database local delimiter = "\'?%s*[,|\t]%s*\'?" local results = {} local errtxt local f local res, err = pcall(function() local connected = databaseconnect() f = modelfunctions.run_executable({"kamctl", "db", "show", dbkam.escape(table)}) if connected then databasedisconnect() end end) if not res and err then errtxt = err else for line in string.gmatch(f, "[^\n]+") do if #results == 0 and string.match(line, "^ERROR:") then errtxt = line results = nil break end if string.match(line, "^[+-]+$") then results = {} else local words = format.string_to_table(line, delimiter) if words and #words > 0 and words[1] ~= "id" then results[#results+1] = words end end end end return results, errtxt end function mymodule.list_users() -- Database format: id | username | domain | password | email_address | ha1 | ha1b | rpid local results = {} local r, errtxt r, errtxt = parse_db_show("subscriber") for i,words in ipairs(r or {}) do if #words > 1 then local temp = {username = words[2], domain = words[3], password = words[4], --email_address = words[5] } results[#results+1] = temp end end table.sort(results, function(a,b) return a.username < b.username end) return cfe({type="structure", value=results, label="Kamailio Users", errtxt=errtxt}) end function mymodule.get_new_user() local user = {} user.username = cfe({label="User Name", seq=1}) user.domain = cfe({label="Domain", seq=2}) user.password = cfe({type="password", label="Password", seq=3}) user.password_confirm = cfe({type="password", label="Password (confirm)", seq=4}) --user.email_address = cfe({label="E-mail Address", seq=4}) -- get the default domain config = config or format.parse_ini_file(fs.read_file(kamctlrc_file), "") or {} user.domain.value = removequotes(config.SIP_DOMAIN) or "" return cfe({type="group", value=user, label="Kamailio User"}) end function mymodule.create_new_user(self, user) local success = validate_user(user) if success then local res, err = pcall(function() local connected = databaseconnect() user.descr, user.errtxt = modelfunctions.run_executable({"kamctl", "add", dbkam.escape(user.value.username.value.."@"..user.value.domain.value), dbkam.escape(user.value.password.value)}) if connected then databasedisconnect() end end) if not res and err then user.errtxt = err end else user.errtxt = "Failed to create new user" end return user end function mymodule.get_delete_user(self, clientdata) local user = {} user.username = cfe({label="User Name", seq=1}) user.domain = cfe({label="Domain", seq=2}) -- get the default domain config = config or format.parse_ini_file(fs.read_file(kamctlrc_file), "") or {} user.domain.value = removequotes(config.SIP_DOMAIN) or "" return cfe({type="group", value=user, label="Delete User"}) end function mymodule.delete_user(self, user) local success = validate_user(user) if success then local res, err = pcall(function() local connected = databaseconnect() user.descr, user.errtxt = modelfunctions.run_executable({"kamctl", "rm", dbkam.escape(user.value.username.value.."@"..user.value.domain.value)}) if connected then databasedisconnect() end end) if not res and err then user.errtxt = err end else user.errtxt = "Failed to delete user" end return user end function mymodule.get_user(self, clientdata) local user = mymodule.get_new_user() self.handle_clientdata(user, clientdata) user.value.password.value = "" user.value.password_confirm.value = "" user.value.username.errtxt = "Invalid user" local users = mymodule.list_users() for i,u in ipairs(users.value) do if u.username == user.value.username.value and u.domain == user.value.domain.value then user.value.username.errtxt = nil user.value.username.readonly = true user.value.domain.readonly = true break end end return user end function mymodule.update_user(self, user) local success = validate_user(user) if success then local res, err = pcall(function() local connected = databaseconnect() user.descr, user.errtxt = modelfunctions.run_executable({"kamctl", "passwd", dbkam.escape(user.value.username.value.."@"..user.value.domain.value), dbkam.escape(user.value.password.value)}) if connected then databasedisconnect() end end) if not res and err then user.errtxt = err end else user.errtxt = "Failed to update user" end return user end function mymodule.get_create_database(self, clientdata) local retval = cfe({ type="group", value={}, label="Create Database" }) if not dbkam then databasecreate() end if dbkam and dbkam.engine == db.engine.mysql then -- parse the kamctlrc file to determine the root user config = config or format.parse_ini_file(fs.read_file(kamctlrc_file), "") or {} local user = removequotes(config.DBROOTUSER) or "root" retval.value.password = cfe({ type="password", label="Password", seq=1 }) self.handle_clientdata(retval, clientdata) -- MYSQL has some character sets that Kamailio cannot use, namely utf8 and ucs2 -- If it's using one of them, kamdbctl will prompt for which one to use -- so, have to check in advance local out,err = modelfunctions.run_executable({"mysql", "-h", dbkam.host, "-e", "show variables like '%character_set_server%'", "-u", user, "-p"..retval.value.password.value}) local charset = string.match(out, "([^%s]+)%s*$") or "" if string.find(charset, "utf8") or string.find(charset, "ucs2") then retval.value.characterset = cfe({ type="select", label="Character Set", option={}, seq=2 }) out,err = modelfunctions.run_executable({"mysql", "-h", dbkam.host, "-e", "show character set", "-u", user, "-p"..retval.value.password.value}) local charsets = format.string_to_table(out, "\n") for i,c in ipairs(charsets) do local label = string.match(c, "[^\t]*\t[^\t]*") local value = string.match(c, "[^\t]*") if label and value and value ~= "Charset" and not string.find (value, "utf8") and not string.find(value, "ucs2") then retval.value.characterset.option[#retval.value.characterset.option+1] = {label=label, value=value} end end end end return retval end function mymodule.create_database(self, create_db) local input = "y\ny\ny\n" -- MYSQL requires three y's, PGSQL only requires two if create_db.value.characterset then if not modelfunctions.validateselect(create_db.value.characterset) then create_db.errtxt = "Failed to create database" return create_db end input = create_db.value.characterset.value.."\n"..input end if create_db.value.password then input = create_db.value.password.value.."\n"..input end create_db.descr, create_db.errtxt = modelfunctions.run_executable({"kamdbctl", "create"}, true, input) return create_db end function mymodule.get_search_database(self, clientdata) local errtxt retval = {} retval.id = cfe({type="select", value=clientdata.id or "", label="Table.Column", option={}, seq=1}) retval.comparison = cfe({type="select", value=clientdata.comparison or "=", label="Comparison", option={"=", "!=", "~", "!~", "~*", "!*~"}, seq=2}) retval.value = cfe({label="Value", value=clientdata.value or "", descr="Value or SQL regular expression", seq=3}) local res, err = pcall(function() local connected = databaseconnect() local tables = dbkam.listtables() or {} for i,t in ipairs(tables) do local columns = dbkam.listcolumns(t) or {} for i,c in ipairs(columns) do retval.id.option[#retval.id.option + 1] = t.."."..c end end if connected then databasedisconnect() end end) if not res and err then errtxt = err end return cfe({type="group", value=retval, label="Database Search", errtxt=errtxt}) end function mymodule.search_database(self, search) if modelfunctions.validateselect(search.value.id) and modelfunctions.validateselect(search.value.comparison) then local res, err = pcall(function() local connected = databaseconnect() -- Get the rows from the DB search.value.result = cfe({type="structure", value={}, label="List of Rows", seq=4 }) local table, column = string.match(search.value.id.value, "^([^.]*)%.(.*)") if table then local sql = "SELECT * FROM "..dbkam.escape(table).." WHERE "..dbkam.escape(column)..dbkam.escape(search.value.comparison.value).."'"..dbkam.escape(search.value.value.value).."'" search.value.result.value = getselectresponse(sql) end if connected then databasedisconnect() end end) if not res and err then search.errtxt = err end else search.errtxt = "Failed to search database" end return search end for n,f in pairs(dbmodelfunctions) do mymodule[n] = function(...) return f(databasecreate, ...) end end return mymodule