diff --git a/.gitattributes b/.gitattributes
index 176a458f..205021e4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
-* text=auto
+# Enforce Unix newlines
+* text=auto eol=lf
diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
new file mode 100644
index 00000000..d2d76ddb
--- /dev/null
+++ b/.github/codeql/codeql-config.yml
@@ -0,0 +1,3 @@
+name: "CodeQL config"
+paths-ignore:
+ - "**/vendor/**"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index cbed6a2e..7fc79500 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -7,29 +7,40 @@ on:
- development
- "!dependabot/**"
pull_request:
- # The branches below must be a subset of the branches above
branches:
- master
- development
+ - "!dependabot/**"
schedule:
- cron: "0 0 * * 0"
+ workflow_dispatch:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
steps:
- - name: Checkout repository
+ - name: Clone repository
uses: actions/checkout@v4.2.2
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v3
with:
+ persist-credentials: false
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3.28.11
+ with:
+ config-file: ./.github/codeql/codeql-config.yml
languages: "javascript"
+ queries: +security-and-quality
- name: Autobuild
- uses: github/codeql-action/autobuild@v3
+ uses: github/codeql-action/autobuild@v3.28.11
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v3
+ uses: github/codeql-action/analyze@v3.28.11
+ with:
+ category: "/language:javascript"
diff --git a/img/logo-bw.svg b/img/logo-bw.svg
new file mode 100644
index 00000000..73600230
--- /dev/null
+++ b/img/logo-bw.svg
@@ -0,0 +1,6 @@
+
diff --git a/img/pihole_icon.svg b/img/pihole_icon.svg
deleted file mode 100644
index 7abd50fa..00000000
--- a/img/pihole_icon.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/index.lp b/index.lp
index 3e5e580c..36998d83 100644
--- a/index.lp
+++ b/index.lp
@@ -14,7 +14,7 @@ mg.include('scripts/lua/header_authenticated.lp','r')
-
+
Total queries
---
diff --git a/login.lp b/login.lp
index 69493531..07054e34 100644
--- a/login.lp
+++ b/login.lp
@@ -14,7 +14,7 @@ mg.include('scripts/lua/header.lp','r')
-

