{% /* * Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks * Copyright (C) 2024 Tim Wilkinson * 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® 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 */ %} {% function curl(url, filename, start, len) { const agent = `Node: ${configuration.getName()} Version: ${configuration.getFirmwareVersion()}`; const name = filename ? filename : `/tmp/download.${time()}`; const f = fs.popen(`/usr/bin/curl --progress-bar --remove-on-error --user-agent '${agent}' -o ${name} ${url} 2>&1`); if (!f) { return null; } uhttpd.send(`event: progress\r\ndata: ${start}\r\n\r\n`); for (;;) { const l = f.read("\r"); if (!length(l)) { break; } const m = match(trim(l), /([0-9\.]+)%$/); if (m) { uhttpd.send(`event: progress\r\ndata: ${start + len * m[1] / 100}\r\n\r\n`); } } f.close(); if (!fs.access(name)) { return null; } if (filename === name) { return filename; } const f2 = fs.open(name); fs.unlink(name); return f2; }; function prepareUpgrade(firmwarefile) { let error = "Failed."; if (firmwarefile) { const f = fs.popen(`/usr/libexec/validate_firmware_image ${firmwarefile}`); if (f) { const info = json(f.read("all")); f.close(); if (info.valid) { error = null; } else if (info.forceable && fs.access("/tmp/force-upgrade-this-is-dangerous")) { error = null; } else if (!info.tests.fwtool_device_match) { error = "Firmware not compatible with this device."; } else if (!info.tests.fwtool_signature) { error = "Corrupted firmware, bad signature."; } else { error = "Unknown error validating firmware."; } } else { error = "Failed to validate firmware."; } } if (!error) { fs.unlink("/tmp/arednsysupgradebackup.tgz"); if (!fs.access("/tmp/do-not-keep-configuration")) { const fi = fs.open("/etc/arednsysupgrade.conf"); if (fi) { const fo = fs.open("/tmp/sysupgradefilelist", "w"); if (fo) { for (let l = fi.read("line"); length(l); l = fi.read("line")) { if (!match(l, "^#") && fs.access(trim(l))) { fo.write(l); } } fo.close(); if (system("/bin/tar -czf /tmp/arednsysupgradebackup.tgz -T /tmp/sysupgradefilelist > /dev/null 2>&1") < 0) { fs.unlink("/tmp/arednsysupgradebackup.tgz"); } fs.unlink("/tmp/sysupgradefilelist"); } fi.close(); } if (!fs.access("/tmp/arednsysupgradebackup.tgz")) { error = "Failed to create backup."; } } if (!error) { return { upgrade: `/usr/local/bin/aredn_sysupgrade ${fs.access("/tmp/arednsysupgradebackup.tgz") ? "-f /tmp/arednsysupgradebackup.tgz" : "-n"} -q ${firmwarefile}` }; } } return { error: error }; }; function shutdownServices() { if (hardware.isLowMemNode()) { system([ "/etc/init.d/manager", "stop" ]); system([ "/etc/init.d/telnet", "stop" ]); system([ "/etc/init.d/dropbear", "stop" ]); system([ "/etc/init.d/urngd", "stop" ]); } }; function restoreServices() { if (hardware.isLowMemNode()) { system([ "/etc/init.d/urngd", "start" ]); system([ "/etc/init.d/telnet", "start" ]); system([ "/etc/init.d/dropbear", "start" ]); system([ "/etc/init.d/manager", "start" ]); } }; if (request.env.REQUEST_METHOD === "PUT") { if (request.args.keepconfig) { if (request.args.keepconfig === "off") { fs.open("/tmp/do-not-keep-configuration", "w").close(); } else { fs.unlink("/tmp/do-not-keep-configuration"); } } if (request.args.dangerousupgrade) { if (request.args.dangerousupgrade === "on") { fs.open("/tmp/force-upgrade-this-is-dangerous", "w").close(); } else { fs.unlink("/tmp/force-upgrade-this-is-dangerous"); } } if (request.args.firmwareurl) { if (match(request.args.firmwareurl, constants.reUrl)) { configuration.prepareChanges(); uciMesh.set("aredn", "@downloads[0]", "firmware_aredn", request.args.firmwareurl); uciMesh.commit("aredn"); print(_R("changes")); } } return; } else if (request.env.REQUEST_METHOD === "POST") { if (request.args.sideload) { const upgrade = prepareUpgrade("/tmp/local_firmware"); if (upgrade.error) { print(`
ERROR: ${upgrade.error}
`); } else { response.reboot = upgrade.upgrade; print(_R("reboot-firmware")); } } else if (request.args.firmwarefileprepare) { shutdownServices(); } else if (request.args.firmwarefile) { const upgrade = prepareUpgrade(request.args.firmwarefile); if (upgrade.error) { fs.unlink(request.args.firmwarefile); print(`
ERROR: ${upgrade.error}
`); print(`
`); restoreServices(); } else { response.reboot = upgrade.upgrade; print(_R("reboot-firmware")); } } else if (request.args.restorefile) { const restore = configuration.restore(request.args.restorefile); if (restore.error) { print(`
ERROR: ${restore.error}
`); print(`
`); } else { response.upgrade = "/bin/reboot"; print(_R("reboot-restore")); } } return; } else if (request.env.REQUEST_METHOD === "GET" && request.env.QUERY_STRING === "v=update") { response.override = true; uhttpd.send("Status: 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n\r\n"); fs.unlink("/tmp/firmware.list"); const aredn_firmware = uci.get("aredn", "@downloads[0]", "firmware_aredn"); if (!aredn_firmware) { uhttpd.send(`event: error\r\ndata: missing firmware download url\r\n\r\n`); return; } let f = curl(`${aredn_firmware}/afs/www/config.js`, null, 0, 10); if (!f) { uhttpd.send(`event: error\r\ndata: failed to download firmware configuration\r\n\r\n`); return; } let firmware_versions = {}; for (let l = f.read("line"); length(l); l = f.read("line")) { const m = match(l, /versions: \{(.+)\}/); if (m) { const kvs = split(m[1], ", "); for (let i = 0; i < length(kvs); i++) { const kv = split(kvs[i], ": "); firmware_versions[trim(kv[0], "'")] = trim(kv[1], "'"); } break; } } f.close(); const firmware_version_count = length(keys(firmware_versions)); if (firmware_version_count === 0) { uhttpd.send(`event: error\r\ndata: failed to find firmware versions in downloaded configuration\r\n\r\n`); return; } const board_type = replace(hardware.getBoard().model.id, ",", "_"); const firmware_ulist = {}; let count = 0; for (let ver in firmware_versions) { const data = firmware_versions[ver]; f = curl(`${aredn_firmware}/afs/www/${data}/overview.json`, null, 10 + count * 90 / firmware_version_count, 90 / firmware_version_count); if (f) { let info; try { info = json(f.read("all")); } catch (_) { } f.close(); if (!info) { uhttpd.send(`event: error\r\ndata: firmware version downloaded is corrupt\r\n\r\n`); return; } for (let i = 0; i < length(info.profiles); i++) { const profile = info.profiles[i]; if (profile.id === board_type || ((board_type === "qemu" || board_type === "vmware") && profile.id == "generic" && profile.target === "x86/64")) { firmware_ulist[ver] = { overview: `${aredn_firmware}/afs/www/${data}/${profile.target}/${profile.id}.json`, target: replace(info.image_url, "{target}", profile.target) }; } } } count++; } const firmware_list = {}; const firmware_vers = sort(keys(firmware_ulist), function(a, b) { if (index(a, "-") !== -1) { return 1; } if (index(b, "-") !== -1) { return -1; } const av = split(a, "."); const bv = split(b, "."); for (let i = 0; i < 4; i++) { av[i] = int(av[i]); bv[i] = int(bv[i]); if (av[i] < bv[i]) { return 1 } if (av[i] > bv[i]) { return -1 } } return 0; }); for (let i = 0; i < length(firmware_vers); i++) { const k = firmware_vers[i]; firmware_list[k] = firmware_ulist[k]; } f = fs.open("/tmp/firmware.list", "w"); if (!f) { uhttpd.send(`event: error\r\ndata: failed to create firmware list\r\n\r\n`); return; } f.write(sprintf("%J", firmware_list)); f.close(); let html = ``; for (let k in firmware_list) { html += ``; } uhttpd.send(`event: close\r\ndata: ${html}\r\n\r\n`); return; } else if (request.env.REQUEST_METHOD === "GET" && index(request.env.QUERY_STRING, "v=") === 0) { response.override = true; const version = substr(request.env.QUERY_STRING, 2); uhttpd.send("Status: 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n\r\n"); let f = fs.open("/tmp/firmware.list"); if (!f) { uhttpd.send(`event: error\r\ndata: missing firmware list\r\n\r\n`); return; } let list; try { list = json(f.read("all")); } catch (_) { } f.close(); if (!list) { uhttpd.send(`event: error\r\ndata: firmware list is corrupt\r\n\r\n`); return; } const inst = list[version]; if (!inst) { uhttpd.send(`event: error\r\ndata: bad firmware version\r\n\r\n`); return; } f = curl(inst.overview, null, 0, 5); if (!f) { uhttpd.send(`event: error\r\ndata: could not download firmware version catalog\r\n\r\n`); return; } let overview; try { overview = json(f.read("all")); } catch (_) { } f.close(); if (!overview) { uhttpd.send(`event: error\r\ndata: downloaded firmware version catalog is corrupt\r\n\r\n`); return; } let fwimage = null; let booter_version = null; if (index(hardware.getHardwareType(), "mikrotik-v7") !== -1) { booter_version = "v7" } for (let i = 0; i < length(overview.images); i++) { const image = overview.images[i]; if ((!booter_version && (image.type === "sysupgrade" || image.type === "nand-sysupgrade" || image.type == "combined")) || (booter_version === "v7" && image.type === "sysupgrade-v7")) { fwimage = { url: `${inst.target}/${image.name}`, sha: image.sha256 }; break; } } if (!fwimage) { uhttpd.send(`event: error\r\ndata: missing firmware image in downloaded firmware catalog\r\n\r\n`); return; } shutdownServices(); let r = curl(fwimage.url, "/tmp/firmwarefile", 5, 95); if (!r) { uhttpd.send(`event: error\r\ndata: failed to start firmware download\r\n\r\n`); restoreServices(); return; } const upgrade = prepareUpgrade("/tmp/firmwarefile"); if (upgrade.error) { fs.unlink("/tmp/firmwarefile"); uhttpd.send(`event: error\r\ndata: ${upgrade.error}\r\n\r\n`); restoreServices(); } else { response.reboot = upgrade.upgrade; uhttpd.send(`event: close\r\ndata: ${sprintf("%J", { v:_R("reboot-firmware")})}\r\n\r\n`); } return; } fs.unlink("/tmp/force-upgrade-this-is-dangerous"); fs.unlink("/tmp/do-not-keep-configuration"); const sideload = fs.access("/tmp/local_firmware"); const needreboot = (uci.get("aredn", "@watchdog[0]", "enable") === "1") || (fs.access("/tmp/reboot-required") ? true : false); %}
{{_R("dialog-header", "Firmware")}}
{{_R("dialog-messages")}}
{{configuration.getFirmwareVersion()}}
Current version
{{hardware.getHardwareType()}}
Hardware type
{{_H("The hardware type is useful when finding firmware files to upload or sideload. When using the download feature the node will automatically find the correct firmware.")}}
Download Firmware
Download firmware from an AREDN server.
{{_H("Download firmware directly from a central server, either on the Internet or a locally configured mesh server. Refresh the list of available firmware version using the refresh button to the right of the firmware list. Once a firmware is selected it can be downloaded and installed using the button at the base of the dialog.")}}
Upload Firmware
Upload a firmware file from your computer.
{{_H("Upload a firmware file from your computer. Once the firmware has been selected it can be uploaded and installed using the button at the base of the dialog.")}}
Sideload Firmware
Use an alternatve way to load firmware onto the node.
{{_H("Sideload a firmware file by transferring it onto the node by some other means (e.g. scp) and putting it in the /tmp directory with the name local_firmware. It can then be installed using the button at the base of the dialog.")}}
Backup Configuration
Backup this node's configuration.
{{_H("Backup the current configuration. This can be used to transfer a configuration to new hardware or as a safety precaution in case of hardware failure.")}}
Restore Configuration
Upload a previous configuration.
{{_H("Restore a previous backup to this node. This will replace whatever the current node's configuration is. Be aware that no attempt is made to valiate the backup's integrity. Restoring to different hardware could result in unexpected behaviour.")}} {{_R("dialog-advanced")}}
{% if (includeAdvanced) { %} {% if (fs.access("/rom/etc")) { %}
Keep Configuration
Keep existing configuration after upgrade.
{{_R("switch", { name: "keepconfig", value: true })}}
{{_H("Keep the current configuration when updating the node's firmware. This is usually what you want to do, but on rare occasions you might want to return the node to its first boot state.")}} {% } %}
Dangerous Upgrade
Force the firmware onto the device, even if it fails the safety checks.
{{_R("switch", { name: "dangerousupgrade", value: false })}}
{{_H("Force the firmware to be installed, even if the system thinks it is not compatible. You almost never want to do this so this should be used with care.")}}
Firmware URL
URL for downloading firmware
{{_H("The base URL used to download firmware. By default this points to the main AREDN repository, but you can change this to a local server, especially if you'd like to do this without a connection to the Internet.")}} {% } %}
{{_H("
Depending on how the firmware it to be installed using the options above, this button will initiate the process.")}}
{{_R("dialog-footer", "nocancel" )}}