diff --git a/Panel/agent_event_receiver.php b/Panel/agent_event_receiver.php new file mode 100644 index 00000000..bf83cf4d --- /dev/null +++ b/Panel/agent_event_receiver.php @@ -0,0 +1,43 @@ + false, 'error' => 'method_not_allowed')); +} + +$db = createDatabaseConnection($db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix, isset($db_port) ? $db_port : NULL); +if (!$db instanceof OGPDatabase) { + gsp_agent_event_fail(503, 'database_unavailable', 'Could not connect to database.'); +} + +$raw_body = file_get_contents('php://input'); +if ($raw_body === false || strlen($raw_body) > 65536) { + gsp_agent_event_fail(400, 'invalid_body', 'Invalid or oversized request body.'); +} + +$remote = gsp_agent_event_authenticate($db, $raw_body); +$event = json_decode($raw_body, true); +if (!is_array($event)) { + gsp_agent_event_fail(400, 'invalid_json', 'Request body is not valid JSON.'); +} + +$header_remote_id = (int)gsp_agent_event_header('X-GSP-Agent-Id'); +$event_remote_id = isset($event['remote_server_id']) && is_numeric($event['remote_server_id']) ? (int)$event['remote_server_id'] : $header_remote_id; +if ($event_remote_id !== $header_remote_id) { + gsp_agent_event_fail(403, 'remote_id_mismatch', 'Payload remote server does not match authenticated agent.'); +} +$event['remote_server_id'] = $header_remote_id; +$event['source'] = 'agent'; + +$errors = gsp_agent_event_validate($event); +if ($errors) { + gsp_agent_event_fail(400, 'validation_failed', implode('; ', $errors)); +} + +$result = gsp_agent_event_insert($db, $event, $remote); +gsp_agent_event_json_response(200, $result); diff --git a/Panel/includes/agent_event_log.php b/Panel/includes/agent_event_log.php new file mode 100644 index 00000000..696a7b23 --- /dev/null +++ b/Panel/includes/agent_event_log.php @@ -0,0 +1,192 @@ + false, 'error' => $code)); +} + +function gsp_agent_event_text($value, $max = 255) { + $value = is_scalar($value) ? (string)$value : ''; + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value); + return substr($value, 0, $max); +} + +function gsp_agent_event_validate($event) { + $errors = array(); + if (!is_array($event)) { + return array('payload must be an object'); + } + if (empty($event['event_uuid']) || !preg_match('/^[A-Za-z0-9._:-]{16,64}$/', (string)$event['event_uuid'])) { + $errors[] = 'invalid event_uuid'; + } + if (empty($event['timestamp_utc'])) { + $errors[] = 'missing timestamp_utc'; + } + if (empty($event['event_type']) || !in_array($event['event_type'], gsp_agent_event_allowed_types(), true)) { + $errors[] = 'invalid event_type'; + } + if (empty($event['severity']) || !in_array($event['severity'], gsp_agent_event_allowed_severities(), true)) { + $errors[] = 'invalid severity'; + } + if (isset($event['remote_server_id']) && $event['remote_server_id'] !== '' && !is_numeric($event['remote_server_id'])) { + $errors[] = 'invalid remote_server_id'; + } + if (isset($event['home_id']) && $event['home_id'] !== '' && !is_numeric($event['home_id'])) { + $errors[] = 'invalid home_id'; + } + return $errors; +} + +function gsp_agent_event_authenticate($db, $raw_body) { + $remote_server_id = gsp_agent_event_header('X-GSP-Agent-Id'); + $timestamp = gsp_agent_event_header('X-GSP-Agent-Timestamp'); + $signature = gsp_agent_event_header('X-GSP-Agent-Signature'); + if (!is_numeric($remote_server_id) || !preg_match('/^\d{10}$/', $timestamp)) { + gsp_agent_event_fail(401, 'auth_headers_missing', 'Missing or invalid agent authentication headers.'); + } + if (abs(time() - (int)$timestamp) > 300) { + gsp_agent_event_fail(401, 'stale_timestamp', 'Agent event timestamp outside allowed request window.'); + } + $remote = $db->getRemoteServer((int)$remote_server_id); + if (!$remote || empty($remote['encryption_key'])) { + gsp_agent_event_fail(401, 'unknown_agent', 'Unknown remote server.'); + } + $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw_body, $remote['encryption_key']); + if (!hash_equals($expected, $signature)) { + gsp_agent_event_fail(401, 'bad_signature', 'Agent signature verification failed.'); + } + return $remote; +} + +function gsp_agent_event_home_belongs_to_remote($db, $home_id, $remote_server_id) { + if (!is_numeric($home_id) || (int)$home_id <= 0) { + return true; + } + $home = $db->getGameHome((int)$home_id); + if (!$home) { + return false; + } + return (int)$home['remote_server_id'] === (int)$remote_server_id; +} + +function gsp_agent_event_format_message($event) { + $name = gsp_agent_event_text($event['game_server_name'] ?? $event['server_name'] ?? $event['game_name'] ?? 'server', 120); + $home_id = isset($event['home_id']) && $event['home_id'] !== '' ? (int)$event['home_id'] : 0; + $port = gsp_agent_event_text($event['game_port'] ?? $event['port'] ?? '', 16); + $pid = gsp_agent_event_text($event['detected_process_pid'] ?? $event['tracked_pid'] ?? $event['pid'] ?? '', 32); + $attempt = gsp_agent_event_text($event['restart_attempt_number'] ?? '', 16); + $base = "Agent event for server '$name'"; + if ($home_id > 0) { + $base .= " (home ID $home_id)"; + } + switch ($event['event_type']) { + case 'server_crash_detected': + return "$base: unexpected stop detected. PID/process/session or required port disappeared."; + case 'server_process_stopped': + return "$base: server process stopped."; + case 'server_process_started': + case 'server_start_confirmed': + return "$base: server start confirmed" . ($pid !== '' ? " with PID $pid" : "") . ($port !== '' ? " and port $port listening" : "") . "."; + case 'server_stop_confirmed': + return "$base: server stop confirmed. Process/session and required port are no longer active."; + case 'server_unresponsive': + return "$base: process/session exists but required game port did not become ready."; + case 'automatic_restart_started': + return "$base: automatic restart attempt" . ($attempt !== '' ? " $attempt" : "") . " started."; + case 'automatic_restart_succeeded': + return "$base: automatic restart succeeded" . ($pid !== '' ? " with PID $pid" : "") . ($port !== '' ? " and port $port listening" : "") . "."; + case 'automatic_restart_failed': + return "$base: automatic restart failed."; + case 'scheduled_restart_started': + return "$base: scheduled restart started."; + case 'scheduled_restart_succeeded': + return "$base: scheduled restart completed successfully."; + case 'scheduled_restart_failed': + return "$base: scheduled restart failed."; + case 'server_port_missing': + case 'server_process_found_without_port': + return "$base: process/session is active but expected port $port is not listening."; + case 'server_port_restored': + return "$base: expected port $port is listening again."; + case 'server_port_found_without_managed_process': + return "$base: expected port $port is listening but no managed process/session was found."; + case 'restart_attempt_limit_reached': + return "$base: restart attempt limit reached."; + case 'agent_event_delivery_failed': + return "Agent event delivery failed on " . gsp_agent_event_text($event['agent_hostname'] ?? 'agent', 120) . "."; + case 'agent_event_queue_replayed': + return "Agent event queue replayed by " . gsp_agent_event_text($event['agent_hostname'] ?? 'agent', 120) . "."; + } + return $base . ': ' . gsp_agent_event_text($event['message'] ?? $event['event_type'], 250); +} + +function gsp_agent_event_insert($db, $event, $remote) { + $remote_server_id = (int)$remote['remote_server_id']; + $home_id = isset($event['home_id']) && is_numeric($event['home_id']) ? (int)$event['home_id'] : null; + if (!gsp_agent_event_home_belongs_to_remote($db, $home_id, $remote_server_id)) { + gsp_agent_event_fail(403, 'home_remote_mismatch', 'Home does not belong to reporting remote server.'); + } + if ($db->loggerEventExists($event['event_uuid'])) { + return array('ok' => true, 'duplicate' => true); + } + $agent_label = gsp_agent_event_text($event['agent_hostname'] ?? $remote['remote_server_name'] ?? $remote['agent_ip'] ?? 'agent', 120); + $actor = trim(gsp_agent_event_text(($event['agent_os'] ?? 'Agent') . ': ' . $agent_label, 120)); + $metadata = $event; + unset($metadata['password'], $metadata['steam_password'], $metadata['encryption_key'], $metadata['database_password']); + $db->loggerEx(gsp_agent_event_format_message($event), array( + 'user_id' => 0, + 'ip' => $agent_label, + 'source_type' => 'agent', + 'category' => 'server_lifecycle', + 'event_type' => gsp_agent_event_text($event['event_type'], 80), + 'severity' => gsp_agent_event_text($event['severity'], 20), + 'remote_server_id' => $remote_server_id, + 'home_id' => $home_id, + 'event_uuid' => gsp_agent_event_text($event['event_uuid'], 64), + 'correlation_id' => gsp_agent_event_text($event['correlation_id'] ?? '', 64), + 'actor' => $actor, + 'metadata_json' => json_encode($metadata) + )); + return array('ok' => true, 'duplicate' => false); +} diff --git a/Panel/includes/database_mysqli.php b/Panel/includes/database_mysqli.php index e61632d9..4f39a773 100644 --- a/Panel/includes/database_mysqli.php +++ b/Panel/includes/database_mysqli.php @@ -3406,26 +3406,147 @@ class OGPDatabaseMySQL extends OGPDatabase public function logger($message){ $user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 0; $client_ip = getClientIPAddress(); - $message = $this->realEscapeSingle($message); - $this->query("INSERT INTO OGP_DB_PREFIXlogger (date, user_id, ip, message) VALUE (FROM_UNIXTIME(UNIX_TIMESTAMP(), '%d-%m-%Y %H:%i:%s'), $user_id, '$client_ip', '$message');"); + $this->loggerEx($message, array( + 'user_id' => $user_id, + 'ip' => $client_ip, + 'source_type' => 'user', + 'category' => 'panel_action', + 'severity' => 'info' + )); } - public function get_logger_count($search_field) { + private function loggerColumnExists($column) { + $column = $this->realEscapeSingle($column); + $result = $this->resultQuery("SHOW COLUMNS FROM `".$this->table_prefix."logger` LIKE '$column'"); + return is_array($result) && count($result) > 0; + } + + public function ensureLoggerExtendedSchema() { + if (!$this->link) return false; + $this->query("ALTER TABLE `".$this->table_prefix."logger` MODIFY `ip` varchar(255) NOT NULL"); + $this->query("ALTER TABLE `".$this->table_prefix."logger` MODIFY `message` varchar(1000) NOT NULL"); + $columns = array( + 'source_type' => "varchar(20) NOT NULL DEFAULT 'user'", + 'category' => "varchar(40) NOT NULL DEFAULT 'panel_action'", + 'event_type' => "varchar(80) DEFAULT NULL", + 'severity' => "varchar(20) NOT NULL DEFAULT 'info'", + 'remote_server_id' => "int(11) DEFAULT NULL", + 'home_id' => "int(11) DEFAULT NULL", + 'event_uuid' => "varchar(64) DEFAULT NULL", + 'correlation_id' => "varchar(64) DEFAULT NULL", + 'actor' => "varchar(120) DEFAULT NULL", + 'metadata_json' => "text DEFAULT NULL" + ); + foreach ($columns as $column => $definition) { + if (!$this->loggerColumnExists($column)) { + $this->query("ALTER TABLE `".$this->table_prefix."logger` ADD `$column` $definition"); + } + } + $indexes = $this->resultQuery("SHOW INDEX FROM `".$this->table_prefix."logger`"); + $index_names = array(); + foreach ((array)$indexes as $index) { + $index_names[$index['Key_name']] = true; + } + if (!isset($index_names['idx_logger_event_uuid'])) { + $this->query("ALTER TABLE `".$this->table_prefix."logger` ADD INDEX `idx_logger_event_uuid` (`event_uuid`)"); + } + if (!isset($index_names['idx_logger_source_category'])) { + $this->query("ALTER TABLE `".$this->table_prefix."logger` ADD INDEX `idx_logger_source_category` (`source_type`,`category`,`severity`)"); + } + if (!isset($index_names['idx_logger_home'])) { + $this->query("ALTER TABLE `".$this->table_prefix."logger` ADD INDEX `idx_logger_home` (`home_id`)"); + } + return true; + } + + public function loggerEx($message, $options = array()){ + $this->ensureLoggerExtendedSchema(); + $user_id = isset($options['user_id']) ? (int)$options['user_id'] : (isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0); + $ip = isset($options['ip']) ? $options['ip'] : getClientIPAddress(); + $source_type = isset($options['source_type']) ? $options['source_type'] : 'user'; + $category = isset($options['category']) ? $options['category'] : 'panel_action'; + $event_type = isset($options['event_type']) ? $options['event_type'] : null; + $severity = isset($options['severity']) ? $options['severity'] : 'info'; + $remote_server_id = isset($options['remote_server_id']) && is_numeric($options['remote_server_id']) ? (int)$options['remote_server_id'] : 'NULL'; + $home_id = isset($options['home_id']) && is_numeric($options['home_id']) ? (int)$options['home_id'] : 'NULL'; + $event_uuid = isset($options['event_uuid']) ? $options['event_uuid'] : null; + $correlation_id = isset($options['correlation_id']) ? $options['correlation_id'] : null; + $actor = isset($options['actor']) ? $options['actor'] : null; + $metadata_json = isset($options['metadata_json']) ? $options['metadata_json'] : null; + + $message = substr((string)$message, 0, 1000); + $ip = substr((string)$ip, 0, 255); + $source_type = substr((string)$source_type, 0, 20); + $category = substr((string)$category, 0, 40); + $event_type_sql = $event_type === null ? 'NULL' : "'".$this->realEscapeSingle(substr((string)$event_type, 0, 80))."'"; + $severity = substr((string)$severity, 0, 20); + $event_uuid_sql = $event_uuid === null ? 'NULL' : "'".$this->realEscapeSingle(substr((string)$event_uuid, 0, 64))."'"; + $correlation_id_sql = $correlation_id === null ? 'NULL' : "'".$this->realEscapeSingle(substr((string)$correlation_id, 0, 64))."'"; + $actor_sql = $actor === null ? 'NULL' : "'".$this->realEscapeSingle(substr((string)$actor, 0, 120))."'"; + $metadata_sql = $metadata_json === null ? 'NULL' : "'".$this->realEscapeSingle($metadata_json)."'"; + $message = $this->realEscapeSingle($message); + $ip = $this->realEscapeSingle($ip); + $source_type = $this->realEscapeSingle($source_type); + $category = $this->realEscapeSingle($category); + $severity = $this->realEscapeSingle($severity); + + $this->query("INSERT INTO `".$this->table_prefix."logger` + (date, user_id, ip, message, source_type, category, event_type, severity, remote_server_id, home_id, event_uuid, correlation_id, actor, metadata_json) + VALUE (FROM_UNIXTIME(UNIX_TIMESTAMP(), '%d-%m-%Y %H:%i:%s'), $user_id, '$ip', '$message', '$source_type', '$category', $event_type_sql, '$severity', $remote_server_id, $home_id, $event_uuid_sql, $correlation_id_sql, $actor_sql, $metadata_sql);"); + } + + public function loggerEventExists($event_uuid) { + $this->ensureLoggerExtendedSchema(); + $event_uuid = $this->realEscapeSingle($event_uuid); + $result = $this->resultQuery("SELECT log_id FROM `".$this->table_prefix."logger` WHERE event_uuid = '$event_uuid' LIMIT 1"); + return is_array($result) && count($result) > 0; + } + + private function buildLoggerWhere($search_field, $filters = array()) { + $this->ensureLoggerExtendedSchema(); + $where = array(); $search_field = $this->realEscapeSingle($search_field); - $sql = "SELECT COUNT(1) AS total FROM ".$this->table_prefix."logger "; - if (!empty($search_field)) { - $sql .= "WHERE ip = '$search_field' OR message LIKE '%$search_field%' - OR user_id IN (SELECT user_id FROM `".$this->table_prefix."users` WHERE users_login LIKE '%$search_field%')"; + $where[] = "(ip = '$search_field' OR message LIKE '%$search_field%' + OR actor LIKE '%$search_field%' + OR event_type LIKE '%$search_field%' + OR user_id IN (SELECT user_id FROM `".$this->table_prefix."users` WHERE users_login LIKE '%$search_field%'))"; + } + foreach (array('source_type','category','severity') as $field) { + if (isset($filters[$field]) && $filters[$field] !== '' && $filters[$field] !== 'all') { + $value = $this->realEscapeSingle($filters[$field]); + $where[] = "$field = '$value'"; + } + } + if (isset($filters['user_id']) && is_numeric($filters['user_id'])) { + $where[] = "user_id = ".(int)$filters['user_id']; + } + if (isset($filters['remote_server_id']) && is_numeric($filters['remote_server_id'])) { + $where[] = "remote_server_id = ".(int)$filters['remote_server_id']; + } + if (isset($filters['home_id']) && is_numeric($filters['home_id'])) { + $where[] = "home_id = ".(int)$filters['home_id']; + } + if (isset($filters['date_from']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $filters['date_from'])) { + $date = $this->realEscapeSingle($filters['date_from']); + $where[] = "STR_TO_DATE(date, '%d-%m-%Y %H:%i:%s') >= '$date 00:00:00'"; + } + if (isset($filters['date_to']) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $filters['date_to'])) { + $date = $this->realEscapeSingle($filters['date_to']); + $where[] = "STR_TO_DATE(date, '%d-%m-%Y %H:%i:%s') <= '$date 23:59:59'"; } + return count($where) ? " WHERE ".implode(" AND ", $where) : ""; + } + + public function get_logger_count($search_field, $filters = array()) { + $sql = "SELECT COUNT(1) AS total FROM ".$this->table_prefix."logger "; + $sql .= $this->buildLoggerWhere($search_field, $filters); return $this->resultQuery($sql); } - public function read_logger($page,$limit, $search_field) { - $search_field = $this->realEscapeSingle($search_field); - + public function read_logger($page,$limit, $search_field, $filters = array()) { $log_id = ($page - 1) * $limit; if(!is_numeric($log_id) || !is_numeric($limit)){ @@ -3433,11 +3554,7 @@ class OGPDatabaseMySQL extends OGPDatabase } $sql = "SELECT * FROM ".$this->table_prefix."logger "; - - if (!empty($search_field)) { - $sql .= "WHERE ip = '$search_field' OR message LIKE '%$search_field%' - OR user_id IN (SELECT user_id FROM `".$this->table_prefix."users` WHERE users_login LIKE '%$search_field%') "; - } + $sql .= $this->buildLoggerWhere($search_field, $filters); $sql .= "ORDER BY log_id DESC LIMIT $log_id, $limit;"; diff --git a/Panel/includes/lib_remote.php b/Panel/includes/lib_remote.php index a810138a..9522e1d3 100644 --- a/Panel/includes/lib_remote.php +++ b/Panel/includes/lib_remote.php @@ -746,11 +746,11 @@ class OGPRemoteLibrary public function remote_restart_server($home_id,$server_ip,$server_port, $control_protocol,$control_password,$control_type, - $home_path,$server_exe,$run_dir,$cmd,$cpu,$nice,$preStart, $envVars, $game_key, $console_log = "") + $home_path,$server_exe,$run_dir,$cmd,$cpu,$nice,$preStart, $envVars, $game_key, $console_log = "", $restart_reason = "") { $params_array = $this->encrypt_params($home_id,$server_ip,$server_port, $control_protocol,$control_password,$control_type, - $home_path,$server_exe,$run_dir,$cmd,$cpu,$nice,$preStart,$envVars, $game_key, $console_log); + $home_path,$server_exe,$run_dir,$cmd,$cpu,$nice,$preStart,$envVars, $game_key, $console_log, $restart_reason); $this->add_enc_chk($params_array); $request = xmlrpc_encode_request("restart_server", $params_array); diff --git a/Panel/modules/addonsmanager/server_content_actions.php b/Panel/modules/addonsmanager/server_content_actions.php index 66e29001..4265b278 100644 --- a/Panel/modules/addonsmanager/server_content_actions.php +++ b/Panel/modules/addonsmanager/server_content_actions.php @@ -433,6 +433,7 @@ function server_content_restart_home($home_id, $options = array()) } sleep($delay); } + $restart_reason = (isset($options['triggered_by']) && $options['triggered_by'] === 'scheduler') ? 'scheduled_restart' : 'panel_restart'; $remote_retval = $remote->remote_restart_server( $home_info['home_id'], @@ -450,7 +451,8 @@ function server_content_restart_home($home_id, $options = array()) $preStart, $envVars, $server_xml->game_key, - (isset($server_xml->console_log) ? $server_xml->console_log : "") + (isset($server_xml->console_log) ? $server_xml->console_log : ""), + $restart_reason ); if ($remote_retval !== 1) { return server_content_result('restart_required', 'Update completed but automatic restart failed.', array( diff --git a/Panel/modules/administration/module.php b/Panel/modules/administration/module.php index 26a14032..6ee79900 100644 --- a/Panel/modules/administration/module.php +++ b/Panel/modules/administration/module.php @@ -46,8 +46,20 @@ $install_queries[1] = array( `log_id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `date` varchar(20) NOT NULL, `user_id` int(11) NOT NULL, - `ip` varchar(15) NOT NULL, - `message` varchar(250) NOT NULL + `ip` varchar(255) NOT NULL, + `message` varchar(1000) NOT NULL, + `source_type` varchar(20) NOT NULL DEFAULT 'user', + `category` varchar(40) NOT NULL DEFAULT 'panel_action', + `event_type` varchar(80) DEFAULT NULL, + `severity` varchar(20) NOT NULL DEFAULT 'info', + `remote_server_id` int(11) DEFAULT NULL, + `home_id` int(11) DEFAULT NULL, + `event_uuid` varchar(64) DEFAULT NULL, + `correlation_id` varchar(64) DEFAULT NULL, + `actor` varchar(120) DEFAULT NULL, + `metadata_json` text DEFAULT NULL, + KEY `idx_logger_event_uuid` (`event_uuid`), + KEY `idx_logger_source_category` (`source_type`,`category`,`severity`), + KEY `idx_logger_home` (`home_id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;"); ?> - diff --git a/Panel/modules/administration/watch_logger.php b/Panel/modules/administration/watch_logger.php index b60e8763..6920c191 100644 --- a/Panel/modules/administration/watch_logger.php +++ b/Panel/modules/administration/watch_logger.php @@ -29,6 +29,13 @@ function exec_ogp_module() { $search_field = (isset($_GET['search']) && !empty($_GET['search'])) ? $_GET['search'] : false; $p = (isset($_GET['page']) && (int)$_GET['page'] > 0) ? (int)$_GET['page'] : 1; $l = (isset($_GET['limit']) && (int)$_GET['limit'] > 0) ? (int)$_GET['limit'] : 10; + $filters = array(); + foreach (array('source_type','category','severity','user_id','remote_server_id','home_id','date_from','date_to') as $filter_key) { + if (isset($_GET[$filter_key]) && $_GET[$filter_key] !== '' && $_GET[$filter_key] !== 'all') { + $filters[$filter_key] = $_GET[$filter_key]; + } + } + $db->ensureLoggerExtendedSchema(); if(hasValue($loggedInUserInfo) && is_array($loggedInUserInfo) && $loggedInUserInfo["users_page_limit"] && !(isset($_GET['limit']) and !empty($_GET['limit']))){ $l = $loggedInUserInfo["users_page_limit"]; @@ -36,14 +43,19 @@ function exec_ogp_module() { echo "

".get_lang('watch_logger')."

"; - $logs = $db->read_logger($p, $l, $search_field); + $logs = $db->read_logger($p, $l, $search_field, $filters); - if (empty($logs) && !empty($search_field)) { - print_failure(get_lang_f('no_results_found', htmlentities($search_field ?? '', ENT_QUOTES, 'UTF-8'))); + if (empty($logs) && (!empty($search_field) || !empty($filters))) { + print_failure(get_lang_f('no_results_found', htmlentities($search_field ?? 'selected filters', ENT_QUOTES, 'UTF-8'))); $view->refresh("?m=administration&p=watch_logger", 5); return; } + $query_params = array_merge(array('m' => 'administration', 'p' => 'watch_logger', 'limit' => $l), $filters); + if (!empty($search_field)) { + $query_params['search'] = $search_field; + } + $base_query = http_build_query($query_params); ?> @@ -53,7 +65,27 @@ function exec_ogp_module() {
- + + + + + + + + +
@@ -61,7 +93,7 @@ function exec_ogp_module() {
- 10 / 20 / 50 / 100 + 10))), ENT_QUOTES, 'UTF-8'); ?>'>10 / 20))), ENT_QUOTES, 'UTF-8'); ?>'>20 / 50))), ENT_QUOTES, 'UTF-8'); ?>'>50 / 100))), ENT_QUOTES, 'UTF-8'); ?>'>100 @@ -74,6 +106,9 @@ function exec_ogp_module() { + Type + Severity + Server @@ -100,10 +135,23 @@ function exec_ogp_module() { foreach ((array)$logs as $log) { $user = $db->getUserById($log['user_id']); - $when = $log['date']; - $who = $user['users_login']; - $where = $log['ip']; - $what = $log['message']; + $user = is_array($user) ? $user : array(); + $when = htmlentities($log['date'] ?? '', ENT_QUOTES, 'UTF-8'); + $who = isset($log['actor']) && $log['actor'] !== '' ? $log['actor'] : ($user['users_login'] ?? 'System'); + $who = htmlentities($who, ENT_QUOTES, 'UTF-8'); + $where = htmlentities($log['ip'] ?? '', ENT_QUOTES, 'UTF-8'); + $what = htmlentities($log['message'] ?? '', ENT_QUOTES, 'UTF-8'); + $source = htmlentities(strtoupper($log['source_type'] ?? 'USER'), ENT_QUOTES, 'UTF-8'); + $category = htmlentities($log['category'] ?? '', ENT_QUOTES, 'UTF-8'); + $severity = htmlentities(strtoupper($log['severity'] ?? 'INFO'), ENT_QUOTES, 'UTF-8'); + $server_ref = ''; + if (!empty($log['home_id'])) { + $server_ref .= 'Home '.$log['home_id']; + } + if (!empty($log['remote_server_id'])) { + $server_ref .= ($server_ref ? ' / ' : '').'Agent '.$log['remote_server_id']; + } + $server_ref = htmlentities($server_ref, ENT_QUOTES, 'UTF-8'); $log_id = $log['log_id']; // Template echo "\n". @@ -118,18 +166,30 @@ function exec_ogp_module() { "$when\n". "$who\n". "$where\n". + "$source
$category\n". + "$severity\n". + "$server_ref\n". "$what\n". "\n"; echo "\n". - "\n". + "\n". "\n"; $show_values = array( "users_login", "users_lang", "users_role", "users_email", "user_expires"); foreach ((array)$user as $key => $value) { if( in_array( $key, $show_values ) ) - echo "\n"; + echo "\n"; + } + if (!empty($log['event_type'])) { + echo "\n"; + } + if (!empty($log['correlation_id'])) { + echo "\n"; + } + if (!empty($log['metadata_json'])) { + echo "\n"; } echo "\n". "\n". @@ -139,13 +199,9 @@ function exec_ogp_module() { echo "\n"; echo "\n"; echo "
".str_replace("_", "", substr($key,5))."$value
".htmlentities(str_replace("_", "", substr($key,5)), ENT_QUOTES, 'UTF-8')."".htmlentities($value, ENT_QUOTES, 'UTF-8')."
event type".htmlentities($log['event_type'], ENT_QUOTES, 'UTF-8')."
correlation id".htmlentities($log['correlation_id'], ENT_QUOTES, 'UTF-8')."
details
".htmlentities($log['metadata_json'], ENT_QUOTES, 'UTF-8')."
\n"; - $count_logs = $db->get_logger_count($search_field); + $count_logs = $db->get_logger_count($search_field, $filters); - if (isset($_GET['search']) && !empty($_GET['search'])) { - $uri = '?m=administration&p=watch_logger&search='.$_GET['search'].'&limit='.$l.'&page='; - } else { - $uri = '?m=administration&p=watch_logger&limit='.$l.'&page='; - } + $uri = '?'.$base_query.'&page='; echo paginationPages($count_logs[0]['total'], $p, $l, $uri, 3, 'watchLogger'); } ?> diff --git a/Panel/modules/cron/shared_cron_functions.php b/Panel/modules/cron/shared_cron_functions.php index 9a21901c..b55ef723 100644 --- a/Panel/modules/cron/shared_cron_functions.php +++ b/Panel/modules/cron/shared_cron_functions.php @@ -158,7 +158,8 @@ function build_cron_scheduler_command($panelURL, $token, $game_home, $action) { case "start": return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/start&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1"; case "restart": - return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/restart&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}\" --no-check-certificate > /dev/null 2>&1"; + $options_json = urlencode(json_encode(array('triggered_by' => 'scheduler'))); + return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/restart&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}&options={$options_json}\" --no-check-certificate > /dev/null 2>&1"; case "steam_auto_update": return "wget -qO- \"{$panelURL}/ogp_api.php?gamemanager/update&token={$token}&ip={$ip}&port={$port}&mod_key={$mod_key}&type=steam\" --no-check-certificate > /dev/null 2>&1"; } diff --git a/Panel/ogp_api.php b/Panel/ogp_api.php index eaa2a278..c0c147c1 100644 --- a/Panel/ogp_api.php +++ b/Panel/ogp_api.php @@ -1099,6 +1099,16 @@ function api_gamemanager() if($request[0] == "restart") { + $options = array(); + if(isset($_POST['options'])) + { + $decoded_options = json_decode((string)$_POST['options'], true); + if(is_array($decoded_options)) + $options = $decoded_options; + } + if(isset($_POST['triggered_by'])) + $options['triggered_by'] = $_POST['triggered_by']; + $restart_reason = (isset($options['triggered_by']) && $options['triggered_by'] === 'scheduler') ? 'scheduled_restart' : 'panel_restart'; $start_cmd = get_start_cmd($user_info,$remote,$server_xml,$home_info,$mod_id,$ip,$port,$db); // Do text replacements in cfg file if( $server_xml->replace_texts ) @@ -1149,7 +1159,8 @@ function api_gamemanager() $preStart, $envVars, $server_xml->game_key, - (isset( $server_xml->console_log ) ? $server_xml->console_log : "")); + (isset( $server_xml->console_log ) ? $server_xml->console_log : ""), + $restart_reason); if($remote_retval === -1) return array("status" => '333', "message" => "The server could not be restarted."); diff --git a/docs/features/AGENT_ACTIVITY_EVENTS.md b/docs/features/AGENT_ACTIVITY_EVENTS.md new file mode 100644 index 00000000..2ecbdbcc --- /dev/null +++ b/docs/features/AGENT_ACTIVITY_EVENTS.md @@ -0,0 +1,123 @@ +# Agent Activity Events + +GSP can record meaningful agent-detected server lifecycle events in the existing Panel activity log. User and administrator actions are still logged by the Panel as before; agents only report operational outcomes that the Panel cannot know by itself. + +## Transport + +Agents send JSON to: + +```text +Panel/agent_event_receiver.php +``` + +The endpoint authenticates each request with: + +- `X-GSP-Agent-Id`: the Panel `remote_server_id` +- `X-GSP-Agent-Timestamp`: current Unix timestamp +- `X-GSP-Agent-Signature`: `sha256=` plus HMAC-SHA256 of `timestamp.body` + +The HMAC key is the existing remote server encryption key. Agents do not receive database credentials and never write directly to the Panel database. + +## Event Types + +Supported event types: + +- `server_process_stopped` +- `server_process_started` +- `server_crash_detected` +- `server_unresponsive` +- `automatic_restart_started` +- `automatic_restart_succeeded` +- `automatic_restart_failed` +- `scheduled_restart_started` +- `scheduled_restart_succeeded` +- `scheduled_restart_failed` +- `server_stop_confirmed` +- `server_start_confirmed` +- `server_port_missing` +- `server_port_restored` +- `server_process_found_without_port` +- `server_port_found_without_managed_process` +- `restart_attempt_limit_reached` +- `agent_event_delivery_failed` +- `agent_event_queue_replayed` + +Severity values are `info`, `notice`, `warning`, `error`, and `critical`. The receiver also accepts `success` for compatibility with existing UI wording. + +## Payload + +Agents include a UUID, UTC timestamp, type, severity, source, OS, hostname, remote server ID, home ID, path, IP, ports, session name, PID, restart reason, expected and actual states, message, technical details, and correlation ID where available. + +Payloads must not contain passwords, Steam credentials, database credentials, encryption keys, full environment dumps, or sensitive command-line arguments. + +## Validation And Deduplication + +The Panel receiver: + +1. Verifies the HMAC signature. +2. Rejects stale request timestamps. +3. Validates event type and severity. +4. Confirms `home_id` belongs to the reporting remote server when provided. +5. Truncates public fields to safe lengths. +6. Escapes activity-log output. +7. Deduplicates by `event_uuid`. + +## Activity Log Schema + +Existing logger rows remain valid. The database helper extends the logger table idempotently with: + +- `source_type` +- `category` +- `event_type` +- `severity` +- `remote_server_id` +- `home_id` +- `event_uuid` +- `correlation_id` +- `actor` +- `metadata_json` + +Fresh installs receive the extended schema from the administration module. Existing installs are migrated lazily when the logger or receiver is used. + +## Filters + +Administration -> Watch Logger supports combined filters for source, category, severity, user ID, agent ID, home ID, date range, and search text. Pagination preserves the active filters. + +## Lifecycle Notes + +The agents use existing process, screen/session, PID metadata, and port validation. They do not use `SERVER_STOPPED` marker files. + +Normal Panel start/stop/restart button clicks remain Panel-side actions. Agents add confirmed operational outcomes, such as stop confirmation after process and port validation, start confirmation after status polling sees the required port, unexpected offline transitions, unresponsive process states, and scheduled restart outcomes. + +External process kills, such as BEC terminating a DayZ Mod server, are detected when validated status polling observes an `ONLINE -> OFFLINE` transition without a stop hint. That transition is reported as `server_crash_detected`. + +## Offline Queue + +Agents append undelivered events to: + +```text +events/pending-events.jsonl +``` + +Each line is one JSON event. The queue is retried when later events are delivered. The queue is rotated when it exceeds 1 MB. Agent lifecycle work continues even when the Panel is unavailable. + +## Configuration + +Agent `Cfg/Config.pm` should include: + +```perl +agent_event_url => 'https://panel.example.com/agent_event_receiver.php', +remote_server_id => '1', +``` + +If `agent_event_url` is empty, the agent tries to derive the receiver URL from `web_api_url`. The `key` must match the Panel remote server encryption key. + +## Manual Test Plan + +1. Start a server from the Panel and confirm the existing Panel user action still appears. +2. Confirm the agent reports `server_start_confirmed` only after validated status shows the process/session and required port ready. +3. Stop from the Panel and confirm `server_stop_confirmed`. +4. Kill a game process outside the Panel and poll status; confirm `server_crash_detected`. +5. Trigger a scheduled restart and confirm scheduled start/success/failure events. +6. Disconnect the Panel, trigger an event, reconnect, and confirm queued delivery without duplicates. +7. Test Watch Logger filters for agent, lifecycle, warning/error, and home ID.