2603.8 purging old records

This commit is contained in:
2026-03-13 16:01:19 -04:00
parent 86573769ca
commit 1e57388299
8 changed files with 191 additions and 17 deletions

25
cron/purge.php Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env php
<?php
/**
* portspoof_concentrator purge cron
*
* Deletes connections older than the retention_days setting.
* Must be run from the command line — exits immediately if called over HTTP.
*
* Recommended crontab entry (once daily at 02:00):
* 0 2 * * * /usr/bin/php /path/to/portspoof_concentrator/cron/purge.php >> /var/log/portspoof_concentrator/purge.log 2>&1
*/
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit;
}
require_once __DIR__ . '/../includes/functions.php';
$days = max(1, (int)get_setting('retention_days', '7'));
$deleted = purge_old_connections();
echo sprintf("[%s] Purged %d connection(s) older than %d day(s).\n",
date('Y-m-d H:i:s'), $deleted, $days);

View File

@@ -181,6 +181,38 @@ function run_fetch(): array {
return $results; return $results;
} }
// ── Settings ──────────────────────────────────────────────────────────────────
function get_setting(string $key, string $default = ''): string {
$s = db()->prepare('SELECT value FROM settings WHERE key_name = ?');
$s->execute([$key]);
$row = $s->fetchColumn();
return $row !== false ? $row : $default;
}
function set_setting(string $key, string $value): void {
$s = db()->prepare(
'INSERT INTO settings (key_name, value) VALUES (?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value)'
);
$s->execute([$key, $value]);
}
// ── Purge ─────────────────────────────────────────────────────────────────────
/**
* Delete connections older than retention_days setting.
* Returns the number of rows deleted.
*/
function purge_old_connections(): int {
$days = max(1, (int)get_setting('retention_days', '7'));
$s = db()->prepare(
'DELETE FROM connections WHERE occurred_at < NOW() - INTERVAL ? DAY'
);
$s->execute([$days]);
return $s->rowCount();
}
// ── Upstream version check ──────────────────────────────────────────────────── // ── Upstream version check ────────────────────────────────────────────────────
define('UPSTREAM_VERSION_URL', 'https://git.ny.daprogs.com/api/v1/repos/DAProgs/portspoof_concentrator/raw/version.php?ref=main'); define('UPSTREAM_VERSION_URL', 'https://git.ny.daprogs.com/api/v1/repos/DAProgs/portspoof_concentrator/raw/version.php?ref=main');

View File

