--
-- (C) 2014-22 - ntop.org
--

-- Hack to avoid include loops

if(pragma_once_lua_utils == true) then
   -- avoid multiple inclusions
   return
end

pragma_once_lua_utils = true


-- ###############################################

local clock_start = os.clock()

dirs = ntop.getDirs()

package.path = dirs.installdir .. "/scripts/lua/modules/i18n/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/timeseries/?.lua;" .. package.path
package.path = dirs.installdir .. "/scripts/lua/modules/pools/?.lua;" .. package.path

require "lua_trace"
require "lua_utils_generic"
require "ntop_utils"
require "locales_utils"
local os_utils = require "os_utils"
local format_utils = require "format_utils"

-- TODO: replace those globals with locals everywhere

secondsToTime   = format_utils.secondsToTime
msToTime        = format_utils.msToTime
bytesToSize     = format_utils.bytesToSize
formatPackets   = format_utils.formatPackets
formatFlows     = format_utils.formatFlows
formatValue     = format_utils.formatValue
pktsToSize      = format_utils.pktsToSize
bitsToSize      = format_utils.bitsToSize
round           = format_utils.round
bitsToSizeMultiplier = format_utils.bitsToSizeMultiplier

-- ##############################################

function isAllowedSystemInterface()
   return ntop.isAllowedInterface(tonumber(getSystemInterfaceId()))
end

-- ##############################################

local cached_allowed_networks_set = nil

function hasAllowedNetworksSet()
   if(cached_allowed_networks_set == nil) then
      local nets = ntop.getAllowedNetworks()
      local allowed_nets = string.split(nets, ",") or {nets}
      cached_allowed_networks_set = false

      for _, net in pairs(allowed_nets) do
         if((not isEmptyString(net)) and (net ~= "0.0.0.0/0") and (net ~= "::/0")) then
            cached_allowed_networks_set = true
            break
         end
      end
   end

   return(cached_allowed_networks_set)
end

-- ##############################################

function hasSoftwareUpdatesSupport()
   return (not ntop.isOffline()
      and isAdministrator()
      and ntop.isPackage()
      and not ntop.isWindows())
end

-- See Utils::l4proto2name()
l4_keys = {
   { "IP",        "ip",          0 },
   { "ICMP",      "icmp",        1 },
   { "IGMP",      "igmp",        2 },
   { "TCP",       "tcp",         6 },
   { "UDP",       "udp",        17 },
   { "IPv6",      "ipv6",       41 },
   { "RSVP",      "rsvp",       46 },
   { "GRE",       "gre",        47 },
   { "ESP",       "esp",        50 },
   { "IPv6-ICMP", "ipv6icmp",   58 },
   { "EIGRP",     "eigrp",      88 },
   { "OSPF",      "ospf",       89 },
   { "PIM",       "pim",       103 },
   { "VRRP",      "vrrp",      112 },
   { "HIP",       "hip",       139 },
   { "SCTP",      "sctp",      132 },
   { "ICMPv6",    "icmpv6",     58 },
   { "IGMP",      "igmp",        2 },
   { "Other IP",  "other_ip",   -1 }
}

function __FILE__() return debug.getinfo(2,'S').source end
function __LINE__() return debug.getinfo(2, 'l').currentline end

-- ##############################################

function findString(str, tofind)
  if(str == nil) then return(nil) end
  if(tofind == nil) then return(nil) end

  str1    = string.lower(string.gsub(str, "-", "_"))
  tofind1 = string.lower(string.gsub(tofind, "-", "_"))

  return(string.find(str1, tofind1, 1))
end

-- ##############################################

function findStringArray(str, tofind)
  if(str == nil) then return(nil) end
  if(tofind == nil) then return(nil) end
  local rsp = false

  for k,v in pairs(tofind) do
    str1    = string.gsub(str, "-", "_")
    tofind1 = string.gsub(v, "-", "_")
    if(str1 == tofind1) then
      rsp = true
    end

  end

  return(rsp)
end

-- ##############################################

--
-- Returns indexes to be used for string shortening. The portion of to_shorten between
-- middle_start and middle_end will be inside the bounds.
--
--    to_shorten: string to be shorten
--    middle_start: middle part begin index
--    middle_end: middle part begin index
--    maxlen: maximum length
--
function shortenInTheMiddle(to_shorten, middle_start, middle_end, maxlen)
  local maxlen = maxlen - (middle_end - middle_start)

  if maxlen <= 0 then
    return 0, string.len(to_shorten)
  end

  local left_slice = math.max(middle_start - math.floor(maxlen / 2), 1)
  maxlen = maxlen - (middle_start - left_slice - 1)
  local right_slice = math.min(middle_end + maxlen, string.len(to_shorten))

  return left_slice, right_slice
end

-- ##############################################

