mirror of
https://github.com/pi-hole/web.git
synced 2024-12-06 19:36:21 +01:00
Merge pull request #1285 from pi-hole/new/mac_clients
Allow defining clients by their MAC address
This commit is contained in:
+17
-8
@@ -62,18 +62,27 @@ if(isset($_GET["network"]) && $auth)
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
+14
-3
@@ -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>⏎</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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
@@ -59,6 +66,11 @@ function getGroups() {
|
||||
|
||||
$(function () {
|
||||
$("#btnAdd").on("click", addClient);
|
||||
$("select").select2({
|
||||
tags: true,
|
||||
placeholder: "Select client...",
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
reloadClientSuggestions();
|
||||
utils.setBsSelectDefaults();
|
||||
@@ -221,6 +233,7 @@ function initTable() {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
// Disable autocorrect in the search box
|
||||
var input = document.querySelector("input[type=search]");
|
||||
if (input !== null) {
|
||||
@@ -238,6 +251,7 @@ function initTable() {
|
||||
$("#resetButton").addClass("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
$("#resetButton").on("click", function () {
|
||||
table.order([[0, "asc"]]).draw();
|
||||
$("#resetButton").addClass("hidden");
|
||||
@@ -245,33 +259,34 @@ function initTable() {
|
||||
}
|
||||
|
||||
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");
|
||||
utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate IP address (may contain CIDR details)
|
||||
var ipv6format = ip.includes(":");
|
||||
|
||||
if (!ipv6format && !utils.validateIPv4CIDR(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 IPv4 address!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ipv6format && !utils.validateIPv6CIDR(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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>");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -234,6 +244,8 @@ window.utils = (function () {
|
||||
setBsSelectDefaults: setBsSelectDefaults,
|
||||
stateSaveCallback: stateSaveCallback,
|
||||
stateLoadCallback: stateLoadCallback,
|
||||
getGraphType: getGraphType
|
||||
getGraphType: getGraphType,
|
||||
validateMAC: validateMAC,
|
||||
validateHostname: validateHostname
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -184,7 +184,7 @@ if ($_POST['action'] == 'get_groups') {
|
||||
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());
|
||||
}
|
||||
@@ -206,6 +206,7 @@ if ($_POST['action'] == 'get_groups') {
|
||||
while ($gres = $group_query->fetchArray(SQLITE3_ASSOC)) {
|
||||
array_push($groups, $gres['group_id']);
|
||||
}
|
||||
$group_query->finalize();
|
||||
$res['groups'] = $groups;
|
||||
array_push($data, $res);
|
||||
}
|
||||
@@ -221,7 +222,7 @@ if ($_POST['action'] == 'get_groups') {
|
||||
$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());
|
||||
}
|
||||
@@ -229,7 +230,47 @@ if ($_POST['action'] == 'get_groups') {
|
||||
// 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();
|
||||
|
||||
@@ -243,6 +284,9 @@ if ($_POST['action'] == 'get_groups') {
|
||||
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');
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
<link rel="stylesheet" href="style/vendor/datatables.min.css">
|
||||
<link rel="stylesheet" href="style/vendor/daterangepicker.min.css">
|
||||
<link rel="stylesheet" href="style/vendor/AdminLTE.min.css">
|
||||
<link rel="stylesheet" href="style/vendor/select2.min.css">
|
||||
|
||||
<?php if (in_array($scriptname, array("groups.php", "groups-adlists.php", "groups-clients.php", "groups-domains.php"))){ ?>
|
||||
<link rel="stylesheet" href="style/vendor/animate.min.css">
|
||||
@@ -205,6 +206,7 @@
|
||||
<script src="style/vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="scripts/vendor/adminlte.min.js"></script>
|
||||
<script src="scripts/vendor/bootstrap-notify.min.js"></script>
|
||||
<script src="scripts/vendor/select2.min.js"></script>
|
||||
<script src="scripts/vendor/datatables.min.js"></script>
|
||||
<script src="scripts/vendor/moment.min.js"></script>
|
||||
<script src="scripts/vendor/Chart.min.js"></script>
|
||||
|
||||
@@ -738,7 +738,11 @@ function addStaticDHCPLease($mac, $ip, $hostname) {
|
||||
// Flush network table
|
||||
case "flusharp":
|
||||
$output = pihole_execute("arpflush quiet");
|
||||
$error = implode("<br>", $output);
|
||||
$error = "";
|
||||
if(is_array($output))
|
||||
{
|
||||
$error = implode("<br>", $output);
|
||||
}
|
||||
if(strlen($error) == 0)
|
||||
{
|
||||
$success .= "The network table has been flushed";
|
||||
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -300,3 +300,7 @@
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.select2-container--default .select2-results > .select2-results__options {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user