* New UI

* Fix gzip filename race condition

* Fix scrolling on first use page
This commit is contained in:
Tim Wilkinson 2024-08-15 20:28:45 -07:00 committed by GitHub
parent a22e6eb4e0
commit 0432bf3165
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
137 changed files with 17170 additions and 906 deletions

View File

@ -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

53
files/app/config.uc Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
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;
}

43
files/app/constants.uc Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
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]+";

106
files/app/main/app.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{% if (!request.headers["hx-boosted"]) { %}
<!DOCTYPE html>
<html>
<head>
{% if (!config.resourcehash) { %}
<link href="/a/css/theme.css" rel="stylesheet">
<link href="/a/css/user.css" rel="stylesheet">
{% if (auth.isAdmin) { %}
<link href="/a/css/admin.css" rel="stylesheet">
<script src="/a/js/htmx.min.js"></script>
{% } %}
{% } else {
versions.themecss = fs.readlink(`${config.application}/resource/css/theme.version`);
%}
<link href="http://localnode.local.mesh/a/css/theme.css.{{versions.themecss}}" rel="stylesheet" onerror="s=document.createElement('link');s.rel='stylesheet';s.href='/a/css/theme.css.{{versions.themecss}}';document.head.appendChild(s)">
<link href="http://localnode.local.mesh/a/css/user.css.{{versions.usercss}}" rel="stylesheet" onerror="s=document.createElement('link');s.rel='stylesheet';s.href='/a/css/user.css.{{versions.usercss}}';document.head.appendChild(s)">
{% if (auth.isAdmin) { %}
<link href="http://localnode.local.mesh/a/css/admin.css.{{versions.admincss}}" rel="stylesheet" onerror="s=document.createElement('link');s.rel='stylesheet';s.href='/a/css/admin.css.{{versions.admincss}}';document.head.appendChild(s)">
<script src="http://localnode.local.mesh/a/js/htmx.min.js.{{versions.htmx}}" onerror="s=document.createElement('script');s.type='text/javascript';s.onload=()=>htmx.process(document.body);s.src='/a/js/htmx.min.js.{{versions.htmx}}';document.head.appendChild(s)"></script>
{% } %}
{% } %}
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
<title>{{configuration.getName()}} {{auth.isAdmin && request.page === "status" ? "admin" : request.page}}</title>
</head>
<body class="{{auth.isAdmin ? "authenticated" : ""}}" hx-indicator="body">
<dialog id="ctrl-modal" onclose="event.target.innerHTML = ''"></dialog>
{{_R("oldui")}}
<div id="all">
<div id="nav">
{{_R("nav")}}
</div>
<div id="panel">
<div id="select">
{{_R("selection")}}
</div>
<div id="main">
<div id="main-container">
{{_R(request.page)}}
</div>
</div>
</div>
</div>
{% if (auth.isAdmin) { %}
<script>
(function(){
document.body.addEventListener("click", e => {
const a = htmx.findAll(".popup-menu input[type=checkbox]:checked");
for (let i = 0; i < a.length; i++) {
if (a[i] !== e.target) {
a[i].checked = false;
}
}
});
})();
</script>
{% } %}
</body>
</html>
{% } else { %}
<title>{{configuration.getName()}} {{auth.isAdmin && request.page === "status" ? "admin" : request.page}}</title>
{{_R("nav-status")}}
<div id="panel" hx-swap-oob="true">
<div id="select">
{{_R("selection")}}
</div>
<div id="main">
<div id="main-container">
{{_R(request.page)}}
</div>
</div>
</div>
{% } %}

56
files/app/main/authenticate.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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');
%}

35
files/app/main/changes.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("changes")}}

35
files/app/main/dhcp.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("dhcp")}}

104
files/app/main/firstuse-ram.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
if (request.env.REQUEST_METHOD === "POST") {
print(_R("reboot-firstuse-ram"));
response.upgrade = `/usr/local/bin/aredn_sysupgrade -n -q ${request.args.firmwarefile}`;
return;
}
%}
<!DOCTYPE>
<html>
<head>
<link href="/a/css/theme.css" rel="stylesheet">
<link href="/a/css/user.css" rel="stylesheet">
<link href="/a/css/admin.css" rel="stylesheet">
<script src="/a/js/htmx.min.js"></script>
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
</head>
<body>
<div id="all">
<div class="firstuse ram">
<div>
<div id="icon-logo""></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div>Welcome</div>
<div>
<div>Congratulations on booting AREDN&reg;</div>
<div>AREDN&reg; is currently running in RAM. The next step is to install AREDN&reg; into Flash.</div>
<div>Download the <b>sysupgrade.bin</b> file for this device (it should be at the same place your found this
<b>kernel.bin</b> file) and upload it using the file selector below</div>
</div>
<div>
<div class="cols">
<div>Select Firmware File</div>
<div><input type="file" name="firmware" accept=".bin"></div>
</div>
<div class="cols">
<div></div>
<div><button disabled hx-trigger="none" hx-encoding="multipart/form-data">Upload &amp; Reboot</button></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
htmx.on("input[name=firmware]", "change", _ => {
if (htmx.find("input[name=firmware]").files[0]) {
htmx.find("button").disabled = false;
}
else {
htmx.find("button").disabled = true;
}
});
htmx.on("button", "click", e => {
htmx.find("button").disabled = true;
htmx.ajax("POST", "{{request.env.REQUEST_URI}}", {
source: e.currentTarget,
values: {
firmwarefile: htmx.find("input[name=firmware]").files[0],
},
swap: "none"
});
});
})();
</script>
</body>
</html>

132
files/app/main/firstuse.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
}
%}
<!DOCTYPE>
<html>
<head>
<link href="/a/css/theme.css" rel="stylesheet">
<link href="/a/css/user.css" rel="stylesheet">
<link href="/a/css/admin.css" rel="stylesheet">
<script src="/a/js/htmx.min.js"></script>
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
</head>
<body>
<div id="all">
<div class="firstuse">
<div>
<div id="icon-logo""></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div>Welcome</div>
<div>
<div>Congratulations on installing AREDN&reg;</div>
<div>There's a few pieces of basic information we need to start setting up your node.</div>
</div>
<div>
<div class="cols">
<div>Node Name</div>
<div><input type="text" name="name" required pattern="{{constants.patNodename}}"></div>
</div>
<div>
<small>This is the unique name given to your node. It must start with your callsign. For example, <b>K6AH-Home</b></small>
</div>
<div class="cols">
<div>New Password</div>
<div><input type="password" name="passwd1" required pattern="`[^#'"]+" minlength="4"></div>
</div>
<div class="cols">
<div>Retype Password</div>
<div><input type="password" name="passwd2" required pattern="" minlength="4"></div>
</div>
<div>
<small>Enter a password, twice, to assign to your node for access to configuration information later</small>
</div>
<div class="cols">
<div></div>
<div><button disabled>Save &amp; Reboot</button></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function(){
function change()
{
const name = htmx.find("input[name=name]");
const passwd1 = htmx.find("input[name=passwd1]");
const passwd2 = htmx.find("input[name=passwd2]");
if (passwd1.value === "hsmm") {
passwd1.pattern = "BAD";
}
else {
passwd1.pattern = `[^#'"]+`;
}
passwd2.required = passwd1.value ? "required" : "";
passwd2.pattern = passwd1.value;
if (name.validity.valid && passwd1.validity.valid && passwd2.validity.valid) {
htmx.find("button").disabled = false;
}
else {
htmx.find("button").disabled = true;
}
}
htmx.on("input[name=name]", "keyup", change);
htmx.on("input[name=passwd1]", "keyup", change);
htmx.on("input[name=passwd2]", "keyup", change);
htmx.on("button", "click", _ => {
htmx.find("button").disabled = true;
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
values: {
name: htmx.find("input[name=name]").value,
passwd: htmx.find("input[name=passwd1]").value
},
swap: "none"
});
});
})();
</script>
</body>
</html>

35
files/app/main/health.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("health")}}

48
files/app/main/info.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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");
}
%}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("local-and-neighbor-devices")}}

35
files/app/main/mesh-summary.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("mesh-summary")}}

40
files/app/main/messages.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{% if (messages.haveMessages() || (auth.isAdmin && messages.haveToDos())) { %}
<div id="messages">
{{_R("messages")}}
</div>
</div>
{% } %}

37
files/app/main/packages.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
div id="packages" hx-swap-oob="innerHTML">
{{_R("packages", "oob")}}
</div>

340
files/app/main/status/e/basics.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
function getSshKeys()
{
let options = "<option value='-'>-</option>";
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 += `<option value="${m[3]}">${m[3]}</option>`;
}
}
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;
}
%}
<div class="dialog basics">
{{_R("dialog-header", "Name &amp; Security")}}
<div>
<div class="cols">
<div>
<div class="o">Node Name</div>
<div class="m">This node's unique name</div>
</div>
<div style="flex:0">
<input name="node_name" hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" type="text" minlength="4" maxlength="63" size="30" required pattern="{{constants.patNodename}}" value="{{configuration.getName()}}">
</div>
</div>
{{_H("Change the node's unique name. The name must start with your callsign and be less than 64 characters long.")}}
<div class="cols">
<div>
<div class="o">Description</div>
<div class="m">Information about this node</div>
</div>
<div style="flex:0">
<textarea name="description_node" style="width:340px" rows="2" columns="80" hx-put="{{request.env.REQUEST_URI}}">{{configuration.getSettingAsString("description_node", "")}}</textarea>
</div>
</div>
{{_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&zwnj;ail
address).")}}
<div class="cols">
<div>
<div class="o">Notes</div>
<div class="m">Private notes about this node</div>
</div>
<div style="flex:0">
<textarea name="notes" style="width:340px" rows="2" columns="80" hx-put="{{request.env.REQUEST_URI}}">{{uciMesh.get("aredn", "@notes[0]", "private") || ""}}</textarea>
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Theme</div>
<div class="m">Display theme and colors</div>
</div>
<div style="flex:0">
{%
const theme = fs.readlink(`${config.application}/resource/css/theme.css`);
const themes = fs.lsdir(`${config.application}/resource/css/themes`);
%}
<select name="theme" hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" hx-on:htmx:after-request="location.reload()">
<option value="default" {{theme === 'themes/default.css' ? 'selected': ''}}>default</option>
{%
for (let i = 0; i < length(themes); i++) {
const t = themes[i];
const m = match(t, /^(.*)\.css$/);
if (t !== "default.css" && m) {
print(`<option value="${m[1]}" ${theme === "themes/" + t ? "selected": ""}>${m[1]}</option>`);
}
}
%}
</select>
</div>
</div>
{{_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.")}}
<hr>
<div class="password">
<div class="cols">
<div>
<div class="o">New Password</div>
<div class="m">Change the node password</div>
</div>
<div>
<input name="passwd1" type="password" pattern="[^#'"]+"><button class="icon eye"></button>
</div>
</div>
<div class="cols">
<div>
<div class="o">Retype Password</div>
<div class="m">Passwords must match</div>
</div>
<div>
<input name="passwd2" type="password" pattern=""><button class="icon eye"></button>
</div>
</div>
{{_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.")}}
</div>
{{_R("dialog-advanced")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">Upload SSH Key</div>
<div class="m">Add SSH key</div>
</div>
<div>
<input name="ssh_add" type="file" accept=".pub">
</div>
</div>
<div class="cols">
<div>
<div class="o">Remove SSH Key</div>
<div class="m">Delete SSH key</div>
</div>
<div style="flex:0">
<select name="ssh_remove">{{getSshKeys()}}</select>
</div>
</div>
{{_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.")}}
<div class="cols">
<div></div>
<div style="flex:0">
<button id="ssh_change" disabled hx-trigger="none" hx-encoding="multipart/form-data">Upload or Remove Key</button>
</div>
</div>
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
htmx.on("dialog input[name=passwd1]", "keyup", e => {
const v = e.target.value;
if (v === "hsmm") {
htmx.find("dialog input[name=passwd1]").pattern = "BAD";
}
else {
htmx.find("dialog input[name=passwd1]").pattern = `[^#'"]+`;
}
htmx.find("dialog input[name=passwd2]").required = v ? "required" : "";
htmx.find("dialog input[name=passwd2]").pattern = v;
});
htmx.on("#dialog-done", "click", _ => {
const passwd1 = htmx.find("dialog input[name=passwd1]");
const passwd2 = htmx.find("dialog input[name=passwd2]");
if (passwd1.validity.valid && passwd2.validity.valid && passwd1.value !== "" && passwd1.value === passwd2.value) {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
values: {
passwd: passwd1.value
},
swap: "none"
});
}
});
function toggle()
{
const t = htmx.find("dialog input[name=passwd1]").type;
htmx.find("dialog input[name=passwd1]").type = t === "text" ? "password" : "text";
htmx.find("dialog input[name=passwd2]").type = t === "text" ? "password" : "text";
}
htmx.on("dialog input[name=passwd1] + button", "click", toggle);
htmx.on("dialog input[name=passwd2] + button", "click", toggle);
{% if (includeAdvanced) { %}
htmx.on("dialog input[name=ssh_add]", "change", e => {
htmx.find("dialog select[name=ssh_remove]").value = "-";
if (e.target.value === "-") {
htmx.find("dialog #ssh_change").disabled = true;
htmx.find("dialog #ssh_change").innerHTML = "Upload or Remove Key";
}
else {
htmx.find("dialog #ssh_change").disabled = false;
htmx.find("dialog #ssh_change").innerHTML = "Upload Key";
}
});
htmx.on("dialog select[name=ssh_remove]", "change", e => {
htmx.find("dialog input[name=ssh_add]").value = "";
if (e.target.value === "-") {
htmx.find("dialog #ssh_change").disabled = true;
htmx.find("dialog #ssh_change").innerHTML = "Upload or Remove Key";
}
else {
htmx.find("dialog #ssh_change").disabled = false;
htmx.find("dialog #ssh_change").innerHTML = "Remove Key";
}
});
htmx.on("dialog #ssh_change", "click", e => {
const upload = htmx.find("dialog input[name=ssh_add]").files[0];
const remove = htmx.find("dialog select[name=ssh_remove]").value;
if (upload) {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
source: e.currentTarget,
values: {
"ssh_add": upload
},
target: "dialog select[name=ssh_remove]"
}).then(_ => htmx.find("dialog input[name=ssh_add]").value = "");
}
else if (remove) {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
values: {
"ssh_remove": remove
},
target: "dialog select[name=ssh_remove]"
});
}
htmx.find("dialog #ssh_change").disabled = true;
htmx.find("dialog #ssh_change").innerHTML = "Upload or Remove Key";
});
{% } %}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("changes")}}