@@ -56,7 +56,7 @@ code { font-family: 'Cascadia Code', 'Fira Mono', monospace; font-size: .8rem; c
label { display: block; margin-bottom: .75rem; font-size: .85rem; color: var(--muted); } label { display: block; margin-bottom: .75rem; font-size: .85rem; color: var(--muted); }
label.inline { display: flex; align-items: center; gap: .5rem; color: var(--text); } label.inline { display: flex; align-items: center; gap: .5rem; color: var(--text); }
label input[type=text], label input[type=url], label input[type=password] { label input[type=text], label input[type=url], label input[type=password], label input[type=number] {
display: block; width: 100%; margin-top: .3rem; display: block; width: 100%; margin-top: .3rem;
background: var(--bg); border: 1px solid var(--border); border-radius: 5px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
color: var(--text); padding: .45rem .6rem; font-size: .9rem; color: var(--text); padding: .45rem .6rem; font-size: .9rem;

67
purge.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
/**
* portspoof_concentrator HTTP purge trigger
*
* Runs the same purge logic as cron/purge.php when called over HTTP.
* Deletes connections older than the retention_days setting.
*
* Authentication (any one of):
* - Active web session (logged-in browser)
* - Authorization: Bearer <TRIGGER_TOKEN>
* - ?token=<TRIGGER_TOKEN>
*
* Usage:
* GET/POST /purge.php
* GET/POST /purge.php?token=<secret>
* GET/POST /purge.php (with header: Authorization: Bearer <secret>)
*
* Always returns JSON.
*/
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/functions.php';
header('Content-Type: application/json');
// ── Auth ──────────────────────────────────────────────────────────────────────
$session_ok = false;
if (auth_enabled()) {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$session_ok = !empty($_SESSION['authenticated']);
}
$token_ok = false;
if (TRIGGER_TOKEN !== '') {
$provided = '';
$auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($auth_header, 'Bearer ')) {
$provided = substr($auth_header, 7);
}
if ($provided === '' && isset($_REQUEST['token'])) {
$provided = $_REQUEST['token'];
}
$token_ok = $provided !== '' && hash_equals(TRIGGER_TOKEN, $provided);
}
if (!$session_ok && !$token_ok) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// ── Run purge ─────────────────────────────────────────────────────────────────
$started_at = microtime(true);
$retention_days = max(1, (int)get_setting('retention_days', '7'));
$deleted = purge_old_connections();
$elapsed_ms = (int)round((microtime(true) - $started_at) * 1000);
echo json_encode([
'ok' => true,
'elapsed_ms' => $elapsed_ms,
'retention_days' => $retention_days,
'deleted' => $deleted,
], JSON_PRETTY_PRINT);

View File

@@ -31,3 +31,11 @@ CREATE TABLE IF NOT EXISTS connections (
KEY idx_dst_port (dst_port), KEY idx_dst_port (dst_port),
CONSTRAINT fk_conn_node FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE CONSTRAINT fk_conn_node FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS settings (
key_name VARCHAR(64) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (key_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO settings (key_name, value) VALUES ('retention_days', '7');

View File

@@ -1,11 +1,15 @@
<?php <?php
require_once __DIR__ . '/includes/auth.php'; require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/functions.php';
require_login(); require_login();
$errors = []; $errors = [];
$success = ''; $success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'password') {
$current = $_POST['current_password'] ?? ''; $current = $_POST['current_password'] ?? '';
$new = $_POST['new_password'] ?? ''; $new = $_POST['new_password'] ?? '';
$confirm = $_POST['confirm_password'] ?? ''; $confirm = $_POST['confirm_password'] ?? '';
@@ -21,7 +25,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else { } else {
$success = 'Password updated successfully.'; $success = 'Password updated successfully.';
} }
} elseif ($action === 'retention') {
$days = (int)($_POST['retention_days'] ?? 0);
if ($days < 1) {
$errors[] = 'Retention period must be at least 1 day.';
} else {
set_setting('retention_days', (string)$days);
$success = 'Retention period saved.';
} }
}
}
$retention_days = (int)get_setting('retention_days', '7');
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -48,10 +64,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<main> <main>
<?php if ($success): ?> <?php if ($success): ?>
<div class="alert ok"><?= htmlspecialchars($success, ENT_QUOTES, 'UTF-8') ?></div> <div class="alert ok"><?= h($success) ?></div>
<?php endif; ?> <?php endif; ?>
<?php foreach ($errors as $e): ?> <?php foreach ($errors as $e): ?>
<div class="alert err"><?= htmlspecialchars($e, ENT_QUOTES, 'UTF-8') ?></div> <div class="alert err"><?= h($e) ?></div>
<?php endforeach; ?> <?php endforeach; ?>
<section class="card"> <section class="card">
@@ -60,6 +76,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<p class="muted">Authentication is disabled. Set <code>UI_PASS_HASH</code> in <code>config.php</code> to enable it.</p> <p class="muted">Authentication is disabled. Set <code>UI_PASS_HASH</code> in <code>config.php</code> to enable it.</p>
<?php else: ?> <?php else: ?>
<form method="post"> <form method="post">
<input type="hidden" name="action" value="password">
<label>Current password <label>Current password
<input type="password" name="current_password" autocomplete="current-password" required> <input type="password" name="current_password" autocomplete="current-password" required>
</label> </label>
@@ -74,6 +91,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php endif; ?> <?php endif; ?>
</section> </section>
<section class="card">
<h2>Data retention</h2>
<p class="muted" style="margin-bottom:1rem;font-size:.85rem">
Connections older than the retention period are removed by the daily purge cron.
</p>
<form method="post">
<input type="hidden" name="action" value="retention">
<label>Retention period <small>(days)</small>
<input type="number" name="retention_days" min="1" max="3650"
value="<?= $retention_days ?>" style="width:120px">
</label>
<button type="submit">Save</button>
</form>
</section>
</main> </main>
<?php include __DIR__ . '/includes/footer.php'; ?> <?php include __DIR__ . '/includes/footer.php'; ?>
</body> </body>

View File

@@ -41,4 +41,14 @@ if ($row > 0) {
echo "Dropped legacy uq_event unique key.\n"; echo "Dropped legacy uq_event unique key.\n";
} }
// Migration: create settings table and seed default retention if upgrading
$pdo->exec(
"CREATE TABLE IF NOT EXISTS settings (
key_name VARCHAR(64) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (key_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$pdo->exec("INSERT IGNORE INTO settings (key_name, value) VALUES ('retention_days', '7')");
echo "Database '" . DB_NAME . "' and tables created/migrated successfully.\n"; echo "Database '" . DB_NAME . "' and tables created/migrated successfully.\n";

View File

@@ -1,2 +1,2 @@
<?php <?php
define('APP_VERSION', '2603.6'); define('APP_VERSION', '2603.8');