added new session-watcher to bin folder

This commit is contained in:
Luca Haid
2025-09-11 10:46:46 +00:00
parent 865992c6a0
commit d0da6cfb74

267
bin/session-watcher.php Normal file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env php
<?php
require_once('../config/config.php');
// --- DATABASE CONFIGURATION ---
const DB_HOST = FRONKDB_DBHOST;
const DB_NAME = FRONKDB_DBNAME;
const DB_USER = FRONKDB_DBUSER;
const DB_PASS = FRONKDB_DBPASS;
// --- General & Cache Configuration ---
const SESSION_CACHE_TTL = 600; // Cache Session ID lookups for 10 minutes
const ROW_SEPARATOR_INTERVAL = 10; // Draw a divider every 10 rows
const HEADER_REDRAW_INTERVAL = 30; // Used only in --simple mode
const IGNORED_EXTENSIONS = ['png','jpg','jpeg','gif','css','js','ico','svg','woff','woff2','json','xml','txt'];
const LOG_REGEX = '/^(\S+) \S+ \S+ \[(.+?)\] "(GET|POST|PUT|DELETE|HEAD|OPTIONS)\s(.*?)\s\S+" (\d{3}) \S+ "(.*?)" ".*?" "(.*?)"$/';
// --- Dynamic Layout Configuration ---
const FIXED_WIDTH_COLS = ['time' => 10, 'status' => 8, 'method' => 8];
const FLEX_WIDTH_RATIOS = ['identity' => 0.25, 'req_url' => 0.50, 'ref_view' => 0.25];
const COLORS = [
'green' => "\033[1;32m", 'cyan' => "\033[1;36m", 'yellow' => "\033[1;33m", 'red' => "\033[1;31m",
'magenta' => "\033[1;35m", 'blue' => "\033[0;34m", 'dim' => "\033[2m", 'reset' => "\033[0m"
];
// --- Global State ---
$pdo = null;
$sessionCache = [];
$logBuffer = [];
$columnWidths = [];
$displayableRows = 0;
$simpleMode = false;
// --- Core & Helper Functions ---
function get_terminal_dimensions(): array {
$width = (int)@shell_exec('tput cols');
$height = (int)@shell_exec('tput lines');
if ($width > 0 && $height > 0) {
return ['width' => $width, 'height' => $height];
}
// Fallback for environments where tput fails
return ['width' => 160, 'height' => 40];
}
function calculate_layout_sizes(): void {
global $columnWidths, $displayableRows;
$dims = get_terminal_dimensions();
$terminalWidth = $dims['width'];
$terminalHeight = $dims['height'];
$displayableRows = $terminalHeight - 5;
$fixedTotalWidth = array_sum(FIXED_WIDTH_COLS);
$numCols = count(FIXED_WIDTH_COLS) + count(FLEX_WIDTH_RATIOS);
$borderWidth = $numCols + 1;
$flexTotalWidth = $terminalWidth - $fixedTotalWidth - $borderWidth;
$columnWidths = FIXED_WIDTH_COLS;
foreach (FLEX_WIDTH_RATIOS as $name => $ratio) {
$columnWidths[$name] = (int)floor($flexTotalWidth * $ratio);
}
}
function db_connect(): ?PDO {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4";
try {
return new PDO($dsn, 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) { return null; }
}
function getIdentityFromSession(string $sessionId): ?array {
global $pdo, $sessionCache;
$now = time();
if (isset($sessionCache[$sessionId]) && ($now - $sessionCache[$sessionId]['timestamp'] < SESSION_CACHE_TTL)) {
return $sessionCache[$sessionId]['data'];
}
try {
$stmt = $pdo->prepare("SELECT w.name, a.company FROM Worker w LEFT JOIN Address a ON w.address_id = a.id WHERE w.sessionid = ? LIMIT 1");
$stmt->execute([$sessionId]);
$result = $stmt->fetch();
$sessionCache[$sessionId] = ['data' => $result ?: null, 'timestamp' => $now];
return $result ?: null;
} catch (PDOException $e) { return null; }
}
function getControllerFromUrl(string $url): string {
if ($url === '[direct]' || $url === '-') return '[direct]';
$path = parse_url($url, PHP_URL_PATH);
if (empty($path) || $path === '/') return '[root]';
$parts = explode('/', trim($path, '/'));
return ucfirst($parts[0]);
}
function getStatusColor(int $statusCode): string {
if ($statusCode >= 500) return 'red';
if ($statusCode >= 400) return 'yellow';
if ($statusCode >= 300) return 'cyan';
return 'green';
}
function parseLogLine(string $line): ?array {
if (!preg_match(LOG_REGEX, $line, $matches)) return null;
$url = $matches[4];
$ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
if (in_array($ext, IGNORED_EXTENSIONS, true)) return null;
$ts = DateTime::createFromFormat('d/M/Y:H:i:s O', $matches[2]);
return ["ip" => $matches[1], "timestamp" => $ts ? $ts->format('H:i:s') : '??:??:??', "method" => $matches[3],
"url" => $url, "status" => (int)$matches[5], "referrer" => $matches[6], "sessionid" => $matches[7]];
}
// --- UI Drawing Functions ---
function draw_divider(string $left, string $mid, string $right): void {
global $columnWidths;
echo COLORS['dim'];
$cols = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view'];
echo $left;
foreach ($cols as $i => $key) {
echo str_repeat('─', $columnWidths[$key]);
echo ($i < count($cols) - 1) ? $mid : '';
}
echo $right . PHP_EOL . COLORS['reset'];
}
/**
* A fully multi-byte aware function to format cell content with padding, truncation, and color.
*/
function format_cell(string $text, int $width, string $color = ''): string {
$text = ' ' . $text; // Add leading space
// mb_strimwidth is width-aware, not just character count-aware
$visible_width = mb_strwidth($text, 'UTF-8');
if ($visible_width > $width) {
// Truncate and add "..." suffix
$text = mb_strimwidth($text, 0, $width - 4, ' ...', 'UTF-8');
}
// Manual padding because str_pad is not multi-byte safe
$final_width = mb_strwidth($text, 'UTF-8');
$padding = $width - $final_width;
$padded_text = $text . str_repeat(' ', $padding > 0 ? $padding : 0);
return ($color ? COLORS[$color] : '') . $padded_text . ($color ? COLORS['reset'] : '');
}
function draw_header(string $logFile, bool $isRedraw = false): void {
global $simpleMode;
if (!$simpleMode && !$isRedraw) {
// Only clear screen in normal mode on initial draw
echo "\033[H\033[J";
}
$title = basename($logFile);
$mode_indicator = $simpleMode ? " [Compatibility Mode]" : "";
echo ($isRedraw ? PHP_EOL : '') . COLORS['magenta'] . " watching {$title}{$mode_indicator} | Press Ctrl+C to exit" . COLORS['reset'] . PHP_EOL;
draw_divider('┌', '┬', '┐');
$headers = [' IDENTITY', ' TIME', ' STATUS', ' METHOD', ' REQUEST URL', ' FROM VIEW'];
$keys = ['identity', 'time', 'status', 'method', 'req_url', 'ref_view'];
echo COLORS['dim'] . '│' . COLORS['magenta'];
foreach ($keys as $i => $key) {
echo str_pad($headers[$i], $GLOBALS['columnWidths'][$key]) . COLORS['dim'] . '│' . COLORS['magenta'];
}
echo COLORS['reset'] . PHP_EOL;
draw_divider('├', '┼', '┤');
}
function redraw_screen(string $logFile): void {
global $logBuffer;
echo "\033[H\033[J"; // Move cursor to top left and clear the screen
draw_header($logFile, true);
foreach ($logBuffer as $index => $data) {
echo COLORS['dim'] . '│';
echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow');
echo COLORS['dim'] . '│';
echo format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']);
echo COLORS['dim'] . '│';
echo format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status']));
echo COLORS['dim'] . '│';
echo format_cell($data['method'], $GLOBALS['columnWidths']['method']);
echo COLORS['dim'] . '│';
echo format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green');
echo COLORS['dim'] . '│';
echo format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue');
echo COLORS['dim'] . '│' . PHP_EOL;
if (($index + 1) % ROW_SEPARATOR_INTERVAL === 0 && ($index + 1) < count($logBuffer)) {
draw_divider('├', '┼', '┤');
}
}
draw_divider('└', '┴', '┘');
}
// --- Main Application ---
if (!extension_loaded('mbstring')) { die("Error: The 'mbstring' PHP extension is required. Please install it (e.g., sudo apt install php-mbstring).\n"); }
if (function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, function () { echo "\nWatcher stopped.\n"; exit; }); }
// Check for --simple compatibility mode flag
$simpleMode = in_array('--simple', $argv);
calculate_layout_sizes();
$logFile = $argv[1] ?? '/var/log/apache2/access.log';
if ($logFile === '--simple') $logFile = '/var/log/apache2/access.log';
if (!is_readable($logFile)) { die("Error: Log file not readable: $logFile\n"); }
$pdo = db_connect();
if (!$pdo) { exit(1); }
$handle = fopen($logFile, 'r');
$position = filesize($logFile);
fseek($handle, $position);
$lineCounter = 0;
draw_header($logFile);
while (true) {
clearstatcache(true, $logFile);
$currentSize = filesize($logFile);
if ($currentSize < $position) { fseek($handle, 0); $position = 0; $logBuffer = []; }
if ($currentSize > $position) {
fseek($handle, $position);
$newContent = fread($handle, $currentSize - $position);
$position = $currentSize;
$hasNewData = false;
foreach (explode(PHP_EOL, trim($newContent)) as $line) {
if (empty($line)) continue;
$data = parseLogLine($line);
if ($data === null || $data['sessionid'] === '-' || $data['method'] === 'OPTIONS') continue;
$identity = getIdentityFromSession($data['sessionid']);
if ($identity === null) continue;
$hasNewData = true;
$data['identity'] = $identity['name'] . (!empty($identity['company']) ? " ({$identity['company']})" : '');
$data['referrer_view'] = getControllerFromUrl($data['referrer']);
if ($simpleMode) {
// Simple mode: just print the new row
echo COLORS['dim'] . '│';
echo format_cell($data['identity'], $GLOBALS['columnWidths']['identity'], 'yellow');
echo COLORS['dim'] . '│' . format_cell($data['timestamp'], $GLOBALS['columnWidths']['time']);
echo COLORS['dim'] . '│' . format_cell($data['status'], $GLOBALS['columnWidths']['status'], getStatusColor($data['status']));
echo COLORS['dim'] . '│' . format_cell($data['method'], $GLOBALS['columnWidths']['method']);
echo COLORS['dim'] . '│' . format_cell($data['url'], $GLOBALS['columnWidths']['req_url'], 'green');
echo COLORS['dim'] . '│' . format_cell($data['referrer_view'], $GLOBALS['columnWidths']['ref_view'], 'blue');
echo COLORS['dim'] . '│' . PHP_EOL;
$lineCounter++;
if ($lineCounter >= HEADER_REDRAW_INTERVAL) { draw_header($logFile, true); $lineCounter = 0; }
} else {
// Normal mode: add to buffer for redrawing
$logBuffer[] = $data;
if (count($logBuffer) > $displayableRows) { array_shift($logBuffer); }
}
}
if ($hasNewData && !$simpleMode) {
redraw_screen($logFile);
}
}
usleep(250000); // 0.25 seconds
}