623
files/app/main/status/e/dhcp.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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();
}
%}
<div class="dialog">
{{_R("dialog-header", "LAN DHCP")}}
<div>
<div class="dhcp">
<div>
<div class="cols">
<div>
<div class="o">Address Reservations</div>
<div class="m">Hostnames with fixed addresses</div>
</div>
<button {{length(options) === reservations ? "disabled" : ""}}>+</button>
</div>
</div>
{{_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 <b>do not propagate</b>")}}
<div id="dhcp-reservations">
<div class="cols reservation-label">
<div style="white-space:nowrap">
<div>hostname</div>
<div>ip address</div>
<div>mac a&zwnj;ddress</div>
<div>do not propagate</div>
</div>
<div></div>
</div>
{% if (reservations > 0) {
for (let i = 0; i < length(options); i++) {
const o = options[i];
if (o.reserved) {
%}
<div class="cols reservation" data-ip="{{o.ip}}">
<div style="white-space:nowrap">
<input name="hostname" type="text" required placeholder="hostname" value="{{o.name}}">
<select class="dhcp-addresses">
</select>
<input name="mac" type="text" required placeholder="mac a&zwnj;ddress" pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="{{o.mac}}">
<label><input type="checkbox" {{o.noprop ? "checked" : ""}}></label>
</div>
<button>-</button>
</div>
{% }
}
} %}
</div>
<hr>
<div class="o">Active Leases</div>
<div class="m">Addresses currently in use</div>
{{_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) {
%}
<div class="cols lease-label">
<div style="white-space:nowrap">
<div>hostname</div>
<div>ip address</div>
<div>mac a&zwnj;ddress</div>
</div>
<div></div>
</div>
{%
for (let i = 0; i < length(options); i++) {
const o = options[i];
if (o.leased) {
%}
<div class="cols lease" data-ip="{{o.ip}}">
<div style="white-space:nowrap">
<input readonly type="text" value="{{o.name}}">
<input readonly type="text" value="{{o.ip}}">
<input readonly type="text" value="{{o.mac}}">
</div>
<button {{o.reserved ? "disabled" : ""}}>+</button>
</div>
{% }
}
} %}
</div>
{{_R("dialog-advanced")}}
<div>
{% if (includeAdvanced) { %}
<div class="dhcp-tags">
<div class="cols">
<div>
<div class="o">Tags</div>
<div class="m">Tags for advanced options</div>
</div>
<button>+</button>
</div>
<div class="cols dhcptag-label">
<div class="row">
<div>tag</div>
<div>type</div>
<div>match</div>
</div>
</div>
<div class="list noborder">{%
for (let i = 0; i < length(advtags); i++) {
const t = advtags[i];
%}<div class="cols">
<div class="row">
<input name="tag_name" type="text" required value="{{t.name}}">
<select name="tag_type" required>
<option value="">-</option>
<option value="vendorclass" {{t.type == "vendorclass" ? "selected": ""}}>Vendor Class</option>
<option value="userclass" {{t.type == "userclass" ? "selected": ""}}>User Class</option>
<option value="mac" {{t.type == "mac" ? "selected": ""}}>MAC Address</option>
<option value="circuitid" {{t.type == "circuitid" ? "selected": ""}}>Agent Circuit ID</option>
<option value="agentid" {{t.type == "agentid" ? "selected": ""}}>Agent Remove ID</option>
<option value="subscriberid" {{t.type == "subscribedid" ? "selected": ""}}>Subscriber ID</option>
</select>
<input name="tag_match" type="text" required value="{{t.match}}">
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
<div class="dhcp-options">
<div class="cols">
<div>
<div class="o">Options</div>
<div class="m">Advanced options</div>
</div>
<button>+</button>
</div>
<div class="cols dhcpoption-label">
<div class="row">
<div>tag</div>
<div>option</div>
<div>value</div>
<div>always</div>
</div>
</div>
<div class="list noborder">{%
for (let i = 0; i < length(advoptions); i++) {
const o = advoptions[i];
%}<div class="cols">
<div class="row">
<select name="option_name">
<option value="{{o.name}}" selected>{{o.name}}</option>
</select>
<select name="option_type" required>
<option value="">-</option>
<option value="1" {{o.type === 1 ? "selected": ""}}>1: netmask</option>
<option value="2" {{o.type === 2 ? "selected": ""}}>2: time-offset</option>
<option value="3" {{o.type === 4 ? "selected": ""}}>3: router</option>
<option value="6" {{o.type === 6 ? "selected": ""}}>6: dns-server</option>
<option value="7" {{o.type === 7 ? "selected": ""}}>7: log-server</option>
<option value="9" {{o.type === 9 ? "selected": ""}}>9: lpr-server</option>
<option value="13" {{o.type === 13 ? "selected": ""}}>13: boot-file-size</option>
<option value="15" {{o.type === 15 ? "selected": ""}}>15: domain-name</option>
<option value="16" {{o.type === 16 ? "selected": ""}}>16: swap-server</option>
<option value="17" {{o.type === 17 ? "selected": ""}}>17: root-path</option>
<option value="18" {{o.type === 18 ? "selected": ""}}>18: extension-path</option>
<option value="19" {{o.type === 19 ? "selected": ""}}>19: ip-forward-enable</option>
<option value="20" {{o.type === 20 ? "selected": ""}}>20: non-local-source-routing</option>
<option value="21" {{o.type === 21 ? "selected": ""}}>21: policy-filter</option>
<option value="22" {{o.type === 22 ? "selected": ""}}>22: max-datagram-reassembly</option>
<option value="23" {{o.type === 23 ? "selected": ""}}>23: default-ttl</option>
<option value="26" {{o.type === 26 ? "selected": ""}}>26: mtu</option>
<option value="27" {{o.type === 27 ? "selected": ""}}>27: all-subnets-local</option>
<option value="31" {{o.type === 31 ? "selected": ""}}>31: router-discovery</option>
<option value="32" {{o.type === 32 ? "selected": ""}}>32: router-solicitation</option>
<option value="33" {{o.type === 33 ? "selected": ""}}>33: static-route</option>
<option value="34" {{o.type === 34 ? "selected": ""}}>34: trailer-encapsulation</option>
<option value="35" {{o.type === 35 ? "selected": ""}}>35: arp-timeout</option>
<option value="36" {{o.type === 36 ? "selected": ""}}>36: ethernet-encap</option>
<option value="37" {{o.type === 37 ? "selected": ""}}>37: tcp-ttl</option>
<option value="38" {{o.type === 38 ? "selected": ""}}>38: tcp-keepalive</option>
<option value="40" {{o.type === 40 ? "selected": ""}}>40: nis-domain</option>
<option value="41" {{o.type === 41 ? "selected": ""}}>41: nis-server</option>
<option value="42" {{o.type === 42 ? "selected": ""}}>42: ntp-server</option>
<option value="44" {{o.type === 44 ? "selected": ""}}>44: netbios-ns</option>
<option value="45" {{o.type === 45 ? "selected": ""}}>45: netbios-dd</option>
<option value="46" {{o.type === 46 ? "selected": ""}}>46: netbios-nodetype</option>
<option value="47" {{o.type === 47 ? "selected": ""}}>47: netbios-scope</option>
<option value="48" {{o.type === 48 ? "selected": ""}}>48: x-windows-fs</option>
<option value="49" {{o.type === 49 ? "selected": ""}}>49: x-windows-dm</option>
<option value="58" {{o.type === 58 ? "selected": ""}}>58: T1</option>
<option value="59" {{o.type === 59 ? "selected": ""}}>59: T2</option>
<option value="60" {{o.type === 60 ? "selected": ""}}>60: vendor-class</option>
<option value="64" {{o.type === 64 ? "selected": ""}}>64: nis+-domain</option>
<option value="65" {{o.type === 65 ? "selected": ""}}>65: nis+-server</option>
<option value="66" {{o.type === 66 ? "selected": ""}}>66: tftp-server</option>
<option value="67" {{o.type === 67 ? "selected": ""}}>67: bootfile-name</option>
<option value="68" {{o.type === 68 ? "selected": ""}}>68: mobile-ip-home</option>
<option value="69" {{o.type === 69 ? "selected": ""}}>69: smtp-server</option>
<option value="70" {{o.type === 70 ? "selected": ""}}>70: pop3-server</option>
<option value="71" {{o.type === 71 ? "selected": ""}}>71: nntp-server</option>
<option value="74" {{o.type === 74 ? "selected": ""}}>74: irc-server</option>
<option value="77" {{o.type === 77 ? "selected": ""}}>77: user-class</option>
<option value="80" {{o.type === 80 ? "selected": ""}}>80: rapid-commit</option>
<option value="93" {{o.type === 93 ? "selected": ""}}>93: client-arch</option>
<option value="94" {{o.type === 94 ? "selected": ""}}>94: client-interface-id</option>
<option value="97" {{o.type === 97 ? "selected": ""}}>97: client-machine-id</option>
<option value="100" {{o.type === 100 ? "selected": ""}}>100: posix-timezone</option>
<option value="101" {{o.type === 101 ? "selected": ""}}>101: tzdb-timezone</option>
<option value="108" {{o.type === 108 ? "selected": ""}}>108: ipv6-only</option>
<option value="119" {{o.type === 119 ? "selected": ""}}>119: domain-search</option>
<option value="120" {{o.type === 120 ? "selected": ""}}>120: sip-server</option>
<option value="121" {{o.type === 121 ? "selected": ""}}>121: classless-static-route</option>
<option value="125" {{o.type === 125 ? "selected": ""}}>125: vendor-id-encap</option>
<option value="150" {{o.type === 150 ? "selected": ""}}>150: tftp-server-address</option>
<option value="255" {{o.type === 255 ? "selected": ""}}>255: server-ip-addressk</option>
</select>
<input name="option_value" type="text" required value="{{o.value}}">
<input name="option_always" type="checkbox" {{o.always ? "checked" : ""}}>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
const options = {{sprintf("%J", options)}};
function update()
{
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { options: JSON.stringify(options) }
});
}
function refreshIPSelectors()
{
const addrs = htmx.findAll(".dhcp-addresses");
for (let i = 0; i < addrs.length; i++) {
let opt = "";
for (let j = 0; j < options.length; j++) {
const o = options[j];
const self = o.ip === addrs[i].parentNode.parentNode.dataset.ip;
if (self || (!o.leased && !o.reserved)) {
opt += `<option value="${o.ip}" ${self ? "selected" : ""}>${o.ip}</option>`;
}
}
addrs[i].innerHTML = opt;
}
}
refreshIPSelectors();
htmx.on("#ctrl-modal .dialog .dhcp", "change", event => {
const target = event.target;
switch (target.nodeName) {
case "SELECT":
{
const oldip = target.parentNode.parentNode.dataset.ip;
const newip = target.value;
const oo = options.find(o => o.ip == oldip);
const no = options.find(o => o.ip == newip);
Object.assign(no, { name: oo.name, mac: oo.mac, noprop: oo.noprop, reserved: true, leased: false });
Object.assign(oo, { name: "", mac: "", noprop: false, reserved: false, leased: false });
target.parentNode.parentNode.dataset.ip = newip;
refreshIPSelectors();
update();
break;
}
case "INPUT":
switch (target.type) {
case "text":
{
if (target.validity.valid) {
const ip = target.parentNode.parentNode.dataset.ip;
const o = options.find(o => o.ip == ip);
if (target.name === "hostname") {
o.name = target.value;
}
else {
o.mac = target.value;
}
update();
}
break;
}
case "checkbox":
{
const ip = target.parentNode.parentNode.parentNode.dataset.ip;
const o = options.find(o => o.ip == ip);
o.noprop = target.checked;
update();
break;
}
default:
break;
}
break;
default:
break;
}
});
htmx.on("#ctrl-modal .dialog .dhcp", "click", event => {
const target = event.target;
switch (target.nodeName) {
case "BUTTON":
switch (target.innerText) {
case "+":
{
const ip = target.parentNode.dataset.ip;
if (ip) {
target.disabled = true;
const o = options.find(o => o.ip == ip);
o.reserved = true;
o.noprop = true;
const item = document.createElement("div");
item.innerHTML = `<div class="cols reservation" data-ip="${o.ip}"><div style="white-space:nowrap"><input name="hostname" type="text" placeholder="hostname" value="${o.name}" required> <select class="dhcp-addresses"></select> <input name="mac" type="text" placeholder="mac a&zwnj;ddress" required pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="${o.mac}"><label> <input type="checkbox" ${o.noprop ? "checked" : ""}></label></div><button>-</button></div>`;
htmx.find("#dhcp-reservations").appendChild(item.firstChild);
update();
}
else {
const o = options.find(o => !o.reserved && !o.leased);
if (o) {
Object.assign(o, { name: "", mac: "", noprop: true, reserved: true, leased: false });
const item = document.createElement("div");
item.innerHTML = `<div class="cols reservation" data-ip="${o.ip}"><div style="white-space:nowrap"><input name="hostname" type="text" placeholder="hostname" value="${o.name}" required> <select class="dhcp-addresses"></select> <input name="mac" type="text" placeholder="mac a&zwnj;ddress" required pattern="([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]" value="${o.mac}"><label> <input type="checkbox" ${o.noprop ? "checked" : ""}></label></div><button>-</button></div>`;
htmx.find("#dhcp-reservations").appendChild(item.firstChild);
}
if (!options.find(o => !o.reserved && !o.leased)) {
target.disabled = true;
}
}
refreshIPSelectors();
break;
}
case "-":
{
const ip = target.parentNode.dataset.ip;
const o = options.find(o => o.ip == ip);
o.reserved = false;
if (o.leased) {
const l = htmx.find(`#ctrl-modal .dialog .lease[data-ip="${ip}"] button`);
l.disabled = false;
}
const item = target.parentNode;
htmx.remove(item);
htmx.find("#ctrl-modal .dialog .dhcp button").disabled = false;
refreshIPSelectors();
update();
break;
}
default:
break;
}
break;
default:
break;
}
});
{% if (includeAdvanced) { %}
function refreshAdvOptions()
{
const tagnames = [];
const names = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row input[name=tag_name]");
for (let i = 0; i < names.length; i++) {
if (names[i].validity.valid && tagnames.indexOf(names[i].value) === -1) {
tagnames.push(names[i].value);
}
}
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row select[name=option_name]");
for (let i = 0; i < options.length; i++) {
const v = options[i].value;
options[i].innerHTML = "<option value=''>[all]</option>" + tagnames.map(t => `<option value="${t}" ${t === v ? "selected": ""}>${t}</option>`).join("");
}
}
function updateTags()
{
const advtags = [];
const tags = htmx.findAll("#ctrl-modal .dialog .dhcp-tags .list .row");
for (let i = 0; i < tags.length; i++) {
const name = htmx.find(tags[i], "input[name=tag_name]");
const type = htmx.find(tags[i], "select[name=tag_type]");
const match = htmx.find(tags[i], "input[name=tag_match]");
if (name.validity.valid && type.validity.valid && match.validity.valid) {
advtags.push({ name: name.value, type: type.value, match: match.value });
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { advtags: JSON.stringify(advtags) }
});
}
function updateOptions()
{
const advoptions = [];
const options = htmx.findAll("#ctrl-modal .dialog .dhcp-options .list .row");
for (let i = 0; i < options.length; i++) {
const name = htmx.find(options[i], "select[name=option_name]");
const type = htmx.find(options[i], "select[name=option_type]");
const value = htmx.find(options[i], "input[name=option_value]");
const always = htmx.find(options[i], "input[name=option_always]");
if (name.validity.valid && type.validity.valid && value.validity.valid) {
advoptions.push({ name: name.value, type: type.value, value: value.value, always: !!always.checked });
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { advoptions: JSON.stringify(advoptions) }
});
}
htmx.on("#ctrl-modal .dialog .dhcp-tags", "click", event => {
const target = event.target;
switch (target.nodeName) {
case "BUTTON":
switch (target.innerText) {
case "+":
const item = document.createElement("div");
item.innerHTML = `<div class="cols"><div class="row"><input name="tag_name" type="text" required value=""> <select name="tag_type" required><option value="">-</option><option value="vendorclass">Vendor Class</option><option value="userclass">User Class</option><option value="mac">MAC Address</option><option value="circuitid">Agent Circuit ID</option><option value="agentid">Agent Remove ID</option><option value="subscriberid">Subscriber ID</option></select> <input name="tag_match" type="text" required value=""></div><button>-</button></div>`;
htmx.find("#ctrl-modal .dialog .dhcp-tags .list").appendChild(item.firstChild);
break;
case "-":
const row = target.parentNode;
row.parentNode.removeChild(row);
refreshAdvOptions();
updateTags();
updateOptions();
break;
default:
break;
}
break;
default:
break;
}
});
htmx.on("#ctrl-modal .dialog .dhcp-tags", "change", _ => {
refreshAdvOptions();
updateTags();
});
htmx.on("#ctrl-modal .dialog .dhcp-options", "click", event => {
const target = event.target;
switch (target.nodeName) {
case "BUTTON":
switch (target.innerText) {
case "+":
const item = document.createElement("div");
item.innerHTML = `<div class="cols"><div class="row"><select name="option_name"><option value="">[all]</option></select> <select name="option_type" required><option value="">-</option><option value="1">1: netmask</option><option value="2">2: time-offset</option><option value="3">3: router</option><option value="6">6: dns-server</option><option value="7">7: log-server</option><option value="9">9: lpr-server</option><option value="13">13: boot-file-size</option><option value="15">15: domain-name</option><option value="16">16: swap-server</option><option value="17">17: root-path</option><option value="18">18: extension-path</option><option value="19">19: ip-forward-enable</option><option value="20">20: non-local-source-routing</option><option value="21">21: policy-filter</option><option value="22">22: max-datagram-reassembly</option><option value="23">23: default-ttl</option><option value="26">26: mtu</option><option value="27">27: all-subnets-local</option><option value="31">31: router-discovery</option><option value="32">32: router-solicitation</option><option value="33">33: static-route</option><option value="34">34: trailer-encapsulation</option><option value="35">35: arp-timeout</option><option value="36">36: ethernet-encap</option><option value="37">37: tcp-ttl</option><option value="38">38: tcp-keepalive</option><option value="40">40: nis-domain</option><option value="41">41: nis-server</option><option value="42">42: ntp-server</option><option value="44">44: netbios-ns</option><option value="45">45: netbios-dd</option><option value="46">46: netbios-nodetype</option><option value="47">47: netbios-scope</option><option value="48">48: x-windows-fs</option><option value="49">49: x-windows-dm</option><option value="58">58: T1</option><option value="59">59: T2</option><option value="60">60: vendor-class</option><option value="64">64: nis+-domain</option><option value="65">65: nis+-server</option><option value="66">66: tftp-server</option><option value="67">67: bootfile-name</option><option value="68">68: mobile-ip-home</option><option value="69">69: smtp-server</option><option value="70">70: pop3-server</option><option value="71">71: nntp-server</option><option value="74">74: irc-server</option><option value="77">77: user-class</option><option value="80">80: rapid-commit</option><option value="93">93: client-arch</option><option value="94">94: client-interface-id</option><option value="97">97: client-machine-id</option><option value="100">100: posix-timezone</option><option value="101">101: tzdb-timezone</option><option value="108">108: ipv6-only</option><option value="119">119: domain-search</option><option value="120">120: sip-server</option><option value="121">121: classless-static-route</option><option value="125">125: vendor-id-encap</option><option value="150">150: tftp-server-address</option><option value="255">255: server-ip-addressk</option></select> <input name="option_value" type="text" required value=""> <input name="option_always" type="checkbox"></div><button>-</button></div>`;
htmx.find("#ctrl-modal .dialog .dhcp-options .list").appendChild(item.firstChild);
refreshAdvOptions();
break;
case "-":
const row = target.parentNode;
row.parentNode.removeChild(row);
updateOptions();
break;
default:
break;
}
break;
default:
break;
}
});
htmx.on("#ctrl-modal .dialog .dhcp-options", "change", _ => {
updateOptions();
});
refreshAdvOptions();
{% } %}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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(`<div id="dialog-messages-error" hx-swap-oob="true">ERROR: ${upgrade.error}</div>`);
}
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(`<div id="dialog-messages-error" hx-swap-oob="true">ERROR: ${upgrade.error}</div>`);
print(`<div id="firmware-upload" hx-swap-oob="true><progress value="0" max="100"></div>`);
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 = `<option value="-">-</option>`;
for (let k in firmware_list) {
html += `<option value="${k}">${k}${index(k, "-") == -1 ? "" : " (nightly)"}</option>`;
}
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");
%}
<div class="dialog">
{{_R("dialog-header", "Firmware")}}
<div id="firmware-update">
{{_R("dialog-messages")}}
<div class="cols">
<div>
<div class="o">Firmware</div>
<div class="m">Current firmware version</div>
</div>
<div style="flex:0">
<input type="text" disabled size="35" value="{{configuration.getFirmwareVersion()}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Hardware</div>
<div class="m">Hardware type</div>
</div>
<div style="flex:0">
<input type="text" disabled size="35" value="{{hardware.getHardwareType()}}">
</div>
</div>
{{_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.")}}
<hr>
<div>
<div class="cols">
<div>
<div class="o">Download Firmware</div>
<div class="m">Download firmware from an AREDN server.</div>
</div>
<div style="flex:0">
<select id="download-firmware" {{sideload ? 'disabled' : ''}}>
<option value="-">-</option>
{%
const f = fs.open("/tmp/firmware.list");
if (f) {
const list = json(f.read("all"));
f.close();
for (let k in list) {
print(`<option value="${k}">${k}${index(k, "-") == -1 ? "" : " (nightly)"}</option>`);
}
}
%}
</select>
</div>
<div style="flex:0">
<div id="firmware-refresh"><button class="icon refresh"></button></div>
</div>
</div>
{{_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.")}}
<br>
<div class="cols">
<div>
<div class="o">Upload Firmware</div>
<div class="m">Upload a firmware file from your computer.</div>
</div>
<div style="flex:0">
<input type="file" accept=".bin,.gz" {{sideload ? 'disabled' : ''}}>
</div>
</div>
{{_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.")}}
<br>
<div class="cols">
<div>
<div class="o">Sideload Firmware</div>
<div class="m">Use an alternatve way to load firmware onto the node.</div>
</div>
<div style="flex:0">
<input id="sideload-firmware" type="text" disabled placeholder="/tmp/local_firmware" value="{{sideload ? '/tmp/local_firmware' : ''}}">
</div>
</div>
{{_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")}}
<div>
{% if (includeAdvanced) { %}
{% if (fs.access("/rom/etc")) { %}
<div class="cols">
<div>
<div class="o">Keep Configuration</div>
<div class="m">Keep existing configuration after upgrade.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "keepconfig", value: true })}}
</div>
</div>
{{_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.")}}
{% } %}
<div class="cols">
<div>
<div class="o">Dangerous Upgrade</div>
<div class="m">Force the firmware onto the device, even if it fails the safety checks.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "dangerousupgrade", value: false })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Firmware URL</div>
<div class="m">URL for downloading firmware</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="firmwareurl" type="text" size="45" pattern="{{constants.patUrl}}" value="{{uciMesh.get("aredn", "@downloads[0]", "firmware_aredn")}}">
</div>
</div>
{{_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.")}}
{% } %}
</div>
<div style="flex:1"></div>
<div class="cols" style="padding-top:16px">
<div id="firmware-upload"><progress value="0" max="100"></div>
<div style="flex:0">
<button id="fetch-and-update" {{sideload ? '' : 'disabled'}} hx-trigger="none" hx-encoding="multipart/form-data">{{sideload ? 'Update' : 'Fetch and Update'}}</button>
</div>
</div>
{{_H("<br>Depending on how the firmware it to be installed using the options above, this button will initiate the process.")}}
</div>
</div>
{{_R("dialog-footer", "nocancel" )}}
<script>
(function(){
{{_R("open")}}
htmx.on("#firmware-update input[type='file']", "change", e => {
if (e.target.files[0]) {
htmx.find("#fetch-and-update").disabled = false;
}
else {
htmx.find("#fetch-and-update").disabled = true;
}
htmx.find("#download-firmware").value = "-";
});
htmx.on("#download-firmware", "change", e => {
if (e.target.value === "-") {
htmx.find("#fetch-and-update").disabled = true;
}
else {
htmx.find("#fetch-and-update").disabled = false;
}
htmx.find("#firmware-update input[type=file]").value = null;
});
htmx.on("#fetch-and-update", "click", e => {
htmx.find("#dialog-messages-error").innerHTML = "";
htmx.find("#dialog-done").disabled = true;
htmx.find("#fetch-and-update").disabled = true;
const upload = htmx.find("#firmware-update input[type=file]").files[0];
const download = htmx.find("#download-firmware").value;
if ({{sideload || false}}) {
htmx.ajax("POST", "{{request.env.REQUEST_URI}}", {
values: {
sideload: 1
},
swap: "none"
}).then( _ => htmx.find("#dialog-done").disabled = false);
}
else if (upload) {
const currentTarget = e.currentTarget;
{% if (hardware.isLowMemNode()) { %}
htmx.ajax("POST", "{{request.env.REQUEST_URI}}", {
values: {
firmwarefileprepare: 1
},
swap: "none"
}).then(_ => {
{% } %}
htmx.on(currentTarget, "htmx:xhr:progress", e => {
const p = htmx.find("#firmware-upload progress");
const v = e.detail.loaded / e.detail.total * 100;
if (v > 99) {
p.removeAttribute("value");
}
else {
p.setAttribute("value", v);
}
});
htmx.ajax("POST", "{{request.env.REQUEST_URI}}", {
source: currentTarget,
values: {
firmwarefile: upload
},
target: "#dialog-done",
swap: "none"
}).then( _ => htmx.find("#dialog-done").disabled = false);
{% if (hardware.isLowMemNode()) { %}
});
{% } %}
}
else if (download !== "-") {
const source = new EventSource(`{{request.env.REQUEST_URI}}?v=${download}`);
source.addEventListener("close", e => {
source.close();
const all = htmx.find("#all");
all.outerHTML = JSON.parse(e.data).v;
const scripts = document.querySelectorAll("#all script");
for (let i = 0; i < scripts.length; i++) {
eval(scripts[i].innerText);
}
});
source.addEventListener("error", e => {
source.close();
htmx.find("#firmware-upload progress").setAttribute("value", "0");
htmx.find("#dialog-messages-error").innerHTML = `ERROR: ${e.data || "Unknown error"}`;
htmx.find("#dialog-done").disabled = false;
});
source.addEventListener("progress", e => {
const p = htmx.find("#firmware-upload progress");
if (e.data > 99) {
p.removeAttribute("value");
}
else {
p.setAttribute("value", e.data);
}
});
}
});
htmx.on("#firmware-refresh", "click", e => {
htmx.find("#firmware-refresh button").classList.add("rotate");
htmx.find("#dialog-messages-error").innerHTML = "";
const source = new EventSource("{{request.env.REQUEST_URI}}?v=update");
source.addEventListener("close", e => {
source.close();
htmx.find("#firmware-refresh button").classList.remove("rotate");
const selector = htmx.find("#download-firmware");
selector.innerHTML = e.data;
selector.value = "-";
htmx.find("#firmware-update input[type=file]").value = null;
htmx.find("#fetch-and-update").disabled = true;
htmx.find("#firmware-upload progress").setAttribute("value", "0");
});
source.addEventListener("error", e => {
source.close();
htmx.find("#firmware-refresh button").classList.remove("rotate");
htmx.find("#firmware-upload progress").setAttribute("value", "0");
htmx.find("#dialog-messages-error").innerHTML = `ERROR: ${e.data || "Unknown error"}`;
});
source.addEventListener("progress", e => {
htmx.find("#firmware-upload progress").setAttribute("value", e.data);
});
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
}
%}
<div class="dialog">
{{_R("dialog-header", "Internal Services")}}
<div>
<div class="cols">
<div>
<div class="o">Cloud Mesh</div>
<div class="m">Use any Supernodes found on the mesh.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "cloudmesh", value: uciMesh.get("aredn", "@supernode[0]", "support") !== "0" })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">iPerf3 Server</div>
<div class="m">Enable the iperf3 server for easy connection speed testing</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "iperf", value: uciMesh.get("aredn", "@iperf[0]", "enable") !== "0" })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Remote logging</div>
<div class="m">Send internal logging information to a remove server</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="remotelog" type="text" size="24" placeholder="None" pattern="(tcp|udp)://((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}:\d+" value="{{uciMesh.get("aredn", "@remotelog[0]", "url") || ""}}">
</div>
</div>
{{_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 <b>udp://ip-address:port</b> or <b>tcp://ip-adress:port</b>.
Leave blank if no remote logging is required.")}}
<div class="cols">
<div>
<div class="o">WAN ssh</div>
<div class="m">Allow ssh access to node from the WAN interface</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "ssh_access", value: uciMesh.get("aredn", "@wan[0]", "ssh_access") !== "0" })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">WAN telnet</div>
<div class="m">Allow telnet access to node from the WAN interface</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "telnet_access", value: uciMesh.get("aredn", "@wan[0]", "telnet_access") !== "0" })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">WAN web</div>
<div class="m">Allow web access to node from the WAN interface</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "web_access", value: uciMesh.get("aredn", "@wan[0]", "web_access") !== "0" })}}
</div>
</div>
{{_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.")}}
<div class="hideable">
<div class="cols">
<div>
<div class="o">Watchdog</div>
<div class="m">Allow watchdog to reboot the node if it becomes unresponsive</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "watchdog", value: uciMesh.get("aredn", "@watchdog[0]", "enable") === "1" })}}
</div>
</div>
{{_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.
")}}
<div class="hideable0">
<div class="cols">
<div>
<div class="o">Watchdog IP address</div>
<div class="m">IP address to check periodically</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="watchdog_ping_address" type="text" size="20" placeholder="None" pattern="{{constants.patIP}}" value="{{uciMesh.get("aredn", "@watchdog[0]", "ping_addresses") || ""}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Daily Watchdog hour</div>
<div class="m">Reboot the node at a specific hour every day</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="watchdog_daily" type="text" size="20" placeholder="None" pattern="(\d|1\d|2[0-3])" value="{{uciMesh.get("aredn", "@watchdog[0]", "daily") || ""}}">
</div>
</div>
{{_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.")}}
</div>
</div>
{% if (hardware.hasPOE()) { %}
<div class="cols">
<div>
<div class="o">PoE Passthrough</div>
<div class="m">Enable power-over-ethernet on ports which support it</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "power_poe", value: uciMesh.get("aredn", "@poe[0]", "passthrough") !== "0" })}}
</div>
</div>
{{_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()) { %}
<div class="cols">
<div>
<div class="o">USB Power</div>
<div class="m">Enable USB power on ports which support it</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "power_usb", value: uciMesh.get("aredn", "@usb[0]", "passthrough") !== "0" })}}
</div>
</div>
{{_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.")}}
{% } %}
<div class="cols">
<div>
<div class="o">Message Updates</div>
<div class="m">Update messages every so many hours</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" type="text" name="message_pollrate" pattern="([1-9]|1[0-9]|2[0-4])" placeholder="1" value="{{uciMesh.get("aredn", "@alerts[0]", "pollrate")}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Local Message URL</div>
<div class="m">Configure the local message sources</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" type="text" name="message_localpath" pattern="{{constants.patUrl}}" value="{{uciMesh.get("aredn", "@alerts[0]", "localpath")}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Message Groups</div>
<div class="m">List of message group names</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" type="text" name="message_groups" pattern="[a-zA-Z]+(,[a-zA-Z]+)*" value="{{uciMesh.get("aredn", "@alerts[0]", "groups")}}">
</div>
</div>
{{_H("A comma seperated list of group names to check for messages.")}}
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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();
}
%}
<div class="dialog">
{{_R("dialog-header", "Node Services")}}
<div>
<div id="service-templates" class="cols">
<div>
<div class="o">Add service</div>
<div class="m">Add a service from a template</div>
</div>
<div style="flex:0;padding-right:10px">
<select style="direction:ltr">
{%
for (let i = 0; i < length(templates); i++) {
print(`<option value="${i}">${templates[i].name}</option>`);
if (templates[i].type) {
types[templates[i].type] = true;
}
}
%}
</select>
</div>
<button>+</button>
</div>
{{_H("Create a service by selecting from the templates above and hitting +. The two most generic templates are
<b>Generic URL</b> and <b>Simple text</b> 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.")}}
<div id="local-services">{%
for (let i = 0; i < length(services); i++) {
const s = services[i];
%}
<div class="service">
<div class="cols">
<div class="cols">
<input name="name" type="text" placeholder="{{s.link === false ? 'service information' : 'service name'}}" required pattern='[^-:"|<>]+' value="{{s.name}}">
<div style="flex:0">
<select name="type">
<option value="">-</option>
{%
for (t in types) {
print(`<option value="${t}" ${t === s.type ? "selected" : ""}>${t}</option>`);
}
%}
</select>
</div>
</div>
<button>-</button>
</div>
<div class="cols">
<div></div>
{% if (s.link === false) { %}
<div class="link">
<select name="hostname">
{%
for (j = 0; j < length(hosts); j++) {
print(`<option value="${hosts[j]}" ${hosts[j] === s.hostname ? "selected" : ""}>${hosts[j]}</option>`);
}
for (let j = 0; j < length(aliases.map); j++) {
print(`<option value="${aliases.map[j].hostname}" ${aliases.map[j].hostname === s.hostname ? "selected" : ""}>${aliases.map[j].hostname}</option>`);
}
%}
</select>
</div>
{% } else { %}
<div>
<input name="protocol" type="text" placeholder="proto" required pattern="[a-z]+" value="{{s.protocol}}">
:// <select name="hostname">
{%
for (j = 0; j < length(hosts); j++) {
print(`<option value="${hosts[j]}" ${hosts[j] === s.hostname ? "selected" : ""}>${hosts[j]}</option>`);
}
for (let j = 0; j < length(aliases.map); j++) {
print(`<option value="${aliases.map[j].hostname}" ${aliases.map[j].hostname === s.hostname ? "selected" : ""}>${aliases.map[j].hostname}</option>`);
}
%}
</select>
: <input name="port" type="text" placeholder="port" required pattern="([1-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])" value="{{s.port}}">
/ <input name="path" type="text" placeholder="path" pattern="[\-\/\?\&\._=#a-zA-Z0-9]*" value="{{s.path}}">
</div>
{% } %}
</div>
</div>
{% } %}</div>
<hr>
<div id="host-aliases-add" class="cols">
<div>
<div class="o">Host aliases</div>
<div class="m">DNS hostname aliases</div>
</div>
<button>+</button>
</div>
{{_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.")}}
<div id="host-aliases">{%
for (let i = 0; i < length(aliases.map); i++) {
%}
<div class="cols alias">
<input type="text" name="hostname" value="{{aliases.map[i].hostname}}" required pattern="[a-zA-Z][a-zA-Z0-9_\-]*">
<div>
<select name="address">
{%
for (let j = as[3]; j <= ae[3]; j++) {
const a = `${as[0]}.${as[1]}.${as[2]}.${j}`;
const n = aliases.leases[a];
print(`<option value="${a}" ${a === aliases.map[i].address ? "selected" : ""}>${n ? n : a}</option>`);
}
%}
</select>
</div>
<button>-</button>
</div>
{%
}
%}</div>
<hr>
<div id="port-forward-add" class="cols">
<div>
<div class="o">Port Forwarding</div>
{% if (dhcp.mode === 0) { %}
<div class="m">Forward WAN and Mesh ports to LAN</div>
{% } else { %}
<div class="m">Forward WAN port to LAN</div>
{% } %}
</div>
<button>+</button>
</div>
{% 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 <b>start-end</b>.")}}
{% } 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 <b>start-end</b>.")}}
{% } %}
<div class="cols port-forwards-label">
<div class="row">
<div>addresses</div>
<div>ports</div>
<div>protocol</div>
<div>enabled</div>
</div>
</div>
<div id="port-forwards">{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
%}<div class="cols noborder">
<div class="row">
<div>
{% if (dhcp.mode === 0) { %}
<select name="port_src">
<option value="wifi" {{p.src == "wifi" ? "selected" : ""}}>Mesh</option>
<option value="wan" {{p.src == "wan" ? "selected" : ""}}>WAN</option>
<option value="both" {{p.src == "both" ? "selected" : ""}}>Mesh &amp; WAN</option>
</select>
{% } else { %}
<input name="port_src" type="text" disabled value="WAN">
{% } %}
<input name="port_sports" type="text" required placeholder="Port or range" pattern="([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-5])(?:-([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-5]))?" value="{{p.sports}}">
<select name="port_type">
<option value="tcp" {{p.type == "tcp" ? "selected" : ""}}>TCP</option>
<option value="udp" {{p.type == "udp" ? "selected" : ""}}>UDP</option>
<option value="both" {{p.type == "both" ? "selected" : ""}}>TCP &amp; UDP</option>
</select>
<label class="switch"><input type="checkbox" name="port_enable" {{p.enabled ? "checked" : ""}}></label>
</div>
<div>
<select name="port_dst">
{%
for (let j = as[3]; j <= ae[3]; j++) {
const a = `${as[0]}.${as[1]}.${as[2]}.${j}`;
const n = aliases.leases[a];
print(`<option value="${a}" ${a === p.dst ? "selected" : ""}>${n ? n : a}</option>`);
}
%}
</select>
<input name="port_dport" type="text" required placeholder="LAN Port" pattern="{{constants.patPort}}" value="{{p.dport}}">
</div>
</div>
<button>-</button>
</div>{%
}
%}</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
const templates = {{templates}};
const hosts = {{hosts}};
function updateServices() {
const services = [];
const svc = htmx.findAll("#local-services .service");
for (let i = 0; i < svc.length; i++) {
const s = svc[i];
const name = htmx.find(s, "input[name=name]");
const type = htmx.find(s, "select[name=type]");
const protocol = htmx.find(s, "input[name=protocol]");
const host = htmx.find(s, "select[name=hostname]");
const port = htmx.find(s, "input[name=port]");
const path = htmx.find(s, "input[name=path]");
if (protocol && port && path) {
if (name.validity.valid && protocol.validity.valid && port.validity.valid && path.validity.valid) {
services.push(`${name.value}${type.value ? " [" + type.value + "]" : ""}|1|${protocol.value}|${host.value}|${port.value}|${path.value}`);
}
}
else {
if (name.validity.valid) {
services.push(`${name.value}${type.value ? " [" + type.value + "]" : ""}|0||${host.value}||`);
}
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { services: JSON.stringify(services) }
});
}
function updateAliases()
{
const aliases = [];
const al = htmx.findAll("#host-aliases .alias");
for (let i = 0; i < al.length; i++) {
const s = al[i];
const hostname = htmx.find(s, "input[name=hostname]");
const address = htmx.find(s, "select[name=address]");
if (hostname.validity.valid) {
aliases.push(`${address.value} ${hostname.value}`);
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { aliases: JSON.stringify(aliases) }
});
}
function updatePortForwards()
{
const ports = [];
const rows = htmx.findAll("#port-forwards .cols .row");
for (let i = 0; i < rows.length; i++) {
const r = rows[i];
const src = htmx.find(r, "[name=port_src]");
const sports = htmx.find(r, "[name=port_sports]");
const type = htmx.find(r, "[name=port_type]");
const dst = htmx.find(r, "[name=port_dst]");
const dport = htmx.find(r, "[name=port_dport]");
const enabled = htmx.find(r, "[name=port_enable]");
if (sports.validity.valid && dport.validity.valid) {
ports.push(`${src.value}:${type.value}:${sports.value}:${dst.value}:${dport.value}:${enabled.checked ? "1" : "0"}`);
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { ports: JSON.stringify(ports) }
});
}
function refreshHostSelectors()
{
const selectors = htmx.findAll("#local-services .service .cols:last-child select");
for (let i = 0; i < selectors.length; i++) {
const s = selectors[i];
const v = s.value;
let o = "";
for (j = 0; j < hosts.length; j++) {
o += `<option value="${hosts[j]}" ${hosts[j] === v ? "selected" : ""}>${hosts[j]}</option>`;
}
const al = htmx.findAll("#host-aliases .alias");
for (let j = 0; j < al.length; j++) {
const hostname = htmx.find(al[j], "input[name=hostname]");
if (hostname.validity.valid) {
o += `<option value="${hostname.value}" ${hostname.value === v ? "selected" : ""}>${hostname.value}</option>`;
}
}
s.innerHTML = o;
}
}
htmx.on("#service-templates button", "click", _ => {
const t = templates[htmx.find("#service-templates select").value];
if (t) {
let template;
if (t.link === false) {
template = `<div class="service">
<div class="cols">
<div class="cols">
<input name="name" type="text" placeholder="service name" required pattern='[^-:"|<>]+' value="${t.text || t.name}">
<div style="flex:0">
<select name="type">
<option value="">-</option>
{%
for (t in types) {
print(`<option value="${t}">${t}</option>`);
}
%}
</select>
</div>
</div>
<button>-</button>
</div>
<div class="cols">
<div></div>
<div class="link">
<select name="hostname"></select>
</div>
</div>
</div>`;
}
else {
template = `<div class="service">
<div class="cols">
<div class="cols">
<input name="name" type="text" placeholder="service name" required pattern='[^-:"|<>]+' value="${t.text || t.name}">
<div style="flex:0">
<select name="type">
<option value="">-</option>
{%
for (t in types) {
print(`<option value="${t}">${t}</option>`);
}
%}
</select>
</div>
</div>
<button>-</button>
</div>
<div class="cols">
<div></div>
<div>
<input name="protocol" type="text" placeholder="proto" required pattern="[a-z]+" value="${t.protocol || ''}">
:// <select name="hostname"></select>
: <input name="port" type="text" placeholder="port" required pattern="([1-9]|[1-9]\\d{1,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])" value="${t.port || ''}">
/ <input name="path" type="text" placeholder="path" pattern="[\\-\\/\\?\\&\\._=#a-zA-Z0-9]*" value="${t.path || ''}">
</div>
</div>
</div>`;
}
const div = document.createElement("div");
div.innerHTML = template;
htmx.find(div, "select[name=type]").value = t.type || "";
const ls = htmx.find("#local-services");
ls.insertBefore(div.firstChild, ls.firstChild);
refreshHostSelectors();
updateServices();
}
});
htmx.on("#local-services", "click", event => {
const target = event.target;
if (target.nodeName === "BUTTON") {
const service = target.parentNode.parentNode;
htmx.remove(service);
updateServices();
}
});
htmx.on("#local-services", "change", updateServices);
htmx.on("#local-services", "select", updateServices);
htmx.on("#host-aliases-add button", "click", _ => {
const div = document.createElement("div");
div.innerHTML = `<div class="cols alias">
<input type="text" name="hostname" value="" required pattern="[a-zA-Z][a-zA-Z0-9_\-]*">
<div>
<select name="address">
{%
for (let j = as[3]; j <= ae[3]; j++) {
const a = `${as[0]}.${as[1]}.${as[2]}.${j}`;
const n = aliases.leases[a];
print(`<option value="${a}">${n ? n : a}</option>`);
}
%}
</select>
</div>
<button>-</button>
</div>`;
const ha = htmx.find("#host-aliases");
ha.insertBefore(div.firstChild, ha.firstChild);
refreshHostSelectors();
updateAliases();
});
htmx.on("#host-aliases", "click", event => {
const target = event.target;
if (target.nodeName === "BUTTON") {
const alias = target.parentNode;
htmx.remove(alias);
refreshHostSelectors();
updateAliases();
}
});
htmx.on("#host-aliases", "change", _ => { refreshHostSelectors(); updateAliases(); });
htmx.on("#host-aliases", "select", _ => { refreshHostSelectors(); updateAliases(); });
htmx.on("#port-forwards", "click", event => {
const target = event.target;
if (target.nodeName === "BUTTON") {
const forward = target.parentNode;
htmx.remove(forward);
updatePortForwards();
}
});
htmx.on("#port-forward-add button", "click", _ => {
const div = document.createElement("div");
div.innerHTML =
`<div class="cols noborder">
<div class="row">
<div>
{% if (dhcp.mode === 0) { %}
<select name="port_src">
<option value="wifi">Mesh</option>
<option value="wan">WAN</option>
<option value="both">Mesh &amp; WAN</option>
</select>
{% } else { %}
<input name="port_src" type="text" disabled value="WAN">
{% } %}
<input name="port_sports" type="text" required placeholder="Port or range" pattern="([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-5])(?:-([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-5]))?" value="">
<select name="port_type">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="both">TCP &amp; UDP</option>
</select>
<label class="switch"><input type="checkbox" name="port_enable"></label>
</div>
<div>
<select name="port_dst">
{%
for (let j = as[3]; j <= ae[3]; j++) {
const a = `${as[0]}.${as[1]}.${as[2]}.${j}`;
const n = aliases.leases[a];
print(`<option value="${a}">${n ? n : a}</option>`);
}
%}
</select>
<input name="port_dport" type="text" required placeholder="LAN Port" pattern="{{constants.patPort}}" value="">
</div>
</div>
<button>-</button>
</div>`;
htmx.find("#port-forwards").appendChild(div.firstChild);
});
htmx.on("#port-forwards", "change", _ => updatePortForwards());
htmx.on("#port-forwards", "select", _ => updatePortForwards());
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
%}
<div class="dialog">
{{_R("dialog-header", "Location")}}
<div>
{% if (mapurl) { %}
<div id="location-edit-map">
<div class="icon plus"></div>
<iframe src="{{mapurl}}"></iframe>
</div>
{% } %}
<div class="cols">
<div>
<div class="o">Latitude</div>
<div class="m">Node's latitude</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="lat" type="text" size="10" pattern="-?\d+(\.\d+)?" value="{{lat}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Longitude</div>
<div class="m">Node's longitude</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="lon" type="text" size="10" pattern="-?\d+(\.\d+)?" value="{{lon}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Gridsquare</div>
<div class="m">Maidenhead gridsquare</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="gridsquare" type="text" size="7" pattern="[A-X][A-X]\d\d[a-x][a-x]" value="{{gridsquare}}">
</div>
</div>
{{_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")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">GPS Location</div>
<div class="m">Use local or network GPS to set location</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "gps_enable", value: uciMesh.get("aredn", "@location[0]", "gps_enable") === "1" })}}
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Map URL</div>
<div class="m">URL for embedded map</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="mapurl" type="text" size="45" pattern="{{constants.patUrl}}" value="{{map}}">
</div>
</div>
{{_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.")}}
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
const lat = htmx.find("#ctrl-modal .dialog input[name=lat]");
const lon = htmx.find("#ctrl-modal .dialog input[name=lon]");
const gridsquare = htmx.find("#ctrl-modal .dialog input[name=gridsquare]");
const map = htmx.find("#ctrl-modal .dialog iframe");
let mapt = null;
if (map) {
window.addEventListener("message", e => {
const msg = JSON.parse(e.data);
if (msg.type === "location") {
lat.value = parseFloat(msg.lat).toFixed(5);
lon.value = parseFloat(msg.lon).toFixed(5);
clearTimeout(mapt);
mapt = setTimeout(() => {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { lat: lat.value, lon: lon.value, gridsquare: gridsquare.value }
});
}, 500);
latlon2gridsquare();
}
});
}
function llchange() {
if (lat.value !== "" && lon.value !== "") {
if (map) {
map.contentWindow.postMessage(JSON.stringify({ type: "change-location", lat: lat.value, lon: lon.value }), "*");
}
latlon2gridsquare();
if (!map) {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { lat: lat.value, lon: lon.value, gridsquare: gridsquare.value }
});
}
}
else if (lat.value === "" && lon.value === "") {
gridsquare.value = "";
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { lat: "", lon: "", gridsquare: "" }
});
}
}
lat.addEventListener("change", llchange);
lon.addEventListener("change", llchange);
gridsquare.addEventListener("change", function() {
if (gridsquare.value !== "") {
gridsquare2latlon();
if (map) {
map.contentWindow.postMessage(JSON.stringify({ type: "change-location", lat: lat.value, lon: lon.value }), "*");
}
else {
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { lat: lat.value, lon: lon.value, gridsquare: gridsquare.value }
});
}
}
});
function latlon2gridsquare()
{
const alat = parseFloat(lat.value) + 90;
const flat = String.fromCharCode(65 + Math.floor(alat / 10));
const slat = Math.floor(alat % 10);
const ulat = String.fromCharCode(97 + Math.floor((alat - Math.floor(alat)) * 60 / 2.5));
const alon = parseFloat(lon.value) + 180;
const flon = String.fromCharCode(65 + Math.floor(alon / 20));
const slon = Math.floor((alon / 2) % 10);
const ulon = String.fromCharCode(97 + Math.floor((alon - 2 * Math.floor(alon / 2)) * 60 / 5));
gridsquare.value = `${flon}${flat}${slon}${slat}${ulon}${ulat}`;
}
function gridsquare2latlon()
{
const grid = gridsquare.value;
lat.value = ((10 * (grid.charCodeAt(1) - 65) + parseInt(grid.charAt(3)) - 90) + (2.5 / 60.0) * (grid.charCodeAt(5) - 97 + 0.5)).toFixed(5);
lon.value = ((20 * (grid.charCodeAt(0) - 65) + 2 * parseInt(grid.charAt(2)) - 180) + (5.0 / 60.0) * (grid.charCodeAt(4) - 97 + 0.5)).toFixed(5);
}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{% 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;
}
}
}
%}
<div class="dialog">
{{_R("dialog-header", "Neighborhood Device")}}
<div>
{{_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 <b>always block</b> or <b>never block</b> 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 `<a href="${replace(replace(map_url, "(lat)", n.lat), "(lon)", n.lon)}" target="_blank">${v}</a>`;
}
return v;
}
if (neighbor) {
const n = neighbor.n;
const l = neighbor.l;
%}
<div class="neighbor">
<div class="o"><a href="{{n.hostname ? `http://${n.hostname}.local.mesh` : n.ip}}" target="_blank">{{n.hostname || n.ip}}</a>
<select name="{{n.mac}}" hx-put={{request.env.REQUEST_URI}} hx-swap="none">
<option value="always" {{n.user_allow ? "selected" : ""}}>never block</option>
<option value="available" {{n.user_allow || n.blocks.user ? "" : "selected"}}>{{n.blocked ? "blocked" : "unblocked"}}</option>
<option value="user" {{n.blocks.user ? "selected" : ""}}>always block</option>
</select>
</div>
<div class="cols">
<div class="i">
<div>{{n.type}}</div>
<div>type</div>
</div>
<div class="i">
<div>{{n.mac}}</div>
<div>mac address</div>
</div>
<div class="i">
<div>{{n.ip}}</div>
<div>ip address</div>
</div>
</div>
{% if (n.model && n.firmware_version) { %}
<div class="cols">
<div class="i">
<div>{{n.model}}</div>
<div>model</div>
</div>
<div class="i">
<div>{{n.firmware_version}}</div>
<div>firmware</div>
</div>
<div>
</div>
</div>
{% } %}
<div class="cols">
<div class="i">
<div>{{map(n, n.lat) || "-"}}</div>
<div>latitude</div>
</div>
<div class="i">
<div>{{map(n, n.lon) || "-"}}</div>
<div>longitude</div>
</div>
<div class="i">
<div>{{"distance" in n ? map(n, sprintf("%.1f %s", units.meters2distance(n.distance), units.distanceUnit())) : "-"}}</div>
<div>distance</div>
</div>
</div>
{%
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);
%}
<div class="cols">
<div class="i">
<div>{{lq}}%</div>
<div>lq | rx success</div>
</div>
<div class="i">
<div>{{nlq}}%</div>
<div>nlq | tx success</div>
</div>
<div class="i">
<div>{{sprintf("%.1f", etx)}}</div>
<div>etx</div>
</div>
</div>
{% } %}
<div class="cols">
<div class="i">
<div>{{type(n.ping_success_time) ? sprintf("%.1f ms", n.ping_success_time * 1000) : "-"}}</div>
<div>ping time</div>
</div>
<div class="i">
<div>{{type(n.ping_quality) ? sprintf("%d%%", n.ping_quality) : "-"}}</div>
<div>ping success</div>
</div>
<div class="i">
<div>{{type(n.avg_tx) ? sprintf("%.1f pkt/sec", n.avg_tx / 60) : "-"}}</div>
<div>avg tx</div>
</div>
</div>
{% if (n.type == "RF") { %}
<div class="cols">
<div class="i">
<div>{{n.snr}}</div>
<div>local snr</div>
</div>
<div class="i">
<div>{{n.rev_snr || "-"}}</div>
<div>neighbor snr</div>
</div>
<div class="i">
<div>{{n.avg_tx_fail ? sprintf("%.1f%%", 100 * n.avg_tx_fail / n.avg_tx) : "-"}}</div>
<div>tx failures</div>
</div>
</div>
<div class="cols">
<div class="i">
<div>{{n.rx_bitrate ? sprintf("%.1f Mbps", n.rx_bitrate) : "-"}}</div>
<div>physical rx bitrate</div>
</div>
<div class="i">
<div>{{n.tx_bitrate ? sprintf("%.1f Mbps", n.tx_bitrate) : "-"}}</div>
<div>physical tx bitrate</div>
</div>
<div class="i">
<div>{{n.avg_tx_retries ? sprintf("%.1f%%", 100 * n.avg_tx_retries / n.avg_tx) : "-"}}</div>
<div>tx retransmissions</div>
</div>
</div>
{% } else if (type(n.avg_tx_fail)) { %}
<div class="cols">
<div class="i">
<div>{{sprintf("%.1f%%", 100 * n.avg_tx_fail / n.avg_tx)}}</div>
<div>tx failures</div>
</div>
<div></div>
<div></div>
</div>
{% } %}
<div class="cols">
<div class="i">
<div>{{state(n)}}</div>
<div>state</div>
</div>
<div class="i">
<div>{{n.node_route_count}}</div>
<div>active routes</div>
</div>
<div></div>
</div>
{%
const snr = `/tmp/snrlog/${uc(selected)}-${lc(n.hostname)}`;
if (fs.access(snr)) {
let signal = "<polyline class='signal' points='";
let noise = "<polyline class='noise' points='";
let hints = "";
const f = fs.open(snr);
if (f) {
const s = [];
const re = /^([^,]+),([-0-9]+),([-0-9]+),([-0-9]+),([-0-9\.]+),([-0-9]+),([-0-9\.]+)$/;
for (let l = f.read("line"); length(l); l = f.read("line")) {
const m = match(trim(l), re);
if (m) {
push(s, m);
}
}
if (length(s) > 90) {
splice(s, 0, length(s) - 90);
}
const slength = length(s);
const slength2 = slength * 0.75;
const step = 180.0 / slength;
let o = 10.0;
let i;
for (i = 0; i < slength2; i++) {
signal += `${o},${10.0 + (s[i][2] / -120.0 * 80.0)} `;
noise += `${o},${10.0 + (s[i][3] / -120.0 * 80.0)} `;
const hx = o + 4;
hints += `<g><rect width="${step}" height="85" x="${o - step / 2}" y="5"></rect><text y="10"><tspan x="${hx}">${s[i][1]}</tspan><tspan x="${hx}" dy="6px">Signal: ${s[i][2]} dBm</tspan><tspan x="${hx}" dy="6px">Noise: ${s[i][3]} dBm</tspan></text></g>`;
o += step;
}
for (; i < slength; i++) {
signal += `${o},${10.0 + (s[i][2] / -120.0 * 80.0)} `;
noise += `${o},${10.0 + (s[i][3] / -120.0 * 80.0)} `;
const hx = o + 4;
const hx2 = o - 4;
hints += `<g class="r"><rect width="${step}" height="85" x="${o - step / 2}" y="5"></rect><text y="10"><tspan x="${hx2}">${s[i][1]}</tspan><tspan x="${hx2}" dy="6px">Signal: ${s[i][2]} dBm</tspan><tspan x="${hx2}" dy="6px">Noise: ${s[i][3]} dBm</tspan></text></g>`;
o += step;
}
f.close();
}
signal += "' />";
noise += "' />";
%}
<div id="neighbor-device-chart">
<svg viewBox="0 0 200 100" preserveAspectRatio="meet">
<g class="frame">
<polyline points="10,10 10,90 190,90" />
<text x="9" y="4">dBm</text>
<text x="8" y="10">0</text>
<text x="8" y="23">-20</text>
<text x="8" y="37">-40</text>
<text x="8" y="50">-60</text>
<text x="8" y="63">-80</text>
<text x="8" y="77">-100</text>
<text x="8" y="90">-120</text>
</g>
<g class="data">{{signal}}{{noise}}</g>
<g class="hints">{{hints}}</g>
</svg>
</div>
{% } %}
</div>
{% } %}
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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 : "";
%}
<div class="dialog">
{{_R("dialog-header", "Network")}}
<div>
<div class="cols">
<div>
<div class="o">Mesh A&zwnj;ddress</div>
<div class="m">The primary address of this node</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="mesh_ip" type="text" size="14" placeholder="10.X.X.X" pattern="10\.((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){3}" value="{{configuration.getSettingAsString("wifi_ip")}}">
</div>
</div>
{{_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.")}}
<hr>
<div class="hideable">
<div class="cols">
<div>
<div class="o">LAN Size</div>
<div class="m">Size of LAN subnet</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="dhcp_mode">
<option value="-1" {{!dhcp.enabled ? "selected" : ""}}>Disabled</option>
<option value="0" {{dhcp.enabled && dmz_mode == 0 ? "selected" : ""}}>NAT</option>
<option value="2" {{dhcp.enabled && dmz_mode == 2 ? "selected" : ""}}>1 host</option>
<option value="3" {{dhcp.enabled && dmz_mode == 3 ? "selected" : ""}}>5 hosts</option>
<option value="4" {{dhcp.enabled && dmz_mode == 4 ? "selected" : ""}}>13 hosts</option>
<option value="5" {{dhcp.enabled && dmz_mode == 5 ? "selected" : ""}}>29 hosts</option>
</select>
</div>
</div>
{{_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.")}}
<div class="compact hideable0">
<div class="cols">
<div>
<div class="o">IP A&zwnj;ddress</div>
<div class="m">Gateway IP a&zwnj;ddress for this LAN network</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_ip" type="text" size="15" required pattern="{{constants.patIP}}" value="{{gateway_nat}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Netmask</div>
<div class="m">Netmask for this LAN network</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_netmask" type="text" size="15" required pattern="{{constants.patNetmask}}" value="{{dhcp.mask}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">DHCP Start</div>
<div class="m">Start of the DHCP range for addresses allocate</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_start" type="text" size="4" required pattern="([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value="{{split(dhcp.start, ".")[3]}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">DHCP End</div>
<div class="m">Last address of the DHCP range for addresses allocated</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_end" type="text" size="4" required pattern="([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value="{{split(dhcp.end, ".")[3]}}">
</div>
</div>
</div>
<div class="compact hideable1">
<div class="cols">
<div>
<div class="o">AltNET IP A&zwnj;ddress</div>
<div class="m">Gateway IP a&zwnj;ddress for AltNET LAN network</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_ip" type="text" size="15" required pattern="((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\.?\b){4}" value="{{gateway_altnet}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Netmask</div>
<div class="m">Netmask for AltNET LAN network</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_netmask" type="text" size="15" required pattern="{{constants.patNetmask}}" value="{{dhcp.mask}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">DHCP Start</div>
<div class="m">Start of the DHCP range for addresses allocate</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_start" type="text" size="4" required pattern="([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value="{{split(dhcp.start, ".")[3]}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">DHCP End</div>
<div class="m">Last address of the DHCP range for addresses allocated</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="lan_dhcp_end" type="text" size="4" required pattern="([2-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value="{{split(dhcp.end, ".")[3]}}">
</div>
</div>
</div>
</div>
<hr>
<div class="hideable">
<div class="cols">
<div>
<div class="o">WAN Enable</div>
<div class="m">Allow node to directly access the Internet</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "wan_enable", value: wan_proto === "disabled" ? false : true })}}
</div>
</div>
{{_H("Enable the WAN interface on this node, to allow it to access the Internet directly.")}}
<div class="compact hideable hideable0">
<div class="cols">
<div>
<div class="o">Mode</div>
<div class="m">Static or DHCP mode</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="wan_mode">
<option value="0" {{wan_proto !== "static" ? "selected" : ""}}>DHCP</option>
<option value="1" {{wan_proto === "static" ? "selected" : ""}}>Static</option>
</select>
</div>
</div>
{{_H("The WAN interface can either use DHCP to retrieve an IP address, or it can be set statically.")}}
<div class="hideable1">
<div class="cols">
<div>
<div class="o">A&zwnj;ddress</div>
<div class="m">WAN IP a&zwnj;ddress</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_ip" type="text" size="15" pattern="{{constants.patIP}}" value="{{configuration.getSettingAsString("wan_ip")}}">
</div>
</div>
{{_H("A fixed IP address to assign to the WAN interace on this node.")}}
<div class="cols">
<div>
<div class="o">Netmask</div>
<div class="m">WAN netmask</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_mask" type="text" size="15" pattern="(((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}))" value="{{configuration.getSettingAsString("wan_mask")}}">
</div>
</div>
{{_H("The netmask (e.g. 255.255.255.0) for this interface.")}}
<div class="cols">
<div>
<div class="o">Gateway</div>
<div class="m">Default gateway</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_gw" type="text" size="15" pattern="{{constants.patIP}}" value="{{configuration.getSettingAsString("wan_gw")}}">
</div>
</div>
{{_H("The default gateway his node should use to access the Internet.")}}
</div>
</div>
</div>
<hr>
<div class="cols">
<div>
<div class="o">DNS</div>
<div class="m">Internet DNS servers</div>
</div>
<div style="flex:0;white-space:nowrap">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_dns1" type="text" size="15" pattern="{{constants.patIP}}" value="{{configuration.getSettingAsString("wan_dns1")}}">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_dns2" type="text" size="15" pattern="{{constants.patIP}}" value="{{configuration.getSettingAsString("wan_dns2")}}">
</div>
</div>
{{_H("For hosts not on the Mesh, use these DNS servers to resolve names to IP addresses.")}}
{{_R("dialog-advanced")}}
<div class="compact">
{% if (includeAdvanced) { %}
{% if (length(hardware.getEthernetPorts()) < 2) { %}
<div class="cols">
<div>
<div class="o">WAN VLAN</div>
<div class="m">Vlan used for Internet access</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="wan_vlan" type="text" size="8" placeholder="Untagged" pattern="\d+" value="{{wan_vlan}}">
</div>
</div>
{{_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.")}}
{% } %}
<div class="cols">
<div>
<div class="o">Mesh to WAN</div>
<div class="m">Allow any mesh device to use local WAN.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "olsrd_gw", value: uciMesh.get("aredn", "@wan[0]", "olsrd_gw") !== "0" })}}
</div>
</div>
{{_H("Allow any node or device on the mesh to use our local Internet connection. This is disabled by default.")}}
<div class="cols">
<div>
<div class="o">LAN to WAN</div>
<div class="m">Allow any LAN device to use local WAN.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "lan_dhcp_route", value: uciMesh.get("aredn", "@wan[0]", "lan_dhcp_route") !== "0" })}}
</div>
</div>
{{_H("Allow LAN devices connected to this node to use our Internet connection. This is enabled by default.")}}
<div class="cols">
<div>
<div class="o">LAN default route</div>
<div class="m">Provide LAN devices with a default route.</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "lan_dhcp_defaultroute", value: uciMesh.get("aredn", "@wan[0]", "lan_dhcp_defaultroute") !== "0" })}}
</div>
</div>
{{_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.")}}
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
function log()
{
return replace(fs.readfile("/tmp/pkg.log"), "\n", "<br>");
}
function getPackageOptions()
{
let i = `<option value="-">-</option>`;
let r = `<option value="-">-</option>`;
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 += `<option value="${m[0]}">${m[0]}</option>`;
}
}
}
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 += `<option value="${m[1]}">${m[1]}</option>`;
}
}
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(`<div id="dialog-messages-success" hx-swap-oob="innerHTML">Package installed</div>`);
}
else {
if (system("/bin/opkg update > /tmp/pkg.log 2>&1") !== 0) {
print(`<div id="dialog-messages-error" hx-swap-oob="innerHTML">${log()}</div>`);
}
else {
if (system(`/bin/opkg -force-overwrite install ${ipk} > /tmp/pkg.log 2>&1`) === 0) {
recordPackage("upload", packagename, ipk);
print(`<div id="dialog-messages-success" hx-swap-oob="innerHTML">Package installed</div>`);
}
else {
print(`<div id="dialog-messages-error" hx-swap-oob="innerHTML">${log()}</div>`);
}
}
}
const po = getPackageOptions();
print(`<select id="download-package" hx-swap-oob="innerHTML">${po.i}</select>`);
print(`<select id="remove-package" hx-swap-oob="innerHTML">${po.r}</select>`);
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(`<div id="dialog-messages-success" hx-swap-oob="innerHTML">Package removed</div>`);
const po = getPackageOptions();
print(`<select id="download-package" hx-swap-oob="innerHTML">${po.i}</select>`);
print(`<select id="remove-package" hx-swap-oob="innerHTML">${po.r}</select>`);
}
else {
print(`<div id="dialog-messages-error" hx-swap-oob="innerHTML">${log()}</div>`);
}
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(`<div id="package-info" hx-swap-oob="innerHTML">${log()}</div>`);
}
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();
%}
<div class="dialog">
{{_R("dialog-header", "Packages")}}
<div id="package-update">
{{_R("dialog-messages")}}
<div class="cols">
<div>
<div class="o">Download Package</div>
<div class="m">Download package from an AREDN server.</div>
</div>
<div style="flex:0">
<select id="download-package">{{po.i}}</select>
</div>
<div style="flex:0">
<div id="package-refresh"><button class="icon refresh"></button></div>
</div>
</div>
{{_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.")}}
<div id="package-info"></div>
<div class="cols">
<div>
<div class="o">Upload Package</div>
<div class="m">Upload a package file from your computer.</div>
</div>
<div style="flex:0">
<input type="file" accept=".ipk">
</div>
</div>
{{_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.")}}
<div><hr></div>
<div class="cols">
<div>
<div class="o">Remove Package</div>
<div class="m">Uninstall package from node.</div>
</div>
<div style="flex:0">
<select id="remove-package">{{po.r}}</select>
</div>
</div>
{{_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")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">Package URL</div>
<div class="m">URL for downloading packages</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="packageurl" type="text" size="45" pattern="{{constants.patUrl}}" value="{{uciMesh.get("aredn", "@downloads[0]", "packages_default")}}">
</div>
</div>
{{_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.")}}
{% } %}
</div>
<div style="flex:1"></div>
<div class="cols" style="padding-top:16px">
<div id="package-upload">
<progress value="0" max="100">
</div>
<div style="flex:0">
<button id="fetch-and-update" disabled hx-trigger="none" hx-encoding="multipart/form-data">Fetch and Install</button>
</div>
</div>
{{_H("<br>Depending on the package option selected above, this button will initiate the download, upload, install or remove process.")}}
</div>
{{_R("dialog-footer", "nocancel")}}
<script>
(function(){
{{_R("open")}}
function clearStatus() {
htmx.find("#dialog-messages-error").innerHTML = "";
htmx.find("#dialog-messages-success").innerHTML = "";
}
htmx.on("#package-update input[type='file']", "change", e => {
clearStatus();
htmx.find("#fetch-and-update").innerText = "Fetch and Install";
htmx.find("#download-package").value = "-";
htmx.find("#remove-package").value = "-";
htmx.find("#package-info").innerHTML = "";
if (e.target.files[0]) {
htmx.find("#fetch-and-update").disabled = false;
}
else {
htmx.find("#fetch-and-update").disabled = true;
}
});
htmx.on("#download-package", "change", e => {
clearStatus();
htmx.find("#fetch-and-update").innerText = "Fetch and Install";
htmx.find("#package-info").innerHTML = "";
htmx.find("#remove-package").value = "-";
htmx.find("#package-update input[type=file]").value = null;
if (e.target.value === "-") {
htmx.find("#fetch-and-update").disabled = true;
}
else {
htmx.find("#fetch-and-update").disabled = false;
htmx.ajax("GET", `{{request.env.REQUEST_URI}}?i=${e.target.value}`, {
source: e.currentTarget,
swap: "none"
});
}
});
htmx.on("#remove-package", "change", e => {
clearStatus();
htmx.find("#download-package").value = "-";
htmx.find("#package-update input[type=file]").value = null;
htmx.find("#package-info").innerHTML = "";
if (e.target.value === "-") {
htmx.find("#fetch-and-update").disabled = true;
}
else {
htmx.find("#fetch-and-update").disabled = false;
htmx.find("#fetch-and-update").innerText = "Remove";
}
});
htmx.on("#fetch-and-update", "click", e => {
clearStatus();
const upload = htmx.find("#package-update input[type=file]").files[0];
const download = htmx.find("#download-package").value;
const remove = htmx.find("#remove-package").value;
if (upload) {
htmx.on(e.currentTarget, "htmx:xhr:progress", e => htmx.find("#package-upload progress").setAttribute("value", e.detail.loaded / e.detail.total * 100));
htmx.ajax("POST", "{{request.env.REQUEST_URI}}", {
source: e.currentTarget,
values: {
packagename: htmx.find("#package-update input[type=file]").value.replace(/^.*\\/, ""),
"packagefile.ipk": upload
},
swap: "none"
}).then(_ => htmx.find("#package-upload progress").setAttribute("value", "0"));
}
else if (download !== "-") {
const source = new EventSource(`{{request.env.REQUEST_URI}}?d=${download}`);
source.addEventListener("close", e => {
source.close();
htmx.find("#package-upload progress").setAttribute("value", "0");
htmx.find("#dialog-messages-success").innerHTML = "Package installed";
const j = JSON.parse(e.data);
htmx.find("#download-package").innerHTML = j.i;
htmx.find("#remove-package").innerHTML = j.r;
});
source.addEventListener("error", e => {
source.close();
htmx.find("#package-upload progress").setAttribute("value", "0");
htmx.find("#dialog-messages-error").innerHTML = e.data || "Unknown error";
});
source.addEventListener("progress", e => {
htmx.find("#package-upload progress").setAttribute("value", e.data);
});
}
else if (remove !== "-") {
htmx.ajax("GET", `{{request.env.REQUEST_URI}}?r=${remove}`, {
source: e.currentTarget,
swap: "none"
});
}
});
htmx.on("#package-refresh", "click", e => {
htmx.find("#package-refresh button").classList.add("rotate");
clearStatus();
htmx.find("#package-update input[type=file]").value = null;
htmx.find("#fetch-and-update").disabled = true;
htmx.find("#fetch-and-update").innerText = "Fetch and Install";
htmx.find("#package-info").innerHTML = "";
const source = new EventSource("{{request.env.REQUEST_URI}}?v=update");
source.addEventListener("close", e => {
source.close();
htmx.find("#package-refresh button").classList.remove("rotate");
htmx.find("#package-upload progress").setAttribute("value", "0");
const j = JSON.parse(e.data);
htmx.find("#download-package").innerHTML = j.i;
htmx.find("#remove-package").innerHTML = j.r;
});
source.addEventListener("error", e => {
source.close();
htmx.find("#package-refresh button").classList.remove("rotate")
htmx.find("#package-upload progress").setAttribute("value", "0");
htmx.find("#dialog-messages-error").innerHTML = e.data || "Unknown error";
});
source.addEventListener("progress", e => {
htmx.find("#package-upload progress").setAttribute("value", e.data);
});
});
htmx.on("#dialog-done", "click", _ => {
htmx.ajax("GET", "/a/packages", {
swap: "none"
});
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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 '<wan_gw>'" : ""}${type === "lan" ? "\toption dns '<wan_dns1> <wan_dns2>'" : ""}
`);
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)
});
});
%}
<div class="dialog">
{{_R("dialog-header", `${haveports ? "Ports &amp; " : ""}XLinks`)}}
<div>
{% if (haveports) { %}
<div class="ports">
<div>
<table>
<thead>
<tr>
<td></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td ${p.info.active ? 'class="active"' : ''}><div>${p.display}</div></td>`);
}
%}
</tr>
</thead>
<tbody>
<tr>
<td><div>dtd</div><div>vlan: 2</div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_dtdlink_${p.name}" ${p.dtdlink ? "checked" : ""} hx-vals='js:{port_dtdlink_${p.name}:htmx.find("[name=port_dtdlink_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
<tr>
<td><div>lan</div><div>vlan: untagged</div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_lan_${p.name}" ${p.lan ? "checked" : ""} hx-vals='js:{port_lan_${p.name}:htmx.find("[name=port_lan_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
<tr>
<td><div>wan</div><div>vlan: <input hx-put="{{request.env.REQUEST_URI}}" type="text" name="port_wan_vlan" hx-include=".ports [name^='port_wan_']" size="5" placeholder="untagged" pattern="([14-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-4])" value="{{wan_vlan != 0 ? wan_vlan : ''}}"></div></td>
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<td><input hx-put="${request.env.REQUEST_URI}" type="checkbox" name="port_wan_${p.name}" ${p.wan ? "checked" : ""} hx-include="previous [name='port_wan_vlan']" hx-vals='js:{port_wan_${p.name}:htmx.find("[name=port_wan_${p.name}]").checked ? "on" : "off"}'></td>`);
}
%}
</tr>
</tbody>
</table>
</div>
</div>
{{_H("<p>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 <i>WAN</i> or <i>LAN</i>. These are just arbitrary names given by the manufacturer
and you are not forced to assign networks of the same name to these ports.
<p>Active network ports, where a cable is present and attached to another device, are shown in green.")}}
<hr>
{% } %}
<div class="xlinks">
<div>
<div class="cols">
<div>
<div class="o">XLinks</div>
<div class="m">Inter-device links across non-AREDN networks</div>
</div>
<button class="add">+</button>
</div>
</div>
<div id="xlink-list">
<div class="cols xlink-label">
<div style="white-space:nowrap">
<div>vlan</div>
<div>ip a&zwnj;ddress</div>
<div>peer a&zwnj;ddress</div>
<div>cidr</div>
<div>weight</div>
{% if (haveports) { %}
<div>port</div>
{% } %}
</div>
<div></div>
</div>
{% for (let i = 0; i < length(xlinks); i++) {
const x = xlinks[i];
%}
<div class="cols xlink" data-name="{{x.name}}">
<div>
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan" value="{{x.vlan}}">
<input name="ipaddr" type="text" size="25" required pattern="{{constants.patIP}}" placeholder="IP A&zwnj;ddress" value="{{x.ipaddr}}">
<input name="peer" type="text" size="25" pattern="{{constants.patIP}}" placeholder="IP A&zwnj;ddress" value="{{x.peer}}">
<select name="cidr">
<option value="32" {{x.cidr === 32 ? "selected" : ""}}>PtP</option>
<option value="31" {{x.cidr === 31 ? "selected" : ""}}>/ 31</option>
<option value="30" {{x.cidr === 30 ? "selected" : ""}}>/ 30</option>
<option value="29" {{x.cidr === 29 ? "selected" : ""}}>/ 29</option>
<option value="28" {{x.cidr === 28 ? "selected" : ""}}>/ 28</option>
<option value="27" {{x.cidr === 27 ? "selected" : ""}}>/ 27</option>
<option value="26" {{x.cidr === 26 ? "selected" : ""}}>/ 26</option>
<option value="25" {{x.cidr === 25 ? "selected" : ""}}>/ 25</option>
<option value="24" {{x.cidr === 24 ? "selected" : ""}}>/ 24</option>
<option value="23" {{x.cidr === 23 ? "selected" : ""}}>/ 23</option>
<option value="22" {{x.cidr === 22 ? "selected" : ""}}>/ 22</option>
<option value="21" {{x.cidr === 21 ? "selected" : ""}}>/ 21</option>
<option value="20" {{x.cidr === 20 ? "selected" : ""}}>/ 20</option>
<option value="19" {{x.cidr === 19 ? "selected" : ""}}>/ 19</option>
<option value="18" {{x.cidr === 18 ? "selected" : ""}}>/ 18</option>
<option value="17" {{x.cidr === 17 ? "selected" : ""}}>/ 17</option>
<option value="16" {{x.cidr === 16 ? "selected" : ""}}>/ 16</option>
</select>
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt" value="{{x.weight}}">
{% if (haveports) { %}
<select name="port">
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<option value="${p.name}" ${x.port === p.name ? "selected" : ""}>${p.display}</option>`);
}
%}
</select>
{% } %}
</div>
<button>-</button>
</div>
{% } %}
</div>
</div>
{{_H("<p>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.")}}
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
const patIP = "((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}";
const xlinks = {{sprintf("%J", xlinks)}};
function update()
{
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { xlinks: JSON.stringify(xlinks) }
});
}
htmx.on("#ctrl-modal .dialog #xlink-list", "change", event => {
const target = htmx.closest(event.target, "[data-name]");
const xlink = xlinks.find(x => x.name == target.dataset.name);
const vlan = htmx.find(target, "[name=vlan]");
const ipaddr = htmx.find(target, "[name=ipaddr]");
const peer = htmx.find(target, "[name=peer]");
const weight = htmx.find(target, "[name=weight]");
const port = htmx.find(target, "[name=port]");
const cidr = htmx.find(target, "[name=cidr]");
if (vlan.validity.valid && ipaddr.validity.valid && peer.validity.valid && weight.validity.valid) {
xlink.vlan = parseInt(vlan.value);
xlink.ipaddr = ipaddr.value;
xlink.peer = peer.value;
xlink.weight = parseInt(weight.value);
xlink.port = port ? port.value : null;
xlink.cidr = parseInt(cidr.value);
update();
}
});
htmx.on("#ctrl-modal .dialog #xlink-list", "click", event => {
if (event.target.nodeName === "BUTTON") {
const target = htmx.closest(event.target, "[data-name]");
const xlink = xlinks.findIndex(x => x.name == target.dataset.name);
htmx.remove(target);
xlinks.splice(xlink, 1);
update();
}
});
htmx.on("#ctrl-modal .dialog .xlinks .add", "click", event => {
let name;
for (let i = 0;; i++) {
name = `xlink${i}`;
if (!htmx.find(`#ctrl-modal .dialog #xlink-list .xlink[data-name=${name}]`)) {
break;
}
}
const x = {
name: name,
vlan: "",
ipaddr: "",
peer: "",
weight: 0,
port: null,
cidr: 32
};
xlinks.push(x);
const div = document.createElement("div");
const ls = htmx.find("#ctrl-modal .dialog #xlink-list");
div.innerHTML = `<div class="cols xlink" data-name="${x.name}">
<div>
<input name="vlan" type="text" size="5" required pattern="([4-9]|[1-9][0-9]{1,2}|[1-3][0-9]{3}|40[0-8][0-9]|409[0-5])" placeholder="VLan">
<input name="ipaddr" type="text" size="25" required pattern="${patIP}" placeholder="IP A&zwnj;ddress">
<input name="peer" type="text" size="25" pattern="${patIP}" placeholder="IP A&zwnj;ddress">
<select name="cidr">
<option value="32">PtP</option>
<option value="31">/ 31</option>
<option value="30">/ 30</option>
<option value="29">/ 29</option>
<option value="28">/ 28</option>
<option value="27">/ 27</option>
<option value="26">/ 26</option>
<option value="25">/ 25</option>
<option value="24">/ 24</option>
<option value="23">/ 23</option>
<option value="22">/ 22</option>
<option value="21">/ 21</option>
<option value="20">/ 20</option>
<option value="19">/ 19</option>
<option value="18">/ 18</option>
<option value="17">/ 17</option>
<option value="16">/ 16</option>
</select>
<input name="weight" type="text" size="2" required pattern="([0-9]|[1-9][0-9]|100)" placeholder="Wt">
{% if (haveports) { %}<select name="port">
{%
for (let i = 0; i < length(ports); i++) {
const p = ports[i];
print(`<option value="${p.name}">${p.display}</option>`);
}
%}
</select>{% } %}
</div>
<button>-</button>
</div>`;
ls.appendChild(div.firstChild);
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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(); %}
<div class="dialog radio-and-antenna">
{{_R("dialog-header", "Radios &amp; Antennas")}}
<div>
{%
const hasradios = length(wlan) > 0;
if (hasradios) {
for (let w = 0; w < length(wlan); w++) {
const prefix = `radio${w}_`;
if (w !== 0) {
print("<hr>");
}
%}
<div id="radio{{w}}" class="hideable compact">
<div class="cols">
<div>
<div class="o" {{length(wlan) > 1 ? "style='font-weight:bold'" : ""}}>Radio {{wlan[w].def.band}}</div>
<div class="m">Radio purpose</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="{{prefix}}mode">
<option value="0" {{wlan[w].mode === radios.RADIO_OFF ? "selected" : ""}}>Off</option>
<option value="1" {{wlan[w].mode === radios.RADIO_MESH ? "selected" : ""}}>Mesh</option>
<option value="2" {{wlan[w].mode === radios.RADIO_LAN ? "selected" : ""}}>LAN Hotspot</option>
<option value="3" {{wlan[w].mode === radios.RADIO_WAN ? "selected" : ""}}>WAN Client</option>
</select>
</div>
</div>
{{_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 <b>off</b>).")}}
<div class="hideable1" {{length(wlan) > 1 ? "style='padding-left:10px'" : ""}}>
<div class="cols">
<div>
<div class="o">Channel</div>
<div class="m">Channel and frequency of this connection</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="radio_channel" style="direction:ltr">
{%
const channel = wlan[w].modes[1].channel;
for (let i = 0; i < length(wlan[w].channels); i++) {
print(`<option value="${wlan[w].channels[i].number}" ${wlan[w].channels[i].number == channel ? "selected" : ""}>${wlan[w].channels[i].label}</option>`);
}
%}
</select>
</div>
</div>
{{_H("Select the central channel/frequency for the radio.")}}
<div class="cols">
<div>
<div class="o">Channel Width</div>
<div class="m">Channel bandwidth</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="{{prefix}}bandwidth" style="direction:ltr">
{%
const bandwidth = wlan[w].modes[1].bandwidth;
for (let i = 0; i < length(wlan[w].bws); i++) {
print(`<option value="${wlan[w].bws[i]}" ${wlan[w].bws[i] == bandwidth ? "selected" : ""}>${wlan[w].bws[i]} MHz</option>`);
}
%}
</select>
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Transmit Power</div>
<div class="m">Transmit power</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="radio_txpower">
{%
const txpower = wlan[w].modes[1].txpower;
for (let i = wlan[w].txmaxpower; i > 0; i--) {
print(`<option value="${i}" ${i == txpower ? "selected" : ""}>${i + wlan[w].txpoweroffset}</option>`);
}
%}
</select>
</div>
</div>
{{_H("Select the transmission power for the radio. Ideally use only enough power to maintain the link at the capacity required.")}}
<div class="cols">
<div>
<div class="o">SSID</div>
<div class="m">AREDN mesh identifier</div>
</div>
<div style="flex:0;white-space:nowrap">
<input hx-put="{{request.env.REQUEST_URI}}" name="{{prefix}}ssid" type="text" size="10" maxlength="32" pattern="[^!#;+\]\/"\t][^+\]\/"\t]{0,30}[^ !#;+\]\/"\t]$|^[^ !#;+\]\/"\t]" value="{{wlan[w].modes[1].ssid}}"><span style="color:var(--ctrl-modal-fg-color)">-{{wlan[w].modes[1].bandwidth}}-v3</span>
</div>
</div>
{% if (uciMesh.get("aredn", "@lqm[0]", "lqm_enable") !== "0") { %}
<div class="cols">
<div>
<div class="o">Minimum SNR</div>
<div class="m">Acceptable SNR for connection (dB)</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_minsnr" type="text" size="3" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "min_snr")}}">
</div>
</div>
{{_H("Low SNR results in higher latency, lower bandwidth and high retranmissions. Setting a minimum SNR allows links with
these characteristics to be ignored.")}}
<div class="cols">
<div>
<div class="o">Maximum Distance</div>
<div class="m">Maximum distance allowed to other nodes in {{units.distanceUnit()}}</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_maxdistance" type="text" size="3" pattern="\d+" value="{{int(0.5 + units.meters2distance(uciMesh.get("aredn", "@lqm[0]", "max_distance")))}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Minimum Quality</div>
<div class="m">Minimum connection quaility percentage</div>
</div>
<div style="flex:0;white-space:nowrap">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_minquality" type="text" size="3" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "min_quality")}}">
</div>
</div>
{{_H("The node management system maintains an estimate of how well each neighbor link performs. This link <b>quality</b> is used
to determine which links to use. Lowering the minimum quality can impact performance, but may also be necessary under specific
circumstances.")}}
{% } %}
</div>
<div class="hideable2" {{length(wlan) > 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.")}}
<div class="cols">
<div>
<div class="o">SSID</div>
<div class="m">Hotspot SSID</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_lan_ssid" type="text" size="10" maxlength="32" pattern=[^!#;+\]\/"\t][^+\]\/"\t]{0,30}[^ !#;+\]\/"\t]$|^[^ !#;+\]\/"\t]" value="{{hexdec(wlan[w].modes[2].ssid)}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Channel</div>
<div class="m">Hotspot channel</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="radio_lan_channel">
{%
const channel = wlan[w].modes[2].channel;
for (let i = 0; i < length(wlan[w].channels); i++) {
print(`<option value="${wlan[w].channels[i].number}" ${wlan[w].channels[i].number == channel ? "selected" : ""}>${wlan[w].channels[i].number}</option>`);
}
%}
</select>
</div>
</div>
<div class="hideable">
<div class="cols">
<div>
<div class="o">Encryption</div>
<div class="m">Encryption algorithm</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="radio_lan_encryption">
<option value="0" {{wlan[w].modes[2].encryption == "psk" ? "selected" : ""}}>WPA PSK</option>
<option value="1" {{wlan[w].modes[2].encryption == "psk2" ? "selected" : ""}}>WPA2 PSK</option>
<option value="2" {{wlan[w].modes[2].encryption == "none" ? "selected" : ""}}>None</option>
</select>
</div>
</div>
<div class="hideable0 hideable1">
<div class="cols">
<div>
<div class="o">Password</div>
<div class="m">Hotspot password</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_lan_password" type="password" required size="10" maxlength="32" value="{{hexdec(wlan[w].modes[2].key) || "AREDN"}}">
</div>
</div>
</div>
</div>
</div>
<div class="hideable3" {{length(wlan) > 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.")}}
<div class="cols">
<div>
<div class="o">SSID</div>
<div class="m">WAN client</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_wan_ssid" type="text" size="10" maxlength="32" pattern='[^!#;+\]\/"\t][^+\]\/"\t]{0,30}[^ !#;+\]\/"\t]$|^[^ !#;+\]\/"\t]' value="{{hexdec(wlan[w].modes[3].ssid)}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Password</div>
<div class="m">Client password</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_wan_password" type="password" size="10" maxlength="32" value="{{hexdec(wlan[w].modes[3].key)}}">
</div>
</div>
</div>
<hr>
<div {{length(wlan) > 1 ? "style='padding-left:10px'" : ""}}>
<div class="cols">
<div>
<div class="o">Antenna</div>
<div class="m">Antenna</div>
</div>
<div style="flex:0;white-space:nowrap">
{% if (length(wlan[w].ants) === 1) { %}
<span>{{wlan[w].ants[0].description}}<span>
{% } else { %}
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="radio_antenna">
{%
const model = wlan[w].ant?.model;
for (let i = 0; i < length(wlan[w].ants); i++) {
print(`<option value="${wlan[w].ants[i].model}" ${wlan[w].ants[i].model == model ? "selected" : ""}>${wlan[w].ants[i].description}</option>`);
}
%}
</select>
{% } %}
</div>
</div>
{% 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)) { %}
<div class="cols">
<div>
<div class="o">Azimuth</div>
<div class="m">Antenna azimuth in degrees</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_azimuth" type="text" size="10" pattern="\d+" value="{{uciMesh.get("aredn", "@location[0]", "azimuth")}}">
</div>
</div>
{{_H("The azimuth, or heading, of the primary radio antenna measured in degrees from north.")}}
{% } %}
<div class="cols">
<div>
<div class="o">Height</div>
<div class="m">Antenna height in meters</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_height" type="text" size="10" pattern="\d+" value="{{uciMesh.get("aredn", "@location[0]", "height")}}">
</div>
</div>
{{_H("The height of the antenna above ground level in meters.")}}
<div class="cols">
<div>
<div class="o">Elevation</div>
<div class="m">Antenna elevation in degrees</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_elevation" type="text" size="10" pattern="\d+" value="{{uciMesh.get("aredn", "@location[0]", "elevation")}}">
</div>
</div>
{{_H("Elevation of the antenna, measured in degress, above or below the horizontal.")}}
{% } %}
</div>
</div>
{%
}
}
else {
%}
<div style="padding-bottom:24px">No Radios</div>
<div>
<div class="cols">
<div>
<div class="o">Minimum Quality</div>
<div class="m">Minimum connection quaility percentage</div>
</div>
<div style="flex:0;white-space:nowrap">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_minquality" type="text" size="3" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "min_quality")}}">
</div>
</div>
</div>
{% } %}
{{_R("dialog-advanced")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">LQM enable</div>
<div class="m">Enable Link Quality Management</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "radio_lqm_enable", value: uciMesh.get("aredn", "@lqm[0]", "lqm_enable") !== "0" })}}
</div>
</div>
{{_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) { %}
<div class="cols">
<div>
<div class="o">Minimum Distance</div>
<div class="m">Minimum distance to other nodes in {{units.distanceUnit()}}</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_mindistance" type="text" size="3" pattern="\d+" value="{{int(0.5 + units.meters2distance(uciMesh.get("aredn", "@lqm[0]", "min_distance")))}}">
</div>
</div>
{{_H("Exclude nodes which are too close to this node.")}}
<div class="cols">
<div>
<div class="o">RTS Threshold</div>
<div class="m">RTS Threshold in bytes before using RTS/CTS when hidden nodes are detected</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_rts_threshold" type="text" size="4" pattern="([1-9]|[1-9]\d{1,2}|1\d{3}|2[0-2]\d{2}|23[0-3]\d|234[0-7])" value="{{uciMesh.get("aredn", "@lqm[0]", "rts_threshold")}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">Max Packet Size</div>
<div class="m">Maximum packet size in bytes sent over WiFi</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_mtu" type="text" size="4" pattern="(25[6-9]|2[6-9]\d|[3-9]\d{2}|1[0-4]\d{2}|1500)" placeholder="1500" value="{{uciMesh.get("aredn", "@lqm[0]", "mtu")}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">SNR Margin</div>
<div class="m">SNR Margin in dB above Min SNR a signal must reach to be re-activated</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_margin_snr" type="text" size="1" pattern="\d" value="{{uciMesh.get("aredn", "@lqm[0]", "margin_snr")}}">
</div>
</div>
{{_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.")}}
{% } %}
<div class="cols">
<div>
<div class="o">Quality Margin</div>
<div class="m">Quality Margin percentage increase before neighbor can be re-activated</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_margin_quality" type="text" size="2" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "margin_quality")}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Ping Penalty</div>
<div class="m">Ping Penalty quality percentage to add when neighbor cannot be pinged</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_ping_penalty" type="text" size="2" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "ping_penalty")}}">
</div>
</div>
<div class="cols">
<div>
<div class="o">Minimum Routes</div>
<div class="m">Minimum number of routes on a link required to disable blocking</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="radio_min_routes" type="text" size="2" pattern="\d\d?" value="{{uciMesh.get("aredn", "@lqm[0]", "min_routes")}}">
</div>
</div>
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
{%
for (let w = 0; w < length(wlan); w++) { %}
const bws{{w}} = htmx.find(`#ctrl-modal .dialog.radio-and-antenna select[name=radio{{w}}_bandwidth]`);
const bwssid{{w}} = htmx.find(`#ctrl-modal .dialog.radio-and-antenna input[name=radio{{w}}_ssid] + span`);
bws{{w}}.addEventListener("change", function() {
bwssid{{w}}.innerHTML = `-${bws{{w}}.value}-v3`;
});
{% }
if (length(wlan) > 1) { %}
const radio0 = htmx.find("#radio0 select[name=radio0_mode]");
const radio1 = htmx.find("#radio1 select[name=radio1_mode]");
htmx.on(radio0, "htmx:beforeRequest", function() {
if (radio0.value === radio1.value && radio1.value !== 0) {
radio1.value = 0;
}
htmx.find("#radio0 select[name=radio_channel]").value = "{{wlan[0].def.channel}}";
htmx.find("#radio0 select[name=radio_lan_channel]").value = "{{wlan[0].def.channel}}";
});
htmx.on(radio1, "htmx:beforeRequest", function() {
if (radio1.value === radio0.value && radio0.value !== 0) {
radio0.value = 0;
}
htmx.find("#radio1 select[name=radio_channel]").value = "{{wlan[1].def.channel}}";
htmx.find("#radio1 select[name=radio_lan_channel]").value = "{{wlan[1].def.channel}}";
});
{% } %}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
response.reboot = true;
const address = configuration.getSettingAsString("wifi_ip", request.env.HTTP_HOST);
%}
<title>{{configuration.getName()}} rebooting</title>
<div class="reboot">
<div>
<div id="icon-logo"></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div>Rebooting</div>
<div>Your node is rebooting.<br>This browser will reconnect automatically once complete.</div>
<div>
<div><progress id="cdprogress" max="120"></div>
<div id="countdown">&nbsp;</div>
</div>
</div>
{{_R("reboot-mon", { delay: 20, countdown: 120, timeout: 5, location: `http://${address}/a/status` })}}
</div>

154
files/app/main/status/e/time.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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";
%}
<div class="dialog">
{{_R("dialog-header", "Time")}}
<div>
<div class="cols">
<div>
<div class="o">Timezone</div>
<div class="m">Timezone</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="timezone">
{%
for (let i = 0; i < length(tz_db_names); i++) {
print(`<option value="${tz_db_names[i].value}" ${tz_db_names[i].name == time_zone_name ? "selected" : ""}>${tz_db_names[i].name}</option>`);
}
%}
</select>
</div>
</div>
{{_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.")}}
<hr>
<div class="cols">
<div>
<div class="o">NTP Server</div>
<div class="m">The ntp server to sync the time</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="ntp_server" type="text" size="20" value="{{ntp_server}}">
</div>
</div>
{{_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.")}}
<div class="cols">
<div>
<div class="o">NTP Updates</div>
<div class="m">NTP update frequency</div>
</div>
<div style="flex:0">
<select hx-put="{{request.env.REQUEST_URI}}" hx-swap="none" name="ntp_mode">
<option value="hourly" {{ntp_mode == "hourly" ? "selected" : ""}}>Hourly</option>
<option value="daily" {{ntp_mode == "daily" ? "selected" : ""}}>Daily</option>
</select>
</div>
</div>
{{_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")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">GPS Time</div>
<div class="m">Use local or network GPS to set time</div>
</div>
<div style="flex:0">
{{_R("switch", { name: "gps_enable", value: uciMesh.get("aredn", "@time[0]", "gps_enable") === "1" })}}
</div>
</div>
{{_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.")}}
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
(function(){
{{_R("open")}}
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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<br>Server";
case "wc":
return "Wireguard<br>Client";
case "ls":
return "Legacy<br>Server";
case "lc":
return "Legacy<br>Client";
default:
return type;
}
}
%}
<div class="dialog">
{{_R("dialog-header", "Tunnels")}}
<div>
<div class="cols">
<div>
<div class="o">Tunnel Server</div>
<div class="m">DNS name of this tunnel server</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="tunnel_server" type="text" size="25" required placeholder="DNS name or IP address" value="{{uciMesh.get("vtun", "@network[0]", "dns")}}">
</div>
</div>
{{_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).")}}
<hr>
<div id="tunnel-templates" class="cols">
<div>
<div class="o">Add tunnel</div>
<div class="m">Add a tunnel from a template</div>
</div>
<div style="flex:0;padding-right:10px">
<select style="direction:ltr">
<option value="wc">Wireguard Client</option>
<option value="ws">Wireguard Server</option>
<option value="lc">Legacy Client</option>
<option value="ls">Legacy Server</option>
</select>
</div>
<button>+</button>
</div>
{{_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.")}}
<br>
<div id="tunnels">{%
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;
%}<div class="tunnel cols" data-index="{{t.index}}">
<div class="cols">
<div data-type="{{t.type}}">{{t2t(t.type)}}</div>
<div>
<div class="cols">
<input type="text" name="name" required placeholder="{{client ? 'Server name' : 'Node name'}}" value="{{t.name}}">
<label class="switch {{t.up ? 'up' : ''}}"><input type="checkbox" name="enable" {{t.enabled ? "checked" : ""}}></label>
</div>
<div class="cols pwnw">
{% if (t.type === "ws") { %}
<input type="hidden" name="key" value="{{t.key}}">
{% } %}
<input type="text" name="password" required pattern="{{wireguard ? '[A-Za-z0-9+\/=]+' : '[\\w@\-]+'}}" placeholder="{{wireguard ? 'Wireguard key' : 'Password'}}" value="{{t.passwd}}" {{t.type === "ws" ? "disabled" : ""}}>
<input type="text" name="network" required pattern="{{constants.patIP}}{{wireguard ? ':' + constants.patPort : ''}}" placeholder="{{wireguard ? 'Network:Port' : 'Network'}}" value="{{t.network}}" {{client ? "" : "disabled"}}>
<input type="text" name="weight" pattern="([0-9]|[1-9][0-9])" placeholder="Wgt" value="{{t.weight}}">
</div>
<div class="cols">
<input type="text" name="notes" placeholder="Notes..." value="{{t.notes}}">
{{client ? '' : '<button class="clipboard"><div class="icon clipboard"></div></button>'}}
</div>
</div>
</div>
<button class="remove">-</button>
</div>{%
}
%}</div>
{{_R("dialog-advanced")}}
<div>
{% if (includeAdvanced) { %}
<div class="cols">
<div>
<div class="o">Tunnel Server Network</div>
<div class="m">IP range to use for tunnel connections</div>
</div>
<div style="flex:0">
{% if (uciMesh.get("aredn", "@supernode[0]", "enable") === "1") { %}
<input hx-put="{{request.env.REQUEST_URI}}" name="tunnel_range_start" type="text" size="14" placeholder="172.30.X.X" pattern="172\.30\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([4-9]|[1-9]\d|1\d{2}|2[0-3]\d|24[0-8])" value="{{uciMesh.get("vtun", "@network[0]", "start")}}">
{% } else { %}
<input hx-put="{{request.env.REQUEST_URI}}" name="tunnel_range_start" type="text" size="14" placeholder="172.31.X.X" pattern="172\.31\.(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.([4-9]|[1-9]\d|1\d{2}|2[0-3]\d|24[0-8])" value="{{uciMesh.get("vtun", "@network[0]", "start")}}">
{% } %}
</div>
</div>
{% 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") { %}
<div class="cols">
<div>
<div class="o">Default Tunnel Weight</div>
<div class="m">Default cost of using a tunnel</div>
</div>
<div style="flex:0">
<input hx-put="{{request.env.REQUEST_URI}}" name="tunnel_weight" type="text" size="4" placeholder="1" pattern="\d+" value="{{uciMesh.get("aredn", "@tunnel[0]", "weight")}}">
</div>
</div>
{{_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.
")}}
{% } %}
{% } %}
</div>
</div>
{{_R("dialog-footer")}}
<script>
!function(){function w(r){var n=new Float64Array(16);if(r)for(var o=0;o<r.length;++o)n[o]=r[o];return n}function l(r){for(var n=0;n<16;++n)r[(n+1)%16]+=(n<15?1:38)*Math.floor(r[n]/65536),r[n]&=65535}function A(r,n,o){for(var a,f=~(o-1),t=0;t<16;++t)a=f&(r[t]^n[t]),r[t]^=a,n[t]^=a}function p(r,n,o){for(var a=0;a<16;++a)r[a]=n[a]+o[a]|0}function d(r,n,o){for(var a=0;a<16;++a)r[a]=n[a]-o[a]|0}function g(r,n,o){for(var a=new Float64Array(31),f=0;f<16;++f)for(var t=0;t<16;++t)a[f+t]+=n[f]*o[t];for(f=0;f<15;++f)a[f]+=38*a[f+16];for(f=0;f<16;++f)r[f]=a[f];l(r),l(r)}function U(r){r[31]=127&r[31]|64,r[0]&=248}function n(r){for(var n,o=new Uint8Array(32),a=w([1]),f=w([9]),t=w(),i=w([1]),u=w(),e=w(),c=w([56129,1]),v=w([9]),y=0;y<32;++y)o[y]=r[y];U(o);for(y=254;0<=y;--y)A(a,f,n=o[y>>>3]>>>(7&y)&1),A(t,i,n),p(u,a,t),d(a,a,t),p(t,f,i),d(f,f,i),g(i,u,u),g(e,a,a),g(a,t,a),g(t,f,u),p(u,a,t),d(a,a,t),g(f,a,a),d(t,i,e),g(a,t,c),p(a,a,i),g(t,t,a),g(a,i,e),g(i,f,v),g(f,u,u),A(a,f,n),A(t,i,n);return function(r,n){for(var o=w(),a=0;a<16;++a)o[a]=n[a];for(a=253;0<=a;--a)g(o,o,o),2!==a&&4!==a&&g(o,o,n);for(a=0;a<16;++a)r[a]=o[a]}(t,t),g(a,a,t),function(r,n){for(var o,a=w(),f=w(),t=0;t<16;++t)f[t]=n[t];l(f),l(f),l(f);for(var i=0;i<2;++i){a[0]=f[0]-65517;for(t=1;t<15;++t)a[t]=f[t]-65535-(a[t-1]>>16&1),a[t-1]&=65535;a[15]=f[15]-32767-(a[14]>>16&1),o=a[15]>>16&1,a[14]&=65535,A(f,a,1-o)}for(t=0;t<16;++t)r[2*t]=255&f[t],r[2*t+1]=f[t]>>8}(o,a),o}function o(){var r,r=(r=new Uint8Array(32),window.crypto.getRandomValues(r),r);return U(r),r}function a(r,n){for(var o=Uint8Array.from([n[0]>>2&63,63&(n[0]<<4|n[1]>>4),63&(n[1]<<2|n[2]>>6),63&n[2]]),a=0;a<4;++a)r[a]=o[a]+65+(25-o[a]>>8&6)-(51-o[a]>>8&75)-(61-o[a]>>8&15)+(62-o[a]>>8&3)}function f(r){for(var n=new Uint8Array(44),o=0;o<32/3;++o)a(n.subarray(4*o),r.subarray(3*o));return a(n.subarray(4*o),Uint8Array.from([r[3*o+0],r[3*o+1],0])),n[43]=61,String.fromCharCode.apply(null,n)}window.wireguard={generateKeypair:function(){var r=o();return{publicKey:f(n(r)),privateKey:f(r)}}}}();
(function(){
{{_R("open")}}
let server = "{{uciMesh.get("vtun", "@network[0]", "dns")}}";
const available = {{sprintf("%J", available)}};
function updateTunnels()
{
const tunnels = [];
const tuns = htmx.findAll("#tunnels .tunnel");
for (let i = 0; i < tuns.length; i++) {
const t = tuns[i];
const index = t.dataset.index;
const name = htmx.find(t, "input[name=name]");
const password = htmx.find(t, "input[name=password]");
const key = htmx.find(t, "input[name=key]");
const type = htmx.find(t, "div[data-type]").dataset.type;
const network = htmx.find(t, "input[name=network]");
const notes = htmx.find(t, "input[name=notes]");
const enable = htmx.find(t, "input[name=enable]");
const weight = htmx.find(t, "input[name=weight]");
if (name.validity.valid && password.validity.valid && network.validity.valid && notes.validity.valid && weight.validity.valid) {
tunnels.push({
index: index,
type: type,
name: name.value,
enabled: enable.checked,
notes: notes.value || "",
network: network.value,
passwd: password.value,
key: key && key.value,
weight: weight.value
});
}
}
htmx.ajax("PUT", "{{request.env.REQUEST_URI}}", {
swap: "none",
values: { tunnels: JSON.stringify(tunnels) }
});
}
htmx.on("input[name=tunnel_server]", "change", e => {
if (e.target.validity.valid) {
server = e.target.value;
}
});
htmx.on("#tunnel-templates button", "click", _ => {
const type = htmx.find("#tunnel-templates select").value;
const client = type === "lc" || type === "wc" ? true : false;
const wireguard = type === "ws" || type === "wc" ? true : false;
const network = client ? "" : Object.keys(available[wireguard ? "w" : "l"])[0];
if (!client && !network) {
return;
}
if (network) {
delete available.l[network];
delete available.w[network];
}
let password = "";
let key = "";
let index = "";
let htype = type;
switch (type) {
case "ws":
htype = "Wireguard<br>Server";
const ks = window.wireguard.generateKeypair();
const kc = window.wireguard.generateKeypair();
key = `${ks.privateKey}${ks.publicKey}${kc.privateKey}${kc.publicKey}`;
password = `${ks.publicKey}${kc.privateKey}${kc.publicKey}`;
for (let i = 0; i < 256; i++) {
index = `server_${i}`;
if (!htmx.find(`.tunnel[data-index="${index}"]`)) {
break;
}
}
break;
case "ls":
htype = "Legacy<br>Server";
password = "0000-0000-0000".replace(/(0)/g, _ => ((Math.random()*36)|0).toString(36));
for (let i = 0; i < 256; i++) {
index = `server_${i}`;
if (!htmx.find(`.tunnel[data-index="${index}"]`)) {
break;
}
}
break;
case "wc":
case "lc":
htype = `${type === "wc" ? "Wireguard" : "Legacy"}<br>Client`;
for (let i = 0; i < 256; i++) {
index = `client_${i}`;
if (!htmx.find(`.tunnel[data-index="${index}"]`)) {
break;
}
}
break;
default:
break;
}
const div = document.createElement("div");
div.innerHTML = `<div class="tunnel cols" data-index="${index}">
<div class="cols">
<div data-type="${type}">${htype}</div>
<div>
<div class="cols">
<input type="text" name="name" required placeholder="${client ? 'Server name' : 'Node name'}" value="">
<label class="switch"><input type="checkbox" name="enable" checked></label>
</div>
<div class="cols pwnw">
${type === "ws" ? '<input type="hidden" name="key" value="' + key + '">' : ''}
<input type="text" name="password" required pattern="${wireguard ? '[A-Za-z0-9+\/=]+' : '[\\w@\-]+'}" placeholder="${wireguard ? 'Wireguard key' : 'Password'}" ${type === "ws" ? "disabled" : ""} value="${password}">
<input type="text" name="network" required pattern="((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])\\.?\\b){4}${wireguard ? ':{{constants.patPort}}' : ''}" placeholder="${wireguard ? 'Network:Port' : 'Network'}" ${client ? "" : "disabled"} value="${network}">
<input type="text" name="weight" pattern="([0-9]|[1-9][0-9])" placeholder="Wgt" value="">
</div>
<div class="cols">
<input type="text" name="notes" placeholder="Notes..." value="">
${client ? '' : '<button class="clipboard"><div class="icon clipboard"></div></button>'}
</div>
</div>
<button class="remove">-</button>
</div>`;
const t = htmx.find("#tunnels");
t.insertBefore(div.firstChild, t.firstChild);
});
htmx.on("#tunnels", "change", updateTunnels);
htmx.on("#tunnels", "click", event => {
const target = event.target;
if (target.nodeName === "BUTTON") {
if (target.classList.contains("remove")) {
const tunnel = htmx.closest(target, ".tunnel");
htmx.remove(tunnel);
updateTunnels();
}
else if (target.classList.contains("clipboard")) {
const t = htmx.closest(target, ".tunnel");
const name = htmx.find(t, "input[name=name]");
const passwd = htmx.find(t, "input[name=password]");
const network = htmx.find(t, "input[name=network]");
if (name.validity.valid && passwd.validity.valid && network.validity.valid) {
const blob = new Blob([
`<h2>Connection details for ${name.value}</h2>
<div>Name: ${name.value}</div>
<div style="white-space:nowrap">Password: ${passwd.value}</div>
<div>Network: ${network.value}</div>
<div>Server address: ${server}</div>`
], { type: "text/html" });
window.open(URL.createObjectURL(blob));
}
}
}
});
htmx.on("#tunnels", "paste", event => {
const target = event.target;
if (target.nodeName === "INPUT" && !target.disabled) {
const txt = event.clipboardData.getData("text/plain");
if (!txt) {
return;
}
const config = {};
txt.split("\n").forEach(line => {
if (line.startsWith("Password: ")) {
config.passwd = line.substring(10);
}
else if (line.startsWith("Network: ")) {
config.network = line.substring(9);
}
else if (line.startsWith("Server address: ")) {
config.server = line.substring(16);
}
});
if (!(config.passwd && config.network && config.server)) {
return;
}
event.stopPropagation();
event.preventDefault();
const t = htmx.closest(target, '.tunnel');
htmx.find(t, "input[name=name]").value = config.server;
htmx.find(t, "input[name=password]").value = config.passwd;
htmx.find(t, "input[name=network]").value = config.network;
updateTunnels();
}
});
})();
</script>
</div>

202
files/app/main/tools/e/iperf3.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const nodes = mesh.getNodeList();
const mynode = configuration.getName();
%}
<div class="dialog wide">
{{_R("tool-header", "iPerf3")}}
<div class="simple-tool compact client-server">
<div class="cols">
<div>
<div class="o">Server A&zwnj;ddress</div>
<div class="m">Node name or a&zwnj;ddress</div>
</div>
<div>
<input id="tool-target" type="text" size="40" required>
<select id="tool-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<button id="target-swap"><div class="icon updownarrow"></div></button>
</div>
</div>
<div class="cols">
<div>
<div class="o">Client A&zwnj;ddress</div>
<div class="m">Node name or a&zwnj;ddress</div>
</div>
<div>
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
<select id="tool-client-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<div style="width:42px"></div>
</div>
</div>
{{_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.")}}
<div class="cols">
<div class="tool-console">
<pre></pre>
</div>
<div>
<button id="tool-start">Go</button>
</div>
</div>
</div>
{{_R("tool-footer")}}
<script>
(function(){
{{_R("open")}}
htmx.find("#tool-target").focus();
const mynode = "{{mynode}}";
htmx.on("#tool-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-target").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#tool-client-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-client").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#target-swap", "click", _ => {
const v = htmx.find("#tool-target").value;
htmx.find("#tool-target").value = htmx.find("#tool-client").value;
htmx.find("#tool-client").value = v;
});
htmx.on("#tool-start", "click", async _ => {
const server = htmx.find("#tool-target").value;
const client = htmx.find("#tool-client").value;
if (server && client) {
const con = htmx.find(".tool-console pre");
con.innerText = "";
function out(line) {
con.innerText += `${line}\n`;
}
out("iperf3 test started");
out("");
try {
const r = await fetch(`http://${client}.local.mesh/cgi-bin/iperf?server=${server}.local.mesh&kill=1&protocol=tcp`);
const reader = r.body.getReader();
let state = "BEGIN";
for (;;) {
const { done, value } = await reader.read();
if (done) {
if (state !== "DONE") {
out("iperf3 Terminated.");
}
break;
}
const lines = String.fromCharCode.apply(null, value).split("\n");
lines.forEach(line => {
line = line.trim();
switch (state) {
case "BEGIN":
{
const m = line.match(/<title>(.*)<\/title>/);
if (m) {
if (m[1] === "SUCCESS") {
state = "CONNECTING";
out(`Client: ${client}`);
out(`Server: ${server}`);
out("");
}
else {
state = "ERROR";
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
}
}
break;
}
case "CONNECTING":
if (line.match(/^Connecting/)) {
state = "PRINT";
}
break;
case "PRINT":
if (line) {
if (line.match(/iperf Done/)) {
state = "DONE";
out("");
}
out(line);
}
break;
case "ERROR":
{
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
break;
}
case "DONE":
default:
break;
}
});
}
}
catch (_) {
out("iperf3 Failed.");
}
}
});
})();
</script>
</div>

204
files/app/main/tools/e/ping.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const nodes = mesh.getNodeList();
const mynode = configuration.getName();
%}
<div class="dialog wide">
{{_R("tool-header", "Ping")}}
<div class="simple-tool compact client-server">
<div class="cols">
<div>
<div class="o">Target A&zwnj;ddress</div>
<div class="m">IP A&zwnj;ddress, Hostname or Node</div>
</div>
<div>
<input id="tool-target" type="text" size="40" required>
<select id="tool-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<button id="target-swap"><div class="icon updownarrow"></div></button>
</div>
</div>
<div class="cols">
<div>
<div class="o">Source A&zwnj;ddress</div>
<div class="m">Node name or a&zwnj;ddress</div>
</div>
<div>
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
<select id="tool-client-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<div style="width:42px"></div>
</div>
</div>
{{_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.")}}
<div class="cols">
<div class="tool-console">
<pre></pre>
</div>
<div>
<button id="tool-start">Go</button>
</div>
</div>
</div>
{{_R("tool-footer")}}
<script>
(function(){
{{_R("open")}}
htmx.find("#tool-target").focus();
const mynode = "{{mynode}}";
htmx.on("#tool-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-target").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#tool-client-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-client").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#target-swap", "click", _ => {
const v = htmx.find("#tool-target").value;
htmx.find("#tool-target").value = htmx.find("#tool-client").value;
htmx.find("#tool-client").value = v;
});
htmx.on("#tool-start", "click", async _ => {
const server = htmx.find("#tool-target").value;
const client = htmx.find("#tool-client").value;
if (server && client) {
const con = htmx.find(".tool-console pre");
con.innerText = "";
function out(line) {
con.innerText += `${line}\n`;
}
out("Ping test started");
out("");
try {
const r = await fetch(`http://${client}${client.indexOf(".") == -1 ? ".local.mesh" : ""}/cgi-bin/ping?server=${server}${server.indexOf(".") == -1 ? ".local.mesh" : ""}`);
const reader = r.body.getReader();
let state = "BEGIN";
for (;;) {
const { done, value } = await reader.read();
if (done) {
if (state !== "DONE") {
out("Ping Terminated.");
}
break;
}
const lines = String.fromCharCode.apply(null, value).split("\n");
lines.forEach(line => {
line = line.trim();
switch (state) {
case "BEGIN":
{
const m = line.match(/<title>(.*)<\/title>/);
if (m) {
if (m[1] === "SUCCESS") {
state = "CONNECTING";
out(`Source: ${client}`);
out(`Target: ${server}`);
out("");
}
else {
state = "ERROR";
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
}
}
break;
}
case "CONNECTING":
if (line.match(/^PING/)) {
state = "PRINT";
out(line);
}
break;
case "PRINT":
if (line) {
if (line.match(/round-trip/)) {
state = "DONE";
out("");
}
out(line);
}
break;
case "ERROR":
{
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
break;
}
case "DONE":
default:
break;
}
});
}
}
catch (_) {
out("Ping Failed.");
}
}
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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"));
%}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const nodes = mesh.getNodeList();
const mynode = configuration.getName();
%}
<div class="dialog wide">
{{_R("tool-header", "Traceroute")}}
<div class="simple-tool compact client-server">
<div class="cols">
<div>
<div class="o">Target A&zwnj;ddress</div>
<div class="m">IP A&zwnj;ddress, Hostname or Node</div>
</div>
<div>
<input id="tool-target" type="text" size="40" required>
<select id="tool-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<button id="target-swap"><div class="icon updownarrow"></div></button>
</div>
</div>
<div class="cols">
<div>
<div class="o">Source A&zwnj;ddress</div>
<div class="m">Node name or a&zwnj;ddress</div>
</div>
<div>
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
<select id="tool-client-select">
<option value="">&#x25BC;</option>
<option value="{{mynode}}">{{mynode}}</option>
{%
for (let i = 0; i < length(nodes); i++) {
if (nodes[i] !== mynode) {
print(`<option value="${nodes[i]}">${nodes[i]}</option>`);
}
}
%}
</select>
<div style="width:42px"></div>
</div>
</div>
{{_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.")}}
<div class="cols">
<div class="tool-console">
<pre></pre>
</div>
<div>
<button id="tool-start">Go</button>
</div>
</div>
</div>
{{_R("tool-footer")}}
<script>
(function(){
{{_R("open")}}
htmx.find("#tool-target").focus();
const mynode = "{{mynode}}";
htmx.on("#tool-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-target").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#tool-client-select", "change", e => {
if (e.target.value !== "") {
htmx.find("#tool-client").value = e.target.value;
e.target.value = "";
}
});
htmx.on("#target-swap", "click", _ => {
const v = htmx.find("#tool-target").value;
htmx.find("#tool-target").value = htmx.find("#tool-client").value;
htmx.find("#tool-client").value = v;
});
htmx.on("#tool-start", "click", async _ => {
const server = htmx.find("#tool-target").value;
const client = htmx.find("#tool-client").value;
if (server && client) {
const con = htmx.find(".tool-console pre");
con.innerText = "";
function out(line) {
con.innerHTML += `${line}\n`;
}
out("Traceroute test started");
out("");
try {
const r = await fetch(`http://${client}${client.indexOf(".") == -1 ? ".local.mesh" : ""}/cgi-bin/traceroute?server=${server}${server.indexOf(".") == -1 ? ".local.mesh" : ""}`);
const reader = r.body.getReader();
let state = "BEGIN";
for (;;) {
const { done, value } = await reader.read();
if (done) {
if (state !== "DONE") {
out("Traceroute Terminated.");
}
break;
}
const lines = String.fromCharCode.apply(null, value).split("\n");
lines.forEach(line => {
line = line.trim();
switch (state) {
case "BEGIN":
{
const m = line.match(/<title>(.*)<\/title>/);
if (m) {
if (m[1] === "SUCCESS") {
state = "CONNECTING";
out(`Source: ${client}`);
out(`Target: ${server}`);
out("");
}
else {
state = "ERROR";
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
}
}
break;
}
case "CONNECTING":
if (line.match(/^traceroute/)) {
state = "PRINT";
out(line);
}
break;
case "PRINT":
if (line) {
if (line === "</pre></body></html>") {
state = "DONE";
out("");
out("Traceroute done");
}
else {
const m = line.match(/^[0-9]+ +(.+\.local\.mesh) /);
if (m) {
line = line.replace(m[1], `<a href="http://${m[1]}">${m[1]}</a>`);
}
out(line);
}
}
break;
case "ERROR":
{
const m = line.match(/<pre>(.*)<\/pre>/);
if (m) {
out(`ERROR: ${m[1]}`);
state = "DONE";
}
break;
}
case "DONE":
default:
break;
}
});
}
}
catch (_) {
out("Traceroute Failed.");
}
}
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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";
}
}
}
%}
<div class="dialog wide">
{{_R("tool-header", "WiFi Scan")}}
<div id="wifi-scan">
<table>
<thead>
<tr>
<td>SNR</td><td>Signal</td><td style="width:50px">Chan</td><td>Enc</td><td>SSID</td><td>Hostname</td><td>MAC/BSSID</td><td>802.11 Mode</td>
</tr>
</thead>
<tbody>
{% for (let i = 0; i < length(last_scan); i++) {
const s = last_scan[i];
%}
<tr><td>{{95 + s.signal}}</td><td>{{s.signal}}</td><td>{{s.chan}}</td><td>-</td><td>{{s.ssid}}</td><td>{{s.hostname || s.ip || "-"}}</td><td>{{s.mac}}</td><td>{{s.mode}}</td></tr>
{% } %}
</tbody>
</table>
</div>
<div class="cols">
<div>Last Scan: {{scan_time}}</div>
<button hx-put="{{request.env.REQUEST_URI}}" hx-target="#ctrl-modal">Rescan</button>
</div>
{{_H("<br>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.<p>By default the last scan is shown.")}}
{{_R("tool-footer")}}
<script>
(function(){
{{_R("open")}}
htmx.on("#wifi-scan + div button", "click", e => {
const target = e.target;
target.style.width="100px";
target.innerText = "Scanning ";
let dots = ".";
const timer = setInterval(_ => {
if (!document.contains(target)) {
clearInterval(timer);
}
target.innerText = "Scanning " + dots;
dots += ".";
if (dots.length > 3) {
dots = "";
}
}, 300);
});
})();
</script>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
}
}
%}
<div class="dialog wide">
{{_R("tool-header", "WiFi Signal")}}
<div id="wifi-chart">
<div class="cols">
<div>
<div class="o">Node</div>
<div class="m">Select the target node</div>
</div>
<div style="flex:0">
<select id="wifi-device">
<option value="average">Average</option>
{%
for (let i = 0; i < length(stations); i++) {
const s = stations[i];
if (s) {
print(`<option value="${s.mac}">${replace(s.hostname || s.mac, ".local.mesh", "")}</option>`);
}
}
%}
</select>
</div>
</div>
<div class="cols" style="padding:10px 0 30px 0">
<div style="flex:0">
<div id="wifi-bar">
<div>- dBm<br><small>snr: -</small></div>
<div class="bars">
<div><div></div><div style="background-color:var(--conn-fg-color-idle)"></div></div>
<div><div></div><div style="background-color:var(--conn-fg-color-good)"></div></div>
<div><div></div><div style="background-color:var(--conn-fg-color-bad)"></div></div>
</div>
</div>
</div>
<div class="chart">
<div>
<svg viewBox="0 0 200 100" preserveAspectRatio="meet">
<polyline class="frame" points="10,10 10,90 190,90" />
<text x="9" y="4">dBm</text>
<text x="8" y="10">0</text>
<text x="8" y="23">-20</text>
<text x="8" y="37">-40</text>
<text x="8" y="50">-60</text>
<text x="8" y="63">-80</text>
<text x="8" y="77">-100</text>
<text x="8" y="90">-120</text>
<text x="105" y="96">Last 5 minutes</text>
<polyline class="signal" />
</svg>
</div>
</div>
</div>
<div class="cols">
<div class="cols">
<div>
<div class="o">Sound</div>
<div class="m">Enable audible indicator</div>
</div>
<div>
<select name="sound"><option value="off">Off</option><option value="on">On</option></select>
</div>
</div>
<div class="cols">
<div>
<div class="o">Volume</div>
</div>
<div>
<input type="range" name="volume" min="0" max="10">
</div>
</div>
<div class="cols">
<div>
<div class="o">Pitch</div>
</div>
<div>
<input type="range" name="pitch" min="5" max="100">
</div>
</div>
</div>
{{_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.<p>A sound indicator is also
provided which is useful when aligning antennas without looking at this display.")}}
</div>
{{_R("tool-footer")}}
<script>
(function(){
{{_R("open")}}
const sf = -95;
const st = -20;
const device = htmx.find("#wifi-device");
const target = htmx.find("#wifi-chart");
const bart = htmx.find("#wifi-bar > div");
const bar1 = htmx.find("#wifi-bar .bars > div:nth-child(1)");
const bar2 = htmx.find("#wifi-bar .bars > div:nth-child(2)");
const bar3 = htmx.find("#wifi-bar .bars > div:nth-child(3)");
const chart = htmx.find("#wifi-chart svg");
const signal = htmx.find("#wifi-chart svg .signal");
let oscillator;
let gain;
function resetAudio() {
const audio = new AudioContext();
oscillator = audio.createOscillator();
gain = audio.createGain();
oscillator.connect(gain);
oscillator.type = "sine";
gain.connect(audio.destination);
gain.gain.value = 0;
}
resetAudio();
let smax = sf;
let smin = st;
let last = null;
const maxpoints = 300;
function p(v) {
if (v <= sf) {
return "0%";
}
const low = Math.max(sf, 1.05 * smin);
const range = 0.98 * smax - low;
return `${Math.min(100, 100 * (v - low) / range)}%`;
}
function reset() {
last = device.value;
smax = sf;
smin = st;
bart.innerText = "dBm";
bar1.style.height = "";
bar2.style.height = "";
bar3.style.height = "";
bar1.firstElementChild.innerText = "";
bar2.firstElementChild.innerText = "";
bar3.firstElementChild.innerText = "";
signal.points.clear();
}
const timer = setInterval(async _ => {
if (!document.contains(target)) {
clearInterval(timer);
return;
}
const r = await fetch("{{request.env.REQUEST_URI}}", { method: "PUT" });
const j = await r.json();
if (last !== device.value) {
reset();
}
let s = j.s[last];
if (!s) {
if (last === "average") {
s = Math.round(Object.values(j.s).reduce((t, v, _, a) => t + v / a.length, 0));
}
if (!s) {
s = -120;
}
}
if (s >= sf) {
if (s > smax) {
smax = s;
}
if (s < smin) {
smin = s;
}
const snr = s - j.n;
bart.innerHTML = `${s} dBm<br><small>snr: ${snr}</small>`;
bar1.style.height = p(smax);
bar2.style.height = p(s);
bar3.style.height = p(smin);
bar1.firstElementChild.innerText = smax;
bar2.firstElementChild.innerText = s;
bar3.firstElementChild.innerText = smin;
}
else {
bart.innerHTML = `- dBm<br><small>snr: -</small>`;
bar2.style.height = p(sf);
bar2.firstElementChild.innerText = "";
}
if (signal.points.length >= maxpoints) {
signal.points.removeItem(0);
for (let i = 0; i < signal.points.length; i++) {
signal.points[i].x = 10 + i / maxpoints * 180;
}
}
const point = chart.createSVGPoint();
point.x = 10 + signal.points.length / maxpoints * 180;
point.y = 90 - 80 * ((s > -120 ? s : -120) + 120) / 120;
signal.points.appendItem(point);
oscillator.frequency.value = (s - sf) * htmx.find("#wifi-chart input[name=pitch]").value;
gain.gain.value = htmx.find("#wifi-chart input[name=volume]").value;
}, 1000);
htmx.on("#wifi-chart select[name=sound]", "change", e => {
if (e.target.value === "on") {
oscillator.start();
}
else {
oscillator.stop();
resetAudio();
}
});
})();
</script>
</div>

35
files/app/main/tunnels.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("tunnels")}}

83
files/app/partial/changes.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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(`<div id="main-container" hx-swap-oob="true">${_R(request.page)}</div>`);
}
}
%}
<div id="changes" {{request.headers["hx-boosted"] ? "" : "hx-swap-oob='true'"}}>{%
let changes = 0;
let reboot = false;
if (pwchanged) {
%}<script>location.reload();</script>{%
}
if (auth.isAdmin) {
changes = configuration.countChanges();
if (fs.access("/tmp/reboot-required")) {
reboot = true;
}
}
if (reboot) {
%}
<div>
Reboot required:
<button name="reboot" value="1" hx-get="/a/status/e/reboot" hx-target="body">Reboot</button>
</div>
{% }
else if (changes > 0) {
%}
<div>
Pending changes: {{changes}}
<button name="commit" value="1" hx-put="/a/status/e/changes">Commit</button>
<button name="revert" value="1" hx-put="/a/status/e/changes">Revert</button>
</div>
<script>
(function(){
htmx.on("#changes button[name=commit]", "click", e => {
e.target.innerText = "Committing ...";
htmx.find("#changes button[name=revert]").style.display = "none";
setTimeout(_ => htmx.ajax("GET", "/a/changes", "#changes"), 5000);
});
})();
</script>
{% } %}</div>

120
files/app/partial/dhcp.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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) { %}
<div class="ctrl" hx-get="status/e/dhcp" hx-target="#ctrl-modal">
<div class="section-title">LAN DHCP</div>
<div class="section">
<div class="t">Active</div>
<div class="s">status</div>
<div class="cols">
<div>
<div class="t">{{dhcp.gateway}} <span class="ts">/ {{dhcp.cidr}}</span></div>
<div class="s">gateway</div>
</div>
<div style="flex:2">
<div class="t">{{dhcp.start}} - {{dhcp.end}}</div>
<div class="s">range</div>
</div>
</div>
<div class="cols">
<div>
<div class="t">{{dr}}</div>
<div class="s">reserved leases</div>
</div>
<div>
<div class="t">{{da}}</div>
<div class="s">active leases</div>
</div>
<div></div>
</div>
<div class="cols">
<div>
<div class="t">{{at}}</div>
<div class="s">tags</div>
</div>
<div>
<div class="t">{{ao}}</div>
<div class="s">options</div>
</div>
<div></div>
</div>
</div>
</div>
{% } else { %}
<div class="noctrl">
<div class="section-title">LAN DHCP</div>
<div class="section">
<div class="t">Disabled</div>
<div class="s">status</div>
</div>
</div>
{% } %}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="ctrl-modal-advanced {{request.headers['include-advanced'] === "1" ? "active" : ""}}">
<button hx-get="{{request.env.REQUEST_URI}}" hx-target="#ctrl-modal" hx-headers='{"Include-Help":"{{includeHelp ? "1" : "0"}}","Include-Advanced":"{{request.headers['include-advanced'] === "1" ? "0" : "1"}}"}'>Advanced options</button>
<hr>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="ctrl-modal-footer">
<hr/>
{% if (inner !== "nocancel") { %}
<button id="dialog-cancel" hx-delete="{{request.env.REQUEST_URI}}" onclick="setTimeout(_ => document.getElementById('ctrl-modal').close(), 10)">Cancel</button>
{% } %}
<button id="dialog-done" onclick="setTimeout(_ => document.getElementById('ctrl-modal').close(), 10)">Done</button>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
if (request.env.REQUEST_METHOD === "GET" && request.headers["hx-target"] === "ctrl-modal" && !request.headers["include-help"] && !request.headers["include-advanced"]) {
configuration.prepareModalChanges();
}
%}
<div>
<input style="position:absolute;top:-10000px">
<button id="dialog-help" class="{{request.headers["include-help"] === "1" ? "enabled" : ""}}" hx-get="{{request.env.REQUEST_URI}}" hx-trigger="click delay:10ms" hx-target="#ctrl-modal" hx-headers='{"Include-Help":"{{includeHelp ? "0" : "1"}}","Include-Advanced":"{{request.headers['include-advanced'] === "1" ? "1" : "0"}}"}'>Help</button>
<div class="t">{{inner}}</div>
<div class="s">configuration</div>
<hr/>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="dialog-messages">
<div id="dialog-messages-error"></div>
<div id="dialog-messages-success"></div>
</div>

68
files/app/partial/firmware.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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";
}
%}
<div class="ctrl" hx-get="status/e/firmware" hx-target="#ctrl-modal">
<div class="firmware">
<div class="t">{{firmware}}<div class="firmware-status {{message}}"></div></div>
<div class="s cols">
<div style="flex:1.5">firmware version</div>
<div><a href="https://github.com/aredn/aredn/issues" target="_blank" onclick="event.stopPropagation()">issues</a></div>
<div><a href="{{uci.get("aredn", "@downloads[0]", "firmware_aredn")}}/snapshots/CHANGELOG.md" target="_blank" onclick="event.stopPropagation()">release notes</a></div>
</div>
</div>
</div>

60
files/app/partial/general.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="ctrl" hx-get="status/e/basics" hx-target="#ctrl-modal">
{% if (fs.access(`${config.application}/resource/img/radio.png`)) { %}
<div class="radio-image"><img src="/a/img/radio.png"></div>
{% } %}
<div class="node-description">
<div class="t">{{configuration.getSettingAsString("description_node", "None")}}</div>
<div class="s">description</div>
</div>
{% if (auth.isAdmin) { %}
<div>
<div class="t">{{uci.get("aredn", "@notes[0]", "private") || "-"}}</div>
<div class="s">notes</div>
</div>
{% } %}
</div>
<div id="health" hx-get="health" hx-trigger="every 30s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{{_R("health")}}
</div>
{{_R("firmware")}}
{% if (auth.isAdmin && !hardware.isLowMemNode()) { %}
<div id="packages" class="ctrl" hx-get="status/e/packages" hx-target="#ctrl-modal">
{{_R("packages")}}
</div>
{% } %}
<hr>
{{_R("network")}}

76
files/app/partial/health.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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");
%}
<div class="ctrl" hx-get="status/e/time" hx-target="#ctrl-modal">
<div class="t">{{tm.hour === 0 ? "12" : tm.hour > 12 ? tm.hour - 12 : tm.hour}}:{{sprintf("%02d", tm.min)}} {{tm.hour >= 12 ? "pm" : "am"}}</div>
<div class="s">time{{tmsource ? " (" + tmsource + ")" : ""}}</div>
</div>
<div class="noctrl">
<div class="t">{{uptime}}</div>
<div class="s">uptime</div>
<div class="t">{{ld[0]}}, {{ld[1]}}, {{ld[2]}}</div>
<div class="s">load</div>
<div class="cols">
<div>
<div class="t">{{flash}} MB</div>
<div class="s">free flash</div>
</div>
<div>
<div class="t">{{ram}} MB</div>
<div class="s">free ram</div>
</div>
<div></div>
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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";
%}
<div class="ctrl" hx-get="status/e/internal-services" hx-target="#ctrl-modal">
<div class="section-title">Internal Services</div>
<div class="section cols">
<div class="service"><div class="status {{cm}}"></div>Cloud Mesh</div>
<div class="service"><div class="status {{mt}}"></div>Metrics</div>
<div class="service"><div class="status {{wd}}"></div>Watchdog</div>
<div class="service"><div class="status {{rl}}"></div>Remote Logging</div>
<div class="service"><div class="status {{ip}}"></div>IPerf3 Server</div>
<div class="service"><div class="status {{sn}}"></div>Supernode</div>
<div class="service"><div class="status {{ws}}"></div>WAN ssh</div>
<div class="service"><div class="status {{wt}}"></div>WAN telnet</div>
<div class="service"><div class="status {{ww}}"></div>WAN web</div>
{% if (hardware.hasPOE()) { %}
<div class="service"><div class="status {{poe}}"></div>PoE out</div>
{% } %}
{% if (hardware.hasUSBPower()) { %}
<div class="service"><div class="status {{pou}}"></div>USB power out</div>
{% } %}
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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 "-";
}
%}
<div class="noctrl">
<div class="section-title">Local Nodes</div>
<div class="section" style="line-height:18px;margin-top:-16px">
<div class="cols">
<div class="heading" style="flex:0.75"></div>
<div class="heading ts cols stats">
<div>lq</div><div>nlq</div><div>snr</div><div>n snr</div><div>errors</div><div>mbps</div><div>{{units.distanceUnit()}}</div>
</div>
</div>
{%
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("<div class='cols " + status + "'>");
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(`<div style='flex:0.75'><a onclick="event.stopPropagation()" title='Link status: ${status}' href='http://${tracker.hostname}.local.mesh'>${tracker.hostname}</a></div>`);
}
else {
print(`<div style='flex:0.75'><a onclick="event.stopPropagation()" title='Link status: ${status}' href='http://${tracker.ip}'>${tracker.ip}</a></div>`);
}
print("<div class='ts cols stats'>");
print(`<div>${lq}</div><div>${nlq}</div><div></div><div></div><div>${100 - tracker.quality}%</div><div></div><div></div>`);
print("</div></div>");
}
}
else {
print("<div>None</div>");
}
%}
</div>
</div>
<div class="noctrl" hx-target="#ctrl-modal">
<div class="section-title">Neighborhood Nodes</div>
<div class="section" style="line-height:18px">
{%
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(`<div class="ctrl cols status ${status}" hx-get="status/e/neighbor-device?m=${tracker.mac}">`);
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(`<div style='flex:0.75'><a onclick="event.stopPropagation()" title='${title}status: ${status}' href='http://${tracker.hostname}.local.mesh'>${tracker.hostname}<div class="icon ${icon}"></div></a></div>`);
}
else {
print(`<div style='flex:0.75'><a onclick="event.stopPropagation()" title='${title}status: ${status}' href='http://${tracker.ip}'>${tracker.ip}<div class="icon ${icon}"></div></a></div>`);
}
print("<div class='ts cols stats'>");
let d = "-";
if ("distance" in tracker) {
d = units.meters2distance(tracker.distance);
if (d < 1) {
d = "< 1";
}
else {
d = sprintf("%.1f", d);
}
}
print(`<div>${lq}</div><div>${nlq}</div><div>${tracker.snr || "-"}</div><div>${tracker.rev_snr || "-"}</div><div>${100 - tracker.quality}%</div><div>${calcBitrate(tracker.tx_bitrate, tracker.rx_bitrate)}</div><div>${d}</div>`);
print("</div></div>");
}
}
else {
print("<div>None</div>");
}
%}
</div>
</div>
{% if (length(hlist) > 0) { %}
<div class="noctrl">
<div class="section-title">Hidden Nodes</div>
<div class="section" style="line-height:18px">
{%
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(`<div style='flex:0.75'><a onclick="event.stopPropagation()" title='Link status: hidden' href='http://${hostname}.local.mesh'>${hostname}</a></div>`);
}
%}
</div>
</div>
{% } %}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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 += ` <div class="icon warning" title="Service cannot be reached"></div>`;
}
const v2 = match(v[1], reType);
if (v2) {
v[1] = v2[1];
type += ` <div class="icon ${v2[2]}"></div>`;
}
switch (v[4]) {
case "80":
push(services, `<div class="service"><a href="http://${v[3]}.local.mesh/${v[5]}" onclick="event.stopPropagation()">${v[1]}${type}</a></div>`);
break;
case "443":
push(services, `<div class="service"><a href="https://${v[3]}.local.mesh/${v[5]}" onclick="event.stopPropagation()">${v[1]}${type}</a></div>`);
break;
default:
push(services, `<div class="service"><a href="${v[2]}://${v[3]}.local.mesh:${v[4]}/${v[5]}" onclick="event.stopPropagation()">${v[1]}${type}</a></div>`);
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 = ` <div class="icon ${k2[2]}"></div>`;
}
push(services, `<div class="service">${k[1]}${type}</div>`);
}
}
}
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, `<div class="device">${v[3]}</div>`);
}
}
f.close();
}
%}
<div class="ctrl" hx-get="status/e/local-services" hx-target="#ctrl-modal">
<div class="section-title">Local Services</div>
<div class="section">
{%
if (length(services) === 0) {
print("None");
}
else {
print("<div class='cols'>");
for (let i = 0; i < length(services); i++) {
print(services[i]);
}
print("</div>");
}
%}
</div>
</div>
<hr>
<div class="noctrl">
<div class="section-title">Local Devices</div>
<div class="section">
{%
if (length(devices) === 0) {
print("None");
}
else {
print("<div class='cols'>");
for (let i = 0; i < length(devices); i++) {
print(devices[i]);
}
print("</div>");
}
%}
</div>
</div>

