mirror of https://github.com/aredn/aredn.git
984 lines
36 KiB
Lua
Executable File
984 lines
36 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(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")
|
|
aredn.html = require("aredn.html")
|
|
require("uci")
|
|
aredn.info = require("aredn.info")
|
|
require("ubus")
|
|
require("luci.jsonc")
|
|
|
|
local html = aredn.html
|
|
|
|
local cursor = uci.cursor()
|
|
local conn = ubus.connect()
|
|
|
|
-- handle firmware updates
|
|
local fw_install = false
|
|
local fw_output = {}
|
|
local fw_versions = {}
|
|
local fw_names = {}
|
|
local fw_version = ""
|
|
|
|
function fwout(msg)
|
|
fw_output[#fw_output + 1] = msg
|
|
end
|
|
|
|
function get_default_gw()
|
|
-- a node with a wired default gw will route via this
|
|
local p = io.popen("ip route list table 254")
|
|
if p then
|
|
for line in p:lines()
|
|
do
|
|
local gw = line:match("^default%svia%s([%d%.]+)")
|
|
if gw then
|
|
p:close()
|
|
return gw
|
|
end
|
|
end
|
|
p:close()
|
|
end
|
|
-- table 31 is populated by OLSR
|
|
p = io.popen("ip route list table 31")
|
|
if p then
|
|
for line in p:lines()
|
|
do
|
|
local gw = line:match("^default%svia%s([%d%.]+)")
|
|
if gw then
|
|
p:close()
|
|
return gw
|
|
end
|
|
end
|
|
p:close()
|
|
end
|
|
return "none"
|
|
end
|
|
|
|
function reboot()
|
|
local node = aredn.info.get_nvram("node")
|
|
if node == "" then
|
|
node = "Node"
|
|
end
|
|
local lanip, _, lanmask = aredn.hardware.get_interface_ip4(aredn.hardware.get_iface_name("lan"))
|
|
local browser = os.getenv("REMOTE_ADDR")
|
|
local browser6 = browser:match("::ffff:([%d%.]+)")
|
|
if browser6 then
|
|
browser = browser6
|
|
end
|
|
local fromlan = false
|
|
local subnet_change = false
|
|
if lanip then
|
|
fromlan = validate_same_subnet(browser, lanip, lanmask)
|
|
if fromlan then
|
|
lanmask = ip_to_decimal(lanmask)
|
|
local cfgip = cursor:get("network", "lan", "ipaddr")
|
|
local cfgmask = ip_to_decimal(cursor:get("network", "lan", "netmask"))
|
|
if lanmask ~= cfgmask or nixio.bit.band(ip_to_decimal(lanip), lanmask) ~= nixio.bit.band(ip_to_decimal(cfgip), cfgmask) then
|
|
subnet_change = true
|
|
end
|
|
end
|
|
end
|
|
http_header()
|
|
if fromlan and subnet_change then
|
|
html.header(node .. " rebooting", true)
|
|
html.print("<body><center>")
|
|
html.print("<h1>" .. node .. " is rebooting</h1><br>")
|
|
html.print("<h3>The LAN subnet has changed. You will need to acquire a new DHCP lease<br>")
|
|
html.print("and reset any name service caches you may be using.</h3><br>")
|
|
html.print("<h3>When the node reboots you get your new DHCP lease and reconnect with<br>")
|
|
html.print("<a href='http://localnode.local.mesh:8080/'>http://localnode.local.mesh:8080/</a><br>or<br>")
|
|
html.print("<a href='http://" .. node .. ".local.mesh:8080/'>http://" .. node .. ".local.mesh:8080/</a></h3>")
|
|
else
|
|
html.header(node .. " rebooting", false)
|
|
html.print("<meta http-equiv='refresh' content='60;url=/cgi-bin/status'>")
|
|
html.print("</head><body><center>")
|
|
html.print("<h1>" .. node .. " is rebooting</h1><br>")
|
|
html.print("<h3>Your browser should return to this node in 60 seconds.</br><br>")
|
|
html.print("If something goes astray you can try to connect with<br><br>")
|
|
html.print("<a href='http://localnode.local.mesh:8080/'>http://localnode.local.mesh:8080/</a><br>")
|
|
if node ~= "Node" then
|
|
html.print("or<br><a href='http://" .. node .. ".local.mesh:8080/'>http://" .. node .. ".local.mesh:8080/</a></h3>")
|
|
end
|
|
end
|
|
html.print("</center></body></html>")
|
|
http_footer()
|
|
os.execute("reboot >/dev/null 2>&1")
|
|
os.exit()
|
|
end
|
|
|
|
function word_wrap(len, lines)
|
|
local output = ""
|
|
for _, str in ipairs(lines)
|
|
do
|
|
while #str > len
|
|
do
|
|
local str1 = str:sub(1, len)
|
|
local str2 = str:sub(len + 1)
|
|
local m, x = str1:match("^(.*)%s(%S+)$")
|
|
if m then
|
|
output = output .. m .. "\n"
|
|
str = x .. str2
|
|
else
|
|
output = output .. str1 .. "\n"
|
|
str = str2
|
|
end
|
|
end
|
|
output = output .. str .. "\n"
|
|
end
|
|
return output:sub(1, #output - 1)
|
|
end
|
|
|
|
-- read_postdata
|
|
local parms = {}
|
|
local firmfile = ""
|
|
if os.getenv("REQUEST_METHOD") == "POST" then
|
|
require('luci.ohttp')
|
|
local request = luci.http.Request(nixio.getenv(),
|
|
function()
|
|
local v = io.read(1024)
|
|
if not v then
|
|
io.close()
|
|
end
|
|
return v
|
|
end
|
|
)
|
|
-- only allow file uploading without active tunnels
|
|
local fp
|
|
request:setfilehandler(
|
|
function(meta, chunk, eof)
|
|
if not fp then
|
|
if meta and meta.file then
|
|
firmfile = meta.file
|
|
if firmfile:match("sysupgrade%.bin$") or firmfile:match("ext4%-combined%.img%.gz$") then
|
|
-- Uploading a system upgrade - clear out memory early
|
|
os.execute("/usr/local/bin/upgrade_prepare.sh stop > /dev/null 2>&1")
|
|
end
|
|
end
|
|
nixio.fs.mkdir("/tmp/web/upload")
|
|
fp = io.open("/tmp/web/upload/file", "w")
|
|
end
|
|
if chunk then
|
|
fp:write(chunk)
|
|
end
|
|
if eof then
|
|
fp:close()
|
|
end
|
|
end
|
|
)
|
|
parms = request:formvalue()
|
|
end
|
|
|
|
if parms.button_reboot then
|
|
reboot()
|
|
end
|
|
local node = aredn.info.get_nvram("node")
|
|
local tmpdir = "/tmp/web/admin"
|
|
nixio.fs.mkdir("/tmp/web")
|
|
nixio.fs.mkdir("/tmp/web/admin")
|
|
|
|
-- set the wget command options
|
|
local wget = "wget -U 'node: " .. node .. "' "
|
|
|
|
local serverpaths = {}
|
|
local uciserverpath = cursor:get("aredn", "@downloads[0]", "firmwarepath")
|
|
if not uciserverpath then
|
|
uciserverpath = ""
|
|
end
|
|
serverpaths[#serverpaths + 1] = uciserverpath
|
|
|
|
local hardwaretype = aredn.hardware.get_type()
|
|
local targettype = conn:call("system", "board", {}).release.target
|
|
|
|
local local_firmware = "local_firmware.bin"
|
|
local firmware_type = "bin"
|
|
if targettype:match("x86") then
|
|
local_firmware = "local_firmware.img.gz"
|
|
firmware_type = "gz"
|
|
end
|
|
|
|
-- handle TPLink and Mikrotik exception conditions
|
|
local mfg = aredn.hardware.get_manufacturer()
|
|
local mfgprefix = ""
|
|
if mfg:match("[Uu]biquiti") then
|
|
mfgprefix = "ubnt"
|
|
elseif mfg:match("[Mm]ikro[Tt]ik") then
|
|
mfgprefix = "mikrotik"
|
|
elseif mfg:match("[Tt][Pp]-[Ll]ink") then
|
|
mfgprefix = "cpe"
|
|
end
|
|
|
|
-- refresh fw
|
|
if parms.button_refresh_fw then
|
|
nixio.fs.remove("/tmp/web/firmware.list")
|
|
if get_default_gw() ~= "none" or uciserverpath:match("%.local%.mesh") then
|
|
fwout("Downloading firmware list from " .. uciserverpath .. "...")
|
|
local config_versions
|
|
local config_serverpath
|
|
for _, serverpath in ipairs(serverpaths)
|
|
do
|
|
config_serverpath = serverpath:match("^(.*)/firmware") .. "/afs/www/"
|
|
for line in io.popen(wget .. " -O - " .. config_serverpath .. "config.js 2> /dev/null"):lines()
|
|
do
|
|
local v = line:match("versions: {(.+)}")
|
|
if v then
|
|
config_versions = v
|
|
break
|
|
end
|
|
end
|
|
if config_versions then
|
|
break
|
|
end
|
|
end
|
|
if not config_versions then
|
|
fwout("Failed to find firmware versions")
|
|
else
|
|
local firmware_versions = {}
|
|
for k, v in config_versions:gmatch("'([^']+)': '([^']+)'")
|
|
do
|
|
firmware_versions[k] = v
|
|
end
|
|
local board_type = aredn.hardware.get_board_type():gsub(",", "_")
|
|
for ver, data in pairs(firmware_versions)
|
|
do
|
|
local raw = io.popen(wget .. " -O - " .. config_serverpath .. data .. "/overview.json 2> /dev/null")
|
|
local info = luci.jsonc.parse(raw:read("*a") or "")
|
|
raw:close()
|
|
firmware_versions[ver] = nil
|
|
if info then
|
|
for _, profile in ipairs(info.profiles)
|
|
do
|
|
if profile.id == board_type or (board_type == "qemu-standard-pc-i440fx-piix-1996" and profile.id == "generic" and profile.target == "x86/64") then
|
|
firmware_versions[ver] = {
|
|
overview = config_serverpath .. data .. "/" .. profile.target .. "/" .. profile.id .. ".json",
|
|
target = info.image_url:gsub("{target}", profile.target)
|
|
}
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local f = io.open("/tmp/web/firmware.list", "w")
|
|
if f then
|
|
f:write(luci.jsonc.stringify(firmware_versions, true))
|
|
f:close()
|
|
end
|
|
fwout("Done")
|
|
end
|
|
else
|
|
fwout("Error: no route to Host")
|
|
end
|
|
end
|
|
|
|
-- generate data structures
|
|
-- and set fw_version
|
|
--firmware_list_gen()
|
|
if nixio.fs.stat("/etc/mesh-release") then
|
|
for line in io.lines("/etc/mesh-release")
|
|
do
|
|
fw_version = line:chomp()
|
|
break
|
|
end
|
|
end
|
|
local f = io.open("/tmp/web/firmware.list")
|
|
if f then
|
|
fw_versions = luci.jsonc.parse(f:read("*a") or "")
|
|
f:close()
|
|
if fw_versions then
|
|
for v, d in pairs(fw_versions)
|
|
do
|
|
fw_names[#fw_names + 1] = v
|
|
end
|
|
-- Sort in version number order (newest at the top) but with nightlies at the bottom
|
|
table.sort(fw_names, function (a, b)
|
|
if a:match("%-") then
|
|
return false
|
|
elseif b:match("%-") then
|
|
return true
|
|
end
|
|
local ai = a:gmatch("(%d+)")
|
|
local bi = b:gmatch("(%d+)")
|
|
while true
|
|
do
|
|
local va = tonumber(ai() or nil)
|
|
local vb = tonumber(bi() or nil)
|
|
if not va then
|
|
return false
|
|
elseif not vb then
|
|
return true
|
|
elseif va < vb then
|
|
return false
|
|
elseif va > vb then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- sideload fw
|
|
if parms.button_apply_fw and nixio.fs.stat("/tmp/web/" .. local_firmware) then
|
|
nixio.fs.mkdir("/tmp/web/upload")
|
|
os.execute("mv -f /tmp/web/" .. local_firmware .. " /tmp/web/upload/file")
|
|
if firmware_type == "gz" then
|
|
firmfile = "arednmesh-sideload-ext4-combined.img.gz"
|
|
else
|
|
firmfile = "arednmesh-sideload-sysupgrade.bin"
|
|
end
|
|
parms.button_ul_fw = "Upload"
|
|
os.execute("/usr/local/bin/upgrade_prepare.sh stop > /dev/null 2>&1")
|
|
end
|
|
|
|
-- upload fw
|
|
if parms.button_ul_fw and nixio.fs.stat("/tmp/web/upload/file") then
|
|
os.execute("mv -f /tmp/web/upload/file " .. tmpdir .. "/firmware")
|
|
if firmware_type == "bin" and firmfile:match("sysupgrade%.bin$") then -- full firmware
|
|
fw_install = true
|
|
-- drop the page cache to take pressure off tmps when checking the firmware
|
|
write_all("/proc/sys/vm/drop_caches", "3")
|
|
-- check firmware header
|
|
local fcheck = capture("/usr/local/bin/firmwarecheck.sh " .. tmpdir .. "/firmware")
|
|
if fcheck ~= "" then
|
|
fwout("Firmware CANNOT be updated")
|
|
fwout("firmware file is not valid: " .. fcheck)
|
|
fw_install = false
|
|
nixio.fs.remove(tmpdir .. "/firmware")
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
fwout("Failed to restart all services, please reboot this node.")
|
|
end
|
|
end
|
|
elseif firmware_type == "gz" and firmfile:match("ext4%-combined%.img%.gz$") then -- full x86 firmware
|
|
fw_install = true
|
|
else
|
|
fwout("Firmware CANNOT be updated")
|
|
fwout("the uploaded file is not recognized")
|
|
nixio.fs.remove(tmpdir .. "/firmware")
|
|
os.execute("/usr/local/bin/upgrade_prepare.sh restore > /dev/null 2>&1")
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
fwout("Failed to restart all services, please reboot this node.")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- download fw
|
|
if parms.button_dl_fw and parms.dl_fw ~= "default" then
|
|
if get_default_gw() ~= "none" or uciserverpath:match("%.local%.mesh") then
|
|
nixio.fs.remove(tmpdir .. "/firmware")
|
|
os.execute("/usr/local/bin/uploadctlservices update > /dev/null 2>&1")
|
|
os.execute("/usr/local/bin/upgrade_prepare.sh stop > /dev/null 2>&1")
|
|
|
|
fw_install = false
|
|
local err
|
|
|
|
local f = io.popen(wget .. " -O - " .. fw_versions[parms.dl_fw].overview .. " 2> /dev/null")
|
|
local fwinfo = luci.jsonc.parse(f:read("*a") or "")
|
|
f:close()
|
|
|
|
if fwinfo then
|
|
local fwimage
|
|
for _, image in ipairs(fwinfo.images)
|
|
do
|
|
if (firmware_type == "bin" and (image.type == "sysupgrade" or image.type == "nand-sysupgrade")) or (firmware_type == "gz" and image.type == "combined") then
|
|
fwimage = {
|
|
url = fw_versions[parms.dl_fw].target .. "/" .. image.name,
|
|
sha = image.sha256
|
|
}
|
|
break
|
|
end
|
|
end
|
|
if fwimage then
|
|
if os.execute(wget .. "-O " .. tmpdir .. "/firmware " .. fwimage.url .. " > /dev/null 2>>" .. tmpdir .. "/wget.err") ~= 0 then
|
|
err = "Download failed!\n" .. read_all(tmpdir .. "/wget.err")
|
|
else
|
|
local sha = capture("sha256sum " .. tmpdir .. "/firmware"):match("^(%S+)")
|
|
if sha ~= fwimage.sha then
|
|
err = "firmware file checksum failed"
|
|
else
|
|
fw_install = true
|
|
end
|
|
end
|
|
else
|
|
err = "upgrade is not available"
|
|
end
|
|
else
|
|
err = "the downloaded file cannot be found"
|
|
end
|
|
|
|
nixio.fs.remove(tmpdir .. "/wget.err")
|
|
|
|
if err then
|
|
fwout("Firmware CANNOT be updated")
|
|
fwout(err)
|
|
os.execute("/usr/local/bin/upgrade_prepare.sh restore > /dev/null 2>&1")
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
fwout("Failed to restart all services, please reboot this node.")
|
|
end
|
|
nixio.fs.remove(tmpdir .. "/firmware")
|
|
end
|
|
else
|
|
fwout("Error: no route to Host")
|
|
nixio.fs.remove(tmpdir .. "/wget.err")
|
|
end
|
|
end
|
|
|
|
-- install fw
|
|
if fw_install and nixio.fs.stat(tmpdir .. "/firmware") then
|
|
http_header()
|
|
html.print("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">")
|
|
html.print("<html>")
|
|
html.print("<head>")
|
|
html.print("<title>FIRMWARE UPDATE IN PROGRESS</title>")
|
|
html.print("<meta http-equiv='expires' content='0'>")
|
|
html.print("<meta http-equiv='cache-control' content='no-cache'>")
|
|
html.print("<meta http-equiv='pragma' content='no-cache'>")
|
|
html.print("<meta name='robots' content='noindex'>")
|
|
if not nixio.fs.readlink("/tmp/web/style.css") then
|
|
if not nixio.fs.stat("/tmp/web") then
|
|
nixio.fs.mkdir("/tmp/web")
|
|
end
|
|
nixio.fs.symlink("/www/aredn.css", "/tmp/web/style.css")
|
|
end
|
|
html.print("<style>")
|
|
html.print(read_all("/tmp/web/style.css"))
|
|
html.print("</style>")
|
|
html.print("<meta http-equiv='refresh' content='300;URL=http://" .. node .. ".local.mesh:8080'>")
|
|
html.print("</head>")
|
|
html.print("<body><center>")
|
|
html.print("<h2>The firmware is being updated.</h2>")
|
|
html.print("<h1>DO NOT REMOVE POWER UNTIL UPDATE IS FINISHED</h1>")
|
|
html.print("</center><br>")
|
|
local upgradecmd = nil
|
|
if parms.checkbox_keep_settings then
|
|
local fin = io.open("/etc/arednsysupgrade.conf", "r")
|
|
if fin then
|
|
local fout = io.open("/tmp/sysupgradefilelist", "w")
|
|
if fout then
|
|
for line in fin:lines()
|
|
do
|
|
if not line:match("^#") and nixio.fs.stat(line) then
|
|
fout:write(line .. "\n")
|
|
end
|
|
end
|
|
fout:close()
|
|
fin:close()
|
|
aredn.info.set_nvram("nodeupgraded", "1")
|
|
if os.execute("tar -czf /tmp/arednsysupgradebackup.tgz -T /tmp/sysupgradefilelist > /dev/null 2>&1") ~= 0 then
|
|
html.print([[
|
|
<center><h2>ERROR: Could not backup filesystem.</h2>
|
|
<h3>An error occured trying to backup the file system. Node will now reboot.
|
|
</center>
|
|
]])
|
|
html.footer()
|
|
html.print("</body></html>")
|
|
http_footer()
|
|
aredn.info.set_nvram("nodeupgraded", "0")
|
|
os.execute("reboot >/dev/null 2>&1")
|
|
else
|
|
html.print([[
|
|
<center><h2>Firmware will be written in the background.</h2>
|
|
<h3>If your computer is connected to the LAN of this node you may need to acquire<br>
|
|
a new IP address and reset any name service caches you may be using.</h3>
|
|
<h3>The node will reboot twice while the configuration is applied<br>
|
|
When the node has finished booting you should ensure your computer has<br>
|
|
received a new IP address and reconnect with<br>
|
|
<a href='http://]] .. node .. [[.local.mesh:8080/'>http://]] .. node .. [[.local.mesh:8080/</a><br>
|
|
(This page will automatically reload in 5 minutes)</h3>
|
|
</center></body></html>
|
|
]])
|
|
http_footer()
|
|
nixio.fs.remove("/tmp/sysupgradefilelist")
|
|
upgradecmd = "/usr/local/bin/aredn_sysupgrade -f /tmp/arednsysupgradebackup.tgz -q " .. tmpdir .. "/firmware 2>&1 &"
|
|
end
|
|
else
|
|
fin:close()
|
|
html.footer()
|
|
html.print("</body></html>")
|
|
http_footer()
|
|
end
|
|
else
|
|
html.print([[
|
|
<center><h2>ERROR: Failed to create backup.</h2>
|
|
<h3>An error occured trying to backup the file system. Node will now reboot.
|
|
</center>
|
|
]])
|
|
html.footer()
|
|
html.print("</body></html>")
|
|
http_footer()
|
|
os.execute("reboot >/dev/null 2>&1")
|
|
end
|
|
else
|
|
html.print([[
|
|
<center><h2>Firmware will be written in the background.</h2>
|
|
<h3>If your computer is connected to the LAN of this node you may need to acquire<br>
|
|
a new IP address and reset any name service caches you may be using.</h3>
|
|
<h3>The node will reboot after the firmware has been written to flash memory<br>
|
|
When the node has finished booting you should ensure your computer has<br>
|
|
received a new IP address and reconnect with<br>
|
|
<a href='http://localnode.local.mesh:8080/'>http://192.168.1.1:8080/</a><br>
|
|
and continue setup of the node in firstboot state.<br>
|
|
(This page will automatically reload in 5 minutes)</h3>
|
|
</center></body></html>
|
|
]])
|
|
http_footer()
|
|
upgradecmd = "/usr/local/bin/aredn_sysupgrade -q -n " .. tmpdir .. "/firmware 2>&1 &"
|
|
end
|
|
if upgradecmd then
|
|
os.execute(upgradecmd)
|
|
end
|
|
os.exit()
|
|
end
|
|
|
|
-- handle package actions
|
|
local pkg_output = {}
|
|
function pkgout(msg)
|
|
pkg_output[#pkg_output + 1] = msg
|
|
end
|
|
|
|
local permpkg = {}
|
|
if nixio.fs.stat("/etc/permpkg") then
|
|
for line in io.lines("/etc/permpkg")
|
|
do
|
|
if not line:match("^#") then
|
|
permpkg[line] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
local meshpkgs = capture("grep -q \".local.mesh\" /etc/opkg/distfeeds.conf"):chomp()
|
|
|
|
function update_advertised_services()
|
|
local script = "/etc/cron.hourly/check-services"
|
|
local stat = nixio.fs.stat(script)
|
|
if stat.type == "reg" and nixio.bit.band(tonumber(stat.modedec, 8), tonumber(111, 8)) ~= 0 then
|
|
os.execute("(cd /tmp;" .. script .. " 2>&1 | logger -p daemon.debug -t " .. script .. ")&")
|
|
end
|
|
end
|
|
|
|
local package_store = "/etc/package_store"
|
|
local package_catalog = package_store .. "/catalog.json"
|
|
|
|
function record_package(op, ipkg, file)
|
|
nixio.fs.mkdir(package_store)
|
|
local catalog = luci.jsonc.parse(read_all(package_catalog) or '{"installed":{}}')
|
|
if op == "upload" then
|
|
local package = ipkg:match("^([^_]+)")
|
|
if package then
|
|
os.execute("/bin/cp -f " .. file .. " " .. package_store .. "/" .. package .. ".ipk")
|
|
catalog.installed[package] = "local"
|
|
end
|
|
elseif op == "download" then
|
|
local result = capture("opkg status " .. ipkg .. " 2>&1")
|
|
local package = result:match("Package: (%S+)")
|
|
if package then
|
|
catalog.installed[package] = "global"
|
|
end
|
|
elseif op == "remove" then
|
|
nixio.fs.remove(package_store .. "/" .. ipkg .. ".ipk")
|
|
catalog.installed[ipkg] = nil
|
|
end
|
|
os.remove(package_catalog)
|
|
for _,_ in pairs(catalog.installed)
|
|
do
|
|
write_all(package_catalog, luci.jsonc.stringify(catalog))
|
|
break
|
|
end
|
|
end
|
|
|
|
-- upload package
|
|
if parms.button_ul_pkg and nixio.fs.stat("/tmp/web/upload/file") then
|
|
os.execute("mv -f /tmp/web/upload/file /tmp/web/upload/newpkg.ipk")
|
|
os.execute("/usr/local/bin/uploadctlservices opkginstall > /dev/null 2>&1")
|
|
local result = capture("opkg -force-overwrite install /tmp/web/upload/newpkg.ipk 2>&1")
|
|
if result:match("satisfy_dependencies_for:") or result:match("cannot find dependency") then
|
|
-- dependency failure - silently update dependencies and try again
|
|
os.execute("opkg update > /dev/null 2>&1")
|
|
os.execute("opkg list | grep -v '^ ' | cut -f1,3 -d' ' | gzip -c > /etc/opkg.list.gz")
|
|
result = capture("opkg -force-overwrite install /tmp/web/upload/newpkg.ipk 2>&1")
|
|
end
|
|
pkgout(result)
|
|
record_package("upload", firmfile, "/tmp/web/upload/newpkg.ipk")
|
|
os.execute("rm -rf /tmp/opkg-*")
|
|
nixio.fs.remove("/tmp/web/upload/newpkg.ipk")
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
pkgout("Failed to restart all services, please reboot this node.")
|
|
else
|
|
update_advertised_services()
|
|
end
|
|
end
|
|
|
|
-- download package
|
|
if parms.button_dl_pkg and parms.dl_pkg ~= "default" then
|
|
if get_default_gw() ~= "none" or meshpkgs ~= "" then
|
|
os.execute("/usr/local/bin/uploadctlservices opkginstall > /dev/null 2>&1")
|
|
local result = capture("opkg -force-overwrite install " .. parms.dl_pkg .. " 2>&1")
|
|
if result:match("satisfy_dependencies_for:") or result:match("cannot find dependency") then
|
|
-- dependency failure - silently update dependencies and try again
|
|
os.execute("opkg update > /dev/null 2>&1")
|
|
os.execute("opkg list | grep -v '^ ' | cut -f1,3 -d' ' | gzip -c > /etc/opkg.list.gz")
|
|
result = capture("opkg -force-overwrite install " .. parms.dl_pkg .. " 2>&1")
|
|
end
|
|
pkgout(result)
|
|
record_package("download", parms.dl_pkg)
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
pkgout("Failed to restart all services, please reboot this node.")
|
|
else
|
|
update_advertised_services()
|
|
end
|
|
else
|
|
pkgout("Error: no route to Host")
|
|
end
|
|
end
|
|
|
|
-- refresh package list
|
|
if parms.button_refresh_pkg then
|
|
if get_default_gw() ~= "none" or meshpkgs ~= "" then
|
|
pkgout(capture("opkg update 2>&1"))
|
|
os.execute("opkg list | grep -v '^ ' | cut -f1,3 -d' ' | gzip -c > /etc/opkg.list.gz")
|
|
else
|
|
pkgout("Error: no route to Host")
|
|
end
|
|
end
|
|
|
|
-- remove package
|
|
if parms.button_rm_pkg and parms.rm_pkg ~= "default" and not permpkg[parms.rm_pkg] then
|
|
local output = capture("opkg remove " .. parms.rm_pkg .. " 2>&1")
|
|
pkgout(output)
|
|
if not output:match("No packages removed") then
|
|
pkgout("Package removed successfully")
|
|
update_advertised_services()
|
|
record_package("remove", parms.rm_pkg)
|
|
end
|
|
end
|
|
|
|
-- generate support data file
|
|
if parms.button_support_data then
|
|
os.execute("/www/cgi-bin/supporttool")
|
|
end
|
|
|
|
-- generate data structures
|
|
|
|
local pkgs = {}
|
|
local pkgver = {}
|
|
local f = io.popen("opkg list_installed | cut -f1,3 -d' '")
|
|
if f then
|
|
for line in f:lines()
|
|
do
|
|
local pkg, ver = line:match("(.+)%s(.+)")
|
|
if ver then
|
|
pkgs[#pkgs + 1] = pkg
|
|
pkgver[pkg] = ver
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
|
|
local dl_pkgs = {}
|
|
local dlpkgver = {}
|
|
if nixio.fs.stat("/etc/opkg.list.gz") then
|
|
local f = io.popen("zcat /etc/opkg.list.gz")
|
|
if f then
|
|
for line in f:lines()
|
|
do
|
|
local pkg, ver = line:match("(.+)%s(.+)")
|
|
if ver and not (pkgver[pkg] and pkgver[pkg] == ver) then
|
|
dl_pkgs[#dl_pkgs + 1] = pkg
|
|
dlpkgver[pkg] = ver
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
end
|
|
|
|
-- handle ssh key actions
|
|
|
|
local key_output = {}
|
|
function keyout(msg)
|
|
key_output[#key_output + 1] = msg
|
|
end
|
|
|
|
local keyfile = "/etc/dropbear/authorized_keys"
|
|
|
|
-- upload key
|
|
if parms.button_ul_key and nixio.fs.stat("/tmp/web/upload/file") then
|
|
local count = 0
|
|
if nixio.fs.stat(keyfile) then
|
|
for _ in io.lines(keyfile)
|
|
do
|
|
count = count + 1
|
|
end
|
|
end
|
|
os.execute("grep ^ssh- /tmp/web/upload/file >> " .. keyfile)
|
|
local count = 0
|
|
for _ in io.lines(keyfile)
|
|
do
|
|
count = count - 1
|
|
end
|
|
if count == 0 then
|
|
keyout("Error: file does not appear to be an ssh key file")
|
|
keyout("Authorized keys not changed.")
|
|
else
|
|
keyout("Key installed.")
|
|
end
|
|
nixio.fs.remove("/tmp/web/upload/file")
|
|
if os.execute("/usr/local/bin/uploadctlservices restore > /dev/null 2>&1") ~= 0 then
|
|
keyout("Failed to restart all services, please reboot this node.")
|
|
end
|
|
end
|
|
|
|
-- remove key
|
|
if parms.button_rm_key and parms.rm_key ~= "default" and nixio.fs.stat(keyfile) then
|
|
local count = 0
|
|
for _ in io.lines(keyfile)
|
|
do
|
|
count = count + 1
|
|
end
|
|
os.execute("grep -v '" .. parms.rm_key .. "' " .. keyfile .. " > " .. tmpdir .. "/keys")
|
|
os.execute("mv -f " .. tmpdir .. "/keys " .. keyfile)
|
|
for _ in io.lines(keyfile)
|
|
do
|
|
count = count - 1
|
|
end
|
|
if count == 0 then
|
|
keyout("Error: authorized keys were not changed.")
|
|
else
|
|
keyout("Key " .. parms.rm_key .. " removed.")
|
|
end
|
|
end
|
|
|
|
-- generate data structures
|
|
local keys = {}
|
|
local f = io.open(tmpdir .. "/newkeys", "w")
|
|
if f then
|
|
if nixio.fs.stat(keyfile) then
|
|
for line in io.lines(keyfile)
|
|
do
|
|
local type, key, who, extra = line:match("(%S+)%s+(%S+)%s+(%S+)(.*)")
|
|
if extra == "" and who:match(".@.") and type:match("^ssh-") then
|
|
keys[#keys + 1] = who
|
|
f:write(type .. " " .. key .. " " .. who .. "\n")
|
|
end
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
|
|
-- sanitize the key file
|
|
if nixio.fs.stat(keyfile) and os.execute("diff " .. keyfile .. " " .. tmpdir .. "/newkeys >/dev/null 2>&1") ~= 0 then
|
|
os.execute("mv -f " .. tmpdir .. "/newkeys " .. keyfile)
|
|
keyout("Info: key file sanitized.")
|
|
end
|
|
|
|
remove_all("/tmp/web/upload")
|
|
remove_all(tmpdir)
|
|
|
|
-- generate the page
|
|
|
|
http_header(true)
|
|
html.header(node .. " administration", true)
|
|
html.print("<body><center>")
|
|
html.alert_banner()
|
|
html.print("<form method=post action=admin enctype='multipart/form-data'>")
|
|
-- nav
|
|
html.navbar_admin("admin")
|
|
html.print("<table width=850>")
|
|
|
|
html.print("<tr><td align=center><a href='/help.html#admin' target='_blank'>Help</a> ")
|
|
html.print("<input type=submit name=button_reboot value=Reboot style='font-weight:bold' title='Immediately reboot this node'>")
|
|
html.print("</td></tr>")
|
|
html.print("<tr><td align=center>")
|
|
|
|
html.print("<table cellspacing=10 width=100%>")
|
|
|
|
-- firmware
|
|
|
|
html.print("<tr><td align=center>")
|
|
html.print("<table cellspacing=10 width=100%>")
|
|
html.print("<tr><th colspan=3 style=background-color:lightseagreen>Firmware Update</th></tr>")
|
|
|
|
if #fw_output > 0 then
|
|
html.print("<tr><td colspan=3 align=center><table><tr><td><b>")
|
|
html.print("<pre>" .. word_wrap(80, fw_output) .. "</pre>")
|
|
html.print("</b></td></tr></table></td></tr>")
|
|
end
|
|
|
|
html.print("<tr><td align=center colspan=3><b>Current Version:</b> " .. fw_version .. " <b>Hardware Type:</b> (" .. targettype .. ") " .. mfgprefix .. " (" .. hardwaretype .. ")</td></tr>")
|
|
html.print("<tr><td align=center colspan=3>Keep Existing Configuration Settings <input type=checkbox name=checkbox_keep_settings checked title='keep existing configuration settings'></td></tr>")
|
|
|
|
if nixio.fs.stat("/tmp/force-upgrade-this-is-dangerous") then
|
|
html.print("<tr><td align=center colspan=3><span style=background-color:red;font-size:140%;> ALL FIRMWARE UPGRADE COMPATIBILITY CHECKS HAVE BEEN DISABLED </span></td></tr>")
|
|
end
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Upload Firmware</td>")
|
|
html.print("<td><input style='width:100%' type=file name=firmfile title='choose the firmware file to install from your hard drive' accept='." .. firmware_type .. "'></td>")
|
|
html.print("<td align=center><input type=submit name=button_ul_fw value=Upload title='install the firmware' style='width:90px' onclick='this.value=\"Uploading...\"'></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Download Firmware</td>")
|
|
html.print("<td><select name=dl_fw style='font-family:monospace'>")
|
|
html.print("<option value=default selected>- Select Firmware -</option>")
|
|
for _, fwi in ipairs(fw_names)
|
|
do
|
|
html.print("<option value=" .. fwi .. ">" .. fwi .. (fwi:match("%-") and " (nightly)" or "") .. "</option>")
|
|
end
|
|
html.print("</select>")
|
|
html.print("<input type=submit name=button_refresh_fw value=Refresh title='download the list of available firmware versions'>")
|
|
html.print("<td align=center><input type=submit name=button_dl_fw value=Download title='install the firmware' style='width:90px' onclick='this.value=\"Downloading...\"'></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Load Local Firmware</td>")
|
|
html.print("<td style='font-family:monospace'>")
|
|
html.print("<input type=submit name=button_apply_fw value='Apply Local Firmware' title='apply firmware previously uploaded to this node and named as shown' " .. (nixio.fs.stat("/tmp/web/" .. local_firmware) and "" or "disabled") .. ">")
|
|
html.print(" /tmp/web/" .. local_firmware .. "</td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("</table></td></tr>")
|
|
html.print("<tr><td colspan=3></td></tr>")
|
|
|
|
-- packages
|
|
|
|
html.print("<tr><td align=center>")
|
|
html.print("<table cellspacing=10 width=100%>")
|
|
html.print("<tr><th colspan=3 style=background-color:lightseagreen>Package Management</th></tr>")
|
|
|
|
-- low memory warning
|
|
if isLowMemNode() then
|
|
html.print("<tr><td align=center colspan=3><span style=background-color:cyan;font-size:140%;> Recommend not to install extra packages due to low memory on this node </span></td></tr>")
|
|
end
|
|
|
|
if #pkg_output > 0 then
|
|
-- opkg can produce duplicate first lines, remove them here
|
|
while pkg_output[2] and pkg_output[1] == pkg_output[2]
|
|
do
|
|
pkg_output:remove(1)
|
|
end
|
|
html.print("<tr><td colspan=3 align=center><table><tr><td><b><pre>")
|
|
html.print(word_wrap(80, pkg_output))
|
|
html.print("</pre></b></td></tr></table></td></tr>")
|
|
end
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Upload Package</td>")
|
|
html.print("<td><input style='width:100%' type=file name=ul_pkg title='choose the .ipk file to install from your hard drive'> </td>")
|
|
html.print("<td align=center><input type=submit name=button_ul_pkg value=Upload title='install the package'>")
|
|
html.print("</td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Download Package</td>")
|
|
html.print("<td><select name=dl_pkg style='font-family:monospace'>")
|
|
html.print("<option value=default selected>- Select Package -</option>")
|
|
for _, dlp in ipairs(dl_pkgs)
|
|
do
|
|
html.print("<option value=" .. dlp .. ">" .. dlp .. " " .. dlpkgver[dlp] .. "</option>")
|
|
end
|
|
html.print("</select>")
|
|
html.print("<input type=submit name=button_refresh_pkg value=Refresh title='download the list of available packages (warning: this takes a lot of space)'>")
|
|
html.print("<td align=center><input type=submit name=button_dl_pkg value=Download title='install the package'></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Remove Package</td>")
|
|
html.print("<td><select name=rm_pkg style='font-family:monospace'>")
|
|
html.print("<option value=default selected>- Select Package -</option>")
|
|
for _, pkg in ipairs(pkgs)
|
|
do
|
|
html.print("<option value=" .. pkg .. " " .. (permpkg[pkg] and "disabled" or "") .. ">" .. pkg .. " " .. pkgver[pkg] .. "</option>")
|
|
end
|
|
html.print("</select></td>")
|
|
html.print("<td align=center><input type=submit name=button_rm_pkg value=Remove title='remove the selected package'></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("</table></td></tr>")
|
|
|
|
html.print("<tr><td colspan=3></td></tr>")
|
|
|
|
-- ssh keys
|
|
html.print("<tr><td align=center>")
|
|
html.print("<table cellspacing=10 width=100%>")
|
|
html.print("<tr><th colspan=3 style=background-color:lightseagreen>Authorized SSH Keys</th></tr>")
|
|
|
|
if #key_output > 0 then
|
|
html.print("<tr><td colspan=3 align=center><table><tr><td><b><pre>")
|
|
html.print(word_wrap(80, key_output))
|
|
html.print("</pre></b></td></tr></table></td></tr>")
|
|
end
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Upload Key</td>")
|
|
html.print("<td><input style='width:100%' type=file name=sshkey title='choose the id_rsa.pub file to install from your hard drive'></td>")
|
|
html.print("<td align=center><input type=submit name=button_ul_key value=Upload title='install the key'")
|
|
html.print("></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("<tr>")
|
|
html.print("<td>Remove Key</td>")
|
|
html.print("<td><select name=rm_key style='font-family:monospace'>")
|
|
html.print("<option value=default selected>- Select Key -</option>")
|
|
for _, key in ipairs(keys)
|
|
do
|
|
html.print("<option value=" .. key .. ">" .. key .. "</option>")
|
|
end
|
|
html.print("</select>")
|
|
html.print("<td align=center><input type=submit name=button_rm_key value=Remove title='remove the selected key'></td>")
|
|
html.print("</tr>")
|
|
|
|
html.print("</table></td></tr>")
|
|
|
|
html.print("<tr><td colspan=3></td></tr>")
|
|
|
|
html.print("<tr><th colspan=3 style=background-color:lightseagreen>Support Data</th></tr>")
|
|
html.print("<tr><td colspan=3 align=center><input type=submit name=button_support_data value='Download Support Data' title='Download a Support Data file'></td></tr>")
|
|
|
|
html.print("<tr><td colspan=3></td></tr>")
|
|
|
|
html.print("</table>")
|
|
html.print("</td></tr>")
|
|
html.print("</table>")
|
|
|
|
html.print("</form>")
|
|
html.print("</center>")
|
|
html.footer()
|
|
html.print("</body>")
|
|
html.print("</html>")
|
|
http_footer()
|