550 lines
17 KiB
PHP
550 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Geofeed Manager Configuration
|
|
*/
|
|
|
|
// Error reporting (disable in production)
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', '0');
|
|
|
|
// Configure error logging to local file
|
|
$errorLogPath = __DIR__ . '/error.log';
|
|
ini_set('log_errors', '1');
|
|
ini_set('error_log', $errorLogPath);
|
|
|
|
// Database configuration
|
|
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
|
|
define('DB_NAME', getenv('DB_NAME') ?: 'geofeed_manager');
|
|
define('DB_USER', getenv('DB_USER') ?: 'root');
|
|
define('DB_PASS', getenv('DB_PASS') ?: '');
|
|
|
|
// Application settings
|
|
define('APP_NAME', 'Geofeed Manager');
|
|
define('APP_VERSION', '1.0.0');
|
|
define('ITEMS_PER_PAGE', 25);
|
|
|
|
// Authentication configuration
|
|
define('AUTH_USERNAME', getenv('AUTH_USERNAME') ?: 'admin');
|
|
define('AUTH_PASSWORD', getenv('AUTH_PASSWORD') ?: 'changeme');
|
|
define('SESSION_TIMEOUT', 86400); // 24 hours
|
|
|
|
// IP Registry configuration
|
|
define('IPREGISTRY_API_KEY', getenv('IPREGISTRY_API_KEY') ?: '');
|
|
|
|
// Session configuration
|
|
session_start();
|
|
|
|
// Database connection
|
|
function getDB() {
|
|
static $pdo = null;
|
|
|
|
if ($pdo === null) {
|
|
try {
|
|
$pdo = new PDO(
|
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
|
DB_USER,
|
|
DB_PASS,
|
|
[
|
|
PDO::ATTR_ERRMODE => 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', '3'));
|
|
$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']
|
|
]);
|
|
return $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
|
|
]);
|
|
return $db->lastInsertId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}";
|
|
|
|
$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];
|
|
}
|