65
files/app/partial/location.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
%}
<div class="ctrl" hx-get="status/e/location" hx-target="#ctrl-modal">
{% if (mapurl) { %}
<div class="location-image"><iframe loading="eager" src="{{mapurl}}"></iframe></div>
<script>
(function(){
fetch("{{mapurl}}").catch(_ => document.querySelector(".location-image").style.display = "none");
})();
</script>
{% } %}
{% if (lat && lon) { %}
<div class="cols">
<div class="t">{{lat}}, {{lon}}</div>
<div style="flex:0">{{gridsquare}}</div>
{% } else if (gridsquare) { %}
<div class="cols">
<div class="t">{{gridsquare}}</div>
{% } else { %}
<div class="cols">
<div class="t">Unknown</div>
{% } %}
</div>
<div class="s">location{{source ? " (" + source + ")" : ""}}</div>
</div>

109
files/app/partial/login.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{% if (!auth.isAdmin) { %}
<dialog id="login">
Password <input type="password"><button>OK</button>
</dialog>
<div id="login-icon" class="popup-menu">
<label>
<input type="checkbox">
<div class="icon login"></div>
</label>
<div class="menu">
<div>Login</div>
</div>
</div>
<script>
const loginDialog = document.getElementById('login');
document.querySelector("#login-icon .menu > div").addEventListener("click", function()
{
loginDialog.showModal();
});
loginDialog.addEventListener("close", function()
{
loginDialog.querySelector("input").value = "";
});
function doAuth() {
fetch(`${location.origin}/a/authenticate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ version: 1, password: document.querySelector("#login input").value })
}).then(function(response) {
if (response.status === 200) {
response.json().then(function(json) {
if (json.authenticated) {
location.reload();
}
});
}
});
loginDialog.close();
}
loginDialog.querySelector('button').addEventListener("click", doAuth);
loginDialog.querySelector('input').addEventListener("change", doAuth);
</script>
{% } else { %}
<div id="logout-icon" class="popup-menu">
<label>
<input type="checkbox">
<div class="icon login authenticated"></div>
</label>
<div class="menu">
<div>Logout</div>
<div>Reboot</div>
<div><a href="http://docs.arednmesh.org/">Help</a></div>
</div>
</div>
<script>
document.querySelector("#logout-icon .menu > div:first-child").addEventListener("click", function()
{
fetch(`${location.origin}/a/authenticate`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ version: 1, logout: true })
}).then(function(response) {
location.reload();
});
});
document.querySelector("#logout-icon .menu > div:nth-child(2)").addEventListener("click", function()
{
htmx.ajax("PUT", `${location.origin}/a/status/e/reboot`);
});
</script>
{% } %}

127
files/app/partial/mesh-data.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
print("<script>\n");
print("window.mesh = {hosts:{");
const reHostsOlsr = /^([0-9.]+)[ \t]+([^ \t]+)[ \t]+#[ \t]+([^ \t]+)/;
const reServices = /^([^|]+)\|(tcp|udp)\|(.+)[ \t]+#[ \t]*([^ \t\n]+)/;
const reRoutes = /^([0-9.]+)\/([0-9]+)\t[0-9.]+\t[0-9]+\t([0-9.]+)/;
let myself = null;
const h = fs.open("/var/run/hosts_olsr");
if (h) {
let originator = null;
let first = true;
for (let line = h.read("line"); length(line); line = h.read("line")) {
const v = match(trim(line), reHostsOlsr);
if (v) {
if (originator !== v[3]) {
if (originator) {
print("],");
}
originator = v[3];
if (originator === "myself") {
myself = v[1];
print(`"${myself}":[`);
}
else {
print(`"${originator}":[`);
}
first = true;
}
const host = v[2];
if (index(host, "mid") !== 0 && index(host, "dtdlink") !== 0 && index(host, "xlink") !== 0) {
if (!first) {
print(",");
}
if (v[1] === originator || (originator == "myself" && v[1] == myself)) {
print(`["${host}"]`);
}
else {
print(`["${host}","${v[1]}"]`);
}
first = false;
}
}
}
print("]");
h.close();
}
print("},services:{");
const s = fs.open("/var/run/services_olsr");
if (s) {
let first = true;
let ip = null;
for (let line = s.read("line"); length(line); line = s.read("line")) {
const v = match(line, reServices);
if (v) {
if (ip !== v[4]) {
if (ip) {
print("],");
}
ip = v[4];
if (ip === "my") {
print(`"${myself}":[`);
}
else {
print(`"${ip}":[`);
}
first = true;
}
if (!first) {
print(",");
}
print(`{n:"${v[3]}",u:"${v[1]}"}`);
first = false;
}
}
print("]");
s.close();
}
print(`},etx:[["${myself}",0.0],`);
const f = fs.popen("exec /usr/bin/curl http://127.0.0.1:2006/rou -o - 2> /dev/null");
if (f) {
for (let l = f.read("line"); length(l); l = f.read("line")) {
const m = match(l, reRoutes);
if (m && m[2] > 8 && m[3] <= 50) {
print(`["${m[1]}",${sprintf("%.1f", m[3])}],`);
}
}
}
print("].sort((a,b)=>a[1]-b[1])};\n");
print("</script>\n");
%}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const counts = mesh.getNodeCounts();
%}
<div class="noctrl">
<div class="section-title">Mesh</div>
<div class="section">
<div class="cols">
<div>
<div class="t">{{counts.nodes}}</div>
<div class="s">nodes</div>
</div>
<div>
<div class="t">{{counts.devices}}</div>
<div class="s">devices</div>
</div>
<div></div>
</div>
</div>
</div>

57
files/app/partial/mesh.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("mesh-data")}}
<div style="width:100%">
<div id="meshfilter">
<div style="width:115px"></div>
<div style="flex:1;text-align:center"><input type="search" autocorrect="off" spellcheck="false" placeholder="Search the mesh ..."></div>
<div style="padding-right:60px"><button id="meshpage-help">Help</button></div>
</div>
<div class="meshpage-help">
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.
<p>
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.
</div>
<div id="meshpage"></div>
</div>
{% if (!config.resourcehash) { %}
<script src="/a/js/meshpage.js"></script>
{% } else { %}
<script src="http://localnode.local.mesh/a/js/meshpage.js.{{versions.meshpage}}" onerror="s=document.createElement('script');s.type='text/javascript';s.onload=()=>htmx.process(document.body);s.src='/a/js/meshpage.js.{{versions.meshpage}}';document.head.appendChild(s)"></script>
{% } %}

99
files/app/partial/messages.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const allmsgs = messages.getMessages();
const todos = messages.getToDos();
%}
<div class="noctrl">
{% if (allmsgs.system) { %}
<div class="section-title">System Messages</div>
<div class="section">
{%
const msgs = allmsgs.system;
for (let i = 0; i < length(msgs); i++) {
print(`<div>${msgs[i]}</div>`);
}
delete allmsgs.system;
%}
</div>
{% }
if (allmsgs.yournode) { %}
<div class="section-title">Your Messages</div>
<div class="section">
{%
const msgs = allmsgs.yournode;
for (let i = 0; i < length(msgs); i++) {
print(`<div>${msgs[i]}</div>`);
}
delete allmsgs.yournode;
%}
</div>
{% }
if (allmsgs["all nodes"]) { %}
<div class="section-title">All Node Messages</div>
<div class="section">
{%
const msgs = allmsgs["all nodes"];
for (let i = 0; i < length(msgs); i++) {
print(`<div>${msgs[i]}</div>`);
}
delete allmsgs["all nodes"];
%}
</div>
{% }
for (let _ in allmsgs) {
%}<div class="section-title">Other Messages</div>
<div class="section">{%
for (let k in allmsgs) {
const msgs = allmsgs[k];
for (let i = 0; i < length(msgs); i++) {
print(`<div><b>${k}: </b>${msgs[i]}</div>`);
}
}
%}</div>{%
break;
}
if (length(todos) > 0) { %}
<div class="section-title">To Do</div>
<div class="section">
{%
for (let i = 0; i < length(todos); i++) {
print(`<div>${todos[i]}</div>`);
}
%}
</div>
{% }
%}
</div>

11
files/app/partial/nav-status.ut Executable file
View File

@ -0,0 +1,11 @@
<div id="nav-status" {{request.headers["hx-boosted"] ? 'hx-swap-oob="true"' : ""}}>{%
if (request.page !== "status") {
print(request.page);
}
else if (auth.isAdmin) {
print("admin");
}
else {
print("status");
}
%}</div>

75
files/app/partial/nav.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{{_R("changes")}}
<a href="http://arednmesh.org"><div id="icon-logo"></div><div>AREDN<span style="font-size:4px">TM</span></div></a>
<div class="nav-node-name">{{configuration.getName()}}</div>
{{_R("nav-status")}}
<div style="flex:1"></div>
{{_R("login")}}
{% if (auth.isAdmin) { %}
<script>
(function navinit()
{
if (!window.htmx) {
document.body.addEventListener("htmx:load", navinit);
return;
}
document.body.removeEventListener("htmx:load", navinit);
const logo = htmx.find("#icon-logo");
htmx.on("htmx:beforeRequest", function(e) {
if (e.detail.requestConfig.triggeringEvent?.type === "click") {
const ismodal = e.detail.target?.id === "ctrl-modal";
setTimeout(_ => {
if (document.body.classList.contains("htmx-request")) {
logo.classList.add("animate");
}
}, ismodal ? 1000 : 500);
if (ismodal) {
setTimeout(_ => {
if (!e.detail.target.open) {
e.detail.target.showModal();
}
}, 200);
}
}
});
htmx.on(logo, "animationiteration", function() {
if (!document.body.classList.contains("htmx-request")) {
logo.classList.remove("animate");
}
});
})();
</script>
{% } %}

80
files/app/partial/network.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="network ctrl" hx-get="status/e/network" hx-target="#ctrl-modal">
<div class="section-title">Network</div>
<div class="t">{{uci.get("network", "wifi", "ipaddr")}} <span class="ts">{{uci.get("network", "wifi", "netmask") == "255.255.255.255" ? "/ 32" : "/ 8"}}<span></div>
<div class="s">mesh address</div>
<div class="t">{{uci.get("network", "lan", "ipaddr")}}
<span class="ts">{{`/ ${network.netmaskToCIDR(uci.get("network", "lan", "netmask"))} `}}<span>
</div>
<div class="s">lan address</div>
{%
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("<div class='t'>" + wan.address + " <span class='ts'>/ " + wan.mask + "</span></div>");
print("<div class='s'>wan address (dhcp)</div>");
print("<div class='t'>" + ifaces[i].route[0].nexthop + "</div>");
print("<div class='s'>wan gateway</div>");
validWan = true;
break;
}
}
}
else if (wan_proto === "static") {
print("<div class='t'>" + uci.get("network", "wan", "ipaddr") + "</div>");
print("<div class='s'>wan address (static)</div>");
print("<div class='t'>" + uci.get("network", "wan", "gateway") + "</div>");
print("<div class='s'>wan gateway</div>");
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 += "&nbsp;&nbsp;&nbsp;" + dns[1];
}
}
print("<div class='t'>" + v + "</div>");
print("<div class='s'>wan dns</div>")
}
%}
</div>

1
files/app/partial/oldui.ut Executable file
View File

@ -0,0 +1 @@
<a style="position:absolute;top:17px;right:70px;z-index:10;text-decoration:none;background-color:green;color:white;border-radius:5px;padding:5px 10px;" href="/cgi-bin/status">Old UI</a>

40
files/app/partial/open.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{
const d = document.querySelector('dialog');
if (d && !d.open) {
d.showModal();
}
}

51
files/app/partial/packages.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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();
}
%}
<div class="t">{{count}}</div>
<div class="s">installed packages</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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++;
});
%}
<div class="ctrl" hx-get="status/e/ports-and-xlinks" hx-target="#ctrl-modal">
<div class="cols">
{% if (length(ports) > 1) { %}
<div>
<div class="section-title">Ethernet Ports</div>
<div class="section cols">
<div>
<div class="t">{{length(ports)}}</div>
<div class="s">ports</div>
</div>
<div>
<div class="t">{{active}}</div>
<div class="s">active</div>
</div>
</div>
</div>
{% } %}
<div>
<div class="section-title">XLinks</div>
<div class="section">
<div class="t">{{xcount}}</div>
<div class="s">xlinks</div>
</div>
</div>
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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;
}
}
%}
<div class="ctrl" hx-get="status/e/radio-and-antenna" hx-target="#ctrl-modal">
<div class="section-title">Radio</div>
<div class="section">
<div class="t">{{hardware.getRadio().name}}</div>
<div class="s">model</div>
{% if (midx !== -1) { %}
<div class="cols">
<div>
<div class="t">{{radio[midx].modes[radios.RADIO_MESH].channel}}</div>
<div class="s">channel</div>
</div>
<div>
<div class="t">{{hardware.getChannelFrequencyRange(radio[midx].iface, radio[midx].modes[radios.RADIO_MESH].channel, radio[midx].modes[radios.RADIO_MESH].bandwidth)}}</div>
<div class="s">frequencies</div>
</div>
<div>
<div class="t">{{radio[midx].modes[radios.RADIO_MESH].bandwidth}} MHz</div>
<div class="s">bandwidth</div>
</div>
</div>
<div class="cols">
<div>
<div class="t">{{radio[midx].modes[radios.RADIO_MESH].txpower + radio[midx].txpoweroffset}} dBm</div>
<div class="s">tx power</div>
</div>
<div>
<div class="t">{{int(0.5 + units.meters2distance(uci.get("aredn", "@lqm[0]", "max_distance")))}} {{units.distanceUnit()}}</div>
<div class="s">maximum distance</div>
</div>
<div>
<div class="t">{{uci.get("aredn", "@lqm[0]", "min_snr")}}</div>
<div class="s">minimum snr</div>
</div>
</div>
{% } %}
</div>
{% if (midx !== -1) { %}
<div class="section-title">Antenna</div>
<div class="section">
{%
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"));
%}
<div class="t">{{(antenna || { description: "-"}).description}}</div>
<div class="s">antenna</div>
{% if (antennaAux) { %}
<div class="t">{{(antennaAux || { description: "-"}).description}}</div>
<div class="s">aux antenna</div>
{% } %}
<div class="cols">
<div>
{% if (uci.get("aredn", "@location[0]", "azimuth")) { %}
<div class="t">{{uci.get("aredn", "@location[0]", "azimuth")}}&deg;</div>
{% } else { %}
<div class="t">-</div>
{% } %}
<div class="s">azimuth</div>
</div>
<div>
{% if (uci.get("aredn", "@location[0]", "height")) { %}
<div class="t">{{uci.get("aredn", "@location[0]", "height")}}m</div>
{% } else { %}
<div class="t">-</div>
{% } %}
<div class="s">height</div>
</div>
<div>
{% if (uci.get("aredn", "@location[0]", "elevation")) { %}
<div class="t">{{uci.get("aredn", "@location[0]", "elevation")}}&deg;</div>
{% } else { %}
<div class="t">-</div>
{% } %}
<div class="s">elevation</div>
</div>
</div>
</div>
{% } %}
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
const firstuse = fs.access("/tmp/do-not-keep-configuration");
%}
<title>{{configuration.getName()}} firmware updating</title>
<div id="all" hx-swap-oob="true">
<div class="reboot">
<div>
<div id="icon-logo""></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div id="firmware-title">Updating Firmware</div>
<div id="firmware-msg">Updating the firmware on your node.<br><b>DO NOT REMOVE POWER UNTIL THIS IS COMPLETE.</b></div>
<div>
<div><progress id="cdprogress" max="300"></div>
<div id="countdown">&nbsp;</div>
</div>
</div>
<script>
document.getElementById("ctrl-modal").close();
setTimeout(function() {
document.getElementById("firmware-title").innerHTML = "Rebooting";
document.getElementById("firmware-msg").innerHTML =
{% if (firstuse) { %}
"<div>Your node is rebooting.<br>After this is complete it will have the address <b>192.168.1.1</b> and be ready to setup from scratch.<p>Your browser will attempt to reconnect automatically, but you may need to adjust the network settings on your computer.</div>";
{% } else { %}
"<div>Your node is rebooting.<br>This browser will reconnect automatically once complete.</div>";
{% } %}
}, 100 * 1000);
</script>
{{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: (firstuse ? `http://192.168.1.1/` : `http://${request.env.HTTP_HOST}/a/status`) })}}
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div id="all" hx-swap-oob="true">
<div class="reboot">
<div>
<div id="icon-logo""></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div id="firmware-title">Installing Firmware</div>
<div id="firmware-msg">Installing the firmware on your node.<br><b>DO NOT REMOVE POWER UNTIL THIS IS COMPLETE.</b></div>
<div>
<div><progress id="cdprogress" max="300"></div>
<div id="countdown">&nbsp;</div>
</div>
</div>
<script>
setTimeout(function() {
document.getElementById("firmware-title").innerHTML = "Rebooting";
document.getElementById("firmware-msg").innerHTML = "<div>Your node is rebooting.<br>This browser will reconnect automatically once complete.</div>";
}, 100 * 1000);
</script>
{{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: `http://192.168.1.1/` })}}
</div>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
configuration.reset();
const address = configuration.getSettingAsString("wifi_ip", "192.168.1.1");
%}
<div id="all" hx-swap-oob="true">
<div class="reboot">
<div>
<div id="icon-logo""></div>
<div></div>
<div>AREDN<span>TM</span></div>
<div>Amateur Radio Emergency Data Network</div>
</div>
<div>
<div>Rebooting</div>
<div>Your node is rebooting.<br>After this is complete it will have the address <b>{{address}}</b><p>Your browser will attempt to reconnect automatically, but you may need to adjust the network settings on your computer.</div>
<div>
<div><progress id="cdprogress" max="300"></div>
<div id="countdown">&nbsp;</div>
</div>
</div>
{{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: `http://${address}/a/status` })}}
</div>
</div>

81
files/app/partial/reboot-mon.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<script>
const TIMEOUT = {{inner.timeout}} * 1000;
const COUNTDOWN = {{inner.countdown}};
const DELAY = {{inner.delay}} * 1000;
const LOCATION = "{{inner.location}}";
function reload() {
const start = Date.now();
const req = new XMLHttpRequest();
req.open('GET', LOCATION);
req.onreadystatechange = function() {
if (req.readyState === 4) {
if (req.status === 200) {
window.location = LOCATION;
}
else {
const time = Date.now() - start;
setTimeout(reload, time > TIMEOUT ? 0 : TIMEOUT - time);
}
}
}
req.timeout = TIMEOUT;
try {
req.send(null);
}
catch (_) {
}
}
const start = Date.now()
function cdown() {
const div = document.getElementById("countdown");
if (div) {
const t = Math.round(COUNTDOWN - (Date.now() - start) / 1000);
div.innerHTML = t <= 0 ? "&nbsp;" : `Time Remaining: ${new Date(1000 * t).toISOString().substring(15, 19)}`;
const cdp = document.getElementById("cdprogress");
if (cdp) {
if (t < 0) {
cdp.removeAttribute("value");
}
else {
cdp.setAttribute("value", cdp.getAttribute("max") - t);
}
}
}
}
setInterval(cdown, 1000);
setTimeout(reload, DELAY);
</script>

62
files/app/partial/selection.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div hx-boost="true" hx-swap="none">
<a title="Node status" href="status">
<div class="icon status {{request.page === "status"}}"></div>
</a>
<a title="See what is on the mesh" href="mesh">
<div class="icon mesh {{request.page === "mesh"}}"></div>
</a>
{%
const ip = (match(fs.readfile("/tmp/dnsmasq.d/supernode.conf"), /^#([0-9.]+)/) || [])[1];
if (ip) { %}
<a title="See what is on the whole AREDN mesh" href="http://{{ip}}/a/mesh">
<div class="icon cloudmesh"></div>
</a>
{% } %}
{%
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) {
%}
<a title="Map of the mesh" href="{{mapurl}}">
<div class="icon map"></div>
</a>
{% } %}
</div>
<div style="flex:1"></div>
{{_R("tools")}}

95
files/app/partial/status.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div id="c1">
<div id="general">
{{_R("general")}}
</div>
<div id="location">
<hr>
{{_R("location")}}
</div>
</div>
<div id="c2">
<div hx-get="messages" hx-trigger="every 300s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{% if (messages.haveMessages() || (auth.isAdmin && messages.haveToDos())) { %}
<div id="messages">
{{_R("messages")}}
</div>
{% } %}
</div>
<div id="services">
{% if (auth.isAdmin) { %}
{{_R("internal-services" )}}
{% } %}
{{_R("local-services")}}
</div>
<div id="local-and-neighbor-devices">
<hr>
<div hx-get="local-and-neighbor-devices" hx-trigger="every 60s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{{_R("local-and-neighbor-devices")}}
</div>
</div>
</div>
<div id="c3">
<div id="radio-and-antenna">
{{_R("radio-and-antenna")}}
</div>
<div id="mesh-summary">
<hr>
<div hx-get="mesh-summary" hx-trigger="every 120s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{{_R("mesh-summary")}}
</div>
</div>
<div id="dhcp">
<hr>
<div hx-get="dhcp" hx-trigger="every 120s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{{_R("dhcp")}}
</div>
</div>
{% if (length(hardware.getEthernetPorts()) > 0) { %}
<div id="ports-and-xlinks">
<hr>
{{_R("ports-and-xlinks")}}
</div>
{% } %}
{% if (fs.access("/usr/bin/wg") || fs.access("/usr/sbin/vtund")) { %}
<div id="tunnels">
<hr>
<div hx-get="tunnels" hx-trigger="every 120s [document.visibilityState === 'visible'], visibilitychange[document.visibilityState === 'visible'] from:document">
{{_R("tunnels")}}
</div>
</div>
{% } %}
</div>

37
files/app/partial/switch.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<label class="switch {{inner.style || ""}}">
<input type="checkbox" name="{{inner.name || "unknown"}}" {{inner.value ? "checked" : ""}} hx-put="{{request.env.REQUEST_URI}}" hx-vals='js:{"{{inner.name}}":htmx.find("[name=\"{{inner.name}}\"]").checked ? "on" : "off"}'>
</label>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div class="ctrl-modal-footer">
<hr/>
<button id="dialog-done" onclick="setTimeout(_ => document.getElementById('ctrl-modal').close(), 10)">Done</button>
</div>

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div>
<input style="position:absolute;top:-10000px">
<button id="dialog-help" class="{{request.headers["include-help"] === "1" ? "enabled" : ""}}" hx-get="{{request.env.REQUEST_URI}}" hx-trigger="click delay:10ms" hx-target="#ctrl-modal" hx-headers='{"Include-Help":"{{includeHelp ? "0" : "1"}}","Include-Advanced":"{{request.headers['include-advanced'] === "1" ? "1" : "0"}}"}'>Help</button>
<div class="t">{{inner}}</div>
<div class="s">tool</div>
<hr/>
</div>

50
files/app/partial/tools.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
<div id="tools" class="popup-menu">
<label>
<input type="checkbox">
<div title="Node tools" class="icon tools"></div>
</label>
<div class="menu">
{% if (hardware.getRadioCount() > 0) { %}
<div hx-trigger="click" hx-get="tools/e/wifiscan" hx-target="#ctrl-modal"><div class="icon signal"></div>WiFi Scan</div>
<div hx-trigger="click" hx-get="tools/e/wifisignal" hx-target="#ctrl-modal"><div class="icon wifi"></div>WiFi Signal</div>
{% } %}
<div hx-trigger="click" hx-get="tools/e/ping" hx-target="#ctrl-modal"><div class="icon bolt"></div>Ping</div>
<div hx-trigger="click" hx-get="tools/e/traceroute" hx-target="#ctrl-modal"><div class="icon plane"></div>Traceroute</div>
<div hx-trigger="click" hx-get="tools/e/iperf3" hx-target="#ctrl-modal"><div class="icon twoarrow"></div>iPerf3</div>
<div hx-trigger="click" hx-put="tools/e/supportdata" hx-swap="none"><div class="icon download"></div>Support Data</div>
</div>
</div>

133
files/app/partial/tunnels.ut Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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();
}
}
%}
<div class="ctrl" hx-get="status/e/tunnels" hx-target="#ctrl-modal" hx-swap="innerHTML">
<div class="section-title">Tunnels</div>
<div class="section">
<div class="section-subtitle">Wireguard</div>
<div class="cols">
<div>
<div class="t">{{wac}}</div>
<div class="s">active clients</div>
</div>
<div>
<div class="t">{{wc}}</div>
<div class="s">allocated clients</div>
</div>
<div>
<div class="t">{{was}}</div>
<div class="s">active servers</div>
</div>
<div>
<div class="t">{{ws}}</div>
<div class="s">allocated servers</div>
</div>
</div>
<div class="section-subtitle">Legacy</div>
<div class="cols">
<div>
<div class="t">{{lac}}</div>
<div class="s">active clients</div>
</div>
<div>
<div class="t">{{lc}}</div>
<div class="s">allocated clients</div>
</div>
<div>
<div class="t">{{las}}</div>
<div class="s">active servers</div>
</div>
<div>
<div class="t">{{ls}}</div>
<div class="s">allocated servers</div>
</div>
</div>
</div>
</div>

