mirror of https://github.com/aredn/aredn.git
346 lines
11 KiB
Lua
Executable File
346 lines
11 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("luci.jsonc")
|
|
require("aredn.http")
|
|
require("aredn.hardware")
|
|
require("aredn.utils")
|
|
local html = require("aredn.html")
|
|
require("aredn.info")
|
|
|
|
local node = aredn.info.get_nvram("node")
|
|
if not node then
|
|
node = "NOCALL"
|
|
end
|
|
local wifiiface = aredn.hardware.get_iface_name("wifi")
|
|
local nf = iwinfo.nl80211.noise(wifiiface) or -95
|
|
local myfreq = tonumber(aredn.info.getFreq(aredn.info.getMeshRadioDevice()))
|
|
|
|
local board_type = aredn.hardware.get_board_type()
|
|
if board_type:match("^ubnt,") and board_type:match("ac") then
|
|
ubnt_ac = true
|
|
end
|
|
|
|
local rescan = string.find((nixio.getenv("QUERY_STRING") or ""):lower(),"rescan=1")
|
|
|
|
local scanlist = {}
|
|
-- Show the last scan if we have one, it's not too old, and we're not rescanning
|
|
if not rescan then
|
|
local lscan = io.open("/tmp/last-scan.json")
|
|
if lscan then
|
|
scanlist = luci.jsonc.parse(lscan:read("*a") or "")
|
|
lscan:close()
|
|
end
|
|
end
|
|
|
|
if rescan then
|
|
local channels = aredn.hardware.get_rfchannels(wifiiface)
|
|
local scan_list = ""
|
|
for _, channel in ipairs(channels)
|
|
do
|
|
scan_list = scan_list .. " " .. channel.frequency
|
|
end
|
|
|
|
-- scan start
|
|
|
|
local scanned = {}
|
|
|
|
local f = io.popen("iw dev " .. wifiiface .. " station dump")
|
|
if f then
|
|
local scan = {}
|
|
local myssid = aredn.info.getSSID()
|
|
for line in f:lines()
|
|
do
|
|
local m = line:match("^Station ([%da-fA-F:]+) %(on " .. wifiiface .. "%)")
|
|
if m then
|
|
scan = scanned[m]
|
|
if not scan then
|
|
scan = {
|
|
mac = m,
|
|
signal = 9999,
|
|
freq = {},
|
|
key = "",
|
|
joined = false
|
|
}
|
|
scanned[m] = scan
|
|
end
|
|
scan.mode = "Connected Ad-Hoc Station"
|
|
scan.ssid = myssid
|
|
scan.freq[tostring(myfreq)] = true
|
|
end
|
|
m = line:match("signal avg:%s+([%d%-]+)")
|
|
if m then
|
|
scan.signal = tonumber(m)
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
|
|
-- Ubiquiti AC device workaround
|
|
if ubnt_ac then
|
|
os.execute("iw dev " .. wifiiface .. " ibss leave > /dev/null 2>&1")
|
|
os.execute("wifi up > /dev/null 2>&1")
|
|
local attempt = 10
|
|
while attempt > 0
|
|
do
|
|
attempt = attempt - 1
|
|
for line in io.popen("iw dev " .. wifiiface .. " scan"):lines()
|
|
do
|
|
if line:match("^BSS ") then
|
|
attempt = 0
|
|
end
|
|
break
|
|
end
|
|
nixio.nanosleep(2, 0)
|
|
end
|
|
end
|
|
|
|
local f = io.popen("iw dev " .. wifiiface .. " scan freq" .. scan_list .. " passive")
|
|
if f then
|
|
local scan = {}
|
|
for line in f:lines()
|
|
do
|
|
local m = line:match("^BSS ([%da-fA-F:]+)")
|
|
if m then
|
|
scan = scanned[m]
|
|
if not scan then
|
|
scan = {
|
|
mac = m,
|
|
mode = "AP",
|
|
ssid = "",
|
|
signal = 9999,
|
|
freq = {},
|
|
key = "",
|
|
joined = false
|
|
}
|
|
scanned[m] = scan
|
|
elseif scan.joined then
|
|
scan = {
|
|
freq = {}
|
|
}
|
|
end
|
|
if line:match("joined") then
|
|
scan.mode = "My Ad-Hoc Network"
|
|
scan.joined = true
|
|
end
|
|
end
|
|
m = line:match("freq: (%d+)")
|
|
if m then
|
|
scan.freq[m] = true
|
|
if tonumber(m) == myfreq and scan.mode == "AP" then
|
|
scan.mode = "My Ad-Hoc Network"
|
|
scan.joined = true
|
|
end
|
|
end
|
|
m = line:match("SSID: (.+)")
|
|
if m then
|
|
scan.ssid = m
|
|
end
|
|
m = line:match("signal: ([%d%-]+)")
|
|
if m then
|
|
scan.signal = tonumber(m)
|
|
end
|
|
m = line:match("Group cipher: (.+)")
|
|
if m then
|
|
scan.key = m
|
|
end
|
|
if line:match("capability: IBSS") and scan.mode == "AP" then
|
|
scan.mode = "Foreign Ad-Hoc Network"
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
|
|
-- scan end
|
|
|
|
-- load arp cache
|
|
local arpcache = {}
|
|
arptable(function(a)
|
|
arpcache[a["HW address"]] = a["IP address"]
|
|
end)
|
|
|
|
scanlist = {}
|
|
for _, v in pairs(scanned)
|
|
do
|
|
if v.signal ~= 9999 or v.joined then
|
|
scanlist[#scanlist + 1] = v
|
|
end
|
|
end
|
|
table.sort(scanlist, function(a, b) return a.signal > b.signal end)
|
|
for _, scan in ipairs(scanlist)
|
|
do
|
|
-- freq to chan
|
|
local chan = {}
|
|
for f, _ in pairs(scan.freq)
|
|
do
|
|
f = tonumber(f)
|
|
if f < 256 then
|
|
elseif f == 2484 then
|
|
chan[#chan + 1] = 14
|
|
elseif f == 2407 then
|
|
chan[#chan + 1] = 0
|
|
elseif f < 2484 then
|
|
chan[#chan + 1] = (f - 2407) / 5
|
|
elseif f < 5000 then
|
|
elseif f < 5380 then
|
|
chan[#chan + 1] = (f - 5000) / 5
|
|
elseif f < 5500 then
|
|
chan[#chan + 1] = f - 2000
|
|
elseif f < 6000 then
|
|
chan[#chan + 1] = (f - 5000) / 5
|
|
end
|
|
end
|
|
table.sort(chan)
|
|
scan.chan = table.concat(chan, " ")
|
|
if scan.joined then
|
|
scan.hostname = node
|
|
else
|
|
-- ip lookup then host lookup
|
|
local ip = arpcache[scan.mac]
|
|
if ip then
|
|
scan.hostname = ip
|
|
local f = io.popen("nslookup " .. ip)
|
|
if f then
|
|
for line in f:lines()
|
|
do
|
|
local m = line:match("name = (.*)%.local%.mesh")
|
|
if m then
|
|
scan.hostname = m:gsub("^mid[0-9]*%.",""):gsub("^dtdlink%.",""):gsub("%.local%.mesh$","")
|
|
break
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
else
|
|
scan.hostname = "-"
|
|
end
|
|
end
|
|
end
|
|
lscan = io.open("/tmp/last-scan.json", "w")
|
|
if lscan then
|
|
lscan:write(luci.jsonc.stringify(scanlist, true))
|
|
lscan:close()
|
|
end
|
|
end
|
|
|
|
-- generate page
|
|
http_header()
|
|
html.header(node .. " WiFi scan", false)
|
|
local autoscan = string.find((nixio.getenv("QUERY_STRING") or ""):lower(),"autoscan=1")
|
|
if autoscan then
|
|
html.print("<script>setTimeout(function(){ window.location.reload(); }, 10000);</script>")
|
|
end
|
|
if rescan then
|
|
html.print([[
|
|
<script>
|
|
if (history.replaceState) {
|
|
history.replaceState(null, "", window.location.origin + window.location.pathname)
|
|
}
|
|
</script>
|
|
]])
|
|
end
|
|
html.print([[
|
|
<script src="/js/sorttable-min.js"></script>
|
|
<style>
|
|
table {
|
|
border-collapse:collapse;
|
|
}
|
|
table.sortable thead {
|
|
background-color:#eee;
|
|
color:#666666;
|
|
font-weight: bold;
|
|
cursor: default;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<center>
|
|
]])
|
|
|
|
html.alert_banner()
|
|
html.print("<h1>" .. node .. " WiFi scan</h1><hr>")
|
|
|
|
if autoscan then
|
|
html.print("<input type=button name=stop value=Stop title='Abort continuous scan' onclick='window.location = window.location.origin + window.location.pathname'>")
|
|
else
|
|
html.print([[<input type=button name=refresh value=Rescan title='Run a new scan' onclick='window.location = window.location.origin + window.location.pathname + "?rescan=1"'>]])
|
|
if not ubnt_ac then
|
|
html.print(" ")
|
|
html.print([[<input type=button name=auto value=Auto title='Begin continuous scan' onclick='window.location = window.location.origin + window.location.pathname + "?autoscan=1"'>]])
|
|
end
|
|
end
|
|
|
|
html.print(" ")
|
|
html.print("<button type=button onClick='window.location=\"status\"' title='Return to status page'>Quit</button><br><br>")
|
|
|
|
-- display scan
|
|
html.print("<table class=sortable border=1 cellpadding=5>")
|
|
html.print("<tr><th>SNR</th><th>Signal</th><th>Chan</th><th>Enc</th><th>SSID</th><th>Hostname</th><th>MAC/BSSID</th><th>802.11 Mode</th></tr>")
|
|
|
|
for _, scan in ipairs(scanlist)
|
|
do
|
|
if scan.ssid:match("^AREDN-") then
|
|
html.print("<tr class=\"wscan-row-node\">")
|
|
else
|
|
html.print("<tr>")
|
|
end
|
|
html.print("<td>" .. (scan.signal - nf) .. "</td><td>" .. scan.signal .. "</td><td>" .. scan.chan .. "</td><td>" .. scan.key .. "</td><td>" .. scan.ssid .. "</td><td align=center>" .. scan.hostname .. "</td><td>" .. scan.mac:upper() .. "</td><td>" .. scan.mode .. "</td>")
|
|
html.print("</tr>")
|
|
end
|
|
|
|
html.print("</table><br>")
|
|
local lastscan = nixio.fs.stat("/tmp/last-scan.json", "mtime")
|
|
if lastscan then
|
|
lastscan = os.time() - lastscan
|
|
if lastscan == 1 then
|
|
html.print("<div>Last scan: 1 second ago")
|
|
elseif lastscan < 60 then
|
|
html.print("<div>Last scan: " .. lastscan .. " seconds ago")
|
|
elseif lastscan < 120 then
|
|
html.print("<div>Last scan: 1 minute ago")
|
|
elseif lastscan < 3600 then
|
|
html.print("<div>Last scan: " .. math.floor(lastscan / 60) .. " minutes ago")
|
|
else
|
|
html.print("<div>Last scan: a long time ago")
|
|
end
|
|
else
|
|
html.print("<div>Last scan: none")
|
|
end
|
|
html.footer()
|
|
html.print("</center></body></html>")
|