#!/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 . Additional Terms: Additional use restrictions exist on the AREDN(TM) 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") aredn.info = require("aredn.info") aredn.olsr = require("aredn.olsr") require("iwinfo") 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 -- post data if os.getenv("REQUEST_METHOD") == "POST" then require('luci.http') local request = luci.http.Request(nixio.getenv(), function() local v = io.read(1024) if not v then io.close() end return v end ) if request:formvalue("auto") then io.open("/tmp/web/automesh", "w"):close() end if request:formvalue("stop") then os.remove("/tmp/web/automesh") end end local cursor = uci.cursor() local node_desc = cursor:get("system", "@system[0]", "description") local lat_lon = "Location Not Available" 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("
Location: %s %s
", lat, lon) end -- low memory mitigation local lowmemory = cursor:get("aredn", "@meshstatus[0]", "lowmem") if not lowmemory then lowmemory = 10000 end lowmemory = 1024 * tonumber(lowmemory) local lowroutes = cursor:get("aredn", "@meshstatus[0]", "lowroutes") if not lowroutes then lowroutes = 1000 else lowroutes = tonumber(lowroutes) end local routes = {} local links = {} local neighbor = {} local wangateway = {} local ipalias = {} local localhosts = {} local dtd = {} local midcount = {} local hosts = {} local services = {} local history = {} local olsr_total = 0 local olsr_nodes = 0 local olsr_routes = 0 for i, node in ipairs(aredn.olsr.getOLSRRoutes()) do if node.genmask ~= 0 then -- don't count default route olsr_total = olsr_total + 1 if node.genmask ~= 32 then olsr_nodes = olsr_nodes + 1 end if node.etx <= 50 then routes[node.destination] = { etx = node.etx } olsr_routes = olsr_routes + 1 end end end -- low memory route reduction if olsr_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,olsr_routes 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 local prefix = "/sys/kernel/debug/ieee80211/" .. phy .. "/netdev:" .. wifiif .. "/stations/" for i, node in ipairs(aredn.olsr.getOLSRLinks()) do links[node.remoteIP] = { lq = node.linkQuality, nlq = node.neighborLinkQuality, mbps = "" } neighbor[node.remoteIP] = true local mac = arpcache[node.remoteIP] if mac then if not iwrates then iwrates = {} 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 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 if nixio.fs.stat("/var/run/hosts_olsr.stable") then for line in io.lines("/var/run/hosts_olsr.stable") 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 dtd[originator] = true 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 end -- discard routes = nil for line in io.lines("/var/run/services_olsr") 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("%.") 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] = "" .. name .. "" else services[host][name] = name end end end end end end end -- load the node history if nixio.fs.stat("/tmp/node.history") then for line in io.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 ------------------ -- generate page ------------------ http_header() html.header(node .. " mesh status", false) local automesh = nixio.fs.stat("/tmp/web/automesh"); if automesh then html.print("") end html.print([[ ]]) html.print("") html.print("
") html.print("") html.print("
") html.alert_banner() html.print("

" .. node .. " mesh status

") html.print(lat_lon) if node_desc then html.print("
" .. node_desc .. "
") end html.print("
") html.print("Help    ") if nixio.fs.stat("/tmp/web/automesh") then html.print("") else html.print("") html.print("  ") html.print("") end html.print("  ") html.print("") html.print("  ") html.print("") html.print("