1460
files/app/resource/css/admin.css Executable file

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -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;
}

1076
files/app/resource/css/user.css Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

118
files/app/resource/js/meshpage.js Executable file
View File

@ -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 = `<div class="icon ${nametype[2]}"></div>`;
}
const r = url.match(/^(.+:\/\/)([^:]+):(\d+)(.*)$/);
switch (r[3]) {
case "0":
view += `<div class="service" data-search="${lname}"><span>${name}</span>${type}</div>`;
break;
case "80":
case "443":
view += `<div class="service" data-search="${lname}"><a target="_blank" href="${r[1]}${r[2]}.local.mesh${r[4]}">${name}</a>&#8288;${type ? type : "<div></div>"}</div>`;
break;
default:
view += `<div class="service" data-search="${lname}"><a target="_blank" href="${r[1]}${r[2]}.local.mesh:${r[3]}${r[4]}">${name}</a>&#8288;${type ? type : "<div></div>"}</div>`;
break;
}
}
}
}
return view;
}
const blocks = [ 1, 2, 3, 5, 10, 1000 ];
const labels = [ "Excellent", "Good", "Fair", "Slow", "Poor", "Improbable" ];
let data = `<div class="block block1"><div class="label">${labels[0]}</div>`;
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 += `</div><div class="block block${blocks[0]}"><div class="label">${labels[0]}</div>`;
}
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 += `<div class="lanhost" data-search="${lanhost[0].toLowerCase()}"><div class="name">&nbsp;&nbsp;${lanhost[0]}</div><div class="services">${serv(ip, lanhost[0])}</div></div>`;
}
}
}
data += `<div class="node"><div class="host" data-search="${hostname.toLowerCase()}"><div class="name"><a href="http://${hostname}.local.mesh">${hostname}</a><span class="etx">${item[1]}</span></div><div class="services">${serv(ip, hostname)}</div></div>${lanview ? '<div class="lanhosts">' + lanview + '</div>' : ''}</div>`;
}
}
}
page.innerHTML = data + "</div>";
help.addEventListener("click", () => {
document.querySelector(".meshpage-help").classList.toggle("visible");
});
}
render();

