mirror of
https://git.hexahost.dev/smueller/HexaHost-Frontend.git
synced 2026-06-02 15:38:43 +00:00
chore(release): production build 2026-05-27 13:05
This commit is contained in:
112
public/includes/api-helpers.php
Normal file
112
public/includes/api-helpers.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* Gemeinsame Hilfsfunktionen für öffentliche DNS/API-Endpunkte
|
||||
*/
|
||||
|
||||
/**
|
||||
* Client-IP für Rate-Limiting (Cloudflare-sicher, kein blindes X-Forwarded-For)
|
||||
*/
|
||||
function getApiClientIp(): string {
|
||||
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])
|
||||
&& filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
|
||||
return $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
}
|
||||
|
||||
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||
$isTrustedProxy = filter_var(
|
||||
$remoteAddr,
|
||||
FILTER_VALIDATE_IP,
|
||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||||
) === false;
|
||||
|
||||
if ($isTrustedProxy) {
|
||||
foreach (['HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR'] as $header) {
|
||||
if (empty($_SERVER[$header])) {
|
||||
continue;
|
||||
}
|
||||
$ip = trim(explode(',', $_SERVER[$header])[0]);
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $remoteAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Einfaches Rate-Limiting pro Endpunkt und IP
|
||||
*/
|
||||
function checkApiRateLimit(string $endpoint, int $maxPerHour = 120): bool {
|
||||
$ip = getApiClientIp();
|
||||
$cacheFile = sys_get_temp_dir() . '/hexahost_api_' . md5($endpoint . '_' . $ip) . '.txt';
|
||||
$currentTime = time();
|
||||
$data = ['requests' => []];
|
||||
|
||||
$handle = @fopen($cacheFile, 'c+');
|
||||
if ($handle === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!flock($handle, LOCK_EX)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$contents = stream_get_contents($handle);
|
||||
if ($contents !== false && $contents !== '') {
|
||||
$decoded = json_decode($contents, true);
|
||||
if (is_array($decoded) && isset($decoded['requests'])) {
|
||||
$data = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$data['requests'] = array_values(array_filter(
|
||||
$data['requests'],
|
||||
static fn($timestamp) => ($currentTime - (int) $timestamp) < 3600
|
||||
));
|
||||
|
||||
if (count($data['requests']) >= $maxPerHour) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data['requests'][] = $currentTime;
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($data));
|
||||
} finally {
|
||||
flock($handle, LOCK_UN);
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain aus GET-Parameter normalisieren und validieren
|
||||
*/
|
||||
function getValidatedDomainParam(string $param = 'domain'): ?string {
|
||||
if (empty($_GET[$param])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain = trim((string) $_GET[$param]);
|
||||
$domain = preg_replace('/^(https?:\/\/)?/', '', $domain);
|
||||
$domain = explode('/', $domain)[0];
|
||||
$domain = explode(':', $domain)[0];
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\-\.]*\.[a-zA-Z]{2,}$/', $domain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate-Limit-JSON-Antwort senden und beenden
|
||||
*/
|
||||
function rejectApiRateLimit(): void {
|
||||
http_response_code(429);
|
||||
echo json_encode(['error' => 'Zu viele Anfragen. Bitte versuchen Sie es später erneut.']);
|
||||
exit;
|
||||
}
|
||||
Reference in New Issue
Block a user