Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 81 additions & 37 deletions scripts/js/settings-dns.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,31 @@

"use strict";

// Remove an element from an array (inline)
function removeFromArray(arr, what) {
let found = arr.indexOf(what);

while (found !== -1) {
//eslint-disable-next-line unicorn/no-array-splice
arr.splice(found, 1);
found = arr.indexOf(what);
}
// Get the manually entered upstream servers from the textarea
function getManualDNSupstreams() {
return $("#DNSupstreamsTextfield")
.val()
.split(/\r?\n/u)
.map(line => line.trim())
.filter(Boolean);
}

// Get the upstream servers selected in the pre-filled provider tables
function getSelectedDNSupstreams() {
const selectedUpstreams = [];
$("#DNSupstreamTabs table input:checked").each(function () {
const title = $(this).closest("td").attr("title");
if (title && !selectedUpstreams.includes(title)) {
selectedUpstreams.push(title);
}
});

return selectedUpstreams;
}

// Check if a manual upstream server is already covered by a selected Plain upstream server
function isCoveredBySelectedPlainServer(upstream, selectedUpstreamsSet) {
return upstream.endsWith("#53") && selectedUpstreamsSet.has(upstream.slice(0, -3));
}

// Show how many upstreams are selected on each Plain/DoT/DoH tab, so it's not
Expand All @@ -32,6 +48,7 @@ function updateUpstreamTabBadges() {
}
}