+
Pi-hole
diff --git a/package-lock.json b/package-lock.json
index 52e9e4dd..0cd45e3b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,7 +41,7 @@
"eslint-plugin-compat": "^6.0.2",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.0",
- "prettier": "^3.5.2",
+ "prettier": "^3.5.3",
"xo": "^0.60.0"
}
},
@@ -7060,9 +7060,9 @@
}
},
"node_modules/prettier": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
- "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
diff --git a/package.json b/package.json
index e779385d..240d6bf5 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,8 @@
"prettier:check": "prettier -l \"style/*.css\" \"style/themes/*.css\" \"scripts/**/*.js\"",
"prettier:fix": "prettier --write \"style/*.css\" \"style/themes/*.css\" \"scripts/**/*.js\"",
"xo": "xo",
- "xo:check": "npm run xo -- \"style/*.css\" \"style/themes/*.css\" \"scripts/**\"",
- "xo:fix": "npm run xo -- --fix \"style/*.css\" \"style/themes/*.css\" \"scripts/**\"",
+ "xo:check": "npm run xo",
+ "xo:fix": "npm run xo -- --fix",
"test": "npm run prettier:check && npm run xo:check",
"testpr": "npm run prettier:fix && git diff --ws-error-highlight=all --color=always --exit-code && npm run xo:check"
},
@@ -57,7 +57,7 @@
"eslint-plugin-compat": "^6.0.2",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.0",
- "prettier": "^3.5.2",
+ "prettier": "^3.5.3",
"xo": "^0.60.0"
},
"browserslist": [
@@ -119,11 +119,13 @@
}
],
"unicorn/filename-case": "off",
+ "unicorn/no-anonymous-default-export": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-for-loop": "off",
"unicorn/no-document-cookie": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-includes": "off",
+ "unicorn/prefer-module": "off",
"unicorn/prefer-node-append": "off",
"unicorn/prefer-number-properties": "off",
"unicorn/prefer-query-selector": "off",
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 00000000..88658064
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,11 @@
+/* eslint-env node */
+
+"use strict";
+
+module.exports = (/* ctx */) => ({
+ plugins: {
+ autoprefixer: {
+ cascade: false,
+ },
+ },
+});
diff --git a/scripts/js/footer.js b/scripts/js/footer.js
index 49a06dbe..0e70a12b 100644
--- a/scripts/js/footer.js
+++ b/scripts/js/footer.js
@@ -330,8 +330,20 @@ function updateSystemInfo() {
var system = data.system;
var percentRAM = system.memory.ram["%used"];
var percentSwap = system.memory.swap["%used"];
- var totalRAMGB = system.memory.ram.total / 1024 / 1024;
- var totalSwapGB = system.memory.swap.total / 1024 / 1024;
+ let totalRAM = system.memory.ram.total / 1024;
+ let totalRAMUnit = "MB";
+ if (totalRAM > 1024) {
+ totalRAM /= 1024;
+ totalRAMUnit = "GB";
+ }
+
+ let totalSwap = system.memory.swap.total / 1024;
+ let totalSwapUnit = "MB";
+ if (totalSwap > 1024) {
+ totalSwap /= 1024;
+ totalSwapUnit = "GB";
+ }
+
var swap =
system.memory.swap.total > 0
? ((1e2 * system.memory.swap.used) / system.memory.swap.total).toFixed(1) + " %"
@@ -347,52 +359,50 @@ function updateSystemInfo() {
);
$("#memory").prop(
"title",
- "Total memory: " + totalRAMGB.toFixed(1) + " GB, Swap usage: " + swap
+ "Total memory: " + totalRAM.toFixed(1) + " " + totalRAMUnit + ", Swap usage: " + swap
);
$("#sysinfo-memory-ram").text(
- percentRAM.toFixed(1) + "% of " + totalRAMGB.toFixed(1) + " GB is used"
+ percentRAM.toFixed(1) + "% of " + totalRAM.toFixed(1) + " " + totalRAMUnit + " is used"
);
if (system.memory.swap.total > 0) {
$("#sysinfo-memory-swap").text(
- percentSwap.toFixed(1) + "% of " + totalSwapGB.toFixed(1) + " GB is used"
+ percentSwap.toFixed(1) + "% of " + totalSwap.toFixed(1) + " " + totalSwapUnit + " is used"
);
} else {
$("#sysinfo-memory-swap").text("No swap space available");
}
- color = system.cpu.load.percent[0] > 100 ? "text-red" : "text-green-light";
+ color = system.cpu.load.raw[0] > system.cpu.nprocs ? "text-red" : "text-green-light";
$("#cpu").html(
' CPU: ' +
- system.cpu.load.percent[0].toFixed(1) +
- " %"
+ '"> Load: ' +
+ system.cpu.load.raw[0].toFixed(2) +
+ " / " +
+ system.cpu.load.raw[1].toFixed(2) +
+ " / " +
+ system.cpu.load.raw[2].toFixed(2)
);
$("#cpu").prop(
"title",
- "Load: " +
- system.cpu.load.raw[0].toFixed(2) +
- " " +
- system.cpu.load.raw[1].toFixed(2) +
- " " +
- system.cpu.load.raw[2].toFixed(2) +
- " on " +
+ "Load averages for the past 1, 5, and 15 minutes\non a system with " +
system.cpu.nprocs +
- " cores running " +
+ " core" +
+ (system.cpu.nprocs > 1 ? "s" : "") +
+ " running " +
system.procs +
- " processes"
+ " processes " +
+ (system.cpu.load.raw[0] > system.cpu.nprocs
+ ? " (load is higher than the number of cores)"
+ : "")
);
- $("#sysinfo-cpu").text(
- system.cpu.load.percent[0].toFixed(1) +
- "% (load: " +
- system.cpu.load.raw[0].toFixed(2) +
- " " +
- system.cpu.load.raw[1].toFixed(2) +
- " " +
- system.cpu.load.raw[2].toFixed(2) +
- ") on " +
+ $("#sysinfo-cpu").html(
+ system.cpu["%cpu"].toFixed(1) +
+ "% on " +
system.cpu.nprocs +
- " cores running " +
+ " core" +
+ (system.cpu.nprocs > 1 ? "s" : "") +
+ " running " +
system.procs +
" processes"
);
@@ -493,7 +503,7 @@ function updateVersionInfo() {
},
{
name: "Core",
- local: version.core.local.version,
+ local: version.core.local.version || "N/A",
remote: version.core.remote.version,
branch: version.core.local.branch,
hash: version.core.local.hash,
@@ -502,7 +512,7 @@ function updateVersionInfo() {
},
{
name: "FTL",
- local: version.ftl.local.version,
+ local: version.ftl.local.version || "N/A",
remote: version.ftl.remote.version,
branch: version.ftl.local.branch,
hash: version.ftl.local.hash,
@@ -511,7 +521,7 @@ function updateVersionInfo() {
},
{
name: "Web interface",
- local: version.web.local.version,
+ local: version.web.local.version || "N/A",
remote: version.web.remote.version,
branch: version.web.local.branch,
hash: version.web.local.hash,
@@ -726,7 +736,7 @@ function applyExpertSettings() {
function addAdvancedInfo() {
const advancedInfoSource = $("#advanced-info-data");
const advancedInfoTarget = $("#advanced-info");
- const isTLS = advancedInfoSource.data("tls");
+ const isTLS = location.protocol === "https:";
const clientIP = advancedInfoSource.data("client-ip");
const XForwardedFor = globalThis.atob(advancedInfoSource.data("xff") ?? "");
const starttime = parseFloat(advancedInfoSource.data("starttime"));
@@ -739,7 +749,7 @@ function addAdvancedInfo() {
// Add TLS and client IP info
advancedInfoTarget.append(
'Client:
'
diff --git a/scripts/js/groups-domains.js b/scripts/js/groups-domains.js
index a4fd7aa1..8a6b0f06 100644
--- a/scripts/js/groups-domains.js
+++ b/scripts/js/groups-domains.js
@@ -496,15 +496,17 @@ function addDomain() {
return;
}
- for (var i = 0; i < domains.length; i++) {
- if (kind === "exact" && wildcardChecked) {
- // Transform domain to wildcard if specified by user
- domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$";
- kind = "regex";
-
- // strip leading "*." if specified by user in wildcard mode
+ // Check if the wildcard checkbox was marked and transform the domains into regex
+ if (kind === "exact" && wildcardChecked) {
+ for (var i = 0; i < domains.length; i++) {
+ // Strip leading "*." if specified by user in wildcard mode
if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2);
+
+ // Transform domain into a wildcard regex
+ domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$";
}
+
+ kind = "regex";
}
// determine list type
diff --git a/scripts/js/index.js b/scripts/js/index.js
index 53128623..cfd67bf4 100644
--- a/scripts/js/index.js
+++ b/scripts/js/index.js
@@ -307,6 +307,7 @@ function updateTopClientsTable(blocked) {
url =
'' +
utils.escapeHtml(clientname) +
"";
@@ -364,7 +365,13 @@ function updateTopDomainsTable(blocked) {
domain = encodeURIComponent(item.domain);
// Substitute "." for empty domain lookups
urlText = domain === "" ? "." : domain;
- url = '' + urlText + "";
+ url =
+ '' +
+ urlText +
+ "";
percentage = (item.count / sum) * 100;
domaintable.append(
" " +
diff --git a/scripts/js/interfaces.js b/scripts/js/interfaces.js
index ee5abe09..cd952257 100644
--- a/scripts/js/interfaces.js
+++ b/scripts/js/interfaces.js
@@ -28,29 +28,52 @@ $(function () {
gateways.add(inet6.gateway);
}
- var json = [];
+ var interfaces = {};
+ var masterInterfaces = {};
// For each interface in data.interface, create a new object and push it to json
data.interfaces.forEach(function (interface) {
- const status = interface.carrier
- ? 'UP'
- : 'DOWN';
+ const carrierColor = interface.carrier ? "text-green" : "text-red";
+ let stateText = interface.state.toUpperCase();
+ if (stateText === "UNKNOWN" && interface.flags !== undefined && interface.flags.length > 0) {
+ if (interface.flags.includes("pointopoint")) {
+ // WireGuards, etc. -> the typo is intentional
+ stateText = "P2P";
+ } else if (interface.flags.includes("loopback")) {
+ // Loopback interfaces
+ stateText = "LOOPBACK";
+ }
+ }
+
+ const status = `${stateText}`;
+
+ let master = null;
+ if (interface.master !== undefined) {
+ // Find interface.master in data.interfaces
+ master = data.interfaces.find(obj => obj.index === interface.master).name;
+ }
+
+ // Show an icon for indenting slave interfaces
+ const indentIcon =
+ master === null ? "" : " ⤷ ";
var obj = {
- text: interface.name + " - " + status,
+ text: indentIcon + interface.name + " - " + status,
class: gateways.has(interface.name) ? "text-bold" : null,
- icon: "fa fa-network-wired fa-fw",
+ icon: master === null ? "fa fa-network-wired fa-fw" : "",
nodes: [],
};
- if (interface.master !== undefined) {
- // Find interface.master in data.interfaces
- const master = data.interfaces.find(obj => obj.index === interface.master);
- if (master !== undefined) {
- obj.nodes.push({
- text: "Master interface: " + utils.escapeHtml(master.name) + "",
- icon: "fa fa-network-wired fa-fw",
- });
+ if (master !== null) {
+ obj.nodes.push({
+ text: "Master interface: " + utils.escapeHtml(master) + "",
+ icon: "fa fa-network-wired fa-fw",
+ });
+
+ if (master in masterInterfaces) {
+ masterInterfaces[master].push(interface.name);
+ } else {
+ masterInterfaces[master] = [interface.name];
}
}
@@ -306,6 +329,21 @@ $(function () {
nodes: [],
};
+ furtherDetails.nodes.push(
+ {
+ text:
+ "Carrier: " +
+ (interface.carrier
+ ? "Connected"
+ : "Disconnected"),
+ icon: "fa fa-link fa-fw",
+ },
+ {
+ text: "State: " + utils.escapeHtml(interface.state.toUpperCase()),
+ icon: "fa fa-server fa-fw",
+ }
+ );
+
if (interface.parent_dev_name !== undefined) {
let extra = "";
if (interface.parent_dev_bus_name !== undefined) {
@@ -378,9 +416,37 @@ $(function () {
obj.nodes.push(furtherDetails);
}
- json.push(obj);
+ interfaces[interface.name] = obj;
});
+ // Sort interfaces based on masterInterfaces. If an item is found in
+ // masterInterfaces, it should be placed after the master interface
+ const ifaces = Object.keys(interfaces);
+ const interfaceList = Object.keys(masterInterfaces);
+
+ // Add slave interfaces next to master interfaces
+ for (const master of interfaceList) {
+ if (master in masterInterfaces) {
+ for (const slave of masterInterfaces[master]) {
+ ifaces.splice(ifaces.indexOf(slave), 1);
+ interfaceList.splice(interfaceList.indexOf(master) + 1, 0, slave);
+ }
+ }
+ }
+
+ // Add interfaces that are not slaves at the top of the list (in reverse order)
+ for (const iface of ifaces.reverse()) {
+ if (!interfaceList.includes(iface)) {
+ interfaceList.unshift(iface);
+ }
+ }
+
+ // Build the tree view
+ const json = [];
+ for (const iface of interfaceList) {
+ json.push(interfaces[iface]);
+ }
+
$("#tree").bstreeview({
data: json,
expandIcon: "fa fa-angle-down fa-fw",
diff --git a/scripts/js/login.js b/scripts/js/login.js
index d27191ac..72d95111 100644
--- a/scripts/js/login.js
+++ b/scripts/js/login.js
@@ -136,7 +136,7 @@ $("#totp").on("input", function () {
// Toggle password visibility button
$("#toggle-password").on("click", function () {
// Toggle font-awesome classes to change the svg icon on the button
- $("svg", this).toggleClass("fa-eye fa-eye-slash");
+ $(".field-icon", this).toggleClass("fa-eye fa-eye-slash");
// Password field
var $pwd = $("#current-password");
diff --git a/scripts/js/logout.js b/scripts/js/logout.js
index 40f290ff..e1489c51 100644
--- a/scripts/js/logout.js
+++ b/scripts/js/logout.js
@@ -9,7 +9,8 @@
document.addEventListener("DOMContentLoaded", () => {
const logoutButton = document.getElementById("logout-button");
- logoutButton.addEventListener("click", () => {
+ logoutButton.addEventListener("click", event => {
+ event.preventDefault();
utils.doLogout();
});
});
diff --git a/scripts/js/network.js b/scripts/js/network.js
index 89cef728..8ec52294 100644
--- a/scripts/js/network.js
+++ b/scripts/js/network.js
@@ -138,6 +138,19 @@ $(function () {
var ips = [],
iptitles = [];
+ // Sort IPs, IPv4 before IPv6, then alphabetically
+ data.ips.sort(function (a, b) {
+ if (a.ip.includes(":") && !b.ip.includes(":")) {
+ return 1;
+ }
+
+ if (!a.ip.includes(":") && b.ip.includes(":")) {
+ return -1;
+ }
+
+ return a.ip.localeCompare(b.ip);
+ });
+
for (index = 0; index < data.ips.length; index++) {
var ip = data.ips[index],
iptext = ip.ip;
@@ -238,6 +251,7 @@ $(function () {
{ data: "numQueries", width: "9%", render: $.fn.dataTable.render.text() },
{ data: "", width: "6%", orderable: false },
{ data: "", width: "6%", orderable: false },
+ { data: "ips[].name", visible: false, class: "hide" },
],
drawCallback: function () {
diff --git a/scripts/js/queries.js b/scripts/js/queries.js
index 27c796ec..2dffe258 100644
--- a/scripts/js/queries.js
+++ b/scripts/js/queries.js
@@ -609,13 +609,13 @@ $(function () {
// Prefix colored DNSSEC icon to domain text
var dnssecIcon = "";
dnssecIcon =
- ' ';
+ '">';
// Escape HTML in domain
domain = dnssecIcon + utils.escapeHtml(domain);
diff --git a/scripts/js/settings-dns.js b/scripts/js/settings-dns.js
index 1719d01d..89cd968e 100644
--- a/scripts/js/settings-dns.js
+++ b/scripts/js/settings-dns.js
@@ -99,6 +99,11 @@ function fillDNSupstreams(value, servers) {
applyCheckboxRadioStyle();
}
+function setInterfaceName(interface) {
+ $("#interface-name-1").text(interface);
+ $("#interface-name-2").text(interface);
+}
+
// Update the textfield with all (incl. custom) upstream servers
function updateDNSserversTextfield(upstreams, customServers) {
$("#DNSupstreamsTextfield").val(upstreams.join("\n"));
@@ -114,6 +119,7 @@ function processDNSConfig() {
.done(function (data) {
// Initialize the DNS upstreams
fillDNSupstreams(data.config.dns.upstreams, data.dns_servers);
+ setInterfaceName(data.config.dns.interface.value);
setConfigValues("dns", "dns", data.config.dns);
})
.fail(function (data) {
diff --git a/scripts/js/settings-teleporter.js b/scripts/js/settings-teleporter.js
index fb2c3113..be8fa5f9 100644
--- a/scripts/js/settings-teleporter.js
+++ b/scripts/js/settings-teleporter.js
@@ -105,3 +105,10 @@ $("#GETTeleporter").on("click", function () {
},
});
});
+
+$(function () {
+ // Show warning if not accessed over HTTPS
+ if (location.protocol !== "https:") {
+ $("#encryption-warning").show();
+ }
+});
diff --git a/scripts/lua/footer.lp b/scripts/lua/footer.lp
index 6dcb5640..34885011 100644
--- a/scripts/lua/footer.lp
+++ b/scripts/lua/footer.lp
@@ -68,6 +68,6 @@ end
-
+