2048) return null; $p = @parse_url($url); if (!is_array($p) || empty($p['host'])) return null; return $url; } /** * X-Forwarded-For があれば常に優先(先頭要素) * ただし、IP形式として正しい場合のみ採用する */ function clientIp(): string { $xff = trim((string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? '')); if ($xff !== '') { // "client, proxy1, proxy2" の先頭を取る $parts = array_map('trim', explode(',', $xff)); $candidate = $parts[0] ?? ''; if ($candidate !== '' && filter_var($candidate, FILTER_VALIDATE_IP)) { return $candidate; } } $remote = trim((string)($_SERVER['REMOTE_ADDR'] ?? '')); if ($remote !== '' && filter_var($remote, FILTER_VALIDATE_IP)) { return $remote; } return '0.0.0.0'; } // code取得(rewriteで PATH_INFO などから取るならここを拡張) $code = $_GET['code'] ?? ''; if (!is_string($code) || !preg_match('/^[0-9A-Za-z]{4,32}$/', $code)) { notFound(); } try { [$dsn, $dbUser, $dbPass] = loadDbConfig('/var/www/NEXTSTEP-RELAY/db.conf'); $pdo = new PDO( $dsn, $dbUser, $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ] ); $st = $pdo->prepare(" SELECT id, user_id, long_url FROM short_urls WHERE code = ? AND is_active = 1 AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1 "); $st->execute([$code]); $row = $st->fetch(); if (!$row) { notFound(); } $shortUrlId = (int)$row['id']; $userId = (int)$row['user_id']; $longUrlRaw = (string)$row['long_url']; $longUrl = sanitizeRedirectUrl($longUrlRaw); if ($longUrl === null) { // DB汚染などで危険なURLの場合は503(404でも可) textResponse(503, "Service Unavailable\n"); } $ip = clientIp(); $ua = trunc($_SERVER['HTTP_USER_AGENT'] ?? null, 512); $ref = trunc($_SERVER['HTTP_REFERER'] ?? null, 1024); $method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); $status = 302; // アクセスログ(失敗してもリダイレクト優先) try { $log = $pdo->prepare(" INSERT INTO short_url_access_logs (short_url_id, user_id, client_ip, user_agent, referer, http_method, status_code) VALUES (?, ?, ?, ?, ?, ?, ?) "); $log->execute([$shortUrlId, $userId, $ip, $ua, $ref, $method, $status]); } catch (Throwable $e) { // error_log("access log insert failed: " . $e->getMessage()); } http_response_code($status); header('Location: ' . $longUrl); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); exit; } catch (Throwable $e) { textResponse(503, "Service Unavailable\n"); }