") if not next(localhosts) and not next(links) then html.print("No other nodes are available.") html.print("
") html.footer() html.print("") os.exit(0) end -- show local node table html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") 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 .. "   (wan)" end local c2 = "" local c3 = "" if services[host.name] then for _, v in pairs(services[host.name]) do c3 = c3 .. v .. "
" c2 = c2 .. "
" end else c2 = c2 .. "
" c3 = c3 .. "
" end -- add locally advertised dmz hosts 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 not nopropd and not aliased then c2 = c2 .. localpart elseif aliased then c2 = c2 .. "" .. localpart .. "" else c2 = c2 .. "" .. localpart .. "" end if services[dmzhost] then for n, v in pairs(services[dmzhost]) do c3 = c3 .. v .. "
" c2 = c2 .. "
" end else c2 = c2 .. "
" c3 = c3 .. "
" end end end -- Build this row local row = "" rows[#rows + 1] = { key = host.name, row = row } 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("") end -- discard localhosts = nil -- end local node table html.print("
Node NameLAN HostnameService Name
" .. c1 .. "" .. c2 .. "" .. c3 .. "
none
") -- show current neighbors table html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") 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 = "" .. localpart .. tactical .. "" local nodeiface 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 if nodeiface then nodeiface = nodeiface .. ",wan" else nodeiface = "wan" end end if nodeiface then c1 = c1 .. "   (" .. nodeiface .. ")" end local c2 = "" local c3 = string.format("%.0f%%", 100 * link.lq) local c4 = string.format("%.0f%%", 100 * link.nlq) local c5 = string.format("%s", link.mbps) local c6 = "" -- print node services if any if not neighservices[name] then neighservices[name] = true if services[name] then for _, v in pairs(services[name]) do c6 = c6 .. v .. "
" c2 = c2 .. "
" end else c2 = c2 .. "
" c6 = c6 .. "
" end -- add advertised dmz hosts if host then for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") if localpart then c2 = c2 .. localpart if services[dmzhost] then for _, v in pairs(services[dmzhost]) do c6 = c6 .. v .. "
" c2 = c2 .. "
" end else c2 = c2 .. "
" c6 = c6 .. "
" end end end end end -- Build this row local row = "" rows[#rows + 1] = { key = name, row = row } 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 -- discard rows = nil else html.print("") 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 local row = "" rows[#rows + 1] = { key = age, row = row } end end if #rows > 0 then html.print("") 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 -- end current neighbors table html.print("
Current NeighborsLAN HostnameLQNLQTxMbpsService Name
" .. c1 .. "" .. c2 .. "" .. c3 .. "" .. c4 .. "" .. c5 .. "" .. c6 .. "
none
" .. host if hosts[ip] and hosts[ip].hosts then for _, v in ipairs(hosts[ip].hosts) do row = row .. "
" .. v end end row = row .. "
" if age < 3600 then local val = math.floor(age / 60) if val == 1 then row = row .. "1 minute ago" else row = row .. val .. " minutes ago" end else local val = string.format("%.1f", age / 3600) if val == "1.0" then row = row .. "1 hour ago" else row = row .. val .. " hours ago" end end row = row .. "
Previous Neighbors
") -- show remote node table html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") local rows = {} for ip, host in pairs(hosts) do local c1,c2,c3,c4 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) c1 = "" .. localpart .. tactical .. "" local nodeiface local mycount = 0 if midcount[ip] then mycount = midcount[ip] end if dtd[ip] then mycount = mycount - 1 end if hosts[ip].tactical then mycount = mycount - 1 end if mycount > 0 then nodeiface = "tun*" .. mycount end if wangateway[ip] then if nodeiface then nodeiface = nodeiface .. ",wan" else nodeiface = "wan" end end if nodeiface then c1 = c1 .. "   (" .. nodeiface .. ")" end c2 = "
" c3 = string.format("%s", etx) -- print node services if any if services[host.name] then local i=1 for _, v in pairs(services[host.name]) do if c4 then c4 = c4 .. v .. "
" else c4 = v .. "
" end if i > 1 then c2 = c2 .. "
" end i=i+1 end else if c4 then c4 = c4 .. "
" else c4 = "
" end end -- add locally advertised dmz hosts for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") if localpart then c2 = c2 .. localpart .. "
" if services[dmzhost] then local i=1 for _, v in pairs(services[dmzhost]) do if c4 then c4 = c4 .. v .. "
" else c4 = v .. "
" end if i > 1 then c2 = c2 .. "
" end i=i+1 end else if c4 then c4 = c4 .. "
" else c4 = "
" end end end end -- Build this row local row = "" rows[#rows + 1] = { key = host.etx, row = row } 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 -- discard rows = nil else html.print("") end -- discard neighbor = nil dtd = nil midcount = nil wangateway = nil services = nil -- discard links = nil ipalias = nil hosts = nil history = nil -- end remote nodes table html.print("
Remote NodesLAN HostnameETXService Name
" .. c1 .. "" .. c2 .. "" .. c3 .. "" .. c4 .. "
none
") html.print("") html.print("") html.footer(); html.print("") html.print("")