= 0; --$i) { $ret |= ord($res[$i]); } return !$ret; } } /** * More safely execute a command with pihole shell script. * * For example, * * pihole_execute("-h"); * * would execute command * * sudo pihole -h * * and returns output of that command as a string. * * @param $argument_string String of arguments to run pihole with */ function pihole_execute($argument_string) { $escaped = escapeshellcmd($argument_string); $output = null; $return_status = -1; $command = 'sudo pihole '.$escaped; exec($command, $output, $return_status); if ($return_status !== 0) { trigger_error("Executing {$command} failed.", E_USER_WARNING); } return $output; } // Custom DNS $customDNSFile = '/etc/pihole/custom.list'; function echoCustomDNSEntries() { $entries = getCustomDNSEntries(); $data = array(); foreach ($entries as $entry) { $data[] = array($entry->domain, $entry->ip); } return array('data' => $data); } function getCustomDNSEntries() { global $customDNSFile; $entries = array(); $handle = fopen($customDNSFile, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $line = str_replace("\r", '', $line); $line = str_replace("\n", '', $line); $explodedLine = explode(' ', $line); if (count($explodedLine) != 2) { continue; } $data = new \stdClass(); $data->ip = $explodedLine[0]; $data->domain = $explodedLine[1]; $entries[] = $data; } fclose($handle); } return $entries; } function addCustomDNSEntry($ip = '', $domain = '', $reload = '', $json = true) { try { if (isset($_REQUEST['ip'])) { $ip = trim($_REQUEST['ip']); } if (isset($_REQUEST['domain'])) { $domain = trim($_REQUEST['domain']); } if (isset($_REQUEST['reload'])) { $reload = $_REQUEST['reload']; } if (empty($ip)) { return returnError('IP must be set', $json); } $ipType = get_ip_type($ip); if (!$ipType) { return returnError('IP must be valid', $json); } if (empty($domain)) { return returnError('Domain must be set', $json); } if (!validDomain($domain)) { return returnError('Domain must be valid', $json); } // Only check for duplicates if adding new records from the web UI (not through Teleporter) if (isset($_REQUEST['ip']) || isset($_REQUEST['domain'])) { $existingEntries = getCustomDNSEntries(); foreach ($existingEntries as $entry) { if ($entry->domain == $domain && get_ip_type($entry->ip) == $ipType) { return returnError('This domain already has a custom DNS entry for an IPv'.$ipType, $json); } } } // Add record pihole_execute('-a addcustomdns '.$ip.' '.$domain.' '.$reload); return returnSuccess('', $json); } catch (\Exception $ex) { return returnError($ex->getMessage(), $json); } } function deleteCustomDNSEntry() { try { $ip = !empty($_REQUEST['ip']) ? $_REQUEST['ip'] : ''; $domain = !empty($_REQUEST['domain']) ? $_REQUEST['domain'] : ''; if (empty($ip)) { return returnError('IP must be set'); } if (empty($domain)) { return returnError('Domain must be set'); } $existingEntries = getCustomDNSEntries(); $found = false; foreach ($existingEntries as $entry) { if ($entry->domain == $domain) { if ($entry->ip == $ip) { $found = true; break; } } } if (!$found) { return returnError('This domain/ip association does not exist'); } pihole_execute('-a removecustomdns '.$ip.' '.$domain); return returnSuccess(); } catch (\Exception $ex) { return returnError($ex->getMessage()); } } function deleteAllCustomDNSEntries($reload = '') { try { if (isset($_REQUEST['reload'])) { $reload = $_REQUEST['reload']; } $existingEntries = getCustomDNSEntries(); // passing false to pihole_execute stops pihole from reloading after each entry has been deleted foreach ($existingEntries as $entry) { pihole_execute('-a removecustomdns '.$entry->ip.' '.$entry->domain.' '.$reload); } } catch (\Exception $ex) { return returnError($ex->getMessage()); } return returnSuccess(); } // CNAME $customCNAMEFile = '/etc/dnsmasq.d/05-pihole-custom-cname.conf'; function echoCustomCNAMEEntries() { $entries = getCustomCNAMEEntries(); $data = array(); foreach ($entries as $entry) { $data[] = array($entry->domain, $entry->target); } return array('data' => $data); } function getCustomCNAMEEntries() { global $customCNAMEFile; $entries = array(); if (!file_exists($customCNAMEFile)) { return $entries; } $handle = fopen($customCNAMEFile, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { $line = str_replace('cname=', '', $line); $line = str_replace("\r", '', $line); $line = str_replace("\n", '', $line); $explodedLine = explode(',', $line); if (count($explodedLine) <= 1) { continue; } $data = new \stdClass(); $data->domains = array_slice($explodedLine, 0, -1); $data->domain = implode(',', $data->domains); $data->target = $explodedLine[count($explodedLine) - 1]; $entries[] = $data; } fclose($handle); } return $entries; } function addCustomCNAMEEntry($domain = '', $target = '', $reload = '', $json = true) { try { if (isset($_REQUEST['domain'])) { $domain = $_REQUEST['domain']; } if (isset($_REQUEST['target'])) { $target = trim($_REQUEST['target']); } if (isset($_REQUEST['reload'])) { $reload = $_REQUEST['reload']; } if (empty($domain)) { return returnError('Domain must be set', $json); } if (empty($target)) { return returnError('Target must be set', $json); } if (!validDomain($target)) { return returnError('Target must be valid', $json); } // Check if each submitted domain is valid $domains = array_map('trim', explode(',', $domain)); foreach ($domains as $d) { if (!validDomain($d)) { return returnError("Domain '{$d}' is not valid", $json); } } $existingEntries = getCustomCNAMEEntries(); // Check if a record for one of the domains already exists foreach ($existingEntries as $entry) { foreach ($domains as $d) { if (in_array($d, $entry->domains)) { return returnError("There is already a CNAME record for '{$d}'", $json); } } } pihole_execute('-a addcustomcname '.$domain.' '.$target.' '.$reload); return returnSuccess('', $json); } catch (\Exception $ex) { return returnError($ex->getMessage(), $json); } } function deleteCustomCNAMEEntry() { try { $target = !empty($_REQUEST['target']) ? $_REQUEST['target'] : ''; $domain = !empty($_REQUEST['domain']) ? $_REQUEST['domain'] : ''; if (empty($target)) { return returnError('Target must be set'); } if (empty($domain)) { return returnError('Domain must be set'); } $existingEntries = getCustomCNAMEEntries(); $found = false; foreach ($existingEntries as $entry) { if ($entry->domain == $domain) { if ($entry->target == $target) { $found = true; break; } } } if (!$found) { return returnError('This domain/ip association does not exist'); } pihole_execute('-a removecustomcname '.$domain.' '.$target); return returnSuccess(); } catch (\Exception $ex) { return returnError($ex->getMessage()); } } function deleteAllCustomCNAMEEntries($reload = '') { try { if (isset($_REQUEST['reload'])) { $reload = $_REQUEST['reload']; } $existingEntries = getCustomCNAMEEntries(); // passing false to pihole_execute stops pihole from reloading after each entry has been deleted foreach ($existingEntries as $entry) { pihole_execute('-a removecustomcname '.$entry->domain.' '.$entry->target.' '.$reload); } } catch (\Exception $ex) { return returnError($ex->getMessage()); } return returnSuccess(); } function returnSuccess($message = '', $json = true) { if ($json) { return array('success' => true, 'message' => $message); } echo $message.'
'; return true; } function returnError($message = '', $json = true) { $message = htmlentities($message); if ($json) { return array('success' => false, 'message' => $message); } echo $message.'
'; return false; } function getQueryTypeStr($querytype) { $qtypes = array('A', 'AAAA', 'ANY', 'SRV', 'SOA', 'PTR', 'TXT', 'NAPTR', 'MX', 'DS', 'RRSIG', 'DNSKEY', 'NS', 'OTHER', 'SVCB', 'HTTPS'); $qtype = intval($querytype); if ($qtype > 0 && $qtype <= count($qtypes)) { return $qtypes[$qtype - 1]; } return 'TYPE'.($qtype - 100); } // Functions to return Alert messages (success, error, warning) in JSON format. // Used in multiple pages. // Return Success message in JSON format function JSON_success($message = null) { header('Content-type: application/json'); echo json_encode(array('success' => true, 'message' => $message)); } // Return Error message in JSON format function JSON_error($message = null) { header('Content-type: application/json'); $response = array('success' => false, 'message' => $message); if (isset($_POST['action'])) { array_push($response, array('action' => $_POST['action'])); } echo json_encode($response); } // Return Warning message in JSON format. // - sends "success", because it wasn't a failure. // - sends "warning" to use the correct alert type. function JSON_warning($message = null) { header('Content-type: application/json'); echo json_encode(array( 'success' => true, 'warning' => true, 'message' => $message, )); } // Returns an integer representing pihole blocking status function piholeStatus() { // Retrieve DNS Port calling FTL API directly $port = callFTLAPI('dns-port'); // Retrieve FTL status $FTLstats = callFTLAPI('stats'); if (array_key_exists('FTLnotrunning', $port) || array_key_exists('FTLnotrunning', $FTLstats)) { // FTL is not running $ret = -1; } elseif (in_array('status enabled', $FTLstats)) { // FTL is enabled if (intval($port[0]) <= 0) { // Port=0; FTL is not listening $ret = -1; } else { // FTL is running on this port $ret = intval($port[0]); } } elseif (in_array('status disabled', $FTLstats)) { // FTL is disabled $ret = 0; } else { // Unknown (unexpected) response $ret = -2; } return $ret; } // Returns the default gateway address and interface function getGateway() { $gateway = callFTLAPI('gateway'); if (array_key_exists('FTLnotrunning', $gateway)) { $ret = array('ip' => -1); } else { $ret = array_combine(array('ip', 'iface'), explode(' ', $gateway[0])); } return $ret; } // Try to convert possible IDNA domain to Unicode function convertIDNAToUnicode($unicode) { if (extension_loaded('intl')) { // we try the UTS #46 standard first // as this is the new default, see https://sourceforge.net/p/icu/mailman/message/32980778/ // We know that this fails for some Google domains violating the standard // see https://github.com/pi-hole/AdminLTE/issues/1223 if (defined('INTL_IDNA_VARIANT_UTS46')) { // We have to use the option IDNA_NONTRANSITIONAL_TO_ASCII here // to ensure sparkasse-gießen.de is not converted into // sparkass-giessen.de but into xn--sparkasse-gieen-2ib.de // as mandated by the UTS #46 standard $unicode = idn_to_utf8($unicode, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); } elseif (defined('INTL_IDNA_VARIANT_2003')) { // If conversion failed, try with the (deprecated!) IDNA 2003 variant // We have to check for its existence as support of this variant is // scheduled for removal with PHP 8.0 // see https://wiki.php.net/rfc/deprecate-and-remove-intl_idna_variant_2003 $unicode = idn_to_utf8($unicode, IDNA_DEFAULT, INTL_IDNA_VARIANT_2003); } } return $unicode; } // Convert a given (unicode) domain to IDNA ASCII function convertUnicodeToIDNA($IDNA) { if (extension_loaded('intl')) { // Be prepared that this may fail and see our comments about convertIDNAToUnicode() if (defined('INTL_IDNA_VARIANT_UTS46')) { $IDNA = idn_to_ascii($IDNA, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46); } elseif (defined('INTL_IDNA_VARIANT_2003')) { $IDNA = idn_to_ascii($IDNA, IDNA_DEFAULT, INTL_IDNA_VARIANT_2003); } } return $IDNA; } // Return PID of FTL (used in settings.php) function pidofFTL() { return shell_exec('pidof pihole-FTL'); } // Get FTL process information (used in settings.php) function get_FTL_data($FTLpid, $arg) { return trim(exec('ps -p '.$FTLpid.' -o '.$arg)); } // Convert seconds into readable time (used in settings.php) function convertseconds($argument) { $seconds = round($argument); if ($seconds < 60) { return sprintf('%ds', $seconds); } if ($seconds < 3600) { return sprintf('%dm %ds', $seconds / 60, $seconds % 60); } if ($seconds < 86400) { return sprintf('%dh %dm %ds', $seconds / 3600 % 24, $seconds / 60 % 60, $seconds % 60); } return sprintf('%dd %dh %dm %ds', $seconds / 86400, $seconds / 3600 % 24, $seconds / 60 % 60, $seconds % 60); }