From d0da6cfb7445dee387e0b105a0afde7180fee9c0 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Thu, 11 Sep 2025 10:46:46 +0000 Subject: [PATCH] added new session-watcher to bin folder --- bin/session-watcher.php | 267 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 bin/session-watcher.php diff --git a/bin/session-watcher.php b/bin/session-watcher.php new file mode 100644 index 000000000..accecf4af --- /dev/null +++ b/bin/session-watcher.php @@ -0,0 +1,267 @@ +#!/usr/bin/env php + 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 +}