/* Pi-hole: A black hole for Internet advertisements * (c) 2017 Pi-hole, LLC (https://pi-hole.net) * Network-wide ad blocking via your own hardware. * * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ /* global moment:false, utils:false, REFRESH_INTERVAL:false */ "use strict"; // These values are provided by the API (/info/database). // We initialize them as null and populate them during page init. let beginningOfTime = null; // seconds since epoch (set from API: info/database.earliest_timestamp) // endOfTime should be the end of today (local), in seconds since epoch const endOfTime = moment().endOf("day").unix(); let from = null; let until = null; const dateformat = "MMM Do YYYY, HH:mm"; let table = null; let cursor = null; const filters = [ "client_ip", "client_name", "domain", "upstream", "type", "status", "reply", "dnssec", ]; let doDNSSEC = false; // Check if pihole is validiting DNSSEC function getDnssecConfig() { $.getJSON(document.body.dataset.apiurl + "/config/dns/dnssec", data => { doDNSSEC = data.config.dns.dnssec; // redraw the table to show the icons when the API call returns $("#all-queries").DataTable().draw(); }); } // Fetch database info (earliest timestamp, sizes, ...) from the API and // initialize related globals. function getDatabaseInfo() { $.getJSON(document.body.dataset.apiurl + "/info/database", data => { // earliest_timestamp is provided in seconds since epoch // We have two sources: earliest_timestamp_disk (on-disk) and earliest_timestamp (in-memory) // Use whichever is smallest and non-zero const diskTimestamp = Number(data.earliest_timestamp_disk); const memoryTimestamp = Number(data.earliest_timestamp); // Filter out zero/invalid timestamps const validTimestamps = [diskTimestamp, memoryTimestamp].filter(ts => ts > 0); // Use the smallest valid timestamp, or null if none exist beginningOfTime = validTimestamps.length > 0 ? Math.min(...validTimestamps) : null; // Round down to nearest 5-minute segment (300 seconds) if valid if (beginningOfTime !== null) { beginningOfTime = Math.floor(beginningOfTime / 300) * 300; } // If from/until were not provided via GET, default them // Only use defaults if beginningOfTime is valid if (beginningOfTime !== null) { from ??= beginningOfTime; until ??= endOfTime; } initDateRangePicker(); }); } function initDateRangePicker() { // If there's no valid data in the database, disable the datepicker if (beginningOfTime === null || endOfTime === null) { $("#querytime").prop("disabled", true); $("#querytime").addClass("disabled"); $("#querytime-note").text("ℹ️ No data in the database"); return; } const minDateMoment = moment.unix(beginningOfTime); const maxDateMoment = moment.unix(endOfTime); const earliestDateStr = minDateMoment.format(dateformat); $("#querytime-note").text(`Earliest date: ${earliestDateStr}`); $("#querytime").daterangepicker( { timePicker: true, timePickerIncrement: 5, timePicker24Hour: true, locale: { format: dateformat }, startDate: moment(from * 1000), // convert to milliseconds since epoch endDate: moment(until * 1000), // convert to milliseconds since epoch ranges: { "Last 10 Minutes": [moment().subtract(10, "minutes"), moment()], "Last Hour": [moment().subtract(1, "hours"), moment()], Today: [moment().startOf("day"), maxDateMoment], Yesterday: [ moment().subtract(1, "days").startOf("day"), moment().subtract(1, "days").endOf("day"), ], "Last 7 Days": [moment().subtract(6, "days"), maxDateMoment], "Last 30 Days": [moment().subtract(29, "days"), maxDateMoment], "This Month": [moment().startOf("month"), maxDateMoment], "Last Month": [ moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month"), ], "This Year": [moment().startOf("year"), maxDateMoment], "All Time": [minDateMoment, maxDateMoment], }, // Don't allow selecting dates outside the database range minDate: minDateMoment, maxDate: maxDateMoment, opens: "center", showDropdowns: true, autoUpdateInput: true, }, (startt, endt) => { // Update global variables // Convert milliseconds (JS) to seconds (API) from = moment(startt).utc().valueOf() / 1000; until = moment(endt).utc().valueOf() / 1000; } ); } function handleAjaxError(xhr, textStatus) { if (textStatus === "timeout") { alert("The server took too long to send the data."); } else { alert("An unknown error occurred while loading the data.\n" + xhr.responseText); } $("#all-queries_processing").hide(); table.clear(); table.draw(); } function parseQueryStatus(data) { // Parse query status let fieldtext; let buttontext; let icon = null; let colorClass = false; let blocked = false; let isCNAME = false; switch (data.status) { case "GRAVITY": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (gravity)"; buttontext = ''; blocked = true; break; case "FORWARDED": colorClass = "text-green"; icon = "fa-solid fa-cloud-download-alt"; fieldtext = (data.reply.type !== "UNKNOWN" ? "Forwarded, reply from " : "Forwarded to ") + data.upstream; buttontext = ''; break; case "CACHE": colorClass = "text-green"; icon = "fa-solid fa-database"; fieldtext = "Served from cache"; buttontext = ''; break; case "REGEX": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (regex)"; buttontext = ''; blocked = true; break; case "DENYLIST": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (exact)"; buttontext = ''; blocked = true; break; case "EXTERNAL_BLOCKED_IP": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (external, IP)"; buttontext = ""; blocked = true; break; case "EXTERNAL_BLOCKED_NULL": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (external, NULL)"; buttontext = ""; blocked = true; break; case "EXTERNAL_BLOCKED_NXRA": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (external, NXRA)"; buttontext = ""; blocked = true; break; case "QUERY_EXTERNAL_BLOCKED_EDE15": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (external, EDE15)"; buttontext = ""; blocked = true; break; case "GRAVITY_CNAME": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (gravity, CNAME)"; buttontext = ''; isCNAME = true; blocked = true; break; case "REGEX_CNAME": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (regex denied, CNAME)"; buttontext = ''; isCNAME = true; blocked = true; break; case "DENYLIST_CNAME": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = "Blocked (exact denied, CNAME)"; buttontext = ''; isCNAME = true; blocked = true; break; case "RETRIED": colorClass = "text-green"; icon = "fa-solid fa-redo"; // fa-repeat fieldtext = "Retried"; buttontext = ""; break; case "RETRIED_DNSSEC": colorClass = "text-green"; icon = "fa-solid fa-redo"; // fa-repeat fieldtext = "Retried (ignored)"; buttontext = ""; break; case "IN_PROGRESS": colorClass = "text-green"; icon = "fa-solid fa-hourglass-half"; fieldtext = "Already forwarded, awaiting reply"; buttontext = ''; break; case "CACHE_STALE": colorClass = "text-green"; icon = "fa-solid fa-infinity"; fieldtext = "Served by cache optimizer"; buttontext = ''; break; case "SPECIAL_DOMAIN": colorClass = "text-red"; icon = "fa-solid fa-ban"; fieldtext = data.status; buttontext = ""; blocked = true; break; default: colorClass = "text-orange"; icon = "fa-solid fa-question"; fieldtext = data.status; buttontext = ""; } const matchText = colorClass === "text-green" ? "allowed" : colorClass === "text-red" ? "blocked" : "matched"; return { fieldtext, buttontext, colorClass, icon, isCNAME, matchText, blocked, }; } function formatReplyTime(replyTime, type) { if (type === "display") { // Units: // - seconds if replytime >= 1 second // - milliseconds if reply time >= 100 µs // - microseconds otherwise return replyTime < 1e-4 ? (1e6 * replyTime).toFixed(1) + " µs" : replyTime < 1 ? (1e3 * replyTime).toFixed(1) + " ms" : replyTime.toFixed(1) + " s"; } // else: return the number itself (for sorting and searching) return replyTime; } // Parse DNSSEC status function parseDNSSEC(data) { let icon = ""; // Icon to display let color = ""; // Class to apply to text let text = data.dnssec; // Text to display switch (text) { case "SECURE": icon = "fa-solid fa-lock"; color = "text-green"; break; case "INSECURE": icon = "fa-solid fa-lock-open"; color = "text-orange"; break; case "BOGUS": icon = "fa-solid fa-exclamation-triangle"; color = "text-red"; break; case "ABANDONED": icon = "fa-solid fa-exclamation-triangle"; color = "text-red"; break; default: // No DNSSEC or UNKNOWN text = "N/A"; color = ""; icon = ""; } return { text, icon, color }; } function formatInfo(data) { // Parse Query Status const dnssec = parseDNSSEC(data); const queryStatus = parseQueryStatus(data); const divStart = '
'; let statusInfo = ""; if (queryStatus.colorClass !== false) { statusInfo = divStart + "Query Status:  " + '' + queryStatus.fieldtext + "
"; } let listInfo = ""; if (data.list_id !== null && data.list_id !== -1) { // Some list matched - add link to search page const searchLink = data.domain !== "hidden" ? `, search lists` : ""; listInfo = `${divStart}Query was ${queryStatus.matchText}${searchLink}`; } let cnameInfo = ""; if (queryStatus.isCNAME) { cnameInfo = divStart + "Query was blocked during CNAME inspection of  " + data.cname + ""; } // Show TTL if applicable let ttlInfo = ""; if (data.ttl > 0) { ttlInfo = divStart + "Time-to-live (TTL):  " + moment.duration(data.ttl, "s").humanize() + " (" + data.ttl + "s)"; } // Show client information, show hostname only if available const ipInfo = data.client.name !== null && data.client.name.length > 0 ? utils.escapeHtml(data.client.name) + " (" + data.client.ip + ")" : data.client.ip; const clientInfo = divStart + "Client:  " + ipInfo + ""; // Show DNSSEC status if applicable let dnssecInfo = ""; if (dnssec.color !== "") { dnssecInfo = divStart + 'DNSSEC status:  ' + dnssec.text + ""; } // Show long-term database information if applicable let dbInfo = ""; if (data.dbid !== false) { dbInfo = divStart + "Database ID:  " + data.id + ""; } // Always show reply info, add reply delay if applicable let replyInfo = ""; replyInfo = data.reply.type !== "UNKNOWN" ? divStart + "Reply:  " + data.reply.type + "" : divStart + "Reply:  No reply received"; // Show extended DNS error if applicable let edeInfo = ""; if (data.ede !== null && data.ede.text !== null) { edeInfo = divStart + "Extended DNS error:  "; } // Compile extra info for displaying return ( '
' + divStart + "Query received on:  " + moment.unix(data.time).format("Y-MM-DD HH:mm:ss.SSS z") + "
" + clientInfo + dnssecInfo + edeInfo + statusInfo + cnameInfo + listInfo + ttlInfo + replyInfo + dbInfo + "" ); } function addSelectSuggestion(name, dict, data) { const obj = $("#" + name + "_filter"); let value = ""; obj.empty(); // In order for the placeholder value to appear, we have to have a blank //