mirror of
https://github.com/ntop/ntopng.git
synced 2026-04-29 23:49:33 +00:00
A "shadow directory" is now populated when the reload occurs and then swapped as the active directory. This avoids breaking the directory structure or changing files when other threads are possibly working on them. Fixes #3595
1298 lines
39 KiB
Lua
1298 lines
39 KiB
Lua
--
|
|
-- (C) 2019-20 - ntop.org
|
|
--
|
|
|
|
-- User scripts provide a scriptable way to interact with the ntopng
|
|
-- core. Users can provide their own modules to trigger custom alerts,
|
|
-- export data, or perform periodic tasks.
|
|
|
|
local os_utils = require("os_utils")
|
|
local json = require("dkjson")
|
|
local plugins_utils = require("plugins_utils")
|
|
|
|
local dirs = ntop.getDirs()
|
|
local info = ntop.getInfo()
|
|
|
|
local user_scripts = {}
|
|
|
|
-- ##############################################
|
|
|
|
user_scripts.field_units = {
|
|
seconds = "field_units.seconds",
|
|
bytes = "field_units.bytes",
|
|
flows = "field_units.flows",
|
|
packets = "field_units.packets",
|
|
mbits = "field_units.mbits",
|
|
hosts = "field_units.hosts",
|
|
syn_sec = "field_units.syn_sec",
|
|
flow_sec = "field_units.flow_sec",
|
|
percentage = "field_units.percentage",
|
|
syn_min = "field_units.syn_min",
|
|
}
|
|
|
|
local NON_TRAFFIC_ELEMENT_CONF_KEY = "all"
|
|
local NON_TRAFFIC_ELEMENT_ENTITY = "no_entity"
|
|
local ALL_HOOKS_CONFIG_KEY = "all"
|
|
local CONFIGSETS_KEY = "ntopng.prefs.user_scripts.configsets_v2"
|
|
user_scripts.DEFAULT_CONFIGSET_ID = 0
|
|
|
|
-- NOTE: the subdir id must be unique
|
|
-- target_type: when used with configsets, specifies the allowed target.
|
|
-- - cidr: IPv4/IPv6 address or CIDR (e.g. 192.168.0.0/16, 1.2.3.4)
|
|
-- - interface: a network interface name (e.g. eth0)
|
|
-- - network: a local network CIDR (e.g. 192.168.0.0/24)
|
|
-- - none: no targets allowed
|
|
local available_subdirs = {
|
|
{
|
|
id = "host",
|
|
label = "hosts",
|
|
target_type = "cidr",
|
|
}, {
|
|
id = "flow",
|
|
label = "flows",
|
|
target_type = "interface",
|
|
}, {
|
|
id = "interface",
|
|
label = "interfaces",
|
|
target_type = "interface",
|
|
}, {
|
|
id = "network",
|
|
label = "networks",
|
|
target_type = "network",
|
|
}, {
|
|
id = "snmp_device",
|
|
label = "host_details.snmp",
|
|
target_type = "cidr",
|
|
}, {
|
|
id = "system",
|
|
label = "system",
|
|
target_type = "none",
|
|
}, {
|
|
id = "syslog",
|
|
label = "Syslog",
|
|
target_type = "interface",
|
|
}
|
|
}
|
|
|
|
-- User scripts category consts
|
|
user_scripts.script_categories = {
|
|
other = {
|
|
icon = "fas fa-scroll",
|
|
i18n_title = "user_scripts.category_other",
|
|
i18n_descr = "user_scripts.category_other_descr",
|
|
},
|
|
security = {
|
|
icon = "fas fa-shield-alt",
|
|
i18n_title = "user_scripts.category_security",
|
|
i18n_descr = "user_scripts.category_security_descr",
|
|
},
|
|
internals = {
|
|
icon = "fas fa-wrench",
|
|
i18n_title = "user_scripts.category_internals",
|
|
i18n_descr = "user_scripts.category_internals_descr",
|
|
},
|
|
network = {
|
|
icon = "fas fa-network-wired",
|
|
i18n_title = "user_scripts.category_network",
|
|
i18n_descr = "user_scripts.category_network_descr",
|
|
},
|
|
system = {
|
|
icon = "fas fa-server",
|
|
i18n_title = "user_scripts.category_system",
|
|
i18n_descr = "user_scripts.category_system_descr",
|
|
}
|
|
}
|
|
|
|
-- Auto-assign an id to the categories
|
|
local cat_id = 1
|
|
for cat_k, cat_v in pairs(user_scripts.script_categories) do
|
|
cat_v["id"] = cat_id
|
|
cat_id = cat_id + 1
|
|
end
|
|
|
|
-- Hook points for flow/periodic modules
|
|
-- NOTE: keep in sync with the Documentation
|
|
user_scripts.script_types = {
|
|
flow = {
|
|
parent_dir = "interface",
|
|
hooks = {"protocolDetected", "statusChanged", "flowEnd", "periodicUpdate"},
|
|
subdirs = {"flow"},
|
|
}, traffic_element = {
|
|
parent_dir = "interface",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
subdirs = {"interface", "host", "network"},
|
|
has_per_hook_config = true, -- Each hook has a separate configuration
|
|
}, snmp_device = {
|
|
parent_dir = "system",
|
|
hooks = {"snmpDevice", "snmpDeviceInterface"},
|
|
subdirs = {"snmp_device"},
|
|
}, system = {
|
|
parent_dir = "system",
|
|
hooks = {"min", "5mins", "hour", "day"},
|
|
subdirs = {"system"},
|
|
has_per_hook_config = true, -- Each hook has a separate configuration
|
|
default_config_only = true, -- Only the default configset can be used
|
|
}, syslog = {
|
|
parent_dir = "system",
|
|
hooks = {"handleEvent"},
|
|
subdirs = {"syslog"},
|
|
default_config_only = true, -- Only the default configset can be used
|
|
}
|
|
}
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Given a category found in a user script, this method checks whether the category is valid
|
|
-- and, if not valid, it assigns to the plugin a default category
|
|
local function checkCategory(category)
|
|
if not category or not category["id"] then
|
|
return user_scripts.script_categories.other
|
|
end
|
|
|
|
for cat_k, cat_v in pairs(user_scripts.script_categories) do
|
|
if category["id"] == cat_v["id"] then
|
|
return cat_v
|
|
end
|
|
end
|
|
|
|
return user_scripts.script_categories.other
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Given a subdir, returns the corresponding script type
|
|
function user_scripts.getScriptType(search_subdir)
|
|
for _, script_type in pairs(user_scripts.script_types) do
|
|
for _, subdir in pairs(script_type.subdirs) do
|
|
if(subdir == search_subdir) then
|
|
return(script_type)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Not found
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Table to keep per-subdir then per-module then per-hook benchmarks
|
|
--
|
|
-- The structure is the following
|
|
--
|
|
-- table
|
|
-- flow table
|
|
-- flow.mud table
|
|
-- flow.mud.protocolDetected table
|
|
-- flow.mud.protocolDetected.tot_elapsed number 0.00031600000000021
|
|
-- flow.mud.protocolDetected.tot_num_calls number 4
|
|
-- flow.score table
|
|
-- flow.score.protocolDetected table
|
|
-- flow.score.protocolDetected.tot_elapsed number 0.00013700000000005
|
|
-- flow.score.protocolDetected.tot_num_calls number 4
|
|
-- flow.score.statusChanged table
|
|
-- flow.score.statusChanged.tot_elapsed number 0
|
|
-- flow.score.statusChanged.tot_num_calls number 0
|
|
local benchmarks = {}
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getSubdirectoryPath(script_type, subdir, is_pro)
|
|
local prefix = plugins_utils.getRuntimePath() .. "/callbacks"
|
|
local path
|
|
|
|
if not isEmptyString(subdir) and subdir ~= "." then
|
|
path = string.format("%s/%s/%s", prefix, script_type.parent_dir, subdir)
|
|
else
|
|
path = string.format("%s/%s", prefix, script_type.parent_dir)
|
|
end
|
|
|
|
return os_utils.fixPath(path)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Wrap any hook function to compute its execution time which is then added
|
|
-- to the benchmarks table.
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
return function(...)
|
|
local start = ntop.getticks()
|
|
local result = {hook_fn(...)}
|
|
local finish = ntop.getticks()
|
|
local elapsed = finish - start
|
|
|
|
-- Update benchmark results by addin a function call and the elapsed time of this call
|
|
benchmarks[subdir][mod_k][hook]["tot_num_calls"] = benchmarks[subdir][mod_k][hook]["tot_num_calls"] + 1
|
|
benchmarks[subdir][mod_k][hook]["tot_elapsed"] = benchmarks[subdir][mod_k][hook]["tot_elapsed"] + elapsed
|
|
|
|
-- traceError(TRACE_NORMAL,TRACE_CONSOLE, string.format("[%s][elapsed: %.2f][tot_elapsed: %.2f][tot_num_calls: %u]",
|
|
-- hook, elapsed,
|
|
-- benchmarks[subdir][mod_k][hook]["tot_elapsed"],
|
|
-- benchmarks[subdir][mod_k][hook]["tot_num_calls"]))
|
|
|
|
return table.unpack(result)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Initializes benchmark facilities for any hook function
|
|
--
|
|
-- @param subdir the modules subdir
|
|
-- @param mod_k the key of the user script
|
|
-- @param hook the name of the hook in the user script
|
|
-- @param hook_fn the hook function in the user script
|
|
--
|
|
-- @return function(...) wrapper ready to be called for the execution of hook_fn
|
|
local function benchmark_init(subdir, mod_k, hook, hook_fn)
|
|
-- NOTE: 5min/hour/day are not monitored. They would collide in the user_scripts_benchmarks_key.
|
|
if((hook ~= "5min") and (hook ~= "hour") and (hook ~= "day")) then
|
|
-- Prepare the benchmark table fo the hook_fn which is being benchmarked
|
|
if not benchmarks[subdir] then
|
|
benchmarks[subdir] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k] then
|
|
benchmarks[subdir][mod_k] = {}
|
|
end
|
|
|
|
if not benchmarks[subdir][mod_k][hook] then
|
|
benchmarks[subdir][mod_k][hook] = {tot_num_calls = 0, tot_elapsed = 0}
|
|
end
|
|
|
|
-- Finally prepare and return the hook_fn wrapped with benchmark facilities
|
|
return benchmark_hook_fn(subdir, mod_k, hook, hook_fn)
|
|
else
|
|
return(hook_fn)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
--~ schema_prefix: "flow_user_script" or "elem_user_script"
|
|
function user_scripts.ts_dump(when, ifid, verbose, schema_prefix, all_scripts)
|
|
local ts_utils = require("ts_utils_core")
|
|
|
|
for subdir, script_type in pairs(all_scripts) do
|
|
local rv = user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local total = {tot_elapsed = 0, tot_num_calls = 0}
|
|
|
|
for modkey, stats in pairs(rv) do
|
|
ts_utils.append(schema_prefix .. ":duration", {ifid = ifid, user_script = modkey, subdir = subdir, num_ms = stats.tot_elapsed * 1000}, when)
|
|
ts_utils.append(schema_prefix .. ":num_calls", {ifid = ifid, user_script = modkey, subdir = subdir, num_calls = stats.tot_num_calls}, when)
|
|
|
|
total.tot_elapsed = total.tot_elapsed + stats.tot_elapsed
|
|
total.tot_num_calls = total.tot_num_calls + stats.tot_num_calls
|
|
end
|
|
|
|
ts_utils.append(schema_prefix .. ":total_stats", {ifid = ifid, subdir = subdir, num_ms = total.tot_elapsed * 1000, num_calls = total.tot_num_calls}, when)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function user_scripts_benchmarks_key(ifid, subdir)
|
|
return string.format("ntopng.cache.ifid_%d.user_scripts_benchmarks.subdir_%s", ifid, subdir)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Returns the benchmark stats, aggregating them by module
|
|
function user_scripts.getAggregatedStats(ifid, script_type, subdir)
|
|
local bencmark = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
local rv = {}
|
|
|
|
if(not isEmptyString(bencmark)) then
|
|
bencmark = json.decode(bencmark)
|
|
|
|
if(bencmark ~= nil) then
|
|
for scriptk, hooks in pairs(bencmark) do
|
|
local aggr_val = {tot_num_calls = 0, tot_elapsed = 0}
|
|
|
|
for _, hook_benchmark in pairs(hooks) do
|
|
aggr_val.tot_elapsed = aggr_val.tot_elapsed + hook_benchmark.tot_elapsed
|
|
aggr_val.tot_num_calls = hook_benchmark.tot_num_calls + aggr_val.tot_num_calls
|
|
end
|
|
|
|
if(aggr_val.tot_num_calls > 0) then
|
|
rv[scriptk] = aggr_val
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Save benchmarks results and possibly print them to stdout
|
|
--
|
|
-- @param to_stdout dump results also to stdout
|
|
function user_scripts.benchmark_dump(ifid, to_stdout)
|
|
-- Convert ticks to seconds
|
|
for subdir, modules in pairs(benchmarks) do
|
|
local rv = {}
|
|
|
|
for mod_k, hooks in pairs(modules) do
|
|
for hook, hook_benchmark in pairs(hooks) do
|
|
if hook_benchmark["tot_num_calls"] > 0 then
|
|
hook_benchmark["tot_elapsed"] = hook_benchmark["tot_elapsed"] / ntop.gettickspersec()
|
|
|
|
rv[mod_k] = rv[mod_k] or {}
|
|
rv[mod_k][hook] = hook_benchmark
|
|
|
|
if to_stdout then
|
|
traceError(TRACE_NORMAL,TRACE_CONSOLE,
|
|
string.format("[%s] %s() [script: %s][elapsed: %.4f][num: %u][speed: %.4f]\n",
|
|
subdir, hook, mod_k, hook_benchmark["tot_elapsed"], hook_benchmark["tot_num_calls"],
|
|
hook_benchmark["tot_elapsed"] / hook_benchmark["tot_num_calls"]))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
ntop.setCache(user_scripts_benchmarks_key(ifid, subdir), json.encode(rv), 3600 --[[ 1 hour --]])
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getScriptsDirectories(script_type, subdir)
|
|
local check_dirs = {
|
|
user_scripts.getSubdirectoryPath(script_type, subdir),
|
|
}
|
|
|
|
return(check_dirs)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Lists available user scripts.
|
|
-- @params script_type one of user_scripts.script_types
|
|
-- @params subdir the modules subdir
|
|
-- @return a list of available module names
|
|
function user_scripts.listScripts(script_type, subdir)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
local rv = {}
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
rv[#rv + 1] = mod_fname
|
|
end
|
|
end
|
|
end
|
|
|
|
return rv
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getLastBenchmark(ifid, subdir)
|
|
local scripts_benchmarks = ntop.getCache(user_scripts_benchmarks_key(ifid, subdir))
|
|
|
|
if(not isEmptyString(scripts_benchmarks)) then
|
|
scripts_benchmarks = json.decode(scripts_benchmarks)
|
|
else
|
|
scripts_benchmarks = {}
|
|
end
|
|
|
|
return(scripts_benchmarks)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
|
|
local user_scripts_templates = require("user_scripts_templates")
|
|
|
|
user_script.key = mod_fname
|
|
user_script.path = full_path
|
|
user_script.subdir = subdir
|
|
user_script.default_enabled = ternary(user_script.default_enabled == false, false, true --[[ a nil value means enabled ]])
|
|
user_script.source_path = plugins_utils.getUserScriptSourcePath(user_script.path)
|
|
user_script.plugin = plugin
|
|
user_script.script_type = script_type
|
|
user_script.edition = plugin.edition
|
|
user_script.category = checkCategory(user_script.category)
|
|
|
|
if(user_script.gui and user_script.gui.input_builder) then
|
|
user_script.template = user_scripts_templates[user_script.gui.input_builder]
|
|
|
|
if(user_script.template == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown template '%s' for user script '%s'", user_script.gui.input_builder, mod_fname))
|
|
end
|
|
|
|
-- Possibly localize the input title/description
|
|
if user_script.gui.input_title then
|
|
user_script.gui.input_title = i18n(user_script.gui.input_title) or user_script.gui.input_title
|
|
end
|
|
if user_script.gui.input_description then
|
|
user_script.gui.input_description = i18n(user_script.gui.input_description) or user_script.gui.input_description
|
|
end
|
|
end
|
|
|
|
if(user_script.template == nil) then
|
|
user_script.template = user_scripts_templates.default
|
|
end
|
|
|
|
-- Expand hooks
|
|
if(user_script.hooks["all"] ~= nil) then
|
|
local callback = user_script.hooks["all"]
|
|
user_script.hooks["all"] = nil
|
|
|
|
for _, hook in pairs(script_type.hooks) do
|
|
user_script.hooks[hook] = callback
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
|
|
local alerts_disabled = (not areAlertsEnabled())
|
|
local setup_ok = true
|
|
|
|
-- Recheck the edition as the demo mode may expire
|
|
if((plugin.edition == "pro" and (not ntop.isPro())) or
|
|
((plugin.edition == "enterprise" and (not ntop.isEnterprise())))) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping user script '%s' with '%s' edition", mod_fname, plugin.edition))
|
|
return(nil)
|
|
end
|
|
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Loading user script '%s'", mod_fname))
|
|
|
|
local user_script = dofile(full_path)
|
|
|
|
if(type(user_script) ~= "table") then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Loading '%s' failed", full_path))
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and user_script.packet_interface_only and (not interface.isPacketInterface())) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for non packet interface", mod_fname))
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and ((user_script.nedge_exclude and ntop.isnEdge()) or (user_script.nedge_only and (not ntop.isnEdge())))) then
|
|
return(nil)
|
|
end
|
|
|
|
if((not return_all) and (user_script.windows_exclude and ntop.isWindows())) then
|
|
return(nil)
|
|
end
|
|
|
|
if(table.empty(user_script.hooks)) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("No 'hooks' defined in user script '%s', skipping", mod_fname))
|
|
return(nil)
|
|
end
|
|
|
|
if(user_script.l7_proto ~= nil) then
|
|
user_script.l7_proto_id = interface.getnDPIProtoId(user_script.l7_proto)
|
|
|
|
if(user_script.l7_proto_id == -1) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown L7 protocol filter '%s' in user script '%s', skipping", user_script.l7_proto, mod_fname))
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
if((not user_script.gui) or (not user_script.gui.i18n_title) or (not user_script.gui.i18n_description)) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Module '%s' does not define a gui", mod_fname))
|
|
end
|
|
|
|
-- Augument with additional attributes
|
|
init_user_script(user_script, mod_fname, full_path, plugin, script_type, subdir)
|
|
|
|
if((not return_all) and alerts_disabled and user_script.is_alert) then
|
|
return(nil)
|
|
end
|
|
|
|
if(hook_filter ~= nil) then
|
|
-- Only return modules which should be called for the specified hook
|
|
if((user_script.hooks[hook_filter] == nil) and (user_script.hooks["all"] == nil)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' for hook '%s'", user_script.key, hook_filter))
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
if(scripts_filter ~= nil) then
|
|
local script_ok = scripts_filter(user_script)
|
|
|
|
if(not script_ok) then
|
|
return(nil)
|
|
end
|
|
end
|
|
|
|
-- If a setup function is available, call it
|
|
if(user_script.setup ~= nil) then
|
|
setup_ok = user_script.setup()
|
|
end
|
|
|
|
if((not return_all) and (not setup_ok)) then
|
|
traceError(TRACE_DEBUG, TRACE_CONSOLE, string.format("Skipping module '%s' as setup() returned %s", user_script.key, setup_ok))
|
|
return(nil)
|
|
end
|
|
|
|
return(user_script)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Load the user scripts.
|
|
-- @param ifid the interface ID
|
|
-- @param script_type one of user_scripts.script_types
|
|
-- @param subdir the modules subdir. *NOTE* this must be unique as it is used as a key.
|
|
-- @param options an optional table with the following supported options:
|
|
-- - hook_filter: if non nil, only load the user scripts for the specified hook
|
|
-- - do_benchmark: if true, computes benchmarks for every hook
|
|
-- - return_all: if true, returns all the scripts, even those with filters not matching the current configuration
|
|
-- NOTE: this can only be applied if the script type has the "has_no_entity" flag set.
|
|
-- - scripts_filter: a filter function(user_script) -> true, false. false will cause the script to be skipped.
|
|
-- @return {modules = key->user_script, hooks = user_script->function}
|
|
function user_scripts.load(ifid, script_type, subdir, options)
|
|
local rv = {modules = {}, hooks = {}, conf = {}}
|
|
local old_ifid = interface.getId()
|
|
options = options or {}
|
|
ifid = tonumber(ifid)
|
|
|
|
-- Load additional schemas
|
|
plugins_utils.loadSchemas(options.hook_filter)
|
|
|
|
local hook_filter = options.hook_filter
|
|
local do_benchmark = options.do_benchmark
|
|
local return_all = options.return_all
|
|
local scripts_filter = options.scripts_filter
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(tostring(ifid)) -- required for interface.isPacketInterface() below
|
|
end
|
|
|
|
for _, hook in pairs(script_type.hooks) do
|
|
rv.hooks[hook] = {}
|
|
end
|
|
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
for fname in pairs(ntop.readdir(checks_dir)) do
|
|
if string.ends(fname, ".lua") then
|
|
local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
|
|
local full_path = os_utils.fixPath(checks_dir .. "/" .. fname)
|
|
local plugin = plugins_utils.getUserScriptPlugin(full_path)
|
|
|
|
if(plugin == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Skipping unknown user script '%s'", mod_fname))
|
|
goto next_module
|
|
end
|
|
|
|
if(rv.modules[mod_fname]) then
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Skipping duplicate module '%s'", mod_fname))
|
|
goto next_module
|
|
end
|
|
|
|
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir, return_all, scripts_filter, hook_filter)
|
|
|
|
if(not user_script) then
|
|
goto next_module
|
|
end
|
|
|
|
-- Checks passed, now load the script information
|
|
|
|
-- Populate hooks fast lookup table
|
|
for hook, hook_fn in pairs(user_script.hooks) do
|
|
-- load previously computed benchmarks (if any)
|
|
-- benchmarks are loaded even if their computation is disabled with a do_benchmark ~= true
|
|
if(rv.hooks[hook] == nil) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Unknown hook '%s' in module '%s'", hook, user_script.key))
|
|
else
|
|
if do_benchmark then
|
|
rv.hooks[hook][user_script.key] = benchmark_init(subdir, user_script.key, hook, hook_fn)
|
|
else
|
|
rv.hooks[hook][user_script.key] = hook_fn
|
|
end
|
|
end
|
|
end
|
|
|
|
if(rv.hooks["periodicUpdate"] ~= nil) then
|
|
-- Set the update frequency
|
|
local default_update_freq = 120 -- Default: every 2 minutes
|
|
|
|
if(user_script.periodic_update_seconds ~= nil) then
|
|
if((user_script.periodic_update_seconds % 30) ~= 0) then
|
|
traceError(TRACE_WARNING, TRACE_CONSOLE, string.format(
|
|
"Update_periodicity '%s' is not multiple of 30 in '%s', using default (%u)",
|
|
user_script.periodic_update_seconds, user_script.key, default_update_freq))
|
|
user_script.periodic_update_seconds = default_update_freq
|
|
end
|
|
else
|
|
user_script.periodic_update_seconds = default_update_freq
|
|
end
|
|
|
|
user_script.periodic_update_divisor = math.floor(user_script.periodic_update_seconds / 30)
|
|
end
|
|
|
|
rv.modules[user_script.key] = user_script
|
|
end
|
|
|
|
::next_module::
|
|
end
|
|
end
|
|
|
|
if(old_ifid ~= ifid) then
|
|
interface.select(tostring(old_ifid))
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Convenient method to only load a specific script
|
|
function user_scripts.loadModule(ifid, script_type, subdir, mod_fname)
|
|
local check_dirs = getScriptsDirectories(script_type, subdir)
|
|
|
|
for _, checks_dir in pairs(check_dirs) do
|
|
local full_path = os_utils.fixPath(checks_dir .. "/" .. mod_fname .. ".lua")
|
|
local plugin = plugins_utils.getUserScriptPlugin(full_path)
|
|
|
|
if(ntop.exists(full_path) and (plugin ~= nil)) then
|
|
local user_script = loadAndCheckScript(mod_fname, full_path, plugin, script_type, subdir)
|
|
|
|
return(user_script)
|
|
end
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.runPeriodicScripts(granularity)
|
|
if(granularity == "min") then
|
|
interface.checkInterfaceAlertsMin()
|
|
interface.checkHostsAlertsMin()
|
|
interface.checkNetworksAlertsMin()
|
|
elseif(granularity == "5mins") then
|
|
interface.checkInterfaceAlerts5Min()
|
|
interface.checkHostsAlerts5Min()
|
|
interface.checkNetworksAlerts5Min()
|
|
elseif(granularity == "hour") then
|
|
interface.checkInterfaceAlertsHour()
|
|
interface.checkHostsAlertsHour()
|
|
interface.checkNetworksAlertsHour()
|
|
elseif(granularity == "day") then
|
|
interface.checkInterfaceAlertsDay()
|
|
interface.checkHostsAlertsDay()
|
|
interface.checkNetworksAlertsDay()
|
|
else
|
|
traceError(TRACE_ERROR, TRACE_CONSOLE, "Unknown granularity " .. granularity)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Teardown function, to be called at the end of the VM
|
|
function user_scripts.teardown(available_modules, do_benchmark, do_print_benchmark)
|
|
for _, script in pairs(available_modules.modules) do
|
|
if script.teardown then
|
|
script.teardown()
|
|
end
|
|
end
|
|
|
|
if do_benchmark then
|
|
local ifid = interface.getId()
|
|
user_scripts.benchmark_dump(ifid, do_print_benchmark)
|
|
end
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.listSubdirs()
|
|
local rv = {}
|
|
|
|
for _, subdir in ipairs(available_subdirs) do
|
|
local item = table.clone(subdir)
|
|
item.label = i18n(item.label) or item.label
|
|
|
|
rv[#rv + 1] = item
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getSubdirTargetType(search_subdir)
|
|
for _, subdir in pairs(available_subdirs) do
|
|
if(subdir.id == search_subdir) then
|
|
return(subdir.target_type)
|
|
end
|
|
end
|
|
|
|
return "none"
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function findConfigSet(configsets, name)
|
|
for id, configset in pairs(configsets) do
|
|
if(configset.name == name) then
|
|
return(configset)
|
|
end
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function getNewConfigSetId(configsets)
|
|
local max_id = -1
|
|
|
|
for i in pairs(configsets) do
|
|
max_id = math.max(max_id, tonumber(i))
|
|
end
|
|
|
|
return(max_id+1)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function validateConfigsets(configsets)
|
|
local cur_targets = {}
|
|
|
|
-- Ensure that no duplicate target is set
|
|
for _, configset in pairs(configsets) do
|
|
for subdir, subdir_table in pairs(configset.targets) do
|
|
cur_targets[subdir] = cur_targets[subdir] or {}
|
|
|
|
for _, conf_target in ipairs(subdir_table) do
|
|
local is_v4 = isIPv4(conf_target)
|
|
local is_v6 = isIPv6(conf_target)
|
|
local conf_target_normalized = nil
|
|
|
|
if(is_v4 or is_v6) then
|
|
local address, prefix = splitNetworkPrefix(conf_target)
|
|
local max_prefixlen = ternary(is_v4, 32, 128)
|
|
|
|
if((prefix == nil) or (prefix >= max_prefixlen)) then
|
|
prefix = max_prefixlen
|
|
end
|
|
|
|
-- Normalize
|
|
conf_target_normalized = ntop.networkPrefix(address, prefix) .. "/" .. prefix
|
|
else
|
|
conf_target_normalized = conf_target
|
|
end
|
|
|
|
local existing_id = cur_targets[subdir][conf_target_normalized]
|
|
|
|
if(existing_id) then
|
|
return false, i18n("configsets.duplicate_target", {target = conf_target, confname1 = configsets[existing_id].name, confname2 = configset.name})
|
|
end
|
|
|
|
cur_targets[subdir][conf_target_normalized] = configset.id
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local function saveConfigsets(configsets)
|
|
local to_delete = ntop.getHashKeysCache(CONFIGSETS_KEY) or {}
|
|
local rv, err = validateConfigsets(configsets)
|
|
|
|
if(not rv) then
|
|
return rv, err
|
|
end
|
|
|
|
for _, configset in pairs(configsets) do
|
|
local k = string.format("%d", configset.id)
|
|
local v = json.encode(configset)
|
|
|
|
ntop.setHashCache(CONFIGSETS_KEY, k, v)
|
|
to_delete[k] = nil
|
|
end
|
|
|
|
for confid in pairs(to_delete) do
|
|
ntop.delHashCache(CONFIGSETS_KEY, confid)
|
|
end
|
|
|
|
-- Reload the periodic scripts as the configuration has changed
|
|
ntop.reloadPeriodicScripts()
|
|
|
|
return true
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local cached_config_sets = nil
|
|
|
|
function user_scripts.getConfigsets()
|
|
if cached_config_sets then
|
|
return(cached_config_sets)
|
|
end
|
|
|
|
local configsets = ntop.getHashAllCache(CONFIGSETS_KEY) or {}
|
|
local rv = {}
|
|
|
|
for _, confset_json in pairs(configsets) do
|
|
local confset = json.decode(confset_json)
|
|
|
|
if confset then
|
|
rv[confset.id] = confset
|
|
end
|
|
end
|
|
|
|
-- Cache to avoid loading them again
|
|
cached_config_sets = rv
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.deleteConfigset(confid)
|
|
confid = tonumber(confid)
|
|
|
|
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
|
|
return false, "Cannot delete default configset"
|
|
end
|
|
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
configsets[confid] = nil
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.createOrReplaceConfigset(configset)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
local existing = findConfigSet(configsets, configset.name)
|
|
if existing then
|
|
configsets[existing.id] = nil
|
|
end
|
|
|
|
local new_confid = 0
|
|
if configset.id ~= 0 then
|
|
new_confid = getNewConfigSetId(configsets)
|
|
end
|
|
|
|
configsets[new_confid] = table.clone(configset)
|
|
configsets[new_confid].id = new_confid
|
|
|
|
local rv, err = saveConfigsets(configsets)
|
|
|
|
if not rv then
|
|
return rv, err
|
|
end
|
|
|
|
return true, new_confid
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.renameConfigset(confid, new_name)
|
|
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
|
|
return false, "Cannot rename default configset"
|
|
end
|
|
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local existing = findConfigSet(configsets, new_name)
|
|
|
|
if existing then
|
|
if(existing.id == confid) then
|
|
-- Renaming to the same name has no effect
|
|
return true
|
|
end
|
|
|
|
return false, i18n("configsets.error_exists", {name=new_name})
|
|
end
|
|
|
|
configsets[confid].name = new_name
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.cloneConfigset(confid, new_name)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local existing = findConfigSet(configsets, new_name)
|
|
|
|
if existing then
|
|
return false, i18n("configsets.error_exists", {name=new_name})
|
|
end
|
|
|
|
local new_confid = getNewConfigSetId(configsets)
|
|
|
|
configsets[new_confid] = table.clone(configsets[confid])
|
|
configsets[new_confid].id = new_confid
|
|
configsets[new_confid].name = new_name
|
|
configsets[new_confid].targets = {}
|
|
|
|
local rv, err = saveConfigsets(configsets)
|
|
|
|
if(not rv) then
|
|
return rv, err
|
|
end
|
|
|
|
return true, new_confid
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.setConfigsetTargets(subdir, confid, targets)
|
|
local configsets = user_scripts.getConfigsets()
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
if(confid == user_scripts.DEFAULT_CONFIGSET_ID) then
|
|
return false, "Cannot set target on the default configuration"
|
|
end
|
|
|
|
-- Update the targets
|
|
configsets[confid].targets[subdir] = targets
|
|
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Update the configuration of a specific script in a configset
|
|
function user_scripts.updateScriptConfig(confid, script_key, subdir, new_config)
|
|
local configsets = user_scripts.getConfigsets()
|
|
new_config = new_config or {}
|
|
local applied_config = {}
|
|
|
|
if(configsets[confid] == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
|
|
|
|
if(script) then
|
|
-- Try to validate the configuration
|
|
for hook, conf in pairs(new_config) do
|
|
local valid = true
|
|
local rv_or_err = ""
|
|
|
|
if(conf.enabled == nil) then
|
|
return false, "Missing 'enabled' item"
|
|
end
|
|
|
|
if(conf.script_conf == nil) then
|
|
return false, "Missing 'script_conf' item"
|
|
end
|
|
|
|
if conf.enabled then
|
|
valid, rv_or_err = script.template:parseConfig(script, conf.script_conf)
|
|
else
|
|
-- Assume the config is valid when the script is disabled to simplify the check
|
|
valid = true
|
|
rv_or_err = conf.script_conf
|
|
end
|
|
|
|
if(not valid) then
|
|
return false, rv_or_err
|
|
end
|
|
|
|
-- The validator may have changed the configuration
|
|
conf.script_conf = rv_or_err
|
|
applied_config[hook] = conf
|
|
end
|
|
end
|
|
|
|
local config = configsets[confid].config
|
|
|
|
config[subdir] = config[subdir] or {}
|
|
config[subdir][script_key] = applied_config
|
|
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.toggleScript(confid, script_key, subdir, enable)
|
|
local configsets = user_scripts.getConfigsets()
|
|
local configset = configsets[confid]
|
|
|
|
if(configset == nil) then
|
|
return false, i18n("configsets.unknown_id", {confid=confid})
|
|
end
|
|
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local script = user_scripts.loadModule(interface.getId(), script_type, subdir, script_key)
|
|
|
|
if(script == nil) then
|
|
return false, i18n("configsets.unknown_user_script", {user_script=script_key})
|
|
end
|
|
|
|
local config = user_scripts.getScriptConfig(configset, script, subdir)
|
|
|
|
if(config == nil) then
|
|
return false
|
|
end
|
|
|
|
for _, hook in pairs(config) do
|
|
hook.enabled = enable
|
|
end
|
|
|
|
return saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.loadDefaultConfig()
|
|
local ifid = getSystemInterfaceId()
|
|
local configsets = user_scripts.getConfigsets()
|
|
local default_conf = configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if default_conf then
|
|
default_conf = default_conf.config or {}
|
|
|
|
-- Drop possible nested values due to a previous bug
|
|
default_conf.config = nil
|
|
else
|
|
default_conf = {}
|
|
end
|
|
|
|
for type_id, script_type in pairs(user_scripts.script_types) do
|
|
for _, subdir in pairs(script_type.subdirs) do
|
|
local scripts = user_scripts.load(ifid, script_type, subdir, {return_all = true})
|
|
|
|
for key, usermod in pairs(scripts.modules) do
|
|
if((usermod.default_enabled ~= nil) or (usermod.default_value ~= nil)) then
|
|
default_conf[subdir] = default_conf[subdir] or {}
|
|
default_conf[subdir][key] = default_conf[subdir][key] or {}
|
|
local script_config = default_conf[subdir][key]
|
|
local hooks = ternary(script_type.has_per_hook_config, usermod.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
|
|
|
|
for hook in pairs(hooks) do
|
|
-- Do not override an existing configuration
|
|
if(script_config[hook] == nil) then
|
|
script_config[hook] = {
|
|
enabled = usermod.default_enabled or false,
|
|
script_conf = usermod.default_value or {},
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
configsets[user_scripts.DEFAULT_CONFIGSET_ID] = {
|
|
id = user_scripts.DEFAULT_CONFIGSET_ID,
|
|
name = i18n("policy_presets.default"),
|
|
config = default_conf,
|
|
targets = {},
|
|
}
|
|
|
|
saveConfigsets(configsets)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.resetConfigsets()
|
|
cached_config_sets = nil
|
|
ntop.delCache(CONFIGSETS_KEY)
|
|
user_scripts.loadDefaultConfig()
|
|
|
|
return(true)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- Returns true if a system script is enabled for some hook
|
|
function user_scripts.isSystemScriptEnabled(script_key)
|
|
-- Verify that the script is currently available
|
|
local k = "ntonpng.cache.user_scripts.available_system_modules." .. script_key
|
|
local available = ntop.getCache(k)
|
|
|
|
if(isEmptyString(available)) then
|
|
local m = user_scripts.loadModule(getSystemInterfaceId(), user_scripts.script_types.system, "system", script_key)
|
|
available = (m ~= nil)
|
|
|
|
ntop.setCache(k, ternary(available, "1", "0"))
|
|
else
|
|
available = ternary(available == "1", true, false)
|
|
end
|
|
|
|
if(not available) then
|
|
return(false)
|
|
end
|
|
|
|
local configsets = user_scripts.getConfigsets()
|
|
local default_config = user_scripts.getDefaultConfig(configsets, "system")
|
|
local script_config = default_config[script_key]
|
|
|
|
if(script_config) then
|
|
for _, hook in pairs(script_config) do
|
|
if(hook.enabled) then
|
|
return(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
return(false)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local default_config = {
|
|
enabled = false,
|
|
script_conf = {},
|
|
}
|
|
|
|
-- @brief Retrieves the configuration of a specific script
|
|
function user_scripts.getScriptConfig(configset, script, subdir)
|
|
local script_key = script.key
|
|
local config = configset.config[subdir]
|
|
|
|
if(config[script_key]) then
|
|
-- A configuration was found
|
|
return(config[script_key])
|
|
end
|
|
|
|
-- Default
|
|
local rv = {}
|
|
local script_type = user_scripts.getScriptType(subdir)
|
|
local hooks = ternary(script_type.has_per_hook_config, script.hooks, {[ALL_HOOKS_CONFIG_KEY]=1})
|
|
|
|
for hook in pairs(script.hooks) do
|
|
rv[hook] = default_config
|
|
end
|
|
|
|
return(rv)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
-- @brief Retrieves the configuration of a specific hook of the target
|
|
-- @param target_config target configuration as returned by
|
|
-- user_scripts.getTargetConfig/user_scripts.getHostTargetConfigset
|
|
function user_scripts.getTargetHookConfig(target_config, script, hook)
|
|
local script_conf = target_config[script.key or script]
|
|
|
|
if not hook then
|
|
-- See has_per_hook_config
|
|
hook = ALL_HOOKS_CONFIG_KEY
|
|
end
|
|
|
|
if(not script_conf) then
|
|
return(default_config)
|
|
end
|
|
|
|
return(script_conf[hook] or default_config)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local fast_target_lookup = nil
|
|
|
|
-- NOTE: this only works for exact searches. For hosts see user_scripts.getHostTargetConfigset
|
|
function user_scripts.getTargetConfig(configsets, subdir, target)
|
|
if(fast_target_lookup == nil) then
|
|
fast_target_lookup = {}
|
|
|
|
for _, configset in pairs(configsets) do
|
|
for _, conf_target in pairs(configset.targets[subdir] or {}) do
|
|
fast_target_lookup[conf_target] = configset
|
|
end
|
|
end
|
|
end
|
|
|
|
local conf = fast_target_lookup[target] or configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if(conf == nil) then
|
|
return({})
|
|
end
|
|
|
|
return conf.config[subdir] or {}, conf.id
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getDefaultConfig(configsets, subdir)
|
|
local conf = configsets[user_scripts.DEFAULT_CONFIGSET_ID]
|
|
|
|
if(conf == nil) then
|
|
return({})
|
|
end
|
|
|
|
return conf.config[subdir] or {}, conf.id
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
local host_confsets_ptree_initialized = false
|
|
|
|
-- Performs an IP based match by using a patricia tree
|
|
function user_scripts.getHostTargetConfigset(configsets, subdir, ip_target)
|
|
if(not host_confsets_ptree_initialized) then
|
|
-- Start with an empty ptree
|
|
ntop.ptreeClear()
|
|
|
|
for _, configset in pairs(configsets) do
|
|
for _, conf_target in pairs(configset.targets[subdir] or {}) do
|
|
ntop.ptreeInsert(conf_target, configset.id)
|
|
end
|
|
end
|
|
|
|
host_confsets_ptree_initialized = true
|
|
end
|
|
|
|
local match_id = ntop.ptreeMatch(ip_target) or user_scripts.DEFAULT_CONFIGSET_ID
|
|
local conf = configsets[match_id]
|
|
|
|
if(conf == nil) then
|
|
return({})
|
|
end
|
|
|
|
return conf.config[subdir] or {}, conf.id
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
function user_scripts.getScriptEditorUrl(script)
|
|
if(script.edition == "community") then
|
|
local plugin_file_path = string.sub(script.source_path, string.len(dirs.scriptdir) + 1)
|
|
local plugin_path = string.sub(script.plugin.path, string.len(dirs.scriptdir) + 1)
|
|
return(string.format("%s/lua/code_viewer.lua?plugin_file_path=%s&plugin_path=%s", ntop.getHttpPrefix(), plugin_file_path, plugin_path))
|
|
end
|
|
|
|
return(nil)
|
|
end
|
|
|
|
-- ##############################################
|
|
|
|
return(user_scripts)
|