480 lines
16 KiB
PHP
480 lines
16 KiB
PHP
<?php
|
|
require_auth();
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$action = (string)($_GET['action'] ?? '');
|
|
$instances = module_fn('pihole', 'instances');
|
|
$listsPrimaryOnly = module_fn('pihole', 'lists_primary_only');
|
|
|
|
$body = file_get_contents('php://input');
|
|
$payload = [];
|
|
if ($body !== false && $body !== '') {
|
|
$decoded = json_decode($body, true);
|
|
if (is_array($decoded)) {
|
|
$payload = $decoded;
|
|
}
|
|
}
|
|
|
|
$respond = function (array $data, int $status = 200): void {
|
|
http_response_code($status);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
|
exit;
|
|
};
|
|
|
|
$normalizeApiPath = function (string $baseUrl, string $apiPath): string {
|
|
$base = rtrim($baseUrl, '/');
|
|
$path = $apiPath;
|
|
if ($path === '') {
|
|
$path = '/admin/api.php';
|
|
}
|
|
if ($path[0] !== '/') {
|
|
$path = '/' . $path;
|
|
}
|
|
return $base . $path;
|
|
};
|
|
|
|
$apiRequest = function (array $instance, array $params) use ($normalizeApiPath): array {
|
|
if (!empty($instance['token']) && !isset($params['auth'])) {
|
|
$params['auth'] = $instance['token'];
|
|
}
|
|
|
|
$url = $normalizeApiPath((string)$instance['url'], (string)$instance['api_path']);
|
|
$qs = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
|
|
$full = $qs !== '' ? $url . '?' . $qs : $url;
|
|
|
|
$timeout = (int)($instance['timeout'] ?? 8);
|
|
if ($timeout <= 0) {
|
|
$timeout = 8;
|
|
}
|
|
$verify = !empty($instance['verify_tls']);
|
|
|
|
$raw = '';
|
|
$httpCode = 0;
|
|
$error = '';
|
|
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init($full);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => $timeout,
|
|
CURLOPT_CONNECTTIMEOUT => $timeout,
|
|
CURLOPT_SSL_VERIFYPEER => $verify,
|
|
CURLOPT_SSL_VERIFYHOST => $verify ? 2 : 0,
|
|
]);
|
|
$raw = (string)curl_exec($ch);
|
|
if ($raw === '' && curl_errno($ch)) {
|
|
$error = curl_error($ch);
|
|
}
|
|
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
} else {
|
|
$ctx = stream_context_create([
|
|
'http' => [
|
|
'method' => 'GET',
|
|
'timeout' => $timeout,
|
|
],
|
|
'ssl' => [
|
|
'verify_peer' => $verify,
|
|
'verify_peer_name' => $verify,
|
|
],
|
|
]);
|
|
$raw = (string)@file_get_contents($full, false, $ctx);
|
|
$httpCode = 200;
|
|
if ($raw === '') {
|
|
$error = 'HTTP request failed';
|
|
}
|
|
}
|
|
|
|
if ($error !== '') {
|
|
return ['ok' => false, 'error' => $error, 'http_code' => $httpCode, 'url' => $full];
|
|
}
|
|
|
|
$data = json_decode($raw, true);
|
|
if (!is_array($data)) {
|
|
return ['ok' => false, 'error' => 'invalid_json', 'http_code' => $httpCode, 'raw' => $raw, 'url' => $full];
|
|
}
|
|
|
|
return ['ok' => true, 'data' => $data, 'http_code' => $httpCode, 'url' => $full];
|
|
};
|
|
|
|
$resolvePrimaryId = function () use ($instances): ?string {
|
|
foreach ($instances as $id => $row) {
|
|
if (!empty($row['is_primary'])) {
|
|
return $id;
|
|
}
|
|
}
|
|
$first = array_key_first($instances);
|
|
return $first !== null ? (string)$first : null;
|
|
};
|
|
|
|
$pickInstances = function (string $target) use ($instances, $resolvePrimaryId): array {
|
|
if ($target === 'all') {
|
|
return $instances;
|
|
}
|
|
if ($target === 'primary') {
|
|
$primaryId = $resolvePrimaryId();
|
|
if ($primaryId !== null && isset($instances[$primaryId])) {
|
|
return [$primaryId => $instances[$primaryId]];
|
|
}
|
|
}
|
|
if (isset($instances[$target])) {
|
|
return [$target => $instances[$target]];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
$aggregateTopList = function (array $items, array &$bucket): void {
|
|
foreach ($items as $label => $count) {
|
|
if (!is_string($label)) {
|
|
continue;
|
|
}
|
|
$bucket[$label] = ($bucket[$label] ?? 0) + (int)$count;
|
|
}
|
|
};
|
|
|
|
$aggregateMap = function (array $map, array &$bucket): void {
|
|
foreach ($map as $label => $count) {
|
|
if (!is_string($label)) {
|
|
continue;
|
|
}
|
|
$bucket[$label] = ($bucket[$label] ?? 0) + (int)$count;
|
|
}
|
|
};
|
|
|
|
$extractQueryTypes = function (array $data): array {
|
|
if (isset($data['querytypes']) && is_array($data['querytypes'])) {
|
|
return $data['querytypes'];
|
|
}
|
|
return $data;
|
|
};
|
|
|
|
$extractForwardDestinations = function (array $data): array {
|
|
if (isset($data['forward_destinations']) && is_array($data['forward_destinations'])) {
|
|
return $data['forward_destinations'];
|
|
}
|
|
return $data;
|
|
};
|
|
|
|
$extractSources = function (array $data): array {
|
|
if (isset($data['query_sources']) && is_array($data['query_sources'])) {
|
|
return $data['query_sources'];
|
|
}
|
|
return $data;
|
|
};
|
|
|
|
$parseUpdates = function (?array $versions): array {
|
|
if (!$versions) {
|
|
return ['available' => false, 'details' => []];
|
|
}
|
|
$details = [];
|
|
$available = false;
|
|
foreach (['core', 'web', 'FTL'] as $key) {
|
|
$current = $versions[$key . '_current'] ?? null;
|
|
$latest = $versions[$key . '_latest'] ?? null;
|
|
$updateFlag = $versions[$key . '_update'] ?? null;
|
|
$needs = false;
|
|
if (is_string($updateFlag)) {
|
|
$needs = $updateFlag === 'true' || $updateFlag === '1';
|
|
} elseif (is_bool($updateFlag)) {
|
|
$needs = $updateFlag;
|
|
}
|
|
if ($current && $latest && $current !== $latest) {
|
|
$needs = true;
|
|
}
|
|
$details[$key] = [
|
|
'current' => $current,
|
|
'latest' => $latest,
|
|
'update' => $needs,
|
|
];
|
|
if ($needs) {
|
|
$available = true;
|
|
}
|
|
}
|
|
return ['available' => $available, 'details' => $details];
|
|
};
|
|
|
|
if ($action === 'dashboard') {
|
|
if (empty($instances)) {
|
|
$respond(['ok' => false, 'error' => 'no_instances'], 400);
|
|
}
|
|
|
|
$aggregate = [
|
|
'summary' => [
|
|
'dns_queries_today' => 0,
|
|
'ads_blocked_today' => 0,
|
|
'unique_clients' => 0,
|
|
'unique_domains' => 0,
|
|
'queries_forwarded' => 0,
|
|
'queries_cached' => 0,
|
|
'status' => 'unknown',
|
|
],
|
|
'top_ads' => [],
|
|
'top_queries' => [],
|
|
'query_types' => [],
|
|
'query_sources' => [],
|
|
'forward_destinations' => [],
|
|
'recent_blocked' => [],
|
|
'updates' => ['available' => false, 'details' => []],
|
|
];
|
|
|
|
$instancePayloads = [];
|
|
$statuses = [];
|
|
|
|
foreach ($instances as $id => $instance) {
|
|
$errors = [];
|
|
|
|
$summary = $apiRequest($instance, ['summaryRaw' => 1]);
|
|
if (!$summary['ok']) {
|
|
$errors[] = ['scope' => 'summary', 'error' => $summary['error'] ?? 'error'];
|
|
}
|
|
|
|
$topItems = $apiRequest($instance, ['topItems' => 50]);
|
|
if (!$topItems['ok']) {
|
|
$errors[] = ['scope' => 'topItems', 'error' => $topItems['error'] ?? 'error'];
|
|
}
|
|
|
|
$queryTypes = $apiRequest($instance, ['getQueryTypes' => 1]);
|
|
if (!$queryTypes['ok']) {
|
|
$errors[] = ['scope' => 'queryTypes', 'error' => $queryTypes['error'] ?? 'error'];
|
|
}
|
|
|
|
$querySources = $apiRequest($instance, ['getQuerySources' => 1]);
|
|
if (!$querySources['ok']) {
|
|
$errors[] = ['scope' => 'querySources', 'error' => $querySources['error'] ?? 'error'];
|
|
}
|
|
|
|
$forwardDest = $apiRequest($instance, ['getForwardDestinations' => 1]);
|
|
if (!$forwardDest['ok']) {
|
|
$errors[] = ['scope' => 'forwardDestinations', 'error' => $forwardDest['error'] ?? 'error'];
|
|
}
|
|
|
|
$recentBlocked = $apiRequest($instance, ['recentBlocked' => 30]);
|
|
if (!$recentBlocked['ok']) {
|
|
$errors[] = ['scope' => 'recentBlocked', 'error' => $recentBlocked['error'] ?? 'error'];
|
|
}
|
|
|
|
$versions = $apiRequest($instance, ['versions' => 1]);
|
|
if (!$versions['ok']) {
|
|
$errors[] = ['scope' => 'versions', 'error' => $versions['error'] ?? 'error'];
|
|
}
|
|
|
|
$summaryData = $summary['ok'] ? $summary['data'] : null;
|
|
$topData = $topItems['ok'] ? $topItems['data'] : null;
|
|
$queryTypesData = $queryTypes['ok'] ? $extractQueryTypes($queryTypes['data']) : null;
|
|
$querySourcesData = $querySources['ok'] ? $extractSources($querySources['data']) : null;
|
|
$forwardDestData = $forwardDest['ok'] ? $extractForwardDestinations($forwardDest['data']) : null;
|
|
$recentData = $recentBlocked['ok'] ? $recentBlocked['data'] : null;
|
|
$versionsData = $versions['ok'] ? $versions['data'] : null;
|
|
$updates = $parseUpdates(is_array($versionsData) ? $versionsData : null);
|
|
|
|
if (is_array($summaryData)) {
|
|
$aggregate['summary']['dns_queries_today'] += (int)($summaryData['dns_queries_today'] ?? 0);
|
|
$aggregate['summary']['ads_blocked_today'] += (int)($summaryData['ads_blocked_today'] ?? 0);
|
|
$aggregate['summary']['unique_clients'] += (int)($summaryData['unique_clients'] ?? 0);
|
|
$aggregate['summary']['unique_domains'] += (int)($summaryData['unique_domains'] ?? 0);
|
|
$aggregate['summary']['queries_forwarded'] += (int)($summaryData['queries_forwarded'] ?? 0);
|
|
$aggregate['summary']['queries_cached'] += (int)($summaryData['queries_cached'] ?? 0);
|
|
$status = (string)($summaryData['status'] ?? 'unknown');
|
|
$statuses[] = $status;
|
|
}
|
|
|
|
if (is_array($topData)) {
|
|
if (!empty($topData['top_ads']) && is_array($topData['top_ads'])) {
|
|
$aggregateTopList($topData['top_ads'], $aggregate['top_ads']);
|
|
}
|
|
if (!empty($topData['top_queries']) && is_array($topData['top_queries'])) {
|
|
$aggregateTopList($topData['top_queries'], $aggregate['top_queries']);
|
|
}
|
|
}
|
|
|
|
if (is_array($queryTypesData)) {
|
|
$aggregateMap($queryTypesData, $aggregate['query_types']);
|
|
}
|
|
|
|
if (is_array($querySourcesData)) {
|
|
$aggregateMap($querySourcesData, $aggregate['query_sources']);
|
|
}
|
|
|
|
if (is_array($forwardDestData)) {
|
|
$aggregateMap($forwardDestData, $aggregate['forward_destinations']);
|
|
}
|
|
|
|
if (is_array($recentData)) {
|
|
foreach ($recentData as $entry) {
|
|
if (is_string($entry)) {
|
|
$aggregate['recent_blocked'][] = ['domain' => $entry, 'instance' => $instance['name']];
|
|
} elseif (is_array($entry) && isset($entry['domain'])) {
|
|
$aggregate['recent_blocked'][] = ['domain' => (string)$entry['domain'], 'instance' => $instance['name']];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($updates['available'])) {
|
|
$aggregate['updates']['available'] = true;
|
|
}
|
|
$aggregate['updates']['details'][$id] = $updates;
|
|
|
|
$instancePayloads[$id] = [
|
|
'meta' => [
|
|
'id' => $id,
|
|
'name' => $instance['name'],
|
|
'url' => $instance['url'],
|
|
'is_primary' => !empty($instance['is_primary']),
|
|
],
|
|
'summary' => $summaryData,
|
|
'top_items' => $topData,
|
|
'query_types' => $queryTypesData,
|
|
'query_sources' => $querySourcesData,
|
|
'forward_destinations' => $forwardDestData,
|
|
'recent_blocked' => $recentData,
|
|
'versions' => $versionsData,
|
|
'updates' => $updates,
|
|
'errors' => $errors,
|
|
];
|
|
}
|
|
|
|
if ($aggregate['summary']['dns_queries_today'] > 0) {
|
|
$aggregate['summary']['ads_percentage_today'] = round(
|
|
$aggregate['summary']['ads_blocked_today'] / $aggregate['summary']['dns_queries_today'] * 100,
|
|
2
|
|
);
|
|
} else {
|
|
$aggregate['summary']['ads_percentage_today'] = 0;
|
|
}
|
|
|
|
$status = 'unknown';
|
|
if ($statuses) {
|
|
$allEnabled = count(array_filter($statuses, fn($s) => $s === 'enabled')) === count($statuses);
|
|
$allDisabled = count(array_filter($statuses, fn($s) => $s === 'disabled')) === count($statuses);
|
|
if ($allEnabled) {
|
|
$status = 'enabled';
|
|
} elseif ($allDisabled) {
|
|
$status = 'disabled';
|
|
} else {
|
|
$status = 'partial';
|
|
}
|
|
}
|
|
$aggregate['summary']['status'] = $status;
|
|
|
|
$respond([
|
|
'ok' => true,
|
|
'ts' => time(),
|
|
'instances' => $instancePayloads,
|
|
'aggregate' => $aggregate,
|
|
]);
|
|
}
|
|
|
|
if ($action === 'disable') {
|
|
require_admin();
|
|
$minutes = (int)($payload['minutes'] ?? 0);
|
|
$target = (string)($payload['instance'] ?? 'all');
|
|
if ($minutes <= 0) {
|
|
$respond(['ok' => false, 'error' => 'invalid_minutes'], 400);
|
|
}
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, ['disable' => $minutes * 60]);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
if ($action === 'enable') {
|
|
require_admin();
|
|
$target = (string)($payload['instance'] ?? 'all');
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, ['enable' => 1]);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
if ($action === 'gravity') {
|
|
require_admin();
|
|
$target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary');
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, ['updateGravity' => 1]);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
if ($action === 'domain_add') {
|
|
require_admin();
|
|
$domain = trim((string)($payload['domain'] ?? ''));
|
|
$type = (string)($payload['type'] ?? 'block');
|
|
if ($domain === '') {
|
|
$respond(['ok' => false, 'error' => 'missing_domain'], 400);
|
|
}
|
|
$target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary');
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
|
|
$params = $type === 'allow'
|
|
? ['list' => 'white', 'add' => $domain]
|
|
: ['list' => 'black', 'add' => $domain];
|
|
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, $params);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
if ($action === 'adlist_add') {
|
|
require_admin();
|
|
$url = trim((string)($payload['url'] ?? ''));
|
|
if ($url === '') {
|
|
$respond(['ok' => false, 'error' => 'missing_url'], 400);
|
|
}
|
|
$target = $listsPrimaryOnly ? 'primary' : (string)($payload['instance'] ?? 'primary');
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, ['list' => 'adlist', 'add' => $url]);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
if ($action === 'update') {
|
|
require_admin();
|
|
$target = (string)($payload['instance'] ?? 'primary');
|
|
$targets = $pickInstances($target);
|
|
if (!$targets) {
|
|
$respond(['ok' => false, 'error' => 'invalid_instance'], 400);
|
|
}
|
|
|
|
$results = [];
|
|
foreach ($targets as $id => $instance) {
|
|
$result = $apiRequest($instance, ['update' => 1]);
|
|
$results[$id] = $result;
|
|
}
|
|
$respond(['ok' => true, 'results' => $results]);
|
|
}
|
|
|
|
$respond(['ok' => false, 'error' => 'unknown_action'], 400);
|