= 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 = []; foreach ($entries as $entry) $data[] = [ $entry->domain, $entry->ip ]; return [ "data" => $data ]; } function getCustomDNSEntries() { global $customDNSFile; $entries = []; $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 = []; foreach ($entries as $entry) $data[] = [ $entry->domain, $entry->target ]; return [ "data" => $data ]; } function getCustomCNAMEEntries() { global $customCNAMEFile; $entries = []; 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 [ "success" => true, "message" => $message ]; } else { echo $message."
"; return true; } } function returnError($message = "", $json = true) { $message = htmlentities($message) ; if ($json) { return [ "success" => false, "message" => $message ]; } else { echo $message."
"; return false; } } function getQueryTypeStr($querytype) { $qtypes = ["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]; else 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(["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; } ?>