Rewrite web interface to allow interaction with database-based lists

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
DL6ER
2019-04-25 15:02:39 +02:00
parent 70c4bf4265
commit 506644b671
6 changed files with 214 additions and 169 deletions

View File

@@ -48,7 +48,7 @@ function resolveHostname($clientip, $printIP)
return $clientname;
}
// Get posible non-standard location of FTL's database
// Get possible non-standard location of FTL's database
$FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf");
if(isset($FTLsettings["DBFILE"]))
{

View File

@@ -12,6 +12,96 @@ var token = $("#token").html();
var listType = $("#list-type").html();
var fullName = listType === "white" ? "Whitelist" : "Blacklist";
function refresh(fade) {
var listw;
var list = $("#list");
if(listType === "black")
{
listw = $("#list-regex");
}
if(fade) {
list.fadeOut(100);
if(listw)
{
listw.fadeOut(100);
}
}
$.ajax({
url: "scripts/pi-hole/php/get.php",
method: "get",
data: {"list":listType},
success: function(response) {
list.html("");
if(listw)
{
listw.html("");
}
if((listType === "black" &&
response.blacklist.length === 0 &&
response.regex.length === 0) ||
(listType === "white" &&
response.whitelist.length === 0))
{
$("h3").hide();
list.html("<div class=\"alert alert-info\" role=\"alert\">Your " + fullName + " is empty!</div>");
}
else
{
$("h3").show();
if(listType === "white")
{
data = response.whitelist.sort();
data2 = []; // No regex data, use empty array
}
else if(listType === "black")
{
data = response.blacklist.sort();
data2 = response.regex.sort();
}
data.forEach(function (entry, index)
{
var used = entry.enabled === "1" ? "used" : "not-used";
// Whitelist entry or Blacklist (exact entry)
list.append(
"<li id=\"" + index + "\" class=\"list-group-item " + used + " clearfix\">" + entry.domain +
"<button class=\"btn btn-danger btn-xs pull-right\" type=\"button\">" +
"<span class=\"glyphicon glyphicon-trash\"></span></button></li>");
// Handle button
$("#list #"+index+"").on("click", "button", function() {
sub(index, entry.domain, "exact");
});
});
// Add regex domains if present in returned list data
data2.forEach(function (entry, index)
{
var used = entry.enabled === "1" ? "used" : "not-used";
// Regex entry
listw.append(
"<li id=\"" + index + "\" class=\"list-group-item " + used + " clearfix\">" + entry.filter +
"<button class=\"btn btn-danger btn-xs pull-right\" type=\"button\">" +
"<span class=\"glyphicon glyphicon-trash\"></span></button></li>");
// Handle button
$("#list-regex #"+index+"").on("click", "button", function() {
sub(index, entry.filter, "regex");
});
});
}
list.fadeIn(100);
if(listw)
{
listw.fadeIn(100);
}
},
error: function(jqXHR, exception) {
$("#alFailure").show();
}
});
}
window.onload = refresh(false);
function sub(index, entry, arg) {
var domain = $("#list #"+index);
var locallistType = listType;
@@ -38,92 +128,6 @@ function sub(index, entry, arg) {
});
}
function refresh(fade) {
var listw;
var list = $("#list");
if(listType === "black")
{
listw = $("#list-regex");
}
if(fade) {
list.fadeOut(100);
if(listw)
{
listw.fadeOut(100);
}
}
$.ajax({
url: "scripts/pi-hole/php/get.php",
method: "get",
data: {"list":listType},
success: function(response) {
list.html("");
if(listw)
{
listw.html("");
}
var data = JSON.parse(response);
if(data.length === 0) {
$("h3").hide();
if(listw)
{
listw.html("<div class=\"alert alert-info\" role=\"alert\">Your " + fullName + " is empty!</div>");
}
else
{
list.html("<div class=\"alert alert-info\" role=\"alert\">Your " + fullName + " is empty!</div>");
}
}
else
{
$("h3").show();
data[0] = data[0].sort();
data[0].forEach(function (entry, index) {
// Whitelist entry or Blacklist (exact entry) are in the zero-th
// array returned by get.php
list.append(
"<li id=\"" + index + "\" class=\"list-group-item clearfix\">" + entry +
"<button class=\"btn btn-danger btn-xs pull-right\" type=\"button\">" +
"<span class=\"glyphicon glyphicon-trash\"></span></button></li>");
// Handle button
$("#list #"+index+"").on("click", "button", function() {
sub(index, entry, "exact");
});
});
// Add regex domains if present in returned list data
if(data.length === 2)
{
data[1] = data[1].sort();
data[1].forEach(function (entry, index) {
// Whitelist entry or Blacklist (exact entry) are in the zero-th
// array returned by get.php
listw.append(
"<li id=\"" + index + "\" class=\"list-group-item clearfix\">" + entry +
"<button class=\"btn btn-danger btn-xs pull-right\" type=\"button\">" +
"<span class=\"glyphicon glyphicon-trash\"></span></button></li>");
// Handle button
$("#list-regex #"+index+"").on("click", "button", function() {
sub(index, entry, "regex");
});
});
}
}
list.fadeIn(100);
if(listw)
{
listw.fadeIn(100);
}
},
error: function(jqXHR, exception) {
$("#alFailure").show();
}
});
}
window.onload = refresh(false);
function add(arg) {
var locallistType = listType;
var domain = $("#domain");
@@ -161,7 +165,7 @@ function add(arg) {
alInfo.delay(8000).fadeOut(2000, function() {
alInfo.hide();
});
} else if (!wild && response.indexOf("] Pi-hole blocking is ") === -1 ||
} else if (!wild && response.indexOf("DONE") === -1 ||
wild && response.length > 1) {
alFailure.show();
err.html(response);

View File

@@ -22,36 +22,36 @@ if($type !== "regex") {
check_domain();
}
// Escape shell metacharacters
$domains = escapeshellcmd($_POST['domain']);
switch($type) {
case "white":
if(!isset($_POST["auditlog"]))
echo shell_exec("sudo pihole -w ${_POST['domain']}");
echo shell_exec("sudo pihole -w --web ".$domains);
else
{
echo shell_exec("sudo pihole -w -n ${_POST['domain']}");
echo shell_exec("sudo pihole -a audit ${_POST['domain']}");
echo shell_exec("sudo pihole -w --web -n ".$domains);
echo shell_exec("sudo pihole -a audit ".$domains);
}
break;
case "black":
if(!isset($_POST["auditlog"]))
echo shell_exec("sudo pihole -b ${_POST['domain']}");
echo shell_exec("sudo pihole -b --web ".$domains);
else
{
echo shell_exec("sudo pihole -b -n ${_POST['domain']}");
echo shell_exec("sudo pihole -a audit ${_POST['domain']}");
echo shell_exec("sudo pihole -b --web -n ".$domains);
echo shell_exec("sudo pihole -a audit ".$domains);
}
break;
case "wild":
// Escape "." so it won't be interpreted as the wildcard character
$domain = str_replace(".","\.",$_POST['domain']);
// Add regex filter for legacy wildcard behavior
add_regex("(^|\.)".$domain."$");
break;
case "regex":
add_regex($_POST['domain']);
echo shell_exec("sudo pihole --regex --web ".$domains);
break;
case "wild":
echo shell_exec("sudo pihole --wild --web ".$domains);
break;
case "audit":
echo exec("sudo pihole -a audit ${_POST['domain']}");
echo exec("sudo pihole -a audit ".$domain);
break;
}

View File

@@ -11,57 +11,100 @@ if(!isset($_GET['list']))
$listtype = $_GET['list'];
$basedir = "/etc/pihole/";
require_once("func.php");
require_once "func.php";
switch ($listtype) {
case "white":
$list = array(getListContent("whitelist.txt"));
break;
case "black":
$exact = getListContent("blacklist.txt");
$regex = getListContent("regex.list");
$list = array($exact, $regex);
break;
default:
die("Invalid list parameter");
break;
// Get possible non-standard location of FTL's database
$FTLsettings = parse_ini_file("/etc/pihole/pihole-FTL.conf");
if(isset($FTLsettings["GRAVITYDB"]))
{
$GRAVITYDB = $FTLsettings["GRAVITYDB"];
}
else
{
$GRAVITYDB = "/etc/pihole/gravity.db";
}
function SQLite3_connect($dbfile, $trytoreconnect)
{
try
{
// connect to database
return new SQLite3($dbfile, SQLITE3_OPEN_READONLY);
}
catch (Exception $exception)
{
// sqlite3 throws an exception when it is unable to connect, try to reconnect after 3 seconds
if($trytoreconnect)
{
sleep(3);
$db = SQLite3_connect($dbfile, false);
}
}
}
function getListContent($listname) {
global $basedir;
$rawList = file_get_contents(checkfile($basedir.$listname));
$list = explode("\n", $rawList);
if(strlen($GRAVITYDB) > 0)
{
$db = SQLite3_connect($GRAVITYDB, true);
// Get rid of empty lines and comments
for($i = sizeof($list)-1; $i >= 0; $i--) {
if(strlen($list[$i]) < 1 || $list[$i][0] === '#')
unset($list[$i]);
}
// Check if we successfully opened the database
if(!$db)
{
die("Error connecting to database");
}
}
else
{
die("No database available");
}
// Re-index list after possible unset() activity
$newlist = array_values($list);
function getTableContent($listname) {
global $db;
$entries = array();
$results = $db->query("SELECT * FROM $listname");
return $newlist;
while($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC))
{
array_push($entries, $res);
}
return array($listname => $entries);
}
function filterArray(&$inArray) {
$outArray = array();
foreach ($inArray as $key=>$value) {
if (is_array($value)) {
$outArray[htmlspecialchars($key)] = filterArray($value);
} else {
$outArray[htmlspecialchars($key)] = htmlspecialchars($value);
}
}
return $outArray;
$outArray = array();
foreach ($inArray as $key => $value)
{
if (is_array($value))
{
$outArray[htmlspecialchars($key)] = filterArray($value);
}
else
{
$outArray[htmlspecialchars($key)] = htmlspecialchars($value);
}
}
return $outArray;
}
switch ($listtype)
{
case "white":
$list = getTableContent("whitelist");
break;
case "black":
$exact = getTableContent("blacklist");
$regex = getTableContent("regex");
$list = array_merge($exact, $regex);
break;
default:
die("Invalid list parameter");
break;
}
// Protect against XSS attacks
$list = filterArray($list);
echo json_encode(array_values($list));
$output = filterArray($list);
// Return results
header('Content-type: application/json');
echo json_encode($output);

View File

@@ -16,43 +16,24 @@ if (empty($api)) {
list_verify($type);
}
// Don't check if the added item is a valid domain for regex expressions. Regex
// filters are validated by FTL on import and skipped if invalid
// Don't check if the added item is a valid domain for regex expressions.
// Regex filters are validated by FTL on import and skipped if invalid
if($type !== "regex") {
check_domain();
}
// Escape shell metacharacters
$domain = escapeshellcmd($_POST['domain']);
switch($type) {
case "white":
exec("sudo pihole -w -q -d ${_POST['domain']}");
exec("sudo pihole -w -q -d ".$domain);
break;
case "black":
exec("sudo pihole -b -q -d ${_POST['domain']}");
exec("sudo pihole -b -q -d ".$domain);
break;
case "regex":
if(($list = file_get_contents($regexfile)) === FALSE)
{
$err = error_get_last()["message"];
echo "Unable to read ${regexfile}<br>Error message: $err";
}
// Remove the regex and any empty lines from the list
$list = explode("\n", $list);
$list = array_diff($list, array($_POST['domain'], ""));
$list = implode("\n", $list);
if(file_put_contents($regexfile, $list."\n") === FALSE)
{
$err = error_get_last()["message"];
echo "Unable to remove regex \"".htmlspecialchars($_POST['domain'])."\" from ${regexfile}<br>Error message: $err";
}
else
{
// Send SIGHUP to pihole-FTL using a frontend command
// to force reloading of the regex domains
// This will also wipe the resolver's cache
echo exec("sudo pihole restartdns reload");
}
exec("sudo pihole --regex -q -d ".$domain);
break;
}

View File

@@ -24,6 +24,23 @@
.skin-blue .list-group-item:hover {
background: #ddd;
}
.skin-blue .not-used {
background: #eee;
}
.skin-blue .not-used:hover {
background: #c5c5c95;
}
.skin-blue .used {
background: #fff;
}
.skin-blue .used:hover {
background: #ddd;
}
@-webkit-keyframes Pulse{
from {color:#630030;-webkit-text-shadow:0 0 2px transparent;}
50% {color:#e33100;-webkit-text-shadow:0 0 5px #e33100;}