PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false ] ); } catch (PDOException $e) { die(json_encode(['error' => 'Database connection failed'])); } } return $pdo; } // CSRF Protection function generateCSRFToken() { if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function validateCSRFToken($token) { return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); } // JSON Response helper function jsonResponse($data, $statusCode = 200) { http_response_code($statusCode); header('Content-Type: application/json'); echo json_encode($data); exit; } // Input sanitization function sanitizeInput($input) { if (is_array($input)) { return array_map('sanitizeInput', $input); } return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8'); } // IP prefix validation function isValidIpPrefix($prefix) { if (strpos($prefix, '/') !== false) { list($ip, $cidr) = explode('/', $prefix); if (!is_numeric($cidr)) { return false; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return $cidr >= 0 && $cidr <= 32; } if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { return $cidr >= 0 && $cidr <= 128; } return false; } return filter_var($prefix, FILTER_VALIDATE_IP) !== false; } // Country code validation function isValidCountryCode($code) { if (empty($code)) return true; return preg_match('/^[A-Z]{2}$/i', $code); } // Region code validation (ISO 3166-2) function isValidRegionCode($code) { if (empty($code)) return true; return preg_match('/^[A-Z]{2}-[A-Z0-9]{1,3}$/i', $code); } // Get a setting value from the database function getSetting($db, $key, $default = null) { static $cache = []; if (isset($cache[$key])) { return $cache[$key]; } try { $stmt = $db->prepare("SELECT setting_value FROM geofeed_settings WHERE setting_key = :key"); $stmt->execute([':key' => $key]); $result = $stmt->fetch(); $cache[$key] = $result ? $result['setting_value'] : $default; return $cache[$key]; } catch (Exception $e) { return $default; } } // Save a setting value to the database function saveSetting($db, $key, $value) { $stmt = $db->prepare(" INSERT INTO geofeed_settings (setting_key, setting_value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP "); $stmt->execute([':key' => $key, ':value' => $value]); } /** * Queue a webhook notification with debouncing * This will schedule a webhook to fire after a delay, consolidating multiple updates */ function queueWebhookNotification($db, $reason = 'manual', $entriesAffected = 1) { // Check if webhooks are enabled $enabled = getSetting($db, 'n8n_webhook_enabled', '0'); if ($enabled !== '1') { return false; } $webhookUrl = getSetting($db, 'n8n_webhook_url', ''); if (empty($webhookUrl)) { return false; } $delayMinutes = intval(getSetting($db, 'n8n_webhook_delay_minutes', '0')); // If delay is 0, send immediately without queuing if ($delayMinutes <= 0) { return sendWebhookImmediately($db, $webhookUrl, $reason, $entriesAffected); } $scheduledFor = date('Y-m-d H:i:s', strtotime("+{$delayMinutes} minutes")); // Check if there's already a pending webhook scheduled $stmt = $db->prepare(" SELECT id, entries_affected FROM webhook_queue WHERE status = 'pending' AND scheduled_for > NOW() ORDER BY scheduled_for DESC LIMIT 1 "); $stmt->execute(); $existing = $stmt->fetch(); if ($existing) { // Update existing pending webhook to consolidate and reschedule $stmt = $db->prepare(" UPDATE webhook_queue SET scheduled_for = :scheduled_for, entries_affected = entries_affected + :entries, trigger_reason = CONCAT(IFNULL(trigger_reason, ''), ', ', :reason) WHERE id = :id "); $stmt->execute([ ':scheduled_for' => $scheduledFor, ':entries' => $entriesAffected, ':reason' => $reason, ':id' => $existing['id'] ]); $queueId = $existing['id']; } else { // Create new webhook queue entry $stmt = $db->prepare(" INSERT INTO webhook_queue (webhook_type, trigger_reason, entries_affected, scheduled_for, status) VALUES ('geofeed_update', :reason, :entries, :scheduled_for, 'pending') "); $stmt->execute([ ':reason' => $reason, ':entries' => $entriesAffected, ':scheduled_for' => $scheduledFor ]); $queueId = $db->lastInsertId(); } // Also process any due webhooks opportunistically processWebhookQueue($db); return $queueId; } /** * Send webhook immediately without queuing (for zero-delay mode) */ function sendWebhookImmediately($db, $webhookUrl, $reason, $entriesAffected) { // Log to queue for history purposes $stmt = $db->prepare(" INSERT INTO webhook_queue (webhook_type, trigger_reason, entries_affected, scheduled_for, status) VALUES ('geofeed_update', :reason, :entries, NOW(), 'processing') "); $stmt->execute([ ':reason' => $reason, ':entries' => $entriesAffected ]); $queueId = $db->lastInsertId(); // Send the webhook immediately $payload = [ 'event' => 'geofeed_update', 'queue_id' => $queueId, 'trigger_reason' => $reason, 'entries_affected' => $entriesAffected, 'timestamp' => date('c') ]; $result = sendWebhook($webhookUrl, $payload); // Update status $finalStatus = $result['success'] ? 'completed' : 'failed'; $updateStmt = $db->prepare(" UPDATE webhook_queue SET status = :status, processed_at = NOW(), response_code = :code, response_body = :body WHERE id = :id "); $updateStmt->execute([ ':status' => $finalStatus, ':code' => $result['http_code'], ':body' => substr($result['response'] ?? '', 0, 1000), ':id' => $queueId ]); return $queueId; } /** * Process pending webhooks that are due * This should be called by a cron job or the webhook processor endpoint */ function processWebhookQueue($db) { $webhookUrl = getSetting($db, 'n8n_webhook_url', ''); if (empty($webhookUrl)) { return ['processed' => 0, 'error' => 'No webhook URL configured']; } // Get pending webhooks that are due $stmt = $db->prepare(" SELECT * FROM webhook_queue WHERE status = 'pending' AND scheduled_for <= NOW() ORDER BY scheduled_for ASC LIMIT 10 "); $stmt->execute(); $webhooks = $stmt->fetchAll(); $processed = 0; $results = []; foreach ($webhooks as $webhook) { // Mark as processing $updateStmt = $db->prepare("UPDATE webhook_queue SET status = 'processing' WHERE id = :id"); $updateStmt->execute([':id' => $webhook['id']]); // Send the webhook $payload = [ 'event' => 'geofeed_update', 'queue_id' => $webhook['id'], 'trigger_reason' => $webhook['trigger_reason'], 'entries_affected' => $webhook['entries_affected'], 'queued_at' => $webhook['queued_at'], 'timestamp' => date('c') ]; $result = sendWebhook($webhookUrl, $payload); // Update status $finalStatus = $result['success'] ? 'completed' : 'failed'; $updateStmt = $db->prepare(" UPDATE webhook_queue SET status = :status, processed_at = NOW(), response_code = :code, response_body = :body WHERE id = :id "); $updateStmt->execute([ ':status' => $finalStatus, ':code' => $result['http_code'], ':body' => substr($result['response'], 0, 1000), ':id' => $webhook['id'] ]); $processed++; $results[] = [ 'id' => $webhook['id'], 'success' => $result['success'], 'http_code' => $result['http_code'] ]; } return ['processed' => $processed, 'results' => $results]; } /** * Send a webhook to the configured URL */ function sendWebhook($url, $payload) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'User-Agent: Geofeed-Manager/1.0' ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => true ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); return [ 'success' => $httpCode >= 200 && $httpCode < 300, 'http_code' => $httpCode, 'response' => $response ?: $error, 'error' => $error ]; } /** * Trigger immediate webhook (bypasses queue for manual triggers) */ function triggerImmediateWebhook($db, $reason = 'manual_trigger') { $webhookUrl = getSetting($db, 'n8n_webhook_url', ''); if (empty($webhookUrl)) { return ['success' => false, 'error' => 'No webhook URL configured']; } $payload = [ 'event' => 'geofeed_update', 'trigger_reason' => $reason, 'immediate' => true, 'timestamp' => date('c') ]; return sendWebhook($webhookUrl, $payload); } /** * Authentication Functions */ /** * Check if user is authenticated */ function isAuthenticated() { if (empty($_SESSION['authenticated']) || empty($_SESSION['auth_token'])) { return false; } // Check session timeout if (!empty($_SESSION['auth_time']) && (time() - $_SESSION['auth_time']) > SESSION_TIMEOUT) { logoutUser(); return false; } return true; } /** * Authenticate user with username and password */ function authenticateUser($username, $password) { if ($username === AUTH_USERNAME && $password === AUTH_PASSWORD) { $_SESSION['authenticated'] = true; $_SESSION['auth_token'] = bin2hex(random_bytes(32)); $_SESSION['auth_time'] = time(); $_SESSION['username'] = $username; return true; } return false; } /** * Logout user */ function logoutUser() { $_SESSION['authenticated'] = false; unset($_SESSION['auth_token']); unset($_SESSION['auth_time']); unset($_SESSION['username']); } /** * Require authentication - redirect to login if not authenticated */ function requireAuth() { if (!isAuthenticated()) { header('Location: login.php'); exit; } } /** * Require authentication for API - return JSON error if not authenticated */ function requireAuthApi() { if (!isAuthenticated()) { jsonResponse(['error' => 'Authentication required', 'redirect' => 'login.php'], 401); } } /** * IP Registry Functions */ /** * Fetch IP data from ipregistry.co */ function fetchIpRegistryData($ipPrefix, $db = null) { // Try environment variable first, then database setting $apiKey = IPREGISTRY_API_KEY; if (empty($apiKey) && $db !== null) { $apiKey = getSetting($db, 'ipregistry_api_key', ''); } if (empty($apiKey)) { return ['success' => false, 'error' => 'IP Registry API key not configured']; } // Extract IP from prefix (remove CIDR notation) $ip = explode('/', $ipPrefix)[0]; // Validate IP if (!filter_var($ip, FILTER_VALIDATE_IP)) { return ['success' => false, 'error' => 'Invalid IP address']; } $url = "https://api.ipregistry.co/{$ip}?key={$apiKey}&hostname=true"; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'User-Agent: Geofeed-Manager/1.0' ] ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error || $httpCode !== 200) { return [ 'success' => false, 'error' => $error ?: "HTTP {$httpCode}", 'http_code' => $httpCode ]; } $data = json_decode($response, true); if (!$data) { return ['success' => false, 'error' => 'Invalid JSON response']; } // Extract relevant fields // Note: ipregistry.co doesn't have a direct 'isp' field - organization is the ISP/org name return [ 'success' => true, 'data' => [ 'ipr_hostname' => $data['hostname'] ?? null, 'ipr_isp' => $data['connection']['organization'] ?? null, 'ipr_org' => $data['company']['name'] ?? $data['connection']['organization'] ?? null, 'ipr_asn' => $data['connection']['asn'] ?? null, 'ipr_asn_name' => $data['connection']['domain'] ?? null, 'ipr_connection_type' => $data['connection']['type'] ?? null, 'ipr_country_name' => $data['location']['country']['name'] ?? null, 'ipr_region_name' => $data['location']['region']['name'] ?? null, 'ipr_timezone' => $data['time_zone']['id'] ?? null, 'ipr_latitude' => $data['location']['latitude'] ?? null, 'ipr_longitude' => $data['location']['longitude'] ?? null, // Security flags 'flag_abuser' => !empty($data['security']['is_abuser']) ? 1 : 0, 'flag_attacker' => !empty($data['security']['is_attacker']) ? 1 : 0, 'flag_bogon' => !empty($data['security']['is_bogon']) ? 1 : 0, 'flag_cloud_provider' => !empty($data['security']['is_cloud_provider']) ? 1 : 0, 'flag_proxy' => !empty($data['security']['is_proxy']) ? 1 : 0, 'flag_relay' => !empty($data['security']['is_relay']) ? 1 : 0, 'flag_tor' => !empty($data['security']['is_tor']) ? 1 : 0, 'flag_tor_exit' => !empty($data['security']['is_tor_exit']) ? 1 : 0, 'flag_vpn' => !empty($data['security']['is_vpn']) ? 1 : 0, 'flag_anonymous' => !empty($data['security']['is_anonymous']) ? 1 : 0, 'flag_threat' => !empty($data['security']['is_threat']) ? 1 : 0, ] ]; } /** * Enrich IP entry with IP Registry data */ function enrichIpEntry($db, $entryId, $ipPrefix) { $result = fetchIpRegistryData($ipPrefix, $db); if (!$result['success']) { return $result; } $data = $result['data']; $stmt = $db->prepare(" UPDATE geofeed_entries SET ipr_enriched_at = NOW(), ipr_hostname = :ipr_hostname, ipr_isp = :ipr_isp, ipr_org = :ipr_org, ipr_asn = :ipr_asn, ipr_asn_name = :ipr_asn_name, ipr_connection_type = :ipr_connection_type, ipr_country_name = :ipr_country_name, ipr_region_name = :ipr_region_name, ipr_timezone = :ipr_timezone, ipr_latitude = :ipr_latitude, ipr_longitude = :ipr_longitude, flag_abuser = :flag_abuser, flag_attacker = :flag_attacker, flag_bogon = :flag_bogon, flag_cloud_provider = :flag_cloud_provider, flag_proxy = :flag_proxy, flag_relay = :flag_relay, flag_tor = :flag_tor, flag_tor_exit = :flag_tor_exit, flag_vpn = :flag_vpn, flag_anonymous = :flag_anonymous, flag_threat = :flag_threat WHERE id = :id "); $stmt->execute([ ':id' => $entryId, ':ipr_hostname' => $data['ipr_hostname'], ':ipr_isp' => $data['ipr_isp'], ':ipr_org' => $data['ipr_org'], ':ipr_asn' => $data['ipr_asn'], ':ipr_asn_name' => $data['ipr_asn_name'], ':ipr_connection_type' => $data['ipr_connection_type'], ':ipr_country_name' => $data['ipr_country_name'], ':ipr_region_name' => $data['ipr_region_name'], ':ipr_timezone' => $data['ipr_timezone'], ':ipr_latitude' => $data['ipr_latitude'], ':ipr_longitude' => $data['ipr_longitude'], ':flag_abuser' => $data['flag_abuser'], ':flag_attacker' => $data['flag_attacker'], ':flag_bogon' => $data['flag_bogon'], ':flag_cloud_provider' => $data['flag_cloud_provider'], ':flag_proxy' => $data['flag_proxy'], ':flag_relay' => $data['flag_relay'], ':flag_tor' => $data['flag_tor'], ':flag_tor_exit' => $data['flag_tor_exit'], ':flag_vpn' => $data['flag_vpn'], ':flag_anonymous' => $data['flag_anonymous'], ':flag_threat' => $data['flag_threat'] ]); return ['success' => true, 'data' => $data]; }