654
files/app/root.ut Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*
* Additional Terms:
*
* Additional use restrictions exist on the AREDN® trademark and logo.
* See AREDNLicense.txt for more info.
*
* Attributions to the AREDN® Project must be retained in the source code.
* If importing this code into a new or existing project attribution
* to the AREDN® project must be added to the source code.
*
* You must not misrepresent the origin of the material contained within.
*
* Modified versions must be modified to attribute to the original source
* and be marked in reasonable ways as differentiate it from the original
* version
*/
%}
{%
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 ? `<div class="help">${str}</div>` : "";
};
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 = `<div><b>ERROR: ${e.message}</b><div><pre>${e.stacktrace[0].context}</pre></div>`;
}
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");
}
};
%}

View File

@ -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

View File

@ -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.<DTDMAC>
time_zone = UTC
time_zone_name = UTC
ntp_server = us.pool.ntp.org
description_node =
compat_version = 1.0

View File

@ -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'

View File

@ -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 <dhcp_start>
option limit <dhcp_limit>
option leasetime 1h
option force 1
option ignore <lan_dhcp>
option interface 'lan'
option start <dhcp_start>
option limit <dhcp_limit>
option leasetime '1h'
option force '1'
option ignore <lan_dhcp>
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'

View File

@ -1,3 +1,4 @@
config dropbear
option PasswordAuth 'on'
option Port '2222'
option Port '2222'

