Files
ip-manager/webapp/api.php
2026-01-16 19:48:04 +00:00

458 lines
13 KiB
PHP

<?php
/**
* Geofeed Manager API
* RESTful API for managing geofeed entries
*/
require_once __DIR__ . '/config.php';
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
$method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? '';
// Handle preflight requests
if ($method === 'OPTIONS') {
http_response_code(204);
exit;
}
try {
$db = getDB();
switch ($action) {
case 'list':
handleList($db);
break;
case 'get':
handleGet($db);
break;
case 'create':
handleCreate($db);
break;
case 'update':
handleUpdate($db);
break;
case 'delete':
handleDelete($db);
break;
case 'export':
handleExport($db);
break;
case 'stats':
handleStats($db);
break;
case 'search':
handleSearch($db);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
} catch (Exception $e) {
jsonResponse(['error' => $e->getMessage()], 500);
}
/**
* List entries with pagination and filtering
*/
function handleList($db) {
$page = max(1, intval($_GET['page'] ?? 1));
$limit = min(100, max(10, intval($_GET['limit'] ?? ITEMS_PER_PAGE)));
$offset = ($page - 1) * $limit;
$where = ['1=1'];
$params = [];
// Filtering
if (!empty($_GET['country'])) {
$where[] = 'country_code = :country';
$params[':country'] = strtoupper($_GET['country']);
}
if (!empty($_GET['search'])) {
$where[] = '(ip_prefix LIKE :search OR city LIKE :search2 OR region_code LIKE :search3)';
$searchTerm = '%' . $_GET['search'] . '%';
$params[':search'] = $searchTerm;
$params[':search2'] = $searchTerm;
$params[':search3'] = $searchTerm;
}
$whereClause = implode(' AND ', $where);
// Get total count
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM geofeed_entries WHERE $whereClause");
$countStmt->execute($params);
$total = $countStmt->fetch()['total'];
// Get entries
$sql = "SELECT * FROM geofeed_entries WHERE $whereClause ORDER BY created_at DESC LIMIT :limit OFFSET :offset";
$stmt = $db->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$entries = $stmt->fetchAll();
jsonResponse([
'success' => true,
'data' => $entries,
'pagination' => [
'page' => $page,
'limit' => $limit,
'total' => $total,
'pages' => ceil($total / $limit)
]
]);
}
/**
* Get single entry
*/
function handleGet($db) {
$id = intval($_GET['id'] ?? 0);
if (!$id) {
jsonResponse(['error' => 'Invalid ID'], 400);
}
$stmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id");
$stmt->execute([':id' => $id]);
$entry = $stmt->fetch();
if (!$entry) {
jsonResponse(['error' => 'Entry not found'], 404);
}
jsonResponse(['success' => true, 'data' => $entry]);
}
/**
* Create new entry
*/
function handleCreate($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
// Validate CSRF
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
jsonResponse(['error' => 'Invalid CSRF token'], 403);
}
// Validate required fields
if (empty($input['ip_prefix'])) {
jsonResponse(['error' => 'IP prefix is required'], 400);
}
if (!isValidIpPrefix($input['ip_prefix'])) {
jsonResponse(['error' => 'Invalid IP prefix format'], 400);
}
// Validate optional fields
$countryCode = strtoupper(trim($input['country_code'] ?? ''));
if (!empty($countryCode) && !isValidCountryCode($countryCode)) {
jsonResponse(['error' => 'Invalid country code (must be 2 letters)'], 400);
}
$regionCode = strtoupper(trim($input['region_code'] ?? ''));
if (!empty($regionCode) && !isValidRegionCode($regionCode)) {
jsonResponse(['error' => 'Invalid region code (format: XX-YYY)'], 400);
}
// Check for duplicate
$checkStmt = $db->prepare("SELECT id FROM geofeed_entries WHERE ip_prefix = :prefix");
$checkStmt->execute([':prefix' => $input['ip_prefix']]);
if ($checkStmt->fetch()) {
jsonResponse(['error' => 'An entry with this IP prefix already exists'], 409);
}
// Insert entry
$stmt = $db->prepare("
INSERT INTO geofeed_entries (ip_prefix, country_code, region_code, city, postal_code, notes)
VALUES (:ip_prefix, :country_code, :region_code, :city, :postal_code, :notes)
");
$stmt->execute([
':ip_prefix' => trim($input['ip_prefix']),
':country_code' => $countryCode ?: null,
':region_code' => $regionCode ?: null,
':city' => trim($input['city'] ?? '') ?: null,
':postal_code' => trim($input['postal_code'] ?? '') ?: null,
':notes' => trim($input['notes'] ?? '') ?: null
]);
$id = $db->lastInsertId();
// Log the action
logAction($db, $id, 'INSERT', null, $input);
jsonResponse(['success' => true, 'id' => $id, 'message' => 'Entry created successfully'], 201);
}
/**
* Update existing entry
*/
function handleUpdate($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$id = intval($input['id'] ?? 0);
if (!$id) {
jsonResponse(['error' => 'Invalid ID'], 400);
}
// Validate CSRF
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
jsonResponse(['error' => 'Invalid CSRF token'], 403);
}
// Get existing entry
$checkStmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id");
$checkStmt->execute([':id' => $id]);
$oldEntry = $checkStmt->fetch();
if (!$oldEntry) {
jsonResponse(['error' => 'Entry not found'], 404);
}
// Validate fields
if (empty($input['ip_prefix'])) {
jsonResponse(['error' => 'IP prefix is required'], 400);
}
if (!isValidIpPrefix($input['ip_prefix'])) {
jsonResponse(['error' => 'Invalid IP prefix format'], 400);
}
$countryCode = strtoupper(trim($input['country_code'] ?? ''));
if (!empty($countryCode) && !isValidCountryCode($countryCode)) {
jsonResponse(['error' => 'Invalid country code'], 400);
}
$regionCode = strtoupper(trim($input['region_code'] ?? ''));
if (!empty($regionCode) && !isValidRegionCode($regionCode)) {
jsonResponse(['error' => 'Invalid region code'], 400);
}
// Check for duplicate (excluding current entry)
$dupStmt = $db->prepare("SELECT id FROM geofeed_entries WHERE ip_prefix = :prefix AND id != :id");
$dupStmt->execute([':prefix' => $input['ip_prefix'], ':id' => $id]);
if ($dupStmt->fetch()) {
jsonResponse(['error' => 'Another entry with this IP prefix already exists'], 409);
}
// Update entry
$stmt = $db->prepare("
UPDATE geofeed_entries SET
ip_prefix = :ip_prefix,
country_code = :country_code,
region_code = :region_code,
city = :city,
postal_code = :postal_code,
notes = :notes
WHERE id = :id
");
$stmt->execute([
':id' => $id,
':ip_prefix' => trim($input['ip_prefix']),
':country_code' => $countryCode ?: null,
':region_code' => $regionCode ?: null,
':city' => trim($input['city'] ?? '') ?: null,
':postal_code' => trim($input['postal_code'] ?? '') ?: null,
':notes' => trim($input['notes'] ?? '') ?: null
]);
// Log the action
logAction($db, $id, 'UPDATE', $oldEntry, $input);
jsonResponse(['success' => true, 'message' => 'Entry updated successfully']);
}
/**
* Delete entry
*/
function handleDelete($db) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$id = intval($input['id'] ?? 0);
if (!$id) {
jsonResponse(['error' => 'Invalid ID'], 400);
}
// Validate CSRF
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
jsonResponse(['error' => 'Invalid CSRF token'], 403);
}
// Get existing entry for logging
$checkStmt = $db->prepare("SELECT * FROM geofeed_entries WHERE id = :id");
$checkStmt->execute([':id' => $id]);
$oldEntry = $checkStmt->fetch();
if (!$oldEntry) {
jsonResponse(['error' => 'Entry not found'], 404);
}
// Delete entry
$stmt = $db->prepare("DELETE FROM geofeed_entries WHERE id = :id");
$stmt->execute([':id' => $id]);
// Log the action
logAction($db, $id, 'DELETE', $oldEntry, null);
jsonResponse(['success' => true, 'message' => 'Entry deleted successfully']);
}
/**
* Export entries as CSV
*/
function handleExport($db) {
$stmt = $db->query("SELECT ip_prefix, country_code, region_code, city, postal_code FROM geofeed_entries ORDER BY ip_prefix");
$entries = $stmt->fetchAll();
// Build CSV content (RFC 8805 format)
$csv = "# Geofeed - Generated by Geofeed Manager\r\n";
$csv .= "# Format: ip_prefix,country_code,region_code,city,postal_code\r\n";
$csv .= "# Generated: " . date('c') . "\r\n";
foreach ($entries as $entry) {
$csv .= implode(',', [
$entry['ip_prefix'],
$entry['country_code'] ?? '',
$entry['region_code'] ?? '',
$entry['city'] ?? '',
$entry['postal_code'] ?? ''
]) . "\r\n";
}
// Return as downloadable or as JSON based on format param
if (($_GET['format'] ?? '') === 'download') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="geofeed.csv"');
echo $csv;
exit;
}
jsonResponse([
'success' => true,
'csv' => $csv,
'count' => count($entries)
]);
}
/**
* Get statistics
*/
function handleStats($db) {
$stats = [];
// Total entries
$stmt = $db->query("SELECT COUNT(*) as count FROM geofeed_entries");
$stats['total_entries'] = $stmt->fetch()['count'];
// Entries by country (top 10)
$stmt = $db->query("
SELECT country_code, COUNT(*) as count
FROM geofeed_entries
WHERE country_code IS NOT NULL AND country_code != ''
GROUP BY country_code
ORDER BY count DESC
LIMIT 10
");
$stats['by_country'] = $stmt->fetchAll();
// Recent changes
$stmt = $db->query("
SELECT action, COUNT(*) as count, DATE(changed_at) as date
FROM geofeed_audit_log
WHERE changed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY action, DATE(changed_at)
ORDER BY date DESC
");
$stats['recent_changes'] = $stmt->fetchAll();
// IPv4 vs IPv6
$stmt = $db->query("
SELECT
SUM(CASE WHEN ip_prefix LIKE '%:%' THEN 1 ELSE 0 END) as ipv6,
SUM(CASE WHEN ip_prefix NOT LIKE '%:%' THEN 1 ELSE 0 END) as ipv4
FROM geofeed_entries
");
$stats['ip_versions'] = $stmt->fetch();
jsonResponse(['success' => true, 'data' => $stats]);
}
/**
* Search entries
*/
function handleSearch($db) {
$query = trim($_GET['q'] ?? '');
if (strlen($query) < 2) {
jsonResponse(['error' => 'Search query too short'], 400);
}
$searchTerm = '%' . $query . '%';
$stmt = $db->prepare("
SELECT * FROM geofeed_entries
WHERE ip_prefix LIKE :q1
OR city LIKE :q2
OR region_code LIKE :q3
OR country_code LIKE :q4
ORDER BY ip_prefix
LIMIT 50
");
$stmt->execute([
':q1' => $searchTerm,
':q2' => $searchTerm,
':q3' => $searchTerm,
':q4' => $searchTerm
]);
jsonResponse(['success' => true, 'data' => $stmt->fetchAll()]);
}
/**
* Log action to audit table
*/
function logAction($db, $entryId, $action, $oldValues, $newValues) {
$stmt = $db->prepare("
INSERT INTO geofeed_audit_log (entry_id, action, old_values, new_values, changed_by)
VALUES (:entry_id, :action, :old_values, :new_values, :changed_by)
");
$stmt->execute([
':entry_id' => $entryId,
':action' => $action,
':old_values' => $oldValues ? json_encode($oldValues) : null,
':new_values' => $newValues ? json_encode($newValues) : null,
':changed_by' => $_SERVER['REMOTE_ADDR'] ?? 'system'
]);
}