2024-08-15 21:28:45 -06:00
|
|
|
{%
|
|
|
|
/*
|
|
|
|
* 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">
|
2024-08-16 13:02:10 -06:00
|
|
|
{{_R("dialog-header", "Local Services")}}
|
2024-08-15 21:28:45 -06:00
|
|
|
<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">
|
2024-08-16 18:21:20 -06:00
|
|
|
<input type="text" name="hostname" value="{{aliases.map[i].hostname}}" required pattern="(\*\.)?[a-zA-Z][a-zA-Z0-9_\-]*">
|
2024-08-15 21:28:45 -06:00
|
|
|
<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>
|