View File

@ -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
<dtdlink_interfaces>
option input REJECT
option output ACCEPT
option forward REJECT
option mtu_fix 1
option name 'dtdlink'
<dtdlink_interfaces>
option input 'REJECT'
option output 'ACCEPT'
option forward 'REJECT'
option mtu_fix '1'
config zone
option name vpn
<vpn_interfaces>
option input REJECT
option output ACCEPT
option forward REJECT
option mtu_fix 1
option name 'vpn'
<vpn_interfaces>
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'

View File

@ -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
<wifi_network_config>
config interface wifi_mon
option proto none
config interface 'wifi_mon'
option proto 'none'
### Bridge configuration
<bridge_network_config>

View File

@ -1,3 +1,4 @@
config olsrd
option IpVersion '4'
option MainIp '<wifi_ip>'

View File

@ -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 '<NODE>'
option sysName '<NODE>.local.mesh'
option sysLocation 'Deployed'
option sysContact '<NODE>'
option sysName '<NODE>.local.mesh'

View File

@ -1,29 +1,30 @@
config 'system'
option 'hostname' '<NODE>'
option 'timezone' '<time_zone>'
option 'description' '<description_node>'
option 'compat_version' '<compat_version>'
option 'log_ip' '<remote_log_ip>'
option 'log_port' '<remote_log_port>'
option 'log_proto' '<remote_log_proto>'
config 'timeserver' 'ntp'
list 'server' '<ntp_server>'
option enable_server 0
option enabled 0
config system
option hostname '<NODE>'
option timezone '<time_zone>'
option description '<description_node>'
option compat_version '<compat_version>'
option log_ip '<remote_log_ip>'
option log_port '<remote_log_port>'
option log_proto '<remote_log_proto>'
config timeserver 'ntp'
list server '<ntp_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

