reworked image-fetching

This commit is contained in:
2025-09-15 08:13:23 +02:00
parent 63898b6391
commit d38123ce67
2 changed files with 391 additions and 147 deletions

181
item.php
View File

@@ -1,6 +1,10 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/include/image_fetch.php';
use WList\Net\ImageFetch;
/* ========= Session & Bootstrap ========= */
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
session_set_cookie_params([
@@ -37,9 +41,7 @@ if (!empty($app_debug)) {
error_reporting(E_ALL);
}
/* ============= Helpers ============= */
function fail(string $msg = 'Unerwarteter Fehler', int $code = 400): void
{
http_response_code($code);
@@ -90,133 +92,6 @@ function is_valid_http_url(string $url): bool
$s = strtolower($p['scheme']);
return $s === 'http' || $s === 'https';
}
function ip_in_cidr(string $ip, string $cidr): bool
{
if (strpos($cidr, ':') !== false) {
[$subnet, $mask] = array_pad(explode('/', $cidr, 2), 2, null);
$mask = (int) $mask;
$binIp = inet_pton($ip);
$binSubnet = inet_pton($subnet);
if ($binIp === false || $binSubnet === false)
return false;
$bytes = intdiv($mask, 8);
$bits = $mask % 8;
if ($bytes && substr($binIp, 0, $bytes) !== substr($binSubnet, 0, $bytes))
return false;
if ($bits) {
$b1 = ord($binIp[$bytes]) & (0xFF << (8 - $bits));
$b2 = ord($binSubnet[$bytes]) & (0xFF << (8 - $bits));
return $b1 === $b2;
}
return true;
} else {
[$subnet, $mask] = array_pad(explode('/', $cidr, 2), 2, null);
$mask = (int) $mask;
$ipL = ip2long($ip);
$subL = ip2long($subnet);
if ($ipL === false || $subL === false)
return false;
$maskL = -1 << (32 - $mask);
return (($ipL & $maskL) === ($subL & $maskL));
}
}
function is_private_ip(string $ip): bool
{
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
foreach (['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '169.254.0.0/16'] as $c)
if (ip_in_cidr($ip, $c))
return true;
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
foreach (['::1/128', 'fc00::/7', 'fe80::/10'] as $c)
if (ip_in_cidr($ip, $c))
return true;
}
return false;
}
function validate_remote_host_not_private(string $url): void
{
$p = parse_url($url);
if (!$p || empty($p['host']))
fail('Ungültige URL', 400);
$host = $p['host'];
global $image_host_whitelist;
if (isset($image_host_whitelist) && is_array($image_host_whitelist) && count($image_host_whitelist) > 0) {
$ok = false;
foreach ($image_host_whitelist as $allowed) {
if (strcasecmp($host, $allowed) === 0) {
$ok = true;
break;
}
if (preg_match('/\.' . preg_quote($allowed, '/') . '$/i', $host)) {
$ok = true;
break;
}
}
if (!$ok)
fail('Host nicht erlaubt', 400);
}
$recs = dns_get_record($host, DNS_A + DNS_AAAA);
if (!$recs || !count($recs))
fail('Host nicht auflösbar', 400);
foreach ($recs as $r) {
$ip = $r['type'] === 'A' ? ($r['ip'] ?? null) : ($r['ipv6'] ?? null);
if (!$ip)
continue;
if (is_private_ip($ip))
fail('Zieladresse unzulässig', 400);
}
}
function download_remote_image_limited(string $url, int $maxBytes = 5_000_000, int $timeout = 8): string
{
$tmp = tempnam(sys_get_temp_dir(), 'wlimg_');
if ($tmp === false)
fail('Temp-Datei Fehler', 500);
$fh = fopen($tmp, 'wb');
if ($fh === false) {
@unlink($tmp);
fail('Temp-Datei Fehler', 500);
}
$ch = curl_init($url);
if ($ch === false) {
fclose($fh);
@unlink($tmp);
fail('Download Fehler', 500);
}
$received = 0;
curl_setopt_array($ch, [
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_USERAGENT => 'wishlist/1.0',
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_WRITEFUNCTION => function ($ch, $data) use (&$received, $maxBytes, $fh) {
$len = strlen($data);
$received += $len;
if ($received > $maxBytes)
return 0;
return fwrite($fh, $data);
}
]);
$ok = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
fclose($fh);
if (!$ok || $code < 200 || $code >= 300) {
@unlink($tmp);
fail('Bild-Download fehlgeschlagen', 400);
}
return $tmp;
}
function safe_image_filename_from_url(string $url): string
{
$stripped = strtok($url, '?#');
$ext = strtolower(pathinfo((string) $stripped, PATHINFO_EXTENSION));
if (!preg_match('/^[a-z0-9]{1,5}$/i', $ext))
$ext = 'jpg';
return bin2hex(random_bytes(10)) . '.' . $ext;
}
/* ============= Controller ============= */
@@ -269,38 +144,50 @@ if ($ItemLink !== '' && !is_valid_http_url($ItemLink))
/* Optional: Bild von externer URL holen */
$imageLocalLink = null;
if (!$removeImage && $ItemImageUrl !== '') {
if (!is_valid_http_url($ItemImageUrl)) {
$whitelist = $image_host_whitelist ?? null;
$fetch = ImageFetch::download($ItemImageUrl, [
'max_bytes' => 8_000_000,
'timeout' => 12,
'connect_timeout' => 5,
'retries' => 4,
'retry_backoff_ms' => 300,
'whitelist_hosts' => $whitelist,
'ip_resolve_v4' => true,
'referer' => 'auto',
'log_prefix' => 'wishlist-img',
]);
if (!$fetch['ok']) {
error_log("wishlist image error: http=" . ($fetch['http_code'] ?? 0) . " curl=" . ($fetch['curl_err'] ?? '-') . " url=$ItemImageUrl");
$conn->close();
fail('Ungültiger Bildlink', 400);
fail('Bild-Download fehlgeschlagen', 400);
}
validate_remote_host_not_private($ItemImageUrl);
$tmp = download_remote_image_limited($ItemImageUrl, 5_000_000, 8);
$info = @getimagesize($tmp);
if ($info === false || empty($info['mime']) || stripos($info['mime'], 'image/') !== 0) {
@unlink($tmp);
$info = @getimagesize($fetch['tmp_path']);
$mime = $info['mime'] ?? $fetch['mime'] ?? 'image/*';
if (stripos($mime, 'image/') !== 0) {
@unlink($fetch['tmp_path']);
$conn->close();
fail('Link ist kein gültiges Bild', 400);
}
global $imagedir;
if (!is_dir($imagedir)) {
if (!is_dir($imagedir))
@mkdir($imagedir, 0755, true);
}
$filename = safe_image_filename_from_url($ItemImageUrl);
$filename = ImageFetch::safeFileNameFromUrl($ItemImageUrl);
$target = rtrim($imagedir, '/') . '/' . $filename;
if (!@rename($tmp, $target)) {
// Fallback falls rename scheitert
if (!@copy($tmp, $target)) {
@unlink($tmp);
if (!@rename($fetch['tmp_path'], $target)) {
if (!@copy($fetch['tmp_path'], $target)) {
@unlink($fetch['tmp_path']);
$conn->close();
fail('Bildspeicherung fehlgeschlagen', 500);
}
@unlink($tmp);
@unlink($fetch['tmp_path']);
}
// HIER: Permissions fixen
@chmod($target, 0644);
$imageLocalLink = $filename;
}