function shortHostName(name)
  local chunks = {name:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
  if(#chunks == 4) then
    return(name)
  else
    local max_len = ntop.getPref("ntopng.prefs.max_ui_strlen")
    max_len = tonumber(max_len)
    if(max_len == nil) then max_len = 24 end

    chunks = {name:match("%w+:%w+:%w+:%w+:%w+:%w+")}
    --io.write(#chunks.."\n")
    if(#chunks == 1) then
      return(name)
    end

    if(string.len(name) < max_len) then
      return(name)
    else
      tot = 0
      n = 0
      ret = ""

      for token in string.gmatch(name, "([%w-]+).") do
	if(tot < max_len) then
	  if(n > 0) then ret = ret .. "." end
	  ret = ret .. token
	  tot = tot+string.len(token)
	  n = n + 1
	end
      end

      return(ret .. "...")
    end
  end

  return(name)
end

-- ##############################################

function _handleArray(name, sev)
  local id

  for id, _ in ipairs(name) do
    local l = name[id][1]
    local key = name[id][2]

    if(string.upper(key) == string.upper(sev)) then
      return(l)
    end
  end

  return(firstToUpper(sev))
end

-- ##############################################

function l4Label(proto)
  return(_handleArray(l4_keys, proto))
end

-- ##############################################

function l4_proto_to_id(proto_name)
  for _, proto in pairs(l4_keys) do
    if proto[1] == proto_name or proto[2] == proto_name then
      return(proto[3])
    end
  end
end

-- ##############################################

function l4_proto_to_string(proto_id)
   if not proto_id then return "" end
   if isEmptyString(proto_id) then return "" end

   proto_id_num = tonumber(proto_id)

   if proto_id_num == nil then
      -- Already string?
      return proto_id
   end

   for _, proto in pairs(l4_keys) do
      if proto[3] == proto_id_num then
         return proto[1], proto[2]
      end
   end

   return string.format("%d", proto_id_num)
end

-- ##############################################

-- Return the list of L4 proto (key = name, value = id)
function l4_proto_list()
   local list = {}

   for _, proto in pairs(l4_keys) do
      -- add L4 proto only
      if proto[2] ~= 'ip' and
         proto[2] ~= 'ipv6' then
         list[proto[1]] =  proto[3]
      end
   end

   return list
end

-- ##############################################

function areAlertsEnabled()
   if(__alert_enabled == nil) then
      -- Not too nice as changes will be read periodically as new VMs are reloaded
      -- but at least we avoid breaking up the performance
      __alert_enabled = (ntop.getPref("ntopng.prefs.disable_alerts_generation") ~= "1")
   end

   return (__alert_enabled)
end

-- ##############################################

function isScoreEnabled()
  return(ntop.isEnterpriseM() or ntop.isnEdgeEnterprise())
end

-- ##############################################

function hasTrafficReport()
   local ts_utils = require("ts_utils_core")
   local is_pcap_dump = interface.isPcapDumpInterface()

   return((not is_pcap_dump) and (ts_utils.getDriverName() == "rrd") and ntop.isEnterpriseM())
end

function hasAlertsDisabled()
  _POST = _POST or {}
  return ((_POST["disable_alerts_generation"] ~= nil) and (_POST["disable_alerts_generation"] == "1")) or
      ((_POST["disable_alerts_generation"] == nil) and (ntop.getPref("ntopng.prefs.disable_alerts_generation") == "1"))
end

-- ##############################################

function hasClickHouseSupport()
   local auth = require "auth"

   if not (ntop.isPro() or ntop.isnEdgeEnterprise())
      or ntop.isWindows() then
      return false
   end

   -- Don't allow nIndex for unauthorized users
   if not auth.has_capability(auth.capabilities.historical_flows) then
      return false
   end

   -- TODO optimize
   if prefs == nil then
      prefs = ntop.getPrefs()
   end

   if prefs.is_dump_flows_to_clickhouse_enabled then
      return true
   end

   return false
end

-- ##############################################

-- NOTE: global nindex support may be enabled but some disable on some interfaces
function interfaceHasClickHouseSupport()
   return(hasClickHouseSupport() and ntop.getPrefs()["is_dump_flows_to_clickhouse_enabled"])
end

-- ##############################################

function truncate(x)
   if(x == nil) then
      tprint(debug.traceback())
   end
   return x<0 and math.ceil(x) or math.floor(x)
end

-- ##############################################

-- Note that the function below returns a string as returning a number
-- would not help as a new float would be returned
function toint(num)
   return string.format("%u", truncate(num))
end

-- ##############################################

function capitalize(str)
  return (str:gsub("^%l", string.upper))
end

-- ##############################################

local function starstring(len)
local s = ""

  while(len > 0) do
   s = s .."*"
   len = len -1
  end

  return(s)
end

-- ##############################################

function obfuscate(str)
  local len = string.len(str)
  local in_clear = 2

  if(len <= in_clear) then
    return(starstring(len))
  else
    return(string.sub(str, 0, in_clear)..starstring(len-in_clear))
  end
end

-- ##############################################

function isnumber(str)
   if((str ~= nil) and (string.len(str) > 0) and (tonumber(str) ~= nil)) then
      return(true)
   else
      return(false)
   end
end

-- ##############################################

-- split
function split(s, delimiter)
   result = {};
   if(s ~= nil) then
      if delimiter == nil then
         -- No delimiter, split all characters
         for match in s:gmatch"." do
   	    table.insert(result, match);
         end
      else
         -- Split by delimiter
         for match in (s..delimiter):gmatch("(.-)"..delimiter) do
   	    table.insert(result, match);
         end
      end
   end
   return result;
end

-- ##############################################

function formatEpoch(epoch, full_time)
   return(format_utils.formatEpoch(epoch, full_time))
end

-- ##############################################

function starts(String,Start)
   if((String == nil) or (Start == nil)) then
      return(false)
   end

  return string.sub(String,1,string.len(Start))==Start
end

-- ##############################################

function ends(String,End)
   return End=='' or string.sub(String,-string.len(End))==End
end

-- #################################################################

function bit(p)
  return 2 ^ (p - 1)  -- 1-based indexing
end

-- ##############################################

-- Typical call:  if hasbit(x, bit(3)) then ...
function hasbit(x, p)
  return x % (p + p) >= p
end

-- ##############################################

function setbit(x, p)
  return hasbit(x, p) and x or x + p
end

-- ##############################################

function clearbit(x, p)
  return hasbit(x, p) and x - p or x
end

-- ##############################################

function isBroadMulticast(ip)
   if(ip == "0.0.0.0") then
      return true
   end
   -- print(ip)
   t = string.split(ip, "%.")
   -- print(table.concat(t, "\n"))
   if(t == nil) then
      return false  -- Might be an IPv6 address
   else
      if(tonumber(t[1]) >= 224)  then
	 return true
      end
   end

   return false
end

-- ##############################################

function isBroadcastMulticast(ip)
   local ainfo = interface.getAddressInfo(ip)

   if(ainfo.is_multicast or ainfo.is_broadcast) then
      return true
   else
      return false
   end
end

-- ##############################################

--
-- Fix member format (IP address to /32 CIDR and VLAN to default 0)
-- E.g. 192.168.1.10 -> 192.168.1.10/32@0
--

function fixPoolMemberFormat(member)
  if isEmptyString(member) then
    return nil
  end

  if isMacAddress(member) then
    return member
  end

  -- VLAN
  local vlan_id
  local vlan_idx = string.find(member, "@")
  if vlan_idx == nil then
     vlan_id = 0
  elseif vlan_idx == 1 then
     return nil
  else
     local other = string.sub(member, 1, vlan_idx-1)
     vlan_id = tonumber(string.sub(member, vlan_idx+1))
     if vlan_id == nil or vlan_id < 0 then
        return nil
     end
     member = other
  end

  -- prefix is mandatory here
  local address, prefix = splitNetworkPrefix(member)
  if address == nil then
    return nil
  elseif prefix == nil then
    prefix = '32'
  end

  if isIPv4(address) and (tonumber(prefix) >= 0) and (tonumber(prefix) <= 32) then
    -- ok
  elseif isIPv6(address) and (tonumber(prefix) >= 0) and (tonumber(prefix) <= 128) then
    -- ok
  else
    return nil
  end

  return address .. '/' .. prefix .. '@' .. vlan_id
end


-- ##############################################

--
-- Members supported format
-- 192.168.1.10/32@10
-- 00:11:22:33:44:55
--

function isValidPoolMember(member)
  if isEmptyString(member) then
    return false
  end

  if isMacAddress(member) then
    return true
  end

  -- vlan is mandatory here
  local vlan_idx = string.find(member, "@")
  if ((vlan_idx == nil) or (vlan_idx == 1)) then
     return false
  end

  local other = string.sub(member, 1, vlan_idx-1)
  local vlan = tonumber(string.sub(member, vlan_idx+1))
  if (vlan == nil) or (vlan < 0) then
    return false
  end

  -- prefix is mandatory here
  local address, prefix = splitNetworkPrefix(other)
  if prefix == nil then
    return false
  end

  if isIPv4(address) and (tonumber(prefix) >= 0) and (tonumber(prefix) <= 32) then
    return true
  elseif isIPv6(address) and (tonumber(prefix) >= 0) and (tonumber(prefix) <= 128) then
    return true
  end

  return false
end

-- ##############################################

function host2member(ip, vlan, prefix)
  if prefix == nil then
    if isIPv4(ip) then
      prefix = 32
    else
      prefix = 128
    end
  end

  return ip .. "/" .. tostring(prefix) .. "@" .. tostring(vlan)
end

-- ##############################################

function isLocal(host_ip)
  host = interface.getHostInfo(host_ip)

  if((host == nil) or (host['localhost'] ~= true)) then
    return(false)
  else
    return(true)
  end
end

-- ##############################################

-- Windows fixes for interfaces with "uncommon chars"
function purifyInterfaceName(interface_name)
  -- io.write(debug.traceback().."\n")
  interface_name = string.gsub(interface_name, "@", "_")
  interface_name = string.gsub(interface_name, ":", "_")
  interface_name = string.gsub(interface_name, "/", "_")
  return(interface_name)
end

-- ##############################################

-- See datatype AggregationType in ntop_typedefs.h
function aggregation2String(value)
  if(value == 0) then return("Client Name")
  elseif(value == 1) then return("Server Name")
  elseif(value == 2) then return("Domain Name")
  elseif(value == 3) then return("Operating System")
  elseif(value == 4) then return("Registrar Name")
  else return(value)
  end
end

-- #################################

-- Aggregates items below some edge
-- edge: minimum percentage value to create collision
-- min_col: minimum collision groups to aggregate
function aggregatePie(values, values_sum, edge, min_col)
   local edge = edge or 0.09
   min_col = min_col or 2
   local aggr = {}
   local other = i18n("other")
   local below_edge = {}

   -- Initial lookup
   for k,v in pairs(values) do
      if v / values_sum <= edge then
         -- too small
         below_edge[#below_edge + 1] = k
      else
         aggr[k] = v
      end
   end

   -- Decide if to aggregate
   for _,k in pairs(below_edge) do
      if #below_edge >= min_col then
         -- aggregate
         aggr[other] = aggr[other] or 0
         aggr[other] = aggr[other] + values[k]
      else
         -- do not aggregate
         aggr[k] = values[k]
      end
   end

   return aggr
end

-- #################################

-- This function actively resolves an host if there is not information about it.
-- NOTE: prefer the host2name on this function
function resolveAddress(hostinfo, allow_empty, shorten_len)
   local alt_name = ip2label(hostinfo["host"], hostinfo["vlan"], shorten_len)

   if(not isEmptyString(alt_name) and (alt_name ~= hostinfo["host"])) then
      -- The host label has priority
      return(alt_name)
   end

   local hostname = ntop.resolveName(hostinfo["host"])
   if isEmptyString(hostname) then
      -- Not resolved
      if allow_empty == true then
         return hostname
      else
         -- this function will take care of formatting the IP
         return hostinfo2label(hostinfo, true, shorten_len)
      end
   end
   return hostinfo2label(hostinfo, true, shorten_len)
end

-- ###########################################

function computeL7Stats(stats, show_breed, show_ndpi_category)
   local _ifstats = {}

   if(show_breed) then
      local breed_stats = {}

      for key, value in pairs(stats["ndpi"]) do
         local b = stats["ndpi"][key]["breed"]

         local traffic = stats["ndpi"][key]["bytes.sent"] + stats["ndpi"][key]["bytes.rcvd"]

         if(breed_stats[b] == nil) then
            breed_stats[b] = traffic
         else
            breed_stats[b] = breed_stats[b] + traffic
         end
      end

      for key, value in pairs(breed_stats) do
         _ifstats[key] = value
      end

   elseif(show_ndpi_category) then
      local ndpi_category_stats = {}

      for key, value in pairs(stats["ndpi_categories"]) do
         key = getCategoryLabel(key, value.category)
         local traffic = value["bytes"]

         if(ndpi_category_stats[key] == nil) then
            ndpi_category_stats[key] = traffic
         else
            ndpi_category_stats[key] = ndpi_category_stats[key] + traffic
         end
      end

      for key, value in pairs(ndpi_category_stats) do
         _ifstats[key] = value
      end

   else
      -- Add ARP to stats
      local arpBytes = 0
      if(stats["eth"] ~= nil) then
         arpBytes = stats["eth"]["ARP_bytes"]
         if(arpBytes > 0) then
            _ifstats["ARP"] = arpBytes
         end
      end

      for key, value in pairs(stats["ndpi"]) do
         local traffic = value["bytes.sent"] + value["bytes.rcvd"]
         if(key == "Unknown") then
            traffic = traffic - arpBytes
         end

         if(traffic > 0) then
            if(show_breed) then
               _ifstats[value["breed"]] = traffic
            else
               _ifstats[key] = traffic
            end
         end
      end
   end

   return _ifstats
end

-- ##############################################

function splitNetworkPrefix(net)
   if not net then
      tprint(debug.traceback())
   end
   local prefix = tonumber(net:match("/(.+)"))
   local address = net:gsub("/.+","")
   return address, prefix
end

-- ##############################################

function splitNetworkWithVLANPrefix(net_mask_vlan)
   local vlan = tonumber(net_mask_vlan:match("@(.+)"))
   local net_mask = net_mask_vlan:gsub("@.+","")
   local prefix = tonumber(net_mask:match("/(.+)"))
   local address = net_mask:gsub("/.+","")
   return address, prefix, vlan
end

-- ##############################################

function splitProtocol(proto_string)
  local parts = string.split(proto_string, "%.")
  local app_proto
  local master_proto

  if parts == nil then
    master_proto = proto_string
    app_proto = nil
  else
    master_proto = parts[1]
    app_proto = parts[2]
  end

  return master_proto, app_proto
end

-- ##############################################

function setHostAltName(host_info, alt_name)
   local host_key

   if type(host_info) == "table" then
     -- Note: we are not using hostinfo2hostkey which includes the
     -- vlan for backward compatibility, compatibility with
     -- the backend, and compatibility with the vpn scripts
     host_key = host_info["host"] -- hostinfo2hostkey(host_info)
   else
     host_key = host_info
   end

   ntop.setCache(getHostAltNamesKey(host_key), alt_name)
end

-- ##############################################

function setHostNotes(host_info, notes)
   local host_key

   if type(host_info) == "table" then
     -- Note: we are not using hostinfo2hostkey which includes the
     -- vlan for backward compatibility, compatibility with
     -- the backend, and compatibility with the vpn scripts
     host_key = host_info["host"] -- hostinfo2hostkey(host_info)
   else
     host_key = host_info
   end

   ntop.setCache(getHostNotesKey(host_key), notes)
end

-- ##############################################

local function label2formattedlabel(alt_name, host_info, show_vlan, shorten_len)
   if not isEmptyString(alt_name) then
      local ip = host_info["ip"] or host_info["host"]
      -- Make it shorter
      local res = alt_name

      -- Special shorting function for IP addresses
      if res ~= ip then
         if (not shorten_len) or (shorten_len == false) then
            -- Don't touch the string, requested as-is without shortening
         elseif tonumber(shorten_len) then
            -- Shorten according to the specified length
            res = shortenString(res, shorten_len)
         else
            -- Use the default system-wide setting for the shortening
            res = shortenString(res)
         end
      end

      -- Adding the vlan if requested
      if show_vlan then
	 local vlan = tonumber(host_info["vlan"])

	 if vlan and vlan > 0 then
	    local full_vlan_name = getFullVlanName(vlan, true --[[ Compact --]])

	    res = string.format("%s@%s", res, full_vlan_name)
	 end
      end

      return res
   end

   -- Fallback: just the IP and VLAN
   return(hostinfo2hostkey(host_info, true))
end

-- ##############################################

-- Attempt at retrieving an host label from an host_info, using local caches and DNS resolution.
-- This can be more expensive if compared to only using information found inside host_info.
local function hostinfo2label_resolved(host_info, show_vlan, shorten_len, skip_resolution)
   local ip = host_info["ip"] or host_info["host"]
   local res

   -- If local broadcast domain host and DHCP, try to get the label associated
   -- to the MAC address
   if host_info["mac"] and (host_info["broadcast_domain_host"] or host_info["dhcpHost"]) then
      res = getHostAltName(host_info["mac"])
   end

   -- In case no resolution is requested, directly skip this part
   if (isEmptyString(res)) and (not skip_resolution) then
      -- Try and get the resolved name
      res = ntop.getResolvedName(ip)

      if not isEmptyString(res) then
         res = string.lower(res)
      else
         -- Nothing found, just fallback to the IP address
         res = ip
      end
   end

   return label2formattedlabel(res, host_info, show_vlan, shorten_len)
end

-- ##############################################

-- Retrieve an host label from an host_info. The minimum fields of
-- the host_info are "host" and "vlan", however a full JSON from Host::lua
-- is needed to provide an accurate result.
--
-- The following order is used to determine the label:
--    MAC label (LBD hosts only), IP label, MDNS/DHCP name from C, resolved IP
--
-- NOTE: The function attempt at labelling an host only using information found in host_info.
-- In case host_info is not enough to label the host, then local caches and DNS resolution kick in
-- to find a label (at the expense of extra time).
function hostinfo2label(host_info, show_vlan, shorten_len, skip_resolution)
   local alt_name = nil
   local ip = host_info["ip"] or host_info["host"]

   -- Take the label as found in the host structure
   local res = host_info.label

   if isEmptyString(res) then
      -- Use any user-configured custom name
      -- This goes first as a label set by the user MUST take precedance over any other possibly available label
      res = getHostAltName(ip)

      if isEmptyString(res) then
	      -- Read what is found inside host `name`, e.g., name as found by dissected traffic such as DHCP
	      res = host_info["name"]

        if isEmptyString(res) then
            return hostinfo2label_resolved(host_info, show_vlan, shorten_len, skip_resolution)
        end
      end
   end

   return label2formattedlabel(res, host_info, show_vlan, shorten_len)
end

-- ##############################################

-- Just a convenience function for hostinfo2label with only IP and VLAN
function ip2label(ip, vlan, shorten_len)
   return hostinfo2label({host = ip, vlan = (vlan or 0)}, true, shorten_len)
end

-- ##############################################

function mac2label(mac)
   local alt_name = getHostAltName(mac)

   if not isEmptyString(alt_name) and (alt_name ~= mac) then
      return(alt_name)
   end

   alt_name = ntop.getCache(getDhcpNameKey(interface.getId(), mac))

   if not isEmptyString(alt_name) and (alt_name ~= mac) then
      return(alt_name)
   end

   -- Fallback: just the MAC
   return(mac)
end

-- ##############################################

-- Mac Addresses --

local specialMACs = {
  "01:00:0C",
  "01:80:C2",
  "01:00:5E",
  "01:0C:CD",
  "01:1B:19",
  "FF:FF",
  "33:33"
}
function isSpecialMac(mac)
  for _,key in pairs(specialMACs) do
     if(string.contains(mac, key)) then
        return true
     end
  end

  return false
end

-- ##############################################

-- Flow Utils --

function flowinfo2hostname(flow_info, host_type, alerts_view, add_hostname)
   local name
   local orig_name

   if (alerts_view and not hasClickHouseSupport()) or (add_hostname ~= nil and add_hostname == false) then
      -- do not return resolved name as it will hide the IP address
      return(flow_info[host_type..".ip"])
   end

   if(flow_info == nil) then return("") end

   if(host_type == "srv") then
      if flow_info["host_server_name"] ~= nil and flow_info["host_server_name"] ~= "" and flow_info["host_server_name"]:match("%w") and not isIPv4(flow_info["host_server_name"]) and not isIPv6(flow_info["host_server_name"]) then
	 -- remove possible ports from the name
	 return(flow_info["host_server_name"]:gsub(":%d+$", ""))
      end
      if(flow_info["protos.tls.certificate"] ~= nil and flow_info["protos.tls.certificate"] ~= "") then
	 return(flow_info["protos.tls.certificate"])
      end
   end

   local hostinfo = {
      host = flow_info[host_type..".ip"],
      label = flow_info[host_type..".host"],
      mac = flow_info[host_type..".mac"],
      dhcpHost = flow_info[host_type..".dhcpHost"],
      broadcast_domain_host = flow_info[host_type..".broadcast_domain_host"],
      vlan = flow_info["vlan"],
   }

   return(hostinfo2label(hostinfo))
end

-- ##############################################

function setLocalNetworkAlias(network, alias)
   if((network ~= alias) or isEmptyString(alias)) then
      ntop.setHashCache(getLocalNetworkAliasKey(), network, alias)
   else
      ntop.delHashCache(getLocalNetworkAliasKey(), network)
   end
end

-- ##############################################

function setVlanAlias(vlan_id, alias)
   if((vlan_id ~= alias) or isEmptyString(alias)) then
      ntop.setHashCache(getVlanAliasKey(), vlan_id, alias)
   else
      ntop.delHashCache(getVlanAliasKey(), vlan_id)
   end
end

-- ##############################################

function flow2hostinfo(host_info, host_type)
  local host_name
  local res = interface.getHostMinInfo(host_info[host_type .. ".ip"])

  if((res == nil) or (res["name"] == nil)) then
      host_name = host_info[host_type .. ".ip"]
  else
      host_name = res["name"]
  end

  return({host = host_info[host_type .. ".ip"], vlan = host_info[host_type .. ".vlan"], name = host_name})
end

-- ##############################################

-- URL Util --

--
-- Split the host key (ip@vlan) creating a new lua table.
-- Example:
--    info = hostkey2hostinfo(key)
--    ip = info["host"]
--    vlan = info["vlan"]
--
function hostkey2hostinfo(key)
  local host = {}
  local info = split(key,"@")

  if(info[1] ~= nil) then
    host["host"] = info[1]
  end

  if(info[2] ~= nil) then
    host["vlan"] = tonumber(info[2])
  else
    host["vlan"] = 0
  end

  return host
end

-- ##############################################

function isHostKey(key)
  local info = split(key,"@")
  -- Check format
  if not info or #info < 1 or #info > 2 then
    return false
  end
  -- Check IP format
  if isEmptyString(info[1]) or (not isIPv4(info[1]) and not isIPv6(info[1])) then
    return false
  end
  -- Check VLAN format (if any)
  if not isEmptyString(info[2]) and tonumber(info[2]) == nil then
    return false
  end
  -- Ok
  return true
end

-- ##############################################

--
-- Analyze the host_info table and return the host key.
-- Example:
--    host_info = interface.getHostInfo("127.0.0.1",0)
--    key = hostinfo2hostkey(host_info)
--
function hostinfo2hostkey(host_info, host_type, show_vlan)
   local rsp = ""

   if(host_type == "cli") then
      local cli_ip = host_info["cli.ip"] or host_info["cli_ip"]

      if cli_ip then
	 rsp = rsp..cli_ip
      end

   elseif(host_type == "srv") then
      local srv_ip = host_info["srv.ip"] or host_info["srv_ip"]

      if srv_ip then
	 rsp = rsp..srv_ip
      end
   else

      if(host_info["ip"] ~= nil) then
	 rsp = rsp..host_info["ip"]
      elseif(host_info["mac"] ~= nil) then
	 rsp = rsp..host_info["mac"]
      elseif(host_info["host"] ~= nil) then
	 rsp = rsp..host_info["host"]
      elseif(host_info["name"] ~= nil) then
	 rsp = rsp..host_info["name"]
      end
   end

   local vlan_id = tonumber(host_info["vlan"] or host_info["vlan_id"] or 0)

   if vlan_id ~= 0 or show_vlan then
      rsp = rsp..'@'..tostring(vlan_id)
   end

   if(debug_host) then traceError(TRACE_DEBUG,TRACE_CONSOLE,"HOST2URL => ".. rsp .. "\n") end
   return rsp
end

-- ##############################################

function member2visual(member)
   local info = hostkey2hostinfo(member)
   local host = info.host
   local hlen = string.len(host)

   if string.ends(host, "/32") and isIPv4(string.sub(host, 1, hlen-3)) then
    host = string.sub(host, 1, hlen-3)
  elseif string.ends(host, "/128") and isIPv6(string.sub(host, 1, hlen-4)) then
    host = string.sub(host, 1, hlen-4)
  end

  return hostinfo2hostkey({host=host, vlan=info.vlan})
end

-- ##############################################

--
-- Catch the main information about an host from the host_info table and return the corresponding json.
-- Example:
--          hostinfo2json(host[key]), return a json string based on the host value
--          hostinfo2json(flow[key],"cli"), return a json string based on the client host information in the flow table
--          hostinfo2json(flow[key],"srv"), return a json string based on the server host information in the flow table
--
function hostinfo2json(host_info,host_type)
  local rsp = ''

  if(host_type == "cli") then
    if(host_info["cli.ip"] ~= nil) then
      rsp = rsp..'host: "'..host_info["cli.ip"]..'"'
    end
  elseif(host_type == "srv") then
    if(host_info["srv.ip"] ~= nil) then
      rsp = rsp..'host: "'..host_info["srv.ip"]..'"'
    end
  else
    if((type(host_info) ~= "table") and (string.find(host_info,"@"))) then
      host_info = hostkey2hostinfo(host_info)
    end

    if(host_info["host"] ~= nil) then
      rsp = rsp..'host: "'..host_info["host"]..'"'
    elseif(host_info["ip"] ~= nil) then
      rsp = rsp..'host: "'..host_info["ip"]..'"'
    elseif(host_info["name"] ~= nil) then
      rsp = rsp..'host: "'..host_info["name"] ..'"'
    elseif(host_info["mac"] ~= nil) then
      rsp = rsp..'host: "'..host_info["mac"] ..'"'
    end
  end

  if((host_info["vlan"] ~= nil) and (host_info["vlan"] ~= 0)) then
    rsp = rsp..', vlan: "'..tostring(host_info["vlan"]) .. '"'
  end

  if(debug_host) then traceError(TRACE_DEBUG,TRACE_CONSOLE,"HOST2JSON => ".. rsp .. "\n") end

  return rsp
end

-- ##############################################

--
-- Catch the main information about an host from the host_info table and return the corresponding jqueryid.
-- Example: host 192.168.1.254, vlan0  ==> 1921681254_0
function hostinfo2jqueryid(host_info,host_type)
  local rsp = ''

  if(host_type == "cli") then
    if(host_info["cli.ip"] ~= nil) then
      rsp = rsp..''..host_info["cli.ip"]
    end

  elseif(host_type == "srv") then
    if(host_info["srv.ip"] ~= nil) then
      rsp = rsp..''..host_info["srv.ip"]
    end
  else
    if((type(host_info) ~= "table") and (string.find(host_info,"@"))) then
      host_info = hostkey2hostinfo(host_info)
    end

    if(host_info["host"] ~= nil) then
      rsp = rsp..''..host_info["host"]
    elseif(host_info["ip"] ~= nil) then
      rsp = rsp..''..host_info["ip"]
    elseif(host_info["name"] ~= nil) then
      rsp = rsp..''..host_info["name"]
    elseif(host_info["mac"] ~= nil) then
      rsp = rsp..''..host_info["mac"]
    end
  end


  if((host_info["vlan"] ~= nil) and (host_info["vlan"] ~= 0)) then
    rsp = rsp..'@'..tostring(host_info["vlan"])
  end

  rsp = string.gsub(rsp, "%.", "__")
  rsp = string.gsub(rsp, "/", "___")
  rsp = string.gsub(rsp, ":", "____")

  if(debug_host) then traceError(TRACE_DEBUG,TRACE_CONSOLE,"HOST2KEY => ".. rsp .. "\n") end

  return rsp
end

-- ##############################################

function isPausedInterface(current_ifname)
   if(not isEmptyString(_POST["toggle_local"])) then
      return(_POST["toggle_local"] == "0")
   end

  state = ntop.getCache("ntopng.prefs.ifid_"..tostring(interface.name2id(current_ifname)).."_not_idle")
  if(state == "0") then return true else return false end
end

-- ##############################################

function tablePreferences(key, value, force_set)
  if not _SESSION then
    -- Not in a user session, ignore preferences
    return
  end

  table_key = getRedisPrefix("ntopng.sort.table")

  if((value == nil) or (value == "")) and (force_set ~= true) then
    -- Get preferences
    return ntop.getHashCache(table_key, key)
  else
    -- Set preferences
    ntop.setHashCache(table_key, key, value)
    return(value)
  end
end

-- ##############################################

function setInterfaceRegreshRate(ifid, refreshrate)
   local key = "ntopng.prefs.ifid_"..tostring(ifid)..".refresh_rate"

   if isEmptyString(refreshrate) then
      ntop.delCache(key)
   else
      ntop.setCache(key, tostring(refreshrate))
   end
end

-- ##############################################

-- "Some Very Long String" -> "Some Ver...g String"
function shortenCollapse(s, max_len)
   local replacement = "..."
   local r_len = string.len(replacement)
   local s_len = string.len(s)

   if max_len == nil then
      max_len = ntop.getPref("ntopng.prefs.max_ui_strlen")
      max_len = tonumber(max_len)
      if(max_len == nil) then max_len = 24 end
   end

   if max_len <= r_len then
      return replacement
   end

   if s_len > max_len then
      local half = math.floor((max_len-r_len) / 2)
      return string.sub(s, 1, half) .. replacement .. string.sub(s, s_len-half+1)
   end

   return s
end

-- ##############################################

function harvestUnusedDir(path, min_epoch)
   local files = ntop.readdir(path)

   -- print("Reading "..path.."<br>\n")

   for k,v in pairs(files) do
      if(v ~= nil) then
	 local p = os_utils.fixPath(path .. "/" .. v)
	 if(ntop.isdir(p)) then
	    harvestUnusedDir(p, min_epoch)
	 else
	    local when = ntop.fileLastChange(path)

	    if((when ~= -1) and (when < min_epoch)) then
	       os.remove(p)
	    end
	 end
      end
   end
end

 -- ##############################################

function harvestJSONTopTalkers(days)
   local when = os.time() - 86400 * days

   ifnames = interface.getIfNames()
   for _,ifname in pairs(ifnames) do
      interface.select(ifname)
      local _ifstats = interface.getStats()
      local dirs = ntop.getDirs()
      local basedir = os_utils.fixPath(dirs.workingdir .. "/" .. _ifstats.id)

      harvestUnusedDir(os_utils.fixPath(basedir .. "/top_talkers"), when)
      harvestUnusedDir(os_utils.fixPath(basedir .. "/flows"), when)
   end
end

-- ###############################################

-- prints purged information for hosts / flows
function purgedErrorString()
    local info = ntop.getInfo(false)
    return i18n("purged_error_message",{url=ntop.getHttpPrefix()..'/lua/admin/prefs.lua?tab=in_memory', product=info["product"]})
end

-- print TCP flags
function formatTCPFlags(flags)
   local out = ''

   if(hasbit(flags,0x02)) then out = out .. '<span class="badge bg-info"    title="SYN">S</span> ' end
   if(hasbit(flags,0x10)) then out = out .. '<span class="badge bg-info"    title="ACK">A</span> ' end
   if(hasbit(flags,0x01)) then out = out .. '<span class="badge bg-info"    title="FIN">F</span> ' end
   if(hasbit(flags,0x08)) then out = out .. '<span class="badge bg-info"    title="PSH">P</span> ' end
   if(hasbit(flags,0x04)) then out = out .. '<span class="badge bg-danger"  title="RST">R</span> ' end
   if(hasbit(flags,0x20)) then out = out .. '<span class="badge bg-primary" title="URG">U</span> ' end
   if(hasbit(flags,0x40)) then out = out .. '<span class="badge bg-info"    title="ECE">E</span> ' end
   if(hasbit(flags,0x80)) then out = out .. '<span class="badge bg-info"    title="CWR">C</span> ' end

   return out
end

-- convert the integer carrying TCP flags in a more convenient lua table
function TCPFlags2table(flags)
   local res = {
      ["FIN"] = 0, ["SYN"] = 0, ["RST"] = 0,
      ["PSH"] = 0, ["ACK"] = 0, ["URG"] = 0,
      ["ECE"] = 0, ["CWR"] = 0,
   }

   if(hasbit(flags,0x01)) then res["FIN"] = 1 end
   if(hasbit(flags,0x02)) then res["SYN"] = 1 end
   if(hasbit(flags,0x04)) then res["RST"] = 1 end
   if(hasbit(flags,0x08)) then res["PSH"] = 1 end
   if(hasbit(flags,0x10)) then res["ACK"] = 1 end
   if(hasbit(flags,0x20)) then res["URG"] = 1 end
   if(hasbit(flags,0x40)) then res["ECE"] = 1 end
   if(hasbit(flags,0x80)) then res["CWR"] = 1 end
   return res
end

-- ##########################################

function historicalProtoHostHref(ifId, host, l4_proto, ndpi_proto_id, info, vlan, no_print)
  if ntop.isEnterpriseM() then
    local now    = os.time()
    local ago1h  = now - 3600

    local prefs = ntop.getPrefs()
    if prefs.is_dump_flows_to_clickhouse_enabled then
      local hist_url = ntop.getHttpPrefix().."/lua/pro/db_search.lua?"
      local params = {epoch_end = now, epoch_begin = ago1h, ifid = ifId}

      if host then
        local host_k = hostinfo2hostkey(host)
        if isEmptyString(host_k) then
          host_k = host
        end
        params["ip"] = host_k..";eq"
      end
      if l4_proto then
        params["l4proto"] = l4_proto..";eq"
      end
      if ndpi_proto_id then
        params["l7proto"] = ndpi_proto_id..";eq"
      end
      if vlan and vlan ~= 0 then
        params["vlan_id"] = vlan..";eq"
      end
      if info then
        params["info"] = info..";in"
      end

      local url_params = table.tconcat(params, "=", "&")

      if not no_print then
        print('&nbsp;')
        -- print('<span class="badge bg-info">')
        print('<a href="'..hist_url..url_params..'" title="'..i18n("db_explorer.last_hour_flows")..'"><i class="fas fa-search-plus"></i></a>')
        -- print('</span>')
      else
        return '<a href="'..hist_url..url_params..'" title="'..i18n("db_explorer.last_hour_flows")..'"><i class="fas fa-search-plus"></i></a>'
      end
    end
  end
end

-- ####################################################

function tableToJsObject(lua_table)
   local json = require("dkjson")
   return json.encode(lua_table, nil)
end

-- ####################################################

-- @brief The difference, in seconds, between the local time of this instance and GMT
local server_timezone_diff_seconds

-- ####################################################

-- @brief Converts a datetime string into an epoch, adjusted with the client time
function makeTimeStamp(d)
   local pattern = "(%d+)%/(%d+)%/(%d+) (%d+):(%d+):(%d+)"
   local day, month, year, hour, minute, seconds = string.match(d, pattern);

   -- Get the epoch out of d. The epoch gets adjusted by os.time in the server timezone, that is, in
   -- the timezone of this running ntopng instance
   -- See https://www.lua.org/pil/22.1.html
   local server_epoch = os.time({year = year, month = month, day = day, hour = hour, min = minute, sec = seconds});

   -- Convert the server_epoch into a gmt_epoch which is adjusted to GMT
   local gmt_epoch = server_epoch + get_server_timezone_diff_seconds()

   -- Finally, compute a client_epoch by adding the seconds of getFrontendTzSeconds() to the GMT epoch just computed
   local client_epoch = gmt_epoch + getFrontendTzSeconds()

   -- Now we can compute the deltas to know the extact number of seconds between the server and the client timezone
   local server_to_gmt_delta = gmt_epoch - server_epoch
   local gmt_to_client_delta = client_epoch - gmt_epoch
   local server_to_client_delta = client_epoch - server_epoch

   -- Make sure everything is OK...
   assert(server_to_client_delta == server_to_gmt_delta + gmt_to_client_delta)

   -- tprint({
   --    server_ts = server_epoch,
   --    gmt_ts = gmt_epoch,
   --    server_to_gmt_delta = (server_to_gmt_delta) / 60 / 60,
   --    gmt_to_client_delta = (gmt_to_client_delta) / 60 / 60,
   --    server_to_client_delta = (server_to_client_delta) / 60 / 60
   -- })

   -- Return the epoch in the client timezone
   return string.format("%u", server_epoch - server_to_client_delta)
end

-- ###########################################

-- Note: the base unit is Kbps here
FMT_TO_DATA_RATES_KBPS = {
   ["k"] = {label="Kbps", value=1},
   ["m"] = {label="Mbps", value=1000},
   ["g"] = {label="Gbps", value=1000*1000},
}

FMT_TO_DATA_BYTES = {
  ["b"] = {label="B",  value=1},
  ["k"] = {label="KB", value=1024},
  ["m"] = {label="MB", value=1024*1024},
  ["g"] = {label="GB", value=1024*1024*1024},
}

FMT_TO_DATA_TIME = {
  ["s"] = {label=i18n("metrics.secs"),  value=1},
  ["m"] = {label=i18n("metrics.mins"),  value=60},
  ["h"] = {label=i18n("metrics.hours"), value=3600},
  ["d"] = {label=i18n("metrics.days"),  value=3600*24},
}

-- ###########################################

--
-- Extracts parameters from a lua table.
-- This function performs the inverse conversion of javascript paramsPairsEncode.
--
-- Note: plain parameters (not encoded with paramsPairsEncode) remain unchanged only
-- when strict mode is *not* enabled
--
function paramsPairsDecode(params, strict_mode)
   local res = {}

   for k,v in pairs(params) do
      local sp = split(k, "key_")
      if #sp == 2 then
         local keyid = sp[2]
         local value = "val_"..keyid
         if params[value] then
            res[v] = params[value]
         end
      end

      if((not strict_mode) and (res[v] == nil)) then
         -- this is a plain parameter
         res[k] = v
      end
   end

   return res
end

function isBridgeInterface(ifstats)
  return ifstats.inline
end

function hasSnmpDevices(ifid)
  if (not ntop.isEnterpriseM()) or (not isAdministrator()) then
    return false
  end

  return has_snmp_devices(ifid)
end

function stripVlan(name)
  local key = string.split(name, "@")
  if((key ~= nil) and (#key == 2)) then
     -- Verify that the host is actually an IP address and the VLAN actually
     -- a number to avoid stripping things that are not vlans (e.g. part of an host name)
     local addr = key[1]

     if((tonumber(key[2]) ~= nil) and (isIPv6(addr) or isIPv4(addr))) then
      return(addr)
     end
  end

  return(name)
end

-- ###########################################

function swapKeysValues(tbl)
   local new_tbl = {}

   for k, v in pairs(tbl or {}) do
      new_tbl[v] = k
   end

   return new_tbl
end

-- ###########################################

function tsQueryToTags(query)
   local tags = {}

   for _, part in pairs(split(query, ",")) do
      local sep_pos = string.find(part, ":")

      if sep_pos then
         local k = string.sub(part, 1, sep_pos-1)
         local v = string.sub(part, sep_pos+1)
         tags[k] = v
      end
   end

   return tags
end

function tsTagsToQuery(tags)
   return table.tconcat(tags, ":", ",")
end

-- ###########################################

-- Compares IPv4 / IPv6 addresses
function ip_address_asc(a, b)
   return(ntop.ipCmp(a, b) < 0)
end

function ip_address_rev(a, b)
   return(ntop.ipCmp(a, b) > 0)
end

-- ###########################################

-- @brief Deletes all the cache/prefs keys matching the pattern
function deleteCachePattern(pattern)
   local keys = ntop.getKeysCache(pattern)

   for key in pairs(keys or {}) do
      ntop.delCache(key)
   end
end

-- ###########################################

-- NOTE: '~= "0"' is used for prefs which are enabled by default
function areInterfaceTimeseriesEnabled(ifid)
   return((ntop.getPref("ntopng.prefs.interface_rrd_creation") ~= "0"))
end

function areInterfaceL7TimeseriesEnabled(ifid)
   return(areInterfaceTimeseriesEnabled(ifid) and
      (ntop.getPref("ntopng.prefs.interface_ndpi_timeseries_creation") ~= "per_category"))
end

function areInterfaceCategoriesTimeseriesEnabled(ifid)
   local rv = ntop.getPref("ntopng.prefs.interface_ndpi_timeseries_creation")

   -- note: categories are disabled by default
   return(areInterfaceTimeseriesEnabled(ifid) and
      ((rv == "per_category") or (rv == "both")))
end

function areHostTimeseriesEnabled(ifid)
   local rv = ntop.getPref("ntopng.prefs.hosts_ts_creation")
   if isEmptyString(rv) then rv = "light" end

   return((rv == "light") or (rv == "full"))
end

function areHostL7TimeseriesEnabled(ifid)
   local rv = ntop.getPref("ntopng.prefs.host_ndpi_timeseries_creation")

   -- note: host protocols are disabled by default
   return((ntop.getPref("ntopng.prefs.hosts_ts_creation") == "full") and
      ((rv == "per_protocol") or (rv == "both")))
end

function areHostCategoriesTimeseriesEnabled(ifid)
   local rv = ntop.getPref("ntopng.prefs.host_ndpi_timeseries_creation")

   -- note: host protocols are disabled by default
   return((ntop.getPref("ntopng.prefs.hosts_ts_creation") == "full") and
      ((rv == "per_category") or (rv == "both")))
end

function areSystemTimeseriesEnabled()
   return(ntop.getPref("ntopng.prefs.system_probes_timeseries") ~= "0")
end

function areHostPoolsTimeseriesEnabled(ifid)
   return(ntop.isPro() and (ntop.getPref("ntopng.prefs.host_pools_rrd_creation") == "1"))
end

function areASTimeseriesEnabled(ifid)
   return(ntop.getPref("ntopng.prefs.asn_rrd_creation") == "1")
end

function areInternalTimeseriesEnabled(ifid)
   -- NOTE: no separate preference so far
   return(areSystemTimeseriesEnabled())
end

function areCountryTimeseriesEnabled(ifid)
   return((ntop.getPref("ntopng.prefs.country_rrd_creation") == "1"))
end

function areOSTimeseriesEnabled(ifid)
   return((ntop.getPref("ntopng.prefs.os_rrd_creation") == "1"))
end

function areVlanTimeseriesEnabled(ifid)
   return(ntop.getPref("ntopng.prefs.vlan_rrd_creation") == "1")
end

function areMacsTimeseriesEnabled(ifid)
   return(ntop.getPref("ntopng.prefs.l2_device_rrd_creation") == "1")
end

function areContainersTimeseriesEnabled(ifid)
   -- NOTE: no separate preference so far
   return(true)
end

function areSnmpTimeseriesEnabled(device, port_idx)
   return(ntop.getPref("ntopng.prefs.snmp_devices_rrd_creation") == "1")
end

function areFlowdevTimeseriesEnabled(ifid, device)
   return(ntop.getPref("ntopng.prefs.flow_device_port_rrd_creation") == "1")
end

-- ###########################################

-- version is major.minor.veryminor
function version2int(v)
   if(v == nil) then return(0) end

  e = string.split(v, "%.");
  if(e ~= nil) then
    major = e[1]
    minor = e[2]
    veryminor = e[3]

    if(major == nil or tonumber(major) == nil or type(major) ~= "string")     then major = 0 end
    if(minor == nil or tonumber(minor) == nil or type(minor) ~= "string")     then minor = 0 end
    if(veryminor == nil or tonumber(veryminor) == nil or type(veryminor) ~= "string") then veryminor = 0 end

    version = tonumber(major)*1000 + tonumber(minor)*100 -- + tonumber(veryminor)
    return(version)
  else
    return(0)
  end
end

--- Check if there is a new major release
--- @return string message If there is a new major release then return a non-nil string
--- containing the update message.
function check_latest_major_release()

   if ntop.isOffline() then
      return nil
   end

   -- get the latest major release
   local latest_version = ntop.getCache("ntopng.cache.major_release")

   -- tprint(debug.traceback())

   if isEmptyString(latest_version) then
     local rsp = ntop.httpGet("https://www.ntop.org/ntopng.version", "", "", 10 --[[ seconds ]])

     if (not isEmptyString(rsp)) and (not isEmptyString(rsp["CONTENT"])) then
        latest_version = trimSpace(string.gsub(rsp["CONTENT"], "\n", ""))
     else
        -- a value that won't trigger an update message
        latest_version = "0.0.0"
     end

     ntop.setCache("ntopng.cache.major_release", latest_version, 86400 --[[ recheck interval]])
   end

   return get_version_update_msg(info, latest_version)
end

-- ###########################################

-- To be called inside the flows tableCallback
function initFlowsRefreshRows()
   print[[
datatableInitRefreshRows($("#table-flows"), "key_and_hash", 10000, {
   /* List of rows with trend icons */
   "column_thpt": ]] print(ternary(getThroughputType() ~= "bps", "NtopUtils.fpackets", "NtopUtils.bitsToSize")) print[[,
   "column_bytes": NtopUtils.bytesToSize,
});

$("#dt-bottom-details > .float-left > p").first().append('. ]]
   print(i18n('flows_page.idle_flows_not_listed'))
   print[[');]]
end

-- ###########################################

function canRestoreHost(ifid, ip, vlan)
   local ip_to_mac = string.format("ntopng.ip_to_mac.ifid_%u__%s@%d", ifid, ip, vlan)
   local key_to_check

   -- Check if there is a MAC address associated
   local mac = ntop.getCache(ip_to_mac)

   if not isEmptyString(mac) then
      key_to_check = string.format("ntopng.serialized_hostsbymac.ifid_%u__%s_%s", ifid, mac, ternary(isIPv4(ip), "v4", "v6"))
   else
      key_to_check = string.format("ntopng.serialized_hosts.ifid_%u__%s@%d", ifid, ip, vlan)
   end

   return(not table.empty(ntop.getKeysCache(key_to_check)))
end

-- ###########################################

function create_ndpi_proto_name(v)
   local app = ""

   if v["proto.ndpi"] then
      app = getApplicationLabel(v["proto.ndpi"])
   else
      local master_proto = interface.getnDPIProtoName(tonumber(v["l7_master_proto"]))
      local app_proto    = interface.getnDPIProtoName(tonumber(v["l7_proto"]))

      if master_proto == app_proto then
         app = app_proto
      elseif master_proto == "Unknown" then
         app = app_proto
      else
         app = master_proto

         if app_proto ~= "Unknown" then
            app = app .. "." .. app_proto
         end
      end

      app = getApplicationLabel(app)
   end

   return app
end

-- ##############################################

function setObsPointAlias(observation_point_id, alias)
   if((observation_point_id ~= alias) and not isEmptyString(alias)) then
      ntop.setHashCache(getObsPointAliasKey(), observation_point_id, alias)
   else
      ntop.delHashCache(getObsPointAliasKey(), observation_point_id)
   end
end

-- ##############################################

function setFlowDevAlias(flowdev_ip, alias)
   if((flowdev_ip ~= alias) and not isEmptyString(alias)) then
      ntop.setHashCache(getFlowDevAliasKey(), flowdev_ip, alias)
   else
      ntop.delHashCache(getFlowDevAliasKey(), flowdev_ip)
   end
end

-- ##############################################

function addScoreToAlertDescr(msg, score)
   return (msg .. string.format(" [%s: %s]",
            i18n("score"),
            format_utils.formatValue(score)))
end

-- ##############################################

function addHTTPInfoToAlertDescr(msg, alert_json, url_only)
   if ((alert_json)
      and (table.len(alert_json["proto"] or {}) > 0)
      and (table.len(alert_json["proto"]["http"] or {}) > 0)) then

      local http_info = format_http_info({ http_info = alert_json["proto"]["http"]["last_method"],
                                            last_return_code = alert_json["proto"]["http"]["last_return_code"],
                                            last_user_agent = alert_json["proto"]["http"]["last_user_agent"],
                                            last_url = alert_json["proto"]["http"]["last_url"] })

      if http_info["last_method"] then
         msg = msg .. string.format(" [ %s: %s ]", i18n("db_explorer.http_method"), http_info["last_method"])
      end

      if http_info["last_return_code"] then
         msg = msg .. string.format(" [ %s: %s ]", i18n("last_response_status_code"), http_info["last_return_code"])
      end

      if http_info["last_user_agent"] then
         msg = msg .. string.format(" [ %s: %s ]", i18n("last_user_agent"), http_info["last_user_agent"])
      end

      if http_info["last_url"] then
         msg = msg .. string.format(" [ %s: %s ]", i18n("last_url"), http_info["last_url"])
      end
   end

   return msg
end

-- ##############################################

function addDNSInfoToAlertDescr(msg, alert_json)
   if ((alert_json)
      and (table.len(alert_json["proto"] or {}) > 0)
      and (table.len(alert_json["proto"]["dns"] or {}) > 0)) then

      local dns_info = format_dns_query_info({ last_query_type = alert_json["proto"]["dns"]["last_query_type"],
                                                last_return_code = alert_json["proto"]["dns"]["last_return_code"],
                                                last_query = alert_json["proto"]["dns"]["last_query"] })

      if dns_info["last_query_type"] then
         msg = msg .. string.format(" [ %s: %s ]",
            i18n("last_query_type"),
            dns_info["last_query_type"])
      end

      if dns_info["last_return_code"] then
         msg = msg .. string.format(" [ %s: %s ]",
            i18n("last_return_code"),
            dns_info["last_return_code"])
      end

      if dns_info["last_query"] then
         msg = msg .. string.format(" [ %s: %s ]", i18n("last_url"), dns_info["last_query"])
      end
   end

   return msg
end

-- ##############################################

function addTLSInfoToAlertDescr(msg, alert_json)
   if ((alert_json)
      and (table.len(alert_json["proto"] or {}) > 0)
      and (table.len(alert_json["proto"]["tls"] or {}) > 0)) then

      local tls_info = format_tls_info({ notBefore = alert_json["proto"]["tls"]["notBefore"],
                                          notAfter = alert_json["proto"]["tls"]["notAfter"],
                                          client_requested_server_name = alert_json["proto"]["tls"]["client_requested_server_name"],
                                          ['ja3.server_unsafe_cipher'] = alert_json["proto"]["tls"]["ja3.server_unsafe_cipher"] })

      if tls_info["tls_certificate_validity"] then
        msg = msg .. string.format(" [ %s: %s ]", i18n("tls_certificate_validity"), tls_info["tls_certificate_validity"])
      end

      if tls_info["ja3.server_unsafe_cipher"] then
        msg = msg .. string.format(" [ %s: %s ]", i18n("ja3.server_unsafe_cipher"), tls_info["ja3.server_unsafe_cipher"])
      end

      if tls_info["client_requested_server_name"] then
        msg = msg .. string.format(" [ %s: %s ]", i18n("client_requested_server_name"), tls_info["client_requested_server_name"])
      end
   end

   return msg
end

-- ##############################################

function addICMPInfoToAlertDescr(msg, alert_json)
   if ((alert_json)
      and (table.len(alert_json["proto"] or {}) > 0)
      and (table.len(alert_json["proto"]["icmp"] or {}) > 0)) then

      local icmp_info = format_icmp_info({ code = alert_json["proto"]["icmp"]["code"],
                                           type = alert_json["proto"]["icmp"]["type"] })

      -- Already formatted by the function
      if icmp_info["type"] then
        msg = msg .. string.format(" [ %s: %s ]", i18n("icmp_type"), icmp_info["type"])
      end

      if icmp_info["code"] then
        msg = msg .. string.format(" [ %s: %s ]", i18n("icmp_code"), icmp_info["code"])
      end
   end

   return msg
end

-- ##############################################

function addBytesInfoToAlertDescr(msg, value)
  msg = string.format("%s [ %s: %s | %s: %s ]", msg,
                      i18n("server_traffic"), bytesToSize(value["srv2cli_bytes"] or 0),
                      i18n("client_traffic"), bytesToSize(value["cli2srv_bytes"] or 0))

  return msg
end

-- ##############################################

function addExtraFlowInfo(alert_json, value)
  local msg = ""
  msg = addHTTPInfoToAlertDescr(msg, alert_json)
  msg = addDNSInfoToAlertDescr(msg, alert_json)
  msg = addTLSInfoToAlertDescr(msg, alert_json)
  msg = addICMPInfoToAlertDescr(msg, alert_json)
  msg = addBytesInfoToAlertDescr(msg, value)

  return msg
end

-- ##############################################

function hostnameIsDomain(name)
  if not isEmptyString(name) then
    local parts = string.split(name, "%.")

    if parts and #parts > 1 then
      local last = parts[#parts]
      if string.len(last) > 0 then
        return true
      end
    end
  end

  return false
end

-- ##############################################

local iec104_typeids = {
   M_SP_NA_1=0x01,
   M_SP_TA_1=0x02,
   M_DP_NA_1=0x03,
   M_DP_TA_1=0x04,
   M_ST_NA_1=0x05,
   M_ST_TA_1=0x06,
   M_BO_NA_1=0x07,
   M_BO_TA_1=0x08,
   M_ME_NA_1=0x09,
   M_ME_TA_1=0x0A,
   M_ME_NB_1=0x0B,
   M_ME_TB_1=0x0C,
   M_ME_NC_1=0x0D,
   M_ME_TC_1=0x0E,
   M_IT_NA_1=0x0F,
   M_IT_TA_1=0x10,
   M_EP_TA_1=0x11,
   M_EP_TB_1=0x12,
   M_EP_TC_1=0x13,
   M_PS_NA_1=0x14,
   M_ME_ND_1=0x15,
   M_SP_TB_1=30,
   M_DP_TB_1=31,
   M_ST_TB_1=32,
   M_BO_TB_1=33,
   M_ME_TD_1=34,
   M_ME_TE_1=35,
   M_ME_TF_1=36,
   M_IT_TB_1=37,
   M_EP_TD_1=38,
   M_EP_TE_1=39,
   M_EP_TF_1=40,
   ASDU_TYPE_41=41,
   ASDU_TYPE_42=42,
   ASDU_TYPE_43=43,
   ASDU_TYPE_44=44,
   C_SC_NA_1=45,
   C_DC_NA_1=46,
   C_RC_NA_1=47,
   C_SE_NA_1=48,
   C_SE_NB_1=49,
   C_SE_NC_1=50,
   C_BO_NA_1=51,
   C_SC_TA_1=58,
   C_DC_TA_1=59,
   C_RC_TA_1=60,
   C_SE_TA_1=61,
   C_SE_TB_1=62,
   C_SE_TC_1=63,
   C_BO_TA_1=64,
   M_EI_NA_1=70,
   C_IC_NA_1=100,
   C_CI_NA_1=101,
   C_RD_NA_1=102,
   C_CS_NA_1=103,
   C_TS_NA_1=104,
   C_RP_NA_1=105,
   C_CD_NA_1=106,
   C_TS_TA_1=107,
   P_ME_NA_1=110,
   P_ME_NB_1=111,
   P_ME_NC_1=112,
   P_AC_NA_1=113,
   F_FR_NA_1=120,
   F_SR_NA_1=121,
   F_SC_NA_1=122,
   F_LS_NA_1=123,
   F_FA_NA_1=124,
   F_SG_NA_1=125,
   F_DR_TA_1=126,
}

function iec104_typeids2str(c)
   if(c == nil) then return end

   for s, v in pairs(iec104_typeids) do
      if(v == tonumber(c)) then
	 return(s.." (".. v ..")")
      end
   end

   return(c)
end

-- ##############################################

function format_device_name(device_ip, short_version)
  local device_name = device_ip

  if ntop.isPro() then
    device_name = hostinfo2label(hostkey2hostinfo(device_ip))

    if device_name ~= device_ip then
      if short_version then
        device_name = shortenString(device_name, 32)
      end
      device_name = string.format('%s [%s]', device_ip, device_name)
    end
  end

  return device_name
end

-- ##############################################

-- This function, given a record and a name return a  standard formatted value
-- and if the value is 0, an empty string is returned
-- e.g.   1000 -> 1,000    | 0 -> 
function format_high_num_value_for_tables(record, name)
  local formatted_record = format_utils.formatValue(record[name] or 0)
  if formatted_record == '0' then
    formatted_record = ''
  end

  return formatted_record
end

-- ##############################################

require "lua_utils_print"
require "lua_utils_get"
require "lua_utils_gui"

if(trace_script_duration ~= nil) then
   io.write(debug.getinfo(1,'S').source .." executed in ".. (os.clock()-clock_start)*1000 .. " ms\n")
end
