Skip to content

Commit

Permalink
Merge branch 'devel' of https://github.com/pi-hole/AdminLTE into devel
Browse files Browse the repository at this point in the history
Signed-off-by: Tony Jeffree <[email protected]>
  • Loading branch information
tjeffree committed Sep 14, 2020
2 parents 4a53a8a + 132a9b9 commit b7244ad
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 65 deletions.
25 changes: 17 additions & 8 deletions api_db.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,27 @@ function resolveHostname($clientip, $printIP)

while($results !== false && $res = $results->fetchArray(SQLITE3_ASSOC))
{
$id = $res["id"];
// Empty array for holding the IP addresses
$id = intval($res["id"]);

// Get IP addresses and host names for this device
$res["ip"] = array();
// Get IP addresses for this device
$network_addresses = $db->query("SELECT ip FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC");
while($network_addresses !== false && $ip = $network_addresses->fetchArray(SQLITE3_ASSOC))
array_push($res["ip"],$ip["ip"]);
// UTF-8 encode host name and vendor
$res["name"] = utf8_encode($res["name"]);
$res["name"] = array();
$network_addresses = $db->query("SELECT ip,name FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC");
while($network_addresses !== false && $network_address = $network_addresses->fetchArray(SQLITE3_ASSOC))
{
array_push($res["ip"],$network_address["ip"]);
if($network_address["name"] !== null)
array_push($res["name"],utf8_encode($network_address["name"]));
else
array_push($res["name"],"");
}
$network_addresses->finalize();

// UTF-8 encode vendor
$res["macVendor"] = utf8_encode($res["macVendor"]);
array_push($network, $res);
}
$results->finalize();

$data = array_merge($data, array('network' => $network));
}
Expand Down
17 changes: 14 additions & 3 deletions groups-clients.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,25 @@
<label for="select">Known clients:</label>
<select id="select" class="form-control" placeholder="">
<option disabled selected>Loading...</option>
</select><br>
<input id="ip-custom" type="text" class="form-control" disabled placeholder="Client IP address (IPv4 or IPv6, CIDR subnetting available, optional)" autocomplete="off" spellcheck="false" autocapitalize="none" autocorrect="off">
</select>
</div>
<div class="form-group col-md-6">
<label for="new_comment">Comment:</label>
<input id="new_comment" type="text" class="form-control" placeholder="Client description (optional)">
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>You can select an existing client or add a custom one by typing into the field above and confirming your entry with <kbd>&#x23CE;</kbd>.</p>
<p>Clients may be described either by their IP addresses (IPv4 and IPv6 are supported),
IP subnets (CIDR notation, like <code>192.168.2.0/24</code>),
their MAC addresses (like <code>12:34:56:78:9A:BC</code>),
by their hostnames (like <code>localhost</code>), or by the interface they are connected to (prefaced with a colon, like <code>:eth0</code>).</p>
<p>Note that client recognition by IP addresses (incl. subnet ranges) are prefered over MAC address, host name or interface recognition as
the two latter will only be available after some time.
Furthermore, MAC address recognition only works for devices at most one networking hop away from your Pi-hole.</p>
</div>
</div>
</div>
<div class="box-footer clearfix">
<button type="button" id="btnAdd" class="btn btn-primary pull-right">Add</button>
Expand All @@ -59,7 +70,7 @@
<thead>
<tr>
<th>ID</th>
<th>IP address</th>
<th title="Acceptable values are: IP address, subnet (CIDR notation), MAC address (AA:BB:CC:DD:EE:FF format) or host names.">Client</th>
<th>Comment</th>
<th>Group assignment</th>
<th>Action</th>
Expand Down
2 changes: 1 addition & 1 deletion scripts/pi-hole/js/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function initCPUtemp() {
switch (unit) {
case "K":
temperature += 273.15;
displaytemp.html(temperature.toFixed(1) + "&nbsp;&deg;K");
displaytemp.html(temperature.toFixed(1) + "&nbsp;K");
break;

case "F":
Expand Down
69 changes: 42 additions & 27 deletions scripts/pi-hole/js/groups-clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,35 @@ function reloadClientSuggestions() {
{ action: "get_unconfigured_clients", token: token },
function (data) {
var sel = $("#select");
var customWasSelected = sel.val() === "custom";
sel.empty();

// In order for the placeholder value to appear, we have to have a blank
// <option> as the first option in our <select> control. This is because
// the browser tries to select the first option by default. If our first
// option were non-empty, the browser would display this instead of the
// placeholder.
sel.append($("<option />"));

// Add data obtained from API
for (var key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}

var text = key;
var keyPlain = key;
if (key.startsWith("IP-")) {
// Mock MAC address for address-only devices
keyPlain = key.substring(3);
text = keyPlain;
}

// Append host name if available
if (data[key].length > 0) {
text += " (" + data[key] + ")";
}

sel.append($("<option />").val(key).text(text));
}

if (data.length === 0) {
$("#ip-custom").prop("disabled", false);
}

sel.append($("<option />").val("custom").text("Custom, specified below..."));
if (customWasSelected) {
sel.val("custom");
sel.append($("<option />").val(keyPlain).text(text));
}
},
"json"
Expand All @@ -59,6 +66,11 @@ function getGroups() {

$(function () {
$("#btnAdd").on("click", addClient);
$("select").select2({
tags: true,
placeholder: "Select client...",
allowClear: true
});

reloadClientSuggestions();
utils.setBsSelectDefaults();
Expand Down Expand Up @@ -221,6 +233,7 @@ function initTable() {
return data;
}
});

// Disable autocorrect in the search box
var input = document.querySelector("input[type=search]");
if (input !== null) {
Expand All @@ -238,40 +251,42 @@ function initTable() {
$("#resetButton").addClass("hidden");
}
});

$("#resetButton").on("click", function () {
table.order([[0, "asc"]]).draw();
$("#resetButton").addClass("hidden");
});
}

function addClient() {
var ip = $("#select").val();
var ip = $("#select").val().trim();
var comment = utils.escapeHtml($("#new_comment").val());
if (ip === "custom") {
ip = utils.escapeHtml($("#ip-custom").val().trim());
}

utils.disableAll();
utils.showAlert("info", "", "Adding client...", ip);

if (ip.length === 0) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Please specify a client IP address");
return;
}

// Validate IP address (may contain CIDR details)
var ipv6format = ip.includes(":");

if (!ipv6format && !utils.validateIPv4CIDR(ip)) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Invalid IPv4 address!");
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
return;
}

