mirror of https://github.com/aredn/aredn.git
1111 lines
37 KiB
Lua
Executable File
1111 lines
37 KiB
Lua
Executable File
#!/usr/bin/lua
|
|
--[[
|
|
|
|
Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks
|
|
Copyright (C) 2021 Tim Wilkinson
|
|
Original Perl Copyright (C) 2015 Conrad Lara
|
|
See Contributors file for additional contributors
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation version 3 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
Additional Terms:
|
|
|
|
Additional use restrictions exist on the AREDN® trademark and logo.
|
|
See AREDNLicense.txt for more info.
|
|
|
|
Attributions to the AREDN® Project must be retained in the source code.
|
|
If importing this code into a new or existing project attribution
|
|
to the AREDN® project must be added to the source code.
|
|
|
|
You must not misrepresent the origin of the material contained within.
|
|
|
|
Modified versions must be modified to attribute to the original source
|
|
and be marked in reasonable ways as differentiate it from the original
|
|
version
|
|
|
|
--]]
|
|
|
|
require("nixio")
|
|
require("aredn.hardware")
|
|
require("aredn.http")
|
|
require("aredn.utils")
|
|
require("aredn.html")
|
|
require("uci")
|
|
require("aredn.info")
|
|
require("aredn.olsr")
|
|
require("iwinfo")
|
|
require('luci.jsonc')
|
|
|
|
local html = aredn.html
|
|
|
|
local node = aredn.info.get_nvram("node")
|
|
if node == "" then
|
|
node = "NOCALL"
|
|
end
|
|
local tactical = aredn.info.get_nvram("tactical")
|
|
local config = aredn.info.get_nvram("config")
|
|
if config == "" or nixio.fs.stat("/etc/config.mesh", "type") ~= "dir" then
|
|
config = "not set"
|
|
end
|
|
local wifiif = aredn.hardware.get_iface_name("wifi")
|
|
local my_ip = aredn.hardware.get_interface_ip4(wifiif)
|
|
if not my_ip then
|
|
my_ip = "none"
|
|
end
|
|
local phy = iwinfo.nl80211.phyname(wifiif)
|
|
if not phy then
|
|
phy = 0
|
|
end
|
|
|
|
local chanbw = 1
|
|
local cb = "/sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/chanbw"
|
|
if not nixio.fs.stat(cb) then
|
|
cb = "/sys/kernel/debug/ieee80211/" .. phy .. "/ath10k/chanbw"
|
|
end
|
|
if nixio.fs.stat(cb) then
|
|
for line in io.lines(cb)
|
|
do
|
|
if line == "0x00000005" then
|
|
chanbw = 4
|
|
elseif line == "0x0000000a" then
|
|
chanbw = 2
|
|
end
|
|
break
|
|
end
|
|
end
|
|
|
|
if not nixio.fs.stat("/tmp/web") then
|
|
nixio.fs.mkdir("/tmp/web")
|
|
end
|
|
|
|
local cursor = uci.cursor()
|
|
local node_desc = cursor:get("system", "@system[0]", "description")
|
|
local lat_lon = "<strong>Location Not Available</strong>"
|
|
local lat = cursor:get("aredn", "@location[0]", "lat")
|
|
local lon = cursor:get("aredn", "@location[0]", "lon")
|
|
if lat and lon then
|
|
lat_lon = string.format("<center><strong>Location: </strong> %s %s</center>", lat, lon)
|
|
end
|
|
|
|
local routes = {}
|
|
local links = {}
|
|
local neighbor = {}
|
|
local wangateway = {}
|
|
local ipalias = {}
|
|
local localhosts = {}
|
|
local dtd = {}
|
|
local midcount = {}
|
|
local xlinkcount = {}
|
|
local hosts = {}
|
|
local services = {}
|
|
local history = {}
|
|
|
|
for i, node in ipairs(aredn.olsr.getOLSRRoutes())
|
|
do
|
|
if node.genmask ~= 0 and node.etx <= 50 then
|
|
routes[node.destination] = { etx = node.etx }
|
|
end
|
|
end
|
|
|
|
-- low memory route reduction
|
|
local lowmemory = 1024 * tonumber(cursor:get("aredn", "@meshstatus[0]", "lowmem") or 10000)
|
|
local lowroutes = tonumber(cursor:get("aredn", "@meshstatus[0]", "lowroutes") or 1000)
|
|
if #routes > lowroutes and nixio.sysinfo().freeram < lowmemory then
|
|
local list = {}
|
|
for k,v in pairs(routes)
|
|
do
|
|
list[#list + 1] = { key = k, etx = v.etx }
|
|
end
|
|
table.sort(list, function (a, b) return a.etx < b.etx end)
|
|
for i = lowroutes, #list - 1
|
|
do
|
|
routes[list[i].key] = nil
|
|
end
|
|
end
|
|
|
|
-- load up arpcache
|
|
local arpcache = {}
|
|
arptable(function(a)
|
|
if a["Flags"] ~= "0x0" and a["HW address"] ~= "00:00:00:00:00:00" then
|
|
arpcache[a["IP address"]] = a
|
|
end
|
|
end)
|
|
|
|
local iwrates
|
|
for i, node in ipairs(aredn.olsr.getOLSRLinks())
|
|
do
|
|
links[node.remoteIP] = { lq = node.linkQuality, nlq = node.neighborLinkQuality, mbps = "", weight = 65536 / node.lossMultiplier }
|
|
neighbor[node.remoteIP] = true
|
|
local mac = arpcache[node.remoteIP]
|
|
if mac then
|
|
if not iwrates then
|
|
iwrates = {}
|
|
if wifiif ~= "br-nomesh" then
|
|
local station = {}
|
|
for line in io.popen("iw " .. wifiif .. " station dump"):lines()
|
|
do
|
|
local mac = line:match("^Station (%S+) ")
|
|
if mac then
|
|
station = {}
|
|
iwrates[mac] = station
|
|
end
|
|
local txbitrate = line:match("tx bitrate:%s+([%d%.]+) MBit/s")
|
|
if txbitrate then
|
|
station.txbitrate = txbitrate
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local station = iwrates[mac["HW address"]]
|
|
if station then
|
|
links[node.remoteIP].mbps = string.format("%.1f", tonumber(station.txbitrate) / chanbw)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- discard
|
|
arpcache = nil
|
|
|
|
for i, node in ipairs(aredn.olsr.getOLSRHNA())
|
|
do
|
|
if node.destination == "0.0.0.0" then
|
|
wangateway[node.gateway] = true
|
|
end
|
|
end
|
|
|
|
for i, node in ipairs(aredn.olsr.getOLSRMid())
|
|
do
|
|
local ip = node.main.ipAddress
|
|
for _, alias in ipairs(node.aliases)
|
|
do
|
|
local aip = alias.ipAddress
|
|
ipalias[aip] = ip
|
|
neighbor[aip] = true
|
|
if links[aip] then
|
|
neighbor[ip] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- load the local hosts file
|
|
for line in io.lines("/etc/hosts")
|
|
do
|
|
if line:match("^10%.") then
|
|
local ip, name = line:match("([%d%.]+)%s+(%S+)")
|
|
if name then
|
|
local name9 = name:sub(1, 9)
|
|
if name9 ~= "localhost" and name9 ~= "localnode" then
|
|
local name7 = name:sub(1, 7)
|
|
if name7 ~= "localap" and name7 ~= "dtdlink" then
|
|
if not name:match("%.") then
|
|
name = name .. ".local.mesh"
|
|
end
|
|
local tac = line:match("[%d%.]+%s+%S+%s+(%S+)")
|
|
if not tac then
|
|
tac = ""
|
|
end
|
|
if not localhosts[my_ip] then
|
|
localhosts[my_ip] = { hosts = {}, noprops = {}, aliases = {}, name = name, tactical = tac }
|
|
end
|
|
local host = localhosts[my_ip]
|
|
if ip == my_ip then
|
|
host.tactical = tac
|
|
host.name = name
|
|
else
|
|
host.hosts[#host.hosts + 1] = name
|
|
end
|
|
if tac == "#NOPROP" then
|
|
host.noprops[#host.noprops + 1] = name
|
|
end
|
|
if tac == "#ALIAS" then
|
|
host.aliases[#host.aliases + 1] = name
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- load the olsr hosts file
|
|
for line in aredn.olsr.getHostAsLines(2)
|
|
do
|
|
local ip, name, originator = line:match("^([%d%.]+)%s+(%S+)%s+%S+%s+(%S+)")
|
|
if ip and originator and originator ~= "myself" and (routes[ip] or routes[originator]) then
|
|
local etx = routes[ip]
|
|
if not etx then
|
|
etx = routes[originator]
|
|
end
|
|
etx = etx.etx
|
|
if not name:match("%.") or name:match("^mid%.[^%.]*$") then
|
|
name = name .. ".local.mesh"
|
|
end
|
|
if ip == originator then
|
|
if not hosts[originator] then
|
|
hosts[originator] = { hosts = {} }
|
|
end
|
|
local host = hosts[originator]
|
|
if host.name then
|
|
host.tactical = name
|
|
else
|
|
host.name = name
|
|
host.etx = etx
|
|
end
|
|
elseif name:match("^dtdlink%.") then
|
|
dtd[originator] = true
|
|
if links[ip] then
|
|
links[ip].dtd = true
|
|
end
|
|
elseif name:match("^xlink%d+%.") then
|
|
if not xlinkcount[originator] then
|
|
xlinkcount[originator] = 1
|
|
else
|
|
xlinkcount[originator] = xlinkcount[originator] + 1
|
|
end
|
|
if links[ip] then
|
|
links[ip].xlink = true
|
|
end
|
|
elseif name:match("^mid%d+%.") then
|
|
if not midcount[originator] then
|
|
midcount[originator] = 1
|
|
else
|
|
midcount[originator] = midcount[originator] + 1
|
|
end
|
|
if links[ip] then
|
|
links[ip].tun = true
|
|
end
|
|
else
|
|
if not hosts[originator] then
|
|
hosts[originator] = { hosts = {} }
|
|
end
|
|
local host = hosts[originator]
|
|
host.hosts[#host.hosts + 1] = name
|
|
end
|
|
end
|
|
end
|
|
|
|
-- discard
|
|
routes = nil
|
|
|
|
for line in aredn.olsr.getServicesAsLines()
|
|
do
|
|
if line:match("^%w") then
|
|
local url, name = line:match("^(.*)|.*|(.*)$")
|
|
if name then
|
|
local protocol, host, port, path = url:match("^([%w][%w%+%-%.]+)%://(.+):(%d+)/(.*)")
|
|
if path then
|
|
local name, originator = name:match("(.*%S)%s*#(.*)")
|
|
if originator == " my own service" or (hosts[originator] and hosts[originator].name) then
|
|
if not host:match("%.local%.mesh$") then
|
|
host = host .. ".local.mesh"
|
|
end
|
|
if not services[host] then
|
|
services[host] = {}
|
|
end
|
|
if not services[host][name] then
|
|
if port ~= "0" then
|
|
services[host][name] = "<a href='" .. protocol .. "://" .. host .. ":" .. port .. "/" .. path .. "' target='_blank'>" .. name .. "</a>"
|
|
else
|
|
services[host][name] = name
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- load the node history
|
|
local f = io.open("/tmp/node.history")
|
|
if f then
|
|
for line in f:lines("/tmp/node.history")
|
|
do
|
|
local ip, age, host = line:match("^(%S+) (%d+) (%S+)")
|
|
if ip and age and host then
|
|
history[ip] = { age = age, host = host:gsub("/", " / ") }
|
|
end
|
|
end
|
|
end
|
|
|
|
function ac(a, b)
|
|
if not a or a == "" then
|
|
return b
|
|
elseif not b or b == "" then
|
|
return a
|
|
else
|
|
return a .. "," .. b
|
|
end
|
|
end
|
|
|
|
------------------
|
|
-- generate page
|
|
------------------
|
|
http_header()
|
|
html.header(node .. " mesh status", false)
|
|
local automesh = string.find((nixio.getenv("QUERY_STRING") or ""):lower(),"automesh=1")
|
|
if automesh then
|
|
html.print("<script>setTimeout(function(){ window.location.reload(); }, 10000);</script>")
|
|
end
|
|
html.print([[
|
|
<style>
|
|
table {
|
|
cellspacing:5px;
|
|
width:80%;
|
|
border-collapse:collapse;
|
|
}
|
|
td {
|
|
padding-left:5px;
|
|
}
|
|
tr {
|
|
vertical-align:top;
|
|
text-align:left;
|
|
}
|
|
tr.spaceUnder>td {
|
|
padding-bottom:2em;
|
|
}
|
|
tbody:hover {
|
|
background-color:rgba(128,128,128,0.15);
|
|
}
|
|
tr.h {
|
|
border-top: 1px solid black;
|
|
border-bottom: 1px solid black;
|
|
}
|
|
th {
|
|
white-space:nowrap;
|
|
vertical-align:middle;
|
|
padding:3px 5px;
|
|
}
|
|
input.search {
|
|
width:150px;
|
|
}
|
|
#nTable {
|
|
margin-bottom: 12px;
|
|
}
|
|
#nTable th:nth-child(1) {
|
|
width: 25%;
|
|
}
|
|
#nTable th:nth-child(2) {
|
|
width: 50%;
|
|
}
|
|
#nTable th:nth-child(3) {
|
|
width: 25%;
|
|
}
|
|
#cTable {
|
|
margin-bottom: 12px;
|
|
}
|
|
#cTable th:nth-child(1), #rTable th:nth-child(1) {
|
|
width: 25%;
|
|
}
|
|
#cTable th:nth-child(2), #rTable th:nth-child(2) {
|
|
width: 20%;
|
|
}
|
|
#cTable th:nth-child(3), #cTable th:nth-child(4), #cTable th:nth-child(5) {
|
|
width: 4%;
|
|
padding: 0 2px 0 0;
|
|
text-align: right;
|
|
}
|
|
#cTable th:nth-child(6), #cTable th:nth-child(7), #cTable th:nth-child(8) {
|
|
width: 6%;
|
|
padding: 0 2px 0 0;
|
|
text-align: right;
|
|
}
|
|
#cTable td:nth-child(3), #cTable td:nth-child(4), #cTable td:nth-child(5), #cTable td:nth-child(6), #cTable td:nth-child(7), #cTable td:nth-child(8) {
|
|
text-align: right;
|
|
padding: 0 2px 0 0;
|
|
}
|
|
#cTable th:nth-child(9) {
|
|
width: 25%;
|
|
}
|
|
#rTable th:nth-child(3), #rTable td:nth-child(3) {
|
|
width: 4%;
|
|
padding: 0 2px 0 0;
|
|
text-align: right;
|
|
}
|
|
#rTable th:nth-child(4) {
|
|
width: 26%;
|
|
}
|
|
#rTable th:nth-child(5) {
|
|
width: 25%;
|
|
}
|
|
tr.s.f.nf {
|
|
display: table-row;
|
|
}
|
|
tr.s.nf {
|
|
display: none;
|
|
}
|
|
tr.s.f.nf td, tr.s.f.nf td.s.f.nf {
|
|
color: transparent;
|
|
}
|
|
tr.s.f.nf td.s.f.nf:last-child a {
|
|
display: none;
|
|
}
|
|
tr.s.f.nf td.s.f {
|
|
color: inherit;
|
|
}
|
|
tr.s.f.nf td:first-child, tr.s.f.nf td:nth-child(2), tr.s.f.nf td:nth-child(3) {
|
|
color: inherit;
|
|
}
|
|
#cTable tr.s.f.nf td:nth-child(4), #cTable tr.s.f.nf td:nth-child(5), #cTable tr.s.f.nf td:nth-child(6), #cTable tr.s.f.nf td:nth-child(7), #cTable tr.s.f.nf td:nth-child(8) {
|
|
color: inherit;
|
|
}
|
|
</style>
|
|
<script>
|
|
let searchPending;
|
|
let cFilter = "";
|
|
function doSearch() {
|
|
clearTimeout(searchPending);
|
|
searchPending = setTimeout(function() {
|
|
const filter = document.getElementById("srch").value.toUpperCase();
|
|
if (filter !== cFilter) {
|
|
cFilter = filter;
|
|
const rows = document.querySelectorAll("tr.s");
|
|
for (let i = 0; i < rows.length; i++) {
|
|
rows[i].classList.remove("f", "nf");
|
|
}
|
|
const cells = document.querySelectorAll("tr.s td.nf");
|
|
for (let i = 0; i < cells.length; i++) {
|
|
cells[i].classList.remove("nf");
|
|
}
|
|
const tds = document.querySelectorAll("td.s");
|
|
if (filter === "") {
|
|
for (let i = 0; i < tds.length; i++) {
|
|
const td = tds[i];
|
|
td.parentElement.classList.add("f");
|
|
td.parentElement.parentElement.firstElementChild.classList.add("f");
|
|
}
|
|
}
|
|
else {
|
|
for (let i = 0; i < tds.length; i++) {
|
|
const td = tds[i];
|
|
let txt = td.dataset.search;
|
|
if (txt === undefined) {
|
|
txt = (td.textContent || td.innerText).toUpperCase();
|
|
td.dataset.search = txt
|
|
}
|
|
if (txt.indexOf(filter) === -1) {
|
|
td.classList.add("nf");
|
|
td.parentElement.classList.add("nf");
|
|
}
|
|
else {
|
|
td.parentElement.classList.add("f");
|
|
td.parentElement.parentElement.firstElementChild.classList.add("f");
|
|
}
|
|
}
|
|
}
|
|
const tables = document.querySelectorAll("table.s");
|
|
for (let i = 0; i < tables.length; i++) {
|
|
if (!tables[i].querySelector("tr.f")) {
|
|
tables[i].style.display = "none";
|
|
}
|
|
else {
|
|
tables[i].style.display = null;
|
|
}
|
|
}
|
|
}
|
|
}, 200);
|
|
}
|
|
</script>
|
|
]])
|
|
html.print("</head>")
|
|
|
|
html.print("<body><form method=post action=/cgi-bin/mesh accept-charset=utf-8 enctype='multipart/form-data'>")
|
|
html.print("<input type=hidden name=reload value=1>")
|
|
html.print("<center>")
|
|
html.alert_banner()
|
|
html.print("<h1>" .. node .. " mesh status</h1>")
|
|
html.print(lat_lon)
|
|
if node_desc then
|
|
html.print("<table id='node_description_display'><tr><td align=center>" .. node_desc .. "</td></tr></table>")
|
|
end
|
|
html.print("<hr><nobr>")
|
|
|
|
html.print("<a href='/help.html#meshstatus' target='_blank'>Help</a> ")
|
|
|
|
if automesh then
|
|
html.print("<input type=button name=stop value=Stop title='Abort continuous status' onclick='window.location = window.location.origin + window.location.pathname'>")
|
|
else
|
|
html.print("<input type=button name=refresh value=Refresh title='Refresh this page' onclick='window.location.reload();'>")
|
|
html.print(" ")
|
|
html.print([[<input type=button name=auto value=Auto title='Automatic page refresh' onclick='window.location = window.location.origin + window.location.pathname + "?automesh=1"'>]])
|
|
end
|
|
if nixio.fs.stat("/tmp/dnsmasq.d/supernode.conf") then
|
|
local ip = read_all("/tmp/dnsmasq.d/supernode.conf"):match("^#(%S+)")
|
|
if ip then
|
|
html.print(" ")
|
|
html.print("<a target=_blank href='http://" .. ip .. "/cgi-bin/mesh'><button type=button title='See what is on the whole AREDN mesh'>Cloud Mesh</button></a>")
|
|
end
|
|
end
|
|
html.print(" ")
|
|
html.print("<button type=button onClick='window.location=\"status\"' title='Return to the status page'>Quit</button>")
|
|
html.print(" ")
|
|
html.print("<input id=srch type=search autocorrect=off spellcheck=false placeholder='Search...' onkeyup='doSearch()' onclick='doSearch()' onkeypress='event.keyCode == 13 && event.preventDefault()'>")
|
|
html.print("</nobr><br><br>")
|
|
|
|
if not next(localhosts) and not next(links) then
|
|
html.print("No other nodes are available.")
|
|
html.print("</center></form>")
|
|
html.footer()
|
|
html.print("</body></html>")
|
|
os.exit(0)
|
|
end
|
|
|
|
-- show local node table
|
|
|
|
html.print("<table id=nTable class=s><thead>")
|
|
html.print("<tr class=h>")
|
|
html.print("<th>Node Name</th>")
|
|
html.print("<th>LAN Hostname</th>")
|
|
html.print("<th>Service Name</th>")
|
|
html.print("</tr></thead>")
|
|
|
|
if next(localhosts) then
|
|
local rows = {}
|
|
for ip, host in pairs(localhosts)
|
|
do
|
|
local localpart = host.name:match("([^.]*)%.")
|
|
if localpart then
|
|
local tactical = ""
|
|
if host.tactical ~= "" then
|
|
tactical = " / " .. host.tactical
|
|
end
|
|
local c1 = localpart .. tactical
|
|
if wangateway[ip] then
|
|
c1 = c1 .. " <small>(wan)</small>"
|
|
end
|
|
|
|
local tbody = "<tbody>"
|
|
if services[host.name] then
|
|
local first = true;
|
|
for _, v in pairs(services[host.name])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='f'></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td class='f'></td></tr>"
|
|
end
|
|
|
|
for _, dmzhost in ipairs(host.hosts)
|
|
do
|
|
local nopropd = false
|
|
local aliased = false
|
|
for _, v in ipairs(host.noprops)
|
|
do
|
|
if v == dmzhost then
|
|
nopropd = true;
|
|
break
|
|
end
|
|
end
|
|
for _, v in ipairs(host.aliases)
|
|
do
|
|
if v == dmzhost then
|
|
aliased = true;
|
|
break
|
|
end
|
|
end
|
|
local localpart = dmzhost:match("(.*)%.local%.mesh")
|
|
if localpart then
|
|
if aliased then
|
|
localpart = "<span class=aliased-hosts>" .. localpart .. "</span>"
|
|
elseif nopropd then
|
|
localpart = "<span class=hidden-hosts>" .. localpart .. "</span>"
|
|
end
|
|
if services[dmzhost] then
|
|
local first = true
|
|
for _, v in pairs(services[dmzhost])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='f'></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td class='f'></td></tr>"
|
|
end
|
|
end
|
|
end
|
|
tbody = tbody .. "</tbody>"
|
|
rows[#rows + 1] = { key = host.name, row = tbody }
|
|
end
|
|
end
|
|
table.sort(rows, function(a,b) return a.key < b.key end)
|
|
for _, row in ipairs(rows)
|
|
do
|
|
html.print(row.row)
|
|
end
|
|
-- discard
|
|
rows = nil
|
|
else
|
|
html.print("<tbody><tr><td class='s f'>none</td></tr></tbody>")
|
|
end
|
|
|
|
-- discard
|
|
localhosts = nil
|
|
-- end local node table
|
|
html.print("</table>")
|
|
|
|
-- show current neighbors table
|
|
|
|
html.print("<table id='cTable' class=s><thead><tr class=h>")
|
|
html.print("<th>Current Neighbors</th>")
|
|
html.print("<th>LAN Hostname</th>")
|
|
html.print("<th title='Percent of packets successfully received'>LQ</th><th title='Percent of packets successfully sent'>NLQ</th>")
|
|
html.print("<th>SNR</th><th>Quality</th><th>TxMbps</th><th>Distance</th>")
|
|
html.print("<th>Service Name</th>")
|
|
html.print("</tr></thead>")
|
|
|
|
local trackers = {}
|
|
local l = io.open("/tmp/lqm.info")
|
|
if l then
|
|
local lqm = luci.jsonc.parse(l:read("*a"))
|
|
l:close()
|
|
for _, tracker in pairs(lqm.trackers)
|
|
do
|
|
if tracker.ip then
|
|
trackers[tracker.ip] = tracker
|
|
end
|
|
end
|
|
end
|
|
local now = nixio.sysinfo().uptime
|
|
|
|
local metric = true
|
|
local language = os.getenv("HTTP_ACCEPT_LANGUAGE") or "en-US"
|
|
if language:match("^..%-US") or language:match("^..%-GB") then
|
|
metric = false
|
|
end
|
|
|
|
local rows = {}
|
|
local neighservices = {}
|
|
for ip, link in pairs(links)
|
|
do
|
|
local ipmain = ipalias[ip]
|
|
if not ipmain then
|
|
ipmain = ip
|
|
end
|
|
local name = ipmain
|
|
local localpart = ipmain
|
|
local tactical = ""
|
|
local host = hosts[ipmain]
|
|
if host then
|
|
if host.name then
|
|
name = host.name
|
|
localpart = name:match("(.*)%.local%.mesh")
|
|
if not localpart then
|
|
localpart = name
|
|
end
|
|
end
|
|
if host.tactical then
|
|
tactical = " / " .. host.tactical
|
|
end
|
|
end
|
|
if rows[name] then
|
|
name = name .. " " -- avoid collision 2 links to same host {rf, dtd}
|
|
end
|
|
local no_space_host = name:match("(.*%S)%s*$")
|
|
local c1 = "<a href='http://" .. no_space_host .. ":8080/'>" .. localpart .. tactical .. "</a>"
|
|
local nodeiface
|
|
local waniface
|
|
local lqmstatus
|
|
if ipmain ~= ip then
|
|
if links[ip].dtd then
|
|
nodeiface = "dtd"
|
|
elseif links[ip].xlink then
|
|
nodeiface = "xlink"
|
|
elseif links[ip].tun then
|
|
nodeiface = "tun"
|
|
else
|
|
nodeiface = "?"
|
|
end
|
|
end
|
|
if wangateway[ip] or wangateway[ipmain] then
|
|
waniface = "wan"
|
|
end
|
|
|
|
local c2 = ""
|
|
local c3 = string.format("%.0f%%", math.min(100, math.ceil(100 * link.lq * link.weight)))
|
|
local c4 = string.format("%.0f%%", math.min(100, math.ceil(100 * link.nlq * link.weight)))
|
|
local c4b = ""
|
|
local c4c = ""
|
|
local c5 = string.format("%s", link.mbps)
|
|
local c5b = ""
|
|
local c6 = ""
|
|
|
|
-- lqm info
|
|
local track = trackers[ip]
|
|
if track and not (track.hidden or track.blocked) then
|
|
if not nodeiface and track.type == "RF" then
|
|
nodeiface = "rf"
|
|
end
|
|
if nodeiface == "tun" and track.type == "Wireguard" then
|
|
nodeiface = "wg"
|
|
end
|
|
if track.pending > now then
|
|
lqmstatus = "pending"
|
|
elseif track.routable then
|
|
lqmstatus = "active"
|
|
else
|
|
lqmstatus = "idle"
|
|
end
|
|
if (track.blocks.signal or track.blocks.distance or track.blocks.quality) and
|
|
((track.leaf == "minor" and track.rev_leaf == "major") or (track.leaf == "major" and track.rev_leaf == "minor")) then
|
|
lqmstatus = ac(lqmstatus, "leaf")
|
|
end
|
|
if track.snr then
|
|
c4b = track.snr
|
|
if track.rev_snr then
|
|
c4b = c4b .. "/" .. track.rev_snr
|
|
end
|
|
end
|
|
if track.quality then
|
|
c4c = track.quality .. "%"
|
|
end
|
|
if track.distance then
|
|
if not metric then
|
|
local v = track.distance * 0.000621371
|
|
if v > 1 then
|
|
c5b = math.ceil(v) .. " miles"
|
|
elseif v > 0.5 then
|
|
c5b = "1 mile"
|
|
else
|
|
c5b = "<1 mile"
|
|
end
|
|
else
|
|
c5b = math.ceil(track.distance * 0.001) .. " km"
|
|
end
|
|
end
|
|
end
|
|
|
|
if nodeiface or waniface or lqmstatus then
|
|
c1 = c1 .. " <small>(" .. ac(ac(nodeiface, waniface), lqmstatus) .. ")</small>"
|
|
end
|
|
|
|
-- print node services if any
|
|
local tbody = "<tbody>"
|
|
if not neighservices[name] then
|
|
neighservices[name] = true
|
|
|
|
if services[name] then
|
|
local first = true
|
|
for _, v in pairs(services[name])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td>" .. c3 .. "</td><td>" .. c4 .. "</td><td>" .. c4b .. "</td><td>" .. c4c .. "</td><td>" .. c5 .. "</td><td>" .. c5b .. "</td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='f'></td><td></td><td></td><td></td><td></td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td>" .. c3 .. "</td><td>" .. c4 .. "</td><td>" .. c4b .. "</td><td>" .. c4c .. "</td><td>" .. c5 .. "</td><td>" .. c5b .. "</td><td class='f'></td></tr>"
|
|
end
|
|
|
|
if host then
|
|
for _, dmzhost in ipairs(host.hosts)
|
|
do
|
|
local localpart = dmzhost:match("(.*)%.local%.mesh")
|
|
if localpart then
|
|
if services[dmzhost] then
|
|
local first = true
|
|
for _, v in pairs(services[dmzhost])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td></td><td></td><td></td><td></td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='s f'></td><td class='f'></td><td></td><td></td><td></td><td></td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td></td><td></td><td></td><td></td><td></td><td></td><td class='f'></td></tr>"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
tbody = tbody .. "</tbody>"
|
|
rows[#rows + 1] = { key = name:lower(), row = tbody }
|
|
end
|
|
for _, track in pairs(trackers)
|
|
do
|
|
if track.blocked then
|
|
local name = track.ip
|
|
if track.hostname then
|
|
name = track.hostname
|
|
end
|
|
local c1 = "<a href='http://" .. name:match("(.*%S)%s*$") .. ":8080/'>" .. name .. "</a>"
|
|
local c4c = ""
|
|
local c4b = ""
|
|
local c5b = ""
|
|
local type = ""
|
|
if track.type == "RF" then
|
|
type = "rf,"
|
|
end
|
|
if track.blocks.user then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked user)</small>"
|
|
elseif track.blocks.dtd then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked dtd)</small>"
|
|
elseif track.blocks.distance then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked distance)</small>"
|
|
elseif track.blocks.signal then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked signal)</small>"
|
|
elseif track.blocks.dup then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked dup)</small>"
|
|
elseif track.blocks.quality then
|
|
if track.ping_quality then
|
|
if track.tx_quality then
|
|
if track.tx_quality < track.ping_quality then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked retries)</small>"
|
|
else
|
|
c1 = c1 .. " <small>(" .. type .. "blocked latency)</small>"
|
|
end
|
|
else
|
|
c1 = c1 .. " <small>(" .. type .. "blocked latency)</small>"
|
|
end
|
|
else
|
|
if track.tx_quality then
|
|
c1 = c1 .. " <small>(" .. type .. "blocked retries)</small>"
|
|
end
|
|
end
|
|
else
|
|
c1 = c1 .. " <small>(" .. type .. "blocked)</small>"
|
|
end
|
|
if track.snr then
|
|
c4b = track.snr
|
|
if track.rev_snr then
|
|
c4b = c4b .. "/" .. track.rev_snr
|
|
end
|
|
end
|
|
if track.quality then
|
|
c4c = track.quality .. "%"
|
|
end
|
|
if track.distance then
|
|
if true then
|
|
local v = track.distance * 0.000621371
|
|
if v > 1 then
|
|
c5b = math.ceil(v) .. " miles"
|
|
elseif v > 0.5 then
|
|
c5b = "1 mile"
|
|
end
|
|
else
|
|
c5b = math.ceil(track.distance * 0.001) .. " km"
|
|
end
|
|
end
|
|
local tbody = "<tbody><tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td></td><td></td><td>" .. c4b .. "</td><td>" .. c4c .. "</td><td></td><td>" .. c5b .. "</td><td class='f'></td></tr></tbody>"
|
|
rows[#rows + 1] = { key = name:lower(), row = tbody }
|
|
end
|
|
end
|
|
|
|
local cn = {}
|
|
if #rows > 0 then
|
|
table.sort(rows, function(a,b) return a.key < b.key end)
|
|
for _, row in ipairs(rows)
|
|
do
|
|
cn[row.key] = true
|
|
html.print(row.row)
|
|
end
|
|
-- discard
|
|
rows = nil
|
|
else
|
|
html.print("<tbody><tr><td class='s f'>none</td></tr></tbody>")
|
|
end
|
|
|
|
--add previous neighbors
|
|
local rows = {}
|
|
local uptime = nixio.sysinfo().uptime
|
|
for ip, node in pairs(history)
|
|
do
|
|
if not (links[ip] or links[ipalias[ip]]) then
|
|
local age = uptime - tonumber(node.age)
|
|
local host = node.host
|
|
if host == "" then
|
|
host = ip
|
|
else
|
|
host = host:gsub("^mid%d+%.", ""):gsub("^dtdlink%.", "")
|
|
end
|
|
if not cn[host:lower()] then
|
|
cn[host:lower()] = true
|
|
local row = "<tbody style='color:sienna;'><tr><td>" .. host .. "</td><td colspan=7></td><td style='text-align:left'>"
|
|
if age < 3600 then
|
|
local val = math.floor(age / 60)
|
|
if val == 1 then
|
|
row = row .. "1 minute ago</td></tr>"
|
|
else
|
|
row = row .. val .. " minutes ago</td></tr>"
|
|
end
|
|
else
|
|
local val = string.format("%.1f", age / 3600)
|
|
if val == "1.0" then
|
|
row = row .. "1 hour ago</td></tr>"
|
|
else
|
|
row = row .. val .. " hours ago</td></tr>"
|
|
end
|
|
end
|
|
if hosts[ip] and hosts[ip].hosts then
|
|
for _, v in ipairs(hosts[ip].hosts)
|
|
do
|
|
row = row .. "<tr><td></td><td colspan=8>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
row = row .. "</tbody>"
|
|
rows[#rows + 1] = { key = age, row = row }
|
|
end
|
|
end
|
|
end
|
|
if #rows > 0 then
|
|
html.print("<tr><td style='color:sienna;font-weight:bold;padding-top:10px;'>Previous Neighbors</td></tr>")
|
|
table.sort(rows, function(a,b) return a.key < b.key end)
|
|
for _, row in ipairs(rows)
|
|
do
|
|
html.print(row.row)
|
|
end
|
|
-- discard
|
|
rows = nil
|
|
end
|
|
cn = nil
|
|
-- end current neighbors table
|
|
html.print("</table>")
|
|
|
|
-- show remote node table
|
|
|
|
html.print("<table id='rTable' class=s><thead><tr class=h>")
|
|
html.print("<th>Remote Nodes</th>")
|
|
html.print("<th>LAN Hostname</th>")
|
|
html.print("<th>ETX</th><th></th>")
|
|
html.print("<th>Service Name</th>")
|
|
html.print("</tr></thead>")
|
|
|
|
local rows = {}
|
|
for ip, host in pairs(hosts)
|
|
do
|
|
if not neighbor[ip] and host.name then
|
|
local localpart = host.name:match("(.*)%.local%.mesh")
|
|
if localpart then
|
|
local tactical = ""
|
|
if host.tactical then
|
|
tactical = " / " .. host.tactical
|
|
end
|
|
local etx = string.format("%.2f", host.etx)
|
|
local c1 = "<a href='http://" .. host.name .. ":8080/'>" .. localpart .. tactical .. "</a>"
|
|
local nodeiface
|
|
local tuniface
|
|
local waniface
|
|
local hidden
|
|
local mycount = 0
|
|
if midcount[ip] then
|
|
mycount = midcount[ip]
|
|
if xlinkcount[ip] then
|
|
mycount = mycount - xlinkcount[ip]
|
|
nodeiface = "xlink*" .. xlinkcount[ip]
|
|
end
|
|
end
|
|
if dtd[ip] then
|
|
mycount = mycount - 1
|
|
end
|
|
if hosts[ip].tactical then
|
|
mycount = mycount - 1
|
|
end
|
|
if mycount > 0 then
|
|
tuniface = "tun*" .. mycount
|
|
end
|
|
if wangateway[ip] then
|
|
waniface = "wan"
|
|
end
|
|
local track = trackers[ip]
|
|
if track and track.hidden then
|
|
hidden = "hidden"
|
|
end
|
|
if nodeiface or waniface then
|
|
c1 = c1 .. " <small>(" .. ac(ac(ac(nodeiface, tuniface), waniface), hidden) .. ")</small>"
|
|
end
|
|
local c3 = string.format("%s", etx)
|
|
|
|
local tbody = "<tbody>"
|
|
if services[host.name] then
|
|
local first = true
|
|
for _, v in pairs(services[host.name])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td>" .. c3 .. "</td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='f'></td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='s f'>" .. c1 .. "</td><td class='f'></td><td>" .. c3 .. "</td><td></td><td class='f'></td></tr>"
|
|
end
|
|
|
|
for _, dmzhost in ipairs(host.hosts)
|
|
do
|
|
local localpart = dmzhost:match("(.*)%.local%.mesh")
|
|
if localpart then
|
|
if services[dmzhost] then
|
|
local first = true
|
|
for _, v in pairs(services[dmzhost])
|
|
do
|
|
if first then
|
|
first = false
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='f'></td><td></td><td></td><td class='s f'>" .. v .. "</td></tr>"
|
|
end
|
|
end
|
|
else
|
|
tbody = tbody .. "<tr class=s><td class='f'></td><td class='s f'>" .. localpart .. "</td><td></td><td></td><td class='f'></td></tr>"
|
|
end
|
|
end
|
|
end
|
|
tbody = tbody .. "</tbody>"
|
|
rows[#rows + 1] = { key = string.format("%05d/%s", math.floor(100 * host.etx), host.name:lower()), row = tbody }
|
|
end
|
|
end
|
|
end
|
|
|
|
if #rows > 0 then
|
|
table.sort(rows, function(a,b) return a.key < b.key end)
|
|
for _, row in ipairs(rows)
|
|
do
|
|
html.print(row.row)
|
|
end
|
|
else
|
|
html.print("<tbody><tr><td class='s f'>none</td></tr></tbody>")
|
|
end
|
|
|
|
-- discard
|
|
rows = nil
|
|
neighbor = nil
|
|
dtd = nil
|
|
midcount = nil
|
|
xlinkcount = nil
|
|
wangateway = nil
|
|
services = nil
|
|
trackers = nil
|
|
links = nil
|
|
ipalias = nil
|
|
hosts = nil
|
|
history = nil
|
|
|
|
-- end remote nodes table
|
|
html.print("</table>")
|
|
|
|
html.print("</center>")
|
|
html.print("</form>")
|
|
html.footer();
|
|
html.print("</body>")
|
|
html.print("</html>")
|