mirror of https://github.com/aredn/aredn.git
New UI (#1263)
* New UI * Fix gzip filename race condition * Fix scrolling on first use page
This commit is contained in:
parent
a22e6eb4e0
commit
0432bf3165
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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]+";
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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');
|
||||
%}
|
|
@ -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")}}
|
|
@ -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")}}
|
|
@ -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®</div>
|
||||
<div>AREDN® is currently running in RAM. The next step is to install AREDN® 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 & 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>
|
|
@ -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®</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 & 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>
|
|
@ -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")}}
|
|
@ -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");
|
||||
}
|
||||
%}
|
|
@ -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")}}
|
|
@ -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")}}
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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>
|
|
@ -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 & 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‌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>
|
|
@ -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")}}
|
|
@ -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‌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‌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‌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‌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‌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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 & 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 & 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 & 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 & 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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‌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‌ddress</div>
|
||||
<div class="m">Gateway IP a‌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‌ddress</div>
|
||||
<div class="m">Gateway IP a‌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‌ddress</div>
|
||||
<div class="m">WAN IP a‌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>
|
|
@ -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>
|
|
@ -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 & " : ""}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‌ddress</div>
|
||||
<div>peer a‌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‌ddress" value="{{x.ipaddr}}">
|
||||
<input name="peer" type="text" size="25" pattern="{{constants.patIP}}" placeholder="IP A‌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‌ddress">
|
||||
<input name="peer" type="text" size="25" pattern="${patIP}" placeholder="IP A‌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>
|
|
@ -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 & 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>
|
|
@ -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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
{{_R("reboot-mon", { delay: 20, countdown: 120, timeout: 5, location: `http://${address}/a/status` })}}
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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‌ddress</div>
|
||||
<div class="m">Node name or a‌ddress</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-target" type="text" size="40" required>
|
||||
<select id="tool-select">
|
||||
<option value="">▼</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‌ddress</div>
|
||||
<div class="m">Node name or a‌ddress</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
|
||||
<select id="tool-client-select">
|
||||
<option value="">▼</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>
|
|
@ -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‌ddress</div>
|
||||
<div class="m">IP A‌ddress, Hostname or Node</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-target" type="text" size="40" required>
|
||||
<select id="tool-select">
|
||||
<option value="">▼</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‌ddress</div>
|
||||
<div class="m">Node name or a‌ddress</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
|
||||
<select id="tool-client-select">
|
||||
<option value="">▼</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>
|
|
@ -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"));
|
||||
|
||||
%}
|
|
@ -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‌ddress</div>
|
||||
<div class="m">IP A‌ddress, Hostname or Node</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-target" type="text" size="40" required>
|
||||
<select id="tool-select">
|
||||
<option value="">▼</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‌ddress</div>
|
||||
<div class="m">Node name or a‌ddress</div>
|
||||
</div>
|
||||
<div>
|
||||
<input id="tool-client" type="text" size="40" required value="{{mynode}}">
|
||||
<select id="tool-client-select">
|
||||
<option value="">▼</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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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")}}
|
|
@ -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>
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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")}}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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");
|
||||
|
||||
%}
|
|
@ -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>
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
{% } %}
|
|
@ -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 += " " + dns[1];
|
||||
}
|
||||
}
|
||||
print("<div class='t'>" + v + "</div>");
|
||||
print("<div class='s'>wan dns</div>")
|
||||
}
|
||||
%}
|
||||
</div>
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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")}}°</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")}}°</div>
|
||||
{% } else { %}
|
||||
<div class="t">-</div>
|
||||
{% } %}
|
||||
<div class="s">elevation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
|
@ -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"> </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>
|
|
@ -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"> </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>
|
|
@ -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"> </div>
|
||||
</div>
|
||||
</div>
|
||||
{{_R("reboot-mon", { delay: 120, countdown: 300, timeout: 5, location: `http://${address}/a/status` })}}
|
||||
</div>
|
||||
</div>
|
|
@ -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 ? " " : `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>
|
|
@ -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")}}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -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>⁠${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>⁠${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"> ${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();
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
%}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
|
||||
config dnsmasq
|
||||
option rebind_protection '0'
|
||||
option confdir '/tmp/dnsmasq.d,*.conf'
|
||||
|
||||
config dhcp
|
||||
option interface lan
|
||||
option interface 'lan'
|
||||
option start <dhcp_start>
|
||||
option limit <dhcp_limit>
|
||||
option leasetime 1h
|
||||
option force 1
|
||||
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'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
config dropbear
|
||||
option PasswordAuth 'on'
|
||||
option Port '2222'
|
||||
|
|
|
@ -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 name 'lan'
|
||||
option network 'lan'
|
||||
option input ACCEPT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option input 'ACCEPT'
|
||||
option output 'ACCEPT'
|
||||
option forward 'REJECT'
|
||||
|
||||
config zone
|
||||
option name wan
|
||||
option name 'wan'
|
||||
option network 'wan'
|
||||
option input REJECT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option masq 1
|
||||
option mtu_fix 1
|
||||
option input 'REJECT'
|
||||
option output 'ACCEPT'
|
||||
option forward 'REJECT'
|
||||
option masq '1'
|
||||
option mtu_fix '1'
|
||||
|
||||
config zone
|
||||
option name wifi
|
||||
option name 'wifi'
|
||||
option network 'wifi'
|
||||
option input REJECT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option mtu_fix 1
|
||||
option input 'REJECT'
|
||||
option output 'ACCEPT'
|
||||
option forward 'REJECT'
|
||||
option mtu_fix '1'
|
||||
|
||||
config zone
|
||||
option name dtdlink
|
||||
option name 'dtdlink'
|
||||
<dtdlink_interfaces>
|
||||
option input REJECT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option mtu_fix 1
|
||||
option input 'REJECT'
|
||||
option output 'ACCEPT'
|
||||
option forward 'REJECT'
|
||||
option mtu_fix '1'
|
||||
|
||||
config zone
|
||||
option name vpn
|
||||
option name 'vpn'
|
||||
<vpn_interfaces>
|
||||
option input REJECT
|
||||
option output ACCEPT
|
||||
option forward REJECT
|
||||
option mtu_fix 1
|
||||
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'
|
||||
|
|
|
@ -7,17 +7,17 @@ config globals 'globals'
|
|||
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>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
config olsrd
|
||||
option IpVersion '4'
|
||||
option MainIp '<wifi_ip>'
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
|
||||
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'
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
config options
|
||||
|
||||
config network
|
||||
|
||||
config default
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 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 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
|
||||
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
|
||||
|
||||
if [ -z "$(uci -q get aredn.@alerts[0])" ]; then
|
||||
uci -q add aredn alerts
|
||||
uci -q commit aredn
|
||||
fi
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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
Loading…
Reference in New Issue