if (ipv6format && !utils.validateIPv6CIDR(ip)) {
// Validate input, can be:
// - IPv4 address (with and without CIDR)
// - IPv6 address (with and without CIDR)
// - MAC address (in the form AA:BB:CC:DD:EE:FF)
// - host name (arbitrary form, we're only checking against some reserved charaters)
if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) {
// Convert input to upper case (important for MAC addresses)
ip = ip.toUpperCase();
} else if (!utils.validateHostname(ip)) {
utils.enableAll();
utils.showAlert("warning", "", "Warning", "Invalid IPv6 address!");
utils.showAlert(
"warning",
"",
"Warning",
"Input is neither a valid IP or MAC address nor a valid host name!"
);
return;
}

Expand Down
11 changes: 8 additions & 3 deletions scripts/pi-hole/js/ip-address-sorting.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */

// This code has been adapted from
// This code has been taken from
// https://datatables.net/plug-ins/sorting/ip-address
// and was modified by the Pi-hole team to support
// CIDR notation and be more robust against invalid
// input data (like empty IP addresses)
$.extend($.fn.dataTableExt.oSort, {
"ip-address-pre": function (a) {
if (!a) {
return 0;
// Skip empty fields (IP address might have expired or
// reassigned to a differenct device)
if (!a || a.length === 0) {
return Infinity;
}

var i, item;
Expand Down
39 changes: 37 additions & 2 deletions scripts/pi-hole/js/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ $(function () {
tableApi = $("#network-entries").DataTable({
rowCallback: function (row, data) {
var color;
var index;
var maxiter;
var iconClasses;
var lastQuery = parseInt(data.lastQuery, 10);
var diff = getTimestamp() - lastQuery;
Expand Down Expand Up @@ -109,14 +111,47 @@ $(function () {
// Set hostname to "unknown" if not available
if (!data.name || data.name.length === 0) {
$("td:eq(3)", row).html("<em>unknown</em>");
} else {
var names = [];
var name = "";
maxiter = Math.min(data.name.length, MAXIPDISPLAY);
index = 0;
for (index = 0; index < maxiter; index++) {
name = data.name[index];
if (name.length === 0) continue;
names.push('<a href="queries.php?client=' + name + '">' + name + "</a>");
}

if (data.name.length > MAXIPDISPLAY) {
// We hit the maximum above, add "..." to symbolize we would
// have more to show here
names.push("...");
}

maxiter = Math.min(data.ip.length, data.name.length);
var allnames = [];
for (index = 0; index < maxiter; index++) {
name = data.name[index];
if (name.length > 0) {
allnames.push(name + " (" + data.ip[index] + ")");
} else {
allnames.push("No host name for " + data.ip[index] + " known");
}
}

$("td:eq(3)", row).html(names.join("<br>"));
$("td:eq(3)", row).hover(function () {
this.title = allnames.join("\n");
});
}

// Set number of queries to localized string (add thousand separators)
$("td:eq(6)", row).html(data.numQueries.toLocaleString());

var ips = [];
var maxiter = Math.min(data.ip.length, MAXIPDISPLAY);
for (var index = 0; index < maxiter; index++) {
maxiter = Math.min(data.ip.length, MAXIPDISPLAY);
index = 0;
for (index = 0; index < maxiter; index++) {
var ip = data.ip[index];
ips.push('<a href="queries.php?client=' + ip + '">' + ip + "</a>");
}
Expand Down
14 changes: 13 additions & 1 deletion scripts/pi-hole/js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ function validateIPv6CIDR(ip) {
return ipv6validator.test(ip);
}

function validateMAC(mac) {
var macvalidator = new RegExp(/^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/);
return macvalidator.test(mac);
}

function validateHostname(name) {
var namevalidator = new RegExp(/[^<>;"]/);
return namevalidator.test(name);
}

// set bootstrap-select defaults
function setBsSelectDefaults() {
var bsSelectDefaults = $.fn.selectpicker.Constructor.DEFAULTS;
Expand Down Expand Up @@ -234,6 +244,8 @@ window.utils = (function () {
setBsSelectDefaults: setBsSelectDefaults,
stateSaveCallback: stateSaveCallback,
stateLoadCallback: stateLoadCallback,
getGraphType: getGraphType
getGraphType: getGraphType,
validateMAC: validateMAC,
validateHostname: validateHostname
};
})();
50 changes: 47 additions & 3 deletions scripts/pi-hole/php/groups.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function JSON_error($message = null)
throw new Exception('Error while querying gravity\'s client_by_group table: ' . $db->lastErrorMsg());
}

$stmt = $FTLdb->prepare('SELECT name FROM network WHERE id = (SELECT network_id FROM network_addresses WHERE ip = :ip);');
$stmt = $FTLdb->prepare('SELECT name FROM network_addresses WHERE ip = :ip;');
if (!$stmt) {
throw new Exception('Error while preparing network table statement: ' . $db->lastErrorMsg());
}
Expand All @@ -206,6 +206,7 @@ function JSON_error($message = null)
while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
array_push($groups, $gres['group_id']);
}
$group_query->finalize();
$res['groups'] = $groups;
array_push($data, $res);
}
Expand All @@ -221,15 +222,55 @@ function JSON_error($message = null)
$QUERYDB = getQueriesDBFilename();
$FTLdb = SQLite3_connect($QUERYDB);

$query = $FTLdb->query('SELECT DISTINCT ip,network.name FROM network_addresses AS name LEFT JOIN network ON network.id = network_id ORDER BY ip ASC;');
$query = $FTLdb->query('SELECT DISTINCT id,hwaddr,macVendor FROM network ORDER BY firstSeen DESC;');
if (!$query) {
throw new Exception('Error while querying FTL\'s database: ' . $db->lastErrorMsg());
}

// Loop over results
$ips = array();
while ($res = $query->fetchArray(SQLITE3_ASSOC)) {
$ips[$res['ip']] = $res['name'] !== null ? $res['name'] : '';
$id = intval($res["id"]);

// Get possibly associated IP addresses and hostnames for this client
$query_ips = $FTLdb->query("SELECT ip,name FROM network_addresses WHERE network_id = $id ORDER BY lastSeen DESC;");
$addresses = [];
$names = [];
while ($res_ips = $query_ips->fetchArray(SQLITE3_ASSOC)) {
array_push($addresses, utf8_encode($res_ips["ip"]));
if($res_ips["name"] !== null)
array_push($names,utf8_encode($res_ips["name"]));
}
$query_ips->finalize();

// Prepare extra information
$extrainfo = "";
// Add list of associated host names to info string (if available)
if(count($names) === 1)
$extrainfo .= "hostname: ".$names[0];
else if(count($names) > 0)
$extrainfo .= "hostnames: ".implode(", ", $names);

// Add device vendor to info string (if available)
if (strlen($res["macVendor"]) > 0) {
if (count($names) > 0)
$extrainfo .= "; ";
$extrainfo .= "vendor: ".htmlspecialchars($res["macVendor"]);
}

// Add list of associated host names to info string (if available and if this is not a mock device)
if (stripos($res["hwaddr"], "ip-") === FALSE) {

if ((count($names) > 0 || strlen($res["macVendor"]) > 0) && count($addresses) > 0)
$extrainfo .= "; ";

if(count($addresses) === 1)
$extrainfo .= "address: ".$addresses[0];
else if(count($addresses) > 0)
$extrainfo .= "addresses: ".implode(", ", $addresses);
}

$ips[strtoupper($res['hwaddr'])] = $extrainfo;
}
$FTLdb->close();

Expand All @@ -243,6 +284,9 @@ function JSON_error($message = null)
if (isset($ips[$res['ip']])) {
unset($ips[$res['ip']]);
}
if (isset($ips["IP-".$res['ip']])) {
unset($ips["IP-".$res['ip']]);
}
}

header('Content-type: application/json');
Expand Down
Loading

0 comments on commit b7244ad

Please sign in to comment.