// Fill the upstreams tables with the given servers, and initialize the manual upstreams textarea with the given value.
function fillDNSupstreams(value, servers) {
let disabledStr = "";
if (value.flags.env_var === true) {
Expand All @@ -40,7 +57,6 @@ function fillDNSupstreams(value, servers) {
}

let i = 0;
let customServers = value.value.length;

// Build a single checkbox <td> for the given address, tracking selection
// state and consuming a custom-server slot when it matches. Plain v4/v6
Expand All @@ -56,7 +72,6 @@ function fillDNSupstreams(value, servers) {
let checkedStr = "";
if (value.value.includes(address) || (portSuffix && value.value.includes(address + "#53"))) {
checkedStr = "checked";
customServers--;
}

return `<td title="${address}">
Expand Down Expand Up @@ -100,35 +115,24 @@ function fillDNSupstreams(value, servers) {

// Add event listener to checkboxes (shared across the Plain/DoT/DoH tabs)
$("input[id^='DNSupstreams-']").on("change", () => {
const upstreams = $("#DNSupstreamsTextfield").val().split("\n");
let customServerCount = 0;
$("#DNSupstreamsTable input").each(function () {
const title = $(this).closest("td").attr("title");
if (this.checked && !upstreams.includes(title)) {
// Add server to array
upstreams.push(title);
} else if (!this.checked && upstreams.includes(title)) {
// Remove server from array
removeFromArray(upstreams, title);
}

if (upstreams.includes(title)) {
customServerCount--;
}
});
// The variable will contain a negative value, we need to add the length to
// get the correct number of custom servers
customServerCount += upstreams.length;
updateDNSserversTextfield(upstreams, customServerCount);
updateCustomDNSserversTitle(getManualDNSupstreams().length);
updateUpstreamTabBadges();
});

// Keep only truly manual entries in the custom textfield
const selectedUpstreamsSet = new Set(getSelectedDNSupstreams());
const manualUpstreams = value.value.filter(
upstream =>
!selectedUpstreamsSet.has(upstream) &&
!isCoveredBySelectedPlainServer(upstream, selectedUpstreamsSet)
);

// Initialize textfield
updateDNSserversTextfield(value.value, customServers);
updateDNSserversTextfield(manualUpstreams);
updateUpstreamTabBadges();

// Expand the box if there are custom servers
if (customServers > 0) {
if (manualUpstreams.length > 0) {
const customBox = document.getElementById("custom-servers-box");
utils.toggleBoxCollapse(customBox, true);
}
Expand All @@ -138,6 +142,11 @@ function fillDNSupstreams(value, servers) {

// Apply styling to the new checkboxes
applyCheckboxRadioStyle();

// Keep custom server count in sync while the user edits the manual list
$("#DNSupstreamsTextfield").on("input", () => {
updateCustomDNSserversTitle(getManualDNSupstreams().length);
});
}

function setInterfaceName(name) {
Expand All @@ -161,14 +170,49 @@ function setInterfaceName(name) {
$("#interface-name-2").text(name);
}

// Update the textfield with all (incl. custom) upstream servers
function updateDNSserversTextfield(upstreams, customServers) {
$("#DNSupstreamsTextfield").val(upstreams.join("\n"));
// Update the textfield with manual upstream servers only
function updateDNSserversTextfield(manualUpstreams) {
$("#DNSupstreamsTextfield").val(manualUpstreams.join("\n"));
updateCustomDNSserversTitle(manualUpstreams.length);
}

function updateCustomDNSserversTitle(customServerCount) {
$("#custom-servers-title").text(
"(" + customServers + " custom server" + (customServers === 1 ? "" : "s") + " enabled)"
"(" + customServerCount + " custom server" + (customServerCount === 1 ? "" : "s") + " enabled)"
);
}

function getMergedDNSupstreams(manualUpstreams) {
// Start with all upstreams selected in the pre-filled provider tables.
const mergedUpstreams = getSelectedDNSupstreams();
const selectedUpstreamsSet = new Set(mergedUpstreams);

// Append manual entries unless already covered by a selected table upstream.
// For plain DNS, selected "IP" also covers manual "IP#53".
for (const line of manualUpstreams) {
const upstream = line.trim();
if (!upstream) {
continue;
}

if (
selectedUpstreamsSet.has(upstream) ||
isCoveredBySelectedPlainServer(upstream, selectedUpstreamsSet)
) {
continue;
}

// Preserve user-entered order while preventing duplicates.
if (!mergedUpstreams.includes(upstream)) {
mergedUpstreams.push(upstream);
}
}

return mergedUpstreams;
}

globalThis.getMergedDNSupstreams = getMergedDNSupstreams;

function getRevServerLines() {
// Return the lines from the textarea (array of lines)
return $(".revServers")
Expand Down
6 changes: 6 additions & 0 deletions scripts/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ function saveSettings() {
if ($(this).is("textarea")) {
value = $(this).val();
value = value === "" ? [] : value.split("\n");

// On DNS settings page, upstream table selections are merged at save
// time so the textarea can remain manual-only in the UI.
if (key === "dns.upstreams" && typeof globalThis.getMergedDNSupstreams === "function") {
value = globalThis.getMergedDNSupstreams(value);
}
}

// If this is an integer number, parse it accordingly
Expand Down
4 changes: 2 additions & 2 deletions settings-dns.lp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mg.include('scripts/lua/settings_header.lp','r')
<div class="box-body">
<div class="row">
<div class="col-sm-12">
<div class="nav-tabs-custom">
<div class="nav-tabs-custom" id="DNSupstreamTabs">
<ul class="nav nav-tabs" role="tablist">
<li class="active" role="presentation">
<a href="#tab-upstreams-plain" aria-controls="tab-upstreams-plain" aria-expanded="true" role="tab" data-toggle="tab">Plain <span class="label label-primary" id="upstreams-count-plain" style="display: none;"></span></a>
Expand Down Expand Up @@ -89,7 +89,7 @@ mg.include('scripts/lua/settings_header.lp','r')
</div>
</div>
<div class="box-body" style="display: none;">
<p>The following list contains all DNS servers selected above. Furthermore, you can add your own custom DNS servers here. The expected format is one server per line in form of <code>IP#port</code>, where the <code>port</code> is optional. If given, it has to be separated by a hash <code>#</code> from the address (e.g. <code>127.0.0.1#5335</code> for a local <code>unbound</code> instance running on port <code>5335</code>). The port defaults to 53 if omitted.</p>
<p>Use this list to add your own custom DNS servers. Servers selected in the table above are configured separately and are not shown here. The expected format is one server per line in form of <code>IP#port</code>, where the <code>port</code> is optional. If given, it has to be separated by a hash <code>#</code> from the address (e.g. <code>127.0.0.1#5335</code> for a local <code>unbound</code> instance running on port <code>5335</code>). The port defaults to 53 if omitted.</p>
<textarea class="form-control" rows="3" id="DNSupstreamsTextfield" data-key="dns.upstreams" placeholder="Enter upstream DNS servers, one per line" style="resize: vertical;"></textarea>
</div>
</div>
Expand Down