| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/clients/client2/web21/web/api/lib/ |
Upload File : |
<?php
/**
* Netzwerk-Kernbibliothek: Eingabe-Parsing, IP-Klassifizierung, SSRF-Schutz,
* sicherer Cache und sichere Outbound-Requests zu bekannten Providern.
*
* WICHTIG (SSRF): Jede vom Nutzer stammende Ziel-Auflösung MUSS über
* smi_resolve_public_ips() laufen. Interne/Private/Reservierte Ziele werden
* konsequent blockiert.
*
* @package SafeMyIdent\Api
*/
declare(strict_types=1);
/**
* Konfiguration lesen (vom WordPress-Plugin geschrieben) inkl. Defaults.
*
* @return array<string,mixed>
*/
function smi_config(): array
{
static $cfg = null;
if ($cfg !== null) {
return $cfg;
}
$defaults = [
'cache_ttl' => 900, // Sekunden.
'rate_limit' => 40, // Anfragen …
'rate_window' => 60, // … pro Fenster (s).
'pdf_enabled' => true,
'debug' => false,
'modules' => [ // aktivierbare Checks.
'geo' => true, 'asn' => true, 'rdap' => true, 'dns' => true,
'blacklist' => true, 'mail' => true, 'url' => true, 'detect' => true,
],
'dnsbl_custom' => [], // optionale zusätzliche Zonen (Plugin).
'keys' => [ 'abuseipdb' => '', 'ipinfo' => '' ],
'geo_provider' => 'ip-api', // ip-api | ipinfo
'ipapi_direct' => false, // ip-api.com DIREKT (US) abfragen.
'ipapi_via_gateway'=> true, // ip-api über Gateway-Proxy (VPN, DSGVO) – Vorrang.
'route_http_via_gateway' => true, // RIPEstat/RDAP/Tor-Liste über den Gateway-VPN leiten.
'route_dns_via_gateway' => false, // DNS (Team Cymru/DNSBL/Records) über Gateway-DoH (Endpoint nötig).
'intel' => [ // IP-Intel-Gateway (horse.pacim.de).
'enabled' => false,
'base_url' => 'https://horse.pacim.de',
'api_key' => '',
],
];
// Bevorzugt PHP-Config (nicht web-abrufbar); Fallback JSON (altes Format, ohne Secrets).
$php = __DIR__ . '/../config/settings.php';
$json = __DIR__ . '/../config/settings.json';
$cfg = $defaults;
if (is_file($php)) {
$dec = @include $php;
if (is_array($dec)) {
$cfg = array_replace_recursive($defaults, $dec);
}
} elseif (is_file($json)) {
$raw = @file_get_contents($json);
$dec = $raw !== false ? json_decode($raw, true) : null;
if (is_array($dec)) {
$cfg = array_replace_recursive($defaults, $dec);
}
}
return $cfg;
}
/**
* Datei-Cache lesen.
*
* @param string $key Eindeutiger Schlüssel.
* @return mixed|null Wert oder null, wenn nicht/abgelaufen.
*/
function smi_cache_get(string $key)
{
$file = sys_get_temp_dir() . '/smi-cache-' . hash('sha256', $key);
if (!is_file($file)) {
return null;
}
$raw = @file_get_contents($file);
$data = $raw !== false ? json_decode($raw, true) : null;
if (is_array($data) && ($data['exp'] ?? 0) >= time()) {
return $data['val'] ?? null;
}
@unlink($file);
return null;
}
/**
* Datei-Cache schreiben.
*
* @param string $key Schlüssel.
* @param mixed $val Wert.
* @param int $ttl Lebensdauer in Sekunden.
*/
function smi_cache_set(string $key, $val, int $ttl): void
{
$file = sys_get_temp_dir() . '/smi-cache-' . hash('sha256', $key);
@file_put_contents($file, json_encode(['exp' => time() + $ttl, 'val' => $val]));
}
/**
* IP-Version bestimmen.
*
* @param string $ip IP.
* @return int 4, 6 oder 0.
*/
function smi_ip_version(string $ip): int
{
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return 4;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return 6;
}
return 0;
}
/**
* Ist die IP öffentlich routbar? (SSRF-Gate)
* Blockiert private, reservierte, Loopback, Link-Local, Multicast,
* IPv4-mapped/compat IPv6 und 0.0.0.0/::.
*
* @param string $ip IP.
* @return bool True = öffentlich & erlaubt.
*/
function smi_ip_is_public(string $ip): bool
{
if (smi_ip_version($ip) === 0) {
return false;
}
// Kernprüfung: keine privaten, keine reservierten Bereiche.
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return false;
}
$bin = @inet_pton($ip);
if ($bin === false) {
return false;
}
// 0.0.0.0 / :: und Multicast.
if ($ip === '0.0.0.0' || $ip === '::') {
return false;
}
// IPv4-Multicast 224.0.0.0/4, Broadcast 255.255.255.255.
if (strlen($bin) === 4) {
$first = ord($bin[0]);
if ($first >= 224) { // 224–239 Multicast, 240–255 reserviert/Broadcast.
return false;
}
// 100.64.0.0/10 CGNAT.
if ($first === 100 && (ord($bin[1]) & 0xC0) === 0x40) {
return false;
}
}
// IPv6-Spezialfälle.
if (strlen($bin) === 16) {
// ff00::/8 Multicast.
if (ord($bin[0]) === 0xff) {
return false;
}
// ::ffff:0:0/96 IPv4-mapped → auf eingebettete IPv4 prüfen.
$mapped = substr($bin, 0, 12);
if ($mapped === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") {
$v4 = inet_ntop(substr($bin, 12, 4));
return $v4 !== false && smi_ip_is_public($v4);
}
// ::/96 IPv4-compat & ::1 werden bereits von NO_RES_RANGE erfasst.
}
return true;
}
/**
* Nutzer-Eingabe klassifizieren und normalisieren.
* Erkennt: IP (v4/v6), URL, Domain, Hostname. Extrahiert bei URL die Domain.
*
* @param string $raw Roh-Eingabe (bereits längenbegrenzt aufrufen).
* @return array{type:string,ip:?string,domain:?string,url:?string,host:?string,error:?string}
*/
function smi_classify_input(string $raw): array
{
$out = ['type' => 'invalid', 'ip' => null, 'domain' => null, 'url' => null, 'host' => null, 'error' => null];
$raw = trim($raw);
if ($raw === '' || strlen($raw) > 2048) {
$out['error'] = 'Leere oder zu lange Eingabe.';
return $out;
}
// Reine IP?
if (smi_ip_version($raw) > 0) {
$out['type'] = 'ip';
$out['ip'] = $raw;
$out['host'] = $raw;
return $out;
}
// IPv6 in Klammern [..]?
if (preg_match('/^\[([0-9a-f:]+)\]$/i', $raw, $m) && smi_ip_version($m[1]) === 6) {
$out['type'] = 'ip';
$out['ip'] = $m[1];
$out['host'] = $m[1];
return $out;
}
// URL (mit Schema oder erkennbarem Pfad/Schema)?
$looksUrl = str_contains($raw, '://') || preg_match('#^[a-z][a-z0-9+.\-]*://#i', $raw);
if ($looksUrl) {
$scheme = strtolower((string) parse_url($raw, PHP_URL_SCHEME));
if (!in_array($scheme, ['http', 'https'], true)) {
$out['error'] = 'Nur http/https-URLs werden geprüft.';
return $out;
}
$host = (string) parse_url($raw, PHP_URL_HOST);
$host = trim($host, '[]');
if ($host === '') {
$out['error'] = 'URL ohne Host.';
return $out;
}
$out['type'] = 'url';
$out['url'] = $raw;
$out['host'] = $host;
if (smi_ip_version($host) > 0) {
$out['ip'] = $host;
} else {
$out['domain'] = smi_valid_domain($host);
}
return $out;
}
// Sonst: Domain/Hostname.
$host = strtolower($raw);
// evtl. Pfad ohne Schema (example.com/foo) → Host abschneiden.
if (str_contains($host, '/')) {
$host = explode('/', $host, 2)[0];
}
$host = trim($host, '.');
$dom = smi_valid_domain($host);
if ($dom !== null) {
$out['type'] = 'domain';
$out['domain'] = $dom;
$out['host'] = $dom;
return $out;
}
$out['error'] = 'Eingabe ist keine gültige IP, Domain oder URL.';
return $out;
}
/**
* Hostname SSRF-sicher zu ÖFFENTLICHEN IPs auflösen.
* Gibt nur öffentlich routbare A/AAAA zurück; leer, wenn nur interne Ziele.
*
* @param string $host Hostname oder IP.
* @return array{ipv4:string[],ipv6:string[],all:string[]}
*/
function smi_resolve_public_ips(string $host): array
{
$res = ['ipv4' => [], 'ipv6' => [], 'all' => []];
if (smi_ip_version($host) > 0) {
if (smi_ip_is_public($host)) {
$key = smi_ip_version($host) === 6 ? 'ipv6' : 'ipv4';
$res[$key][] = $host;
$res['all'][] = $host;
}
return $res;
}
$a = smi_dns($host, DNS_A);
$aaaa = smi_dns($host, DNS_AAAA);
foreach ($a as $r) {
$ip = $r['ip'] ?? '';
if ($ip !== '' && smi_ip_is_public($ip) && !in_array($ip, $res['ipv4'], true)) {
$res['ipv4'][] = $ip;
}
}
foreach ($aaaa as $r) {
$ip = $r['ipv6'] ?? '';
if ($ip !== '' && smi_ip_is_public($ip) && !in_array($ip, $res['ipv6'], true)) {
$res['ipv6'][] = $ip;
}
}
$res['all'] = array_merge($res['ipv4'], $res['ipv6']);
return $res;
}
/**
* DNS-Flag → Typ-String.
*
* @param int $flag DNS_A etc.
* @return string|null
*/
function smi_dns_flag_to_type(int $flag): ?string
{
$map = [DNS_A => 'A', DNS_AAAA => 'AAAA', DNS_MX => 'MX', DNS_TXT => 'TXT', DNS_NS => 'NS', DNS_CNAME => 'CNAME', DNS_SOA => 'SOA', DNS_PTR => 'PTR'];
return $map[$flag] ?? null;
}
/**
* Gateway-DNS-Records → dns_get_record()-kompatibles Format mappen.
*
* @param array<int,array<string,mixed>> $records Gateway-Records.
* @param string $type Typ.
* @return array<int,array<string,mixed>>
*/
function smi_dns_map_gateway(array $records, string $type): array
{
$out = [];
foreach ($records as $r) {
$rt = strtoupper((string) ($r['type'] ?? $type));
$val = $r['value'] ?? $r['data'] ?? $r['target'] ?? $r['address'] ?? '';
$ttl = isset($r['ttl']) ? (int) $r['ttl'] : null;
switch ($rt) {
case 'A': $out[] = ['type' => 'A', 'ip' => $val, 'ttl' => $ttl]; break;
case 'AAAA': $out[] = ['type' => 'AAAA', 'ipv6' => $val, 'ttl' => $ttl]; break;
case 'MX': $out[] = ['type' => 'MX', 'pri' => (int) ($r['priority'] ?? $r['pri'] ?? 0), 'target' => rtrim((string) $val, '.'), 'ttl' => $ttl]; break;
case 'NS': $out[] = ['type' => 'NS', 'target' => rtrim((string) $val, '.'), 'ttl' => $ttl]; break;
case 'CNAME': $out[] = ['type' => 'CNAME', 'target' => rtrim((string) $val, '.'), 'ttl' => $ttl]; break;
case 'PTR': $out[] = ['type' => 'PTR', 'target' => rtrim((string) $val, '.'), 'ttl' => $ttl]; break;
case 'TXT': $out[] = ['type' => 'TXT', 'txt' => (string) $val, 'entries' => [(string) $val], 'ttl' => $ttl]; break;
case 'SOA': $out[] = ['type' => 'SOA', 'mname' => (string) ($r['mname'] ?? ''), 'rname' => (string) ($r['rname'] ?? ''), 'serial' => (int) ($r['serial'] ?? 0), 'refresh' => (int) ($r['refresh'] ?? 0), 'retry' => (int) ($r['retry'] ?? 0), 'expiry' => (int) ($r['expire'] ?? $r['expiry'] ?? 0), 'minimum-ttl' => (int) ($r['minimum'] ?? 0), 'ttl' => $ttl]; break;
default: break;
}
}
return $out;
}
/**
* Typ-String → DNS-Flag.
*
* @param string $type Typ.
* @return int
*/
function smi_dns_type_to_flag(string $type): int
{
$map = ['A' => DNS_A, 'AAAA' => DNS_AAAA, 'MX' => DNS_MX, 'TXT' => DNS_TXT, 'NS' => DNS_NS, 'CNAME' => DNS_CNAME, 'SOA' => DNS_SOA, 'PTR' => DNS_PTR];
return $map[strtoupper($type)] ?? DNS_A;
}
/**
* Mehrere DNS-Abfragen auf einmal – über den Gateway-Batch (1 Round-Trip) oder direkt.
* Gibt ein Array in gleicher Reihenfolge zurück (je Query die Records im
* dns_get_record-Format).
*
* @param array<int,array{name:string,type:string|int}> $queries Abfragen.
* @return array<int,array<int,array<string,mixed>>>
*/
function smi_dns_many(array $queries): array
{
if (!$queries) {
return [];
}
$cfg = smi_config();
if (!empty($cfg['route_dns_via_gateway'])
&& function_exists('smi_gateway_dns_batch') && function_exists('smi_intel_enabled') && smi_intel_enabled()) {
$gwq = [];
foreach ($queries as $q) {
$type = is_int($q['type']) ? smi_dns_flag_to_type($q['type']) : strtoupper((string) $q['type']);
$gwq[] = ['name' => $q['name'], 'type' => $type ?: 'A'];
}
$batch = smi_gateway_dns_batch($gwq);
if (is_array($batch)) {
$out = [];
foreach ($queries as $i => $q) {
$type = is_int($q['type']) ? smi_dns_flag_to_type($q['type']) : strtoupper((string) $q['type']);
$out[] = smi_dns_map_gateway(is_array($batch[$i] ?? null) ? $batch[$i] : [], $type ?: 'A');
}
return $out;
}
}
// Fallback: einzeln direkt.
$out = [];
foreach ($queries as $q) {
$flag = is_int($q['type']) ? $q['type'] : smi_dns_type_to_flag((string) $q['type']);
$out[] = @dns_get_record($q['name'], $flag) ?: [];
}
return $out;
}
/**
* DNS-Abfrage – optional über den Gateway-VPN (DoH), sonst direkt.
* Drop-in-Ersatz für dns_get_record() mit EINEM Typ-Flag.
*
* @param string $name Name.
* @param int $flag DNS_A etc.
* @return array<int,array<string,mixed>>
*/
function smi_dns(string $name, int $flag): array
{
$cfg = smi_config();
if (!empty($cfg['route_dns_via_gateway'])
&& function_exists('smi_gateway_dns') && function_exists('smi_intel_enabled') && smi_intel_enabled()) {
$type = smi_dns_flag_to_type($flag);
if ($type !== null) {
$gw = smi_gateway_dns($name, $type);
if (is_array($gw)) { // Erfolg (auch leer = NXDOMAIN).
return smi_dns_map_gateway($gw, $type);
}
// null = Transportfehler → direkter Fallback.
}
}
return @dns_get_record($name, $flag) ?: [];
}
/**
* Reverse-DNS (PTR) einer IP.
*
* @param string $ip IP.
* @return string Hostname oder ''.
*/
function smi_reverse_dns(string $ip): string
{
if (smi_ip_version($ip) === 0) {
return '';
}
$host = @gethostbyaddr($ip);
if ($host === false || $host === $ip) {
return '';
}
return rtrim(strtolower($host), '.');
}
/**
* Sicherer Outbound-GET zu einem BEKANNTEN Provider-Host (kein Nutzer-Ziel).
* Mit UA, Timeout, Größenlimit. Gibt Body oder '' zurück.
*
* @param string $url Vollständige URL (Provider).
* @param int $timeout Sekunden.
* @param array<string,string> $headers Zusätzliche Header.
* @return string
*/
function smi_provider_get(string $url, int $timeout = 6, array $headers = []): string
{
// Optional über den Gateway-VPN leiten (RIPEstat/RDAP/Tor). Bei Fehler → direkter Fallback.
$cfg = smi_config();
if (!empty($cfg['route_http_via_gateway'])
&& function_exists('smi_gateway_fetch') && function_exists('smi_intel_enabled') && smi_intel_enabled()) {
$body = smi_gateway_fetch($url);
if ($body !== '') {
return substr($body, 0, 1_000_000);
}
// sonst weiter mit direktem Abruf.
}
$hdr = ['User-Agent: SafeMyIdent-IPCheck/1.0 (+https://www.safemyident.de/tools/ip-check/)', 'Accept: application/json'];
foreach ($headers as $k => $v) {
$hdr[] = $k . ': ' . $v;
}
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_HTTPHEADER => $hdr,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_ENCODING => '',
]);
$body = curl_exec($ch);
curl_close($ch);
return is_string($body) ? substr($body, 0, 1_000_000) : '';
}
$ctx = stream_context_create(['http' => [
'method' => 'GET', 'timeout' => $timeout, 'header' => implode("\r\n", $hdr), 'follow_location' => 1, 'max_redirects' => 3,
]]);
$body = @file_get_contents($url, false, $ctx);
return is_string($body) ? substr($body, 0, 1_000_000) : '';
}