View File

@ -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'

View File

@ -1,5 +0,0 @@
config options
config network
config default

View File

@ -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'

38
files/etc/cron.boot/boot-fixups Executable file
View File

@ -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 <http://www.gnu.org/licenses/>.
Additional Terms:
Additional use restrictions exist on the AREDN® trademark and logo.
See AREDNLicense.txt for more info.
Attributions to the AREDN® Project must be retained in the source code.
If importing this code into a new or existing project attribution
to the AREDN® project must be added to the source code.
You must not misrepresent the origin of the material contained within.
Modified versions must be modified to attribute to the original source
and be marked in reasonable ways as differentiate it from the original
version.
LICENSE
# Until I find out what has left pending vtun changes we do this here
/sbin/uci -c /etc/config.mesh commit vtun

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
Additional Terms:
Additional use restrictions exist on the AREDN(TM) trademark and logo.
See AREDNLicense.txt for more info.
Attributions to the AREDN Project must be retained in the source code.
If importing this code into a new or existing project attribution
to the AREDN project must be added to the source code.
You must not misrepresent the origin of the material contained within.
Modified versions must be modified to attribute to the original source
and be marked in reasonable ways as differentiate it from the original
version
--]]
require("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

View File

@ -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
}

File diff suppressed because it is too large Load Diff

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More