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')
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 - + diff --git a/scripts/lua/header.lp b/scripts/lua/header.lp index a9eeee7e..150cf5fb 100644 --- a/scripts/lua/header.lp +++ b/scripts/lua/header.lp @@ -40,9 +40,6 @@ function in_array (val, tab) return false end --- Connection is considered secure if running natively on HTTPS -is_secure = mg.request_info.https - -- Variable to check if user is already authenticated is_authenticated = mg.request_info.is_authenticated @@ -50,7 +47,6 @@ is_authenticated = mg.request_info.is_authenticated - @@ -92,6 +88,7 @@ is_authenticated = mg.request_info.is_authenticated + "> - diff --git a/scripts/lua/header_authenticated.lp b/scripts/lua/header_authenticated.lp index 10b0445c..def5079e 100644 --- a/scripts/lua/header_authenticated.lp +++ b/scripts/lua/header_authenticated.lp @@ -68,7 +68,7 @@ mg.include('header.lp','r')