From 0432bf3165af6949528e5bcd5ac86ace3440bc9e Mon Sep 17 00:00:00 2001 From: Tim Wilkinson Date: Thu, 15 Aug 2024 20:28:45 -0700 Subject: [PATCH] New UI (#1263) * New UI * Fix gzip filename race condition * Fix scrolling on first use page --- configs/common.config | 7 + files/app/config.uc | 53 + files/app/constants.uc | 43 + files/app/main/app.ut | 106 ++ files/app/main/authenticate.ut | 56 + files/app/main/changes.ut | 35 + files/app/main/dhcp.ut | 35 + files/app/main/firstuse-ram.ut | 104 ++ files/app/main/firstuse.ut | 132 ++ files/app/main/health.ut | 35 + files/app/main/info.ut | 48 + files/app/main/local-and-neighbor-devices.ut | 35 + files/app/main/mesh-summary.ut | 35 + files/app/main/messages.ut | 40 + files/app/main/packages.ut | 37 + files/app/main/status/e/basics.ut | 340 ++++ files/app/main/status/e/changes.ut | 35 + files/app/main/status/e/dhcp.ut | 623 +++++++ files/app/main/status/e/firmware.ut | 616 +++++++ files/app/main/status/e/internal-services.ut | 261 +++ files/app/main/status/e/local-services.ut | 616 +++++++ files/app/main/status/e/location.ut | 242 +++ files/app/main/status/e/neighbor-device.ut | 371 +++++ files/app/main/status/e/network.ut | 391 +++++ files/app/main/status/e/packages.ut | 431 +++++ files/app/main/status/e/ports-and-xlinks.ut | 473 ++++++ files/app/main/status/e/radio-and-antenna.ut | 596 +++++++ files/app/main/status/e/reboot.ut | 56 + files/app/main/status/e/time.ut | 154 ++ files/app/main/status/e/tunnels.ut | 541 ++++++ files/app/main/tools/e/iperf3.ut | 202 +++ files/app/main/tools/e/ping.ut | 204 +++ files/app/main/tools/e/supportdata.ut | 170 ++ files/app/main/tools/e/traceroute.ut | 211 +++ files/app/main/tools/e/wifiscan.ut | 274 ++++ files/app/main/tools/e/wifisignal.ut | 292 ++++ files/app/main/tunnels.ut | 35 + files/app/partial/changes.ut | 83 + files/app/partial/dhcp.ut | 120 ++ files/app/partial/dialog-advanced.ut | 38 + files/app/partial/dialog-footer.ut | 41 + files/app/partial/dialog-header.ut | 46 + files/app/partial/dialog-messages.ut | 38 + files/app/partial/firmware.ut | 68 + files/app/partial/general.ut | 60 + files/app/partial/health.ut | 76 + files/app/partial/internal-services.ut | 72 + .../app/partial/local-and-neighbor-devices.ut | 209 +++ files/app/partial/local-services.ut | 145 ++ files/app/partial/location.ut | 65 + files/app/partial/login.ut | 109 ++ files/app/partial/mesh-data.ut | 127 ++ files/app/partial/mesh-summary.ut | 53 + files/app/partial/mesh.ut | 57 + files/app/partial/messages.ut | 99 ++ files/app/partial/nav-status.ut | 11 + files/app/partial/nav.ut | 75 + files/app/partial/network.ut | 80 + files/app/partial/oldui.ut | 1 + files/app/partial/open.ut | 40 + files/app/partial/packages.ut | 51 + files/app/partial/ports-and-xlinks.ut | 74 + files/app/partial/radio-and-antenna.ut | 122 ++ files/app/partial/reboot-firmware.ut | 69 + files/app/partial/reboot-firstuse-ram.ut | 59 + files/app/partial/reboot-firstuse.ut | 57 + files/app/partial/reboot-mon.ut | 81 + files/app/partial/selection.ut | 62 + files/app/partial/status.ut | 95 ++ files/app/partial/switch.ut | 37 + files/app/partial/tool-footer.ut | 38 + files/app/partial/tool-header.ut | 41 + files/app/partial/tools.ut | 50 + files/app/partial/tunnels.ut | 133 ++ files/app/resource/css/admin.css | 1460 +++++++++++++++++ files/app/resource/css/themes/dark.css | 70 + files/app/resource/css/themes/light.css | 70 + files/app/resource/css/user.css | 1076 ++++++++++++ files/app/resource/js/htmx.min.js.gz | Bin 0 -> 15677 bytes files/app/resource/js/meshpage.js | 118 ++ files/app/root.ut | 654 ++++++++ files/etc/arednsysupgrade.conf | 3 + files/etc/config.mesh/_setup | 11 +- files/etc/config.mesh/aredn | 5 +- files/etc/config.mesh/dhcp | 23 +- files/etc/config.mesh/dropbear | 3 +- files/etc/config.mesh/firewall | 360 ++-- files/etc/config.mesh/network | 16 +- files/etc/config.mesh/olsrd | 1 + files/etc/config.mesh/snmpd | 67 +- files/etc/config.mesh/system | 45 +- files/etc/config.mesh/uhttpd | 6 +- files/etc/config.mesh/vtun | 5 - files/etc/config/uhttpd | 20 +- files/etc/cron.boot/boot-fixups | 38 + files/etc/cron.daily/update-clock | 4 + files/etc/cron.hourly/firmware-helper | 83 + files/etc/init.d/local | 36 +- files/etc/radios.json | 398 ++--- files/etc/uci-defaults/10_dmz_mode_migrate | 11 - files/etc/uci-defaults/45_aredn_reset_paths | 66 - files/etc/uci-defaults/80_aredn_lqm | 26 - .../uci-defaults/81_aredn_migrate_wansettings | 29 - .../uci-defaults/82_aredn_ntp_period_migrate | 18 - .../etc/uci-defaults/90_aredn_default_tunnels | 11 - files/etc/uci-defaults/94_fix_wpad | 6 + .../uci-defaults/95_aredn_migrate_leafletjs | 6 - files/etc/uci-defaults/95_lowmem_devices | 11 + .../etc/uci-defaults/96_aredn_migrate_latlon | 26 - .../uci-defaults/97_aredn_setup_meshstatus | 5 - .../uci-defaults/98_change_default_maptiles | 12 - ...p_aredn_include => 98_setup_aredn_include} | 0 files/etc/uci-defaults/99_canonical_config | 191 +++ files/usr/lib/lua/aredn/hardware.lua | 99 ++ files/usr/local/bin/mgr/gps.lua | 120 ++ files/usr/local/bin/mgr/lqm.lua | 14 +- files/usr/local/bin/mgr/snrlog.lua | 12 +- files/usr/local/bin/node-setup | 138 +- files/usr/share/ucode/aredn/configuration.uc | 401 +++++ files/usr/share/ucode/aredn/hardware.uc | 583 +++++++ files/usr/share/ucode/aredn/lqm.uc | 70 + files/usr/share/ucode/aredn/mesh.uc | 79 + files/usr/share/ucode/aredn/messages.uc | 135 ++ files/usr/share/ucode/aredn/network.uc | 127 ++ files/usr/share/ucode/aredn/olsr.uc | 90 + files/usr/share/ucode/aredn/radios.uc | 196 +++ files/usr/share/ucode/aredn/units.uc | 75 + files/www/cgi-bin/admin | 22 +- files/www/cgi-bin/advancedconfig | 126 +- files/www/cgi-bin/iperf | 4 +- files/www/cgi-bin/ping | 74 + files/www/cgi-bin/setup | 2 +- files/www/cgi-bin/status | 2 + files/www/cgi-bin/traceroute | 74 + files/www/index.html | 4 +- patches/802-gpio-typo.patch | 21 + patches/series | 1 + 137 files changed, 17170 insertions(+), 906 deletions(-) create mode 100755 files/app/config.uc create mode 100755 files/app/constants.uc create mode 100755 files/app/main/app.ut create mode 100755 files/app/main/authenticate.ut create mode 100755 files/app/main/changes.ut create mode 100755 files/app/main/dhcp.ut create mode 100755 files/app/main/firstuse-ram.ut create mode 100755 files/app/main/firstuse.ut create mode 100755 files/app/main/health.ut create mode 100755 files/app/main/info.ut create mode 100755 files/app/main/local-and-neighbor-devices.ut create mode 100755 files/app/main/mesh-summary.ut create mode 100755 files/app/main/messages.ut create mode 100755 files/app/main/packages.ut create mode 100755 files/app/main/status/e/basics.ut create mode 100755 files/app/main/status/e/changes.ut create mode 100755 files/app/main/status/e/dhcp.ut create mode 100755 files/app/main/status/e/firmware.ut create mode 100755 files/app/main/status/e/internal-services.ut create mode 100755 files/app/main/status/e/local-services.ut create mode 100755 files/app/main/status/e/location.ut create mode 100755 files/app/main/status/e/neighbor-device.ut create mode 100755 files/app/main/status/e/network.ut create mode 100755 files/app/main/status/e/packages.ut create mode 100755 files/app/main/status/e/ports-and-xlinks.ut create mode 100755 files/app/main/status/e/radio-and-antenna.ut create mode 100755 files/app/main/status/e/reboot.ut create mode 100755 files/app/main/status/e/time.ut create mode 100755 files/app/main/status/e/tunnels.ut create mode 100755 files/app/main/tools/e/iperf3.ut create mode 100755 files/app/main/tools/e/ping.ut create mode 100755 files/app/main/tools/e/supportdata.ut create mode 100755 files/app/main/tools/e/traceroute.ut create mode 100755 files/app/main/tools/e/wifiscan.ut create mode 100755 files/app/main/tools/e/wifisignal.ut create mode 100755 files/app/main/tunnels.ut create mode 100755 files/app/partial/changes.ut create mode 100755 files/app/partial/dhcp.ut create mode 100755 files/app/partial/dialog-advanced.ut create mode 100755 files/app/partial/dialog-footer.ut create mode 100755 files/app/partial/dialog-header.ut create mode 100755 files/app/partial/dialog-messages.ut create mode 100755 files/app/partial/firmware.ut create mode 100755 files/app/partial/general.ut create mode 100755 files/app/partial/health.ut create mode 100755 files/app/partial/internal-services.ut create mode 100755 files/app/partial/local-and-neighbor-devices.ut create mode 100755 files/app/partial/local-services.ut create mode 100755 files/app/partial/location.ut create mode 100755 files/app/partial/login.ut create mode 100755 files/app/partial/mesh-data.ut create mode 100755 files/app/partial/mesh-summary.ut create mode 100755 files/app/partial/mesh.ut create mode 100755 files/app/partial/messages.ut create mode 100755 files/app/partial/nav-status.ut create mode 100755 files/app/partial/nav.ut create mode 100755 files/app/partial/network.ut create mode 100755 files/app/partial/oldui.ut create mode 100755 files/app/partial/open.ut create mode 100755 files/app/partial/packages.ut create mode 100755 files/app/partial/ports-and-xlinks.ut create mode 100755 files/app/partial/radio-and-antenna.ut create mode 100755 files/app/partial/reboot-firmware.ut create mode 100755 files/app/partial/reboot-firstuse-ram.ut create mode 100755 files/app/partial/reboot-firstuse.ut create mode 100755 files/app/partial/reboot-mon.ut create mode 100755 files/app/partial/selection.ut create mode 100755 files/app/partial/status.ut create mode 100755 files/app/partial/switch.ut create mode 100755 files/app/partial/tool-footer.ut create mode 100755 files/app/partial/tool-header.ut create mode 100755 files/app/partial/tools.ut create mode 100755 files/app/partial/tunnels.ut create mode 100755 files/app/resource/css/admin.css create mode 100755 files/app/resource/css/themes/dark.css create mode 100755 files/app/resource/css/themes/light.css create mode 100755 files/app/resource/css/user.css create mode 100644 files/app/resource/js/htmx.min.js.gz create mode 100755 files/app/resource/js/meshpage.js create mode 100644 files/app/root.ut create mode 100755 files/etc/cron.boot/boot-fixups create mode 100755 files/etc/cron.hourly/firmware-helper delete mode 100644 files/etc/uci-defaults/10_dmz_mode_migrate delete mode 100755 files/etc/uci-defaults/45_aredn_reset_paths delete mode 100755 files/etc/uci-defaults/80_aredn_lqm delete mode 100755 files/etc/uci-defaults/81_aredn_migrate_wansettings delete mode 100644 files/etc/uci-defaults/82_aredn_ntp_period_migrate delete mode 100755 files/etc/uci-defaults/90_aredn_default_tunnels create mode 100755 files/etc/uci-defaults/94_fix_wpad delete mode 100755 files/etc/uci-defaults/95_aredn_migrate_leafletjs create mode 100755 files/etc/uci-defaults/95_lowmem_devices delete mode 100755 files/etc/uci-defaults/96_aredn_migrate_latlon delete mode 100644 files/etc/uci-defaults/97_aredn_setup_meshstatus delete mode 100755 files/etc/uci-defaults/98_change_default_maptiles rename files/etc/uci-defaults/{99_setup_aredn_include => 98_setup_aredn_include} (100%) create mode 100755 files/etc/uci-defaults/99_canonical_config create mode 100755 files/usr/local/bin/mgr/gps.lua create mode 100755 files/usr/share/ucode/aredn/configuration.uc create mode 100755 files/usr/share/ucode/aredn/hardware.uc create mode 100755 files/usr/share/ucode/aredn/lqm.uc create mode 100755 files/usr/share/ucode/aredn/mesh.uc create mode 100755 files/usr/share/ucode/aredn/messages.uc create mode 100755 files/usr/share/ucode/aredn/network.uc create mode 100755 files/usr/share/ucode/aredn/olsr.uc create mode 100755 files/usr/share/ucode/aredn/radios.uc create mode 100755 files/usr/share/ucode/aredn/units.uc create mode 100755 files/www/cgi-bin/ping create mode 100755 files/www/cgi-bin/traceroute create mode 100755 patches/802-gpio-typo.patch diff --git a/configs/common.config b/configs/common.config index 84701b70..a8865d9b 100644 --- a/configs/common.config +++ b/configs/common.config @@ -3,6 +3,8 @@ CONFIG_BUSYBOX_CONFIG_ARPING=y CONFIG_BUSYBOX_CONFIG_CROND=n CONFIG_BUSYBOX_CONFIG_FEATURE_IPV6=n CONFIG_BUSYBOX_CONFIG_FEATURE_TOP_INTERACTIVE=y +CONFIG_BUSYBOX_CONFIG_DIFF=y +CONFIG_BUSYBOX_CONFIG_MKPASSWD=y CONFIG_BUSYBOX_CONFIG_MKSWAP=n CONFIG_BUSYBOX_CONFIG_NTPD=y CONFIG_BUSYBOX_CONFIG_SETSID=y @@ -98,6 +100,7 @@ CONFIG_PACKAGE_libext2fs=m CONFIG_PACKAGE_libiwinfo-lua=y CONFIG_PACKAGE_liblua=y CONFIG_PACKAGE_liblucihttp-lua=y +CONFIG_PACKAGE_liblucihttp-ucode=y CONFIG_PACKAGE_liblucihttp=y CONFIG_PACKAGE_liblzo=m CONFIG_PACKAGE_libncurses=m @@ -154,7 +157,11 @@ CONFIG_PACKAGE_socat=m CONFIG_PACKAGE_sysfsutils=m CONFIG_PACKAGE_tcpdump-mini=m CONFIG_PACKAGE_ubi-utils=y +CONFIG_PACKAGE_ucode-mod-log=y +CONFIG_PACKAGE_ucode-mod-math=y +CONFIG_PACKAGE_ucode-mod-resolv=y CONFIG_PACKAGE_uhttpd=y +CONFIG_PACKAGE_uhttpd-mod-ucode=y CONFIG_PACKAGE_vtun=y CONFIG_PACKAGE_wireguard=y CONFIG_PACKAGE_wireguard-tools=y diff --git a/files/app/config.uc b/files/app/config.uc new file mode 100755 index 00000000..0b58f4e1 --- /dev/null +++ b/files/app/config.uc @@ -0,0 +1,53 @@ +/* + * 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 + */ + +import * as hardware from "aredn.hardware"; + +export const debug = false; + +export const application = "/app"; +export let preload = true; +export let compress = true; +export let resourcehash = true; +export let authenable = true; + + +if (hardware.isLowMemNode()) { + preload = false; +} +if (debug) { + preload = false; + compress = false; + resourcehash = false; + authenable = false; +} diff --git a/files/app/constants.uc b/files/app/constants.uc new file mode 100755 index 00000000..3868774c --- /dev/null +++ b/files/app/constants.uc @@ -0,0 +1,43 @@ +/* + * 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 + */ + +export const reUrl = /^http(s)?:\/\/.+$/; +export const patUrl = "http(s)?:\/\/.+"; +export const reIP = /^((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.?\b){4}$/; +export const patIP = "((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}"; +export const rePort = /^([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-4])$/; +export const patPort = "([1-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-4])"; +export const reNetmask = /^((128|192|224|240|248|252|254)\.0\.0\.0)|(255\.(((0|128|192|224|240|248|252|254)\.0\.0)|(255\.(((0|128|192|224|240|248|252|254)\.0)|255\.(0|128|192|224|240|248|252|254)))))$/; +export const patNetmask = "((128|192|224|240|248|252|254)\\.0\\.0\\.0)|(255\\.(((0|128|192|224|240|248|252|254)\\.0\\.0)|(255\\.(((0|128|192|224|240|248|252|254)\\.0)|255\\.(0|128|192|224|240|248|252|254)))))"; +export const reNodename = /^[0-9]?[A-Z]{1,2}[0-9]{1,4}[A-Z]{1,3}-[\-_a-zA-Z0-9]+$/; +export const patNodename = "[0-9]?[A-Z]{1,2}[0-9]{1,4}[A-Z]{1,3}-[\\-_a-zA-Z0-9]+"; diff --git a/files/app/main/app.ut b/files/app/main/app.ut new file mode 100755 index 00000000..557cd668 --- /dev/null +++ b/files/app/main/app.ut @@ -0,0 +1,106 @@ +{% +/* + * 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 + */ +%} +{% if (!request.headers["hx-boosted"]) { %} + + + + {% if (!config.resourcehash) { %} + + + {% if (auth.isAdmin) { %} + + + {% } %} + {% } else { + versions.themecss = fs.readlink(`${config.application}/resource/css/theme.version`); + %} + + + {% if (auth.isAdmin) { %} + + + {% } %} + {% } %} + + {{configuration.getName()}} {{auth.isAdmin && request.page === "status" ? "admin" : request.page}} + + + + {{_R("oldui")}} +
+ +
+
+ {{_R("selection")}} +
+
+
+ {{_R(request.page)}} +
+
+
+
+ {% if (auth.isAdmin) { %} + + {% } %} + + +{% } else { %} +{{configuration.getName()}} {{auth.isAdmin && request.page === "status" ? "admin" : request.page}} +{{_R("nav-status")}} +
+
+ {{_R("selection")}} +
+
+
+ {{_R(request.page)}} +
+
+
+{% } %} diff --git a/files/app/main/authenticate.ut b/files/app/main/authenticate.ut new file mode 100755 index 00000000..0f7b0fc1 --- /dev/null +++ b/files/app/main/authenticate.ut @@ -0,0 +1,56 @@ +{% +/* + * 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 + */ +%} +{% +response.headers["Content-Type"] = "application/json"; +if (request.env.REQUEST_METHOD === "POST") { + try { + const j = json(uhttpd.recv(1024)); + if (j.version === 1) { + if (j.logout) { + auth.deauthenticate(); + print('{"authenticated":false}\n'); + return; + } + if (auth.authenticate(j.password)) { + print('{"authenticated":true}\n'); + return; + } + } + } + catch (_) { + } +} +print('{"authenticated":false}\n'); +%} diff --git a/files/app/main/changes.ut b/files/app/main/changes.ut new file mode 100755 index 00000000..39db6ea7 --- /dev/null +++ b/files/app/main/changes.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("changes")}} diff --git a/files/app/main/dhcp.ut b/files/app/main/dhcp.ut new file mode 100755 index 00000000..0f89d799 --- /dev/null +++ b/files/app/main/dhcp.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("dhcp")}} diff --git a/files/app/main/firstuse-ram.ut b/files/app/main/firstuse-ram.ut new file mode 100755 index 00000000..cfd24061 --- /dev/null +++ b/files/app/main/firstuse-ram.ut @@ -0,0 +1,104 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "POST") { + print(_R("reboot-firstuse-ram")); + response.upgrade = `/usr/local/bin/aredn_sysupgrade -n -q ${request.args.firmwarefile}`; + return; +} +%} + + + + + + + + + + +
+
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Welcome
+
+
Congratulations on booting AREDN®
+
AREDN® is currently running in RAM. The next step is to install AREDN® into Flash.
+
Download the sysupgrade.bin file for this device (it should be at the same place your found this + kernel.bin file) and upload it using the file selector below
+
+
+
+
Select Firmware File
+
+
+
+
+
+
+
+
+
+
+ + + diff --git a/files/app/main/firstuse.ut b/files/app/main/firstuse.ut new file mode 100755 index 00000000..117590c5 --- /dev/null +++ b/files/app/main/firstuse.ut @@ -0,0 +1,132 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + system(`/usr/local/bin/firstuse-setup '${request.args.name}' '${request.args.passwd}'`); + response.reboot = true; + print(_R("reboot-firstuse")); + return; +} +%} + + + + + + + + + + +
+
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Welcome
+
+
Congratulations on installing AREDN®
+
There's a few pieces of basic information we need to start setting up your node.
+
+
+
+
Node Name
+
+
+
+ This is the unique name given to your node. It must start with your callsign. For example, K6AH-Home +
+
+
New Password
+
+
+
+
Retype Password
+
+
+
+ Enter a password, twice, to assign to your node for access to configuration information later +
+
+
+
+
+
+
+
+
+ + + diff --git a/files/app/main/health.ut b/files/app/main/health.ut new file mode 100755 index 00000000..0dabe6ce --- /dev/null +++ b/files/app/main/health.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("health")}} diff --git a/files/app/main/info.ut b/files/app/main/info.ut new file mode 100755 index 00000000..8cd8aa42 --- /dev/null +++ b/files/app/main/info.ut @@ -0,0 +1,48 @@ +{% +/* + * 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 + */ +%} +{% +response.headers["Content-Type"] = "text/plain"; +for (k in request.env) { + if (k !== "headers") { + print("request.env." + k + ": " + request.env[k] + "\r\n"); + } +} +for (k in request.headers) { + print("request.headers." + k + ": " + request.headers[k] + "\r\n"); +} +for (k in uhttpd) { + print("uhttpd." + k + ": " + uhttpd[k] + "\r\n"); +} +%} diff --git a/files/app/main/local-and-neighbor-devices.ut b/files/app/main/local-and-neighbor-devices.ut new file mode 100755 index 00000000..1fa99f76 --- /dev/null +++ b/files/app/main/local-and-neighbor-devices.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("local-and-neighbor-devices")}} diff --git a/files/app/main/mesh-summary.ut b/files/app/main/mesh-summary.ut new file mode 100755 index 00000000..3b323c2d --- /dev/null +++ b/files/app/main/mesh-summary.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("mesh-summary")}} diff --git a/files/app/main/messages.ut b/files/app/main/messages.ut new file mode 100755 index 00000000..47ef9b23 --- /dev/null +++ b/files/app/main/messages.ut @@ -0,0 +1,40 @@ +{% +/* + * 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 + */ +%} +{% if (messages.haveMessages() || (auth.isAdmin && messages.haveToDos())) { %} +
+ {{_R("messages")}} +
+ +{% } %} diff --git a/files/app/main/packages.ut b/files/app/main/packages.ut new file mode 100755 index 00000000..dd63ce0f --- /dev/null +++ b/files/app/main/packages.ut @@ -0,0 +1,37 @@ +<{% +/* + * 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 + */ +%} +div id="packages" hx-swap-oob="innerHTML"> +{{_R("packages", "oob")}} + diff --git a/files/app/main/status/e/basics.ut b/files/app/main/status/e/basics.ut new file mode 100755 index 00000000..f5e761cd --- /dev/null +++ b/files/app/main/status/e/basics.ut @@ -0,0 +1,340 @@ +{% +/* + * 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 getSshKeys() +{ + let options = ""; + const f = fs.open("/etc/dropbear/authorized_keys"); + if (f) { + const re = /^(.+) (.+) (.+)$/; + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(trim(l), re); + if (m) { + options += ``; + } + } + f.close(); + } + return options; +} +if (request.env.REQUEST_METHOD === "PUT") { + if ("theme" in request.args) { + const theme = request.args.theme; + if (fs.access(`${config.application}/resource/css/themes/${theme}.css`)) { + fs.unlink(`${config.application}/resource/css/theme.css`); + fs.symlink(`themes/${theme}.css`, `${config.application}/resource/css/theme.css`); + if (config.resourcehash) { + const themes = fs.lsdir(`${config.application}/resource/css/themes`); + const re = regexp(`^${theme}\.css\.(.+)\.gz`); + for (let i = 0; i < length(themes); i++) { + const m = match(themes[i], re); + if (m) { + fs.unlink(`${config.application}/resource/css/theme.version`); + fs.symlink(m[1], `${config.application}/resource/css/theme.version`); + break; + } + } + } + } + return; + } + configuration.prepareChanges(); + if ("description_node" in request.args) { + configuration.setSetting("description_node", replace(request.args.description_node || "", "'", "’")); + } + if ("notes" in request.args) { + uciMesh.set("aredn", "@notes[0]", "private", replace(request.args.notes || "", "'", "’")); + uciMesh.commit("aredn"); + } + if ("node_name" in request.args) { + const name = request.args.node_name; + if (match(name, constants.reNodename)) { + configuration.setName(name); + uciMesh.foreach("vtun", "server", s => { + const netip = s.netip; + if (index(net, ":") === -1) { + const np = split(netip, ":"); + const n = iptoarr(np[0]); + uciMesh.set("vtun", s[".name"], "node", `${uc(substr(name, 0, 23))}-${n[0]}-${n[1]}-${n[2]}-${n[3]}:${np[1]}`); + } + else { + const n = iptoarr(netip); + uciMesh.set("vtun", s[".name"], "node", `${uc(substr(name, 0, 23))}-${n[0]}-${n[1]}-${n[2]}-${n[3]}`); + } + }); + uciMesh.commit("vtun"); + } + } + if ("passwd" in request.args) { + configuration.setPassword(request.args.passwd); + } + if ("ssh_remove" in request.args) { + print(request.args); + const keys = split(fs.readfile("/etc/dropbear/authorized_keys"), "\n"); + const re = /^(.+) (.+) (.+)$/; + for (let i = 0; i < length(keys); i++) { + const m = match(keys[i], re); + if (m && m[3] === request.args.ssh_remove) { + splice(keys, i, 1); + break; + } + } + fs.writefile("/etc/dropbear/authorized_keys", join("\n", keys)); + print(getSshKeys()); + } + if ("ssh_add" in request.args) { + configuration.prepareChanges(); + const key = fs.readfile(request.args.ssh_add); + if (key && match(trim(key), /^(ssh-rsa|ecdsa-sha2-nistp256) [a-zA-Z0-9+\/=]+ .+$/)) { + const keys = fs.readfile("/etc/dropbear/authorized_keys") || ""; + fs.writefile("/etc/dropbear/authorized_keys", `${keys}${key}`); + } + print(getSshKeys()); + } + configuration.saveSettings(); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +%} +
+ {{_R("dialog-header", "Name & Security")}} +
+
+
+
Node Name
+
This node's unique name
+
+
+ +
+
+ {{_H("Change the node's unique name. The name must start with your callsign and be less than 64 characters long.")}} +
+
+
Description
+
Information about this node
+
+
+ +
+
+ {{_H("Some optional descriptive text about this node. This can be anything you think relevant. + People include various thing, such as some basic information about the location or hardware or + the services this node provides. Some people include alternate ways to reach the owner (e.g em‌ail + address).")}} +
+
+
Notes
+
Private notes about this node
+
+
+ +
+
+ {{_H("Private notes about this node which are only visible to the operator. This can be anything + thought relevant or useful. For example it might include information about custom configurations + or attached devices.")}} +
+
+
Theme
+
Display theme and colors
+
+
+ {% + const theme = fs.readlink(`${config.application}/resource/css/theme.css`); + const themes = fs.lsdir(`${config.application}/resource/css/themes`); + + %} + +
+
+ {{_H("Select the display theme for this node. This theme determines how everyone see this node when they visit. The + default theme automatically selects either Light or Dark depending on the viewers browser settings.")}} +
+
+
+
+
New Password
+
Change the node password
+
+
+ +
+
+
+
+
Retype Password
+
Passwords must match
+
+
+ +
+
+ {{_H("Set a new password for this device by entering it twice in the boxes. Don't use the # or any quote character. This password + is used for logging into the UI as well as telnet and ssh access.")}} +
+ {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
Upload SSH Key
+
Add SSH key
+
+
+ +
+
+
+
+
Remove SSH Key
+
Delete SSH key
+
+
+ +
+
+ {{_H("Console access to the node is possible using SSH. To avoid typing a password you can upload an SSH public key which allows + password-less login. Multiple keys can be uploaded above, and keys can also be selectively removed.")}} +
+
+
+ +
+
+ {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/changes.ut b/files/app/main/status/e/changes.ut new file mode 100755 index 00000000..39db6ea7 --- /dev/null +++ b/files/app/main/status/e/changes.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("changes")}} diff --git a/files/app/main/status/e/dhcp.ut b/files/app/main/status/e/dhcp.ut new file mode 100755 index 00000000..588be828 --- /dev/null +++ b/files/app/main/status/e/dhcp.ut @@ -0,0 +1,623 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + if ("options" in request.args) { + configuration.prepareChanges(); + const options = json(request.args.options); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.reservations, "w"); + if (f) { + const base = iptoarr(dhcp.start)[3]; + for (let i = 0; i < length(options); i++) { + const o = options[i]; + if (o.reserved) { + const ip = iptoarr(o.ip)[3]; + if (length(o.name) > 0 && ip >= base && match(o.mac, /^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$/)) { + f.write(`${o.mac} ${ip - base + 2} ${o.name}${o.noprop ? " #NOPROP" : ""}\n`); + } + } + } + f.close(); + } + } + if ("advtags" in request.args) { + configuration.prepareChanges(); + const advtags = json(request.args.advtags); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.dhcptags, "w"); + if (f) { + for (let i = 0; i < length(advtags); i++) { + const t = advtags[i]; + f.write(`${t.name} ${t.type} ${t.match}\n`); + } + f.close(); + } + } + if ("advoptions" in request.args) { + configuration.prepareChanges(); + const advoptions = json(request.args.advoptions); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.dhcpoptions, "w"); + if (f) { + for (let i = 0; i < length(advoptions); i++) { + const o = advoptions[i]; + f.write(`${o.name} ${o.always ? "force" : "onrequest"} ${o.type} ${o.value}\n`); + } + f.close(); + } + } + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const dhcp = configuration.getDHCP(); +const start = iptoarr(dhcp.start); +const end = iptoarr(dhcp.end); +const leases = []; +const options = []; +const advoptions = []; +const advtags = []; +for (let i = start[3]; i <= end[3]; i++) { + push(options, { mac: "", ip: `${start[0]}.${start[1]}.${start[2]}.${i}`, name: "", noprop: false, reserved: false, leased: false }); +} +let reservations = 0; +let active = 0; +let f = fs.open(dhcp.reservations); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + // mac, last-ip, name, flags + const v = match(trim(l), /^([^ ]+) ([^ ]+) ([^ ]+) ?(.*)/); + if (v) { + const o = options[int(v[2]) - 2]; + if (o) { + o.mac = v[1]; + o.name = v[3]; + o.noprop = v[4] == "#NOPROP"; + o.reserved = true; + reservations++; + } + } + } + f.close(); +} +f = fs.open(dhcp.leases); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + // ?, mac, ip, name, ? + const v = match(l, /^(.+) (.+) (.+) (.+) (.+)$/); + if (v) { + const ip = iptoarr(v[3]); + const o = options[ip[3] - start[3]]; + if (o) { + o.leased = true; + if (o.mac === "") { + o.mac = v[2]; + } + if (o.name === "") { + o.name = v[4]; + } + active++; + } + } + } + f.close(); +} +f = fs.open(dhcp.dhcptags); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(trim(l), /^(.+) (.+) (.+)$/); + if (m) { + push(advtags, { name: m[1], type: m[2], match: m[3] }); + } + } + f.close(); +} +f = fs.open(dhcp.dhcpoptions); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(trim(l), /^(.*) (force|onrequest) ([0-9]+) (.+)$/); + if (m) { + push(advoptions, { name: m[1], always: m[2] === "force", type: int(m[3]), value: m[4] }); + } + } + f.close(); +} +%} +
+ {{_R("dialog-header", "LAN DHCP")}} +
+
+
+
+
+
Address Reservations
+
Hostnames with fixed addresses
+
+ +
+
+ {{_H("Creates a permenant mapping between a device MAC address and an IP address on the LAN network. + The given hostname is available to everyone on the mesh unless the entry is marked as do not propagate")}} +
+
+
+
hostname
+
ip address
+
mac a‌ddress
+
do not propagate
+
+
+
+ {% if (reservations > 0) { + for (let i = 0; i < length(options); i++) { + const o = options[i]; + if (o.reserved) { + %} +
+
+ + + + +
+ +
+ {% } + } + } %} +
+
+
Active Leases
+
Addresses currently in use
+ {{_H("The list of active leases currently allocated to LAN devices. Any of these leases can be promoted + to a permanent mapping to allow IP Addresses to be fixed to specific devices.")}} + {% if (active > 0) { + %} +
+
+
hostname
+
ip address
+
mac a‌ddress
+
+
+
+ {% + for (let i = 0; i < length(options); i++) { + const o = options[i]; + if (o.leased) { + %} +
+
+ + + +
+ +
+ {% } + } + } %} +
+ {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
+
Tags
+
Tags for advanced options
+
+ +
+
+
+
tag
+
type
+
match
+
+
+
{% + for (let i = 0; i < length(advtags); i++) { + const t = advtags[i]; + %}
+
+ + + +
+ +
{% + } + %}
+
+
+
+
+
Options
+
Advanced options
+
+ +
+
+
+
tag
+
option
+
value
+
always
+
+
+
{% + for (let i = 0; i < length(advoptions); i++) { + const o = advoptions[i]; + %}
+
+ + + + +
+ +
{% + } + %}
+
+ {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/firmware.ut b/files/app/main/status/e/firmware.ut new file mode 100755 index 00000000..ddfd687a --- /dev/null +++ b/files/app/main/status/e/firmware.ut @@ -0,0 +1,616 @@ +{% +/* + * 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 name = filename ? filename : `/tmp/download.${time()}`; + const f = fs.popen(`/usr/bin/curl --progress-bar -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(); + configuration.setUpgrade("1"); + if (system("tar -czf /tmp/arednsysupgradebackup.tgz -T /tmp/sysupgradefilelist > /dev/null 2>&1") < 0) { + fs.unlink("/tmp/arednsysupgradebackup.tgz"); + } + configuration.setUpgrade("0"); + 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.upgrade = 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.upgrade = upgrade.upgrade; + print(_R("reboot-firmware")); + } + } + 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: missing firmware config\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 load firmware versions\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) { + const info = json(f.read("all")); + f.close(); + 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; + } + const list = json(f.read("all")); + f.close(); + 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 find firmware\r\n\r\n`); + return; + } + const overview = json(f.read("all")); + f.close(); + let fwimage = null; + + let booter_version = null; + const bv = fs.open("/sys/firmware/mikrotik/soft_config/bios_version"); + if (bv) { + const v = bv.read("all"); + bv.close(); + if (substr(v, 2) === "7.") { + 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\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.upgrade = 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"); +%} +
+ {{_R("dialog-header", "Firmware")}} +
+ {{_R("dialog-messages")}} +
+
+
Firmware
+
Current firmware version
+
+
+ +
+
+
+
+
Hardware
+
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.")}} + {{_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" )}} + +
diff --git a/files/app/main/status/e/internal-services.ut b/files/app/main/status/e/internal-services.ut new file mode 100755 index 00000000..9d3e1e49 --- /dev/null +++ b/files/app/main/status/e/internal-services.ut @@ -0,0 +1,261 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if (request.args.cloudmesh) { + uciMesh.set("aredn", "@supernode[0]", "support", request.args.cloudmesh === "on" ? "1" : "0"); + } + if (request.args.iperf) { + uciMesh.set("aredn", "@iperf[0]", "enable", request.args.iperf === "on" ? "1" : "0"); + } + if (request.args.ssh_access) { + uciMesh.set("aredn", "@wan[0]", "ssh_access", request.args.ssh_access === "on" ? "1" : "0"); + } + if (request.args.telnet_access) { + uciMesh.set("aredn", "@wan[0]", "telnet_access", request.args.telnet_access === "on" ? "1" : "0"); + } + if (request.args.web_access) { + uciMesh.set("aredn", "@wan[0]", "web_access", request.args.web_access === "on" ? "1" : "0"); + } + if ("remotelog" in request.args) { + uciMesh.set("aredn", "@remotelog[0]", "url", request.args.remotelog); + } + if (request.args.watchdog) { + uciMesh.set("aredn", "@watchdog[0]", "enable", request.args.watchdog === "on" ? "1" : "0"); + } + if ("watchdog_ping_address" in request.args) { + uciMesh.set("aredn", "@watchdog[0]", "ping_addresses", request.args.watchdog_ping_address); + } + if ("watchdog_daily" in request.args) { + uciMesh.set("aredn", "@watchdog[0]", "daily", request.args.watchdog_daily); + } + if ("power_poe" in request.args) { + uciMesh.set("aredn", "@poe[0]", "passthrough", request.args.power_poe === "on" ? "1" : "0"); + } + if ("power_usb" in request.args) { + uciMesh.set("aredn", "@usb[0]", "passthrough", request.args.power_usb === "on" ? "1" : "0"); + } + if ("message_pollrate" in request.args) { + uciMesh.set("aredn", "@alerts[0]", "pollrate", request.args.message_pollrate); + } + if ("message_localpath" in request.args) { + uciMesh.set("aredn", "@alerts[0]", "localpath", request.args.message_localpath); + } + if ("message_groups" in request.args) { + uciMesh.set("aredn", "@alerts[0]", "groups", request.args.message_groups); + } + uciMesh.commit("aredn"); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +%} +
+ {{_R("dialog-header", "Internal Services")}} +
+
+
+
Cloud Mesh
+
Use any Supernodes found on the mesh.
+
+
+ {{_R("switch", { name: "cloudmesh", value: uciMesh.get("aredn", "@supernode[0]", "support") !== "0" })}} +
+
+ {{_H("By default, your node will locate and use any available supernode it finds on your local mesh. + This allows you to connect to any node in the AREDN cloud. Disable this option if you don't want to connect.")}} +
+
+
iPerf3 Server
+
Enable the iperf3 server for easy connection speed testing
+
+
+ {{_R("switch", { name: "iperf", value: uciMesh.get("aredn", "@iperf[0]", "enable") !== "0" })}} +
+
+ {{_H("Enable the included iperf3 client and server tools. This makes it easy to perform bandwidth tests between arbitrary nodes + in the network. The client and server are only invoked on demand, so there is no performance impact on the node except when tests + are performed.")}} +
+
+
Remote logging
+
Send internal logging information to a remove server
+
+
+ +
+
+ {{_H("Local node logs are kept in a limited amount of RAM which means older information is lost, and all logs are lost on reboot. + This options will send the logs to a remote log server using the syslog protocol. The option should be udp://ip-address:port or tcp://ip-adress:port. + Leave blank if no remote logging is required.")}} +
+
+
WAN ssh
+
Allow ssh access to node from the WAN interface
+
+
+ {{_R("switch", { name: "ssh_access", value: uciMesh.get("aredn", "@wan[0]", "ssh_access") !== "0" })}} +
+
+ {{_H("Allow access to the node via the WAN network using ssh. Disabling this option will not prevent ssh acccess to the node from the mesh.")}} +
+
+
WAN telnet
+
Allow telnet access to node from the WAN interface
+
+
+ {{_R("switch", { name: "telnet_access", value: uciMesh.get("aredn", "@wan[0]", "telnet_access") !== "0" })}} +
+
+ {{_H("Allow access to the node via the WAN network using telnet. Disabling this option will not prevent telnet acccess to the node from the mesh.")}} +
+
+
WAN web
+
Allow web access to node from the WAN interface
+
+
+ {{_R("switch", { name: "web_access", value: uciMesh.get("aredn", "@wan[0]", "web_access") !== "0" })}} +
+
+ {{_H("Allow access to the node's web interface via the WAN network. Disabling this option will not prevent web acccess to the node from the mesh.")}} +
+
+
+
Watchdog
+
Allow watchdog to reboot the node if it becomes unresponsive
+
+
+ {{_R("switch", { name: "watchdog", value: uciMesh.get("aredn", "@watchdog[0]", "enable") === "1" })}} +
+
+ {{_H("Enables the hardware watchdog timer. This timer will reboot the device if it becomes unresponsive or various critical AREDN components + stop running correctly. Because the watchdog is in the hardware, even if the kernel crashes, the device will still reboot itself. + ")}} +
+
+
+
Watchdog IP address
+
IP address to check periodically
+
+
+ +
+
+ {{_H("As part of the watchdog process, the watchdog periodically makes sure it can reach a given IP address. If it can't the node will be rebooted. + It is important that this IP address is easy and quick to reach. Don't try to reach IP addresses on the Internet.")}} +
+
+
Daily Watchdog hour
+
Reboot the node at a specific hour every day
+
+
+ +
+
+ {{_H("For the node to reboot at a specific hour of the day (between 0 and 23), every day. Hopefully this isn't necessary. but it can be a good fallback for nodes which + are unreliable and in places difficult to reach.")}} +
+
+ {% if (hardware.hasPOE()) { %} +
+
+
PoE Passthrough
+
Enable power-over-ethernet on ports which support it
+
+
+ {{_R("switch", { name: "power_poe", value: uciMesh.get("aredn", "@poe[0]", "passthrough") !== "0" })}} +
+
+ {{_H("Enable power over ethernet on ethernet ports where this is avaiable. Typically these ports provide passive power, + so the voltage out will be the same as whatever is powering the node.")}} + {% } + if (hardware.hasUSBPower()) { %} +
+
+
USB Power
+
Enable USB power on ports which support it
+
+
+ {{_R("switch", { name: "power_usb", value: uciMesh.get("aredn", "@usb[0]", "passthrough") !== "0" })}} +
+
+ {{_H("Enable power on the node's USB port. Which ports support this is device dependend, and some devices with USB + port may only have some with power available.")}} + {% } %} +
+
+
Message Updates
+
Update messages every so many hours
+
+
+ +
+
+ {{_H("Change the frequency we fetch messsage for this node. By default this happens every hour, but you can decrese the frequency up to + every 24 hours.")}} +
+
+
Local Message URL
+
Configure the local message sources
+
+
+ +
+
+ {{_H("Add a local message server URL. By default messages are fetched from the global AREDN server, but you can also specify a + local server.")}} +
+
+
Message Groups
+
List of message group names
+
+
+ +
+
+ {{_H("A comma seperated list of group names to check for messages.")}} +
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/local-services.ut b/files/app/main/status/e/local-services.ut new file mode 100755 index 00000000..a84cceb1 --- /dev/null +++ b/files/app/main/status/e/local-services.ut @@ -0,0 +1,616 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("services" in request.args) { + const services = json(request.args.services); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.services, "w"); + if (f) { + for (let i = 0; i < length(services); i++) { + f.write(`${services[i]}\n`); + } + f.close(); + } + } + if ("aliases" in request.args) { + const aliases = json(request.args.aliases); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.aliases, "w"); + if (f) { + for (let i = 0; i < length(aliases); i++) { + f.write(`${aliases[i]}\n`); + } + f.close(); + } + } + if ("ports" in request.args) { + const ports = json(request.args.ports); + const dhcp = configuration.getDHCP(); + let f = fs.open(dhcp.ports, "w"); + if (f) { + for (let i = 0; i < length(ports); i++) { + f.write(`${ports[i]}\n`); + } + f.close(); + } + } + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const types = { + mail: true, + router: true, + switch: true, + radio: true, + solar: true, + battery: true, + power: true, + weather: true, + wiki: true, +}; +const templates = [ + { name: "Generic URL" }, + { name: "Simple text", link: false }, + { name: "Ubiquiti camera", type: "camera", protocol: "http", port: 80, path: "snap.jpeg" }, + { name: "Reolink camera", type: "camera", protocol: "http", port: 80, path: "cgi-bin/api.cgi?cmd=Snap&channel=0&rs=abcdef&user=USERNAME&password=PASSWORD" }, + { name: "Axis camera", type: "camera", protocol: "http", port: 80, path: "jpg/image.jpg" }, + { name: "Sunba Lite camera", type: "camera", protocol: "http", port: 80, path: "webcapture.jpg?command=snap&channel=1&user=USERNAME&password=PASSWORD" }, + { name: "Sunba Performance camera", type: "camera", protocol: "http", port: 80, path: "images/snapshot.jpg" }, + { name: "Sunba Smart camera", type: "camera", protocol: "http", port: 80, path: "ISAPI/Custom/snapshot?authInfo=USERNAME:PASSWORD" }, + { name: "NTP Server", type: "time", protocol: "ntp", port: 123 }, + { name: "Streaming video", type: "video", protocol: "rtsp", port: 554 }, + { name: "Winlink Server", type: "winlink", protocol: "winlink", port: 8772 }, + { name: "Phone info", type: "phone", link: false, text: "xYOUR-PHONE-EXTENSION" }, + { name: "Map", type: "map", protocol: "http", port: 80 }, + { name: "MeshChat", type: "chat", protocol: "http", port: 80, path: "meshchat" }, + { name: "WeeWx Weather Station", type: "weather", protocol: "http", port: 80, path: "weewx" }, + { name: "Proxmox Server", type: "server", protocol: "https", port: 8006 }, + { name: "Generic HTTP", protocol: "http", port: 80 }, + { name: "Generic HTTPS", protocol: "https", port: 443 }, +]; +const services = []; +const dhcp = configuration.getDHCP(); +const as = iptoarr(dhcp.start); +const ae = iptoarr(dhcp.end); +let f = fs.open(dhcp.services); +if (f) { + const reService = regexp("^([^|]+)\\|1\\|([^|]+)\\|([^|]+)\\|(\\d+)\\|(.*)$"); + const reLink = regexp("^([^|]+)\\|0\\|\\|([^|]+)\\|\\|$"); + const reType = regexp("^(.+) \\[([a-z]+)\\]$"); + for (let l = f.read("line"); length(l); l = f.read("line")) { + const v = match(trim(l), reService); + if (v) { + let type = null; + const v2 = match(v[1], reType); + if (v2) { + v[1] = v2[1]; + type = v2[2]; + } + push(services, { name: v[1], type: type, link: true, protocol: v[2], hostname: v[3], port: v[4], path: v[5] }); + } + else { + const k = match(trim(l), reLink); + if (k) { + let type = null; + const k2 = match(k[1], reType); + if (k2) { + k[1] = k2[1]; + type = k2[2]; + } + push(services, { name: k[1], type: type, link: false, hostname: k[2] }); + } + } + } + f.close(); +} +const hosts = [ configuration.getName() ]; +const aliases = { start: dhcp.start, end: dhcp.end, leases: {}, map: [] }; +f = fs.open(dhcp.reservations); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const v = match(trim(l), /^[^ ]+ ([^ ]+) ([^ ]+) ?(.*)/); + if (v) { + aliases.leases[`${as[0]}.${as[1]}.${as[2]}.${as[3] - 2 + int(v[1])}`] = v[2]; + if (v[3] !== "#NOPROP") { + push(hosts, v[2]); + } + } + } + f.close(); +} +f = fs.open(dhcp.aliases); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const v = match(trim(l), /^(.+) (.+)$/); + if (v) { + push(aliases.map, { hostname: v[2], address: v[1] }); + } + } + f.close(); +} +const ports = []; +f = fs.open(dhcp.ports); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(trim(l), /^(wan|wifi|both):(tcp|udp|both):([0-9\-]+):([.0-9]+):([0-9]+):([01])$/); + if (m) { + push(ports, { src: m[1], type: m[2], sports: m[3], dst: m[4], dport: m[5], enabled: m[6] === "1" }); + } + } + f.close(); +} +%} +
+ {{_R("dialog-header", "Node Services")}} +
+
+
+
Add service
+
Add a service from a template
+
+
+ +
+ +
+ {{_H("Create a service by selecting from the templates above and hitting +. The two most generic templates are + Generic URL and Simple text and can be used for any services. You can also use the other templates to + help create services for specific cameras, mail servers, phones, etc. and these will pre-populate various service fields.")}} +
{% + for (let i = 0; i < length(services); i++) { + const s = services[i]; + %} +
+
+
+ +
+ +
+
+ +
+
+
+ {% if (s.link === false) { %} + + {% } else { %} +
+ + :// + : + / +
+ {% } %} +
+
+ {% } %}
+
+
+
+
Host aliases
+
DNS hostname aliases
+
+ +
+ {{_H("Assign a hostname to an LAN IP address on this node. Multiple hostnames can be assigned to the same IP address. You are + encouraged to prefix your hostnames with your callsign so it will be unique on the network.")}} +
{% + for (let i = 0; i < length(aliases.map); i++) { + %} +
+ +
+ +
+ +
+ {% + } + %}
+
+
+
+
Port Forwarding
+ {% if (dhcp.mode === 0) { %} +
Forward WAN and Mesh ports to LAN
+ {% } else { %} +
Forward WAN port to LAN
+ {% } %} +
+ +
+ {% if (dhcp.mode === 0) { %} + {{_H("For specific ports (or port ranges) from the WAN or Mesh network to the LAN network. TCP, UDP or both kinds of + traffic may be included. When forwarding a port range, specify the range as start-end.")}} + {% } else { %} + {{_H("For specific ports (or port ranges) from the WAN network to the LAN network. TCP, UDP or both kinds of + traffic may be included. When forwarding a port range, specify the range as start-end.")}} + {% } %} +
+
+
addresses
+
ports
+
protocol
+
enabled
+
+
+
{% + for (let i = 0; i < length(ports); i++) { + const p = ports[i]; + %}
+
+
+ {% if (dhcp.mode === 0) { %} + + {% } else { %} + + {% } %} + + + +
+
+ + +
+
+ +
{% + } + %}
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/location.ut b/files/app/main/status/e/location.ut new file mode 100755 index 00000000..48450cc3 --- /dev/null +++ b/files/app/main/status/e/location.ut @@ -0,0 +1,242 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("gridsquare" in request.args) { + if (match(request.args.gridsquare, /^[A-X][A-X]\d\d[a-x][a-x]$/)) { + uciMesh.set("aredn", "@location[0]", "gridsquare", request.args.gridsquare); + } + else if (request.args.gridsquare === "") { + uciMesh.set("aredn", "@location[0]", "gridsquare", ""); + } + } + if ("lat" in request.args) { + if (request.args.lat > -90 && request.args.lat < 90) { + uciMesh.set("aredn", "@location[0]", "lat", request.args.lat); + } + else if (request.args.lat === "") { + uciMesh.set("aredn", "@location[0]", "lat", ""); + } + uciMesh.set("aredn", "@location[0]", "source", ""); + } + if ("lon" in request.args) { + if (request.args.lon > -180 && request.args.lon < 180) { + uciMesh.set("aredn", "@location[0]", "lon", request.args.lon); + } + else if (request.args.lon === "") { + uciMesh.set("aredn", "@location[0]", "lon", ""); + } + uciMesh.set("aredn", "@location[0]", "source", ""); + } + if (request.args.gps_enable) { + uciMesh.set("aredn", "@location[0]", "gps_enable", request.args.gps_enable === "on" ? "1" : "0"); + } + if ("mapurl" in request.args) { + if (match(request.args.mapurl, constants.reUrl)) { + uciMesh.set("aredn", "@location[0]", "map", request.args.mapurl); + } + } + uciMesh.commit("aredn"); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const map = uciMesh.get("aredn", "@location[0]", "map"); +const lat = uciMesh.get("aredn", "@location[0]", "lat") ?? 37; +const lon = uciMesh.get("aredn", "@location[0]", "lon") ?? -122; +const gridsquare = uciMesh.get("aredn", "@location[0]", "gridsquare"); +const mapurl = map ? replace(replace(map, "(lat)", lat), "(lon)", lon) : null; +%} +
+ {{_R("dialog-header", "Location")}} +
+ {% if (mapurl) { %} +
+
+ +
+ {% } %} +
+
+
Latitude
+
Node's latitude
+
+
+ +
+
+ {{_H("The latitude of this node. This information is used to determine the distance between this node and others and is required to + optimize connection latency and bandwidth.")}} +
+
+
Longitude
+
Node's longitude
+
+
+ +
+
+ {{_H("The longitude of this node. This information is used to determine the distance between this node and others and is required to + optimize connection latency and bandwidth.")}} +
+
+
Gridsquare
+
Maidenhead gridsquare
+
+
+ +
+
+ {{_H("A gridsquare is a 6 character (2 uppercase letters, 2 digits, 2 lowercase letters) designation of the node's location. + A gridsquare is approximately 3x4 miles in size.")}} + {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
GPS Location
+
Use local or network GPS to set location
+
+
+ {{_R("switch", { name: "gps_enable", value: uciMesh.get("aredn", "@location[0]", "gps_enable") === "1" })}} +
+
+ {{_H("Use either a local GPS devices to set the location, or search for a GPS device on another local node, and use its + GPS to set our location.")}} +
+
+
Map URL
+
URL for embedded map
+
+
+ +
+
+ {{_H("The map URL is used to embed maps in this page (see above) and in other node pages. The (lat) and (lon) parameters in + the URL are expanded before the URL is used.")}} + {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/neighbor-device.ut b/files/app/main/status/e/neighbor-device.ut new file mode 100755 index 00000000..d2a5f71c --- /dev/null +++ b/files/app/main/status/e/neighbor-device.ut @@ -0,0 +1,371 @@ +{% +/* + * 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 + */ +%} +{% if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + const userb = split(uciMesh.get("aredn", "@lqm[0]", "user_blocks"), ","); + const blockedmacs = {}; + for (let i = 0; i < length(userb); i++) { + blockedmacs[uc(userb[i])] = true; + } + const usera = split(uciMesh.get("aredn", "@lqm[0]", "user_allows"), ","); + const allowedmacs = {}; + for (let i = 0; i < length(usera); i++) { + allowedmacs[uc(usera[i])] = true; + } + for (let mac in request.args) { + if (request.args[mac] === "always") { + delete blockedmacs[uc(mac)]; + allowedmacs[uc(mac)] = true; + } + else if (request.args[mac] === "user") { + blockedmacs[uc(mac)] = true; + delete allowedmacs[uc(mac)]; + } + else { + delete blockedmacs[uc(mac)]; + delete allowedmacs[uc(mac)]; + } + } + uciMesh.set("aredn", "@lqm[0]", "user_blocks", join(",", keys(blockedmacs))); + uciMesh.set("aredn", "@lqm[0]", "user_allows", join(",", keys(allowedmacs))); + uciMesh.commit("aredn"); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const selected = substr(request.env.QUERY_STRING, 2); +const blockedmacs = {}; +const user = split(uciMesh.get("aredn", "@lqm[0]", "user_blocks"), ","); +for (let i = 0; i < length(user); i++) { + blockedmacs[uc(user[i])] = true; +} +const usera = split(uciMesh.get("aredn", "@lqm[0]", "user_allows"), ","); +const allowedmacs = {}; +for (let i = 0; i < length(usera); i++) { + allowedmacs[uc(usera[i])] = true; +} +const lqmInfo = lqm.get(); +const trackers = lqm.getTrackers(); +const tracker = trackers[selected]; +let neighbor = null; +if (tracker) { + if (allowedmacs[uc(tracker.mac)]) { + tracker.user_allow = true; + } + else if (blockedmacs[uc(tracker.mac)]) { + tracker.blocks.user = true; + } + neighbor = { name: tracker.hostname || `|${tracker.ip}`, n: tracker, l: null }; + const o = olsr.getLinks(); + for (let i = 0; i < length(o); i++) { + if (o[i].remoteIP === tracker.ip) { + neighbor.l = o[i]; + break; + } + } +} +%} +
+ {{_R("dialog-header", "Neighborhood Device")}} +
+ {{_H("Provides more detailed information about the state of a link from this node to a neighbor. The current blocked + state is show in the top/right corner. This can be changed to either always block or never block to override + the automatic management of the link's use.")}} + {% + function state(n) + { + if (n.lastseen < lqmInfo.now) { + return "disconnected"; + } + if (n.blocks.user) { + return "blocked by user"; + } + if (n.blocked) { + if (n.blocks.signal) { + return "blocked: low snr"; + } + if (n.blocks.distance) { + return "blocked: too far away"; + } + if (n.blocks.quality) { + if ("tx_quality" in n) { + if ("ping_quality" in n && n.ping_quality < n.tx_quality) { + return "blocked: poor tx latency"; + } + else { + return "blocked: too many tx error"; + } + } + else { + return "blocked: poor tx latency"; + } + } + if (n.blocks.dup || n.blocks.dtd) { + return "blocked: duplicate link"; + } + return "blocked"; + } + if (n.routable) { + if ((n.blocks.signal || n.blocks.distance || n.blocks.quality) && ( + (n.leaf === "major" && n.rev_leaf === "minor") || (n.leaf === "minor" && n.rev_leaf === "major"))) { + return "routing leaf"; + } + return "routing"; + } + return "unused"; + } + function map(n, v) + { + const map_url = uci.get("aredn", "@location[0]", "map"); + if (n.lat && n.lon && map_url) { + return `${v}`; + } + return v; + } + if (neighbor) { + const n = neighbor.n; + const l = neighbor.l; + %} +
+
{{n.hostname || n.ip}} + +
+
+
+
{{n.type}}
+
type
+
+
+
{{n.mac}}
+
mac address
+
+
+
{{n.ip}}
+
ip address
+
+
+ {% if (n.model && n.firmware_version) { %} +
+
+
{{n.model}}
+
model
+
+
+
{{n.firmware_version}}
+
firmware
+
+
+
+
+ {% } %} +
+
+
{{map(n, n.lat) || "-"}}
+
latitude
+
+
+
{{map(n, n.lon) || "-"}}
+
longitude
+
+
+
{{"distance" in n ? map(n, sprintf("%.1f %s", units.meters2distance(n.distance), units.distanceUnit())) : "-"}}
+
distance
+
+
+ {% + if (l && l.lossMultiplier) { + const lq = int(100 * l.linkQuality * 65536 / l.lossMultiplier); + const nlq = int(100 * l.neighborLinkQuality * 65536 / l.lossMultiplier); + const etx = 10000.0 / (lq * nlq); + %} +
+
+
{{lq}}%
+
lq | rx success
+
+
+
{{nlq}}%
+
nlq | tx success
+
+
+
{{sprintf("%.1f", etx)}}
+
etx
+
+
+ {% } %} +
+
+
{{type(n.ping_success_time) ? sprintf("%.1f ms", n.ping_success_time * 1000) : "-"}}
+
ping time
+
+
+
{{type(n.ping_quality) ? sprintf("%d%%", n.ping_quality) : "-"}}
+
ping success
+
+
+
{{type(n.avg_tx) ? sprintf("%.1f pkt/sec", n.avg_tx / 60) : "-"}}
+
avg tx
+
+
+ {% if (n.type == "RF") { %} +
+
+
{{n.snr}}
+
local snr
+
+
+
{{n.rev_snr || "-"}}
+
neighbor snr
+
+
+
{{n.avg_tx_fail ? sprintf("%.1f%%", 100 * n.avg_tx_fail / n.avg_tx) : "-"}}
+
tx failures
+
+
+
+
+
{{n.rx_bitrate ? sprintf("%.1f Mbps", n.rx_bitrate) : "-"}}
+
physical rx bitrate
+
+
+
{{n.tx_bitrate ? sprintf("%.1f Mbps", n.tx_bitrate) : "-"}}
+
physical tx bitrate
+
+
+
{{n.avg_tx_retries ? sprintf("%.1f%%", 100 * n.avg_tx_retries / n.avg_tx) : "-"}}
+
tx retransmissions
+
+
+ {% } else if (type(n.avg_tx_fail)) { %} +
+
+
{{sprintf("%.1f%%", 100 * n.avg_tx_fail / n.avg_tx)}}
+
tx failures
+
+
+
+
+ {% } %} +
+
+
{{state(n)}}
+
state
+
+
+
{{n.node_route_count}}
+
active routes
+
+
+
+ {% + const snr = `/tmp/snrlog/${uc(selected)}-${lc(n.hostname)}`; + if (fs.access(snr)) { + let signal = ""; + noise += "' />"; + %} +
+ + + + dBm + 0 + -20 + -40 + -60 + -80 + -100 + -120 + + {{signal}}{{noise}} + {{hints}} + +
+ {% } %} +
+ {% } %} +
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/network.ut b/files/app/main/status/e/network.ut new file mode 100755 index 00000000..9a9c0493 --- /dev/null +++ b/files/app/main/status/e/network.ut @@ -0,0 +1,391 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("mesh_ip" in request.args) { + if (match(request.args.mesh_ip, constants.reIP)) { + configuration.setSetting("wifi_ip", request.args.mesh_ip); + } + } + if ("dhcp_mode" in request.args) { + const mode = int(request.args.dhcp_mode || 3); + if (mode === -1) { + configuration.setSetting("lan_dhcp", 0); + } + else if (mode >= 0 && mode <= 5) { + configuration.setSetting("lan_dhcp", 1); + configuration.setSetting("dmz_mode", mode); + } + } + if ("lan_dhcp_ip" in request.args) { + if (match(request.args.lan_dhcp_ip, constants.reIP)) { + configuration.setSetting("lan_ip", request.args.lan_dhcp_ip); + } + } + if ("lan_dhcp_netmask" in request.args) { + if (match(request.args.lan_dhcp_netmask, constants.reNetmask)) { + configuration.setSetting("lan_mask", request.args.lan_dhcp_mask); + } + } + if ("lan_dhcp_start" in request.args) { + if (match(request.args.lan_dhcp_start, /^([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$/)) { + configuration.setSetting("dhcp_start", request.args.lan_dhcp_start); + } + } + if ("lan_dhcp_end" in request.args) { + if (match(request.args.lan_dhcp_end, /^([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])$/)) { + configuration.setSetting("dhcp_end", request.args.lan_dhcp_end); + } + } + if ("wan_enable" in request.args) { + if (request.args.wan_enable === "off") { + configuration.setSetting("wan_proto", "disabled"); + } + else { + configuration.setSetting("wan_proto", "dhcp"); + } + } + if ("wan_mode" in request.args) { + if (request.args.wan_mode === "0") { + configuration.setSetting("wan_proto", "dhcp"); + } + else { + configuration.setSetting("wan_proto", "static"); + } + } + if ("wan_ip" in request.args) { + if (match(request.args.wan_ip, /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) { + configuration.setSetting("wan_ip", request.args.wan_ip); + } + } + if ("wan_mask" in request.args) { + if (match(request.args.wan_mask, /^(((255\.){3}(255|254|252|248|240|224|192|128|0+))|((255\.){2}(255|254|252|248|240|224|192|128|0+)\.0)|((255\.)(255|254|252|248|240|224|192|128|0+)(\.0+){2})|((255|254|252|248|240|224|192|128|0+)(\.0+){3}))$/)) { + configuration.setSetting("wan_mask", request.args.wan_mask); + } + } + if ("wan_gw" in request.args) { + if (match(request.args.wan_gw, /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) { + configuration.setSetting("wan_gw", request.args.wan_gw); + } + } + if ("wan_dns1" in request.args) { + if (match(request.args.wan_dns1, /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) { + configuration.setSetting("wan_dns1", request.args.wan_dns1); + } + } + if ("wan_dns2" in request.args) { + if (match(request.args.wan_dns2, /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) { + configuration.setSetting("wan_dns2", request.args.wan_dns2); + } + } + if ("wan_vlan" in request.args) { + const wan_iface = split(configuration.getSettingAsString("wan_intf", ""), "."); + const wan_vlan = int(request.args.wan_vlan || 0); + if (wan_vlan === 0) { + configuration.setSetting("wan_intf", wan_iface[0]); + } + else if (wan_vlan >= 3 && wan_vlan <= 4095) { + configuration.setSetting("wan_intf", `${wan_iface[0]}.${wan_vlan}`); + } + } + if ("olsrd_gw" in request.args) { + uciMesh.set("aredn", "@wan[0]", "olsrd_gw", request.args.olsrd_gw === "on" ? 1 : 0); + } + if ("lan_dhcp_route" in request.args) { + uciMesh.set("aredn", "@wan[0]", "lan_dhcp_route", request.args.lan_dhcp_route === "on" ? 1 : 0); + } + if ("lan_dhcp_defaultroute" in request.args) { + uciMesh.set("aredn", "@wan[0]", "lan_dhcp_defaultroute", request.args.lan_dhcp_defaultroute === "on" ? 1 : 0); + } + uciMesh.commit("aredn"); + configuration.saveSettings(); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const dmz_mode = configuration.getSettingAsInt("dmz_mode", 3); +const dhcp = configuration.getDHCP("nat"); +const wan_proto = configuration.getSettingAsString("wan_proto", "disabled"); +const wan_iface = split(configuration.getSettingAsString("wan_intf", ""), "."); +const wan_vlan = wan_iface[1] ? wan_iface[1] : ""; +const gateway_nat = dmz_mode !== 1 ? dhcp.gateway : "172.27.0.1"; +const gateway_altnet = dmz_mode === 1 ? dhcp.gateway : ""; +%} +
+ {{_R("dialog-header", "Network")}} +
+
+
+
Mesh A‌ddress
+
The primary address of this node
+
+
+ +
+
+ {{_H("The primary address of this AREDN node. This will always be of the form 10.X.X.X. The address is generated automatically based on hardware information so it will + always be the same even if you reinstall this node from scratch. You shouldn't have to change it.")}} +
+
+
+
+
LAN Size
+
Size of LAN subnet
+
+
+ +
+
+ {{_H("Select how many hosts you want to support on this nodes LAN network. This determines the size of the netmask associated with that network. + You can also select NAT which allows more hosts, firewalls your LAN hosts from the Mesh network, but requires explicity ports to be forwarded when + creating services.")}} +
+
+
+
IP A‌ddress
+
Gateway IP a‌ddress for this LAN network
+
+
+ +
+
+
+
+
Netmask
+
Netmask for this LAN network
+
+
+ +
+
+
+
+
DHCP Start
+
Start of the DHCP range for addresses allocate
+
+
+ +
+
+
+
+
DHCP End
+
Last address of the DHCP range for addresses allocated
+
+
+ +
+
+
+
+
+
+
AltNET IP A‌ddress
+
Gateway IP a‌ddress for AltNET LAN network
+
+
+ +
+
+
+
+
Netmask
+
Netmask for AltNET LAN network
+
+
+ +
+
+
+
+
DHCP Start
+
Start of the DHCP range for addresses allocate
+
+
+ +
+
+
+
+
DHCP End
+
Last address of the DHCP range for addresses allocated
+
+
+ +
+
+
+
+
+
+
+
+
WAN Enable
+
Allow node to directly access the Internet
+
+
+ {{_R("switch", { name: "wan_enable", value: wan_proto === "disabled" ? false : true })}} +
+
+ {{_H("Enable the WAN interface on this node, to allow it to access the Internet directly.")}} +
+
+
+
Mode
+
Static or DHCP mode
+
+
+ +
+
+ {{_H("The WAN interface can either use DHCP to retrieve an IP address, or it can be set statically.")}} +
+
+
+
A‌ddress
+
WAN IP a‌ddress
+
+
+ +
+
+ {{_H("A fixed IP address to assign to the WAN interace on this node.")}} +
+
+
Netmask
+
WAN netmask
+
+
+ +
+
+ {{_H("The netmask (e.g. 255.255.255.0) for this interface.")}} +
+
+
Gateway
+
Default gateway
+
+
+ +
+
+ {{_H("The default gateway his node should use to access the Internet.")}} +
+
+
+
+
+
+
DNS
+
Internet DNS servers
+
+
+ + +
+
+ {{_H("For hosts not on the Mesh, use these DNS servers to resolve names to IP addresses.")}} + {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} + {% if (length(hardware.getEthernetPorts()) < 2) { %} +
+
+
WAN VLAN
+
Vlan used for Internet access
+
+
+ +
+
+ {{_H("By default, the WAN uses an untagged VLAN on multi-port devices, and VLAN 1 on single port devices. This can be changed here if your setup requires something different. + Enter the VLAN number required, or leave blank for untagged.")}} + {% } %} +
+
+
Mesh to WAN
+
Allow any mesh device to use local WAN.
+
+
+ {{_R("switch", { name: "olsrd_gw", value: uciMesh.get("aredn", "@wan[0]", "olsrd_gw") !== "0" })}} +
+
+ {{_H("Allow any node or device on the mesh to use our local Internet connection. This is disabled by default.")}} +
+
+
LAN to WAN
+
Allow any LAN device to use local WAN.
+
+
+ {{_R("switch", { name: "lan_dhcp_route", value: uciMesh.get("aredn", "@wan[0]", "lan_dhcp_route") !== "0" })}} +
+
+ {{_H("Allow LAN devices connected to this node to use our Internet connection. This is enabled by default.")}} +
+
+
LAN default route
+
Provide LAN devices with a default route.
+
+
+ {{_R("switch", { name: "lan_dhcp_defaultroute", value: uciMesh.get("aredn", "@wan[0]", "lan_dhcp_defaultroute") !== "0" })}} +
+
+ {{_H("Provide a default route to a LAN connected device, even if our local WAN is disabled. By default this is only provided to + devices if our local WAN is available.")}} + {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/packages.ut b/files/app/main/status/e/packages.ut new file mode 100755 index 00000000..3d86437a --- /dev/null +++ b/files/app/main/status/e/packages.ut @@ -0,0 +1,431 @@ +{% +/* + * 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 log() +{ + return replace(fs.readfile("/tmp/pkg.log"), "\n", "
"); +} +function getPackageOptions() +{ + let i = ``; + let r = ``; + const perm_pkgs = {}; + const installed_pkgs = {}; + + map(split(fs.readfile("/etc/permpkg"), "\n"), p => perm_pkgs[p] = true); + + let f = fs.popen("/bin/opkg list-installed"); + if (f) { + const re = /^[^ \t]+/; + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, re); + if (m) { + installed_pkgs[m[0]] = true; + if (!perm_pkgs[m[0]]) { + r += ``; + } + } + } + f.close(); + } + + f = fs.popen("/bin/opkg list"); + if (f) { + const re = /^([^ ]+) - ([-0-9a-fr\.]+)(.*)$/; + for (let l = f.read("line"); length(l); l = f.read("line")) { + let m = match(trim(l), re); + if (m && !installed_pkgs[m[1]]) { + i += ``; + } + } + f.close(); + } + return { i: i, r: r }; +} +function recordPackage(op, pkgname, pkgfile) +{ + const store = "/etc/package_store"; + const catfile = `${store}/catalog.json`; + fs.mkdir(store); + const catalog = json(fs.readfile(catfile) || '{ "installed": {} }'); + switch (op) { + case "upload": + const package = split(pkgname, "_")[0]; + if (package) { + fs.writefile(`${store}/${package}.ipk`, fs.readfile(pkgfile)); + catalog.installed[package] = "local"; + } + break; + case "download": + const f = fs.popen(`/bin/opkg status ${pkgname} 2>&1`); + if (f) { + const status = replace(f.read("all"), "\n", " "); + f.close(); + const m = match(status, /Package: ([^ \t]+)/); + if (m) { + catalog.installed[m[1]] = "global"; + } + } + break; + case "remove": + fs.unlink(`${store}/${pkgname}.ipk`); + delete catalog.installed[pkgname]; + break; + default: + break; + } + fs.unlink(catfile); + for (let k in catalog.installed) { + fs.writefile(catfile, sprintf("%J", catalog)); + break; + } +} +if (request.env.REQUEST_METHOD === "POST" && request.args["packagefile.ipk"] && request.args.packagename) { + const ipk = request.args["packagefile.ipk"]; + const packagename = fs.readfile(request.args.packagename); + if (system(`/bin/opkg -force-overwrite install ${ipk} > /dev/null 2>&1`) === 0) { + recordPackage("upload", packagename, ipk); + print(`
Package installed
`); + } + else { + if (system("/bin/opkg update > /tmp/pkg.log 2>&1") !== 0) { + print(`
${log()}
`); + } + else { + if (system(`/bin/opkg -force-overwrite install ${ipk} > /tmp/pkg.log 2>&1`) === 0) { + recordPackage("upload", packagename, ipk); + print(`
Package installed
`); + + } + else { + print(`
${log()}
`); + } + } + } + const po = getPackageOptions(); + print(``); + print(``); + fs.unlink(ipk); + fs.unlink(request.args.packagename); + fs.unlink("/tmp/pkg.log"); + return; +} +else if (request.env.REQUEST_METHOD === "GET" && index(request.env.QUERY_STRING, "d=") === 0) { + 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/pkg.log"); + const ipk = substr(request.env.QUERY_STRING, 2); + uhttpd.send(`event: progress\r\ndata: 10\r\n\r\n`); + if (system(`/bin/opkg -force-overwrite install ${ipk} > /dev/null 2>&1`) === 0) { + uhttpd.send(`event: progress\r\ndata: 100\r\n\r\n`); + recordPackage("download", ipk); + uhttpd.send(`event: close\r\ndata: ${sprintf("%J", getPackageOptions())}\r\n\r\n`); + } + else { + uhttpd.send(`event: progress\r\ndata: 20\r\n\r\n`); + if (system("/bin/opkg update > /tmp/pkg.log 2>&1") !== 0) { + uhttpd.send(`event: error\r\ndata: ${log()}\r\n\r\n`); + } + else { + uhttpd.send(`event: progress\r\ndata: 40\r\n\r\n`); + if (system(`/bin/opkg -force-overwrite install ${ipk} > /tmp/pkg.log 2>&1`) === 0) { + uhttpd.send(`event: progress\r\ndata: 100\r\n\r\n`); + recordPackage("download", ipk); + uhttpd.send(`event: close\r\ndata: ${sprintf("%J", getPackageOptions())}\r\n`); + } + else { + uhttpd.send(`event: error\r\ndata: ${log()}\r\n\r\n`); + } + } + } + fs.unlink("/tmp/pkg.log"); + return; +} +else if (request.env.REQUEST_METHOD === "GET" && index(request.env.QUERY_STRING, "r=") === 0) { + const ipk = substr(request.env.QUERY_STRING, 2); + if (system(`/bin/opkg remove ${ipk} > /tmp/pkg.log 2>&1`) === 0) { + recordPackage("remove", ipk); + print(`
Package removed
`); + const po = getPackageOptions(); + print(``); + print(``); + } + else { + print(`
${log()}
`); + } + return; +} +else if (request.env.REQUEST_METHOD === "GET" && index(request.env.QUERY_STRING, "i=") === 0) { + const ipk = substr(request.env.QUERY_STRING, 2); + if (system(`/bin/opkg info ${ipk} > /tmp/pkg.log 2>&1`) === 0) { + print(`
${log()}
`); + } + 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"); + + const pulines = 7; + const f = fs.popen("/bin/opkg update 2>&1"); + if (!f) { + uhttpd.send(`event: error\r\ndata: package update failed\r\n\r\n`); + return; + } + let count = 0; + const re = /^Updated/; + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (match(l, re)) { + count++; + uhttpd.send(`event: progress\r\ndata: ${100 * count / pulines}\r\n\r\n`); + } + } + uhttpd.send(`event: progress\r\ndata: 100\r\n\r\n`); + f.close(); + uhttpd.send(`event: close\r\ndata: ${sprintf("%J", getPackageOptions())}\r\n\r\n`); + return; +} +else if (request.env.REQUEST_METHOD === "PUT" && "packageurl" in request.args) { + if (match(request.args.packageurl, constants.reUrl)) { + configuration.prepareChanges(); + uciMesh.set("aredn", "@downloads[0]", "packages_default", request.args.packageurl); + uciMesh.commit("aredn"); + print(_R("changes")); + } + return; +} +%} +{% +const po = getPackageOptions(); +%} +
+ {{_R("dialog-header", "Packages")}} +
+ {{_R("dialog-messages")}} +
+
+
Download Package
+
Download package from an AREDN server.
+
+
+ +
+
+
+
+
+ {{_H("Download packages directly from a central server, either on the Internet or a locally configured mesh server. + Refresh the list of available packages using the refresh button to the right of the packages list. Once a + package is selected it can be downloaded and installed using the button at the base of the dialog.")}} +
+
+
+
Upload Package
+
Upload a package file from your computer.
+
+
+ +
+
+ {{_H("Upload a package file from your computer. Once the package has been selected it can be uploaded and installed + using the button at the base of the dialog.")}} +

+
+
+
Remove Package
+
Uninstall package from node.
+
+
+ +
+
+ {{_H("Remove a currently installed package from the node by first selecting it and + then using the button at the based of the dialog to remove it.")}} + {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
Package URL
+
URL for downloading packages
+
+
+ +
+
+ {{_H("The base URL used to download packages. 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 the package option selected above, this button will initiate the download, upload, install or remove process.")}} +
+ {{_R("dialog-footer", "nocancel")}} + +
diff --git a/files/app/main/status/e/ports-and-xlinks.ut b/files/app/main/status/e/ports-and-xlinks.ut new file mode 100755 index 00000000..8c11f85e --- /dev/null +++ b/files/app/main/status/e/ports-and-xlinks.ut @@ -0,0 +1,473 @@ +{% +/* + * 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 loadPorts(type, def) +{ + const f = fs.open(`/etc/aredn_include/${type}.network.user`); + if (!f) { + return def; + } + const r = { vlan: null, ports: {} }; + let pcount = 0; + for (let l = f.read("line"); length(l); l = f.read("line")) { + let m = match(l, /list ports '(.+):([ut])'/); + if (m) { + r.ports[m[1]] = true; + if (m[2] === "u") { + r.vlan = 0; + } + pcount++; + } + m = match(l, /option vlan '(\d+)'/); + if (m) { + r.vlan = int(m[1]); + } + } + if (pcount === 0 && ((type === "wan" && r.vlan === 1) || (type === "lan" && r.vlan === 3))) { + r.vlan = 0; + } + f.close(); + return r; +} +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("xlinks" in request.args) { + const xlinks = {}; + const xs = json(request.args.xlinks); + for (let i = 0; i < length(xs); i++) { + xlinks[xs[i].name] = xs[i]; + } + uciMesh.foreach("xlink", "interface", x => { + const ux = xlinks[x[".name"]]; + if (ux) { + uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan); + uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port}:t` ]); + uciMesh.commit("xlink"); + + uciMesh.set("xlink", ux.name, "ifname", `br0.${ux.vlan}`); + uciMesh.set("xlink", ux.name, "ipaddr", ux.ipaddr); + if (uciMesh.get("xlink", ux.name, "peer") !== ux.peer) { + uciMesh.set("xlink", ux.name, "peer", ux.peer); + uciMesh.commit("xlink"); + uciMesh.set("xlink", ux.name, "peer", ux.peer); + if (ux.peer) { + uciMesh.set("xlink", `${ux.name}route`, "route"); + uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name); + uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer); + } + else { + uciMesh.delete("xlink", `${name}route`); + } + uciMesh.commit("xlink"); + } + uciMesh.set("xlink", ux.name, "weight", ux.weight); + uciMesh.set("xlink", ux.name, "netmask", network.CIDRToNetmask(ux.cidr)); + delete xlinks[ux.name]; + } + else { + const name = x[".name"]; + uciMesh.delete("xlink", name); + uciMesh.delete("xlink", `${name}bridge`); + uciMesh.delete("xlink", `${name}route`); + } + uciMesh.commit("xlink"); + }); + for (let name in xlinks) { + const ux = xlinks[name]; + uciMesh.set("xlink", `${ux.name}bridge`, "bridge-vlan"); + uciMesh.set("xlink", `${ux.name}bridge`, "device", "br0"); + uciMesh.set("xlink", `${ux.name}bridge`, "vlan", ux.vlan); + uciMesh.set("xlink", `${ux.name}bridge`, "ports", [ `${ux.port}:t` ]); + uciMesh.commit("xlink"); + + uciMesh.set("xlink", name, "interface"); + uciMesh.set("xlink", name, "ifname", `br0.${ux.vlan}`); + uciMesh.set("xlink", name, "ipaddr", ux.ipaddr); + uciMesh.set("xlink", name, "weight", ux.weight); + uciMesh.set("xlink", name, "netmask", network.CIDRToNetmask(ux.cidr)); + if (ux.peer) { + uciMesh.set("xlink", name, "peer", ux.peer); + uciMesh.set("xlink", `${ux.name}route`, "route"); + uciMesh.set("xlink", `${ux.name}route`, "interface", ux.name); + uciMesh.set("xlink", `${ux.name}route`, "target", ux.peer); + uciMesh.commit("xlink"); + } + uciMesh.set("xlink", name, "proto", "static"); + uciMesh.set("xlink", name, "macaddr", replace("x2:xx:xx:xx:xx:xx", "x", _ => sprintf("%X",math.rand()&15))); + uciMesh.commit("xlink"); + } + delete request.args.xlinks; + } + const k = keys(request.args); + if (length(k) > 0) { + if (length(hardware.getEthernetPorts()) > 1) { + const defnet = hardware.getDefaultNetworkConfiguration(); + const nets = { + wan: loadPorts("wan", defnet.wan), + lan: loadPorts("lan", defnet.lan), + dtdlink: loadPorts("dtdlink", defnet.dtdlink) + }; + for (let i = 0; i < length(k); i++) { + if (k[i] === "port_wan_vlan") { + nets.wan.vlan = int(request.args[k[i]] || 0); + } + else { + const ptp = split(k[i], "_"); + const net = nets[ptp[1]]; + if (net) { + if (request.args[k[i]] === "on") { + net.ports[ptp[2]] = true; + } + else { + delete net.ports[ptp[2]]; + } + } + } + } + if (nets.wan.vlan === 0) { + for (let p in nets.wan.ports) { + if (nets.lan.ports[p]) { + delete nets.wan.ports[p]; + } + } + } + function savePort(type, c) + { + const f = fs.open(`/etc/aredn_include/${type}.network.user`, "w"); + if (f) { + f.write(`# Generated by advancednetwork + +config bridge-vlan + option device 'br0' + option vlan '${c.vlan ? c.vlan : type == "lan" ? 3 : 1}' +${join("\n", map(keys(c.ports), p => "\tlist ports '" + p + (c.vlan ? ":t'" : ":u'")))} + +config device + option name 'br-${type}' + option type 'bridge' + option macaddr '<${type}_mac>' + list ports 'br0.${c.vlan ? c.vlan : type == "lan" ? 3 : 1}' + +config interface '${type}' + option device 'br-${type}' +${type === "dtdlink" ? "\toption proto 'static'" : "\toption proto '<" + type + "_proto>'"} + option ipaddr '<${type}_ip>' +${type === "dtdlink" ? "\toption netmask '255.0.0.0'" : "\toption netmask '<" + type + "_mask>'"} +${type === "wan" ? "\toption gateway ''" : ""}${type === "lan" ? "\toption dns ' '" : ""} +`); + f.close(); + } + } + savePort("wan", nets.wan); + savePort("lan", nets.lan); + savePort("dtdlink", nets.dtdlink); + } + } + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +let haveports = false; +let wan_vlan = 0; +const ports = hardware.getEthernetPorts(); +if (length(ports) > 1) { + haveports = true; + const defnet = hardware.getDefaultNetworkConfiguration(); + const wan = loadPorts("wan", defnet.wan); + const lan = loadPorts("lan", defnet.lan); + const dtdlink = loadPorts("dtdlink", defnet.dtdlink); + for (let i = 0; i < length(ports); i++) { + const v = { + name: ports[i].k, + display: ports[i].d, + info: hardware.getEthernetPortInfo(ports[i].k), + dtdlink: dtdlink.ports[ports[i].k], + lan: lan.ports[ports[i].k], + wan: wan.ports[ports[i].k] + }; + ports[i] = v; + } + wan_vlan = wan.vlan; +} +const xlinks = []; +uciMesh.foreach("xlink", "interface", x => { + push(xlinks, { + name: x[".name"], + vlan: int(split(x.ifname, ".")[1]), + ipaddr: x.ipaddr, + peer: x.peer || "", + weight: int(x.weight), + port: match(uciMesh.get("xlink", `${x[".name"]}bridge`, "ports")[0], /^(.+):/)[1], + cidr: network.netmaskToCIDR(x.netmask) + }); +}); +%} +
+ {{_R("dialog-header", `${haveports ? "Ports & " : ""}XLinks`)}} +
+ {% if (haveports) { %} +
+
+ + + + + {% + for (let i = 0; i < length(ports); i++) { + const p = ports[i]; + print(``); + } + %} + + + + + + {% + for (let i = 0; i < length(ports); i++) { + const p = ports[i]; + print(``); + } + %} + + + + {% + for (let i = 0; i < length(ports); i++) { + const p = ports[i]; + print(``); + } + %} + + + + {% + for (let i = 0; i < length(ports); i++) { + const p = ports[i]; + print(``); + } + %} + + +
${p.display}
dtd
vlan: 2
lan
vlan: untagged
wan
vlan:
+
+
+ {{_H("

AREDN nodes have three primary networks; DTD, LAN and WAN (shown above in the left column). You can modify the default assignment + of these networks to ports (shown across the top) using the checkboxes at the intersection of a network and a port. You can also choose the + VLAN to assign to the WAN network. Networks can be assigned to multiple ports, or no ports. + Note that on some devices, ports may have names like WAN or LAN. These are just arbitrary names given by the manufacturer + and you are not forced to assign networks of the same name to these ports. +

Active network ports, where a cable is present and attached to another device, are shown in green.")}} +


+ {% } %} + + {{_H("

XLinks provide a way of routing AREDN traffic across external non-AREDN networks. Each is created with a specific VLAN, + IP address for both ends, a weight of how likely to link is to be used, and an optional network size and port (on multi-port + devices). Think of xlinks as extra dtds between devices. How xlink traffic is routed once it leaves the node is dependent + on the non-AREDN network, which allows for the greatest flexibility.")}} +

+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/radio-and-antenna.ut b/files/app/main/status/e/radio-and-antenna.ut new file mode 100755 index 00000000..be3a0f47 --- /dev/null +++ b/files/app/main/status/e/radio-and-antenna.ut @@ -0,0 +1,596 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("radio0_mode" in request.args || "radio1_mode" in request.args) { + const wlan = radios.getConfiguration(); + let radio0_mode = "radio0_mode" in request.args ? int(request.args.radio0_mode) : wlan[0].mode; + let radio1_mode = "radio1_mode" in request.args ? int(request.args.radio1_mode) : wlan[1]?.mode; + if (radio0_mode === radio1_mode) { + if ("radio0_mode" in request.args) { + radio1_mode = radios.RADIO_OFF; + } + else { + radio0_mode = radios.RADIO_OFF; + } + } + configuration.setSetting("wifi_enable", "0"); + configuration.setSetting("wifi2_enable", "0"); + configuration.setSetting("wifi3_enable", "0"); + switch (radio0_mode) { + case radios.RADIO_MESH: + configuration.setSetting("wifi_enable", "1"); + if (configuration.setSetting("wifi_intf", "wlan0")) { + configuration.setSetting("wifi_channel", wlan[0].def.channel); + } + break; + case radios.RADIO_LAN: + configuration.setSetting("wifi2_enable", "1"); + if (configuration.setSetting("wifi2_hwmode", wlan[0].def.band === "5GHz" ? "11a" : "11g")) { + configuration.setSetting("wifi2_channel", wlan[0].def.channel); + } + if (configuration.getSettingAsString("wifi2_key", "") === "") { + configuration.setSetting("wifi2_key", hexenc("AREDN")); + } + break; + case radios.RADIO_WAN: + configuration.setSetting("wifi3_enable", "1"); + configuration.setSetting("wifi3_hwmode", wlan[0].def.band === "5GHz" ? "11a" : "11g"); + break; + case radios.RADIO_OFF: + default: + break; + } + switch (radio1_mode) { + case radios.RADIO_MESH: + configuration.setSetting("wifi_enable", "1"); + if (configuration.setSetting("wifi_intf", "wlan1")) { + configuration.setSetting("wifi_channel", wlan[1].def.channel); + } + break; + case radios.RADIO_LAN: + configuration.setSetting("wifi2_enable", "1"); + if (configuration.setSetting("wifi2_hwmode", wlan[1].def.band === "5GHz" ? "11a" : "11g")) { + configuration.setSetting("wifi2_channel", wlan[1].def.channel); + } + if (configuration.getSettingAsString("wifi2_key", "") === "") { + configuration.setSetting("wifi2_key", hexenc("AREDN")); + } + break; + case radios.RADIO_WAN: + configuration.setSetting("wifi3_enable", "1"); + configuration.setSetting("wifi3_hwmode", wlan[1].def.band === "5GHz" ? "11a" : "11g"); + break; + case radios.RADIO_OFF: + default: + break; + } + } + if ("radio_channel" in request.args) { + configuration.setSetting("wifi_channel", request.args.radio_channel); + } + if ("radio0_bandwidth" in request.args || "radio1_bandwidth" in request.args) { + configuration.setSetting("wifi_chanbw", request.args.radio0_bandwidth || request.args.radio1_bandwidth); + } + if ("radio_txpower" in request.args) { + configuration.setSetting("wifi_txpower", request.args.radio_txpower); + } + if ("radio0_ssid" in request.args || "radio1_ssid" in request.args) { + configuration.setSetting("wifi_ssid", request.args.radio0_ssid || request.args.radio1_ssid); + } + if ("radio_minsnr" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "min_snr", request.args.radio_minsnr); + } + if ("radio_maxdistance" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "max_distance", units.distance2meters(request.args.radio_maxdistance)); + } + if ("radio_minquality" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "min_quality", request.args.radio_minquality); + } + if ("radio_lan_ssid" in request.args) { + configuration.setSetting("wifi2_ssid", hexenc(request.args.radio_lan_ssid)); + } + if ("radio_lan_channel" in request.args) { + configuration.setSetting("wifi2_channel", request.args.radio_lan_channel); + } + if ("radio_lan_encryption" in request.args) { + const encrypt = [ "psk", "psk2", "none" ]; + configuration.setSetting("wifi2_encryption", encrypt[int(request.args.radio_lan_encryption)]); + } + if ("radio_lan_password" in request.args) { + configuration.setSetting("wifi2_key", hexenc(request.args.radio_lan_password)); + } + if ("radio_wan_ssid" in request.args) { + configuration.setSetting("wifi3_ssid", hexenc(request.args.radio_wan_ssid)); + } + if ("radio_wan_password" in request.args) { + configuration.setSetting("wifi3_key", hexenc(request.args.radio_wan_password)); + } + if ("radio_antenna" in request.args) { + uciMesh.set("aredn", "@location[0]", "antenna", request.args.radio_antenna); + } + if ("radio_azimuth" in request.args) { + uciMesh.set("aredn", "@location[0]", "azimuth", request.args.radio_azimuth); + } + if ("radio_height" in request.args) { + uciMesh.set("aredn", "@location[0]", "height", request.args.radio_height); + } + if ("radio_elevation" in request.args) { + uciMesh.set("aredn", "@location[0]", "elevation", request.args.radio_elevation); + } + if ("radio_lqm_enable" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "lqm_enable", request.args.radio_lqm_enable === "on" ? 1 : 0); + } + if ("radio_mindistance" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "min_distance", request.args.radio_mindistance); + } + if ("radio_rts_threshold" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "rts_threshold", request.args.radio_rts_threshold); + } + if ("radio_mtu" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "mtu", request.args.radio_mtu); + } + if ("radio_margin_snr" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "margin_snr", request.args.radio_margin_snr); + } + if ("radio_margin_quality" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "margin_quality", request.args.radio_margin_quality); + } + if ("radio_ping_penalty" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "ping_penalty", request.args.radio_ping_penalty); + } + if ("radio_min_routes" in request.args) { + uciMesh.set("aredn", "@lqm[0]", "min_routes", request.args.radio_min_routes); + } + uciMesh.commit("aredn"); + configuration.saveSettings(); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +%} +{% const wlan = radios.getConfiguration(); %} +
+ {{_R("dialog-header", "Radios & Antennas")}} +
+ {% + const hasradios = length(wlan) > 0; + if (hasradios) { + for (let w = 0; w < length(wlan); w++) { + const prefix = `radio${w}_`; + if (w !== 0) { + print("
"); + } + %} +
+
+
+
1 ? "style='font-weight:bold'" : ""}}>Radio {{wlan[w].def.band}}
+
Radio purpose
+
+
+ +
+
+ {{_H("Select the purpose of the radio. Each radio can be assigned to a specific purpose, but devices with multiple radios + cannot have the same purpose for multiple radios (except off).")}} +
1 ? "style='padding-left:10px'" : ""}}> +
+
+
Channel
+
Channel and frequency of this connection
+
+
+ +
+
+ {{_H("Select the central channel/frequency for the radio.")}} +
+
+
Channel Width
+
Channel bandwidth
+
+
+ +
+
+ {{_H("Select the bandwidth of the radio. Be aware that larger bandwidth settings will consume more channels. Avoid overlapping + channels as this will impact performance.")}} +
+
+
Transmit Power
+
Transmit power
+
+
+ +
+
+ {{_H("Select the transmission power for the radio. Ideally use only enough power to maintain the link at the capacity required.")}} +
+
+
SSID
+
AREDN mesh identifier
+
+
+ -{{wlan[w].modes[1].bandwidth}}-v3 +
+
+ {% if (uciMesh.get("aredn", "@lqm[0]", "lqm_enable") !== "0") { %} +
+
+
Minimum SNR
+
Acceptable SNR for connection (dB)
+
+
+ +
+
+ {{_H("Low SNR results in higher latency, lower bandwidth and high retranmissions. Setting a minimum SNR allows links with + these characteristics to be ignored.")}} +
+
+
Maximum Distance
+
Maximum distance allowed to other nodes in {{units.distanceUnit()}}
+
+
+ +
+
+ {{_H("Distance beyond which neighbor node connections will be ignored. Longer distances to nodes can result in poor performance. + This will effect all neighbors, not just the distant ones, so it often makes sense to keep this distance to a minimum.")}} +
+
+
Minimum Quality
+
Minimum connection quaility percentage
+
+
+ +
+
+ {{_H("The node management system maintains an estimate of how well each neighbor link performs. This link quality is used + to determine which links to use. Lowering the minimum quality can impact performance, but may also be necessary under specific + circumstances.")}} + {% } %} +
+
1 ? "style='padding-left:10px'" : ""}}> + {{_H("In LAN Hotpot mode, the WiFi acts as a wireless hotspot. Any device connecting will appear as a LAN device attached to the node.")}} +
+
+
SSID
+
Hotspot SSID
+
+
+ +
+
+
+
+
Channel
+
Hotspot channel
+
+
+ +
+
+
+
+
+
Encryption
+
Encryption algorithm
+
+
+ +
+
+
+
+
+
Password
+
Hotspot password
+
+
+ +
+
+
+
+
+
1 ? "style='padding-left:10px'" : ""}}> + {{_H("In WAN Client mode, the WiFi connection is used to connect to another wireless network. This network is expected to provide + access to the Internet.")}} +
+
+
SSID
+
WAN client
+
+
+ +
+
+
+
+
Password
+
Client password
+
+
+ +
+
+
+
+
1 ? "style='padding-left:10px'" : ""}}> +
+
+
Antenna
+
Antenna
+
+
+ {% if (length(wlan[w].ants) === 1) { %} + {{wlan[w].ants[0].description}} + {% } else { %} + + {% } %} +
+
+ {% if (length(wlan[w].ants) !== 1) { %} + {{_H("Select the external antenna attached to the primary radio.")}} + {% } %} + {% if (w === 0) { %} + {% if (!(length(wlan[w].ants) === 1 && wlan[w].ants[0].beamwidth === 360)) { %} +
+
+
Azimuth
+
Antenna azimuth in degrees
+
+
+ +
+
+ {{_H("The azimuth, or heading, of the primary radio antenna measured in degrees from north.")}} + {% } %} +
+
+
Height
+
Antenna height in meters
+
+
+ +
+
+ {{_H("The height of the antenna above ground level in meters.")}} +
+
+
Elevation
+
Antenna elevation in degrees
+
+
+ +
+
+ {{_H("Elevation of the antenna, measured in degress, above or below the horizontal.")}} + {% } %} +
+
+ {% + } + } + else { + %} +
No Radios
+
+
+
+
Minimum Quality
+
Minimum connection quaility percentage
+
+
+ +
+
+
+ {% } %} + {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
LQM enable
+
Enable Link Quality Management
+
+
+ {{_R("switch", { name: "radio_lqm_enable", value: uciMesh.get("aredn", "@lqm[0]", "lqm_enable") !== "0" })}} +
+
+ {{_H("Link Quality Management (LQM) is an automatic management system which monitors the efficiency of each neighbor link + and optimizes their use for best performance. When disabled, it still gathers data on each link, but this information is + not used to effect operation.")}} + {% if (hasradios) { %} +
+
+
Minimum Distance
+
Minimum distance to other nodes in {{units.distanceUnit()}}
+
+
+ +
+
+ {{_H("Exclude nodes which are too close to this node.")}} +
+
+
RTS Threshold
+
RTS Threshold in bytes before using RTS/CTS when hidden nodes are detected
+
+
+ +
+
+ {{_H("When hidden nodes are detected, the RTS/CTS protocol is automatically enabled to improve performance. By default this is used for all packets + being sent, but this can be optimized to only packets over a specific size (between 1 and 2347 bytes). Setting the value to + 2347 disables the protocol.")}} +
+
+
Max Packet Size
+
Maximum packet size in bytes sent over WiFi
+
+
+ +
+
+ {{_H("By default, WiFi uses the same packet size (1500 bytes) as Ethernet. However, in some noisy environment performance can be + improved by reducing the packet size. A value must be between 256 and 1500.")}} +
+
+
SNR Margin
+
SNR Margin in dB above Min SNR a signal must reach to be re-activated
+
+
+ +
+
+ {{_H("The SNR margin avoids a link switching quickly between blocked and unblocked when the SNR is the same as the minimum SNR. Once + a link falls below the minimum SNR, it must move above minimum SNR + SNR margin to become active again.")}} + {% } %} +
+
+
Quality Margin
+
Quality Margin percentage increase before neighbor can be re-activated
+
+
+ +
+
+
+
+
Ping Penalty
+
Ping Penalty quality percentage to add when neighbor cannot be pinged
+
+
+ +
+
+
+
+
Minimum Routes
+
Minimum number of routes on a link required to disable blocking
+
+
+ +
+
+ {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/reboot.ut b/files/app/main/status/e/reboot.ut new file mode 100755 index 00000000..10644d7d --- /dev/null +++ b/files/app/main/status/e/reboot.ut @@ -0,0 +1,56 @@ +{% +/* + * 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 + */ +%} +{% + response.reboot = true; + const address = configuration.getSettingAsString("wifi_ip", request.env.HTTP_HOST); +%} +{{configuration.getName()}} rebooting +
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Rebooting
+
Your node is rebooting.
This browser will reconnect automatically once complete.
+
+
+
 
+
+
+ {{_R("reboot-mon", { delay: 20, countdown: 120, timeout: 5, location: `http://${address}/a/status` })}} +
diff --git a/files/app/main/status/e/time.ut b/files/app/main/status/e/time.ut new file mode 100755 index 00000000..79bba384 --- /dev/null +++ b/files/app/main/status/e/time.ut @@ -0,0 +1,154 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("timezone" in request.args) { + const nt = match(request.args.timezone, /^(.*)\t(.*)$/); + if (nt) { + configuration.setSetting("time_zone_name", nt[1]); + configuration.setSetting("time_zone", nt[2]); + } + } + if ("ntp_server" in request.args) { + configuration.setSetting("ntp_server", request.args.ntp_server); + } + if ("ntp_mode" in request.args) { + switch (request.args.ntp_mode) { + case "daily": + case "hourly": + uciMesh.set("aredn", "@ntp[0]", "period", request.args.ntp_mode); + break; + default: + break; + } + } + if (request.args.gps_enable) { + uciMesh.set("aredn", "@time[0]", "gps_enable", request.args.gps_enable === "on" ? "1" : "0"); + } + configuration.saveSettings(); + uciMesh.commit("aredn"); + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const time_zone_name = configuration.getSettingAsString("time_zone_name", "UTC"); +const tz_db_names = []; +const f = fs.open("/etc/zoneinfo"); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + l = rtrim(l); + const nt = split(l, "\t"); + push(tz_db_names, { name: nt[0], value: l }); + } + f.close(); +} +const ntp_server = configuration.getSettingAsString("ntp_server", ""); +const ntp_mode = uciMesh.get("aredn", "@ntp[0]", "period") || "daily"; +%} +
+ {{_R("dialog-header", "Time")}} +
+
+
+
Timezone
+
Timezone
+
+
+ +
+
+ {{_H("The timezone for this node. Setting this correctly means that timed events will run in the appopriate timezone, + logs will have the expected times, etc.")}} +
+
+
+
NTP Server
+
The ntp server to sync the time
+
+
+ +
+
+ {{_H("The default NTP server to use when syncing the node's time. If this cannot be found, the node will search for one on the mesh.")}} +
+
+
NTP Updates
+
NTP update frequency
+
+
+ +
+
+ {{_H("NTP is used to keep the node's time up to date. Syncing the time every day is probably sufficient + but you can increase the frequency to hourly. Having accurate time means your timed events will run when you expected + and log information will show meaningful times.")}} + {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
GPS Time
+
Use local or network GPS to set time
+
+
+ {{_R("switch", { name: "gps_enable", value: uciMesh.get("aredn", "@time[0]", "gps_enable") === "1" })}} +
+
+ {{_H("Use either a local GPS devices to set the time, or search for a GPS device on another local node, and use its + GPS to set our time.")}} + {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/status/e/tunnels.ut b/files/app/main/status/e/tunnels.ut new file mode 100755 index 00000000..3b2ce781 --- /dev/null +++ b/files/app/main/status/e/tunnels.ut @@ -0,0 +1,541 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + configuration.prepareChanges(); + if ("tunnel_server" in request.args) { + uciMesh.set("vtun", "@network[0]", "dns", request.args.tunnel_server); + uciMesh.commit("vtun"); + } + if ("tunnel_range_start" in request.args) { + uciMesh.set("vtun", "@network[0]", "start", request.args.tunnel_range_start); + uciMesh.commit("vtun"); + } + if ("tunnel_weight" in request.args) { + uciMesh.set("aredn", "@tunnel[0]", "weight", request.args.tunnel_weight); + uciMesh.commit("aredn"); + } + if ("tunnels" in request.args) { + const tunnels = json(request.args.tunnels); + const found = { ls: {}, lc: {}, ws: {}, wc: {} }; + for (let i = 0; i < length(tunnels); i++) { + const t = tunnels[i]; + found[t.type][t.index] = true; + switch (t.type) { + case "wc": + { + if (!uciMesh.get("vtun", t.index)) { + uciMesh.set("vtun", t.index, "server"); + } + uciMesh.set("vtun", t.index, "enabled", t.enabled ? "1" : "0"); + uciMesh.set("vtun", t.index, "host", t.name); + uciMesh.set("vtun", t.index, "passwd", t.passwd); + const np = split(t.network, ":"); + const n = iptoarr(np[0]); + uciMesh.set("vtun", t.index, "node", `${uc(substr(configuration.getName(), 0, 23))}-${n[0]}-${n[1]}-${n[2]}-${n[3]}:${np[1]}`); + uciMesh.set("vtun", t.index, "clientip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 1}`); + uciMesh.set("vtun", t.index, "serverip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 2}`); + uciMesh.set("vtun", t.index, "netip", t.network); + uciMesh.set("vtun", t.index, "weight", t.weight); + break; + } + case "lc": + { + if (!uciMesh.get("vtun", t.index)) { + uciMesh.set("vtun", t.index, "server"); + } + uciMesh.set("vtun", t.index, "enabled", t.enabled ? "1" : "0"); + uciMesh.set("vtun", t.index, "host", t.name); + uciMesh.set("vtun", t.index, "passwd", t.passwd); + const n = iptoarr(t.network); + uciMesh.set("vtun", t.index, "node", `${uc(substr(configuration.getName(), 0, 23))}-${n[0]}-${n[1]}-${n[2]}-${n[3]}`); + uciMesh.set("vtun", t.index, "clientip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 1}`); + uciMesh.set("vtun", t.index, "serverip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 2}`); + uciMesh.set("vtun", t.index, "netip", t.network); + uciMesh.set("vtun", t.index, "weight", t.weight); + break; + } + case "ws": + { + if (!uciMesh.get("wireguard", t.index)) { + uciMesh.set("wireguard", t.index, "client"); + } + uciMesh.set("wireguard", t.index, "enabled", t.enabled ? "1" : "0"); + uciMesh.set("wireguard", t.index, "name", t.name); + uciMesh.set("wireguard", t.index, "key", t.key); + uciMesh.set("wireguard", t.index, "clientip", t.network); + uciMesh.set("wireguard", t.index, "weight", t.weight); + break; + } + case "ls": + { + if (!uciMesh.get("vtun", t.index)) { + uciMesh.set("vtun", t.index, "client"); + } + uciMesh.set("vtun", t.index, "enabled", t.enabled ? "1" : "0"); + uciMesh.set("vtun", t.index, "passwd", t.passwd); + const n = iptoarr(t.network); + uciMesh.set("vtun", t.index, "node", `${uc(substr(t.name, 0, 23))}-${n[0]}-${n[1]}-${n[2]}-${n[3]}`); + uciMesh.set("vtun", t.index, "clientip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 1}`); + uciMesh.set("vtun", t.index, "serverip", `${n[0]}.${n[1]}.${n[2]}.${n[3] + 2}`); + uciMesh.set("vtun", t.index, "netip", t.network); + uciMesh.set("vtun", t.index, "weight", t.weight); + break; + } + default: + break; + } + } + uciMesh.foreach("vtun", "server", a => { + if (!found.lc[a[".name"]] && !found.wc[a[".name"]]) { + uciMesh.delete("vtun", a[".name"]); + } + }); + uciMesh.foreach("vtun", "client", a => { + if (!found.ls[a[".name"]]) { + uciMesh.delete("vtun", a[".name"]); + } + }); + uciMesh.foreach("wireguard", "client", a => { + if (!found.ws[a[".name"]]) { + uciMesh.delete("wireguard", a[".name"]); + } + }); + uciMesh.commit("vtun"); + uciMesh.commit("wireguard"); + } + print(_R("changes")); + return; +} +if (request.env.REQUEST_METHOD === "DELETE") { + configuration.revertModalChanges(); + print(_R("changes")); + return; +} +const available = { l: {}, w: {} }; +const l = iptoarr(uciMesh.get("vtun", "@network[0]", "start")); +const w = [ l[0], l[1], (l[2] === 255 ? 0 : l[2] + 1), l[3] ]; +for (let i = 0; i < 62; i++) { + available.l[`${l[0]}.${l[1]}.${l[2]}.${l[3]}`] = 1; + l[3] += 4; + if (l[3] >= 252) { + l[2]++; + l[3] = 4; + } +} +let p = uciMesh.get("aredn", "@supernode[0]", "enable") === 1 ? 6526 : 5525; +for (let i = 0; i < 126; i++) { + available.w[`${w[0]}.${w[1]}.${w[2]}.${w[3]}:${p}`] = 1; + w[3] += 2; + if (w[3] >= 254) { + w[2]++; + w[3] = 2; + } + p++; +} +const up = { lg: {}, wg: [] }; +let f = fs.popen("ps -w | grep vtun | grep ' tun '"); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, /.*:.*-(172-.*) tun tun/); + if (m) { + up.lg[replace(m[1], "-", ".")] = true; + } + } + f.close(); +} +const handshaketime = time(); +f = fs.popen("/usr/bin/wg show all latest-handshakes"); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = split(l, /\t/); + if (m && int(m[2]) + 300 > handshaketime) { + push(up.wg, m[1]); + } + } + f.close(); +} +const tunnels = []; +uciMesh.foreach("vtun", "server", a => { + const wireguard = index(a.node, ":") !== -1; + push(tunnels, { + index: a[".name"], + type: wireguard ? "wc" : "lc", + name: a.host, + enabled: a.enabled === "1", + notes: a.comment || "", + network: a.netip, + passwd: a.passwd, + weight: a.weight ?? "", + up: wireguard ? (length(filter(up.wg, v => index(a.key, v) !== -1)) === 0 ? false : true) : (up.lg[a.netip] ? true : false) + }); + delete available.l[a.clientip]; + delete available.w[a.clientip]; +}); +uciMesh.foreach("vtun", "client", a => { + const n = iptoarr(a.netip); + const remove = `-${n[0]}-${n[1]}-${n[2]}-${n[3]}`; + push(tunnels, { + index: a[".name"], + type: "ls", + name: replace(a.name, remove, ""), + enabled: a.enabled === "1", + notes: a.comment || "", + network: a.netip, + passwd: a.passwd, + weight: a.weight ?? "", + up: up.lg[a.netip] ? true : false + }); + delete available.l[a.clientip]; +}); +uciMesh.foreach("wireguard", "client", a => { + push(tunnels, { + index: a[".name"], + type: "ws", + name: a.name, + enabled: a.enabled === "1", + notes: a.comment || "", + network: a.clientip, + key: a.key, + passwd: replace(a.key, /^[A-Za-z0-9+\/]+=/, ""), + weight: a.weight ?? "", + up: length(filter(up.wg, v => index(a.key, v) !== -1)) === 0 ? false : true + }); + delete available.w[a.clientip]; +}); +function t2t(type) +{ + switch (type) { + case "ws": + return "Wireguard
Server"; + case "wc": + return "Wireguard
Client"; + case "ls": + return "Legacy
Server"; + case "lc": + return "Legacy
Client"; + default: + return type; + } +} +%} +
+ {{_R("dialog-header", "Tunnels")}} +
+
+
+
Tunnel Server
+
DNS name of this tunnel server
+
+
+ +
+
+ {{_H("Set either the hostname or the Internet IP Address for this tunnel server. This is the target for any tunnels + created which will connect to this node (legacy or wireguard).")}} +
+
+
+
Add tunnel
+
Add a tunnel from a template
+
+
+ +
+ +
+ {{_H("Create a tunnel by selecting the specific type and hitting the +. The tunnel configuration will auto-populate with as much + information as possible. For tunnel clients all the fields can be easily populated by copy-n-pasting the entire server configuration + from another node into any field.")}} +
+
{% + for (let i = 0; i < length(tunnels); i++) { + const t = tunnels[i]; + const client = t.type === "lc" || t.type === "wc" ? true : false; + const wireguard = t.type === "ws" || t.type === "wc" ? true : false; + %}
+
+
{{t2t(t.type)}}
+
+
+ + +
+
+ {% if (t.type === "ws") { %} + + {% } %} + + + +
+
+ + {{client ? '' : ''}} +
+
+
+ +
{% + } + %}
+ {{_R("dialog-advanced")}} +
+ {% if (includeAdvanced) { %} +
+
+
Tunnel Server Network
+
IP range to use for tunnel connections
+
+
+ {% if (uciMesh.get("aredn", "@supernode[0]", "enable") === "1") { %} + + {% } else { %} + + {% } %} +
+
+ {% if (uciMesh.get("aredn", "@supernode[0]", "enable") === "1") { %} + {{_H("The range of IP addresses allocated to the tunnels. Tunnels always start 172.30")}} + {% } else { %} + {{_H("The range of IP addresses allocated to the tunnels. Tunnels always start 172.31")}} + {% } %} + {% if (uciMesh.get("aredn", "@supernode[0]", "enable") !== "1") { %} +
+
+
Default Tunnel Weight
+
Default cost of using a tunnel
+
+
+ +
+
+ {{_H("The tunnel weight is the cost of routing traffic via a tunnel. The higher the weight, the less likely a tunnel is used to reach a destination. +This value is used by default, but each tunnel may overide it. +")}} + {% } %} + {% } %} +
+
+ {{_R("dialog-footer")}} + +
diff --git a/files/app/main/tools/e/iperf3.ut b/files/app/main/tools/e/iperf3.ut new file mode 100755 index 00000000..94aaf8e4 --- /dev/null +++ b/files/app/main/tools/e/iperf3.ut @@ -0,0 +1,202 @@ +{% +/* + * 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 + */ +%} +{% +const nodes = mesh.getNodeList(); +const mynode = configuration.getName(); +%} +
+ {{_R("tool-header", "iPerf3")}} +
+
+
+
Server A‌ddress
+
Node name or a‌ddress
+
+
+ + + +
+
+
+
+
Client A‌ddress
+
Node name or a‌ddress
+
+
+ + +
+
+
+ {{_H("Use iperf3 to measure the bandwidth between a client and a server. The client and server must be nodes on the mesh. By default the client is the current node.")}} +
+
+

+            
+
+ +
+
+
+ {{_R("tool-footer")}} + +
diff --git a/files/app/main/tools/e/ping.ut b/files/app/main/tools/e/ping.ut new file mode 100755 index 00000000..b0754aff --- /dev/null +++ b/files/app/main/tools/e/ping.ut @@ -0,0 +1,204 @@ +{% +/* + * 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 + */ +%} +{% +const nodes = mesh.getNodeList(); +const mynode = configuration.getName(); +%} +
+ {{_R("tool-header", "Ping")}} +
+
+
+
Target A‌ddress
+
IP A‌ddress, Hostname or Node
+
+
+ + + +
+
+
+
+
Source A‌ddress
+
Node name or a‌ddress
+
+
+ + +
+
+
+ {{_H("Send ping packets between a source host and a target. The source should be node on the mesh, and by default is the current node. + The target can be a node, an IP address, or any hostname that might be reachable.")}} +
+
+

+            
+
+ +
+
+
+ {{_R("tool-footer")}} + +
diff --git a/files/app/main/tools/e/supportdata.ut b/files/app/main/tools/e/supportdata.ut new file mode 100755 index 00000000..76aa2517 --- /dev/null +++ b/files/app/main/tools/e/supportdata.ut @@ -0,0 +1,170 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + response.headers["HX-Redirect"] = request.env.REQUEST_URI; + return; +} + +const wifiiface = uci.get("network", "wifi", "device"); + +const files = [ + "/etc/board.json", + "/etc/config/", + "/etc/config.mesh/", + "/etc/local/", + "/etc/mesh-release", + "/etc/os-release", + "/var/run/hosts_olsr", + "/var/run/services_olsr", + "/tmp/etc/", + "/tmp/dnsmasq.d/", + "/tmp/lqm.info", + "/tmp/wireless_monitor.info", + "/tmp/service-validation-state", + "/tmp/sysinfo/", + "/sys/kernel/debug/ieee80211/phy0/ath9k/ack_to", + "/sys/kernel/debug/ieee80211/phy1/ath9k/ack_to" +]; +const sensitive = [ + "/etc/config/vtun", + "/etc/config.mesh/vtun", + "/etc/config/network", + "/etc/config.mesh/wireguard", + "/etc/config/wireless", + "/etc/config.mesh/_setup", +]; +const cmds = [ + "cat /proc/cpuinfo", + "cat /proc/meminfo", + "df -k", + "dmesg", + "ifconfig", + "ethtool eth0", + "ethtool eth1", + "ip link", + "ip addr", + "ip neigh", + "ip route list", + "ip route list table 29", + "ip route list table 30", + "ip route list table 31", + "ip route list table main", + "ip route list table default", + "ip rule list", + "netstat -aln", + "iwinfo", + `${wifiiface ? "iwinfo " + wifiiface + " assoclist" : null}`, + `${wifiiface ? "iw phy " + (replace(wifiiface, "wlan", "phy")) + " info" : null}`, + `${wifiiface ? "iw dev " + wifiiface + " info" : null}`, + `${wifiiface ? "iw dev " + wifiiface + " scan" : null}`, + `${wifiiface ? "iw dev " + wifiiface + " station dump" : null}`, + "wg show all", + "wg show all latest-handshakes", + "nft list ruleset", + "md5sum /www/cgi-bin/*", + "echo /all | nc 127.0.0.1 2006", + "opkg list-installed", + "ps -w", + "/usr/local/bin/get_hardwaretype", + "/usr/local/bin/get_boardid", + "/usr/local/bin/get_model", + "/usr/local/bin/get_hardware_mfg", + "logread", +]; +if (trim(fs.popen("/usr/local/bin/get_hardware_mfg").read("all")) === "Ubiquiti") { + push(cmds, "cat /dev/mtd0|grep 'U-Boot'|head -n1"); +} + +system("/bin/rm -rf /tmp/sd"); +system("/bin/mkdir -p /tmp/sd"); + +for (let i = 0; i < length(files); i++) { + const file = files[i]; + const s = fs.stat(file); + if (s) { + if (s.type === "directory") { + system(`/bin/mkdir -p /tmp/sd${file}`); + system(`/bin/cp -rp ${file}/* /tmp/sd/${file}`); + } + else { + system(`/bin/mkdir -p /tmp/sd${fs.dirname(file)}`); + system(`/bin/cp -p ${file} /tmp/sd/${file}`); + } + } +} + +for (let i = 0; i < length(sensitive); i++) { + const file = sensitive[i]; + const f = fs.open(file); + if (f) { + const lines = []; + for (let l = f.read("line"); length(l); l = f.read("line")) { + l = replace(l, /option passwd.+/, "option passwd '***HIDDEN***'\n"); + l = replace(l, /option public_key.+/, "option public_key '***HIDDEN***'\n"); + l = replace(l, /option private_key.+/, "option private_key '***HIDDEN***'\n"); + l = replace(l, /option key.+/, "option key '***HIDDEN***'\n"); + push(lines, l); + } + f.close(); + fs.writefile(`/tmp/sd${file}`, join("", lines)); + } +} + +const f = fs.open("/tmp/sd/data.txt", "w"); +if (f) { + for (let i = 0; i < length(cmds); i++) { + const cmd = cmds[i]; + if (cmd) { + const p = fs.popen(`(${cmd}) 2> /dev/null`); + if (p) { + f.write(`\n===\n========== ${cmd} ==========\n===\n`); + f.write(p.read("all")); + p.close(); + } + } + } + f.close(); +} + +system("/bin/tar -zcf /tmp/supportdata.tar.gz -C /tmp/sd ./"); +system("/bin/rm -rf /tmp/sd"); + +const tm = localtime(); +response.override = true; +uhttpd.send(`Status: 200 OK\r\nContent-Type: application/x-gzip\r\nContent-Disposition: attachment; filename=supportdata-${configuration.getName()}-${tm.year}-${tm.mon}-${tm.mday}-${tm.hour}-${tm.min}.tar.gz\r\nCache-Control: no-store\r\n\r\n`); +uhttpd.send(fs.readfile("/tmp/supportdata.tar.gz")); + +%} diff --git a/files/app/main/tools/e/traceroute.ut b/files/app/main/tools/e/traceroute.ut new file mode 100755 index 00000000..38491e0e --- /dev/null +++ b/files/app/main/tools/e/traceroute.ut @@ -0,0 +1,211 @@ +{% +/* + * 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 + */ +%} +{% +const nodes = mesh.getNodeList(); +const mynode = configuration.getName(); +%} +
+ {{_R("tool-header", "Traceroute")}} +
+
+
+
Target A‌ddress
+
IP A‌ddress, Hostname or Node
+
+
+ + + +
+
+
+
+
Source A‌ddress
+
Node name or a‌ddress
+
+
+ + +
+
+
+ {{_H("Trace the route between a source host and a target. The source should be node on the mesh, and by default is the current node. + The target can be a node, an IP address, or any hostname that might be reachable.")}} +
+
+

+            
+
+ +
+
+
+ {{_R("tool-footer")}} + +
diff --git a/files/app/main/tools/e/wifiscan.ut b/files/app/main/tools/e/wifiscan.ut new file mode 100755 index 00000000..008d5651 --- /dev/null +++ b/files/app/main/tools/e/wifiscan.ut @@ -0,0 +1,274 @@ +{% +/* + * 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 + */ +%} +{% +const last_scan_file = "/tmp/last-scan.json"; +let last_scan = []; +let scan_time = "Unknown"; + +if (request.env.REQUEST_METHOD === "PUT") { + const config = radios.getActiveConfiguration(); + const radio = config[0]?.mode === radios.RADIO_MESH ? config[0] : config[1]?.mode === radios.RADIO_MESH ? config[1] : null; + if (!radio) { + return; + } + const radiomode = radio.modes[radios.RADIO_MESH]; + const wifiiface = radio.iface; + const myssid = radiomode.ssid; + const mychan = radiomode.channel; + const myfreq = hardware.getChannelFrequency(wifiiface, radiomode.channel); + const nodename = configuration.getName(); + const scan = {}; + + let ubnt_ac = false; + const board_type = hardware.getBoard().model.id; + if (index(board_type, "ubnt,") === 0 && index(board_type, "ac") !== -1) { + ubnt_ac = true + } + + const reArp = /^([\.0-9]+) +0x. +0x. +([0-9a-fA-F:]+)/; + const arp = {}; + let f = fs.open("/proc/net/arp"); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, reArp); + if (m) { + arp[m[2]] = m[1]; + } + } + f.close(); + } + + f = fs.popen(`/usr/sbin/iw dev ${wifiiface} station dump`); + if (f) { + const re = regexp(`^Station ([0-9a-fA-F:]+) \\(on ${wifiiface}\\)`); + const reSi = /signal:[ \t]+(-[0-9]+)/; + let station; + for (let l = f.read("line"); length(l); l = f.read("line")) { + let m = match(l, re); + if (m) { + station = scan[m[1]]; + if (!station) { + const ip = arp[m[1]]; + const hostname = ip ? network.nslookup(ip) : null; + station = { + mac: m[1], + signal: 9999, + chan: { [mychan]: true }, + key: "", + joined: false, + mode: "Connected Ad-Hoc Station", + ssid: myssid, + ip: ip, + hostname: hostname + }; + scan[m[1]] = station; + } + } + m = match(l, reSi); + if (m) { + station.signal = int(m[1]); + } + } + f.close(); + } + + if (ubnt_ac) { + system(`/usr/sbin/iw dev ${wifiiface} ibss leave > /dev/null 2>&1`); + system("/sbin/wifi up > /dev/null 2>&1"); + for (let attempt = 10; attempt > 0; attempt--) { + f = fs.popen(`/usr/sbin/iw dev ${wifiiface} scan`); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (substr(l, 0, 4) === "BSS ") { + attempt = 0; + break; + } + } + f.close(); + } + if (attempt > 0) { + sleep(2000); + } + } + } + + f = fs.popen(`/usr/sbin/iw dev ${wifiiface} scan passive`); + if (f) { + const re = /^BSS ([0-9a-fA-F:]+)/; + const reF = /freq: ([0-9]+)/; + const reSs = /SSID: (.+)\n/; + const reSi = /signal: (.+)\n/; + const reC = /Group cipher: (.+)\n/; + let station = {}; + for (let l = f.read("line"); length(l); l = f.read("line")) { + let m = match(l, re); + if (m) { + station = scan[m[1]]; + if (!station) { + const ip = arp[m[1]]; + const hostname = ip ? network.nslookup(ip) : null; + station = { + mac: m[1], + signal: 9999, + chan: {}, + key: "", + joined: false, + mode: "AP", + ssid: "", + ip: ip, + hostname: hostname + }; + scan[m[1]] = station; + } + if (index(l, "joined") !== -1) { + station.mode = "My Ad-Hoc Network"; + station.joined = true; + station.hostname = nodename; + } + } + m = match(l, reF); + if (m) { + if (m[1] == myfreq) { + station.mode = "My Ad-Hoc Network"; + station.joined = true; + } + const chan = hardware.getChannelFromFrequency(int(m[1])); + if (chan) { + station.chan[chan] = true; + } + } + m = match(l, reSs); + if (m) { + station.ssid = m[1]; + } + m = match(l, reSi); + if (m) { + station.signal = int(m[1]); + } + m = match(l, reC); + if (m) { + station.key = m[1]; + } + if (index(l, "capability: IBSS") !== -1 && station.mode === "AP") { + station.mode = "Foreign Ad-Hoc Network"; + } + } + f.close(); + } + + for (let k in scan) { + scan[k].chan = join(" ", sort(keys(scan[k].chan))); + scan[k].hostname = scan[k].hostname ? replace(scan[k].hostname, ".local.mesh", "") : null; + } + + last_scan = sort( + filter(values(scan), v => v.signal !== 9999 || v.joined), + (a, b) => b.signal - a.signal + ); + + fs.writefile(last_scan_file, sprintf("%J", last_scan)); + scan_time = "0 seconds ago"; +} +else { + const d = fs.readfile(last_scan_file); + if (d) { + last_scan = json(d); + const last = time() - fs.stat(last_scan_file).mtime; + if (last === 1) { + scan_time = "1 second ago"; + } + else if (last < 60) { + scan_time = `${last} seconds ago`; + } + else if (last < 120) { + scan_time = `1 minute ago`; + } + else if (last < 3600) { + scan_time = `${int(last / 60)} minutes ago`; + } + else { + scan_time = "a long time ago"; + } + } +} +%} +
+ {{_R("tool-header", "WiFi Scan")}} +
+ + + + + + + + {% for (let i = 0; i < length(last_scan); i++) { + const s = last_scan[i]; + %} + + {% } %} + +
SNRSignalChanEncSSIDHostnameMAC/BSSID802.11 Mode
{{95 + s.signal}}{{s.signal}}{{s.chan}}-{{s.ssid}}{{s.hostname || s.ip || "-"}}{{s.mac}}{{s.mode}}
+
+
+
Last Scan: {{scan_time}}
+ +
+ {{_H("
Scan the appropriate radio spectrum for other nodes and wifi devices. What a node can find while scanning is highly dependent + on the hardware itself. Also, due to the nature of wireless scanning and beaconing, multiple scans are something required for a + complete pictures of the surrounding radio area.

By default the last scan is shown.")}} + {{_R("tool-footer")}} + +

diff --git a/files/app/main/tools/e/wifisignal.ut b/files/app/main/tools/e/wifisignal.ut new file mode 100755 index 00000000..4f804c9f --- /dev/null +++ b/files/app/main/tools/e/wifisignal.ut @@ -0,0 +1,292 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "PUT") { + const wifiiface = uci.get("network", "wifi", "device"); + let f = fs.popen(`iw ${wifiiface} station dump`); + if (f) { + const signals = {}; + let mac = null; + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (index(l, "Station") === 0) { + const m = match(l, / ([0-9a-fA-F:]+) /); + if (m) { + mac = m[1]; + } + } + if (index(l, "signal:") !== -1) { + const m = match(l, /[\t ]([0-9\-]+)[\t ]/); + if (m) { + signals[mac] = int(m[1]); + } + } + } + f.close(); + let noise = -95; + f = fs.popen(`iw ${wifiiface} survey dump`); + if (f) { + const reN = /noise:[ \t]+([0-9\-]+) dBm/; + let ff = false; + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (index(l, "[in use]") !== -1) { + ff = true; + } + else if (ff) { + const m = match(l, reN); + if (m) { + noise = int(m[1]); + break; + } + } + } + f.close(); + } + printf("%J", { s: signals, n: noise }); + } + return; +} +const stations = fs.lsdir("/tmp/snrlog"); +for (let i = 0; i < length(stations); i++) { + const s = stations[i]; + const m = match(s, /^([0-9A-Fa-f:]+)-(.*)$/); + if (m) { + stations[i] = { hostname: m[2], mac: lc(m[1]) }; + } + else { + stations[i] = null; + } +} +%} +
+ {{_R("tool-header", "WiFi Signal")}} +
+
+
+
Node
+
Select the target node
+
+
+ +
+
+
+
+
+
- dBm
snr: -
+
+
+
+
+
+
+
+
+
+ + + dBm + 0 + -20 + -40 + -60 + -80 + -100 + -120 + Last 5 minutes + + +
+
+
+
+
+
+
Sound
+
Enable audible indicator
+
+
+ +
+
+
+
+
Volume
+
+
+ +
+
+
+
+
Pitch
+
+
+ +
+
+
+ {{_H("This tool helps to align the node's antenna with its neighbors for the best signal strength. The indicator on the left + shows the current, best, and worst signal strenghts. The graph on the right show the history of the most recent signal strengths. + Specific neighbors can be selected, the default being an average of all those currently visible.

A sound indicator is also + provided which is useful when aligning antennas without looking at this display.")}} +

+ {{_R("tool-footer")}} + +
diff --git a/files/app/main/tunnels.ut b/files/app/main/tunnels.ut new file mode 100755 index 00000000..e9015e9a --- /dev/null +++ b/files/app/main/tunnels.ut @@ -0,0 +1,35 @@ +{% +/* + * 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 + */ +%} +{{_R("tunnels")}} diff --git a/files/app/partial/changes.ut b/files/app/partial/changes.ut new file mode 100755 index 00000000..cbc0a6c0 --- /dev/null +++ b/files/app/partial/changes.ut @@ -0,0 +1,83 @@ +{% +/* + * 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 + */ +%} +{% + let pwchanged = false; + if (request.env.REQUEST_METHOD === "PUT") { + if (request.args.revert) { + configuration.revertChanges(); + } + else if (request.args.commit) { + pwchanged = configuration.isPasswordChanged(); + configuration.commitChanges(); + print(`
${_R(request.page)}
`); + } + } +%} +
{% + let changes = 0; + let reboot = false; + if (pwchanged) { + %}{% + } + if (auth.isAdmin) { + changes = configuration.countChanges(); + if (fs.access("/tmp/reboot-required")) { + reboot = true; + } + } + if (reboot) { + %} +
+ Reboot required: + +
+ {% } + else if (changes > 0) { + %} +
+ Pending changes: {{changes}} + + +
+ + {% } %}
diff --git a/files/app/partial/dhcp.ut b/files/app/partial/dhcp.ut new file mode 100755 index 00000000..2ff02860 --- /dev/null +++ b/files/app/partial/dhcp.ut @@ -0,0 +1,120 @@ +{% +/* + * 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 + */ +%} +{% + const dhcp = configuration.getDHCP(); + let da = 0; + let dr = 0; + let at = 0; + let ao = 0; + if (dhcp.enabled) { + let f = fs.open(dhcp.leases); + if (f) { + while (length(f.read("line"))) { + da++; + } + f.close(); + } + f = fs.open(dhcp.reservations); + if (f) { + while (length(f.read("line"))) { + dr++; + } + f.close(); + } + f = fs.open(dhcp.dhcptags); + if (f) { + while (length(f.read("line"))) { + at++; + } + f.close(); + } + f = fs.open(dhcp.dhcpoptions); + if (f) { + while (length(f.read("line"))) { + ao++; + } + f.close(); + } + } +%} +{% if (dhcp.enabled) { %} +
+
LAN DHCP
+
+
Active
+
status
+
+
+
{{dhcp.gateway}} / {{dhcp.cidr}}
+
gateway
+
+
+
{{dhcp.start}} - {{dhcp.end}}
+
range
+
+
+
+
+
{{dr}}
+
reserved leases
+
+
+
{{da}}
+
active leases
+
+
+
+
+
+
{{at}}
+
tags
+
+
+
{{ao}}
+
options
+
+
+
+
+
+{% } else { %} +
+
LAN DHCP
+
+
Disabled
+
status
+
+
+{% } %} diff --git a/files/app/partial/dialog-advanced.ut b/files/app/partial/dialog-advanced.ut new file mode 100755 index 00000000..f894c5fe --- /dev/null +++ b/files/app/partial/dialog-advanced.ut @@ -0,0 +1,38 @@ +{% +/* + * 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 + */ +%} +
+ +
+
diff --git a/files/app/partial/dialog-footer.ut b/files/app/partial/dialog-footer.ut new file mode 100755 index 00000000..d9b0363d --- /dev/null +++ b/files/app/partial/dialog-footer.ut @@ -0,0 +1,41 @@ +{% +/* + * 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 + */ +%} + diff --git a/files/app/partial/dialog-header.ut b/files/app/partial/dialog-header.ut new file mode 100755 index 00000000..3b8a6d3d --- /dev/null +++ b/files/app/partial/dialog-header.ut @@ -0,0 +1,46 @@ +{% +/* + * 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 + */ +%} +{% +if (request.env.REQUEST_METHOD === "GET" && request.headers["hx-target"] === "ctrl-modal" && !request.headers["include-help"] && !request.headers["include-advanced"]) { + configuration.prepareModalChanges(); +} +%} +
+ + +
{{inner}}
+
configuration
+
+
diff --git a/files/app/partial/dialog-messages.ut b/files/app/partial/dialog-messages.ut new file mode 100755 index 00000000..193ed333 --- /dev/null +++ b/files/app/partial/dialog-messages.ut @@ -0,0 +1,38 @@ +{% +/* + * 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 + */ +%} +
+
+
+
diff --git a/files/app/partial/firmware.ut b/files/app/partial/firmware.ut new file mode 100755 index 00000000..1cf4085e --- /dev/null +++ b/files/app/partial/firmware.ut @@ -0,0 +1,68 @@ +{% +/* + * 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 + */ +%} +{% + const firmware = configuration.getFirmwareVersion(); + let releases = split(fs.readfile("/etc/current_releases"), " "); + let message = "uptodate"; + if (!releases) { + message = ""; + releases = [ firmware, firmware ]; + } + if (match(firmware, /^\d+\.\d+\.\d+\.\d+$/)) { + // release + if (firmware !== releases[0]) { + message = "needupdate"; + } + } + else if (match(firmware, /^\d\d\d\d\d\d\d\d-/)) { + // nightly + if (firmware !== releases[1]) { + message = "needupdate"; + } + } + else { + message = "custom"; + } +%} +
+
+
{{firmware}}
+
+
firmware version
+ + +
+
+
diff --git a/files/app/partial/general.ut b/files/app/partial/general.ut new file mode 100755 index 00000000..a459be7c --- /dev/null +++ b/files/app/partial/general.ut @@ -0,0 +1,60 @@ +{% +/* + * 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 + */ +%} +
+ {% if (fs.access(`${config.application}/resource/img/radio.png`)) { %} +
+ {% } %} +
+
{{configuration.getSettingAsString("description_node", "None")}}
+
description
+
+ {% if (auth.isAdmin) { %} +
+
{{uci.get("aredn", "@notes[0]", "private") || "-"}}
+
notes
+
+ {% } %} +
+
+{{_R("health")}} +
+{{_R("firmware")}} +{% if (auth.isAdmin && !hardware.isLowMemNode()) { %} +
+{{_R("packages")}} +
+{% } %} +
+{{_R("network")}} diff --git a/files/app/partial/health.ut b/files/app/partial/health.ut new file mode 100755 index 00000000..1f7126ba --- /dev/null +++ b/files/app/partial/health.ut @@ -0,0 +1,76 @@ +{% +/* + * 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 + */ +%} +{% + const d = split(fs.readfile("/proc/uptime"), " "); + const up = int(d[0]); + let uptime = sprintf("%d:%02d", int(up / 3600) % 24, int(up / 60) % 60); + if (up >= 172800) { + uptime = int(up / 86400) + " days, " + uptime; + } + else if (up > 86400) { + uptime = "1 day, " + uptime; + } + const ld = split(fs.readfile("/proc/loadavg"), " "); + const ram = int(match(fs.readfile("/proc/meminfo"), /MemFree: +(\d+) kB/)[1]) / 1000.0; + const f = fs.popen("exec /bin/df /"); + let flash = "-"; + if (f) { + flash = int(split(f.read("all"), /\s+/)[10]) / 1000.0; + f.close(); + } + const tm = localtime(); + const tmsource = fs.readfile("/tmp/timesync"); +%} +
+
{{tm.hour === 0 ? "12" : tm.hour > 12 ? tm.hour - 12 : tm.hour}}:{{sprintf("%02d", tm.min)}} {{tm.hour >= 12 ? "pm" : "am"}}
+
time{{tmsource ? " (" + tmsource + ")" : ""}}
+
+
+
{{uptime}}
+
uptime
+
{{ld[0]}}, {{ld[1]}}, {{ld[2]}}
+
load
+
+
+
{{flash}} MB
+
free flash
+
+
+
{{ram}} MB
+
free ram
+
+
+
+
diff --git a/files/app/partial/internal-services.ut b/files/app/partial/internal-services.ut new file mode 100755 index 00000000..936f611b --- /dev/null +++ b/files/app/partial/internal-services.ut @@ -0,0 +1,72 @@ +{% +/* + * 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 + */ +%} +{% + const rl = uci.get("aredn", "@remotelog[0]", "url") ? "active" : "disabled"; + const sn = uci.get("aredn", "@supernode[0]", "enable") === "1" ? "active" : "disabled"; + const cm = uci.get("aredn", "@supernode[0]", "support") === "0" ? "disabled" : "active"; + const wd = uci.get("aredn", "@watchdog[0]", "enable") === "1" ? "active" : "disabled"; + const ip = uci.get("aredn", "@iperf[0]", "enable") === "0" ? "disabled" : "active"; + const s = fs.stat("/tmp/metrics-ran"); + const mt = s && time() - s.mtime < 3600000 ? "active" : "inactive"; + const ws = "active"; + const ww = "active"; + const wt = "active"; + const ws = uci.get("aredn", "@wan[0]", "ssh_access") === "0" ? "disabled" : "active"; + const ww = uci.get("aredn", "@wan[0]", "web_access") === "0" ? "disabled" : "active"; + const wt = uci.get("aredn", "@wan[0]", "telnet_access") === "0" ? "disabled" : "active"; + const poe = uci.get("aredn", "@poe[0]", "passthrough") === "0" ? "disabled" : "active"; + const pou = uci.get("aredn", "@usb[0]", "passthrough") === "0" ? "disabled" : "active"; + +%} +
+
Internal Services
+
+
Cloud Mesh
+
Metrics
+
Watchdog
+
Remote Logging
+
IPerf3 Server
+
Supernode
+
WAN ssh
+
WAN telnet
+
WAN web
+ {% if (hardware.hasPOE()) { %} +
PoE out
+ {% } %} + {% if (hardware.hasUSBPower()) { %} +
USB power out
+ {% } %} +
+
diff --git a/files/app/partial/local-and-neighbor-devices.ut b/files/app/partial/local-and-neighbor-devices.ut new file mode 100755 index 00000000..fc84fd36 --- /dev/null +++ b/files/app/partial/local-and-neighbor-devices.ut @@ -0,0 +1,209 @@ +{% +/* + * 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 + */ +%} +{% + let links = {}; + const o = olsr.getLinks(); + for (let i = 0; i < length(o); i++) { + links[o[i].remoteIP] = o[i]; + } + function calcColor(tracker) + { + if (tracker.blocked) { + return "blocked"; + } + if (!tracker.routable) { + return "idle"; + } + const quality = tracker.quality; + if (quality < 40) { + return "bad"; + } + else if (quality < 50) { + return "poor"; + } + else if (quality < 75) { + return "okay"; + } + else if (quality < 95) { + return "good"; + } + else { + return "excellent"; + } + }; + function calcBitrate(txbitrate, rxbitrate) + { + if (txbitrate) { + if (rxbitrate) { + return sprintf("%.1f", ((txbitrate + rxbitrate) * 5 + 0.5) / 10); + } + else { + return sprintf("%.1f", (txbitrate * 10 + 0.5) / 10); + } + } + return "-"; + } +%} +
+
Local Nodes
+
+
+
+
+
lq
nlq
snr
n snr
errors
mbps
{{units.distanceUnit()}}
+
+
+ {% + const trackers = lqm.getTrackers(); + const llist = []; + const nlist = []; + const hlist = lqm.getHidden(); + for (mac in trackers) { + const tracker = trackers[mac]; + if (tracker.hostname || (tracker.ip && tracker.routable)) { + if (tracker.type === "DtD" && tracker.distance < 50) { + push(llist, { name: tracker.hostname || `|${tracker.ip}`, mac: mac }); + } + else { + push(nlist, { name: tracker.hostname || `|${tracker.ip}`, mac: mac }); + } + } + } + if (length(llist) > 0) { + sort(llist, (a, b) => a.name == b.name ? 0 : a.name < b.name ? -1 : 1); + for (let i = 0; i < length(llist); i++) { + const tracker = trackers[llist[i].mac]; + const status = calcColor(tracker); + print("
"); + const link = links[tracker.ip] || {}; + const lq = link.lossMultiplier ? (int(100 * link.linkQuality * 65536 / link.lossMultiplier) + "%"): "-"; + const nlq = link.lossMultiplier ? (int(100 * link.neighborLinkQuality * 65536 / link.lossMultiplier) + "%") : "-"; + if (tracker.hostname) { + print(``); + } + else { + print(``); + } + print("
"); + print(`
${lq}
${nlq}
${100 - tracker.quality}%
`); + print("
"); + } + } + else { + print("
None
"); + } + %} +
+
+
+
Neighborhood Nodes
+
+ {% + if (length(nlist) > 0) { + sort(nlist, (a, b) => a.name == b.name ? 0 : a.name < b.name ? -1 : 1); + for (let i = 0; i < length(nlist); i++) { + const tracker = trackers[nlist[i].mac]; + const status = calcColor(tracker); + print(`
`); + const link = links[tracker.ip] || {}; + const lq = link.lossMultiplier ? (int(100 * link.linkQuality * 65536 / link.lossMultiplier) + "%"): "-"; + const nlq = link.lossMultiplier ? (int(100 * link.neighborLinkQuality * 65536 / link.lossMultiplier) + "%") : "-"; + let icon = ""; + let title = ""; + switch (tracker.type) { + case "RF": + title = "RF "; + icon = "wifi"; + break; + case "DtD": + title = "DtD "; + icon = "twoarrow"; + break; + case "Xlink": + title = "Xlink "; + icon = "plane"; + break; + case "Tunnel": + title = "Legacy tunnel "; + icon = "globe"; + break; + case "Wireguard": + title = "Wireguard tunnel "; + icon = "globe"; + break; + default: + break; + } + if (tracker.hostname) { + print(``); + } + else { + print(``); + } + print("
"); + let d = "-"; + if ("distance" in tracker) { + d = units.meters2distance(tracker.distance); + if (d < 1) { + d = "< 1"; + } + else { + d = sprintf("%.1f", d); + } + } + print(`
${lq}
${nlq}
${tracker.snr || "-"}
${tracker.rev_snr || "-"}
${100 - tracker.quality}%
${calcBitrate(tracker.tx_bitrate, tracker.rx_bitrate)}
${d}
`); + print("
"); + } + } + else { + print("
None
"); + } + %} +
+
+{% if (length(hlist) > 0) { %} +
+
Hidden Nodes
+
+ {% + sort(hlist, (a, b) => a.hostname == b.hostname ? 0 : a.hostname < b.hostname ? -1 : 1); + for (let i = 0; i < length(hlist); i++) { + const hostname = nlist[i].hostname; + print(``); + } + %} +
+
+{% } %} diff --git a/files/app/partial/local-services.ut b/files/app/partial/local-services.ut new file mode 100755 index 00000000..8c2e5b99 --- /dev/null +++ b/files/app/partial/local-services.ut @@ -0,0 +1,145 @@ +{% +/* + * 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 + */ +%} +{% + const reService = /^([^|]+)\|1\|([^|]+)\|([^|]+)\|(\d+)\|(.*)$/; + const reLink = /^([^|]+)\|0\|\|([^|]+)\|\|$/; + const reType = /^(.+) \[([a-z]+)\]$/; + const reOlsr = /PlParam "service" ".*\|([^|]+)"/; + + const services = []; + const devices = []; + const leases = {}; + const activesvc = {}; + + const dhcp = configuration.getDHCP(); + + let f = fs.open("/var/etc/olsrd.conf"); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, reOlsr); + if (m) { + activesvc[m[1]] = true; + } + } + f.close(); + } + f = fs.open(dhcp.services); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const v = match(trim(l), reService); + if (v) { + let type = ""; + if (!activesvc[v[1]]) { + type += `
`; + } + const v2 = match(v[1], reType); + if (v2) { + v[1] = v2[1]; + type += `
`; + } + switch (v[4]) { + case "80": + push(services, ``); + break; + case "443": + push(services, ``); + break; + default: + push(services, ``); + break; + } + } + else { + const k = match(trim(l), reLink); + if (k) { + let type = ""; + const k2 = match(k[1], reType); + if (k2) { + k[1] = k2[1]; + type = `
`; + } + push(services, `
${k[1]}${type}
`); + } + } + } + f.close(); + } + + f = fs.open(dhcp.reservations); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const v = match(trim(l), /^([^ ]+) ([^ ]+) ([^ ]+) ?(.*)/); + if (v && v[4] !== "#NOPROP") { + push(devices, `
${v[3]}
`); + } + } + f.close(); + } +%} +
+
Local Services
+
+ {% + if (length(services) === 0) { + print("None"); + } + else { + print("
"); + for (let i = 0; i < length(services); i++) { + print(services[i]); + } + print("
"); + } + %} +
+
+
+
+
Local Devices
+
+ {% + if (length(devices) === 0) { + print("None"); + } + else { + print("
"); + for (let i = 0; i < length(devices); i++) { + print(devices[i]); + } + print("
"); + } + %} +
+
diff --git a/files/app/partial/location.ut b/files/app/partial/location.ut new file mode 100755 index 00000000..1e608ddf --- /dev/null +++ b/files/app/partial/location.ut @@ -0,0 +1,65 @@ +{% +/* + * 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 + */ +%} +{% + const map = uci.get("aredn", "@location[0]", "map"); + const lat = uci.get("aredn", "@location[0]", "lat"); + const lon = uci.get("aredn", "@location[0]", "lon"); + const gridsquare = uci.get("aredn", "@location[0]", "gridsquare"); + const source = uci.get("aredn", "@location[0]", "source"); + const mapurl = lat && lon && map ? replace(replace(map, "(lat)", lat), "(lon)", lon) : null; +%} +
+{% if (mapurl) { %} +
+ +{% } %} + {% if (lat && lon) { %} +
+
{{lat}}, {{lon}}
+
{{gridsquare}}
+ {% } else if (gridsquare) { %} +
+
{{gridsquare}}
+ {% } else { %} +
+
Unknown
+ {% } %} +
+
location{{source ? " (" + source + ")" : ""}}
+
diff --git a/files/app/partial/login.ut b/files/app/partial/login.ut new file mode 100755 index 00000000..0fdc8779 --- /dev/null +++ b/files/app/partial/login.ut @@ -0,0 +1,109 @@ +{% +/* + * 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 + */ +%} +{% if (!auth.isAdmin) { %} + + Password + + + +{% } else { %} + + +{% } %} diff --git a/files/app/partial/mesh-data.ut b/files/app/partial/mesh-data.ut new file mode 100755 index 00000000..33480633 --- /dev/null +++ b/files/app/partial/mesh-data.ut @@ -0,0 +1,127 @@ +{% +/* + * 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 + */ +%} +{% +print("\n"); + +%} diff --git a/files/app/partial/mesh-summary.ut b/files/app/partial/mesh-summary.ut new file mode 100755 index 00000000..c8ceac55 --- /dev/null +++ b/files/app/partial/mesh-summary.ut @@ -0,0 +1,53 @@ +{% +/* + * 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 + */ +%} +{% + const counts = mesh.getNodeCounts(); +%} +
+
Mesh
+
+
+
+
{{counts.nodes}}
+
nodes
+
+
+
{{counts.devices}}
+
devices
+
+
+
+
+
diff --git a/files/app/partial/mesh.ut b/files/app/partial/mesh.ut new file mode 100755 index 00000000..36615c98 --- /dev/null +++ b/files/app/partial/mesh.ut @@ -0,0 +1,57 @@ +{% +/* + * 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 + */ +%} +{{_R("mesh-data")}} +
+
+
+
+
+
+
+ This page shows a list of all the other nodes on the network, as well as what server and services they provide. + Nodes which are closer to you (have less radio hops to reach from here) are toward the top of this page, while nodes further + away are toward the bottom. As nodes get further away, they often become harder (or impossible) to reach. We group nodes together + with a simple colored border where greener is better, and redder is worse. +

+ The search box above can be used to filter the nodes, servers and services on this page, making it easier to find specific things. + For example, typing "cam" in the box will filter out everything except names containsing "cam" ... which are probably cameras. +

+
+
+{% if (!config.resourcehash) { %} + +{% } else { %} + +{% } %} diff --git a/files/app/partial/messages.ut b/files/app/partial/messages.ut new file mode 100755 index 00000000..ca042eab --- /dev/null +++ b/files/app/partial/messages.ut @@ -0,0 +1,99 @@ +{% +/* + * 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 + */ +%} +{% + const allmsgs = messages.getMessages(); + const todos = messages.getToDos(); +%} +
+ {% if (allmsgs.system) { %} +
System Messages
+
+ {% + const msgs = allmsgs.system; + for (let i = 0; i < length(msgs); i++) { + print(`
${msgs[i]}
`); + } + delete allmsgs.system; + %} +
+ {% } + if (allmsgs.yournode) { %} +
Your Messages
+
+ {% + const msgs = allmsgs.yournode; + for (let i = 0; i < length(msgs); i++) { + print(`
${msgs[i]}
`); + } + delete allmsgs.yournode; + %} +
+ {% } + if (allmsgs["all nodes"]) { %} +
All Node Messages
+
+ {% + const msgs = allmsgs["all nodes"]; + for (let i = 0; i < length(msgs); i++) { + print(`
${msgs[i]}
`); + } + delete allmsgs["all nodes"]; + %} +
+ {% } + for (let _ in allmsgs) { + %}
Other Messages
+
{% + for (let k in allmsgs) { + const msgs = allmsgs[k]; + for (let i = 0; i < length(msgs); i++) { + print(`
${k}: ${msgs[i]}
`); + } + } + %}
{% + break; + } + if (length(todos) > 0) { %} +
To Do
+
+ {% + for (let i = 0; i < length(todos); i++) { + print(`
${todos[i]}
`); + } + %} +
+ {% } + %} +
diff --git a/files/app/partial/nav-status.ut b/files/app/partial/nav-status.ut new file mode 100755 index 00000000..24463af9 --- /dev/null +++ b/files/app/partial/nav-status.ut @@ -0,0 +1,11 @@ + diff --git a/files/app/partial/nav.ut b/files/app/partial/nav.ut new file mode 100755 index 00000000..60217b94 --- /dev/null +++ b/files/app/partial/nav.ut @@ -0,0 +1,75 @@ +{% +/* + * 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 + */ +%} +{{_R("changes")}} +
AREDNTM
+ +{{_R("nav-status")}} +
+{{_R("login")}} +{% if (auth.isAdmin) { %} + +{% } %} diff --git a/files/app/partial/network.ut b/files/app/partial/network.ut new file mode 100755 index 00000000..5617deae --- /dev/null +++ b/files/app/partial/network.ut @@ -0,0 +1,80 @@ +{% +/* + * 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 + */ +%} +
+
Network
+
{{uci.get("network", "wifi", "ipaddr")}} {{uci.get("network", "wifi", "netmask") == "255.255.255.255" ? "/ 32" : "/ 8"}}
+
mesh address
+
{{uci.get("network", "lan", "ipaddr")}} + {{`/ ${network.netmaskToCIDR(uci.get("network", "lan", "netmask"))} `}} +
+
lan address
+ {% + let validWan = false; + const wan_proto = uci.get("network", "wan", "proto"); + if (wan_proto === "dhcp") { + const ifaces = ubus.call("network.interface", "dump").interface; + for (let i = 0; i < length(ifaces); i++) { + if (ifaces[i].interface === "wan" && ifaces[i]["ipv4-address"]) { + const wan = ifaces[i]["ipv4-address"][0]; + print("
" + wan.address + " / " + wan.mask + "
"); + print("
wan address (dhcp)
"); + print("
" + ifaces[i].route[0].nexthop + "
"); + print("
wan gateway
"); + validWan = true; + break; + } + } + } + else if (wan_proto === "static") { + print("
" + uci.get("network", "wan", "ipaddr") + "
"); + print("
wan address (static)
"); + print("
" + uci.get("network", "wan", "gateway") + "
"); + print("
wan gateway
"); + valueWan = true; + } + if (validWan) { + let v = "-"; + const dns = split(uci.get("network", "lan", "dns"), " "); + if (dns && dns[0]) { + v = dns[0]; + if (dns[1]) { + v += "   " + dns[1]; + } + } + print("
" + v + "
"); + print("
wan dns
") + } + %} +
diff --git a/files/app/partial/oldui.ut b/files/app/partial/oldui.ut new file mode 100755 index 00000000..68474dc6 --- /dev/null +++ b/files/app/partial/oldui.ut @@ -0,0 +1 @@ +Old UI diff --git a/files/app/partial/open.ut b/files/app/partial/open.ut new file mode 100755 index 00000000..aa769df3 --- /dev/null +++ b/files/app/partial/open.ut @@ -0,0 +1,40 @@ +{% +/* + * 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 + */ +%} +{ + const d = document.querySelector('dialog'); + if (d && !d.open) { + d.showModal(); + } +} diff --git a/files/app/partial/packages.ut b/files/app/partial/packages.ut new file mode 100755 index 00000000..9ef4dee4 --- /dev/null +++ b/files/app/partial/packages.ut @@ -0,0 +1,51 @@ +{% +/* + * 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 + */ +%} +{% +let count = 0; +const opkgs = {}; +map(split(fs.readfile("/etc/permpkg"), "\n"), p => opkgs[p] = true); +const f = fs.popen("/bin/opkg list-installed"); +if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, /^[^ \t]+/); + if (m && !opkgs[m[0]]) { + count++; + } + } + f.close(); +} +%} +
{{count}}
+
installed packages
diff --git a/files/app/partial/ports-and-xlinks.ut b/files/app/partial/ports-and-xlinks.ut new file mode 100755 index 00000000..eab5151c --- /dev/null +++ b/files/app/partial/ports-and-xlinks.ut @@ -0,0 +1,74 @@ +{% +/* + * 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 + */ +%} +{% + const ports = hardware.getEthernetPorts(); + let active = 0; + for (let i = 0; i < length(ports); i++) { + const state = hardware.getEthernetPortInfo(ports[i].k); + if (state.active) { + active++; + } + } + let xcount = 0; + uciMesh.foreach("xlink", "interface", _ => { + xcount++; + }); +%} +
+
+ {% if (length(ports) > 1) { %} +
+
Ethernet Ports
+
+
+
{{length(ports)}}
+
ports
+
+
+
{{active}}
+
active
+
+
+
+ {% } %} +
+
XLinks
+
+
{{xcount}}
+
xlinks
+
+
+
+
diff --git a/files/app/partial/radio-and-antenna.ut b/files/app/partial/radio-and-antenna.ut new file mode 100755 index 00000000..41899f94 --- /dev/null +++ b/files/app/partial/radio-and-antenna.ut @@ -0,0 +1,122 @@ +{% +/* + * 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 + */ +%} +{% + const radio = radios.getActiveConfiguration(); + let midx = -1; + for (let i = 0; i < length(radio); i++) { + if (radio[i].mode === radios.RADIO_MESH) { + midx = i; + break; + } + } +%} +
+
Radio
+
+
{{hardware.getRadio().name}}
+
model
+ {% if (midx !== -1) { %} +
+
+
{{radio[midx].modes[radios.RADIO_MESH].channel}}
+
channel
+
+
+
{{hardware.getChannelFrequencyRange(radio[midx].iface, radio[midx].modes[radios.RADIO_MESH].channel, radio[midx].modes[radios.RADIO_MESH].bandwidth)}}
+
frequencies
+
+
+
{{radio[midx].modes[radios.RADIO_MESH].bandwidth}} MHz
+
bandwidth
+
+
+
+
+
{{radio[midx].modes[radios.RADIO_MESH].txpower + radio[midx].txpoweroffset}} dBm
+
tx power
+
+
+
{{int(0.5 + units.meters2distance(uci.get("aredn", "@lqm[0]", "max_distance")))}} {{units.distanceUnit()}}
+
maximum distance
+
+
+
{{uci.get("aredn", "@lqm[0]", "min_snr")}}
+
minimum snr
+
+
+ {% } %} +
+{% if (midx !== -1) { %} +
Antenna
+
+ {% + const antenna = hardware.getAntennaInfo(radio[midx].iface, uci.get("aredn", "@location[0]", "antenna")); + const antennaAux = hardware.getAntennaAuxInfo(radio[midx].iface, uci.get("aredn", "@location[0]", "antenna_aux")); + %} +
{{(antenna || { description: "-"}).description}}
+
antenna
+ {% if (antennaAux) { %} +
{{(antennaAux || { description: "-"}).description}}
+
aux antenna
+ {% } %} +
+
+ {% if (uci.get("aredn", "@location[0]", "azimuth")) { %} +
{{uci.get("aredn", "@location[0]", "azimuth")}}°
+ {% } else { %} +
-
+ {% } %} +
azimuth
+
+
+ {% if (uci.get("aredn", "@location[0]", "height")) { %} +
{{uci.get("aredn", "@location[0]", "height")}}m
+ {% } else { %} +
-
+ {% } %} +
height
+
+
+ {% if (uci.get("aredn", "@location[0]", "elevation")) { %} +
{{uci.get("aredn", "@location[0]", "elevation")}}°
+ {% } else { %} +
-
+ {% } %} +
elevation
+
+
+
+{% } %} +
diff --git a/files/app/partial/reboot-firmware.ut b/files/app/partial/reboot-firmware.ut new file mode 100755 index 00000000..85a8f07c --- /dev/null +++ b/files/app/partial/reboot-firmware.ut @@ -0,0 +1,69 @@ +{% +/* + * 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 + */ +%} +{% + const firstuse = fs.access("/tmp/do-not-keep-configuration"); +%} +{{configuration.getName()}} firmware updating +
+
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Updating Firmware
+
Updating the firmware on your node.
DO NOT REMOVE POWER UNTIL THIS IS COMPLETE.
+
+
+
 
+
+
+ + {{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: (firstuse ? `http://192.168.1.1/` : `http://${request.env.HTTP_HOST}/a/status`) })}} +
+
diff --git a/files/app/partial/reboot-firstuse-ram.ut b/files/app/partial/reboot-firstuse-ram.ut new file mode 100755 index 00000000..ad5dc312 --- /dev/null +++ b/files/app/partial/reboot-firstuse-ram.ut @@ -0,0 +1,59 @@ +{% +/* + * 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 + */ +%} +
+
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Installing Firmware
+
Installing the firmware on your node.
DO NOT REMOVE POWER UNTIL THIS IS COMPLETE.
+
+
+
 
+
+
+ + {{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: `http://192.168.1.1/` })}} +
+
diff --git a/files/app/partial/reboot-firstuse.ut b/files/app/partial/reboot-firstuse.ut new file mode 100755 index 00000000..c87ef15c --- /dev/null +++ b/files/app/partial/reboot-firstuse.ut @@ -0,0 +1,57 @@ +{% +/* + * 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 + */ +%} +{% +configuration.reset(); +const address = configuration.getSettingAsString("wifi_ip", "192.168.1.1"); +%} +
+
+
+ +
+
AREDNTM
+
Amateur Radio Emergency Data Network
+
+
+
Rebooting
+
Your node is rebooting.
After this is complete it will have the address {{address}}

Your browser will attempt to reconnect automatically, but you may need to adjust the network settings on your computer.

+
+
+
 
+
+
+ {{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: `http://${address}/a/status` })}} +
+
diff --git a/files/app/partial/reboot-mon.ut b/files/app/partial/reboot-mon.ut new file mode 100755 index 00000000..c2aedbf0 --- /dev/null +++ b/files/app/partial/reboot-mon.ut @@ -0,0 +1,81 @@ +{% +/* + * 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 + */ +%} + diff --git a/files/app/partial/selection.ut b/files/app/partial/selection.ut new file mode 100755 index 00000000..97356a13 --- /dev/null +++ b/files/app/partial/selection.ut @@ -0,0 +1,62 @@ +{% +/* + * 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 + */ +%} +
+ +
+
+ +
+
+ {% + const ip = (match(fs.readfile("/tmp/dnsmasq.d/supernode.conf"), /^#([0-9.]+)/) || [])[1]; + if (ip) { %} + +
+
+ {% } %} + {% + const map = uci.get("aredn", "@location[0]", "map"); + const lat = uci.get("aredn", "@location[0]", "lat"); + const lon = uci.get("aredn", "@location[0]", "lon"); + const mapurl = lat && lon && map ? replace(replace(map, "(lat)", lat), "(lon)", lon) : null; + if (mapurl) { + %} + +
+
+ {% } %} +
+
+{{_R("tools")}} diff --git a/files/app/partial/status.ut b/files/app/partial/status.ut new file mode 100755 index 00000000..4eca171a --- /dev/null +++ b/files/app/partial/status.ut @@ -0,0 +1,95 @@ +{% +/* + * 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 + */ +%} +
+
+ {{_R("general")}} +
+
+
+ {{_R("location")}} +
+
+
+
+ {% if (messages.haveMessages() || (auth.isAdmin && messages.haveToDos())) { %} +
+ {{_R("messages")}} +
+ {% } %} +
+
+ {% if (auth.isAdmin) { %} + {{_R("internal-services" )}} + {% } %} + {{_R("local-services")}} +
+
+
+
+ {{_R("local-and-neighbor-devices")}} +
+
+
+
+
+ {{_R("radio-and-antenna")}} +
+
+
+
+ {{_R("mesh-summary")}} +
+
+
+
+
+ {{_R("dhcp")}} +
+
+ {% if (length(hardware.getEthernetPorts()) > 0) { %} + + {% } %} + {% if (fs.access("/usr/bin/wg") || fs.access("/usr/sbin/vtund")) { %} +
+
+
+ {{_R("tunnels")}} +
+
+ {% } %} +
diff --git a/files/app/partial/switch.ut b/files/app/partial/switch.ut new file mode 100755 index 00000000..38feb991 --- /dev/null +++ b/files/app/partial/switch.ut @@ -0,0 +1,37 @@ +{% +/* + * 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 + */ +%} + diff --git a/files/app/partial/tool-footer.ut b/files/app/partial/tool-footer.ut new file mode 100755 index 00000000..bacd47b7 --- /dev/null +++ b/files/app/partial/tool-footer.ut @@ -0,0 +1,38 @@ +{% +/* + * 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 + */ +%} + diff --git a/files/app/partial/tool-header.ut b/files/app/partial/tool-header.ut new file mode 100755 index 00000000..4d131fb3 --- /dev/null +++ b/files/app/partial/tool-header.ut @@ -0,0 +1,41 @@ +{% +/* + * 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 + */ +%} +
+ + +
{{inner}}
+
tool
+
+
diff --git a/files/app/partial/tools.ut b/files/app/partial/tools.ut new file mode 100755 index 00000000..fc2bacd5 --- /dev/null +++ b/files/app/partial/tools.ut @@ -0,0 +1,50 @@ +{% +/* + * 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 + */ +%} + diff --git a/files/app/partial/tunnels.ut b/files/app/partial/tunnels.ut new file mode 100755 index 00000000..7b6d7d10 --- /dev/null +++ b/files/app/partial/tunnels.ut @@ -0,0 +1,133 @@ +{% +/* + * 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 + */ +%} +{% + let wc = 0; + let ws = 0; + let lc = 0; + let ls = 0; + uciMesh.foreach("wireguard", "client", function() + { + ws++; + }); + uciMesh.foreach("vtun", "client", function() + { + ls++; + }); + uciMesh.foreach("vtun", "server", function(s) + { + if (index(s.netip, ":") !== -1) { + wc++; + } + else { + lc++; + } + }); + let wac = 0; + let was = 0; + let lac = 0; + let las = 0; + const t = fs.popen("ps -w | grep vtun | grep ' tun '"); + if (t) { + for (let l = t.read("line"); length(l); l = t.read("line")) { + if (index(l, "vtund[s]") !== -1) { + las++; + } + else if (index(l, "vtund[c]") !== -1) { + lac++; + } + } + t.close(); + } + if (fs.access("/usr/bin/wg")) { + const w = fs.popen("/usr/bin/wg show all latest-handshakes"); + if (w) { + for (let l = w.read("line"); length(l); l = w.read("line")) { + const v = split(trim(l), /\t/); + if (v && int(v[2]) + 300 > time()) { + if (index(v[0], "wgc") === 0) { + was++; + } + else { + wac++; + } + } + } + w.close(); + } + } +%} +
+
Tunnels
+
+
Wireguard
+
+
+
{{wac}}
+
active clients
+
+
+
{{wc}}
+
allocated clients
+
+
+
{{was}}
+
active servers
+
+
+
{{ws}}
+
allocated servers
+
+
+
Legacy
+
+
+
{{lac}}
+
active clients
+
+
+
{{lc}}
+
allocated clients
+
+
+
{{las}}
+
active servers
+
+
+
{{ls}}
+
allocated servers
+
+
+
+
diff --git a/files/app/resource/css/admin.css b/files/app/resource/css/admin.css new file mode 100755 index 00000000..5e93da97 --- /dev/null +++ b/files/app/resource/css/admin.css @@ -0,0 +1,1460 @@ +/* ctrls start */ + +#ctrl-modal[open] +{ + display: flex; + font: inherit; + font-size: 16px; + height: 90%; + padding: 40px; + border: 1px solid var(--ctrl-modal-border-color); + background-color: var(--ctrl-modal-bg-color); + border-radius: 10px; + outline: none; +} +#ctrl-modal[open]:empty +{ + background-color: transparent; + border-color: transparent; +} +#ctrl-modal::backdrop +{ + background-color: var(--ctrl-modal-backdrop-color); +} +#ctrl-modal > .dialog +{ + display: flex; + flex-direction: column; + width: 520px; + flex: 1; + color: var(--ctrl-modal-fg-color); +} +#ctrl-modal .dialog.wide +{ + width: 720px; +} +#ctrl-modal .dialog > div:first-child +{ + flex: 0; +} +#ctrl-modal .dialog > div:nth-child(2) +{ + flex: 1; + overflow-y: scroll; + overscroll-behavior: contain; + padding: 0 10px; +} +#ctrl-modal .dialog > div:last-child +{ + flex: 0; +} +#ctrl-modal .t +{ + font-size: 24px; + color: var(--ctrl-modal-fg-title-color); +} +#ctrl-modal .s +{ + font-size: 14px; + color: var(--ctrl-modal-fg-subtitle-color); +} +.ctrl-modal-footer #dialog-done +{ + float: right; +} +#dialog-help +{ + float: right; +} +#dialog-help.enabled +{ + background-color: var(--ctrl-modal-bg-secondary-color); +} +#dialog-help.enabled:active +{ + background-color: var(--ctrl-modal-textbox-bg-color); +} +.ctrl-modal-advanced +{ + display: flex; + padding: 15px 0; +} +.ctrl-modal-advanced hr +{ + flex: 1; + border-color: var(--ctrl-modal-advance-options-color); +} +#ctrl-modal .ctrl-modal-advanced button +{ + margin-left: -8px; + font: inherit; + font-size: 14px; + color: var(--ctrl-modal-advance-options-color); + border: 0 !important; + background-color: transparent !important; +} +#ctrl-modal .ctrl-modal-advanced button:hover +{ + color: var(--ctrl-modal-fg-color); +} +.ctrl-modal-advanced + div +{ + display: none; +} +.ctrl-modal-advanced.active + div +{ + display: block; +} + +#ctrl-modal .o +{ + color: var(--ctrl-modal-fg-option-color); + font-size: 16px; +} +#ctrl-modal .m +{ + color: var(--ctrl-modal-fg-message-color); + font-size: 14px; + padding: 4px 8px 16px 0; +} +#ctrl-modal .compact .m +{ + padding-bottom: 10px; +} +#ctrl-modal .help +{ + color: var(--ctrl-modal-fg-help-color); + font-size: 14px; + padding: 0px 40px 20px 10px; +} + +#ctrl-modal input[type=text], +#ctrl-modal input[type=password], +#ctrl-modal input[type=file] +{ + color: var(--ctrl-modal-fg-color); + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); + padding: 5px; + margin-top: 2px; + border-radius: .2em; + outline: none; + font: inherit; + font-size: 14px; + text-align: right; +} +#ctrl-modal input[type=text]:invalid, +#ctrl-modal input[type=password]:invalid, +#ctrl-modal select:invalid +{ + border-color: var(--ctrl-modal-border-color-error) !important; +} +#ctrl-modal input[type=file]::file-selector-button +{ + color: var(--ctrl-modal-fg-color); + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); +} +#ctrl-modal .noborder input[type=text], +#ctrl-modal .noborder input[type=password], +#ctrl-modal .noborder input[type=file] +{ + border-color: transparent; +} +#ctrl-modal input[type=file] +{ + max-width: 350px; + text-align: left; +} + +#ctrl-modal select +{ + font: inherit; + font-size: 14px; + appearance: none; + outline: none; + padding: 5px 5px 5px 10px; + color: var(--ctrl-modal-fg-color); + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); + border-radius: .2em; + direction: rtl; + text-align: right; +} +#ctrl-modal select option +{ + direction: ltr; +} + +#ctrl-modal textarea +{ + font: inherit; + font-size: 14px; + color: var(--ctrl-modal-fg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); + background-color: var(--ctrl-modal-textbox-bg-color); + padding: 5px; + outline: none; + resize: none; +} + +#ctrl-modal button +{ + margin-top: 4px; + min-width: 24px; + height: 24px; + border-radius: 4px; + color: var(--ctrl-modal-textbox-border-color); + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); + white-space: nowrap; + font-size: 14px; +} +#ctrl-modal button:hover:not(:disabled) +{ + background-color: var(--ctrl-modal-bg-secondary-color); +} +#ctrl-modal button:disabled +{ + border-color: var(--ctrl-modal-textbox-bg-color); +} + +label.switch +{ + display: inline-block; + position: relative; + height: 2rem; + width: 3.5rem; + margin-right: 2px; + vertical-align: bottom; +} +.switch input[type=checkbox] +{ + position: absolute; + appearance: none; + height: 2rem; + width: 3.5rem; + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); + border-radius: .2em; + cursor: pointer; + outline: none; + margin-right: 0; +} + +.switch input[type=checkbox]::before +{ + content: ""; + display: block; + height: 1.9em; + width: 1.9em; + transform: translate(-50%, -50%); + position: absolute; + top: 50%; + left: calc(1.9em/2 + .3em); + background-color: var(--service-bg-color-status-disabled); + border-radius: .2em; + transition: .3s ease; +} +.switch.inactive input[type=checkbox]::before +{ + background-color: var(--service-bg-color-status-inactive); +} + +.switch input[type=checkbox]:checked::before +{ + background-color: var(--service-bg-color-status-active); + left: calc(100% - (1.9em/2 + .3em)); +} + +/* ctrls end */ + +/* changes start */ + +#changes +{ + position: absolute; + left: 0; + width: 100%; + font-size: 16px; + color: var(--title-fg-color); + text-align: center; +} +#changes > div +{ + display: inline-block; + width: 450px; + margin-top: 4px; + padding: 10px; + border: 2px solid var(--firmware-status-bg-color-positive); +} +#changes button +{ + font: inherit; + font-size: 12px; + margin: 0 10px; + padding: 8px; + background-color: transparent; +} +#changes button:first-child +{ + border: 2px solid var(--firmware-status-bg-color-positive); + color: var(--firmware-status-bg-color-positive); +} +#changes button:last-child +{ + border: 2px solid var(--firmware-status-bg-color-negative); + color: var(--firmware-status-bg-color-negative); +} + +/* changes end */ + +/* basics start */ + +#ctrl-modal .basics select[name=theme] +{ + text-transform: capitalize; +} +#ctrl-modal .basics .password .cols div:last-child +{ + position: relative; + flex: 0; +} +#ctrl-modal .basics .password input +{ + padding-right: 30px; +} +#ctrl-modal .basics .password input + button +{ + position: absolute; + right: 2px; + border: 0; + border-radius: 12px; + scale: 0.8; + background-color: transparent; + filter: var(--icon-filter); +} + +/* basics end */ + +/* tools start */ + +.authenticated #tools +{ + display: block; +} +#tools input +{ + width: 64px; + height: 64px; + margin: 0; + opacity: 0; +} +#tools label +{ + position: absolute; + bottom: 0; + left: 0; +} +#tools label .icon +{ + position: absolute; + bottom: 0; + pointer-events: none; +} +#tools:hover label .icon +{ + filter: var(--nav-icon-filter-select); +} +#tools .menu +{ + display: none; + position: absolute; + bottom: 0; + left: 64px; + padding: 10px; + color: var(--title-fg-color); + background-color: var(--nav-bg-color); + border-top-right-radius: 10px; + border-left: 1px solid var(--ctrl-modal-bg-tertiary-color); + z-index: 1; +} +#tools:has(input:checked) .menu +{ + display: block; +} +#tools .menu > div +{ + padding: 10px 20px; + color: var(--title-fg-color); + background-color: var(--nav-bg-color); +} +#tools .menu > div[hx-target]:hover +{ + color: var(--menu-fg-select-color); +} +#tools .menu > div[hx-trigger]:hover .icon +{ + filter: var(--nav-icon-filter-select); +} +#tools .menu > div > div +{ + display: inline-block; + position: relative; + top: 4px; + height: 20px; + width: 30px; + margin: 0; +} + +/* tools end */ + +/* customization start */ + +#ctrl-modal .hideable:has(> div:first-child input[type='checkbox']:not(:checked)) > .hideable0 +{ + display: none; +} + +#ctrl-modal .hideable:has(> div:first-child select > option) > .hideable0, +#ctrl-modal .hideable:has(> div:first-child select > option) > .hideable1, +#ctrl-modal .hideable:has(> div:first-child select > option) > .hideable2, +#ctrl-modal .hideable:has(> div:first-child select > option) > .hideable3, +#ctrl-modal .hideable:has(> div:first-child select > option) > .hideable4 +{ + display: none; +} +#ctrl-modal .hideable:has(> div:first-child select > option[value="0"]:checked) > .hideable0, +#ctrl-modal .hideable:has(> div:first-child select > option[value="1"]:checked) > .hideable1, +#ctrl-modal .hideable:has(> div:first-child select > option[value="2"]:checked) > .hideable2, +#ctrl-modal .hideable:has(> div:first-child select > option[value="3"]:checked) > .hideable3, +#ctrl-modal .hideable:has(> div:first-child select > option[value="4"]:checked) > .hideable4 +{ + display: block; +} + +#location-edit-map +{ + position: relative; +} +#location-edit-map .icon.plus +{ + position: absolute; + top: 60px; + left: calc(50% - 40px); + width: 80px; + height: 80px; + pointer-events: none; + filter: var(--icon-filter); + z-index: 1; +} +#location-edit-map iframe +{ + width: 100%; + height: 200px; + border: 1px solid var(--hr-color); + margin-bottom: 16px; + filter: var(--map-filter); +} + +/* customization end */ + +/* dhcp start */ + +#ctrl-modal .noborder input, +#ctrl-modal .noborder select +{ + border-color: var(--ctrl-modal-textbox-bg-color); +} +#ctrl-modal #dhcp-reservations:not(:has(.reservation)) .reservation-label +{ + display: none; +} +#ctrl-modal .reservation input[type=text]:first-child, +#ctrl-modal .lease input:first-child, +#ctrl-modal .reservation-label div:first-child div:first-child, +#ctrl-modal .lease-label div:first-child div:first-child +{ + width: 155px; + border-color: var(--ctrl-modal-textbox-bg-color); +} +#ctrl-modal .reservation select:nth-child(2), +#ctrl-modal .lease input:nth-child(2), +#ctrl-modal .reservation-label div:first-child div:nth-child(2), +#ctrl-modal .lease-label div:first-child div:nth-child(2) +{ + width: 120px; + border-color: var(--ctrl-modal-textbox-bg-color); +} +#ctrl-modal .reservation input:nth-child(3), +#ctrl-modal .lease input:nth-child(3), +#ctrl-modal .reservation-label div:first-child div:nth-child(3), +#ctrl-modal .lease-label div:first-child div:nth-child(3) +{ + width: 130px; + border-color: var(--ctrl-modal-textbox-bg-color); +} +#ctrl-modal .reservation-label div:first-child div:nth-child(4) +{ + white-space: normal; + width: 50px; + text-align: center; +} +#ctrl-modal .reservation input[type=checkbox], +#ctrl-modal .dhcp-options .row input[type=checkbox] +{ + width: 14px; + height: 14px; + accent-color: var(--ctrl-modal-checkbox-color); + margin-left: 18px; +} +#ctrl-modal .reservation-label div:first-child div, +#ctrl-modal .lease-label div:first-child div, +#ctrl-modal .xlink-label div:first-child div, +#ctrl-modal .dhcptag-label div:first-child div, +#ctrl-modal .dhcpoption-label div:first-child div, +#ctrl-modal .port-forwards-label div:first-child div +{ + display: inline-block; + font-size: 10px; + text-align: right; + color: var(--ctrl-modal-fg-message-color); +} +#ctrl-modal .dhcp-options +{ + padding-top: 20px; +} +#ctrl-modal .dhcp-options .row :first-child, +#ctrl-modal .dhcp-tags .row :first-child +{ + width: 80px; + max-width: 80px; + overflow-x: hidden; +} +#ctrl-modal .dhcp-options .row :nth-child(2), +#ctrl-modal .dhcp-tags .row :nth-child(2) +{ + width: 210px; +} +#ctrl-modal .dhcp-options .row :nth-child(3) +{ + width: 120px; + max-width: 120px; + overflow-x: hidden; +} +#ctrl-modal .dhcp-tags .row :nth-child(3) +{ + width: 155px; + max-width: 155px; + overflow-x: hidden; +} +#ctrl-modal .dhcpoption-label .row :last-child +{ + position: relative; + top: -2px; + right: -10px; +} +#ctrl-modal .dhcp-tags:has(.list:empty) .dhcptag-label, +#ctrl-modal .dhcp-options:has(.list:empty) .dhcpoption-label +{ + display: none; +} + +/* dhcp end */ + +/* neighbors start */ + +#ctrl-modal .neighbor +{ + padding-bottom: 16px; +} +#ctrl-modal .neighbor a +{ + font: inherit; + color: inherit; + text-decoration: none; +} +#ctrl-modal .neighbor a:hover +{ + color: var(--section-link-fg-color); +} +#ctrl-modal .neighbor .o +{ + font-size: 20px; + padding: 8px; + border-radius: 6px; + margin-bottom: 4px; + background-color: var(--ctrl-modal-bg-secondary-color); +} +#ctrl-modal .neighbor > div:not(.o) +{ + padding-left: 7px; +} +#ctrl-modal .neighbor .i +{ + padding-top: 4px; + font-size: 14px; + color: var(--title-fg-color); +} +#ctrl-modal .neighbor .i div:last-child +{ + font-size: 12px; + color: var(--subtitle-fg-color); +} +#ctrl-modal .neighbor select +{ + padding: 2px 4px; + vertical-align: top; + float: right; + border-color: var(--subtitle-fg-color); +} +#neighbor-device-chart +{ + padding-top: 20px; +} +#neighbor-device-chart svg +{ + width: 100%; +} +#neighbor-device-chart .frame +{ + fill: none; + stroke: var(--title-fg-color); + stroke-width: 0.2px; + font-size: 4px; +} +#neighbor-device-chart .frame text +{ + text-anchor: end; +} +#neighbor-device-chart .signal +{ + fill: none; + stroke: var(--conn-fg-color-good); + stroke-width: 1px; +} +#neighbor-device-chart .noise +{ + fill: none; + stroke: var(--conn-fg-color-bad); + stroke-width: 1px; +} +#neighbor-device-chart .hints rect +{ + fill: transparent; +} +#neighbor-device-chart .hints rect:hover +{ + fill: var(--ctrl-modal-fg-color); + opacity: 0.1; +} +#neighbor-device-chart .hints g text +{ + display: none; + font-size: 4px; + fill: var(--ctrl-modal-fg-color); +} +#neighbor-device-chart .hints g.r text +{ + text-anchor: end; +} +#neighbor-device-chart .hints g:hover text +{ + display: block; +} + +/* neighbors end */ + +/* service start */ + +#host-aliases +{ + padding-bottom: 20px; +} +#local-services .service +{ + padding-top: 10px; + color: var(--ctrl-modal-fg-color); +} +#local-services .service .cols:first-child input +{ + flex: 1; + margin-right: 4px; +} +#local-services .service .cols:first-child > .cols +{ + flex: 1; + padding-right: 10px; +} +#local-services .service .cols:first-child > div select +{ + position: relative; + top: 2px; +} +#local-services .service .cols:last-child div:nth-child(2) +{ + white-space: nowrap; + padding-right: 34px; + font-size: 10px; +} +#local-services .service .cols:last-child input[name=protocol], +#local-services .service .cols:last-child input[name=port] +{ + width: 50px; +} +#local-services .service .cols:last-child select[name=hostname] +{ + width: 171.5px; +} +#ctrl-modal #local-services .service input[type=text], +#ctrl-modal #local-services .service select +{ + border-color: var(--ctrl-modal-textbox-bg-color); + text-align: left; +} +#ctrl-modal #local-services .service .cols:last-child input[type=text], +#ctrl-modal #local-services .service .cols:last-child select +{ + font-size: 14px; +} +#ctrl-modal #local-services .link +{ + padding-top: 3px; +} +#ctrl-modal #local-services .link select +{ + width: 100%; +} +#ctrl-modal #host-aliases .cols input +{ + flex: 1; + margin-right: 6px; + text-align: left; +} +#ctrl-modal #host-aliases .cols div +{ + flex: 0; + position: relative; + top: 2px; + padding-right: 6px; +} +#ctrl-modal #port-forwards > .cols +{ + padding-bottom: 6px; +} +#ctrl-modal #port-forwards label.switch input +{ + top: 1px; + height: 28px; + margin-left: 0; +} +#ctrl-modal #port-forwards select[name=port_src], +#ctrl-modal #port-forwards input[name=port_src], +#ctrl-modal #port-forwards select[name=port_dst], +#ctrl-modal .port-forwards-label div:first-child div:first-child +{ + width: 180px; + max-width: 180px; + overflow-x: hidden; +} +#ctrl-modal #port-forwards input[name=port_sports], +#ctrl-modal #port-forwards input[name=port_dport], +#ctrl-modal .port-forwards-label div:first-child div:nth-child(2) +{ + width: 110px; +} +#ctrl-modal .port-forwards-label div:first-child div:nth-child(3) +{ + width: 92px; +} +#ctrl-modal .port-forwards-label div:first-child div:nth-child(4) +{ + width: 56px; +} +#ctrl-modal:has(#port-forwards:empty) .port-forwards-label +{ + display: none; +} + +/* services end */ + +/* reboot and firstuse start */ + +.reboot, +.firstuse +{ + display: flex; + height: 100%; + padding: 64px; + background-color: white; + color: black; + overflow-y: scroll; +} +.reboot > div:first-child, +.firstuse > div:first-child +{ + flex: 0; +} +.reboot > div:nth-child(2), +.firstuse > div:nth-child(2) +{ + display: flex; + flex-direction: column; + padding: 64px 172px; + font-size: 32px; +} +.reboot #icon-logo, +.firstuse #icon-logo +{ + transform: scale(4) translate(47%,37%); +} +.reboot #icon-logo + div + div, +.firstuse #icon-logo + div + div +{ + position: relative; + margin-top: 107px; + font-size: 82px; + font-weight: bold; + line-height: 70px; + background-color: rgba(255,255,255,0.6); +} +.reboot #icon-logo + div + div span, +.firstuse #icon-logo + div + div span +{ + font-size: 10px; + line-height: 10px; +} +.reboot #icon-logo + div + div + div, +.firstuse #icon-logo + div + div + div +{ + font-size: 15px; + font-weight: bold; + white-space: nowrap; +} +.reboot > div:nth-child(2) > div + div +{ + flex: 1; + font-size: 18px; + line-height: 32px; + padding-top: 64px; +} +.reboot #countdown +{ + font-size: 24px; + font-variant: proportional-nums; +} +.reboot progress +{ + width: 100%; +} +.firstuse > div:nth-child(2) > div:nth-child(2) +{ + font-size: 24px; + line-height: 34px; + padding-top: 30px; +} +.firstuse > div:nth-child(2) > div:nth-child(3) +{ + padding-top: 40px; + font-size: 20px; + line-height: 30px; +} +.firstuse > div:nth-child(2) > div:nth-child(3) .cols div:first-child +{ + margin: auto 0; + white-space: nowrap; +} +.firstuse .cols div:nth-child(2) +{ + flex: 0; +} +.firstuse input[type=text], +.firstuse input[type=password] +{ + width: 350px; + font: inherit; + margin: 5px; + border: 1px solid black; + outline: none; +} +.firstuse input[type=password] +{ + width: 200px; +} +.firstuse input[type=text]:invalid, +.firstuse input[type=password]:invalid +{ + border-color: red; +} +.firstuse small +{ + font-size: 14px; +} +.firstuse button +{ + width: 200px; + padding: 10px; + border: 0; + margin: 40px 7px 0 0; + background-color: green; + color: white; + font: inherit; +} +.firstuse button:disabled +{ + background-color: #e0e0e0; + color: #c0c0c0; +} +.firstuse.ram > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) +{ + font-size: 16px; + padding-top: 10px; +} +.firstuse.ram > div:nth-child(2) > div:nth-child(2) > div:nth-child(3) +{ + font-size: 18px; + padding-top: 10px; +} +.firstuse.ram input[type=file] +{ + width: 465px; + margin-right: 6px; + padding: 6px; + border: 1px solid black; + font: inherit; + font-size: 12px; +} + +/* reboot and firstuse end */ + +/* firmware and package update start */ + +#firmware-update, +#package-update +{ + display: flex; + flex-direction: column; +} +#firmware-update hr + div, +#package-update hr + div +{ + display: flex; + flex-direction: column; + flex: 1; +} +#firmware-upload, +#package-upload +{ + padding: 6px 10px 0 0; +} +#firmware-upload progress, +#package-upload progress +{ + width: 100%; +} +#dialog-messages-error +{ + margin: 10px 0; + padding: 10px; + border-radius: 8px; + color: var(--firmware-status-fg-color); + background-color: var(--firmware-status-bg-color-negative); +} +#package-info +{ + font-size: 14px; + padding: 10px; + border-radius: 10px; + margin-bottom: 15px; +} +#dialog-messages-success +{ + margin: 10px 0; + padding: 10px; + border-radius: 8px; + color: var(--firmware-status-fg-color); + background-color: var(--firmware-status-bg-color-positive); +} +#dialog-messages-error:empty, +#dialog-messages-success:empty, +#package-info:empty +{ + display: none; +} +#download-firmware, +#download-package +{ + width: 248px; +} +#sideload-firmware +{ + width: 278px; +} +#firmware-refresh, +#package-refresh +{ + width: 24px; + height: 24px; + margin: 2px 0 0 4px; + border: 1px solid var(--ctrl-modal-textbox-border-color); + border-radius: 4px; + background-color: var(--ctrl-modal-bg-color); +} +#firmware-refresh:hover, +#package-refresh:hover +{ + background-color: var(--ctrl-modal-bg-secondary-color); +} +#ctrl-modal #firmware-refresh button, +#ctrl-modal #package-refresh button +{ + position: relative; + top: -1px; + left: 3px; + min-width: 16px; + height: 16px; + border: 0; + background-color: transparent; + background-size: 16px; + background-position: center center; + background-repeat: no-repeat; + filter: var(--icon-filter); +} +#firmware-refresh button.rotate, +#package-refresh button.rotate +{ + animation-name: logo-rotate; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} +#remove-package +{ + width: 277px; +} + +/* firmware and package update end */ + +/* tunnel start */ + +.tunnel +{ + padding: 10px 0; +} +.tunnel:not(:last-child) +{ + border-bottom: 1px solid #d0d0d0; +} +.tunnel input[type="text"] +{ + border-color: transparent !important; + direction: ltr !important; + text-align: left !important; +} +.tunnel input[type="text"][name="name"] +{ + border-color: inherit !important; +} +.tunnel > .cols > div:first-child +{ + flex: 0; + font-size: 12px; + margin: 0 10px 0 0; + padding: 0 4px; + border-radius: 6px;; + writing-mode: vertical-rl; + rotate: 180deg; + text-align: center; + color: var(--tunnel-fg-color); + cursor: default; +} +.tunnel > .cols > div:first-child[data-type=ws] +{ + background-color: var(--tunnel-bg-wireguard-server-color); +} +.tunnel > .cols > div:first-child[data-type=wc] +{ + background-color: var(--tunnel-bg-wireguard-client-color); +} +.tunnel > .cols > div:first-child[data-type=ls] +{ + background-color: var(--tunnel-bg-legacy-server-color); +} +.tunnel > .cols > div:first-child[data-type=lc] +{ + background-color: var(--tunnel-bg-legacy-client-color); +} +.tunnel > .cols > div:nth-child(3) +{ + flex: 0; + padding: 10px 10px 0 10px; +} +.tunnel .cols .cols.pwnw input:first-child +{ + width: 50%; +} +.tunnel .cols .cols.pwnw input:nth-child(2) +{ + width: calc(50% - 50px); + margin-left: 3px; +} +.tunnel .switch input[type=checkbox] +{ + top: -1px; + height: 30px; + width: 53px; +} +.tunnel .switch input[type=checkbox]::before +{ + height: 1.4em; + width: 1.4em; + left: calc(1.7em/2 + .3em); +} +.tunnel .switch input[type=checkbox]:checked::before +{ + left: calc(100% - (1.7em/2 + .3em)); + background-color: var(--service-bg-color-status-inactive); +} +.tunnel .switch.up input[type=checkbox]:checked::before +{ + background-color: var(--service-bg-color-status-active); +} +.tunnel input[name=name] +{ + flex: 1; +} +.tunnel input[name=password] +{ + flex: 1; +} +.tunnel input[name=network] +{ + width: 115px; + margin-right: 3px; +} +.tunnel input[name=weight] +{ + width: 53px; + margin-right: 3px; +} +.tunnel input[name=notes] +{ + flex: 1; + margin-right: 3px; +} +.tunnel button.clipboard +{ + margin-right: 3px; +} +.tunnel .icon.clipboard +{ + width: 16px; + height: 16px; + pointer-events: none; + filter: var(--icon-filter); +} + +/* tunnel end */ + +/* ports start */ + +#ctrl-modal .ports +{ + color: var(--ctrl-modal-fg-color); + background-color: var(--ctrl-modal-bg-secondary-color); + border-radius: 10px; + padding: 20px; +} +#ctrl-modal .ports table +{ + width: 100%; + border-collapse: collapse; +} +#ctrl-modal .ports table td:not(:first-child) +{ + text-align: center; +} +#ctrl-modal .ports table thead td div +{ + width: 54px; + padding: 5px 0; + margin: 0 auto; + border-radius: 14px; + color: var(--service-fg-color-status); + background-color: var(--service-bg-color-status-inactive); +} +#ctrl-modal .ports table thead td.active div +{ + background-color: var(--service-bg-color-status-active); +} +#ctrl-modal .ports table thead td:first-child, +#ctrl-modal .ports table tbody td:first-child +{ + width: 160px; + padding: 10px 25px; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; +} +#ctrl-modal .ports table tbody td:last-child +{ + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} +#ctrl-modal .ports table tbody td:first-child div:nth-child(2) +{ + font-size: 12px; + color: var(--section-subtitle-fg-color); +} +#ctrl-modal .ports table tbody td:first-child input +{ + font: inherit; +} +#ctrl-modal .ports table tbody tr:nth-child(even) +{ + background-color: var(--ctrl-modal-bg-tertiary-color); +} +#ctrl-modal .ports table input[type=checkbox] +{ + width: 14px; + height: 14px; + accent-color: var(--ctrl-modal-checkbox-color); +} + +/* ports end */ + +/* xlink start */ + +#ctrl-modal .xlink-label div:first-child div:first-child, +#ctrl-modal .xlink-label div:first-child div:nth-child(6), +#ctrl-modal .xlink input:first-child, +#ctrl-modal .xlink select:first-child, +#ctrl-modal .xlink select[name=port] +{ + width: 55px; +} +#ctrl-modal .xlink-label div:first-child div:nth-child(2), +#ctrl-modal .xlink-label div:first-child div:nth-child(3), +#ctrl-modal .xlink input:nth-child(2), +#ctrl-modal .xlink input:nth-child(3) +{ + width: 125px; +} +#ctrl-modal .xlink-label div:first-child div:nth-child(4), +#ctrl-modal .xlink-label div:first-child div:nth-child(5), +#ctrl-modal .xlink input:nth-child(5), +#ctrl-modal .xlink select[name=cidr] +{ + width: 40px; +} +#ctrl-modal .xlink input, +#ctrl-modal .xlink select +{ + border-color: transparent; +} +.xlink-label +{ + padding-top: 6px; +} +#xlink-list:not(:has(.xlink)) .xlink-label +{ + display: none; +} + +/* xlink end */ + +/* tools start */ + +.tool-console +{ + position: absolute; + top: 0; + right: 60px; + bottom: 0; + left: 0; + font-size: 12px; + padding: 10px; + color: var(--console-fg-color); + background-color: var(--console-bg-color); + border: 1px solid var(--console-border-color); + border-radius: 10px; +} +.tool-console pre +{ + height: 100%; + margin: 0; + overflow: hidden scroll; +} +.tool-console pre a +{ + color: inherit; + text-decoration: none; +} +.tool-console pre a:hover +{ + text-decoration: underline; +} +.simple-tool +{ + display: flex; + flex-direction: column; +} +.simple-tool select +{ + width: 30px; + height: 28px; + margin: 2px 0 0 2px; +} +.simple-tool .cols:first-child > div:nth-child(2), +.simple-tool.client-server .cols:nth-child(2) > div:nth-child(2) +{ + display: flex; + height: 30px; + flex: 0; +} +.simple-tool .cols:last-child +{ + position: relative; + flex: 1; + align-items: flex-end; +} +.simple-tool .cols:last-child div:nth-child(2) +{ + text-align: right; +} +.simple-tool button +{ + width: 40px; + height: 30px; + margin-left: 10px; +} + +.simple-tool #target-swap +{ + position: relative; + top: 20px; + width: 32px; + height: 32px; +} +.simple-tool #target-swap > div +{ + width: 20px; + height: 20px; + filter: var(--icon-filter); +} + +#wifi-scan table +{ + width: 100%; + font-size: 12px; +} +#wifi-scan table thead tr +{ + font-weight: bold; +} +#wifi-scan table thead tr td +{ + padding-bottom: 4px; +} +#wifi-scan table tbody tr +{ + vertical-align: top; +} +#wifi-scan + div button +{ + text-align: left; +} + +#wifi-char > .cols +{ + padding-bottom: 10px; +} +#wifi-bar +{ + height: 300px; + width: 120px; + padding: 10px 0; + color: var(--ctrl-modal-fg-title-color); + background-color: var(--ctrl-modal-bg-secondary-color); + color: var(--ctrl-modal-fg-title-color); + border-radius: 10px; + font-size: 14px; +} +#wifi-bar > div:first-child +{ + text-align: center; + font-size: 18px; +} +#wifi-bar .bars +{ + position: relative; + height: calc(100% - 42px); +} +#wifi-bar .bars > div +{ + position: absolute; + width: 100%; + bottom: 0; + left: 10px; + transition: height 1s linear; + +} +#wifi-bar .bars div div +{ + display: inline-block; + width: 24px; + vertical-align: top; + padding-right: 5px; +} +#wifi-bar .bars div div:nth-child(2) +{ + border-radius: 10px; + width: 50px; + height: 100%; +} +#wifi-chart .cols .cols:nth-child(2) .o, +#wifi-chart .cols .cols:nth-child(3) .o +{ + text-align: right; + padding-right: 10px; +} +#wifi-chart .chart +{ + padding-left: 10px; +} +#wifi-chart .chart > div +{ + height: 300px; + padding: 10px; + background-color: var(--ctrl-modal-bg-secondary-color); + border-radius: 10px; +} +#wifi-chart svg +{ + width: 100%; + height: 100%; +} +#wifi-chart .frame +{ + fill: none; + stroke: var(--title-fg-color); + stroke-width: 0.5px; +} +#wifi-chart .signal +{ + fill: none; + stroke: var(--conn-fg-color-good); + stroke-width: 1px; +} +#wifi-chart text +{ + fill: var(--title-fg-color); + font-size: 4px; + text-anchor: end; +} +#wifi-chart input[type=range]::-webkit-slider-runnable-track +{ + background-color: var(--ctrl-modal-bg-secondary-color); + border-radius: 5px; +} + +/* tools end */ diff --git a/files/app/resource/css/themes/dark.css b/files/app/resource/css/themes/dark.css new file mode 100755 index 00000000..0c9c625b --- /dev/null +++ b/files/app/resource/css/themes/dark.css @@ -0,0 +1,70 @@ +:root +{ + --font-family: sans-serif; + --default-fg-color: #d8d8d8; + --body-bg-color: #282828; + --logo-fg-color: hsl(0, 0%, 60%); + --nav-bg-color: #181818; + --hr-color: #808080; + --title-fg-color: #e0e0e0; + --subtitle-fg-color: #808080; + --section-title-fg-color: #808080; + --section-subtitle-fg-color: #909090; + --section-link-fg-color: #0078a8; + --ctrl-bg-color-hover: #202020; + --ctrl-modal-backdrop-color: rgba(43, 32, 32, 0.7); + --ctrl-modal-fg-color: #f0f0f0; + --ctrl-modal-bg-color: #101010; + --ctrl-modal-border-color: #404040; + --ctrl-modal-border-color-error: hsl(0, 100%, 37%); + --ctrl-modal-fg-title-color: #f0f0f0; + --ctrl-modal-fg-subtitle-color: #808080; + --ctrl-modal-fg-option-color: #f0f0f0; + --ctrl-modal-fg-message-color: #808080; + --ctrl-modal-fg-help-color: #808080; + --ctrl-modal-textbox-bg-color: #282828; + --ctrl-modal-textbox-border-color: #a0a0a0; + --ctrl-modal-advance-options-color: #a0a0a0; + --ctrl-modal-checkbox-color: #1010c0; + --ctrl-modal-bg-secondary-color: #383838; + --ctrl-modal-bg-tertiary-color: #2c2c2c; + --firmware-status-fg-color: #f0f0f0; + --firmware-status-bg-color-positive: #005500; + --firmware-status-bg-color-negative: #900000; + --firmware-status-bg-color-other: #494cd6; + --conn-fg-color-excellent: #107410; + --conn-fg-color-good: #005500; + --conn-fg-color-okay: #005500; + --conn-fg-color-poor: #005500; + --conn-fg-color-bad: #900000; + --conn-fg-color-idle: #c0c0c0; + --service-fg-color-status: #f0f0f0; + --service-bg-color-status-active: #356535; + --service-bg-color-status-inactive: #808080; + --service-bg-color-status-disabled: #900000; + --icon-filter: invert(86%) sepia(86%) saturate(38%) hue-rotate(322deg) brightness(116%) contrast(69%); + --menu-fg-select-color: #1278a5; + --nav-icon-filter: invert(75%) sepia(0%) saturate(1338%) hue-rotate(137deg) brightness(86%) contrast(87%); + --nav-icon-filter-select: invert(30%) sepia(27%) saturate(4907%) hue-rotate(173deg) brightness(95%) contrast(102%); + --map-filter: grayscale(100%) invert(100%); + --meshpage-node-bg-color-hover: rgba(0,0,0,0.15); + --meshpage-etx-fg-color: #808080; + --meshpage-hostname-fg-color: #1278a5; + --meshpage-service-hover-fg-color: #1278a5; + --meshpage-block1-border-color: green; + --meshpage-block2-border-color: rgb(0, 122, 0); + --meshpage-block3-border-color: rgb(161, 115, 0); + --meshpage-block5-border-color: orange; + --meshpage-block10-border-color: darkred; + --meshpage-block1000-border-color: red; + --tunnel-fg-color: #c0c0c0; + --tunnel-bg-wireguard-server-color: #ac0b0b; + --tunnel-bg-wireguard-client-color: #88181a; + --tunnel-bg-legacy-server-color: #101080; + --tunnel-bg-legacy-client-color: #1010c0; + --message-bg-color: #ffff85; + --message-fg-color: #000000; + --console-bg-color: #000000; + --console-fg-color: #ffffff; + --console-border-color: #606060; +} diff --git a/files/app/resource/css/themes/light.css b/files/app/resource/css/themes/light.css new file mode 100755 index 00000000..000298f2 --- /dev/null +++ b/files/app/resource/css/themes/light.css @@ -0,0 +1,70 @@ +:root +{ + --font-family: sans-serif; + --default-fg-color: #000000; + --body-bg-color: #f8f8f8; + --logo-fg-color: hsl(0, 0%, 38%); + --nav-bg-color: #e8e8e8; + --hr-color: #e0e0e0; + --title-fg-color: #202020; + --subtitle-fg-color: #a0a0a0; + --section-title-fg-color: #808080; + --section-subtitle-fg-color: #909090; + --section-link-fg-color: #0078a8; + --ctrl-bg-color-hover: #f0f0f0; + --ctrl-modal-backdrop-color: rgba(0,0,0,0.7); + --ctrl-modal-fg-color: #404040; + --ctrl-modal-bg-color: #f0f0f0; + --ctrl-modal-border-color: #202020; + --ctrl-modal-border-color-error: hsl(0, 100%, 37%); + --ctrl-modal-fg-title-color: #404040; + --ctrl-modal-fg-subtitle-color: #c0c0c0; + --ctrl-modal-fg-option-color: #404040; + --ctrl-modal-fg-message-color: #808080; + --ctrl-modal-fg-help-color: #808080; + --ctrl-modal-textbox-bg-color: #ffffff; + --ctrl-modal-textbox-border-color: #404040; + --ctrl-modal-advance-options-color: #a0a0a0; + --ctrl-modal-checkbox-color: #1010c0; + --ctrl-modal-bg-secondary-color: #e0e0e0; + --ctrl-modal-bg-tertiary-color: #d0d0d0; + --firmware-status-fg-color: #f0f0f0; + --firmware-status-bg-color-positive: #005500; + --firmware-status-bg-color-negative: #900000; + --firmware-status-bg-color-other: #494cd6; + --conn-fg-color-excellent: #005500; + --conn-fg-color-good: #003e00; + --conn-fg-color-okay: #000280; + --conn-fg-color-poor: #c03300; + --conn-fg-color-bad: #900000; + --conn-fg-color-idle: #a0a0a0; + --service-fg-color-status: #f0f0f0; + --service-bg-color-status-active: #005500; + --service-bg-color-status-inactive: #808080; + --service-bg-color-status-disabled: #900000; + --icon-filter: none; + --menu-fg-select-color: #1278a5; + --nav-icon-filter: invert(75%) sepia(0%) saturate(1338%) hue-rotate(137deg) brightness(86%) contrast(87%); + --nav-icon-filter-select: invert(30%) sepia(27%) saturate(4907%) hue-rotate(173deg) brightness(95%) contrast(102%); + --map-filter: none; + --meshpage-node-bg-color-hover: rgba(0,0,0,0.05); + --meshpage-etx-fg-color: #808080; + --meshpage-hostname-fg-color: #1278a5; + --meshpage-service-hover-fg-color: #1278a5; + --meshpage-block1-border-color: green; + --meshpage-block2-border-color: rgb(0, 122, 0); + --meshpage-block3-border-color: rgb(161, 115, 0); + --meshpage-block5-border-color: orange; + --meshpage-block10-border-color: darkred; + --meshpage-block1000-border-color: red; + --tunnel-fg-color: #f0f0f0; + --tunnel-bg-wireguard-server-color: #ac0b0b; + --tunnel-bg-wireguard-client-color: #88181a; + --tunnel-bg-legacy-server-color: #101080; + --tunnel-bg-legacy-client-color: #1010c0; + --message-bg-color: #ffff85; + --message-fg-color: #000000; + --console-bg-color: #000000; + --console-fg-color: #ffffff; + --console-border-color: #000000; +} diff --git a/files/app/resource/css/user.css b/files/app/resource/css/user.css new file mode 100755 index 00000000..c24efaa4 --- /dev/null +++ b/files/app/resource/css/user.css @@ -0,0 +1,1076 @@ +* +{ + box-sizing: border-box; +} +html +{ + width: 100%; + height: 100%; +} +body +{ + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + color: var(--default-fg-color); + background-color: var(--body-bg-color); + font-family: var(--font-family); + user-select: none; +} +hr +{ + border: 0; + border-top: 1px solid var(--hr-color); + margin: 16px 0; +} + +/* -- start common layout */ + +.section-title +{ + color: var(--section-title-fg-color); + font-size: 16px; + font-variant: small-caps; + padding: 20px 0 10px 0; +} +hr + .section-title, +.section-title:first-child +{ + padding-top: 0px; +} +.section .t +{ + color: var(--title-fg-color); + font-size: 16px; +} +.section .s +{ + color: var(--subtitle-fg-color); + font-size: 12px; + padding-bottom: 4px; +} +.section .ts +{ + font-size: 12px; +} +.section a +{ + font: inherit; + color: inherit; + text-decoration: none; + font-variant: proportional-nums; +} +.section a:hover +{ + color: var(--section-link-fg-color); +} +.section-subtitle +{ + color: var(--section-subtitle-fg-color); + font-size: 12px; +} + +.heading +{ + color: var(--section-title-fg-color); + font-size: 14px; + padding-bottom: 6px; +} + +.cols +{ + display: flex; + flex-wrap: wrap; +} +.cols > div +{ + flex: 1 +} + +.ctrl +{ + padding: 5px; + border-radius: 10px; + border: 1px solid transparent; +} +.ctrl:last-child, +.noctrl:last-child +{ + flex: 1; +} +.noctrl +{ + padding: 5px; + border: 1px solid transparent; +} +body.authenticated .ctrl:hover +{ + padding: 10px; + margin: -5px; + background-color: var(--ctrl-bg-color-hover); +} + +/* -- end common layout */ + +/* -- start overall layout -- */ + +#all +{ + display: flex; + min-width: 100%; + height: 100%; + flex-direction: column; + background-color: var(--body-bg-color); +} +#nav +{ + position: absolute; + display: flex; + width: 100%; + min-height: 64px; + max-height: 64px; + background-color: var(--nav-bg-color); +} +#panel +{ + display: flex; + flex: 1; + height: 100%; + padding-top: 64px; + background-color: var(--nav-bg-color); +} +#select +{ + display: flex; + min-width: 64px; + flex-direction: column; + background-color: var(--nav-bg-color); +} +#main +{ + flex: 1; + height: 100%; + padding: 10px; + background-color: var(--body-bg-color); + overflow-y: scroll; + overscroll-behavior: contain; + border-top-left-radius: 20px; +} +#main-container +{ + display: flex; + flex: 1; +} +#c1 +{ + display: flex; + flex-direction: column; + min-width: 330px; + width: 330px; +} +#general +{ + display: flex; + flex-direction: column; + padding: 10px 5px; +} +#location +{ +} +#c2 +{ + display: flex; + flex-direction: column; + flex: 5; + padding: 20px 15px 0 15px; + min-width: 420px; +} +#services +{ + display: flex; + flex-direction: column; +} +#local-and-neighbor-devices +{ + container-type: inline-size; +} + +#c3 +{ + display: flex; + flex-direction: column; + flex: 4; + padding: 20px 0 0 5px; + container-type: inline-size; +} +#radio-and-antenna +{ + display: flex; + flex-direction: column; +} +#mesh-summary +{ +} +#tunnels +{ + display: flex; + flex-direction: column; +} +#dhcp +{ + display: flex; + flex-direction: column; +} +@container (width < 400px) +{ + #dhcp .t + { + font-size: 12px; + } + #dhcp .ts + { + font-size: 10px; + } +} + +/* -- end overall layout -- */ + +/* -- start nav layout -- */ + +.nav-node-name +{ + color: var(--title-fg-color); + font-size: 24px; + font-weight: bold; + margin: auto 0 auto 20px; +} +#nav-status +{ + color: var(--subtitle-fg-color); + font-size: 12px; + padding: 8px 0 0 8px; + margin: auto 0; +} + +/* -- end nav layout -- */ + +/* -- start general layout -- */ + +#general .radio-image +{ + display: flex; + align-items: center; + justify-content: center; + height: 150px; + margin-bottom: 20px; +} +#general .t +{ + font-size: 16px; + color: var(--title-fg-color); +} +#general .s +{ + color: var(--subtitle-fg-color); + font-size: 12px; + padding-bottom: 6px; +} +#general .ts +{ + font-size: 12px; +} + +.node-description +{ + padding-bottom: 4px; +} +.node-description .t +{ + font-size: 16px; +} + +#general .firmware +{ + font-size: 12px; +} +#general .firmware .s +{ + padding-top: 2px; +} +#general .firmware .s a +{ + color: inherit; + text-decoration: none; +} +#general .firmware .s a:hover +{ + text-decoration: underline; +} +#general .firmware-status +{ + display: inline-block; + margin-left: 4px; + font-size: 10px; + padding: 2px 5px; + border-radius: 5px; + color: var(--firmware-status-fg-color); +} +#general .firmware-status.uptodate +{ + background-color: var(--firmware-status-bg-color-positive); +} +#general .firmware-status.uptodate::after +{ + content: "Up to date"; +} +#general .firmware-status.needupdate +{ + background-color: var(--firmware-status-bg-color-negative); +} +#general .firmware-status.needupdate::after +{ + content: "Update available"; +} +#general .firmware-status.custom +{ + background-color: var(--firmware-status-bg-color-other); +} +#general .firmware-status.custom::after +{ + content: "Custom"; +} + +#health +{ + padding: 20px 0; + font-size: 14px; +} + +/* -- end general layout -- */ + +/* -- start location layout -- */ + +#location .t +{ + color: var(--title-fg-color); + font-size: 16px; +} +#location .s +{ + color: var(--subtitle-fg-color); + font-size: 12px; +} +#location .location-image +{ + pointer-events: none; + width: 300px; + height: 150px; + margin: 0 0 15px 10px; +} +#location .location-image iframe +{ + border: 1px solid var(--hr-color); + filter: var(--map-filter); +} + +/* -- end location layout -- */ + +/* -- start local and neighborhood devices layout -- */ + +@container (width < 450px) +{ + #local-and-neighbor-devices .section + { + font-size: 12px; + } + #local-and-neighbor-devices .ts + { + font-size: 10px; + } +} +#local-and-neighbor-devices .stats +{ + text-align: right; + min-width: 260px; +} + +#local-and-neighbor-devices .status +{ + padding: 2.5px 5px; + margin: -5px -5px; +} +body.authenticated #local-and-neighbor-devices .status.ctrl:hover +{ + padding: 7.5px 10px; + margin: -10px -10px; +} +#local-and-neighbor-devices .excellent +{ + color: var(--conn-fg-color-excellent); +} +#local-and-neighbor-devices .good +{ + color: var(--conn-fg-color-good); +} +#local-and-neighbor-devices .okay +{ + color: var(--conn-fg-color-okay); +} +#local-and-neighbor-devices .poor +{ + color: var(--conn-fg-color-poor); +} +#local-and-neighbor-devices .bad +{ + color: var(--conn-fg-color-bad); +} +#local-and-neighbor-devices .blocked +{ + color: var(--conn-fg-color-idle); + text-decoration: line-through; +} +#local-and-neighbor-devices .idle +{ + color: var(--conn-fg-color-idle); +} +#local-and-neighbor-devices .icon +{ + display: inline-block; + width: 12px; + height: 12px; + margin-left: 4px; + background-position: center center; + background-repeat: no-repeat; + filter: var(--icon-filter); +} + +/* -- end local and neighborhood devices layout -- */ + +/* -- start services layout -- */ + +#services .service, +#services .device +{ + font-size: 13px; + padding: 2px 0; + flex-basis: 50%; +} +#services .service .status +{ + display: inline-block; + width: 52px; + padding: 2px 0; + border-radius: 5px; + margin-right: 5px; + text-align: center; + font-size: 10px; + color: var(--service-fg-color-status); + background-color: var(--service-bg-color-status-inactive); +} +#services .service .icon +{ + position: relative; + display: inline-block; + top: 3px; + width: 14px; + height: 14px; + filter: var(--icon-filter); +} +#services .service .status.inactive +{ + background-color: var(--service-bg-color-status-inactive); +} +#services .service .status.active +{ + background-color: var(--service-bg-color-status-active); +} +#services .service .status.disabled +{ + background-color: var(--service-bg-color-status-disabled); +} +#services .service .status.inactive::after +{ + content: "inactive"; +} +#services .service .status.active::after +{ + content: "active"; +} +#services .service .status.disabled::after +{ + content: "disabled"; +} + +/* -- end services layout -- */ + +/* -- start icon layout -- */ + +#nav a +{ + position: relative; + text-decoration: none; +} +#icon-logo +{ + display: table-cell; + width: 64px; + height: 64px; + background: url("data:image/svg+xml,") center center no-repeat; + text-align: center; + vertical-align: bottom; +} +#icon-logo + div +{ + position: absolute; + display: inline-block; + bottom: 0; + left: 8px; + font-family: inherit; + font-size: 13px; + font-weight: bold; + color: var(--logo-fg-color); + background-color: var(--nav-bg-color); +} +#icon-logo.animate +{ + animation-name: logo-rotate; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} +@keyframes logo-rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +#select .icon, +#select .icon:hover:active, +#login-icon .icon, +#logout-icon .icon +{ + width: 32px; + height: 32px; + margin: 16px; + filter: var(--nav-icon-filter); + background-position: center center; + background-repeat: no-repeat; +} +#select .icon:hover, +#select .icon.true, +#login-icon:hover .icon, +#logout-icon:hover .icon +{ + filter: var(--nav-icon-filter-select); +} +#nav:has(#changes:not(:empty)) #logout-icon .menu div:last-child +{ + display: none; +} + +/* -- end icon layout -- */ + +/* login start */ + +#login-icon, +#logout-icon +{ + position: relative; +} +#login-icon .icon, +#logout-icon .icon +{ + position: absolute; + top: 0; + pointer-events: none; +} +#login-icon label, +#logout-icon label +{ + position: absolute; + top: 0; + right: 0; +} +#login-icon input, +#logout-icon input +{ + width: 64px; + height: 64px; + opacity: 0; +} +#login-icon .menu, +#logout-icon .menu +{ + display: none; + position: absolute; + top: 64px; + right: 0; + padding: 0 0 5px 5px; + text-align: right; + font-size: 14px; + background-color: var(--nav-bg-color); + border-bottom-left-radius: 10px; + border-top: 1px solid var(--ctrl-modal-bg-tertiary-color); + z-index: 1; +} +#login-icon .menu > div, +#logout-icon .menu > div +{ + padding: 12px 20px; + color: var(--title-fg-color); +} +#login-icon:has(input:checked) .menu, +#logout-icon:has(input:checked) .menu +{ + display: block; +} +#login-icon .menu > div:hover, +#logout-icon .menu > div:hover +{ + color: var(--menu-fg-select-color); +} +#login-icon .menu a, +#logout-icon .menu a +{ + color: inherit; +} + +#login +{ + font: inherit; + font-size: 16px; + padding: 50px; + color: var(--ctrl-modal-fg-color); + border-radius: 20px; + background-color: var(--ctrl-modal-bg-tertiary-color); +} +#login input { + color: inherit; + background-color: var(--ctrl-modal-textbox-bg-color); + margin: 0 10px; + outline: none; +} +#login button { + font: inherit; + font-size: 12px; + color: inherit; + background-color: var(--ctrl-modal-textbox-bg-color); +} +#login::backdrop +{ + background-color: var(--ctrl-modal-backdrop-color); +} + +/* login end */ + +/* messages start */ + +#messages +{ + position: relative; + background-color: var(--message-bg-color); + padding: 10px; + border-radius: 10px; + z-index: 1; +} +#messages .section-title +{ + color: var(--message-fg-color); +} +#messages .section > div +{ + color: var(--message-fg-color); + padding: 0 0 4px 2px; +} + +/* messages end */ + +/* tools start */ + +#tools +{ + display: none; +} + +/* tools end */ + +/* meshpage start */ + +#meshfilter +{ + padding-top: 30px; + display: flex; +} +#meshfilter input +{ + width: 300px; + outline: none; + font: inherit; + padding: 5px; + color: var(--ctrl-modal-fg-color); + background-color: var(--ctrl-modal-textbox-bg-color); + border: 1px solid var(--ctrl-modal-textbox-border-color); +} +#meshfilter button +{ + padding: 5px 10px; + border: 1px solid var(--ctrl-modal-textbox-border-color); + background-color: transparent; + font: inherit; + color: var(--ctrl-modal-textbox-border-color); +} +#meshfilter button:hover +{ + background-color: var(--meshpage-node-bg-color-hover); +} +#meshpage +{ + padding: 40px 60px; + font-size: 14px; +} +#meshpage .block +{ + position: relative; + display: flex; + flex-flow: wrap; + padding: 40px 20px 30px 20px; + border-radius: 10px; + margin-bottom: 10px; + border-width: 2px; + border-style: solid; +} +#meshpage .block:empty +{ + display: none; +} +#meshpage .block .label +{ + position: absolute; + top: -1px; + left: -1px; + padding: 4px 10px; + font-size: 14px; + text-transform: uppercase; + color: var(--body-bg-color); + border-top-left-radius: 10px; + border-bottom-right-radius: 10px; +} +#meshpage .node +{ + width: 50%; + padding: 2px 20px; + border-radius: 10px; +} +#meshpage .node:hover +{ + background-color: var(--meshpage-node-bg-color-hover); +} +#meshpage .node .etx +{ + font-size: 10px; + color: var(--meshpage-etx-fg-color); + padding-left: 6px; +} +#meshpage .lanhosts +{ + font-size: 13px; + padding: 4px 0 6px 0; +} +#meshpage .host, +#meshpage .lanhost +{ + display: flex; +} +#meshpage .host .name, +#meshpage .lanhost .name +{ + flex: 1; +} +#meshpage .host .name a +{ + text-decoration: none; + color: var(--meshpage-hostname-fg-color); +} +#meshpage .host .name a:hover +{ + text-decoration: underline; +} +#meshpage .services +{ + flex: 1; + font-size: 13px; +} +#meshpage .service +{ + white-space: nowrap; +} +#meshpage .service a, +#meshpage .service span +{ + text-decoration: none; + color: inherit; + white-space: normal; +} +#meshpage .service a:hover +{ + text-decoration: underline; + color: var(--meshpage-service-hover-fg-color); +} +#meshpage .service div +{ + position: relative; + display: inline-block; + top: 2.5px; + width: 14px; + height: 14px; + margin: 0; + background: url('data:image/svg+xml;utf8,') center center no-repeat; + filter: var(--icon-filter); +} +.meshpage-help +{ + display: none; + padding: 40px 120px 0 120px; +} +.meshpage-help.visible +{ + display: block; + color: var(--ctrl-modal-fg-help-color); + font-size: 12px; +} +.block1 +{ + border-color: var(--meshpage-block1-border-color); +} +.block2 +{ + border-color: var(--meshpage-block2-border-color); +} +.block3 +{ + border-color: var(--meshpage-block3-border-color); +} +.block5 +{ + border-color: var(--meshpage-block5-border-color); +} +.block10 +{ + border-color: var(--meshpage-block10-border-color); +} +.block1000 +{ + border-color: var(--meshpage-block1000-border-color); +} +.block1 .label +{ + background-color: var(--meshpage-block1-border-color); +} +.block2 .label +{ + background-color: var(--meshpage-block2-border-color); +} +.block3 .label +{ + background-color: var(--meshpage-block3-border-color); +} +.block5 .label +{ + background-color: var(--meshpage-block5-border-color); +} +.block10 .label +{ + background-color: var(--meshpage-block10-border-color); +} +.block1000 .label +{ + background-color: var(--meshpage-block1000-border-color); +} + +#meshpage.filtering .service, +#meshpage.filtering .lanhost, +#meshpage.filtering .node, +#meshpage.filtering .block +{ + display: none; +} +#meshpage.filtering .service.valid, +#meshpage.filtering .node:has(.service.valid), +#meshpage.filtering .node:has(.lanhost.valid), +#meshpage.filtering .node:has(.host.valid) +{ + display: block; +} +#meshpage.filtering .lanhost:has(.service.valid), +#meshpage.filtering .lanhost.valid, +#meshpage.filtering .block:has(.service.valid), +#meshpage.filtering .block:has(.lanhost.valid), +#meshpage.filtering .block:has(.host.valid) +{ + display: flex; +} + +/* meshpage end + +/* start icons */ + +.icon.status +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.mesh +{ + background-image: url("data:image/svg+xml;utf8,") !important; +} +.icon.cloudmesh +{ + background-image: url("data:image/svg+xml;utf8,") !important; +} +.icon.tools +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.login +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.login.authenticated +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.map +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.camera +{ + background-image: url('data:image/svg+xml;utf8, ') !important; +} +.icon.phone +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.time +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.winlink, +.icon.mail +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.local +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.server +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.router, +.icon.switch +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.radio +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.video +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.chat +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.solar +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.battery +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.power +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.wiki +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.refresh +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.cloud +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.cloudup +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.clipboard +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.wifi +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.download +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.twoarrow +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.tool +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.signal +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.plane +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.bolt +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.updownarrow +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.globe +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.eye +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.warning +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.weather +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} +.icon.plus +{ + background-image: url('data:image/svg+xml;utf8,') !important; +} + +/* end icons */ diff --git a/files/app/resource/js/htmx.min.js.gz b/files/app/resource/js/htmx.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..396e9d4c644bd49cafa93b0ba4bd9bf7626e7143 GIT binary patch literal 15677 zcmV-DJ;K5tiwFor{U2rm188(@crI;eZZ2wb0JVK-d(&8!@LzG%JjSXhPD1ze%*Y7d z5JHhEAaoLHGie^hzK#Wo)Fq_gW0l}$gC^|B9h@M zn+@dcLX@-KRLrtMBuNtJ5<$PuiNo}A8m{>FV0;Gk8j;tcXDeTp(`8PT%k$4-BB3fL z4aLo(tYjUo{O?2#xRI3@&gJDz!hi1WFue(NyS2z?8_?>q;NGXfb4-fmJgXx!lWF4w&epf}%>one{KYMu|&!S0Q)}og2Rdki+OHs%Ef?`)yGOh6I?k-rAwG6_M z8ecW!I!#oG%koOZpQ3a+-Op1Ti%R3KB9i6BMXtUcMKb-Oe*YZtQXJe!QPj|H{Gn1+ zZPJS>%F7EJ@K5;TU0I0uGycNa70W!2CS@_pF5(p|q%5o3gCaf8#Wa@HQba0kKb_3Q zN%n_`cXyzNStaWEQSq8H-)1nDSqeLd<4KpfJh@I6C-OEIagY^-s9vAGeG|BuFGZf- z#yciSNSO;xe6kZ|#Ux)&#bGhcCMonlP1QW0OegXZ1b9@oOkx0o`yEzQy16PTzy%gl zH-{2}sK)NI;=;|+X^;!!U9@vm8nl_`<@EvVB@d52PbO8ikf-T|88fsbI2G1^0?Psw zB^dUgk8K{kZ zRZf=m36)V5E`+4R^49WQiRe6)#G-rMznj0-P+wq^2R# zZE2!KTGeniVYImG)96zyqe&%TF%GU^B~Hp^1v_j(fdNT{xb8Vc04%K9+ld`FVWZmn zS)8AglP^Mgtsz*E2{vZ(p{em%IFiG2v%ASZhJ3Gw?w)JtBb(?-R0JP_-QoWZcb^2! zh~d==F2yNN{dHPQbAS?WWOQCmZ}-bWrWx!3tlK0l_9h58-;|RyKY??TUWoVu0moAS z8V9++V-ep7NXlWYeqsoDTSfD<-qUA4F7W`EPbw5kJ{1gHTX}_>_coQ2c|5_CgPR4w zB%IJsus!kX3KL(8bPBVKZ|Li=SS+O)##E)iz=||~2`4;Gsbzv(u;bYb0Sd0qH5EN| z4nv$tN=hsEKfZ<22oPCcggA%82^mL({OUO8Y`)=##jK1^qq025e&PcW!4a))_02t4`QlZa+;gdMLycE+6js+a&qKfKyxy+~gDS(qaUP^sj>3|{D(%)58s>GQf z@L^Cx9Ut?iUnhKTBF0hhXNmt$VH^^n&;r*vIHR&zay`6E7lZZlO>iUF6q|tyH8>$? z!r|vKD+WQ-100AAog`uaa4nq%Ve~uxmUu1U<28QNR{&e_HiRSHQ%^8jb#BwKxPXlx z1W&1#!N-sF_@B?tzI%4}EF6sC>*L3hv+tgVPd|R#rV`JBXk$Y@4=%FH08rCq5H_au zDb57Q7$98?iD5c3DGe+M+`f0|yAhTh)_DdudFx3Cl*wfsge#d4e0Y_YsWkaJ5tOi$ zlBu?f)gG#~8;0NQiof1d^YZI8zx(?R(^h`Duu&7sYM9DEhR@9mV`}pm1fspTQDkNJ4)X+}&lvdXZ;x5WvkD0(ccSN3%f{K1+5&bM=-5z=(i% zTHlYx4yNGV)mU(#qY0#R;i%*dWC$qS(0~dixv}-ItL3>%>I#4i04YR`yQzL^*hWe9 z+m6KYkB-!9asDNf9T{aS&Ct{Rel-MCl+%Fh0QW4)+GM|KsDscSe2J2aNvszJib!g$Zeo5^FSYyVu)yW#79p)+kxA@wKd9;Y&gp* zpm8C6+Q1;;sNkLg_)73B51D#+sgAP7OapuA?NHy@z2AO{y=@PHA|(i6%H(zlH?X`~ zr#=0%NCYhv9#{tGD>!7sN&tlj!003j0M0@ILy>gRkNv({u`H z44!VA@2A^TAc%H&2q~fbXU-qr6(nO{`pa-l!d1<3_s)h6zl zME1aJ6@Ns#;Yio-DEw46tE-uB+rksof~>k4kbDqSr+jSItD4!(?{DL&tV}!Av-QUQ zsv1)(%ndT5=C(P_u5>f>6IVcSKeku_2iE}@nb5=s9G-!jbLb&FCMt)Es+1*)MF1W# z{7{e&Pe?Fkxl0c)AyHnYtuGFo$998PTDs$0_V?{V+^1V46&4_9{+kKuEkwbAf)8RW z&+y7^APl9iWYy?1SlQ;@+SbNY0X}DNgSnNRQT0>|b5UH#`Ka351Q7_Fht$FD1k5{=0Afw3A{CcQG+LBxljNQIImL5^3q_Vr`K?O z%j+SFKZtwp_Y1(;4wf*RjvXDtReM)XHC3fQ2O%(A5{dkB)>Cbs%v1P-B`k76u#e}J zU>!1PgH|)cIgs`q%Id7-p1qeI@dW4W63R0zvi-5%+;&lfT6447y$tq*Wr|4G9bFgi zt8yVKc?(bn4mqrQBH=%?fxnG$wCd%sK#em^hs3Cxp=g;SiWHn-ya2+MCcPEkG-xkT!5ePe%u zp8hdp(PQGuN|82&X~QwB%N!b0Kd`8#Mx6K}R;ZBNyG zJyBWPS7rJl>EoNMmI#E*$1t5vajFhG@h3Vfx;rDVXSxX8&ebIKHE}W+b_0&v!3tc= z;TTzXibur`^^v#!ZeuJ|_n6k7y~2n`TFXDdg@*+1g;_kqBR76V={&ueBib|Sr!NG>>t^ZR?NXtcR? zPt6}(tjUN1=Zlv&cp8NL{`dH6`&nRTBAw=U&4{+1_*cq>W53x;X=!7%d_5wLcXaDx zT>2jdx{tyCgkgAQ6;-x6iYwc7_x-y6<<~_Ds{l^Tu)|}Z_TgI{k(2BkwFaG?VX<~+ zKYq2dMJ2AXa#_0_>a=g@R2OY`{Hx#b|E_neMIp9hozg1EWQi@t->pK#R;;x|i0Evc zDp>=yKOh=x8a!h^CS7W_?rH!-G)Ynb0a!e(Gyzy30m%GjH75ncG}zdBMq%B|fI z(u|{GYwQ1Ju&<8ZygB-LZLn5eCOR-5U%c^-zfZq8l!TyMdsP>TvLyzBITYvW?<4AR zn{sKUE8!3XUj`QTtt89aonA?*EutL2xd1Xd258|lt1IAOR9R`uRxXy9l6)*G!qw-> zMP5wzTZSZQ~^p>Ne8 zc20d_lMCSrh!pe~1c0(p;_rysFNH6{VkFLZUWhn=TjA>1@chniws`RaO~7fG=B)sC?3VThscu$d*QZK|k)~CJ0}bq%qi`^% zynRvZNY9;-TCvueR(a%ez|!M@nbaUo%q|VGHjT&9m_DzSAQ@;Q>GahWj%`%FFdtAP zusA>moSWas$G0gsmDMyvd{qy0;I~t|S_n>u%Ru%O*q9yuiFd4DReu6MAR9+uH>(d> zns=QpNE6^Ko5!76=9bK1V03H=;jp?mpe#y_IOQ+FrVxWH3bLsZ1D9^ik=n2TdxN9r zD25EikAo0J#bu%?IH36a>~JJ)rk}OB8);%s%NY~Vd+|U>VV-Cso6{wqivn=}K8|&tM;}y;jM}hAL0^f+GJ~ zmgg@7>^$va01!6hokBiEfm#AvBEO5L2@$|ddo59}cUK-M> zD5io?w9C+Cnm4+?z-T5Mv}-ycCGWeEHPS(}QUdQdvqE3z67LINH)X&a1LQ4ekU?Fc zZ$P0{P*GqnObQn!eVXYDlj;joCfPVW(=}p}!eV-zlt3jo>_Kf$-d{9zS|_#37=*OR zIn>-eySw94PmL`T*%tW2)v2l7Ww?TSl}AgK8^A8jko`dQG}`Up{9FySNoxts`9?~! zII5V!0vk%@av+R^!T~hL1e>X6!9AmSf~c-MiyD*mK+gsasQZVu4ve>7+#XK#?bD+X z{?Q?Uk-9)p3l^U8QD)6Yhj1epXVdP12|F-ciGE+>6a+h3Hmapn!DBcyA3r`0jIIdo z*Tp*k(Y8oTMPj4(DqCAyY~o;&#{rND!Q(UTH|zJajU*|#%QO)Vgb7F21X#mS4ZYQ0 zY8Dwg1Qz9|cp<$-ITEfo&zE9Y9Ba(Tfagc)+UOnq|2v^`=lYYB%oUvc?n=(b=~9+6 zG>AIWB6H^UpNn{1X60>jHKcl;rXb}@jkM$`4uR-K78X=2xWJ{UTU!O#%~2+f{s7d| zIZwnAa8@NlO(?}9%7z6rik3wiisv{z_~rD|yQ7x}usv&;`$unyAHjTBRCjM+<+IQe z^S$Mi}7|5qj|0~>*o3Q)ywAzPG_T&Ibo4cJ3)xaBP{dZU*V`+?aU4hN> zh$elGZ8nfkOyx4o0Rw}<7fgsqM0NB@nM4D23~kRDnxPBjuq>1j*4psgJa2d?Je8!A zumx~{qLXCTN(W1vC07`^)&@m_Ij$`NHCF(!hoUakKvNNkl<=P+ zTLX5OkM#gb$VP*#qTff;6K1vw14y)F*(@~bLmtT^!p;hi}SG#0;?U(!IzV_S&ZPj^c>bUwTtk%{RSoK(o z0CZ3873U3LrVO`k&1MEVC9LB1eR^Ms9%=If8R>krhGw zrLzQs2UX<#14Ur?LHzdqUMT;U&U|Iu8NY1jYFh=Ss(c=~1k6)DO*OhHH8!l1bX+oz z%=CH<{68WNW|Gy~;ST1mpGmG7^4&TtqFX|D%cr{3XsNrKCHZ(s7@uO@^qv+TbL&w%EBav9d>kx<5sL@pz#9EJwyswzLPmD$|IQb1fb z+V(~ZkBX?;Dlpp!qQ19h+3jQlP1p1$I~m#_5=qy!A(I+fmxLBFby-XrUdWFh10LJg z$7eZ3)4i$yoFFx^2K@ig#>i_cxD$pj&J8V-Q@=@Wsy|3h%)cw(mN9AQ2m#4|L#sUG zc;}3)xB?=4=h;kK92Lo~BGR;gyl2TMVvWO<$q1BLB-#2Z1JV}jS0>mOU3={C49zyt zycDh{-$?kCCf2kx+vFKGc39bF0{m?l7|35IrDLSriZ-9B3+~X1@3pu`5a56bvY8 z3+YOW#DG7IH6Bw1?eHKoh%^Tqml`zvEo>a9w6yann+FHBYz>T$QReJ^2D^?+2iVP9 z@)<0-xw$|);hF*vD=vuWS`O9kgs!5VC6z*xWhz2u&FlFnasVQnTjAdwrKaeR81VS; zJI1{=BU%nAUv1ZogHovStST?>tqX!1m@2be#YM=>?l}xc$o^4m_FolTjMN`Qla2D!*oh_Tcwc9M8_~8L_D6Wj=klrqdq+deIHwW%)%Ev8jsU z4|U;|Or&R4)0~EXsv&h%^o3F*jd0Yulp(dzn7L?-{@EIG%MQ(fy4Z$tXDer^;iTai z)*MbpRf2L5=nVK_hOPxky(;Ih8N{F$9fimNP_>uVbr$~ zSTb2?@nh0$orqc}EC{P?TSmDPswhd5J&~w7iGBudVSW=MVu|&BMk{B>;>su<8>E)W zg~kl3X`;%*N{%$9b`5mV@)%ReT)SZDoiHCjL-L`j#0<^q+%Hd=22*e>&2ve1Bw}4I zxCf`3j1T*L${?tNSoiME{rrUf)IrFJrt+(!WNnw}L`X3A>3oZ5SK@T>a#0PzSPxP`nbTM5h>EBX) zAEO@M*T=AjSGWXx^*1!(-fCxU`{fXKf%f0?=2V-5#r{Iap(}Uo(1qV*MlzG0jSk;z zb&8PbTN|RkW;ED|tnwelEshM1OA0@oJ2jDQQG`P@DM_N^VNkID3|3H=|3Rk1Y3n}D zqfmTI<9iI;vmS6P(}=XOQ64IK5Edn6j3HTBj)VlNmVSe>>Pr*t@fWWa3MBYD(naiixFS0aqVa;Bl9(P*nkq&+-OA*X z+S++zT2?62GP}*N9$G`GT6J?(UPFT*S{$46w0Sn2OKD2e-P78nK4TfehFh+rGvE@b z-H6#Kz>Wj9+0b(FHID~7GDEwkLGBpap>pc#OyC@=OKqt%RkUdoTkR?n6Ju!MC&Q^~ zCAHWspNw) zQZkFR&H|2t5=2mURI9ic=nCXhN>`HF;z5)OEOyw9Sduq+j8{5^yY%=hGG;FwnJqh$ zg;c7|=*f=fD7lcz{&J&4RHs|i4IxeR3p-5tMuF=kBV|ITY;;rWZ;}<6e4W;F68}Oc zws8e+I~I^m5UE_=b37?jHw107Fu$>AMWKLH$QNglmo4P=5(Sl7Or4?O!c%QeJIb_9 zBu$EOrme2F3RmZ}_^w4Q!eBBiQJ}x} zfVhR9!8XzhJKB7Cjh5XUm?Vt+^&Hs1O)n)5l$gULqNnSm%+a%4%0-QQW0eHgbscY0 zNDs^Q+*ij`F*O=vGwVm$VR>*_5Gwaj9?$@_0u$Hu!5YS=5Iv~7zLh8-qDDxx(8inN z-JPAkN4q<_Pw>CLp1GoOI4~5`K_TgDi2%GCmgx=I+zrW9jC(8>qVPH_fCm_*xLwxL zB9Wu6^Lq(LHoa(_SQs!(sD*mP5HHi{c;H)Bu5huhTwFE_;(O_0 zi)-0oA>WRWHnRF?>4iMBoD9N4ZS(JjQ`5xO+f>enml-mYe>b`s@z?JteNkQyoK$F9-)e;Az{ zXmsKu6Et}aSNk^8*_Z=l!-ZZx4T!ziXs}-Qfnup8dbK#{=DsSpXQ$*-IU>Z~G9XJF zEy_NzURtRzfsZ351nN-PGB5=7TTnL0EogZvTc}a;=a{LLGU`JvWt1po7NA<;~rq3oF#(z82$jdwZIw>h=!!L>cZtlBPB(5qmx3ca{kI&qZpATo-IkQDIa zN>t~7a=N8?ZwQP)=l`Ze4{c-Ehr1!?BNHdb8VP`&hzd~MZH0OE<2S*D$wcXVyv`Wp8mUnWc_|l33fMoPHS)yE0r8V4!R=beNkN)tZ z#o02A0{Y^*cc^=Wi65PlgMx3bsc{F|P)i$9MQYq(L%&qyEv1`7yTJ*L)1B1l9AN(! zxPb_Z(qYLRBJJxeQ1{ZXdA*j>BS=FjJ%^@PiCiLY4V(SDX-BjEPIInIo&s!93JjG` zVyQM)oqTslj_J0!;eJ%7e*eB`QxpMD%QPwO*XQ&qRe^DX@Lw%D&OaTR^7MC zn`H&8>@8nOuYvHZp)*{IcD;e5I@3!SuJ%}oqr7-JBWvx+R=ku$>Bd5^8$cFnvK1=k zA=gX=AIH@hZ4bv&C!>L{`me4Bj!i!%ulGr5Y(Lk@tbWTQ+P z8-IR`AhU!6smH&TF?c-O-u`a<|Gpao;hI*m-oluq1DxU@jCy8zNZoc8@FonU9&bK= zprx&EY6(l7ojpcStZb!#Ny-2!z<}Su*&l?Vn(lhMbK$sT)h}*Ot?I(zB(&XISJ6T6 z-B-Noc+rvNwd+n_O;aLK^a>I=|9hrLM8?#=o z72xINzcxk>3@PBd7%0DEqdf%B;ph@g6gQMBRYgjth9^XVi=%~Y;tAG zimwzXF(mOXZg`y+&H|7PtTBD{UAm>`(#_sc830xlM+0x_N|})At4)awCT?wYU9neE zJK7jy1LjyZ_Npqq1*(Vs4hq+Q3)N-z6Ey||7214^7pzN0ExH{|V2MgI29?S@3}s!J zR$~%M>Js#!s%_*BLuo12LTRKfPW!3Lss-ViW5-eZDXA?%V(TSbs;cTlcnpi$89C9G-2);b8ckmPw%HUB8dsU)Jpya*`TjwmKn^ zN7XnxgVZP5_yj&Tld?%S6F5y}Xb^Iamglg&uaK_HqiUS{0*ueJBJ2^?CWqI(23^Y7 z^%|54wRboaLKabDjP@(D;)v2K7o}@*5FI&<9G=oQ|EdR=UI%Wva>=^(4aB2bkf?g&54xb{d#a&}?WL z5d;rG8I<(>`v$(hGQL;w=*7L8pyyFh-Y-aQh+(9_?;D zdJ=@Kh|o5aoOGe!$sk5 zj>L#MDFx^}W|5i*oc02JA^bknGZ^f&yp9ED#y4WZzW}H!Nvy<3f6j7z?=39_itaoD zj#!Y_4|eKR)!s&A4&_=bP|uP7T+zTfk|Yhw29~7Hfpg8Ah?9Q!$>8Al_~Wsa8oS@AnxvLpe57EkB;yZq4tM?C)77!sQot5Q`Fwx7dnzHvgB6*?d zS9F2%+PF}GX*(MLdH|U6ukel(j%8WpXi7?E*9$_tDMqGN(liFTr_w9+tzSuNI1xSf-fk31LpZME+ZNyx#46?X_lJ9HtNHBm(#HjUvqL`7|=(poHkys2!(ILLL(H#)gJauNAW*w)u%G~7NG{rBIALQNL z*TDfJRT2?)fq};k7|@=0y=)yAIC+m9T=LCr-rpj&r+DU$3&WJ9dOYnUy@K<(!nwkQ z(@(JFWJW&v5g{sVQACZnz?bsZunQ)sH{3$?scD%Li4wS#vUdZ5wWC>UwNr_i z`}RuJe)*u|=`KWv4&_u<7avI^{&7f$csCgXqh&%WzFry$mdjWF_T&`0iuemy5D zH}PJxX&#!Ue!7le%=#n)Lf(LB91&-=Hl07abkyp|Z?&jY+$SGR3TDhbL2-u4S|>92 zFK^$xmU5w9EmO7`d@j=HW?sR8cmVp+M&Lty0h(_BLSBes5PW}d8law+E-Zma)x7e? z4qpGVb*x{<(*|+(lvC#w%Hv=0&Gq(|6^h}vJ{-RZ__+Be3F(|Nbr%khS^L8UhH%emP~>CiDQwfI+w2;iU;)cVan3UF#!zn?_A zGgqa(Zyvl-d{kzkWTMP{)r~YVbavb?n#Kfh89C-ncofqIj)>Yg4Qx`zVf1(-(fF+C z)8+-SH+IMV=37==VN3M8r(;n}UEV0AOM(ahm;YM;;-V z0NwjRkjk!Q=+IhH_4gigI}hHTmJ~pnKf(qdpg|Du?dvWgqKyn|=7ONEmo-iQW>G4E z*M-rrYNKkIY!Y*$(E6p8*x6Syc@>R$e1hSMe9QkeX8}zUl**ULMdE@n3rPHho+{Xm4l$bW|N?DOb<%F3~7DE(`g0GJX z!~vi{`_{S7AVP8?jkKhtF5f1p4Al|xpUW`|jpv+;yqPQSA>Bj~*P(+zqN;43#ehYDR$Xq{H3f22CAw4o2n zbzN=0kH&r-8K?odPqoXt zA7*@ARZ`I%M)k7tpU8d%`-aau{|5)D;s_4BVcNud26|KZX3r~^7mt+(3#O@;r-Rsr zU3SyA@DAxrKK-1{Ax2_W$C+9#Hy*EZv|k7oxFKa^hR2*fT~P>iunSij2! zYC0-%0C1hTb&(hR31w6asrAqqx0UM?V8TBx=wbJu{rCjMT{KUFSc}ezU(IzMLNhte zLp-H@hld^-Fd=!Iz;M0*$G&Uig?Jr#-K-4K;8wrdRzYNy*HY2k`vB5;<=5z@QqGqt;kRK?0^aVG!D$% zq1)-8bl!F9j2lHW_3GQX`z`!;cMIeeLht^#t=Lp(#5A$HYYBy}wz7+&ti);78j_Rj zwf5xU)|Af34ewYd&U8jPQmA9G!t;*&SmzaJhFS-5R=tk{(uTC0$b>WCds&WT=# ztF>K8-P1{T6gQIL%=sYrP<$!MYrvi5Gy`6kFr)$raWB}U=$Rb=`@{uD29OXXLGL}0 z{W`HrX(hLjfa2$2`B6Jd<5EqiX34&;T8{IWCID8BY1wh>?GgV~Y+!+iz#$!lK;Hw@ zRP&<^1O_(zU@4j5Okt9h6$SuxQTWgLEGWfw_pytzB4J}ZK8r9sb&O!%@jLTiF$U_> zW?*CU+TJb)iqgVHcfL;gsg5*(P9Rm(N^YEzbYB4!_WOhC?k)>Se}8vZp$=0CHy3~} zS~d7(TdY(#l?tzt{>?AlDw88T_Z@>aw?XsVZd$vH_r6@Jo{?+RRMXIQ^wqg(v@uwn z1IhCx4#*}vh|a}4y~@feZlReMD!plS^0nny>j%B+zoi$~C$91=5DFV{eWfE2d*`*V z23q=|u1v3Pril$@;%DstmPIY><9@6c#yks_tW$~sN@m?2hSiN79LG%%r0v3eL#oY9pg$m`?)gS>k7oi+Ojwz+_u#aYET zKQrr&zcCK50|{Y|C^oxdMl1kuc{kqy=l}zMS+C-CM?!Rxc$w8ySST%uW3KKv1Xf-E zB}25s+<;=&CR^4W@ZRFNA@JSuuU*g%eCrXJVEs;XmE#m=05L9?2{=X8S?jt$S|y`o z?<~;4g%i_g`54PP_}z{@-tk}Xf)!&$O>k)32NeEO4ZB)86$q! zBdXNfBP$NLRK=AFy;Mbt`@|IfrNtzb3SJPc4?>4lA_l}{u+wwI;cD@VesqtwYrxLD zyX#ojugVL=t>XLI2kTke?0Kbi2nN3Rf&rF4IzbBHq_oUKGka7cqu6XI*%=qC=ayA= zkrfzc4;szXrw(Q;A^oDWQ7-43o?whXbxZ4`BEMz(r=tBjx_*~&O_&CFo~JRp%%($c0}T{V-)%_1i~ugJDlqWf zjLSrfYJ9S!@lmw3D8v@t0`gOb`tI&+MI-zL5h5K!ARH87gaTtyq_~mxStz`O;{IvB zySvr#y_QDNkt7QSJ%x^FMRm00nu<;-h4K(IuT(-a7~#P0bEYDQF@H2$7rasoU0$R) z%3dc-(d+n&6cX1^)ybSASUCkvh(|Q;)&=pm53+DmrdgVo7YLh^D;0fziUJ~@2c16c zr6hc{6&8Kja5jhY?q_VFfbVEmbNS3()eB@xxStR5nPNS@@ zmqMNW_+>;0eD- z7H#JBRr11rdheC8;0xzsv;h?HJ2lCiZkIs{9sLj^jq(_WQSP6vNgh z2V)Lbmq|x54ahWqnecGcFsZ~;17uxjEuF(5Ua}IpswHipU&ed8cP+F1vYvajv^>10 zy8y~I_;=gjJ?Was=ubD?b@k4>%<*X3lk<_Y>B$Y;QuTPvVF<a~05xz0?B++y{9w|=>M zK$~@6{yGn9&0gf>v!b`aSV>iZK$$0{OaEViF~asy)>!>0<2 zMQ!o#aCMAL9Y-dCpMSxySqeWs;sSs0*C~e0dZhFv5828B{b2(bpN@mFx)^dKD92P5 z{YNO~6>4yFWV6OPo7~-1927#EE&J*m9MDm6T&Z)R40se)_*uo(QIc~?``ucBGls5D z=c)WaObda(Co9!P{ByK)%ErG(3nOxiPorhQ&%cSOmBhu546%P;j+;if zN=dpOq38ZS)Pq9-)Ic4l=*y(P?(UBKPaEvZuNJ9j;80!t!m8${jl|9=8vC!Iee<5M zhu1HRN$l(H^Is*eiEG3MAt|!W=m37G*^f>sS1-$GA3oLT{2hL}%Zk14_xt;P1h(S- z2yCy3#%m9UULZ=0p6V~^Sp1NDuceW1;O8#?++)en@8N1M`GFrMO5BGKPec2d7d@@{ z8yf{#touBm@Va}O3H)U2xW_7O__L^>1+4alOZ+9+eA5tk+4zE|caPT8A#SsR=J_6$ zQ_s((Vb9qD8u;e&!(ySSNoa;`p&?t5(r;m_5y}iW4hyuMV^Es*$$=ecr9c@STmH5F zg%da$7UgRVhLnUyNuA!I)aiiM<)AHfdZ%>HKPCs`cW0wF)!;mO7yYQ8n>tKZXajz3 zZ-_C*!;=@kR+Ol{hl=6-g$^vz;pi>w)nPcwtHGD(Jz4J*uIHz@EQjlPM+Mt$ABHY#=yR zfogvh=WUQ$kPsi=3~$(cxb2488jS27Ow*WK#8 zR%E+yrPpcpz&bg3XjH9wI@G5h$3=}3R2SD6hzXQrn2hl&)6+vMfiw^s1D1cWJ}&9{qCC+9K%;70AF=a%RrxrQe@; zCMpR(4LC_An~3(;$~W%P&v@x4{;s%!^S9L?k4$jhr8(AK(W;Ga+y!jP75XQzDL+alyUHLlz2L}LW>4R zt?tOg$4_Wh7M13Pps9_zgcoSq;&m`eAxU*HTHtSIh-OtM@RM!6lN5f|%Lzu0m@V^U zBW2?(I|sFyyb7>oPyXlc;VP8?`I=+g8wYV;bjH}dh-P?KX?9ccyWsoTXw!OE)#RUR zGM}LXRX=c&+3rqB$FK1Ku+{NZn?LZ(u%OXJa^0O+4J~W+(6XvW!y9MczUGPjVL`_9M7S&0y?Ba8+u4134Uotigp5-{RbiL! z9~Kf*v;|SEYxh5{V|1erHlk27G;?1g3Lu{XYQ_N1S5AQENt8zyWaPv)HLyYKmOww* z@QQx2Y)KpHnC-Tq3SZnFP6w^h3c{>Zq;@twi?4=`DVl34{Cva9iZ|LepRa6Yqa9+& z>IP2_y}&2ri9R;R_|y%b(T)sVD}wPBQ-@OnB83NmmLT4RVJEhG?+n^}LdHxD2=4 zcX4oTWTHedSTK7(i=_RX7?4OOIZdgP*%a7 zz;II*THxS&YEy7V$5WfjKj^QmiZuI`UdXG5wwi5%&1P5rW>?>^*=jv2(r;WcXMHtv zOp9BgyShy`B397 zNqu6)7uV|4G2HIjn%l_9QyX)uRFSE-E1LFO6F&tSL-tOH4(d^f-hMe1y4fA?fccrD7%6xE?|F<7U zvC$x4pvYp*r!`i6*6aK?3^tuERbPakH-lBVNGIqlyVHEVvKwmvV8}PMyF2PRek(nd z+yg%MvhInr7F$^{`qO>?hr52d&FkrhsjMD~NB0;XCLL4k0gmX!qr>n}ibzoh`hXq) z<2WXs4PqD;gYa)#E61zj+{pYzWS&iMKrnZ zQ9qp-bcb_xjAS}uQV3T_=Z11c*g&Q?^oNZr=w+x8hjgwIMShjVqFf+S6VKODhUbu? zVsR>GowYK_8ktMU=#9;7-~_@|iYvJmSi_-h^+EvCrbz?xxYX)o#)bGqtP=_b=f|;J j?g>WYbf8|kXwa^d5$ui=pyVG7QSbjBB}5O=<+}g?zqWDf literal 0 HcmV?d00001 diff --git a/files/app/resource/js/meshpage.js b/files/app/resource/js/meshpage.js new file mode 100755 index 00000000..42b79eec --- /dev/null +++ b/files/app/resource/js/meshpage.js @@ -0,0 +1,118 @@ +function render() +{ + +const search = document.querySelector("#meshfilter input"); +const page = document.getElementById("meshpage"); +const help = document.getElementById("meshpage-help"); +const etx = mesh.etx; +const hosts = mesh.hosts; +const services = mesh.services; + +let filtering; +let cfilter; +function filter() +{ + clearTimeout(filtering); + filtering = setTimeout(function() { + const filter = search.value.toLowerCase(); + if (filter === cfilter) { + return; + } + cfilter = filter; + const filtered = document.querySelectorAll(".valid"); + for (let i = 0; i < filtered.length; i++) { + filtered[i].classList.remove("valid"); + } + if (filter === "") { + page.classList.remove("filtering"); + } + else { + page.classList.add("filtering"); + const targets = document.querySelectorAll("[data-search]"); + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; + if (target.dataset.search.indexOf(filter) !== -1) { + target.classList.add("valid"); + } + } + } + }, 200); +} +search.addEventListener("keyup", filter); +search.addEventListener("click", filter); +search.addEventListener("keypress", event => event.keyCode == 13 && event.preventDefault()); + +function serv(ip, hostname) +{ + let view = ""; + const s = services[ip]; + if (s) { + const re = new RegExp(`//${hostname}:`, "i"); + for (let i = 0; i < s.length; i++) { + let name = s[i].n; + const url = s[i].u; + if (url.match(re)) { + const lname = name.toLowerCase(); + let type = ""; + const nametype = name.match(/^(.*)\[(.*)\]$/); + if (nametype) { + name = nametype[1]; + type = `
`; + } + const r = url.match(/^(.+:\/\/)([^:]+):(\d+)(.*)$/); + switch (r[3]) { + case "0": + view += `
${name}${type}
`; + break; + case "80": + case "443": + view += `
${name}⁠${type ? type : "
"}
`; + break; + default: + view += `
${name}⁠${type ? type : "
"}
`; + break; + } + } + } + } + return view; +} + +const blocks = [ 1, 2, 3, 5, 10, 1000 ]; +const labels = [ "Excellent", "Good", "Fair", "Slow", "Poor", "Improbable" ]; +let data = `
${labels[0]}
`; +for (let i = 0; i < etx.length; i++) { + const item = etx[i]; + const ip = item[0]; + const hostlist = hosts[ip]; + if (hostlist) { + const hostname = (hostlist.find(h => !h[1]) || [])[0]; + if (hostname) { + if (item[1] >= blocks[0]) { + while (item[1] >= blocks[0]) { + blocks.shift(); + labels.shift(); + } + data += `
${labels[0]}
`; + } + let lanview = ""; + for (let j = 0; j < hostlist.length; j++) { + const lanhost = hostlist[j]; + if (lanhost[1] && lanhost[1] !== ip) { + if (lanhost[0].indexOf("*.") !== 0) { + lanview += `
  ${lanhost[0]}
${serv(ip, lanhost[0])}
`; + } + } + } + data += `
${hostname}${item[1]}
${serv(ip, hostname)}
${lanview ? '
' + lanview + '
' : ''}
`; + } + } +} +page.innerHTML = data + "
"; + +help.addEventListener("click", () => { + document.querySelector(".meshpage-help").classList.toggle("visible"); +}); + +} +render(); diff --git a/files/app/root.ut b/files/app/root.ut new file mode 100644 index 00000000..fb134179 --- /dev/null +++ b/files/app/root.ut @@ -0,0 +1,654 @@ +{% +/* + * 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 + */ +%} +{% +import * as config from "./config.uc"; +import * as fs from "fs"; +import * as math from "math"; +import * as uci from "uci"; +import * as ubus from "ubus"; +import * as log from "log"; +import * as lucihttp from "lucihttp"; +import * as configuration from "aredn.configuration"; +import * as hardware from "aredn.hardware"; +import * as lqm from "aredn.lqm"; +import * as network from "aredn.network"; +import * as olsr from "aredn.olsr"; +import * as units from "aredn.units"; +import * as radios from "aredn.radios"; +import * as messages from "aredn.messages"; +import * as mesh from "aredn.mesh"; +import * as constants from "./constants.uc"; + +const pageCache = {}; +const resourceVersions = {}; + +log.openlog("uhttpd.aredn", log.LOG_PID, log.LOG_USER); + +const light = fs.readfile(`${config.application}/resource/css/themes/light.css`); +const dark = fs.readfile(`${config.application}/resource/css/themes/dark.css`); +fs.writefile(`${config.application}/resource/css/themes/default.css`, `${light}@media (prefers-color-scheme: dark) {\n${dark}\n}`); +if (!fs.access(`${config.application}/resource/css/theme.css`)) { + fs.symlink("themes/default.css", `${config.application}/resource/css/theme.css`); +} + +if (config.preload) { + function cp(path) { + const dir = fs.opendir(`${config.application}${path}`); + for (;;) { + const entry = dir.read(); + if (!entry) { + break; + } + if (match(entry, /\.ut$/)) { + const tpath = `${config.application}${path}${entry}`; + pageCache[tpath] = loadfile(tpath, { raw_mode: false }); + } + } + } + cp("/main/"); + cp("/partial/"); + cp("/main/status/e/"); + cp("/main/tools/e/"); + + radios.getCommonConfiguration(); +} + +if (config.resourcehash) { + function prepareResource(id, resource) + { + const path = `${config.application}/resource/${resource}`; + const pathgz = `${config.application}/resource/${resource}.gz`; + if (fs.access(path)) { + fs.unlink(pathgz); + system(`/bin/gzip -k ${path}`); + } + const md = fs.popen(`/usr/bin/md5sum ${pathgz}`); + resourceVersions[id] = match(md.read("all"), /^([0-9a-f]+)/)[1]; + md.close(); + fs.symlink(pathgz, `${path}.${resourceVersions[id]}.gz`); + } + prepareResource("usercss", "css/user.css"); + prepareResource("admincss", "css/admin.css"); + prepareResource("themecss", "css/theme.css"); + prepareResource("htmx", "js/htmx.min.js"); + prepareResource("meshpage", "js/meshpage.js"); + let cthemeversion = null; + const ctheme = fs.readlink(`${config.application}/resource/css/theme.css`); + const themes = fs.lsdir(`${config.application}/resource/css/themes`); + for (let i = 0; i < length(themes); i++) { + const theme = themes[i]; + if (match(theme, /^.*\.css$/)) { + prepareResource("themecss", `css/themes/${theme}`); + if (ctheme === `themes/${theme}`) { + cthemeversion = resourceVersions.themecss; + } + fs.unlink(`${config.application}/resource/css/theme.css.${resourceVersions.themecss}.gz`); + fs.symlink(`themes/${theme}.${resourceVersions.themecss}.gz`, `${config.application}/resource/css/theme.css.${resourceVersions.themecss}.gz`); + } + } + resourceVersions.themecss = cthemeversion; + fs.unlink(`${config.application}/resource/css/theme.version`); + fs.symlink(cthemeversion, `${config.application}/resource/css/theme.version`); +} +else { + let dir = fs.lsdir(`${config.application}/resource/css`); + for (let i = 0; i < length(dir); i++) { + if (match(dir[i], /\.gz$/)) { + fs.unlink(`${config.application}/resource/css/${dir[i]}`); + } + } + dir = fs.lsdir(`${config.application}/resource/css/themes`); + for (let i = 0; i < length(dir); i++) { + if (match(dir[i], /\.gz$/)) { + fs.unlink(`${config.application}/resource/css/themes/${dir[i]}`); + } + } + dir = fs.lsdir(`${config.application}/resource/js`); + for (let i = 0; i < length(dir); i++) { + if (match(dir[i], /\.gz$/) && dir[i] !== "htmx.min.js.gz") { + fs.unlink(`${config.application}/resource/js/${dir[i]}`); + } + } + fs.unlink(`${config.application}/resource/css/theme.version`); +} + +global._R = function(path, arg) +{ + const tpath = `${config.application}/partial/${path}.ut`; + const fn = pageCache[tpath] || loadfile(tpath, { raw_mode: false }); + let old = inner; + let r = ""; + try { + inner = arg; + r = render(fn); + } + catch (_) { + } + inner = old; + return r; +}; + +global._H = function(str) +{ + return includeHelp ? `
${str}
` : ""; +}; + +const uciMethods = +{ + init: function() + { + if (!cursor) + { + cursor = uci.cursor(); + } + }, + + add: function(a, b) + { + this.init(); + cursor.load(a); + return cursor.add(a, b); + }, + + get: function(a, b, c) + { + this.init(); + return cursor.get(a, b, c); + }, + + set: function(a, b, c, d) + { + this.init(); + if (d === undefined) { + cursor.set(a, b, c); + } + else { + cursor.set(a, b, c, d); + } + }, + + foreach: function(a, b, fn) + { + this.init(); + cursor.foreach(a, b, fn); + }, + + commit: function(a) + { + if (cursor) { + cursor.commit(a); + } + }, + + "delete": function(a, b) + { + this.init(); + cursor.delete(a, b); + }, + + error: function() + { + if (cursor) { + return cursor.error(); + } + } +}; + +const uciMeshMethods = +{ + init: function() + { + if (!cursorm) + { + cursorm = uci.cursor("/etc/config.mesh"); + } + }, + + load: function(a) + { + this.init(); + cursorm.load(a); + }, + + add: function(a, b) + { + this.init(); + cursorm.load(a); + return cursorm.add(a, b); + }, + + get: function(a, b, c) + { + this.init(); + return cursorm.get(a, b, c); + }, + + set: function(a, b, c, d) + { + this.init(); + if (d === undefined) { + cursorm.set(a, b, c); + } + else { + cursorm.set(a, b, c, d); + } + }, + + foreach: function(a, b, fn) + { + this.init(); + cursorm.foreach(a, b, fn); + }, + + commit: function(a) + { + if (cursorm) { + cursorm.commit(a); + } + }, + + "delete": function(a, b) + { + this.init(); + cursorm.delete(a, b); + }, + + error: function() + { + if (cursorm) { + return cursorm.error(); + } + } +}; + +const auth = { + isAdmin: false, + key: null, + age: 315360000, // 10 years + + DAYS: [ "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" ], + MONTHS: [ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ], + + initKey: function() + { + if (!this.key) { + const f = fs.open("/etc/shadow"); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (index(l, "root:") === 0) { + this.key = trim(l); + break; + } + } + f.close(); + } + } + }, + + runAuthentication: function(env) + { + const cookieheader = env.headers?.cookie; + if (cookieheader) { + const ca = split(cookieheader, ";"); + for (let i = 0; i < length(ca); i++) { + const cookie = trim(ca[i]); + if (index(cookie, "authV1=") === 0) { + this.initKey(); + if (this.key == b64dec(substr(cookie, 7))) { + this.isAdmin = true; + } + else { + this.isAdmin = false; + } + break; + } + } + } + }, + + authenticate: function(password) + { + if (!this.isAdmin) { + this.initKey(); + const s = split(this.key, /[:$]/); // s[3] = salt, s[4] = hashed + const f = fs.popen(`exec /usr/bin/mkpasswd -S '${s[3]}' '${password}'`); + if (f) { + const pwd = rtrim(f.read("all")); + f.close(); + if (index(this.key, `root:${pwd}:`) === 0) { + const time = clock(); + const gm = gmtime(time[0] + this.age); + const tm = `${this.DAYS[gm.wday]}, ${gm.mday} ${this.MONTHS[gm.mon]} ${gm.year} 00:00:00 GMT`; + response.headers["Set-Cookie"] = `authV1=${b64enc(this.key)}; Path=/; Domain=${replace(request.headers.host, /:\d+$/, "")}; Expires=${tm}; SameSite=Strict`; + this.isAdmin = true; + } + } + } + return this.isAdmin; + }, + + deauthenticate: function() + { + if (this.isAdmin) { + response.headers["Set-Cookie"] = `authV1=; Path=/; Domain=${replace(request.headers.host, /:\d+$/, "")}; Max-Age=0;`; + this.isAdmin = false; + } + } +}; + +const ubusMethods = +{ + call: function(path, method) + { + if (!connection) { + connection = ubus.connect(); + } + return connection.call(path, method); + } +}; + +const rePath = /^\/([-a-z]*)(.*)$/; + +global.handle_request = function(env) +{ + const path = match(env.PATH_INFO, rePath); + const page = path[1] || "status"; + const secured = index(path[2], "/e/") === 0; + const firstuse = !!fs.access("/etc/config/unconfigured"); + + if (path[2] == "" || secured) { + let tpath; + if (firstuse) { + tpath = `${config.application}/main/firstuse-ram.ut`; + const f = fs.open("/proc/mounts"); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (index(l, "overlay") !== -1 || index(l, "ext4") !== -1) { + tpath = `${config.application}/main/firstuse.ut`; + break; + } + } + f.close(); + } + } + else if (secured) { + tpath = `${config.application}/main${env.PATH_INFO}.ut`; + } + else { + tpath = `${config.application}/main/${page}.ut`; + if (!pageCache[tpath] && !fs.access(tpath)) { + tpath = `${config.application}/main/app.ut`; + } + } + + if (pageCache[tpath] || fs.access(tpath)) { + auth.runAuthentication(env); + if (secured && !auth.isAdmin && config.authenable) { + uhttpd.send("Status: 401 Unauthorized\r\n\r\n"); + return; + } + const args = {}; + if (env.CONTENT_TYPE === "application/x-www-form-urlencoded") { + let b = ""; + for (;;) { + const v = uhttpd.recv(10240); + if (!length(v)) { + break; + } + b += v; + } + const v = split(b, "&"); + for (let i = 0; i < length(v); i++) { + const kv = split(v[i], "="); + const k = uhttpd.urldecode(kv[0]); + if (!(k in args)) { + args[k] = uhttpd.urldecode(kv[1]); + } + } + } + if (index(env.CONTENT_TYPE, "multipart/form-data") === 0) { + let key; + let val; + let header; + let file; + let parser; + parser = lucihttp.multipart_parser(env.CONTENT_TYPE, (what, buffer, length) => { + switch (what) { + case parser.PART_INIT: + key = null; + val = null; + break; + case parser.HEADER_NAME: + header = lc(buffer); + break; + case parser.HEADER_VALUE: + if (header === "content-disposition") { + const filename = lucihttp.header_attribute(buffer, "filename"); + key = lucihttp.header_attribute(buffer, "name"); + file = { + name: `/tmp/${key}`, + filename: filename + }; + val = filename; + } + break; + case parser.PART_BEGIN: + if (file) { + fs.writefile("/proc/sys/vm/drop_caches", "3"); + file.fd = fs.open(file.name, "w"); + return false + } + break; + case parser.PART_DATA: + if (file) { + file.fd.write(buffer); + } + else { + val = buffer; + } + break; + case parser.PART_END: + if (file) { + file.fd.close(); + file.fd = null; + args[key] = file.name; + } + else if (key) { + args[key] = val; + } + key = null; + val = null; + file = null; + break; + case parser.ERROR: + log.syslog(log.LOG_ERR, `multipart error: ${buffer}`); + break; + } + return true; + }); + for (;;) { + const v = uhttpd.recv(10240); + if (!length(v)) { + parser.parse(null); + break; + } + parser.parse(v); + } + } + const response = { statusCode: 200, headers: { "Content-Type": "text/html", "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*" } }; + const fn = pageCache[tpath] || loadfile(tpath, { raw_mode: false }); + let res = ""; + try { + res = render(call, fn, null, { + config: config, + constants: constants, + versions: resourceVersions, + request: { env: env, headers: env.headers, args: args, page: page }, + response: response, + uci: uciMethods, + uciMesh: uciMeshMethods, + ubus: ubusMethods, + auth: auth, + includeHelp: (env.headers || {})["include-help"] === "1", + includeAdvanced: (env.headers || {})["include-advanced"] === "1", + fs: fs, + configuration: configuration, + hardware: hardware, + lqm: lqm, + network: network, + olsr: olsr, + units: units, + radios: radios, + math: math, + log: log, + messages: messages, + mesh: mesh + }); + } + catch (e) { + log.syslog(log.LOG_ERR, `${e.message}\n${e.stacktrace[0].context}`); + res = `
ERROR: ${e.message}
${e.stacktrace[0].context}
`; + } + if (!response.override) { + if (index(env.HTTP_ACCEPT_ENCODING || "", "gzip") === -1 || !config.compress) { + response.headers["Content-Length"] = `${length(res)}`; + uhttpd.send( + `Status: ${response.statusCode} OK\r\n`, + join("", map(keys(response.headers), k => k + ": " + response.headers[k] + "\r\n")), + "\r\n", + res + ); + } + else { + const r = fs.open("/dev/urandom"); + let datafile; + if (r) { + const rid = r.read(8); + r.close(); + datafile = `/tmp/uhttpd.${hexenc(rid)}`; + } + else { + datafile = `/tmp/uhttpd.${time()}${math.rand()}`; + } + try { + fs.writefile(datafile, res); + const z = fs.popen("exec /bin/gzip -c " + datafile); + try { + res = z.read("all"); + response.headers["Content-Length"] = `${length(res)}`; + uhttpd.send( + `Status: ${response.statusCode} OK\r\nContent-Encoding: gzip\r\n`, + join("", map(keys(response.headers), k => k + ": " + response.headers[k] + "\r\n")), + "\r\n", + res + ); + } + catch (_) { + } + z.close(); + } + catch (_) { + } + fs.unlink(datafile); + } + } + if (response.reboot) { + system("(sleep 2; exec /sbin/reboot)&"); + } + if (response.upgrade) { + system(`(sleep 2; ${response.upgrade})&`); + } + return; + } + uhttpd.send("Status: 404 Not Found\r\n\r\n"); + return; + } + + const rpath = `${config.application}/resource/${env.PATH_INFO || "unknown"}`; + const gzrpath = `${rpath}.gz`; + if (fs.access(gzrpath)) { + uhttpd.send("Status: 200 OK\r\nContent-Encoding: gzip\r\n"); + if (substr(rpath, -3) === ".js") { + uhttpd.send("Content-Type: application/javascript\r\n"); + } + else if (substr(rpath, -4) === ".css") { + uhttpd.send("Content-Type: text/css\r\n"); + } + if (!config.resourcehash) { + uhttpd.send("Cache-Control: no-store\r\n"); + } + else { + uhttpd.send("Cache-Control: max-age=604800\r\n"); + } + const res = fs.readfile(gzrpath); + uhttpd.send(`Content-Length: ${length(res)}\n`); + uhttpd.send("\r\n", res); + return; + } + + if (fs.access(rpath)) { + uhttpd.send("Status: 200 OK\r\n"); + if (substr(rpath, -3) === ".js") { + uhttpd.send("Content-Type: application/javascript\r\n"); + } + else if (substr(rpath, -4) === ".png") { + uhttpd.send("Content-Type: image/png\r\n"); + } + else if (substr(rpath, -4) === ".jpg") { + uhttpd.send("Content-Type: image/jpeg\r\n"); + } + else if (substr(rpath, -4) === ".css") { + uhttpd.send("Content-Type: text/css\r\n"); + } + if (!config.resourcehash) { + uhttpd.send("Cache-Control: no-store\r\n"); + } + else { + uhttpd.send("Cache-Control: max-age=604800\r\n"); + } + const res = fs.readfile(rpath); + uhttpd.send(`Content-Length: ${length(res)}\n`); + uhttpd.send("\r\n", res); + return; + } + + if (!config.resourcehash) { + uhttpd.send("Status: 404 Not Found\r\nCache-Control: no-store\r\n\r\n"); + } + else { + uhttpd.send("Status: 404 Not Found\r\nCache-Control: max-age=600\r\n\r\n"); + } +}; +%} diff --git a/files/etc/arednsysupgrade.conf b/files/etc/arednsysupgrade.conf index f8172077..d80c378d 100644 --- a/files/etc/arednsysupgrade.conf +++ b/files/etc/arednsysupgrade.conf @@ -27,6 +27,7 @@ /etc/aredn_include/lan.network.user /etc/aredn_include/wan.network.user /etc/aredn_include/dtdlink.network.user +/etc/aredn_include/olsrd.user /etc/aredn_include/dnsmasq-user.conf /etc/dropbear/dropbear_dss_host_key /etc/dropbear/dropbear_rsa_host_key @@ -41,3 +42,5 @@ /etc/passwd /etc/shadow /etc/package_store +/app/resource/css/theme.css +/app/resource/css/theme.version diff --git a/files/etc/config.mesh/_setup b/files/etc/config.mesh/_setup index a6f20c77..bca2ec27 100644 --- a/files/etc/config.mesh/_setup +++ b/files/etc/config.mesh/_setup @@ -9,40 +9,31 @@ wifi_chanbw = 20 wifi_distance = 0 wifi_country = 00 wifi_enable = 1 - wifi2_enable = 0 wifi2_ssid = NoCall-AREDN wifi2_channel = 36 wifi2_encryption = wifi2_key = wifi2_hwmode = 11a - wifi3_enable = 0 wifi3_ssid = wifi3_key = wifi3_hwmode = 11a - dmz_mode = 3 lan_proto = static lan_ip = 172.27.0.1 lan_mask = 255.255.255.0 lan_dhcp = 1 - dhcp_start = 5 dhcp_end = 25 dhcp_limit = 20 - olsrd_bridge = 0 - wan_proto = dhcp wan_dns1 = 8.8.8.8 wan_dns2 = 8.8.4.4 - dtdlink_ip=10. - time_zone = UTC +time_zone_name = UTC ntp_server = us.pool.ntp.org - description_node = - compat_version = 1.0 diff --git a/files/etc/config.mesh/aredn b/files/etc/config.mesh/aredn index f0503094..467de1de 100644 --- a/files/etc/config.mesh/aredn +++ b/files/etc/config.mesh/aredn @@ -1,3 +1,4 @@ + config downloads option firmwarepath 'http://downloads.arednmesh.org/firmware' @@ -12,13 +13,9 @@ config map option leafletcss 'http://unpkg.com/leaflet@0.7.7/dist/leaflet.css' option maptiles 'http://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg' -config meshstatus - config dmz option mode '3' -config location - config tunnel option weight '1' diff --git a/files/etc/config.mesh/dhcp b/files/etc/config.mesh/dhcp index 60f9eb35..01bc936a 100644 --- a/files/etc/config.mesh/dhcp +++ b/files/etc/config.mesh/dhcp @@ -1,19 +1,20 @@ + config dnsmasq option rebind_protection '0' - option confdir '/tmp/dnsmasq.d,*.conf' + option confdir '/tmp/dnsmasq.d,*.conf' config dhcp - option interface lan - option start - option limit - option leasetime 1h - option force 1 - option ignore + option interface 'lan' + option start + option limit + option leasetime '1h' + option force '1' + option ignore config dhcp - option interface wan - option ignore 1 + option interface 'wan' + option ignore '1' config dhcp - option interface wifi - option ignore 1 + option interface 'wifi' + option ignore '1' diff --git a/files/etc/config.mesh/dropbear b/files/etc/config.mesh/dropbear index 2bf01221..0ac01670 100644 --- a/files/etc/config.mesh/dropbear +++ b/files/etc/config.mesh/dropbear @@ -1,3 +1,4 @@ + config dropbear option PasswordAuth 'on' - option Port '2222' + option Port '2222' diff --git a/files/etc/config.mesh/firewall b/files/etc/config.mesh/firewall index e7a94023..1dbe3947 100644 --- a/files/etc/config.mesh/firewall +++ b/files/etc/config.mesh/firewall @@ -1,266 +1,264 @@ + config defaults - option syn_flood 1 - option input ACCEPT - option output ACCEPT - option forward REJECT + option syn_flood '1' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'REJECT' config zone - option name lan - option network 'lan' - option input ACCEPT - option output ACCEPT - option forward REJECT + option name 'lan' + option network 'lan' + option input 'ACCEPT' + option output 'ACCEPT' + option forward 'REJECT' config zone - option name wan - option network 'wan' - option input REJECT - option output ACCEPT - option forward REJECT - option masq 1 - option mtu_fix 1 + option name 'wan' + option network 'wan' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option masq '1' + option mtu_fix '1' config zone - option name wifi - option network 'wifi' - option input REJECT - option output ACCEPT - option forward REJECT - option mtu_fix 1 + option name 'wifi' + option network 'wifi' + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option mtu_fix '1' config zone - option name dtdlink - - option input REJECT - option output ACCEPT - option forward REJECT - option mtu_fix 1 + option name 'dtdlink' + + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option mtu_fix '1' config zone - option name vpn - - option input REJECT - option output ACCEPT - option forward REJECT - option mtu_fix 1 + option name 'vpn' + + option input 'REJECT' + option output 'ACCEPT' + option forward 'REJECT' + option mtu_fix '1' config forwarding - option src lan - option dest wan + option src 'lan' + option dest 'wan' config forwarding - option src lan - option dest wifi + option src 'lan' + option dest 'wifi' config forwarding - option src wifi - option dest wifi + option src 'wifi' + option dest 'wifi' config forwarding - option src lan - option dest dtdlink + option src 'lan' + option dest 'dtdlink' config forwarding - option src wifi - option dest dtdlink + option src 'wifi' + option dest 'dtdlink' config forwarding - option src dtdlink - option dest wifi + option src 'dtdlink' + option dest 'wifi' config forwarding - option src dtdlink - option dest dtdlink + option src 'dtdlink' + option dest 'dtdlink' config forwarding - option src vpn - option dest wifi + option src 'vpn' + option dest 'wifi' config forwarding - option src wifi - option dest vpn + option src 'wifi' + option dest 'vpn' config forwarding - option src lan - option dest vpn + option src 'lan' + option dest 'vpn' config forwarding - option src vpn - option dest dtdlink + option src 'vpn' + option dest 'dtdlink' config forwarding - option src dtdlink - option dest vpn + option src 'dtdlink' + option dest 'vpn' config forwarding - option src vpn - option dest vpn - -# Allow IPv4 ping -config rule - option name Allow-Ping - option src wifi - option proto icmp - option icmp_type echo-request - option family ipv4 - option target ACCEPT + option src 'vpn' + option dest 'vpn' config rule - option name Allow-Ping - option src dtdlink - option proto icmp - option icmp_type echo-request - option family ipv4 - option target ACCEPT + option name 'Allow-Ping' + option src 'wifi' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' config rule - option name Allow-Ping - option src vpn - option proto icmp - option icmp_type echo-request - option family ipv4 - option target ACCEPT + option name 'Allow-Ping' + option src 'dtdlink' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' + +config rule + option name 'Allow-Ping' + option src 'vpn' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' config include - option path /usr/local/bin/mesh-firewall - option fw4_compatible 1 + option path '/usr/local/bin/mesh-firewall' + option fw4_compatible '1' config include - option path /etc/firewall.user - option fw4_compatible 1 + option path '/etc/firewall.user' + option fw4_compatible '1' config rule - option name Allow-Ping - option src wan - option proto icmp - option icmp_type echo-request - option family ipv4 - option target ACCEPT + option name 'Allow-Ping' + option src 'wan' + option proto 'icmp' + option icmp_type 'echo-request' + option family 'ipv4' + option target 'ACCEPT' config rule - option src wifi - option dest_port 2222 - option proto tcp - option target ACCEPT + option src 'wifi' + option dest_port '2222' + option proto 'tcp' + option target 'ACCEPT' config rule - option src wifi - option dest_port 8080 - option proto tcp - option target ACCEPT + option src 'wifi' + option dest_port '8080' + option proto 'tcp' + option target 'ACCEPT' config rule - option src wifi - option dest_port 80 - option proto tcp - option target ACCEPT + option src 'wifi' + option dest_port '80' + option proto 'tcp' + option target 'ACCEPT' config rule - option src wifi - option dest_port 698 - option proto udp - option target ACCEPT + option src 'wifi' + option dest_port '698' + option proto 'udp' + option target 'ACCEPT' config rule - option src wifi - option dest_port 23 - option proto tcp - option target ACCEPT + option src 'wifi' + option dest_port '23' + option proto 'tcp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 2222 - option proto tcp - option target ACCEPT + option src 'dtdlink' + option dest_port '2222' + option proto 'tcp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 8080 - option proto tcp - option target ACCEPT + option src 'dtdlink' + option dest_port '8080' + option proto 'tcp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 80 - option proto tcp - option target ACCEPT + option src 'dtdlink' + option dest_port '80' + option proto 'tcp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 698 - option proto udp - option target ACCEPT + option src 'dtdlink' + option dest_port '698' + option proto 'udp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 23 - option proto tcp - option target ACCEPT + option src 'dtdlink' + option dest_port '23' + option proto 'tcp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 2222 - option proto tcp - option target ACCEPT + option src 'vpn' + option dest_port '2222' + option proto 'tcp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 8080 - option proto tcp - option target ACCEPT + option src 'vpn' + option dest_port '8080' + option proto 'tcp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 80 - option proto tcp - option target ACCEPT + option src 'vpn' + option dest_port '80' + option proto 'tcp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 698 - option proto udp - option target ACCEPT + option src 'vpn' + option dest_port '698' + option proto 'udp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 23 - option proto tcp - option target ACCEPT - -#SNMPD -config rule - option src wifi - option dest_port 161 - option proto udp - option target ACCEPT + option src 'vpn' + option dest_port '23' + option proto 'tcp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 161 - option proto udp - option target ACCEPT + option src 'wifi' + option dest_port '161' + option proto 'udp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 161 - option proto udp - option target ACCEPT - -# olsr jsoninfo -config rule - option src wifi - option dest_port 9090 - option proto tcp - option target ACCEPT + option src 'dtdlink' + option dest_port '161' + option proto 'udp' + option target 'ACCEPT' config rule - option src dtdlink - option dest_port 9090 - option proto tcp - option target ACCEPT + option src 'vpn' + option dest_port '161' + option proto 'udp' + option target 'ACCEPT' config rule - option src vpn - option dest_port 9090 - option proto tcp - option target ACCEPT + option src 'wifi' + option dest_port '9090' + option proto 'tcp' + option target 'ACCEPT' + +config rule + option src 'dtdlink' + option dest_port '9090' + option proto 'tcp' + option target 'ACCEPT' + +config rule + option src 'vpn' + option dest_port '9090' + option proto 'tcp' + option target 'ACCEPT' diff --git a/files/etc/config.mesh/network b/files/etc/config.mesh/network index 9bfa6dc4..24a1d4ba 100644 --- a/files/etc/config.mesh/network +++ b/files/etc/config.mesh/network @@ -4,20 +4,20 @@ #### Globals config globals 'globals' - option packet_steering '1' + option packet_steering '1' #### Loopback configuration -config interface loopback - option device "lo" - option proto static - option ipaddr 127.0.0.1 - option netmask 255.0.0.0 +config interface 'loopback' + option device 'lo' + option proto 'static' + option ipaddr '127.0.0.1' + option netmask '255.0.0.0' #### WIFI configuration -config interface wifi_mon - option proto none +config interface 'wifi_mon' + option proto 'none' ### Bridge configuration diff --git a/files/etc/config.mesh/olsrd b/files/etc/config.mesh/olsrd index 7257a898..56f210d4 100644 --- a/files/etc/config.mesh/olsrd +++ b/files/etc/config.mesh/olsrd @@ -1,3 +1,4 @@ + config olsrd option IpVersion '4' option MainIp '' diff --git a/files/etc/config.mesh/snmpd b/files/etc/config.mesh/snmpd index 636e08dd..6526d2eb 100644 --- a/files/etc/config.mesh/snmpd +++ b/files/etc/config.mesh/snmpd @@ -1,42 +1,43 @@ + config agent - option agentaddress UDP:161 + option agentaddress 'UDP:161' -config com2sec public - option secname ro - option source default - option community public +config com2sec 'public' + option secname 'ro' + option source 'default' + option community 'public' -config group public_v1 - option group public - option version v1 - option secname ro +config group 'public_v1' + option group 'public' + option version 'v1' + option secname 'ro' -config group public_v2c - option group public - option version v2c - option secname ro +config group 'public_v2c' + option group 'public' + option version 'v2c' + option secname 'ro' -config group public_usm - option group public - option version usm - option secname ro +config group 'public_usm' + option group 'public' + option version 'usm' + option secname 'ro' -config view all - option viewname all - option type included - option oid .1 +config view 'all' + option viewname 'all' + option type 'included' + option oid '.1' -config access public_access - option group public - option context none - option version any - option level noauth - option prefix exact - option read all - option write none - option notify none +config access 'public_access' + option group 'public' + option context 'none' + option version 'any' + option level 'noauth' + option prefix 'exact' + option read 'all' + option write 'none' + option notify 'none' config system - option sysLocation 'Deployed' - option sysContact '' - option sysName '.local.mesh' + option sysLocation 'Deployed' + option sysContact '' + option sysName '.local.mesh' diff --git a/files/etc/config.mesh/system b/files/etc/config.mesh/system index 34c24197..576335d8 100644 --- a/files/etc/config.mesh/system +++ b/files/etc/config.mesh/system @@ -1,29 +1,30 @@ -config 'system' - option 'hostname' '' - option 'timezone' '' - option 'description' '' - option 'compat_version' '' - option 'log_ip' '' - option 'log_port' '' - option 'log_proto' '' -config 'timeserver' 'ntp' - list 'server' '' - option enable_server 0 - option enabled 0 +config system + option hostname '' + option timezone '' + option description '' + option compat_version '' + option log_ip '' + option log_port '' + option log_proto '' + +config timeserver 'ntp' + list server '' + option enable_server '0' + option enabled '0' config button - option button 'reset' - option action 'released' - option handler '/usr/local/bin/recoverymode' - option min '3' - option max '7' + option button 'reset' + option action 'released' + option handler '/usr/local/bin/recoverymode' + option min '3' + option max '7' config button - option button 'reset' - option action 'released' - option handler 'firstboot -y && reboot' - option min '12' - option max '20' + option button 'reset' + option action 'released' + option handler 'firstboot -y && reboot' + option min '12' + option max '20' include /etc/aredn_include/system_netled diff --git a/files/etc/config.mesh/uhttpd b/files/etc/config.mesh/uhttpd index 6c6afd08..56246669 100644 --- a/files/etc/config.mesh/uhttpd +++ b/files/etc/config.mesh/uhttpd @@ -1,11 +1,13 @@ -# Server configuration -config uhttpd main + +config uhttpd 'main' list listen_http '0.0.0.0:8080' list listen_http '0.0.0.0:80' option home '/www' option rfc1918_filter '1' option cgi_prefix '/cgi-bin' + list ucode_prefix '/a=/app/root.ut' option script_timeout '240' option network_timeout '30' option http_keepalive '0' list alias '/metrics=/cgi-bin/metrics' + option max_requests '3' diff --git a/files/etc/config.mesh/vtun b/files/etc/config.mesh/vtun index 48a40ff5..e69de29b 100755 --- a/files/etc/config.mesh/vtun +++ b/files/etc/config.mesh/vtun @@ -1,5 +0,0 @@ -config options - -config network - -config default diff --git a/files/etc/config/uhttpd b/files/etc/config/uhttpd index 7e65aafb..3610a9d7 100644 --- a/files/etc/config/uhttpd +++ b/files/etc/config/uhttpd @@ -1,13 +1,11 @@ # Server configuration config uhttpd main - - # HTTP listen addresses, multiple allowed - list listen_http 0.0.0.0:8080 - list listen_http 0.0.0.0:80 - option home /www - option rfc1918_filter 1 - option cgi_prefix /cgi-bin - option script_timeout 240 - option network_timeout 30 - option tcp_keepalive 5 -# option config /etc/httpd.conf + list listen_http '0.0.0.0:8080' + list listen_http '0.0.0.0:80' + option home '/www' + option rfc1918_filter '1' + option cgi_prefix '/cgi-bin' + list ucode_prefix '/a=/app/root.ut' + option script_timeout '240' + option network_timeout '30' + option tcp_keepalive '5' diff --git a/files/etc/cron.boot/boot-fixups b/files/etc/cron.boot/boot-fixups new file mode 100755 index 00000000..d2e41982 --- /dev/null +++ b/files/etc/cron.boot/boot-fixups @@ -0,0 +1,38 @@ +#!/bin/sh +true <<'LICENSE' + + Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks + Copyright (C) 2024 Tim Wilkinson KN6PLV + 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. + +LICENSE + +# Until I find out what has left pending vtun changes we do this here +/sbin/uci -c /etc/config.mesh commit vtun diff --git a/files/etc/cron.daily/update-clock b/files/etc/cron.daily/update-clock index cf395762..4cc8b01f 100755 --- a/files/etc/cron.daily/update-clock +++ b/files/etc/cron.daily/update-clock @@ -42,6 +42,7 @@ exec 2> /dev/null candidate=$(uci -q get system.ntp.server) if [ "${candidate}" != "" ]; then if $(ntpd -n -q -p ${candidate}); then + echo -n "ntp" > /tmp/timesync logger -p notice -t update-time "Update clock from ${candidate}" exit 0 fi @@ -52,10 +53,13 @@ fi for candidate in $(grep -i ntp /var/run/services_olsr | sed "s/^.*:\/\/\(.*\):.*$/\1/") do if $(ntpd -n -q -p ${candidate}); then + echo -n "ntp" > /tmp/timesync logger -p notice -t update-time "Update clock from ${candidate}" exit 0 fi done +rm -f /tmp/timesync + logger -p notice -t update-time "Failed to update clock: No reachable ntpd servers." exit 1 diff --git a/files/etc/cron.hourly/firmware-helper b/files/etc/cron.hourly/firmware-helper new file mode 100755 index 00000000..0335e045 --- /dev/null +++ b/files/etc/cron.hourly/firmware-helper @@ -0,0 +1,83 @@ +#!/usr/bin/lua +--[[ + + 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(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("uci") + +local update_hour = 13 -- Run at ~1pm UTC +local current_releases = "/etc/current_releases" + +local cursor = uci.cursor() + +local do_version_update = false; +local time = os.date("!*t") +if time.hour == update_hour then + do_version_update = true +end + +local f = io.open(current_releases) +if f then + f:close() +else + do_version_update = true +end + +-- Update firmware version information +if do_version_update then + local config_url = cursor:get("aredn", "@downloads[0]", "firmware_aredn") .. "/afs/www/config.js" + local release_version + local nightly_version + for line in io.popen("/usr/bin/curl -o - " .. config_url .. " 2> /dev/null"):lines() + do + local v = line:match("versions: {(.+)}") + if v then + for i in v:gmatch("'(%d+-[^']+)'") + do + nightly_version = i + end + end + v = line:match('default_version: "(.+)"') + if v then + release_version = v + end + end + if release_version and nightly_version then + local f = io.open(current_releases, "w") + if f then + f:write(release_version .. " " .. nightly_version) + f:close() + end + end +end diff --git a/files/etc/init.d/local b/files/etc/init.d/local index 723a2e57..119dba6a 100755 --- a/files/etc/init.d/local +++ b/files/etc/init.d/local @@ -18,7 +18,6 @@ boot() { if [ -z "$poevalue" ]; then local dpval=$(jsonfilter -e '@.gpioswitch.poe_passthrough.default' < /etc/board.json) if [ ! -z "$dpval" ]; then - uci -q add aredn poe uci -q set aredn.@poe[0].passthrough="$dpval" uci -q commit aredn poevalue=$dpval @@ -30,7 +29,6 @@ boot() { local usbvalue=$(uci -q get aredn.@usb[0].passthrough) if [ -z "$usbvalue" ]; then local duval=$(jsonfilter -e '@.gpioswitch.usb_power_switch.default' < /etc/board.json) - uci -q add aredn usb uci -q set aredn.@usb[0].passthrough="$duval" uci -q commit aredn usbvalue=$duval @@ -38,27 +36,17 @@ boot() { /usr/local/bin/usb_passthrough "${usbvalue}" # package repositories - local repos="core base arednpackages packages luci routing telephony" - set -- $repos - while [ -n "$1" ]; do - local ucirepo=$(uci -q get aredn.@downloads[0].pkgs_$1) - local distrepo=$(grep aredn_$1 /etc/opkg/distfeeds.conf | cut -d' ' -f3) - # get the URLs from distfeeds.conf and set the initial UCI values if not present - if [ -z $ucirepo ]; then - uci set aredn.@downloads[0].pkgs_$1=$distrepo - uci commit aredn - uci -c /etc/config.mesh set aredn.@downloads[0].pkgs_$1=$distrepo - uci -c /etc/config.mesh commit aredn - # check values in distfeeds.conf against UCI settings - # and change distfeeds.conf if needed (upgrades?) - elif [ $ucirepo != $distrepo ]; then - sed -i "s|$distrepo|$ucirepo|g" /etc/opkg/distfeeds.conf - fi - shift - done - - if [ -z "$(uci -q get aredn.@alerts[0])" ]; then - uci -q add aredn alerts - uci -q commit aredn + local packages_default = $(uci -q get "aredn.@downloads[0].packages_default") + if [ "${packages_default}" != "" ]; then + local repos="core base arednpackages packages luci routing telephony" + set -- $repos + while [ -n "$1" ]; do + local distrepo = $(grep aredn_$1 /etc/opkg/distfeeds.conf | cut -d' ' -f3) + local prefixurl = $(echo $distrepo | sed -n 's/\(http[s]\?:\/\/[^/]\+\).*/\1/p') + if [ "$packages_default" != "$prefixurl" ]; then + sed -i "s|$prefixurl|$packages_default|g" /etc/opkg/distfeeds.conf + fi + shift + done fi } diff --git a/files/etc/radios.json b/files/etc/radios.json index 5c773ee9..0c2f0aef 100644 --- a/files/etc/radios.json +++ b/files/etc/radios.json @@ -1,9 +1,9 @@ { "meraki mr16": { - "maxpower": "21" + "maxpower": 21 }, "gl.inet gl-ar150": { - "maxpower": "18", + "maxpower": 18, "antenna": { "description": "2 dBi Omni", "gain": 2, @@ -11,7 +11,7 @@ } }, "gl.inet gl-ar300m": { - "maxpower": "23", + "maxpower": 23, "antenna": { "description": "2.5 dBi Omni", "gain": 2.5, @@ -27,7 +27,7 @@ } }, "gl.inet gl-ar300m16": { - "maxpower": "23", + "maxpower": 23, "antenna": { "description": "2.5 dBi Omni", "gain": 2.5, @@ -35,7 +35,7 @@ } }, "gl.inet gl-usb150": { - "maxpower": "20", + "maxpower": 20, "antenna": { "description": "Omni", "gain": 0, @@ -43,10 +43,10 @@ } }, "gl.inet gl-ar750": { - "maxpower": "23" + "maxpower": 23 }, "gl.inet gl-ar750s (nor/nand)": { - "maxpower": "23" + "maxpower": 23 }, "gl.inet gl-b1300": { "wlan0": { @@ -88,7 +88,7 @@ }, "gl.inet gl-e750": { "wlan0": { - "maxpower": "20", + "maxpower": 20, "antenna": { "description": "3.5 dBi Omni", "gain": 3.5, @@ -96,7 +96,7 @@ } }, "wlan1": { - "maxpower": "20", + "maxpower": 20, "antenna": { "description": "1.5 dBi Omni", "gain": 1.5, @@ -105,7 +105,7 @@ } }, "tp-link cpe210 v1": { - "maxpower": "23", + "maxpower": 23, "chanpower": { "1": "22", "14": "23" @@ -117,7 +117,7 @@ } }, "tp-link cpe210 v2": { - "maxpower": "29", + "maxpower": 29, "chanpower": { "1": "27", "2": "28", @@ -131,7 +131,7 @@ } }, "tp-link cpe210 v3": { - "maxpower": "25", + "maxpower": 25, "chanpower": { "1": "21", "2": "25", @@ -144,7 +144,7 @@ } }, "tp-link cpe220 v2": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "1": "25", "2": "28", @@ -157,7 +157,7 @@ } }, "tp-link cpe220 v3": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "1": "25", "2": "28", @@ -170,7 +170,7 @@ } }, "tp-link cpe510 v1": { - "maxpower": "23", + "maxpower": 23, "chanpower": { "48": "10", "149": "17", @@ -183,7 +183,7 @@ } }, "tp-link cpe510 v2": { - "maxpower": "26", + "maxpower": 26, "chanpower": { "140": "17", "184": "26" @@ -195,7 +195,7 @@ } }, "tp-link cpe510 v3": { - "maxpower": "26", + "maxpower": 26, "chanpower": { "140": "17", "184": "26" @@ -207,7 +207,7 @@ } }, "tp-link cpe605 v1": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "133": "30", "141": "30", @@ -223,7 +223,7 @@ } }, "tp-link cpe610 v1": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "133": "15", "141": "26", @@ -239,7 +239,7 @@ } }, "tp-link cpe610 v2": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "133": "15", "141": "26", @@ -255,7 +255,7 @@ } }, "tp-link cpe710 v1": { - "maxpower": "30", + "maxpower": 30, "chanpower": { "133": "30", "141": "30", @@ -271,7 +271,7 @@ } }, "tp-link wbs210 v1": { - "maxpower": "27", + "maxpower": 27, "chanpower": { "1": "13", "10": "18", @@ -281,7 +281,7 @@ "antenna": "external" }, "tp-link wbs210 v2": { - "maxpower": "27", + "maxpower": 27, "chanpower": { "1": "13", "10": "18", @@ -291,7 +291,7 @@ "antenna": "external" }, "tp-link wbs510 v1": { - "maxpower": "26", + "maxpower": 26, "chanpower": { "133": "26", "149": "24", @@ -301,7 +301,7 @@ "antenna": "external" }, "tp-link wbs510 v2": { - "maxpower": "26", + "maxpower": 26, "chanpower": { "133": "26", "149": "24", @@ -311,23 +311,23 @@ "antenna": "external" }, "mikrotik routerboard 911g-2hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard rb911g-2hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard 911g-5hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard rb911g-5hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard 911g-5hpnd-qrt": { - "maxpower": "30", + "maxpower": 30, "antenna": { "description": "24 dBi 10.5° Panel", "gain": 24, @@ -335,7 +335,7 @@ } }, "mikrotik routerboard 911g-2hpnd-12s": { - "maxpower": "30", + "maxpower": 30, "antenna": { "description": "12 dBi 120° Sector", "gain": 12, @@ -343,7 +343,7 @@ } }, "mikrotik routerboard 921gs-5hpacd-15s": { - "maxpower": "31", + "maxpower": 31, "bandwidths": [ 10, 20 ], "antenna": { "description": "15 dBi 120° Sector", @@ -352,7 +352,7 @@ } }, "mikrotik routerboard 921gs-5hpacd-19s": { - "maxpower": "31", + "maxpower": 31, "bandwidths": [ 10, 20 ], "antenna": { "description": "19 dBi 120° Sector", @@ -421,52 +421,52 @@ } }, "mikrotik routerboard 912uag-2hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard rb912uag-2hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard 912uag-5hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard rb912uag-5hpnd": { - "maxpower": "30", + "maxpower": 30, "antenna": "external" }, "mikrotik routerboard ldf-5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": "external" }, "mikrotik routerboard ldf 5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": "external" }, "mikrotik routerboard ldf-2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": "external" }, "mikrotik routerboard ldf 2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": "external" }, "mikrotik ldf 5 ac (rbldfg-5acd)": { - "maxpower": "25", + "maxpower": 25, "bandwidths": [ 10, 20 ], "antenna": "external" }, "mikrotik routerboard rbldf-5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": "external" }, "mikrotik routerboard rbldf-2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": "external" }, "mikrotik routerboard lhg 5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "24.5 dBi 7° Dish", "gain": 24.5, @@ -474,7 +474,7 @@ } }, "mikrotik routerboard rblhg-5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "24.5 dBi 7° Dish", "gain": 24.5, @@ -482,7 +482,7 @@ } }, "mikrotik routerboard lhg 2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "18 dBi 18° Dish", "gain": 18, @@ -490,7 +490,7 @@ } }, "mikrotik routerboard rblhg 2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "18 dBi 18° Dish", "gain": 18, @@ -498,7 +498,7 @@ } }, "mikrotik routerboard rblhg-2nd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "18 dBi 18° Dish", "gain": 18, @@ -506,7 +506,7 @@ } }, "mikrotik routerboard lhg 2nd-xl": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "21 dBi 18° Dish", "gain": 21, @@ -514,7 +514,7 @@ } }, "mikrotik routerboard rblhg 2nd-xl": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "21 dBi 18° Dish", "gain": 21, @@ -522,7 +522,7 @@ } }, "mikrotik routerboard rblhg-2nd-xl": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "21 dBi 18° Dish", "gain": 21, @@ -530,7 +530,7 @@ } }, "mikrotik routerboard lhg 5hpnd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "24.5 dBi 10.4° Dish", "gain": 24.5, @@ -538,7 +538,7 @@ } }, "mikrotik routerboard rblhg-5hpnd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "24.5 dBi 10.4° Dish", "gain": 24.5, @@ -546,7 +546,7 @@ } }, "mikrotik routerboard lhg 5hpnd-xl": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "27 dBi 6.4° Dish", "gain": 27, @@ -554,7 +554,7 @@ } }, "mikrotik lhg 5 ac (rblhgg-5acd)": { - "maxpower": "25", + "maxpower": 25, "bandwidths": [ 10, 20 ], "antenna": { "description": "24.5 dBi 7° Dish", @@ -563,7 +563,7 @@ } }, "mikrotik lhg 5 ac xl (rblhgg-5acd-xl)": { - "maxpower": "25", + "maxpower": 25, "bandwidths": [ 10, 20 ], "antenna": { "description": "27 dBi 7° Dish", @@ -572,7 +572,7 @@ } }, "mikrotik routerboard sxtsq 5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -580,7 +580,7 @@ } }, "mikrotik routerboard rbsxtsq5nd": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -588,7 +588,7 @@ } }, "mikrotik routerboard sxtsq 2nd": { - "maxpower": "30", + "maxpower": 30, "antenna": { "description": "10 dBi 60° Panel", "gain": 10, @@ -596,7 +596,7 @@ } }, "mikrotik routerboard rbsxtsq2nd": { - "maxpower": "30", + "maxpower": 30, "antenna": { "description": "10 dBi 60° Panel", "gain": 10, @@ -604,7 +604,7 @@ } }, "mikrotik routerboard sxt 2nd (sxt lite2)": { - "maxpower": "30", + "maxpower": 30, "antenna": { "description": "10 dBi 60° Panel", "gain": 10, @@ -612,7 +612,7 @@ } }, "mikrotik routerboard sxtsq 5hpnd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -620,7 +620,7 @@ } }, "mikrotik routerboard rbsxtsq5hpnd": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -628,7 +628,7 @@ } }, "mikrotik routerboard sxt 5nd (sxt lite5)": { - "maxpower": "25", + "maxpower": 25, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -636,7 +636,7 @@ } }, "mikrotik routerboard sxt 5hpnd (sxt 5 high power)": { - "maxpower": "28", + "maxpower": 28, "antenna": { "description": "16 dBi 23° Panel", "gain": 16, @@ -644,7 +644,7 @@ } }, "mikrotik sxtsq 5 ac (rbsxtsqg-5acd)": { - "maxpower": "25", + "maxpower": 25, "bandwidths": [ 10, 20 ], "antenna": { "description": "16 dBi 23° Panel", @@ -694,8 +694,8 @@ }, "0xe005": { "name": "Ubiquiti NanoStation M5", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": { "description": "16 dBi 43° Panel", "gain": 16, @@ -705,8 +705,8 @@ }, "0xe009": { "name": "Ubiquiti NanoStation Loco M9", - "maxpower": "22", - "pwroffset": "6", + "maxpower": 22, + "pwroffset": 6, "antenna": { "description": "8 dBi 60° Panel", "gain": 8, @@ -715,8 +715,8 @@ }, "0xe012": { "name": "Ubiquiti NanoStation M2", - "maxpower": "18", - "pwroffset": "10", + "maxpower": 18, + "pwroffset": 10, "antenna": { "description": "11 dBi 55° Panel", "gain": 11, @@ -725,8 +725,8 @@ }, "0xe035": { "name": "Ubiquiti NanoStation M3", - "maxpower": "22", - "pwroffset": "3", + "maxpower": 22, + "pwroffset": 3, "antenna": { "description": "13 dBi 60° Panel", "gain": 13, @@ -735,8 +735,8 @@ }, "0xe0a2": { "name": "Ubiquiti NanoStation Loco M2", - "maxpower": "18", - "pwroffset": "5", + "maxpower": 18, + "pwroffset": 5, "antenna": { "description": "8.5 dBi 60° Panel", "gain": 8.5, @@ -745,8 +745,8 @@ }, "0xe0a5": { "name": "Ubiquiti NanoStation Loco M5", - "maxpower": "22", - "pwroffset": "1", + "maxpower": 22, + "pwroffset": 1, "antenna": { "description": "13 dBi 45° Panel", "gain": 13, @@ -755,164 +755,164 @@ }, "0xe105": { "name": "Ubiquiti Rocket M5", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe112": { "name": "Ubiquiti Rocket M2 with USB", - "maxpower": "18", - "pwroffset": "10", + "maxpower": 18, + "pwroffset": 10, "antenna": "external" }, "0xe1b2": { "name": "Ubiquiti Rocket M2", - "maxpower": "18", - "pwroffset": "10", + "maxpower": 18, + "pwroffset": 10, "antenna": "external" }, "0xe1b5": { "name": "Ubiquiti Rocket M5", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe1b9": { "name": "Ubiquiti Rocket M9", - "maxpower": "22", - "pwroffset": "6", + "maxpower": 22, + "pwroffset": 6, "antenna": "external" }, "0xe1c3": { "name": "Ubiquiti Rocket M3", - "maxpower": "22", - "pwroffset": "3", + "maxpower": 22, + "pwroffset": 3, "antenna": "external" }, "0xe1c5": { "name": "Ubiquiti Rocket M5 GPS", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe1d2": { "name": "Ubiquiti Rocket M2 Titanium", - "maxpower": "18", - "pwroffset": "10", + "maxpower": 18, + "pwroffset": 10, "antenna": "external" }, "0xe1d5": { "name": "Ubiquiti Rocket M5 Titanium GPS", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe1f5": { "name": "Ubiquiti Rocket 5AC Lite", - "maxpower": "23", - "pwroffset": "4", + "maxpower": 23, + "pwroffset": 4, "antenna": "external" }, "0xe202": { "name": "Ubiquiti Bullet M2 HP", - "maxpower": "16", - "pwroffset": "12", + "maxpower": 16, + "pwroffset": 12, "antenna": "external" }, "0xe205": { "name": "Ubiquiti Bullet M5", - "maxpower": "19", - "pwroffset": "6", + "maxpower": 19, + "pwroffset": 6, "antenna": "external" }, "0xe212": { "name": "Ubiquiti airGrid M2", - "maxpower": "28" + "maxpower": 28 }, "0xe215": { "name": "Ubiquiti airGrid M5", - "maxpower": "19", - "pwroffset": "1" + "maxpower": 19, + "pwroffset": 1 }, "0xe232": { "name": "Ubiquiti NanoBridge M2", - "maxpower": "21", - "pwroffset": "2" + "maxpower": 21, + "pwroffset": 2 }, "0xe235": { "name": "Ubiquiti NanoBridge M5", - "maxpower": "22", - "pwroffset": "1", + "maxpower": 22, + "pwroffset": 1, "antenna": "external" }, "0xe239": { "name": "Ubiquiti NanoBridge M9", - "maxpower": "22", - "pwroffset": "6" + "maxpower": 22, + "pwroffset": 6 }, "0xe242": { "name": "airGrid M2 HP", - "maxpower": "19", - "pwroffset": "9" + "maxpower": 19, + "pwroffset": 9 }, "0xe243": { "name": "Ubiquiti NanoBridge M3", - "maxpower": "22", - "pwroffset": "3" + "maxpower": 22, + "pwroffset": 3 }, "0xe252": { "name": "Ubiquiti airGrid M2 HP", - "maxpower": "19", - "pwroffset": "9" + "maxpower": 19, + "pwroffset": 9 }, "0xe245": { "name": "Ubiquiti airGrid M5 HP", - "maxpower": "19", - "pwroffset": "6" + "maxpower": 19, + "pwroffset": 6 }, "0xe255": { "name": "Ubiquiti airGrid M5 HP", - "maxpower": "19", - "pwroffset": "6" + "maxpower": 19, + "pwroffset": 6 }, "0xe2b5": { "name": "Ubiquiti NanoBridge M5 (XM)", - "maxpower": "22", - "pwroffset": "1", + "maxpower": 22, + "pwroffset": 1, "antenna": "external" }, "0xe2c2": { "name": "Ubiquiti NanoBeam M2 International", - "maxpower": "18", - "pwroffset": "10" + "maxpower": 18, + "pwroffset": 10 }, "0xe2c4": { "name": "Ubiquiti Bullet M2 XW", - "maxpower": "19", - "pwroffset": "6", + "maxpower": 19, + "pwroffset": 6, "antenna": "external" }, "0xe2d2": { "name": "Ubiquiti Bullet M2 Titanium HP", - "maxpower": "16", - "pwroffset": "12", + "maxpower": 16, + "pwroffset": 12, "antenna": "external" }, "0xe2d5": { "name": "Ubiquiti Bullet M5 Titanium", - "maxpower": "19", - "pwroffset": "6", + "maxpower": 19, + "pwroffset": 6, "antenna": "external" }, "0xe302": { "name": "Ubiquiti PicoStation M2", - "maxpower": "16", - "pwroffset": "12" + "maxpower": 16, + "pwroffset": 12 }, "0xe3d5": { "name": "Ubiquiti PowerBeam 5AC 500", - "maxpower": "23", - "pwroffset": "1", + "maxpower": 23, + "pwroffset": 1, "antenna": { "description": "27 dBi 10° Dish", "gain": 27, @@ -921,8 +921,8 @@ }, "0xe3d6": { "name": "Ubiquiti PowerBeam 5AC 400", - "maxpower": "23", - "pwroffset": "1", + "maxpower": 23, + "pwroffset": 1, "antenna": { "description": "25 dBi 10° Dish", "gain": 25, @@ -931,8 +931,8 @@ }, "0xe3d7": { "name": "Ubiquiti NanoBeam AC Gen2 (XC)", - "maxpower": "23", - "pwroffset": "1", + "maxpower": 23, + "pwroffset": 1, "antenna": { "description": "19 dBi 30° Panel", "gain": 19, @@ -941,8 +941,8 @@ }, "0xe7f5": { "name": "Ubiquiti PowerBeam 5AC 400", - "maxpower": "23", - "pwroffset": "1", + "maxpower": 23, + "pwroffset": 1, "antenna": { "description": "25 dBi 10° Dish", "gain": 25, @@ -951,8 +951,8 @@ }, "0xe3e5": { "name": "Ubiquiti PowerBeam M5 XW 300", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "22 dBi 12° Dish", "gain": 22, @@ -961,25 +961,25 @@ }, "0xe4a2": { "name": "Ubiquiti AirRouter", - "maxpower": "19", - "pwroffset": "1" + "maxpower": 19, + "pwroffset": 1 }, "0xe4b2": { "name": "Ubiquiti AirRouter HP", - "maxpower": "19", - "pwroffset": "9", + "maxpower": 19, + "pwroffset": 9, "antenna": "external" }, "0xe4d5": { "name": "Ubiquiti Rocket M5 Titanium", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe4e5": { "name": "Ubiquiti PowerBeam M5 400", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "25 dBi 10° Dish", "gain": 25, @@ -1001,8 +1001,8 @@ }, "0xe4f5": { "name": "Ubiquiti NanoBeam AC (XC)", - "maxpower": "23", - "pwroffset": "3", + "maxpower": 23, + "pwroffset": 3, "antenna": { "description": "19 dBi 30° Panel", "gain": 19, @@ -1011,8 +1011,8 @@ }, "0xe5e5": { "name": "Ubiquiti PowerBeam M5 300-ISO", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "22 dBi 12° Dish", "gain": 22, @@ -1021,8 +1021,8 @@ }, "0xe5f5": { "name": "Ubiquiti PowerBeam 5AC 620", - "maxpower": "23", - "pwroffset": "1", + "maxpower": 23, + "pwroffset": 1, "antenna": { "description": "29 dBi 6° Dish", "gain": 29, @@ -1031,8 +1031,8 @@ }, "0xe6e5": { "name": "Ubiquiti PowerBeam M5 400-ISO", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "25 dBi 8° Dish", "gain": 25, @@ -1041,7 +1041,7 @@ }, "0xe7fb": { "name": "Ubiquiti NanoStation AC (WA)", - "maxpower": "27", + "maxpower": 27, "antenna": { "description": "16 dBi 43° Dish", "gain": 16, @@ -1067,8 +1067,8 @@ }, "0xe7f9": { "name": "Ubiquiti LiteBeam 5AC Gen2", - "maxpower": "24", - "pwroffset": "1", + "maxpower": 24, + "pwroffset": 1, "antenna": { "description": "23 dBi 15° Dish", "gain": 23, @@ -1077,8 +1077,8 @@ }, "0xe7fe": { "name": "Ubiquiti LiteBeam 5AC LR", - "maxpower": "24", - "pwroffset": "1", + "maxpower": 24, + "pwroffset": 1, "antenna": { "description": "26 dBi 7° Dish", "gain": 26, @@ -1087,8 +1087,8 @@ }, "0xe8f5": { "name": "Ubiquiti LiteBeam 5AC Gen2", - "maxpower": "24", - "pwroffset": "1", + "maxpower": 24, + "pwroffset": 1, "antenna": { "description": "23 dBi 15° Dish", "gain": 23, @@ -1097,8 +1097,8 @@ }, "0xe8e5": { "name": "Ubiquiti LiteAP 5AC", - "maxpower": "25", - "pwroffset": "1", + "maxpower": 25, + "pwroffset": 1, "antenna": { "description": "16 dBi 120° Sector", "gain": 16, @@ -1107,8 +1107,8 @@ }, "0xe805": { "name": "Ubiquiti NanoStation M5", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": { "description": "16 dBi 43° Panel", "gain": 16, @@ -1117,8 +1117,8 @@ }, "0xe825": { "name": "Ubiquiti NanoBeam M5 19", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "19 dBi 30° Panel", "gain": 19, @@ -1127,13 +1127,13 @@ }, "0xe835": { "name": "Ubiquiti AirGrid M5 XW", - "maxpower": "19", - "pwroffset": "6" + "maxpower": 19, + "pwroffset": 6 }, "0xe845": { "name": "Ubiquiti NanoStation Loco M5 XW", - "maxpower": "22", - "pwroffset": "1", + "maxpower": 22, + "pwroffset": 1, "antenna": { "description": "13 dBi 45° Panel", "gain": 13, @@ -1142,8 +1142,8 @@ }, "0xe855": { "name": "Ubiquiti NanoStation M5 XW", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": { "description": "16 dBi 43° Panel", "gain": 16, @@ -1153,8 +1153,8 @@ }, "0xe865": { "name": "Ubiquiti LiteBeam M5", - "maxpower": "19", - "pwroffset": "6", + "maxpower": 19, + "pwroffset": 6, "antenna": { "description": "23 dBi 15° Panel", "gain": 23, @@ -1163,8 +1163,8 @@ }, "0xe866": { "name": "Ubiquiti NanoStation M2 XW", - "maxpower": "22", - "pwroffset": "6", + "maxpower": 22, + "pwroffset": 6, "antenna": { "description": "11 dBi 55° Panel", "gain": 11, @@ -1173,8 +1173,8 @@ }, "0xe867": { "name": "Ubiquiti NanoStation Loco M2 XW", - "maxpower": "21", - "pwroffset": "2", + "maxpower": 21, + "pwroffset": 2, "antenna": { "description": "8.5 dBi 60° Panel", "gain": 8.5, @@ -1183,14 +1183,14 @@ }, "0xe868": { "name": "Ubiquiti Rocket M2 XW", - "maxpower": "21", - "pwroffset": "7", + "maxpower": 21, + "pwroffset": 7, "antenna": "external" }, "0xe885": { "name": "Ubiquiti PowerBeam M5 620 XW", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "29 dBi 6° Dish", "gain": 29, @@ -1199,8 +1199,8 @@ }, "0xe8a5": { "name": "Ubiquiti NanoStation Loco M5", - "maxpower": "22", - "pwroffset": "1", + "maxpower": 22, + "pwroffset": 1, "antenna": { "description": "13 dBi 45° Panel", "gain": 13, @@ -1209,14 +1209,14 @@ }, "0xe6b5": { "name": "Ubiquiti Rocket M5 XW", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": "external" }, "0xe812": { "name": "Ubiquiti NanoBeam M2 13", - "maxpower": "22", - "pwroffset": "6", + "maxpower": 22, + "pwroffset": 6, "antenna": { "description": "13 dBi 45° Panel", "gain": 13, @@ -1225,8 +1225,8 @@ }, "0xe815": { "name": "Ubiquiti NanoBeam M5 16", - "maxpower": "22", - "pwroffset": "4", + "maxpower": 22, + "pwroffset": 4, "antenna": { "description": "16 dBi 30° Panel", "gain": 16, @@ -1235,8 +1235,8 @@ }, "0xe1a5": { "name": "Ubiquiti PowerBridge M5", - "maxpower": "22", - "pwroffset": "5", + "maxpower": 22, + "pwroffset": 5, "antenna": { "description": "25 dBi 6° Panel", "gain": 25, diff --git a/files/etc/uci-defaults/10_dmz_mode_migrate b/files/etc/uci-defaults/10_dmz_mode_migrate deleted file mode 100644 index f8ab8451..00000000 --- a/files/etc/uci-defaults/10_dmz_mode_migrate +++ /dev/null @@ -1,11 +0,0 @@ -#! /bin/sh -if [ -e /etc/config/dmz-mode ] ; then - /sbin/uci -q -c /etc/config.mesh add aredn dmz - /sbin/uci -q -c /etc/config.mesh set aredn.@dmz[0].mode=$(cat /etc/config/dmz-mode) - /sbin/uci -q -c /etc/config.mesh commit aredn - rm -f /etc/config/dmz-mode -elif [ "$(/sbin/uci -q -c /etc/config.mesh get aredn.@dmz[0].mode)" = "" ]; then - /sbin/uci -q -c /etc/config.mesh add aredn dmz - /sbin/uci -q -c /etc/config.mesh set aredn.@dmz[0].mode=0 - /sbin/uci -q -c /etc/config.mesh commit aredn -fi diff --git a/files/etc/uci-defaults/45_aredn_reset_paths b/files/etc/uci-defaults/45_aredn_reset_paths deleted file mode 100755 index d95dbd6c..00000000 --- a/files/etc/uci-defaults/45_aredn_reset_paths +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/sh - -#check for modified path values and update if needed -#will not change existing custom entries -DISTRIB_TARGET=$(grep DISTRIB_TARGET /etc/openwrt_release|cut -d'=' -f2|tr -d "'") -DISTRIB_RELEASE=$(grep DISTRIB_RELEASE /etc/openwrt_release|cut -d'=' -f2|tr -d "'") -DISTRIB_ARCH=$(grep DISTRIB_ARCH /etc/openwrt_release|cut -d'=' -f2|tr -d "'") -SERVER_PREFIX='http://downloads.arednmesh.org/' -SNAPSHOT_PREFIX='snapshots/' - -getReleasePrefix() -{ - local PREFIX="" - ccount=`expr "${DISTRIB_RELEASE}" : '[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*'` - if [ "$ccount" -ne 0 ]; then - v1=$(echo ${DISTRIB_RELEASE} | cut -d'.' -f1) - v2=$(echo ${DISTRIB_RELEASE} | cut -d'.' -f2) - PREFIX="releases/${v1}/${v2}/${DISTRIB_RELEASE}/" - else - PREFIX="${SNAPSHOT_PREFIX}" - fi - echo $PREFIX -} - - -defaultPackageRepos() -{ - repo=$1 - - if [ "$repo" == "core" ]; then - echo "${SERVER_PREFIX}${BUILD_PREFIX}targets/${DISTRIB_TARGET}/packages" - else - echo "${SERVER_PREFIX}${BUILD_PREFIX}packages/${DISTRIB_ARCH}/${repo}" - fi -} - -checkPath() -{ - uciopt=$1 - repo=$2 - uci_val=$(/sbin/uci -c /etc/config.mesh get "aredn.@downloads[0].${uciopt}") - default_val=$(eval defaultPackageRepos "${repo}") - - # is the current value different than the default? - if [ "${uci_val}" != "${default_val}" ]; then - # does the non-standard value START with downloads.arednmesh.org? if so, change it, if NOT, leave it. - count=$(expr "${uci_val}" : "http:\\/\\/downloads.arednmesh.org\\/.*") - if [ "${count}" -gt 0 ]; then #starts with default server - /sbin/uci set "aredn.@downloads[0].${uciopt}=${default_val}" - fi - fi -} - -BUILD_PREFIX=$(eval getReleasePrefix) - -# set -checkPath pkgs_core core -checkPath pkgs_base base -checkPath pkgs_arednpackages arednpackages -checkPath pkgs_luci luci -checkPath pkgs_packages packages -checkPath pkgs_routing routing -checkPath pkgs_telephony telephony - -/sbin/uci commit aredn -cp /etc/config/aredn /etc/config.mesh diff --git a/files/etc/uci-defaults/80_aredn_lqm b/files/etc/uci-defaults/80_aredn_lqm deleted file mode 100755 index 849557d6..00000000 --- a/files/etc/uci-defaults/80_aredn_lqm +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -for c in /etc/config /etc/config.mesh -do - if [ "$(/sbin/uci -c $c -q get aredn.@lqm[0].enable)" = "" ]; then - /sbin/uci -c $c -m import aredn <<__EOF__ - config lqm - option enable '0' - option min_snr '15' - option margin_snr '1' - option rts_threshold '1' - option min_distance '0' - option max_distance '80467' - option min_quality '50' - option ping_penalty '5' - option margin_quality '1' -__EOF__ - /sbin/uci -c $c commit aredn - fi -done - -if [ "$(/sbin/uci -q get aredn.@lqm[0].min_quality)" = "50" ]; then - /sbin/uci -q set aredn.@lqm[0].min_quality=35 - /sbin/uci commit aredn - /sbin/uci -q -c /etc/config.mesh set aredn.@lqm[0].min_quality=35 - /sbin/uci -c /etc/config.mesh commit aredn -fi diff --git a/files/etc/uci-defaults/81_aredn_migrate_wansettings b/files/etc/uci-defaults/81_aredn_migrate_wansettings deleted file mode 100755 index ab12e159..00000000 --- a/files/etc/uci-defaults/81_aredn_migrate_wansettings +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -noroute=$(grep "lan_dhcp_noroute" /etc/config.mesh/_setup | sed s/^lan_dhcp_noroute\ =\ //) -olsrd_gw=$(grep "olsrd_gw" /etc/config.mesh/_setup | sed s/^olsrd_gw\ =\ //) - -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@wan[0])" != "wan" ]; then - /sbin/uci -c /etc/config.mesh -q add aredn wan -fi - -if [ "${noroute}" != "" ]; then - if [ "${noroute}" = "0" ]; then - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].lan_dhcp_route=1 - else - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].lan_dhcp_route=0 - fi - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].lan_dhcp_defaultroute=0 - /sbin/uci -c /etc/config.mesh commit aredn - sed -i /^lan_dhcp_noroute\ =/d /etc/config.mesh/_setup -elif [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@wan[0].lan_dhcp_route)" = "" ]; then - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].lan_dhcp_route=1 - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].lan_dhcp_defaultroute=0 - /sbin/uci -c /etc/config.mesh commit aredn -fi - -if [ "${olsrd_gw}" != "" ]; then - /sbin/uci -c /etc/config.mesh set aredn.@wan[0].olsrd_gw=${olsrd_gw} - /sbin/uci -c /etc/config.mesh commit aredn - sed -i /^olsrd_gw\ =/d /etc/config.mesh/_setup -fi diff --git a/files/etc/uci-defaults/82_aredn_ntp_period_migrate b/files/etc/uci-defaults/82_aredn_ntp_period_migrate deleted file mode 100644 index 40ddb97a..00000000 --- a/files/etc/uci-defaults/82_aredn_ntp_period_migrate +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# get current value of ntp_period if any -if [ -e /etc/config.mesh/_setup ] ; then - period=$(grep "ntp_period" /etc/config.mesh/_setup | sed s/^ntp_period\ =\ //) -fi - -# ensure /etc/config.mesh/aredn has ntp values -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@ntp[0])" != "ntp" ]; then - /sbin/uci -c /etc/config.mesh -q add aredn ntp - if [ -n "${period}" ] ; then - /sbin/uci -c /etc/config.mesh -q set aredn.@ntp[0].period="${period}" - else - /sbin/uci -c /etc/config.mesh -q set aredn.@ntp[0].period="daily" - fi - /sbin/uci -c /etc/config.mesh -q commit aredn -fi - diff --git a/files/etc/uci-defaults/90_aredn_default_tunnels b/files/etc/uci-defaults/90_aredn_default_tunnels deleted file mode 100755 index caa20971..00000000 --- a/files/etc/uci-defaults/90_aredn_default_tunnels +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@tunnel[0])" != "tunnel" ]; then - /sbin/uci -c /etc/config.mesh -q add aredn tunnel - /sbin/uci -c /etc/config.mesh -q commit aredn -fi - -# Default tunnel weight to 1 (perfect RF) -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@tunnel[0].weight)" = "" ]; then - /sbin/uci -c /etc/config.mesh -q set aredn.@tunnel[0].weight=1 - /sbin/uci -c /etc/config.mesh -q commit aredn -fi diff --git a/files/etc/uci-defaults/94_fix_wpad b/files/etc/uci-defaults/94_fix_wpad new file mode 100755 index 00000000..26973082 --- /dev/null +++ b/files/etc/uci-defaults/94_fix_wpad @@ -0,0 +1,6 @@ +#! /bin/sh +# Disable hostapd (etc) on devices without wifi + +if [ ! -d /sys/class/ieee80211 ]; then + /etc/init.d/wpad disable +fi diff --git a/files/etc/uci-defaults/95_aredn_migrate_leafletjs b/files/etc/uci-defaults/95_aredn_migrate_leafletjs deleted file mode 100755 index d4af01d4..00000000 --- a/files/etc/uci-defaults/95_aredn_migrate_leafletjs +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@map[0].leafletjs)" = "http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js" ]; then - /sbin/uci -c /etc/config.mesh -q set aredn.@map[0].leafletjs="http://unpkg.com/leaflet@0.7.7/dist/leaflet.js" - /sbin/uci -c /etc/config.mesh -q set aredn.@map[0].leafletcss="http://unpkg.com/leaflet@0.7.7/dist/leaflet.css" - /sbin/uci -c /etc/config.mesh -q commit aredn -fi diff --git a/files/etc/uci-defaults/95_lowmem_devices b/files/etc/uci-defaults/95_lowmem_devices new file mode 100755 index 00000000..6679f988 --- /dev/null +++ b/files/etc/uci-defaults/95_lowmem_devices @@ -0,0 +1,11 @@ +#!/bin/sh +# Modify configuration for low memory devices + +MEM=$(awk '/MemTotal/ {print $2}' /proc/meminfo) +if [ $MEM -le 32768 ]; then + # Reduce parallel requests + /sbin/uci -c /etc/config.mesh -q set uhttpd.main.max_requests=1 + /sbin/uci -c /etc/config.mesh -q commit uhttpd + # Disable wpad + /etc/init.d/wpad disable +fi diff --git a/files/etc/uci-defaults/96_aredn_migrate_latlon b/files/etc/uci-defaults/96_aredn_migrate_latlon deleted file mode 100755 index d2504fe0..00000000 --- a/files/etc/uci-defaults/96_aredn_migrate_latlon +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -if [ "$(/sbin/uci -c /etc/config.mesh -q get aredn.@location[0])" = "" ]; then - /sbin/uci -c /etc/config.mesh add aredn location - /sbin/uci -c /etc/config.mesh commit aredn -fi - -# read /etc/latlon -if [ -f /etc/latlon ] -then - LAT=$(head -1 /etc/latlon) - LON=$(tail -1 /etc/latlon) - /sbin/uci -c /etc/config.mesh -q set aredn.@location[0].lat="$LAT" - /sbin/uci -c /etc/config.mesh -q set aredn.@location[0].lon="$LON" - /sbin/uci -c /etc/config.mesh -q commit aredn - rm -f /etc/latlon -fi - -if [ -f /etc/gridsquare ] -then - GRIDSQUARE=$(head -1 /etc/gridsquare) - /sbin/uci -c /etc/config.mesh -q set aredn.@location[0].gridsquare="$GRIDSQUARE" - /sbin/uci -c /etc/config.mesh -q commit aredn - rm -f /etc/gridsquare -fi - -cp /etc/config.mesh/aredn /etc/config/aredn diff --git a/files/etc/uci-defaults/97_aredn_setup_meshstatus b/files/etc/uci-defaults/97_aredn_setup_meshstatus deleted file mode 100644 index a337ceb7..00000000 --- a/files/etc/uci-defaults/97_aredn_setup_meshstatus +++ /dev/null @@ -1,5 +0,0 @@ -#! /bin/sh -if [ "$(/sbin/uci -q get aredn.\@meshstatus[0])" = "" ]; then - /sbin/uci -q add aredn meshstatus - /sbin/uci -q commit aredn -fi diff --git a/files/etc/uci-defaults/98_change_default_maptiles b/files/etc/uci-defaults/98_change_default_maptiles deleted file mode 100755 index 1125a15b..00000000 --- a/files/etc/uci-defaults/98_change_default_maptiles +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -#check for old default map tiles and change to the new map tiles if req'd -#will not change existing custom entries. -OLDTILES_1='http://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiazVkbHEiLCJhIjoiY2lqMnlieTM4MDAyNXUwa3A2eHMxdXE3MiJ9.BRFvx4q2vi70z5Uu2zRYQw' -OLDTILES_2='http://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg' -NEWTILES='http://tile.openstreetmap.org/{z}/{x}/{y}.png' -MAPTILESERVER=$(/sbin/uci -c /etc/config.mesh get aredn.@map[0].maptiles) -if [ "$MAPTILESERVER" = "$OLDTILES_1" -o "$MAPTILESERVER" = "$OLDTILES_2" ]; then - /sbin/uci -c /etc/config.mesh set aredn.@map[0].maptiles="$NEWTILES" - /sbin/uci -c /etc/config.mesh commit aredn -fi -exit 0 diff --git a/files/etc/uci-defaults/99_setup_aredn_include b/files/etc/uci-defaults/98_setup_aredn_include similarity index 100% rename from files/etc/uci-defaults/99_setup_aredn_include rename to files/etc/uci-defaults/98_setup_aredn_include diff --git a/files/etc/uci-defaults/99_canonical_config b/files/etc/uci-defaults/99_canonical_config new file mode 100755 index 00000000..5c9c090b --- /dev/null +++ b/files/etc/uci-defaults/99_canonical_config @@ -0,0 +1,191 @@ +#!/bin/sh +true <<'LICENSE' + 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(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. + +LICENSE + +cat > /tmp/canonical_config << __EOF__ + +require("nixio") +require("uci") +require("aredn.hardware") + +local root = "/etc/config.mesh" +local DELETE = "__DELETE__" + +function shell(cmd) + local h = io.popen(cmd) + local r = h:read("*a") + h:close() + return r ~= "" and r:gsub("^%s+", ""):gsub("%s+$", "") or null +end + +function vtun_network() + local mac = aredn.hardware.get_interface_mac("eth0") + local a, b = mac:match("^..:..:..:..:(..):(..)$") + return "172.31." .. "" .. tonumber(b, 16) .. "." .. ((tonumber(a, 16) * 4) % 256) +end + +local config = { + aredn = { + alerts = {}, + dmz = { + mode = shell("cat /etc/config/dmz-mode 2>/dev/null; rm -f /etc/config/dmz-mode") or 0 + }, + downloads = { + firmware_aredn = "http://downloads.arednmesh.org", + firmwarepath = DELETE, + packages_default = "http://downloads.arednmesh.org", + pkgs_core = DELETE, + pkgs_base = DELETE, + pkgs_arednpackages = DELETE, + pkgs_packages = DELETE, + pkgs_luci = DELETE, + pkgs_routing = DELETE, + pkgs_telephony = DELETE + }, + iperf = { + enable = 1 + }, + location = { + lat = shell("head -1 /etc/latlon 2>/dev/null"), + lon = shell("tail -1 /etc/latlon 2>/dev/null; rm -f /etc/latlon"), + gridsquare = shell("head -1 /etc/gridsquare 2>/dev/null ; rm -f /etc/gridsquare"), + map = "https://worldmap.arednmesh.org/#12/(lat)/(lon)", + gps_enable = 1 + }, + lqm = { + enable = 1, + min_snr = 15, + margin_snr = 1, + rts_threshold = 1, + min_distance = 0, + max_distance = 80467, + min_quality = 35, + ping_penalty = 5, + margin_quality = 1, + min_routes = 8 + }, + map = DELETE, + meshstatus = DELETE, + notes = {}, + ntp = { + period = "daily", + }, + poe = {}, + remotelog = {}, + supernode = { + enable = 0, + support = 1 + }, + time = { + ntp_enable = 1, + gps_enable = 1 + }, + tunnel = { + weight = 1 + }, + usb = {}, + wan = { + lan_dhcp_route = 1, + lan_dhcp_defaultroute = 0, + olsrd_gw = 0, + ssh_access = 1, + telnet_access = 1, + web_access = 1 + }, + watchdog = { + enable = 0, + } + }, + dhcp = {}, + dropbear = {}, + olsrd = {}, + snmpd = {}, + uhttpd = {}, + vtun = { + defaults = {}, + options = {}, + network = { + start = vtun_network() + } + }, + wireguard = {}, + xlink = {} +} + +local cursor = uci.cursor(root) +for fn, f in pairs(config) +do + if not nixio.fs.stat(root .. "/" .. fn) then + io.open(root .. "/" .. fn, "w"):close() + else + cursor:add(fn, "__dummy__") + cursor:delete(fn, "@__dummy__[0]") + for k, v in pairs(cursor:get_all(fn)) + do + local c = 0 + for k1, _ in pairs(v) + do + if not k1:match("^%.") then + c = c + 1 + end + end + if c == 0 then + cursor:delete(fn, k) + end + end + end + for sn, s in pairs(f) + do + local san = "@" .. sn .. "[0]" + if s == DELETE then + cursor:delete(fn, san) + else + if not cursor:get(fn, san) then + cursor:add(fn, sn) + end + for on, o in pairs(s) + do + if o == DELETE then + cursor:delete(fn, san, on) + elseif not cursor:get(fn, san, on) then + cursor:set(fn, san, on, o) + end + end + end + end + cursor:commit(fn) +end +__EOF__ +/usr/bin/lua /tmp/canonical_config +rm -f /tmp/canonical_config diff --git a/files/usr/lib/lua/aredn/hardware.lua b/files/usr/lib/lua/aredn/hardware.lua index bbe7ddf9..6d5dc6c1 100644 --- a/files/usr/lib/lua/aredn/hardware.lua +++ b/files/usr/lib/lua/aredn/hardware.lua @@ -494,6 +494,105 @@ function hardware.get_interface_mac(intf) return mac end +local gpsd = "/usr/sbin/gpsd"; +local gps_ttys = { + "/dev/ttyACM0", + "/dev/ttyUSB0" +} + +function hardware.gps_find() + if nixio.fs.stat(gpsd) then + for _, tty in ipairs(gps_ttys) + do + if nixio.fs.stat(tty) then + return tty + end + end + end + 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.type == "DtD" and tracker.ip then + local s = nixio.socket("inet", "stream") + s:setopt("socket", "sndtimeo", 1) + local r = s:connect(tracker.ip, 2947) + s:close() + if r then + return tracker.ip + end + end + end + end + return nil +end + +local function gps_open(gps) + if gps:match("^/dev/") then + gps = "127.0.0.1" + end + local s = nixio.socket("inet", "stream") + local r = s:connect(gps, 2947) + if not r then + return nil + end + return s +end + +local function gps_read(s) + local l = "" + while true + do + local b = s:read(1) + if #b == 0 then + return nil + elseif b == "\n" then + local ok, jsn = pcall(function() return luci.jsonc.parse(l) end) + if ok then + return jsn + end + l = "" + else + l = l .. b + end + end +end + +local function gps_close(s) + s:close() +end + +function hardware.gps_read_llt(gps, maxlines) + local info = { + lat = nil, + lon = nil, + time = nil + } + local s = gps_open(gps) + if not s then + return nil + end + s:write('?WATCH={"enable":true,"json":true}\n') + if not maxlines then + maxlines = 10 + end + while maxlines > 0 + do + local j = gps_read(s) + if j.class == "TPV" then + info.time = j.time:gsub("T", " "):gsub(".000Z", "") + info.lat = j.lat + info.lon = j.lon + break + end + maxlines = maxlines - 1 + end + gps_close(s) + return info +end + if not aredn then aredn = {} end diff --git a/files/usr/local/bin/mgr/gps.lua b/files/usr/local/bin/mgr/gps.lua new file mode 100755 index 00000000..0c8a48f1 --- /dev/null +++ b/files/usr/local/bin/mgr/gps.lua @@ -0,0 +1,120 @@ +--[[ + + 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 + +--]] + +local app = {} +local CONFIG0 = "/etc/config.mesh/gpsd" +local CONFIG1 = "/etc/config/gpsd" +local CHANGEMARGIN = 0.0001 + +function app.run() + + wait_for_ticks(60) + + local gps + while true + do + gps = aredn.hardware.gps_find() + if gps then + break + end + wait_for_ticks(600) -- 10 minutes + end + + -- Create the GPSD daemon if device is local, + -- otherwise we get the GPS info from another node on our local network + if gps:match("^/dev/") then + local f = io.open(CONFIG0, "w") + f:write( +[[config gpsd 'core' + option enabled '1' + option device ']] .. gps .. [[' + option port '2947' + option listen_globally '1' +]]) + f:close() + filecopy(CONFIG0, CONFIG1, true) + os.execute("nft insert rule ip fw4 input_dtdlink tcp dport 2947 accept comment \"gpsd\" 2> /dev/null") + os.execute("/etc/init.d/gpsd restart") + end + + while true + do + local c = uci.cursor() + local j = aredn.hardware.gps_read_llt(gps) + + -- Update time and date + if c:get("aredn", "@time[0]", "gps_enable") == "1" and j.time then + os.execute("/bin/date -u -s '" .. j.time .. "' > /dev/nul 2>&1") + write_all("/tmp/timesync", "gps") + end + + -- Set location if significantly changed + if c:get("aredn", "@location[0]", "gps_enable") == "1" then + local clat = tonumber(c:get("aredn", "@location[0]", "lat") or 0) + local clon = tonumber(c:get("aredn", "@location[0]", "lon") or 0) + if math.abs(clat - j.lat) > CHANGEMARGIN or math.abs(clon - j.lon) > CHANGEMARGIN then + -- Calculate gridsquare from lat/lon + local alat = j.lat + 90 + local flat = 65 + math.floor(alat / 10) + local slat = math.floor(alat % 10) + local ulat = 97 + math.floor((alat - math.floor(alat)) * 60 / 2.5) + + local alon = j.lon + 180 + local flon = 65 + math.floor(alon / 20) + local slon = math.floor((alon / 2) % 10) + local ulon = 97 + math.floor((alon - 2 * math.floor(alon / 2)) * 60 / 5) + + local gridsquare = string.format("%c%c%d%d%c%c", flon, flat, slon, slat, ulon, ulat) + + -- Update location information + c:set("aredn", "@location[0]", "lat", j.lat) + c:set("aredn", "@location[0]", "lon", j.lon) + c:set("aredn", "@location[0]", "gridsquare", gridsquare) + c:set("aredn", "@location[0]", "source", "gps") + c:commit("aredn") + local cm = uci.cursor("/etc/config.mesh") + cm:set("aredn", "@location[0]", "lat", j.lat) + cm:set("aredn", "@location[0]", "lon", j.lon) + cm:set("aredn", "@location[0]", "gridsquare", gridsquare) + cm:set("aredn", "@location[0]", "source", "gps") + cm:commit("aredn") + end + end + + wait_for_ticks(600) -- 10 minutes + end +end + +return app.run diff --git a/files/usr/local/bin/mgr/lqm.lua b/files/usr/local/bin/mgr/lqm.lua index 3364c1a9..15c7ee15 100755 --- a/files/usr/local/bin/mgr/lqm.lua +++ b/files/usr/local/bin/mgr/lqm.lua @@ -529,6 +529,7 @@ function lqm() lat = nil, lon = nil, distance = nil, + localarea = nil, blocks = { dtd = false, signal = false, @@ -623,8 +624,8 @@ function lqm() -- Refresh the hostname periodically as it can change track.hostname = canonical_hostname(nixio.getnameinfo(track.ip)) or track.hostname - if track.blocked or not track.routable then - -- Remote is blocked not directly routable + if track.blocked then + -- Remote is blocked -- We cannot update so invalidate any information considered stale and set time to attempt refresh track.refresh = is_pending(track) and 0 or now + refresh_retry_timeout track.rev_snr = nil @@ -652,8 +653,17 @@ function lqm() track.lon = tonumber(info.lon) or track.lon if track.lat and track.lon and lat and lon then track.distance = calc_distance(lat, lon, track.lat, track.lon) + if track.type == "DtD" and track.distance < dtd_distance then + track.localarea = true + else + track.localarea = false + end end + -- Keep some useful info + track.model = info.node_details.model + track.firmware_version = info.node_details.firmware_version + if track.type == "RF" then rflinks[track.mac] = nil if info.lqm and info.lqm.enabled and info.lqm.info and info.lqm.info.trackers then diff --git a/files/usr/local/bin/mgr/snrlog.lua b/files/usr/local/bin/mgr/snrlog.lua index 9fa226a1..4eb9de29 100644 --- a/files/usr/local/bin/mgr/snrlog.lua +++ b/files/usr/local/bin/mgr/snrlog.lua @@ -47,7 +47,6 @@ local AGETIME = 43200 local INACTIVETIMEOUT = 10000 local tmpdir = "/tmp/snrlog" local lastdat = "/tmp/snr.dat" -local autolog = "/tmp/AutoDistReset.log" local defnoise = -95 local cursor = uci.cursor() @@ -124,9 +123,9 @@ function run_snrlog() local arp = arpcache[mac] if arp then local ip = arp["IP address"] - local hostname = nslookup(ip) + local hostname = nixio.getnameinfo(ip) if hostname then - datafile = datafile..hostname:lower() + datafile = datafile..hostname:lower():gsub("^dtdlink%.", ""):gsub("^mid%d+%.", ""):gsub("^xlink%d+%.", ""):gsub("%.local%.mesh$", "") elseif ip then datafile = datafile..ip end @@ -230,12 +229,7 @@ function run_snrlog() -- trigger auto distancing if necessary if trigger_auto_distance and cursor:get("aredn", "@lqm[0]", "enable") ~= "1" then reset_auto_distance() - file_trim(autolog, MAXLINES) - f, err = assert(io.open(autolog, "a"),"Cannot open file (autolog) to write!") - if f then - f:write(now .. "\n") - f:close() - end + nixio.syslog("notice", "snrlog: reset_auto_distance, " .. now) end end diff --git a/files/usr/local/bin/node-setup b/files/usr/local/bin/node-setup index e174a17a..efea100b 100755 --- a/files/usr/local/bin/node-setup +++ b/files/usr/local/bin/node-setup @@ -117,7 +117,7 @@ local cfg = { olsrd_dtd_interface_mode = "ether", tun_network_config = "", wireguard_network_config = "", - dtdlink_interfaces = "list network 'dtdlink'", + dtdlink_interfaces = "\tlist network 'dtdlink'", vpn_interfaces = "", olsrd_pollrate = "0.05", tun_devices_config = "", @@ -137,7 +137,8 @@ local changes = { firewall = false, tunnels = false, wireless = false, - localservices = false + localservices = false, + wpad = false } function valid_config(config) @@ -149,6 +150,16 @@ function valid_config(config) return r == 0 and true or false end +function is_nat_mode() + return is_null(cfg.dmz_mode) +end +function is_dmz_mode() + return is_notnull(cfg.dmz_mode) and cfg.dmz_mode ~= "1" +end +function is_altnet_mode() + return cfg.dmz_mode == "1" +end + function expand_vars(lines) local nlines = {} for line in lines:gmatch("([^\n]*\n?)") @@ -228,7 +239,7 @@ if cfg.wan_proto == "dhcp" then deleteme.wan_gw = true deleteme.wan_mask = true end -if is_notnull(cfg.dmz_mode) or cfg.wan_proto ~= "disabled" then +if is_dmz_mode() or is_altnet_mode() or cfg.wan_proto ~= "disabled" then deleteme.lan_gw = true end @@ -285,7 +296,7 @@ if is_null(cfg.dmz_mode) then end -- switch to dmz values if needed -if is_notnull(cfg.dmz_mode) then +if is_dmz_mode() then cfg.lan_ip = cfg.dmz_lan_ip cfg.lan_mask = cfg.dmz_lan_mask cfg.dhcp_start = cfg.dmz_dhcp_start @@ -300,7 +311,7 @@ local dhcptagsfile = "/etc/config.mesh/_setup.dhcptags" local dhcpoptionsfile = "/etc/config.mesh/_setup.dhcpoptions" local aliasfile = "/etc/config.mesh/aliases" local servfile = "/etc/config.mesh/_setup.services" -if is_null(cfg.dmz_mode) then +if is_nat_mode() then portfile = portfile .. ".nat" dhcpfile = dhcpfile .. ".nat" dhcptagsfile = dhcptagsfile .. ".nat" @@ -529,12 +540,23 @@ if tun_start or tun_dns then end cfg.tun_network_config = cfg.tun_network_config .. "\n" end +local def_tun_weight = tonumber(cm:get("aredn", "@tunnel[0]", "weight") or 1) or 0 +local is_supernode = cm:get("aredn", "@supernode[0]", "enable") == "1" +if is_supernode then + def_tun_weight = 0 +end +local tun_weights = {} local vtunclients = 0 cm:foreach("vtun", "client", function(s) if s.enabled == "1" then cfg.tun_network_config = cfg.tun_network_config .. string.format("config client\n\toption enabled '1'\n\toption node '%s'\n\toption passwd '%s'\n\toption clientip '%s'\n\toption serverip '%s'\n\toption netip '%s'\n\n", s.node:upper(), s.passwd, s.clientip, s.serverip, s.netip) + local w = s.weight or def_tun_weight + if not tun_weights[w] then + tun_weights[w] = {} + end + table.insert(tun_weights[w], string.format("tun%d", 50 + vtunclients)) vtunclients = vtunclients + 1 end end @@ -551,12 +573,18 @@ cm:foreach("wireguard", "client", cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config wireguard_wgc%d\n\toption public_key '%s'\n\toption persistent_keepalive '25'\n\tlist allowed_ips '0.0.0.0/0'\n\n", wgclients, client_pub) + local w = s.weight or def_tun_weight + if not tun_weights[w] then + tun_weights[w] = {} + end + table.insert(tun_weights[w], string.format("wgc%d", wgclients)) wgclients = wgclients + 1 end end ) local vtunservers = 0 local wgservers = 0 +local vtunclients_roundup = 10 * math.ceil(vtunclients / 10) cm:foreach("vtun", "server", function(s) if s.enabled == "1" then @@ -570,11 +598,21 @@ cm:foreach("vtun", "server", cfg.wireguard_network_config = cfg.wireguard_network_config .. string.format("config wireguard_wgs%d\n\toption public_key '%s'\n\toption endpoint_host '%s'\n\toption endpoint_port '%s'\n\toption persistent_keepalive '25'\n\tlist allowed_ips '0.0.0.0/0'\n\n", wgservers, server_pub, s.host, p) + local w = s.weight or def_tun_weight + if not tun_weights[w] then + tun_weights[w] = {} + end + table.insert(tun_weights[w], string.format("wgs%d", wgservers)) wgservers = wgservers + 1 else cfg.tun_network_config = cfg.tun_network_config .. string.format("config server\n\toption enabled '1'\n\toption host '%s'\n\toption node '%s'\n\toption passwd '%s'\n\toption clientip '%s'\n\toption serverip '%s'\n\toption netip '%s'\n\n", s.host, s.node:upper(), s.passwd, s.clientip, s.serverip, s.netip) + local w = s.weight or def_tun_weight + if not tun_weights[w] then + tun_weights[w] = {} + end + table.insert(tun_weights[w], string.format("tun%d", 50 + vtunclients_roundup + vtunservers)) vtunservers = vtunservers + 1 end end @@ -606,15 +644,15 @@ if maxclients + maxservers + wgclients + wgservers > 0 then vpnzone = true for i = 50, 50 + maxclients + maxservers - 1 do - cfg.vpn_interfaces = cfg.vpn_interfaces .. " list network 'tun" .. i .. "'\n" + cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'tun" .. i .. "'\n" end for i = 0, wgclients-1 do - cfg.vpn_interfaces = cfg.vpn_interfaces .. " list network 'wgc" .. i .. "'\n" + cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'wgc" .. i .. "'\n" end for i = 0, wgservers-1 do - cfg.vpn_interfaces = cfg.vpn_interfaces .. " list network 'wgs" .. i .. "'\n" + cfg.vpn_interfaces = cfg.vpn_interfaces .. "\tlist network 'wgs" .. i .. "'\n" end end @@ -622,7 +660,7 @@ end if nixio.fs.stat("/etc/config.mesh/xlink") then uci.cursor("/etc/config.mesh"):foreach("xlink", "interface", function(section) - cfg.dtdlink_interfaces = cfg.dtdlink_interfaces .. "\n list network '" .. section[".name"] .. "'" + cfg.dtdlink_interfaces = cfg.dtdlink_interfaces .. "\n\tlist network '" .. section[".name"] .. "'" end ) end @@ -652,7 +690,7 @@ local nc = uci.cursor("/tmp/new_config") -- append to firewall local fw = io.open("/tmp/new_config/firewall", "a") if fw then - if is_notnull(cfg.dmz_mode) then + if not is_nat_mode() then fw:write("\nconfig forwarding\n option src wifi\n option dest lan\n") fw:write("\nconfig forwarding\n option src dtdlink\n option dest lan\n") if vpnzone then @@ -673,7 +711,7 @@ if fw then do if not (line:match("^%s*#") or line:match("^%s*$")) then local dip = line:match("dmz_ip = (%w+)") - if dip and cfg.dmz_mode ~= 0 then + if dip and is_dmz_mode() then fw:write("\nconfig redirect\n option src wifi\n option proto tcp\n option src_dip " .. cfg.wifi_ip .. "\n option dest_ip " .. dip .. "\n") fw:write("\nconfig redirect\n option src wifi\n option proto udp\n option src_dip " .. cfg.wifi_ip .. "\n option dest_ip " .. dip .. "\n") else @@ -691,7 +729,7 @@ if fw then if not oport:match("-") then host = host .. " option dest_port " .. iport .. "\n" end - if is_notnull(cfg.dmz_mode) and intf == "both" then + if is_dmz_mode() and intf == "both" then intf = "wan" end if intf == "both" then @@ -701,7 +739,7 @@ if fw then fw:write("\nconfig redirect\n option src vpn\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") end fw:write("config redirect\n option src wan\n option dest lan\n " .. match .. " " .. host .. "\n") - elseif intf == "wifi" and is_null(cfg.dmz_mode) then + elseif intf == "wifi" and is_nat_mode() then fw:write("\nconfig redirect\n option src dtdlink\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") fw:write("\nconfig redirect\n option src wifi\n option dest lan\n " .. match .. " option src_dip " .. cfg.wifi_ip .. "\n " .. host .. "\n") if vpnzone then @@ -721,7 +759,7 @@ if fw then end -- setup nat -if is_null(cfg.dmz_mode) then +if is_nat_mode() then -- zone[0] = lan, zone[1] = wan, zone[2] = wifi, zone[3] = dtdlink, zone[4] = vpn local masq_src = cfg.lan_ip .. "/" .. netmask_to_cidr(cfg.lan_mask) for z = 2, 4 @@ -999,7 +1037,7 @@ if h and e then if is_notnull(cfg.dtdlink_ip) then h:write(cfg.dtdlink_ip .. "\tdtdlink." .. node .. ".local.mesh dtdlink." .. node .."\n") end - if is_null(cfg.dmz_mode) then + if is_nat_mode() then h:write(decimal_to_ip(ip_to_decimal(cfg.lan_ip) + 1) .. "\tlocalap\n") end @@ -1084,10 +1122,14 @@ if nixio.fs.access("/etc/config.mesh/olsrd", "r") then of:write(line .. "\n") end - if is_notnull(cfg.dmz_mode) then + if is_dmz_mode() then local a, b, c, d = cfg.dmz_lan_ip:match("(.*)%.(.*)%.(.*)%.(.*)") of:write(string.format("\nconfig Hna4\n\toption netaddr %s.%s.%s.%d\n\toption netmask 255.255.255.%d\n\n", a, b, c, d - 1, nixio.bit.band(255 * 2 ^ cfg.dmz_mode, 255))) end + if is_altnet_mode() then + local a, b, c, d = cfg.lan_ip:match("(.*)%.(.*)%.(.*)%.(.*)") + of:write(string.format("\nconfig Hna4\n\toption netaddr %s.%s.%s.%d\n\toption netmask %s\n\n", a, b, c, d - 1, cfg.lan_mask)) + end if cfg.wifi_enable ~= "1" and is_notnull(cfg.wifi_ip) then of:write(string.format("config Hna4\n\toption netaddr %s\n\toption netmask 255.255.255.255\n\n", cfg.wifi_ip)) @@ -1095,6 +1137,11 @@ if nixio.fs.access("/etc/config.mesh/olsrd", "r") then if is_supernode then of:write("config Hna4\n\toption netaddr 10.0.0.0\n\toption netmask 255.0.0.0\n\n") + local altnetwork = nc:get("aredn", "@supernode[0]", "altnetwork") + local altnetmask = nc:get("aredn", "@supernode[0]", "altnetmask") + if altnetwork and altnetmask then + of:write("config Hna4\n\toption netaddr " .. altnetwork .. "\n\toption netmask " .. altnetmask .. "\n\n") + end end if nixio.fs.stat("/etc/config.mesh/xlink") then @@ -1133,39 +1180,25 @@ if nixio.fs.access("/etc/config.mesh/olsrd", "r") then -- add all the tunnel interfaces if vtunclients + vtunservers + wgclients + wgservers > 0 then - of:write("config Interface\n") - for dev = 50, 50 + vtunclients - 1 + for weight, ifaces in pairs(tun_weights) do - of:write("\tlist interface 'tun" .. dev .. "'\n") - end - for dev = 50 + maxclients, 50 + maxclients + vtunservers - 1 - do - of:write("\tlist interface 'tun" .. dev .. "'\n") - end - if wgclients > 0 then - for dev = 0, wgclients - 1 + of:write("\nconfig Interface\n") + for _, iface in ipairs(ifaces) do - of:write("\tlist interface 'wgc" .. dev .. "'\n") + of:write("\tlist interface '" .. iface .. "'\n") end - end - if wgservers > 0 then - for dev = 0, wgservers - 1 - do - of:write("\tlist interface 'wgs" .. dev .. "'\n") + of:write("\toption Ip4Broadcast '255.255.255.255'\n") + weight = tonumber(weight) + if weight < 1 then + of:write("\toption Mode 'ether'\n") + elseif weight > 1 then + of:write("\toption LinkQualityMult 'default " .. (1 / weight) .. "'\n") end + of:write("\toption HelloInterval '" .. cfg.hello_interval .. "'\n") + of:write("\toption TcInterval '" .. cfg.tc_interval .. "'\n") + of:write("\toption MidInterval '" .. cfg.mid_interval .. "'\n") + of:write("\toption HnaInterval '" .. cfg.hna_interval .. "'\n") end - of:write("\toption Ip4Broadcast '255.255.255.255'\n") - local tun_weight = tonumber(nc:get("aredn", "@tunnel[0]", "weight") or 1) - local is_supernode = nc:get("aredn", "@supernode[0]", "enable") == "1" - if not tun_weight or tun_weight < 1 or is_supernode then - of:write("\toption Mode 'ether'\n") - elseif tun_weight > 1 then - of:write("\toption LinkQualityMult 'default " .. (1 / tun_weight) .. "'\n") - end - of:write("\toption HelloInterval '" .. cfg.hello_interval .. "'\n") - of:write("\toption TcInterval '" .. cfg.tc_interval .. "'\n") - of:write("\toption MidInterval '" .. cfg.mid_interval .. "'\n") - of:write("\toption HnaInterval '" .. cfg.hna_interval .. "'\n") end nc:set("aredn", "@tunnel[0]", "maxclients", maxclients) nc:set("aredn", "@tunnel[0]", "maxservers", maxservers) @@ -1199,6 +1232,12 @@ if nixio.fs.access("/etc/config.mesh/olsrd", "r") then ) end + -- OLSRD user extras + if nixio.fs.stat("/etc/aredn_include/olsrd.user") then + of:write("\n") + of:write(expand_vars(read_all("/etc/aredn_include/olsrd.user"))) + end + of:close() end end @@ -1244,6 +1283,7 @@ end -- Handle special cases local config_special = { + dmz_mode = c:get("aredn", "@dmz[0]", "mode"), lqm_enable = c:get("aredn", "@lqm[0]", "enable"), tunnel_weight = c:get("aredn", "@tunnel[0]", "weight"), supernode_enable = c:get("aredn", "@supernode[0]", "enable"), @@ -1279,6 +1319,9 @@ do changes.system = true elseif file == "aredn" then local oc = uci:cursor() + if oc:get("aredn", "@dmz[0]", "mode") ~= config_special.dmz_mode then + changes.reboot = true + end if oc:get("aredn", "@lqm[0]", "enable") ~= config_special.lqm_enable then changes.manager = true end @@ -1312,6 +1355,13 @@ do elseif file == "wireless" then local oc = uci:cursor() if oc:get("wireless", "@wifi-iface[0]", "mode") ~= config_special.wifi_mode_0 or oc:get("wireless", "@wifi-iface[1]", "mode") ~= config_special.wifi_mode_1 then + -- Only start the hostapd (etc) if we need to. This doesn't change what is currently running + -- only what automatically runs in the future + if oc:get("wireless", "@wifi-iface[0]", "mode") == "ap" or oc:get("wireless", "@wifi-iface[1]", "mode") == "ap" then + os.execute("/etc/init.d/wpad enable > /dev/null 2>&1") + else + os.execute("/etc/init.d/wpad disable > /dev/null 2>&1") + end changes.reboot = true else changes.wireless = true diff --git a/files/usr/share/ucode/aredn/configuration.uc b/files/usr/share/ucode/aredn/configuration.uc new file mode 100755 index 00000000..fb5e135d --- /dev/null +++ b/files/usr/share/ucode/aredn/configuration.uc @@ -0,0 +1,401 @@ +/* + * 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 + */ + +import * as fs from "fs"; +import * as uci from "uci"; +import * as math from "math"; +import * as network from "aredn.network"; + +let cursor; +let setup; +let setupKeys; +let setupChanged = false; +let firmwareVersion = null; + +const currentConfig = "/tmp/config.current"; +const modalConfig = "/tmp/config.modal"; +const configDirs = [ + "/etc", + "/etc/config.mesh", + "/etc/local", + "/etc/local/uci", + "/etc/aredn_include", + "/etc/dropbear", + "/tmp" +]; +const configFiles = [ + "/etc/config.mesh/_setup", + "/etc/config.mesh/_setup.dhcp.dmz", + "/etc/config.mesh/_setup.dhcp.nat", + "/etc/config.mesh/_setup.dhcpoptions.dmz", + "/etc/config.mesh/_setup.dhcpoptions.nat", + "/etc/config.mesh/_setup.dhcptags.dmz", + "/etc/config.mesh/_setup.dhcptags.nat", + "/etc/config.mesh/_setup.ports.dmz", + "/etc/config.mesh/_setup.ports.nat", + "/etc/config.mesh/_setup.services.dmz", + "/etc/config.mesh/_setup.services.nat", + "/etc/config.mesh/aliases.dmz", + "/etc/config.mesh/aliases.nat", + "/etc/config.mesh/aredn", + "/etc/config.mesh/dhcp", + "/etc/config.mesh/dropbear", + "/etc/config.mesh/firewall", + "/etc/config.mesh/firewall.user", + "/etc/config.mesh/network", + "/etc/config.mesh/olsrd", + "/etc/config.mesh/snmpd", + "/etc/config.mesh/system", + "/etc/config.mesh/uhttpd", + "/etc/config.mesh/vtun", + "/etc/config.mesh/wireguard", + "/etc/config.mesh/xlink", + "/etc/local/uci/hsmmmesh", + "/etc/aredn_include/dtdlink.network.user", + "/etc/aredn_include/lan.network.user", + "/etc/aredn_include/wan.network.user", + "/etc/dropbear/authorized_keys", + "/tmp/newpassword" +]; + +function initCursor() +{ + if (!cursor) { + cursor = uci.cursor("/etc/local/uci"); + } +}; + +function initSetup() +{ + if (!setup) { + setup = {}; + setupKeys = []; + const f = fs.open("/etc/config.mesh/_setup"); + if (f) { + for (;;) { + const line = f.read("line"); + if (!length(line)) { + break; + } + const kv = split(line, " ="); + if (length(kv) === 2) { + setup[kv[0]] = trim(kv[1]); + push(setupKeys, kv[0]); + } + } + f.close(); + } + } +}; + +export function reset() +{ + setup = null; + cursor = null; +}; + +export function getSettingAsString(key, def) +{ + initSetup(); + return setup[key] || def; +}; + +export function getSettingAsInt(key, def) +{ + initSetup(); + const v = int(setup[key]); + if (type(v) === "int") { + return v; + } + return def; +}; + +export function setSetting(key, value, def) +{ + initSetup(); + const old = setup[key]; + setup[key] = `${value || def || ""}`; + if (old !== setup[key]) { + setupChanged = true; + return true; + } + return false; +}; + +export function saveSettings() +{ + if (setupChanged) { + const f = fs.open("/etc/config.mesh/_setup", "w"); + if (f) { + for (let i = 0; i < length(setupKeys); i++) { + const k = setupKeys[i]; + f.write(`${k} = ${setup[k] || ""}\n`); + } + f.close(); + setupChanged = false; + } + } +}; + +export function getName() +{ + initCursor(); + return cursor.get("hsmmmesh", "settings", "node"); +}; + +export function setName(name) +{ + initCursor(); + cursor.set("hsmmmesh", "settings", "node", name); + cursor.commit("hsmmmesh"); +}; + +export function getFirmwareVersion() +{ + if (firmwareVersion === null) { + firmwareVersion = trim(fs.readfile("/etc/mesh-release")); + } + return firmwareVersion; +}; + +export function setUpgrade(v) +{ + initCursor(); + cursor.set("hsmmmesh", "settings", "nodeupgraded", v); + cursor.commit("hsmmmesh"); +}; + +export function setPassword(passwd) +{ + fs.writefile("/tmp/newpassword", passwd); +}; + +export function isPasswordChanged() +{ + return fs.access("/tmp/newpassword") ? true : false; +}; + +export function getDHCP(mode) +{ + initSetup(); + if (mode === "nat" || (!mode && setup.dmz_mode === "0")) { + const root = replace(setup.lan_ip, /\d+$/, ""); + return { + enabled: setup.lan_dhcp ? true : false, + mode: 0, + start: `${root}${setup.dhcp_start}`, + end: `${root}${setup.dhcp_end}`, + gateway: setup.lan_ip, + mask: setup.lan_mask, + cidr: network.netmaskToCIDR(setup.lan_mask), + leases: "/tmp/dhcp.leases", + reservations: "/etc/config.mesh/_setup.dhcp.nat", + services: "/etc/config.mesh/_setup.services.nat", + ports: "/etc/config.mesh/_setup.ports.nat", + dhcptags: "/etc/config.mesh/_setup.dhcptags.nat", + dhcpoptions: "/etc/config.mesh/_setup.dhcpoptions.nat", + aliases: "/etc/config.mesh/aliases.nat" + }; + } + else if (setup.dmz_mode === "1") { + const root = replace(setup.lan_ip, /\d+$/, ""); + return { + enabled: setup.lan_dhcp ? true : false, + mode: 1, + start: `${root}${setup.dhcp_start}`, + end: `${root}${setup.dhcp_end}`, + gateway: setup.lan_ip, + mask: setup.lan_mask, + cidr: network.netmaskToCIDR(setup.lan_mask), + leases: "/tmp/dhcp.leases", + reservations: "/etc/config.mesh/_setup.dhcp.dmz", + services: "/etc/config.mesh/_setup.services.dmz", + ports: "/etc/config.mesh/_setup.ports.dmz", + dhcptags: "/etc/config.mesh/_setup.dhcptags.dmz", + dhcpoptions: "/etc/config.mesh/_setup.dhcpoptions.dmz", + aliases: "/etc/config.mesh/aliases.dmz" + }; + } + else { + const root = replace(setup.dmz_lan_ip, /\d+$/, ""); + return { + enabled: setup.lan_dhcp ? true : false, + mode: int(setup.dmz_mode), + start: `${root}${setup.dmz_dhcp_start}`, + end: `${root}${setup.dmz_dhcp_end}`, + gateway: setup.dmz_lan_ip, + mask: setup.dmz_lan_mask, + cidr: network.netmaskToCIDR(setup.dmz_lan_mask), + leases: "/tmp/dhcp.leases", + reservations: "/etc/config.mesh/_setup.dhcp.dmz", + services: "/etc/config.mesh/_setup.services.dmz", + ports: "/etc/config.mesh/_setup.ports.dmz", + dhcptags: "/etc/config.mesh/_setup.dhcptags.dmz", + dhcpoptions: "/etc/config.mesh/_setup.dhcpoptions.dmz", + aliases: "/etc/config.mesh/aliases.dmz" + }; + } +}; + +function copyConfig(configRoot) +{ + fs.mkdir(configRoot); + for (let i = 0; i < length(configDirs); i++) { + fs.mkdir(`${configRoot}${configDirs[i]}`); + } + for (let i = 0; i < length(configFiles); i++) { + const entry = configFiles[i]; + if (fs.access(entry)) { + fs.writefile(`${configRoot}${entry}`, fs.readfile(entry)); + } + } +}; + +function removeConfig(configRoot) +{ + for (let i = 0; i < length(configFiles); i++) { + fs.unlink(`${configRoot}${configFiles[i]}`); + } + for (let i = length(configDirs) - 1; i >= 0; i--) { + fs.rmdir(`${configRoot}${configDirs[i]}`); + } + fs.rmdir(configRoot); +}; + +function revertConfig(configRoot) +{ + if (fs.access(`${configRoot}/etc/config.mesh/_setup`)) { + for (let i = 0; i < length(configFiles); i++) { + const to = configFiles[i]; + const from = `${configRoot}${to}`; + if (fs.access(from)) { + fs.writefile(to, fs.readfile(from)); + fs.unlink(from); + } + else { + fs.unlink(to); + } + } + for (let i = length(configDirs) - 1; i >= 0; i--) { + fs.rmdir(`${configRoot}${configDirs[i]}`); + } + fs.rmdir(currentConfig); + } +}; + +export function prepareChanges() +{ + if (!fs.access(`${currentConfig}/etc/config.mesh/_setup`)) { + copyConfig(currentConfig); + } +}; + +export function prepareModalChanges() +{ + if (fs.access(`${modalConfig}/etc/config.mesh/_setup`)) { + removeConfig(modalConfig); + } + copyConfig(modalConfig); +}; + +function fileChanges(from, to) +{ + let count = 0; + const p = fs.popen(`exec /usr/bin/diff -NBbdiU0 ${from} ${to}`); + if (p) { + for (;;) { + const l = rtrim(p.read("line")); + if (!l) { + break; + } + if (index(l, "@@") === 0) { + const v = match(l, /^@@ [+-]\d+,?(\d*) [+-]\d+,?(\d*) @@$/); + if (v) { + count += max(math.abs(int(v[1] === "" ? 1 : v[1])), math.abs(int(v[2] === "" ? 1 : v[2]))); + } + } + } + p.close(); + } + return count; +}; + +export function commitChanges() +{ + const status = {}; + if (fs.access(`${currentConfig}/etc/config.mesh/_setup`)) { + if (fileChanges(`${currentConfig}/etc/local/uci/hsmmmesh`, "/etc/local/uci/hsmmmesh") > 0) { + fs.mkdir("/tmp/reboot-required"); + fs.writefile("/tmp/reboot-required/reboot", ""); + } + removeConfig(modalConfig); + removeConfig(currentConfig); + if (fs.access("/tmp/newpassword")) { + const pw = fs.readfile("/tmp/newpassword"); + system(`{ echo '${pw}'; sleep 1; echo '${pw}'; } | passwd > /dev/null 2>&1`); + fs.unlink("/tmp/newpassword"); + } + const n = fs.popen("exec /usr/local/bin/node-setup"); + if (n) { + status.setup = n.read("all"); + n.close(); + const c = fs.popen("exec /usr/local/bin/restart-services.sh"); + if (c) { + status.restart = c.read("all"); + c.close(); + } + } + } + return status; +}; + +export function revertChanges() +{ + revertConfig(currentConfig); +}; + +export function revertModalChanges() +{ + revertConfig(modalConfig); +}; + +export function countChanges() +{ + let count = 0; + if (fs.access(`${currentConfig}/etc/config.mesh/_setup`)) { + for (let i = 0; i < length(configFiles); i++) { + count += fileChanges(`${currentConfig}${configFiles[i]}`, configFiles[i]); + } + } + return count; +}; diff --git a/files/usr/share/ucode/aredn/hardware.uc b/files/usr/share/ucode/aredn/hardware.uc new file mode 100755 index 00000000..d83c2809 --- /dev/null +++ b/files/usr/share/ucode/aredn/hardware.uc @@ -0,0 +1,583 @@ +/* + * 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 + */ + +import * as fs from "fs"; +import * as uci from "uci"; +import * as ubus from "ubus"; + +let radioJson; +let boardJson; +const antennasCache = {}; +const channelsCache = {}; + +export function getBoard() +{ + if (!boardJson) { + const f = fs.open("/etc/board.json"); + if (!f) { + return {}; + } + boardJson = json(f.read("all")); + f.close(); + // Collapse virtualized hardware into the two basic types + if (index(boardJson.model.id, "qemu-") === 0) { + boardJson.model.id = "qemu"; + boardJson.model.name = "QEMU"; + } + else if (index(lc(boardJson.model.id), "vmware") === 0) { + boardJson.model.id = "vmware"; + boardJson.model.name = "VMware"; + } + } + return boardJson; +}; + +export function getBoardId() +{ + let name = ""; + const board = getBoard(); + if (index(board.model.name, "Ubiquiti") === 0) { + name = fs.readfile("/sys/devices/pci0000:00/0000:00:00.0/subsystem_device"); + if (!name || name === "" || name === "0x0000") { + const f = fs.open("/dev/mtd7"); + if (f) { + f.seek(12); + const d = f.read(2); + f.close(); + name = sprintf("0x%02x%02x", ord(d, 0), ord(d, 1)); + } + } + } + if (!name || name === "" || name === "0x0000") { + name = board.model.name; + } + return trim(name); +}; + +export function getRadio() +{ + if (!radioJson) { + const f = fs.open("/etc/radios.json"); + if (!f) { + return {}; + } + const radios = json(f.read("all")); + f.close(); + const id = getBoardId(); + radioJson = radios[lc(id)]; + if (radioJson && !radioJson.name) { + radioJson.name = id; + } + } + return radioJson; +}; + +export function getRadioCount() +{ + const radio = getRadio(); + if (radio.wlan0) { + if (radio.wlan1) { + return 2; + } + else { + return 1; + } + } + else { + let count = 0; + const d = fs.opendir("/sys/class/ieee80211"); + if (d) { + for (;;) { + const l = d.read(); + if (!l) { + break; + } + if (l !== "." && l !== "..") { + count++; + } + } + d.close(); + } + return count; + } +}; + +function getRadioIntf(wifiIface) +{ + const radio = getRadio(); + if (radio[wifiIface]) { + return radio[wifiIface]; + } + else { + return radio; + } +}; + +export function getRfChannels(wifiIface) +{ + let channels = channelsCache[wifiIface]; + if (!channels) { + channels = []; + const f = fs.popen("/usr/bin/iwinfo " + wifiIface + " freqlist"); + if (f) { + let freq_adjust = 0; + let freq_min = 0; + let freq_max = 0x7FFFFFFF; + if (wifiIface === "wlan0") { + const radio = getRadio(); + if (index(radio.name, "M9") !== -1) { + freq_adjust = -1520; + freq_min = 907; + freq_max = 922; + } + else if (index(radio.name, "M3") !== -1) { + freq_adjust = -2000; + freq_min = 3380; + freq_max = 3495; + } + } + for (;;) { + const line = f.read("line"); + if (!line) { + break; + } + const fn = match(line, /(\d+\.\d+) GHz \(Band: .*, Channel (-?\d+)\)/); + if (fn && index(line, "restricted") == -1 && index(line, "disabled") === -1) { + const freq = int(replace(fn[1], ".", "")) + freq_adjust; + if (freq >= freq_min && freq <= freq_max) { + const num = int(replace(fn[2], "0+", "")); + push(channels, { + label: freq_adjust === 0 ? num + " (" + freq + ")" : "" + freq, + number: num, + frequency: freq + }); + } + } + } + sort(channels, (a, b) => a.frequency - b.frequency); + f.close(); + channelsCache[wifiIface] = channels; + } + } + return channels; +}; + +export function getRfBandwidths(wifiIface) +{ + const radio = getRadioIntf(wifiIface); + if (radio.bandwidths) { + return radio.bandwidths; + } + else { + return [ 5, 10, 20 ]; + } +}; + +export function getDefaultChannel(wifiIface) +{ + const rfchannels = getRfChannels(wifiIface); + for (let i = 0; i < length(rfchannels); i++) { + const c = rfchannels[i]; + if (c.frequency == 912) { + return { channel: 5, bandwidth: 5, band: "900MHz" }; + } + const bws = {}; + const b = getRfBandwidths(wifiIface); + for (let j = 0; j < length(b); j++) { + bws[b[j]] = b[j]; + } + const bw = bws[10] || bws[20] || bws[5] || 0; + if (c.frequency === 2397) { + return { channel: -2, bandwidth: bw, band: "2.4GHz" }; + } + if (c.frequency === 2412) { + return { channel: 1, bandwidth: bw, band: "2.4GHz" }; + } + if (c.frequency === 3420) { + return { channel: 84, bandwidth: bw, band: "3GHz" }; + } + if (c.frequency === 5745) { + return { channel: 149, bandwidth: bw, band: "5GHz" }; + } + } + return null; +}; + +export function getAntennas(wifiIface) +{ + let ants = antennasCache[wifiIface]; + if (!ants) { + const radio = getRadioIntf(wifiIface); + if (radio && radio.antenna) { + if (radio.antenna === "external") { + const dchan = getDefaultChannel(wifiIface); + if (dchan && dchan.band) { + const f = fs.open("/etc/antennas.json"); + if (f) { + ants = json(f.read("all")); + f.close(); + ants = ants[dchan.band]; + } + } + } + else { + radio.antenna.builtin = true; + ants = [ radio.antenna ]; + } + antennasCache[wifiIface] = ants; + } + } + return ants; +}; + +export function getAntennasAux(wifiIface) +{ + let ants = antennasCache["aux:" + wifiIface]; + if (!ants) { + const radio = getRadioIntf(wifiIface); + if (radio && radio.antenna_aux === "external") { + const dchan = getDefaultChannel(wifiIface); + if (dchan && dchan.band) { + const f = fs.open("/etc/antennas.json"); + if (f) { + ants = json(f.read("all")); + f.close(); + ants = ants[dchan.band]; + } + } + antennasCache["aux:" + wifiIface] = ants; + } + } + return ants; +}; + +export function getAntennaInfo(wifiIface, antenna) +{ + const ants = getAntennas(wifiIface); + if (ants) { + if (length(ants) === 1) { + return ants[0]; + } + if (antenna) { + for (let i = 0; i < length(ants); i++) { + if (ants[i].model === antenna) { + return ants[i]; + } + } + } + } + return null; +}; + +export function getAntennaAuxInfo(wifiIface, antenna) +{ + const ants = getAntennasAux(wifiIface); + if (ants) { + if (length(ants) === 1) { + return ants[0]; + } + if (antenna) { + for (let i = 0; i < length(ants); i++) { + if (ants[i].model === antenna) { + return ants[i]; + } + } + } + } + return null; +}; + +export function getChannelFrequency(wifiIface, channel) +{ + const rfchans = getRfChannels(wifiIface); + if (rfchans[0]) { + for (let i = 0; i < length(rfchans); i++) { + const c = rfchans[i]; + if (c.number === channel) { + return c.frequency; + } + } + } + return null; +}; + +export function getChannelFrequencyRange(wifiIface, channel, bandwidth) +{ + const rfchans = getRfChannels(wifiIface); + if (rfchans[0]) { + for (let i = 0; i < length(rfchans); i++) { + const c = rfchans[i]; + if (c.number === channel) { + return (c.frequency - bandwidth / 2) + " - " + (c.frequency + bandwidth / 2) + " MHz"; + } + } + } + return null; +}; + +export function getChannelFromFrequency(freq) +{ + if (freq < 256) { + return null; + } + if (freq === 2484) { + return 14; + } + if (freq === 2407) { + return 0; + } + if (freq < 2484) { + return (freq - 2407) / 5; + } + if (freq < 5000) { + return null; + } + if (freq < 5380) { + return (freq - 5000) / 5; + } + if (freq < 5500) { + return freq - 2000; + } + if (freq < 6000) { + return (freq - 5000) / 5; + } +}; + +export function getMaxTxPower(wifiIface, channel) +{ + const radio = getRadioIntf(wifiIface); + if (radio) { + const maxpower = radio.maxpower; + const chanpower = radio.chanpower; + if (channel && chanpower) { + for (let k in chanpower) { + if (channel <= k) { + return chanpower[k]; + } + } + } + if (maxpower) { + return maxpower; + } + } + return 27; +}; + +export function getTxPowerOffset(wifiIface) +{ + const radio = getRadioIntf(wifiIface); + if (radio && radio.pwroffset) { + return radio.pwroffset; + } + const f = fs.popen("/usr/bin/iwinfo " + wifiIface + " info"); + if (f) { + for (;;) { + const line = f.read("line"); + if (!line) { + break; + } + if (index(line, "TX power offset: ") !== -1) { + const pwroff = match(line, /TX power offset: (\d+)/); + if (pwroff) { + f.close(); + return int(pwroff[1]); + } + return 0; + } + } + f.close(); + } + return 0; +}; + +export function supportsXLink() +{ + switch (getBoard().model.id) { + case "mikrotik,hap-ac2": + case "mikrotik,hap-ac3": + case "glinet,gl-b1300": + case "qemu": + case "vmware": + return true; + default: + return false; + } +}; + +const default5PortLayout = [ { k: "wan", d: "port1" }, { k: "lan1", d: "port2" }, { k: "lan2", d: "port3" }, { k: "lan3", d: "port4" }, { k: "lan4", d: "port5" } ]; +const default3PortLayout = [ { k: "lan2", d: "port1" }, { k: "lan1", d: "port2" }, { k: "wan", d: "port3" } ]; +const defaultNPortLayout = []; + +export function getEthernetPorts() +{ + switch (getBoard().model.id) { + case "mikrotik,hap-ac2": + case "mikrotik,hap-ac3": + return default5PortLayout; + case "glinet,gl-b1300": + return default3PortLayout; + case "qemu": + case "vmware": + if (length(defaultNPortLayout) === 0) { + const dir = fs.opendir("/sys/class/net"); + if (dir) { + for (;;) { + const file = dir.read(); + if (!file) { + break; + } + if (match(file, /^eth\d+$/)) { + push(defaultNPortLayout, { k: file, d: file }); + } + } + dir.close(); + sort(defaultNPortLayout, (a, b) => a.d == b.d ? 0 : a.d < b.d ? -1 : 1); + } + } + return defaultNPortLayout; + default: + return []; + } +}; + +export function getEthernetPortInfo(port) +{ + const s = { active: false }; + if (fs.readfile(`/sys/class/net/${port}/carrier`, 1) === "1") { + s.active = true; + } + return s; +}; + +export function getDefaultNetworkConfiguration() +{ + const c = { + dtdlink: { vlan: 2, ports: {} }, + lan: { vlan: 0, ports: {} }, + wan: { vlan: 0, ports: {} } + }; + const board = getBoard(); + const network = board.network; + for (let k in network) { + const net = c[k]; + if (net) { + const devices = split(network[k].device, " "); + for (let i = 0; i < length(devices); i++) { + const m = match(devices[i], /^([^\.]+)\.?(\d*)$/); + if (m) { + net.ports[m[1]] = true; + if (m[2]) { + net.vlan = int(m[2]); + } + } + } + const ports = network[k].ports || []; + for (let i = 0; i < length(ports); i++) { + net.ports[ports[i]] = true; + } + } + } + return c; +}; + +export function hasPOE() +{ + const board = getBoard(); + if (board?.gpioswitch?.poe_passthrough?.pin) { + return true; + } + const gpios = fs.lsdir("/sys/class/gpio/"); + for (let i = 0; i < length(gpios); i++) { + if (match(gpios[i], /^enable-poe:/)) { + return true; + } + } + return false; +}; + +export function hasUSBPower() +{ + const board = getBoard(); + if (board?.gpioswitch?.usb_power_switch?.pin) { + return true; + } + if (fs.access("/sys/class/gpio/usb-power")) { + return true; + } + return false; +}; + +export function isLowMemNode() +{ + const f = fs.open("/proc/meminfo"); + if (f) { + const l = f.read("line"); + f.close(); + const m = match(l, /([0-9]+)/); + if (m && int(m[1]) <= 32768) { + return true; + } + } + return false; +}; + +export function getHardwareType() +{ + const model = getBoard().model; + let targettype = ubus.connect().call("system", "board", {}).release.target; + let hardwaretype = model.id; + let m = match(hardwaretype, /,(.*)/); + if (m) { + hardwaretype = m[1]; + } + const mfg = trim(model.name); + let mfgprefix = ""; + if (match(mfg, /[Uu]biquiti/)) { + mfgprefix = "ubnt"; + } + else if (match(mfg, /[Mm]ikro[Tt]ik/)) { + mfgprefix = "mikrotik"; + const bv = fs.open("/sys/firmware/mikrotik/soft_config/bios_version"); + if (bv) { + const v = bv.read("all"); + bv.close(); + if (substr(v, 2) === "7.") { + targettype += "-v7" + } + } + } + else if (match(mfg, /[Tt][Pp]-[Ll]ink/)) { + mfgprefix = "cpe"; + } + return `(${targettype}) ${mfgprefix ? mfgprefix + " " : ""}(${hardwaretype})`; +}; diff --git a/files/usr/share/ucode/aredn/lqm.uc b/files/usr/share/ucode/aredn/lqm.uc new file mode 100755 index 00000000..1b38c136 --- /dev/null +++ b/files/usr/share/ucode/aredn/lqm.uc @@ -0,0 +1,70 @@ +/* + * 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 + */ + +import * as fs from "fs"; + +let lqm; + +function initLQM() +{ + if (!lqm) { + try { + lqm = json(fs.readfile("/tmp/lqm.info")); + } + catch (_) { + } + } +} + +export function get() +{ + initLQM(); + return lqm || { trackers:{}, hidden_nodes:[], now: 0 }; +}; + +export function getTrackers() +{ + initLQM(); + return lqm?.trackers || {}; +}; + +export function getHidden() +{ + initLQM(); + return lqm?.hidden_nodes || []; +}; + +export function reset() +{ + lqm = null; +}; diff --git a/files/usr/share/ucode/aredn/mesh.uc b/files/usr/share/ucode/aredn/mesh.uc new file mode 100755 index 00000000..0e3b26b8 --- /dev/null +++ b/files/usr/share/ucode/aredn/mesh.uc @@ -0,0 +1,79 @@ +/* + * 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 + */ + +import * as fs from "fs"; + +export function getNodeList() +{ + const re = /^10.+\tdtdlink\.(.+)\.local\.mesh\t#.+$/; + const nodes = []; + const f = fs.open("/var/run/hosts_olsr"); + if (!f) { + return nodes; + } + const hash = {}; + for (let l = f.read("line"); length(l); l = f.read("line")) { + const m = match(l, re); + if (m) { + const n = m[1]; + const ln = lc(n); + push(nodes, ln); + hash[ln] = n; + } + } + f.close(); + sort(nodes); + return map(nodes, n => hash[n]); +}; + +export function getNodeCounts() +{ + let nodes = 0; + let devices = 0; + const f = fs.open("/var/run/hosts_olsr"); + if (f) { + for (let l = f.read("line"); length(l); l = f.read("line")) { + if (substr(l, 0, 3) == "10." && index(l, "\tmid") === -1) { + devices++; + if (index(l, "\tdtdlink.") !== -1) { + nodes++; + } + } + } + f.close(); + } + return { + nodes: nodes, + devices: devices + }; +}; diff --git a/files/usr/share/ucode/aredn/messages.uc b/files/usr/share/ucode/aredn/messages.uc new file mode 100755 index 00000000..fcb6caa3 --- /dev/null +++ b/files/usr/share/ucode/aredn/messages.uc @@ -0,0 +1,135 @@ +/* + * 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 + */ + +import * as fs from "fs"; +import * as uci from "uci"; +import * as configuration from "aredn.configuration"; +import * as hardware from "aredn.hardware"; +import * as radios from "aredn.radios"; + +export function haveMessages() +{ + if (fs.access("/etc/cron.boot/reinstall-packages") && fs.access("/etc/package_store/catalog.json")) { + return true; + } + if (fs.stat("/tmp/aredn_message")?.size || fs.stat("/tmp/local_message")?.size) { + return true; + } + return false; +}; + +function parseMessages(nodename, msgs, text) +{ + if (text) { + const t = split(text, ""); + for (let i = 0; i < length(t); i++) { + const m = match(t[i], /↣ (.+):<\/strong>(.+)

/); + if (m) { + const label = m[1] !== nodename ? m[1] : "yournode"; + if (!msgs[label]) { + msgs[label] = []; + } + push(msgs[label], trim(m[2])); + } + } + } +} + +export function getMessages() +{ + const nodename = lc(configuration.getName()); + const msgs = {}; + if (fs.access("/etc/cron.boot/reinstall-packages") && fs.access("/etc/package_store/catalog.json")) { + msgs.system = [ "Packages are being reinstalled in the background. This can take a few minutes." ]; + } + parseMessages(nodename, msgs, fs.readfile("/tmp/aredn_message")); + parseMessages(nodename, msgs, fs.readfile("/tmp/local_message")); + return msgs; +}; + +export function haveToDos() +{ + const cursor = uci.cursor(); + if (!cursor.get("aredn", "@location[0]", "lat") || + !cursor.get("aredn", "@location[0]", "lon") || + configuration.getSettingAsString("time_zone_name", "Not Set") === "Not Set" + ) { + return true; + } + if (hardware.getRadioCount() > 0) { + const wlan = cursor.get("network", "wifi", "device"); + const ants = hardware.getAntennas(wlan); + const ant = cursor.get("aredn", "@location[0]", "antenna"); + if (length(ants) > 1 && !ant) { + return true; + } + if (ant || length(ants) === 1) { + if (!cursor.get("aredn", "@location[0]", "azimuth")) { + const ainfo = hardware.getAntennaInfo(wlan, ant || ants[0]); + if (ainfo.beamwidth !== 360) { + return true; + } + } + } + } + return false; +}; + +export function getToDos() +{ + const cursor = uci.cursor(); + const todos = []; + if (!cursor.get("aredn", "@location[0]", "lat") || !cursor.get("aredn", "@location[0]", "lon")) { + push(todos, "Set the latitude and longitude"); + } + if (configuration.getSettingAsString("time_zone_name", "Not Set") === "Not Set") { + push(todos, "Set the timzeone"); + } + if (hardware.getRadioCount() > 0) { + const wlan = cursor.get("network", "wifi", "device"); + const ants = hardware.getAntennas(wlan); + const ant = cursor.get("aredn", "@location[0]", "antenna"); + if (length(ants) > 1 && !ant) { + push(todos, "Select an antenna"); + } + else if (ant || length(ants) === 1) { + if (!cursor.get("aredn", "@location[0]", "azimuth")) { + const ainfo = hardware.getAntennaInfo(wlan, ant || ants[0]); + if (ainfo.beamwidth !== 360) { + push(todos, "Set antenna azimuth"); + } + } + } + } + return todos; +}; diff --git a/files/usr/share/ucode/aredn/network.uc b/files/usr/share/ucode/aredn/network.uc new file mode 100755 index 00000000..3496d0d6 --- /dev/null +++ b/files/usr/share/ucode/aredn/network.uc @@ -0,0 +1,127 @@ +/* + * 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 + */ + +import * as fs from "fs"; +import * as resolv from "resolv"; + +export function hasInternet() +{ + const p = fs.popen("exec /bin/ping -W1 -c1 8.8.8.8"); + if (p) { + const d = p.read("all"); + p.close(); + if (index(d, "1 packets received") !== -1) { + return true; + } + } + return false; +}; + +export function getIPAddressFromHostname(hostname) +{ + const p = fs.popen(`exec /usr/bin/nslookup ${hostname}`); + if (p) { + const d = p.read("all"); + p.close(); + const i = match(d, /Address: ([0-9.]+)/); + if (i) { + return i[1]; + } + } + return null; +}; + +export function netmaskToCIDR(mask) +{ + const m = iptoarr(mask); + let cidr = 32; + for (let i = 3; i >= 0; i--) { + switch (m[i]) { + default: + case 255: + return cidr - 0; + case 254: + return cidr - 1; + case 252: + return cidr - 2; + case 248: + return cidr - 3; + case 240: + return cidr - 4; + case 224: + return cidr - 5; + case 192: + return cidr - 6; + case 128: + return cidr - 7; + case 0: + cidr -= 8; + break; + } + } + return 0; +}; + +export function CIDRToNetmask(cidr) +{ + const v = (0xFF00 >> (cidr % 8)) & 0xFF; + switch (int(cidr / 8)) { + case 0: + return `${v}.0.0.0`; + case 1: + return `255.${v}.0.0`; + case 2: + return `255.255.${v}.0`; + case 3: + return `255.255.255.${v}`; + default: + return "255.255.255.255"; + } +}; + +export function nslookup(aorh) +{ + const r = resolv.query([aorh]); + if (r) { + for (let k in r) { + const v = r[k]; + if (v.PTR) { + return v.PTR[0]; + } + if (v.A) { + return v.A[0]; + } + } + } + return null; +}; diff --git a/files/usr/share/ucode/aredn/olsr.uc b/files/usr/share/ucode/aredn/olsr.uc new file mode 100755 index 00000000..66c6c79e --- /dev/null +++ b/files/usr/share/ucode/aredn/olsr.uc @@ -0,0 +1,90 @@ +/* + * 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 + */ + +import * as fs from "fs"; + +export function getLinks() +{ + const f = fs.popen("exec /usr/bin/curl http://127.0.0.1:9090/links -o - 2> /dev/null"); + try { + const links = json(f.read("all")).links; + f.close(); + return links; + } + catch (_) { + f.close(); + return []; + } +}; + +export function getRoutes() +{ + const f = fs.popen("exec /usr/bin/curl http://127.0.0.1:9090/routes -o - 2> /dev/null"); + try { + const routes = json(f.read("all")).routes; + f.close(); + return routes; + } + catch (_) { + f.close(); + return []; + } +}; + +export function getHNAs() +{ + const f = fs.popen("exec /usr/bin/curl http://127.0.0.1:9090/hna -o - 2> /dev/null"); + try { + const hna = json(f.read("all")).hna; + f.close(); + return hna; + } + catch (_) { + f.close(); + return []; + } +}; + +export function getMids() +{ + const f = fs.popen("exec /usr/bin/curl http://127.0.0.1:9090/mid -o - 2> /dev/null"); + try { + const mid = json(f.read("all")).mid; + f.close(); + return mid; + } + catch (_) { + f.close(); + return []; + } +}; diff --git a/files/usr/share/ucode/aredn/radios.uc b/files/usr/share/ucode/aredn/radios.uc new file mode 100755 index 00000000..05478809 --- /dev/null +++ b/files/usr/share/ucode/aredn/radios.uc @@ -0,0 +1,196 @@ +/* + * 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 + */ + +import * as hardware from "aredn.hardware"; +import * as configuration from "aredn.configuration"; +import * as uci from "uci"; + +export const RADIO_OFF = 0; +export const RADIO_MESH = 1; +export const RADIO_LAN = 2; +export const RADIO_WAN = 3; + +export function getCommonConfiguration() +{ + const radio = []; + const nrradios = hardware.getRadioCount(); + for (let i = 0; i < nrradios; i++) { + const iface = `wlan${i}`; + radio[i] = { + iface: iface, + mode: 0, + modes: [], + ant: null, + antaux: null, + def: hardware.getDefaultChannel(iface), + bws: hardware.getRfBandwidths(iface), + channels: hardware.getRfChannels(iface), + ants: hardware.getAntennas(iface), + antsaux: hardware.getAntennasAux(iface), + txpoweroffset: hardware.getTxPowerOffset(iface), + txmaxpower: hardware.getMaxTxPower(iface) + }; + } + return radio; +}; + +export function getActiveConfiguration() +{ + const cursor = uci.cursor(); + const radio = getCommonConfiguration(); + const nrradios = length(radio); + if (nrradios > 0) { + const meshrf = cursor.get("network", "wifi", "device"); + const widx = match(meshrf, /^wlan(\d+)$/); + if (widx) { + let device; + const mode = { + channel: 0, + bandwidth: 10, + ssid: "AREDN", + txpower: configuration.getSettingAsInt("wifi_txpower") + }; + cursor.foreach("wireless", "wifi-iface", function(s) + { + if (s.network === "wifi" && s.ifname === meshrf) { + device = s.device; + mode.ssid = s.ssid; + return false; + } + }); + cursor.foreach("wireless", "wifi-device", function(s) + { + if (s[".name"] === device) { + mode.channel = int(s.channel); + mode.bandwidth = int(s.chanbw); + return false; + } + }); + radio[widx[1]].mode = RADIO_MESH; + radio[widx[1]].modes = [ null, mode, null, null ]; + } + } + return radio; +}; + +export function getConfiguration() +{ + const cursor = uci.cursor("/etc/config.mesh"); + const radio = getCommonConfiguration(); + const nrradios = length(radio); + if (nrradios > 0) { + const modes = [ null, { + channel: configuration.getSettingAsInt("wifi_channel"), + bandwidth: configuration.getSettingAsInt("wifi_chanbw", 10), + ssid: configuration.getSettingAsString("wifi_ssid", "AREDN"), + txpower: configuration.getSettingAsInt("wifi_txpower", 27) + }, + { + channel: configuration.getSettingAsInt("wifi2_channel"), + encryption: configuration.getSettingAsString("wifi2_encryption", "psk2"), + key: configuration.getSettingAsString("wifi2_key", ""), + ssid: configuration.getSettingAsString("wifi2_ssid", "") + }, + { + key: configuration.getSettingAsString("wifi3_key", ""), + ssid: configuration.getSettingAsString("wifi3_ssid", "") + }]; + for (let i = 0; i < nrradios; i++) { + radio[i].modes = modes; + } + + radio[0].ant = hardware.getAntennaInfo(radio[0].iface, cursor.get("aredn", "@location[0]", "antenna")); + radio[0].antaux = hardware.getAntennaAuxInfo(radio[0].iface, cursor.get("aredn", "@location[0]", "antenna_aux")); + + const wifi_enable = configuration.getSettingAsInt("wifi_enable", 0); + const wifi2_enable = configuration.getSettingAsInt("wifi2_enable", 0); + const wifi3_enable = configuration.getSettingAsInt("wifi3_enable", 0); + if (nrradios === 1) { + if (wifi_enable) { + radio[0].mode = 1; + } + else if (wifi2_enable) { + radio[0].mode = 2; + } + else if (wifi3_enable) { + radio[0].mode = 3; + } + } + else if (wifi_enable) { + const wifi_iface = configuration.getSettingAsString("wifi_intf", "wlan0"); + if (wifi_iface === "wlan0") { + radio[0].mode = 1; + if (wifi2_enable) { + radio[1].mode = 2; + } + else if (wifi3_enable) { + radio[1].mode = 3; + } + } + else { + radio[1].mode = 1; + if (wifi2_enable) { + radio[0].mode = 2; + } + else if (wifi3_enable) { + radio[0].mode = 3; + } + } + } + else if (wifi2_enable) { + const wifi2_hwmode = configuration.getSettingAsString("wifi2_hwmode", "11a"); + if ((wifi2_hwmode === "11a" && radio[0].def.band === "5GHz") || (wifi2_hwmode === "11g" && radio[0].def.band === "2.4GHz")) { + radio[0].mode = 2; + if (wifi3_enable) { + radio[1].mode = 3; + } + } + else { + radio[1].mode = 2; + if (wifi3_enable) { + radio[0].mode = 3; + } + } + } + else if (wifi3_enable) { + const wifi3_hwmode = configuration.getSettingAsString("wifi3_hwmode", "11a"); + if ((wifi3_hwmode === "11a" && radio[0].def.band === "5GHz") || (wifi3_hwmode === "11g" && radio[0].def.band === "2.4GHz")) { + radio[0].mode = 3; + } + else { + radio[1].mode = 3; + } + } + } + return radio; +}; diff --git a/files/usr/share/ucode/aredn/units.uc b/files/usr/share/ucode/aredn/units.uc new file mode 100755 index 00000000..d50d1b06 --- /dev/null +++ b/files/usr/share/ucode/aredn/units.uc @@ -0,0 +1,75 @@ +/* + * 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 + */ + +const meters_to_miles = 0.000621371; +const meters_to_km = 0.001; +let metric = null; + +function isMetric() +{ + if (metric === null) { + const lang = request?.env?.HTTP_ACCEPT_LANGUAGE || "en-US"; + if (index(lang, "-US") !== -1 || index(lang, "-GB") !== -1) { + metric = false; + } + else { + metric = true; + } + } + return metric; +}; + +export function distanceUnit() +{ + return isMetric() ? "km" : "miles"; +}; + +export function meters2distance(meters) +{ + if (isMetric()) { + return meters * meters_to_km; + } + else { + return meters * meters_to_miles; + } +}; + +export function distance2meters(distance) +{ + if (isMetric()) { + return distance / meters_to_km; + } + else { + return distance / meters_to_miles; + } +}; diff --git a/files/www/cgi-bin/admin b/files/www/cgi-bin/admin index 00dc57aa..bdcab160 100755 --- a/files/www/cgi-bin/admin +++ b/files/www/cgi-bin/admin @@ -155,6 +155,7 @@ if os.getenv("REQUEST_METHOD") == "POST" then os.execute("/usr/local/bin/upgrade_prepare.sh stop > /dev/null 2>&1") end end + nixio.fs.mkdir("/tmp/web") nixio.fs.mkdir("/tmp/web/upload") fp = io.open("/tmp/web/upload/file", "w") end @@ -181,11 +182,16 @@ nixio.fs.mkdir("/tmp/web/admin") local curl = "curl -A 'node: " .. node .. "' " local serverpaths = {} -local uciserverpath = cursor:get("aredn", "@downloads[0]", "firmwarepath") -if not uciserverpath then - uciserverpath = "" +local aredn_firmware = cursor:get("aredn", "@downloads[0]", "firmware_aredn") +if aredn_firmware then + serverpaths[#serverpaths + 1] = aredn_firmware +else + local uciserverpath = cursor:get("aredn", "@downloads[0]", "firmwarepath") + if not uciserverpath then + uciserverpath = "" + end + serverpaths[#serverpaths + 1] = uciserverpath:match("^(.*)/firmware") or uciserverpath end -serverpaths[#serverpaths + 1] = uciserverpath local hardwaretype = aredn.hardware.get_type() local targettype = conn:call("system", "board", {}).release.target @@ -224,13 +230,13 @@ 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 .. "...") + if get_default_gw() ~= "none" or serverpaths[1]:match("%.local%.mesh") then + fwout("Downloading firmware list from " .. serverpaths[1] .. "...") local config_versions local config_serverpath for _, serverpath in ipairs(serverpaths) do - config_serverpath = (serverpath:match("^(.*)/firmware") or serverpath) .. "/afs/www/" + config_serverpath = serverpath .. "/afs/www/" for line in io.popen(curl .. " -o - " .. config_serverpath .. "config.js 2> /dev/null"):lines() do local v = line:match("versions: {(.+)}") @@ -389,7 +395,7 @@ 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 + if get_default_gw() ~= "none" or serverpaths[1]:match("%.local%.mesh") then nixio.fs.remove(tmpdir .. "/firmware") os.execute("/usr/local/bin/uploadctlservices update > /dev/null 2>&1") diff --git a/files/www/cgi-bin/advancedconfig b/files/www/cgi-bin/advancedconfig index 538aa6eb..469e9005 100755 --- a/files/www/cgi-bin/advancedconfig +++ b/files/www/cgi-bin/advancedconfig @@ -45,43 +45,6 @@ require("aredn.info") local html = aredn.html -local urlprefix -local target = "unknown" -local arch = "unknown" -function defaultPackageRepos(repo) - if not urlprefix then - urlprefix = "http://downloads.arednmesh.org" - local release = "unknown" - for line in io.lines("/etc/openwrt_release") - do - local m = line:match("DISTRIB_RELEASE='(.*)'") - if m then - release = m - end - m = line:match("DISTRIB_TARGET='(.*)'") - if m then - target = m - end - m = line:match("DISTRIB_ARCH='(.*)'") - if m then - arch = m - end - end - local a, b = release:match("^(%d+)%.(%d+)%.") - if a and b then - urlprefix = urlprefix .. "/releases/" .. a .. "/" .. b .. "/" .. release - else - -- nightly - urlprefix = urlprefix .. "/snapshots" - end - end - if repo:match("aredn_core") then - return urlprefix .. "/targets/" .. target .. "/packages" - else - return urlprefix .. "/packages/" .. arch .. "/" .. repo - end -end - local settings = { { category = "Link Quality Settings", @@ -314,95 +277,24 @@ local settings = { }, { category = "Map Paths", - key = "aredn.@map[0].maptiles", + key = "aredn.@location[0].map", type = "string", - desc = "Map Tiles URL

aredn.@map[0].maptiles", - default = "http://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg" - }, - { - category = "Map Paths", - key = "aredn.@map[0].leafletcss", - type = "string", - desc = "Leaflet.css URL

aredn.@map[0].leafletcss", - default = "http://unpkg.com/leaflet@0.7.7/dist/leaflet.css" - }, - { - category = "Map Paths", - key = "aredn.@map[0].leafletjs", - type = "string", - desc = "Leaflet.js URL

aredn.@map[0].leafletjs", - default = "http://unpkg.com/leaflet@0.7.7/dist/leaflet.js" + desc = "Map URL

aredn.@location[0].maps", + default = "https://worldmap.arednmesh.org/#12/(lat)/(lon)" }, { category = "Firmware", - key = "aredn.@downloads[0].firmwarepath", + key = "aredn.@downloads[0].firmware_aredn", type = "string", - desc = "Firmware Download URL

aredn.@downloads[0].firmwarepath", - default = "http://downloads.arednmesh.org/firmware" + desc = "Firmware Download URL

aredn.@downloads[0].firmware_aredn", + default = "http://downloads.arednmesh.org" }, { category = "Firmware", - key = "aredn.@downloads[0].pkgs_core", + key = "aredn.@downloads[0].packages_default", type = "string", - desc = "Core Packages Download URL

aredn.@downloads[0].pkgs_core", - default = defaultPackageRepos('aredn_core'), - postcallback = "writePackageRepo('core')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_base", - type = "string", - desc = "Base Packages URL

aredn.@downloads[0].pkgs_base", - default = defaultPackageRepos('base'), - postcallback = "writePackageRepo('base')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_arednpackages", - type = "string", - desc = "AREDN Packages URL

aredn.@downloads[0].pkgs_arednpackages", - default = defaultPackageRepos('arednpackages'), - postcallback = "writePackageRepo('arednpackages')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_luci", - type = "string", - desc = "Luci Packages URL

aredn.@downloads[0].pkgs_luci", - default = defaultPackageRepos('luci'), - postcallback = "writePackageRepo('luci')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_packages", - type = "string", - desc = "Package Download URL for packages not included in the other sections

aredn.@downloads[0].pkgs_packages", - default = defaultPackageRepos('packages'), - postcallback = "writePackageRepo('packages')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_routing", - type = "string", - desc = "Routing Packages URL

aredn.@downloads[0].pkgs_routing", - default = defaultPackageRepos('routing'), - postcallback = "writePackageRepo('routing')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_telephony", - type = "string", - desc = "Telephony Packages URL

aredn.@downloads[0].pkgs_telephony", - default = defaultPackageRepos('telephony'), - postcallback = "writePackageRepo('telephony')" - }, - { - category = "Firmware", - key = "aredn.@downloads[0].pkgs_freifunk", - type = "string", - desc = "Freifunk Packages URL

aredn.@downloads[0].pkgs_freifunk", - default = defaultPackageRepos('freifunk'), - postcallback = "writePackageRepo('freifunk')" + desc = "Packages Download URL

aredn.@downloads[0].packages_default", + default = "http://downloads.arednmesh.org" }, { category = "Firmware", diff --git a/files/www/cgi-bin/iperf b/files/www/cgi-bin/iperf index 989b283f..b350f299 100755 --- a/files/www/cgi-bin/iperf +++ b/files/www/cgi-bin/iperf @@ -46,8 +46,8 @@ local server = q:match("server=([^&]*)") local protocol = q:match("protocol=([^&]*)") or "tcp" local kill = q:match("kill=1") and true or false -print "Content-type: text/html\r" -print "Cache-Control: no-store\r" +print("Content-type: text/html\r") +print("Cache-Control: no-store\r") print("Access-Control-Allow-Origin: *\r") print("\r") if uci.cursor():get("aredn", "@iperf[0]", "enable") == "0" then diff --git a/files/www/cgi-bin/ping b/files/www/cgi-bin/ping new file mode 100755 index 00000000..2b307f6d --- /dev/null +++ b/files/www/cgi-bin/ping @@ -0,0 +1,74 @@ +#!/usr/bin/lua +--[[ + + Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks + Copyright (C) 2022-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 + +--]] + +require("uci") +require("nixio") +require("aredn.utils") +require("aredn.info") + +local node = aredn.info.get_nvram("node") + +local q = os.getenv("QUERY_STRING") or "" +local server = q:match("server=([^&]*)") + +print("Content-type: text/html\r") +print("Cache-Control: no-store\r") +print("Access-Control-Allow-Origin: *\r") +print("\r") +if not server then + print("ERROR

Provide a server name to run a test between this client and a server [/cgi-bin/ping?server=<ServerName>
") +elseif server:match("[^%w%-%.]") then + print("ERROR
Illegal server name
") +else + if not server:match("%.") then + server = server .. ".local.mesh" + end + local running = io.popen("/bin/ping -c 5 -w 10 " .. server .. " 2>&1") + if not running then + print("ERROR
ping failed
") + else + print("SUCCESS") + print("
Client: " .. node .. "\nServer: " .. server)
+        io.flush()
+        for line in running:lines()
+        do
+            print(line)
+            io.flush()
+        end
+        running:close()
+        print("
") + end +end diff --git a/files/www/cgi-bin/setup b/files/www/cgi-bin/setup index 20153338..9a223567 100755 --- a/files/www/cgi-bin/setup +++ b/files/www/cgi-bin/setup @@ -937,7 +937,7 @@ function noLocation() { req.addEventListener("load", function() { try { const json = JSON.parse(this.responseText); - foundLocation({ coords: { latitude: json.lat, longitude: json.lon }}) + foundLocation({ coords: { latitude: json.lat, longitude: json.lon}}) } catch (_) { } diff --git a/files/www/cgi-bin/status b/files/www/cgi-bin/status index 05c8843e..19b83771 100755 --- a/files/www/cgi-bin/status +++ b/files/www/cgi-bin/status @@ -281,6 +281,8 @@ end -- nav buttons html.navbar_user("status", config_mode) +html.print([[New UI]]) + html.print("") if config_mode then diff --git a/files/www/cgi-bin/traceroute b/files/www/cgi-bin/traceroute new file mode 100755 index 00000000..003ed340 --- /dev/null +++ b/files/www/cgi-bin/traceroute @@ -0,0 +1,74 @@ +#!/usr/bin/lua +--[[ + + Part of AREDN® -- Used for creating Amateur Radio Emergency Data Networks + Copyright (C) 2022-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 + +--]] + +require("uci") +require("nixio") +require("aredn.utils") +require("aredn.info") + +local node = aredn.info.get_nvram("node") + +local q = os.getenv("QUERY_STRING") or "" +local server = q:match("server=([^&]*)") + +print("Content-type: text/html\r") +print("Cache-Control: no-store\r") +print("Access-Control-Allow-Origin: *\r") +print("\r") +if not server then + print("ERROR
Provide a server name to run a test between this client and a server [/cgi-bin/traceroute?server=<ServerName>
") +elseif server:match("[^%w%-%.]") then + print("ERROR
Illegal server name
") +else + if not server:match("%.") then + server = server .. ".local.mesh" + end + local running = io.popen("/bin/traceroute -q 1 -w 1 " .. server .. " 2>&1") + if not running then + print("ERROR
traceroute failed
") + else + print("SUCCESS") + print("
Client: " .. node .. "\nServer: " .. server)
+        io.flush()
+        for line in running:lines()
+        do
+            print(line)
+            io.flush()
+        end
+        running:close()
+        print("
") + end +end diff --git a/files/www/index.html b/files/www/index.html index 94784c0a..84087d73 100644 --- a/files/www/index.html +++ b/files/www/index.html @@ -5,10 +5,10 @@ - + Mesh Node Administrative Console -Redirecting to status page +Redirecting to status page diff --git a/patches/802-gpio-typo.patch b/patches/802-gpio-typo.patch new file mode 100755 index 00000000..ed68e10b --- /dev/null +++ b/patches/802-gpio-typo.patch @@ -0,0 +1,21 @@ +--- a/target/linux/ipq40xx/base-files/etc/board.d/03_gpio_switches ++++ b/target/linux/ipq40xx/base-files/etc/board.d/03_gpio_switches +@@ -13,16 +13,16 @@ + ucidef_add_gpio_switch "usb_vcc" "USB power" "401" "0" + ;; + cilab,meshpoint-one) +- ucidef_add_gpio_switch "poe_passtrough" "POE passtrough enable" "413" "1" ++ ucidef_add_gpio_switch "poe_passthrough" "POE passthrough enable" "413" "1" + ;; + compex,wpj428) + ucidef_add_gpio_switch "sim_card_select" "SIM card select" "3" "0" + ;; + mikrotik,cap-ac) +- ucidef_add_gpio_switch "poe_passtrough" "POE passtrough enable" "414" "0" ++ ucidef_add_gpio_switch "poe_passthrough" "POE passthrough enable" "414" "0" + ;; + mikrotik,hap-ac3) +- ucidef_add_gpio_switch "poe_passtrough" "PoE Passthrough" "452" "0" ++ ucidef_add_gpio_switch "poe_passthrough" "PoE Passthrough" "452" "0" + ;; + mikrotik,hap-ac3-lte6-kit) diff --git a/patches/series b/patches/series index 781cd487..88efc99e 100644 --- a/patches/series +++ b/patches/series @@ -46,3 +46,4 @@ 780-restore-request-class.patch 800-upgrade-compatibility.patch 801-mikrotik-lhg-variants.patch +802-gpio-typo.patch