diff --git a/Agent-Windows/ogp_agent.pl b/Agent-Windows/ogp_agent.pl index c2ea2f00..626112b6 100644 --- a/Agent-Windows/ogp_agent.pl +++ b/Agent-Windows/ogp_agent.pl @@ -340,6 +340,7 @@ my $d = Frontier::Daemon::OGP::Forking->new( fastdl_get_info => \&fastdl_get_info, fastdl_create_config => \&fastdl_create_config, agent_restart => \&agent_restart, + component_update => \&component_update, scheduler_add_task => \&scheduler_add_task, scheduler_del_task => \&scheduler_del_task, scheduler_list_tasks => \&scheduler_list_tasks, @@ -3498,6 +3499,162 @@ sub fastdl_create_config return 1; } +sub component_update_shell_quote +{ + my ($value) = @_; + $value = '' unless defined $value; + $value =~ s/'/'"'"'/g; + return "'" . $value . "'"; +} + +sub component_update_response +{ + my (%data) = @_; + my %encoded = (); + foreach my $key (keys %data) + { + $encoded{$key} = encode_base64(defined $data{$key} ? $data{$key} : '', ''); + } + return \%encoded; +} + +sub component_update_parse_payload +{ + my ($payload) = @_; + my %data = (); + foreach my $line (split(/\r?\n/, $payload || '')) + { + next if $line !~ /^([A-Za-z0-9_]+)=(.*)$/; + $data{$1} = decode_base64($2); + } + return %data; +} + +sub component_update +{ + return component_update_response(success => 0, status => 'bad_encryption', message => 'Bad Encryption Key') + unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); + my ($payload) = decrypt_params(@_); + my %cfg = component_update_parse_payload($payload); + + my $component = $cfg{component} || 'windows_agent'; + if ($component ne 'linux_agent' && $component ne 'windows_agent') + { + return component_update_response(success => 0, status => 'invalid_component', message => 'Invalid component.'); + } + my $repo_url = $cfg{repo_url} || ''; + my $branch = $cfg{branch} || ''; + my $source_path = $cfg{source_path} || ''; + my $install_path = $cfg{install_path} || AGENT_RUN_DIR; + my $git_path = $cfg{git_path} || 'git'; + my $backup_path = $cfg{backup_path} || Path::Class::Dir->new(AGENT_RUN_DIR, 'component_backups')->stringify; + my $post_update_command = $cfg{post_update_command} || ''; + + if ($repo_url !~ m{^(https?://|ssh://|git@|/)} || $repo_url =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_repo', message => 'Invalid repository URL or path.'); + } + if ($branch !~ /^[A-Za-z0-9._\/-]{1,128}$/) + { + return component_update_response(success => 0, status => 'invalid_branch', message => 'Invalid branch.'); + } + if ($source_path eq '' || $source_path =~ /\.\./ || $source_path =~ /[\r\n\0]/ || $source_path !~ /^[A-Za-z0-9._\/-]+$/) + { + return component_update_response(success => 0, status => 'invalid_source_path', message => 'Invalid source path.'); + } + if ($install_path !~ m{^/} || $install_path =~ /\.\./ || $install_path =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_install_path', message => 'Invalid install path.'); + } + if ($post_update_command =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_post_update', message => 'Post-update command must be a single line.'); + } + + my $ts = time(); + my $script_path = Path::Class::File->new(AGENT_RUN_DIR, "gsp_component_update_$ts.sh")->stringify; + my $log_path = Path::Class::File->new(AGENT_RUN_DIR, 'gsp_component_update.log')->stringify; + my $screenrc = SCREENRC_FILE; + my $repo_q = component_update_shell_quote($repo_url); + my $branch_q = component_update_shell_quote($branch); + my $source_q = component_update_shell_quote($source_path); + my $install_q = component_update_shell_quote($install_path); + my $git_q = component_update_shell_quote($git_path); + my $backup_q = component_update_shell_quote($backup_path); + my $post_q = component_update_shell_quote($post_update_command); + my $log_q = component_update_shell_quote($log_path); + my $screenrc_q = component_update_shell_quote($screenrc); + + my $script = <<"EOS"; +#!/usr/bin/env bash +set -u +LOG=$log_q +log(){ printf '[%s] %s\\n' "\$(date '+%Y-%m-%d %H:%M:%S')" "\$*" >> "\$LOG"; } +fail(){ log "FAILED: \$*"; exit 1; } +REPO=$repo_q +BRANCH=$branch_q +SOURCE_REL=$source_q +DEST=$install_q +GIT_BIN=$git_q +BACKUP_BASE=$backup_q +POST_UPDATE=$post_q +TMP="\${TMPDIR:-/tmp}/gsp_agent_update_\$(date +%s)_\$\$" +mkdir -p "\$TMP" "\$BACKUP_BASE" || fail "Cannot create staging or backup directories" +log "queued component=$component repo=\$REPO branch=\$BRANCH source=\$SOURCE_REL dest=\$DEST" +if ! command -v "\$GIT_BIN" >/dev/null 2>&1 && [ ! -x "\$GIT_BIN" ]; then + fail "Git executable not found: \$GIT_BIN" +fi +"\$GIT_BIN" clone --depth 1 --branch "\$BRANCH" "\$REPO" "\$TMP/repo" >> "\$LOG" 2>&1 || fail "git clone failed" +SRC="\$TMP/repo/\$SOURCE_REL" +[ -d "\$SRC" ] || fail "Source component folder missing: \$SRC" +case "\$DEST" in /*) ;; *) fail "Destination must be absolute: \$DEST" ;; esac +mkdir -p "\$DEST" || fail "Cannot create destination: \$DEST" +ARCHIVE="\$BACKUP_BASE/${component}_\$(date +%Y%m%d_%H%M%S).tar.gz" +tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -czf "\$ARCHIVE" -C "\$DEST" . >> "\$LOG" 2>&1 || fail "Backup failed" +if command -v rsync >/dev/null 2>&1; then + rsync -a --exclude='Cfg/' --exclude='ServerFiles/' --exclude='Schedule/' --exclude='logs/' --exclude='screenlogs/' --exclude='steamcmd/' --exclude='startups/' --exclude='tmp/' --exclude='shared/' --exclude='component_backups/' --exclude='*.pid' "\$SRC"/ "\$DEST"/ >> "\$LOG" 2>&1 || fail "rsync copy failed" +else + (cd "\$SRC" && tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -cf - .) | (cd "\$DEST" && tar -xf -) >> "\$LOG" 2>&1 || fail "tar copy failed" +fi +if [ -f "\$DEST/ogp_agent.pl" ]; then + perl -c "\$DEST/ogp_agent.pl" >> "\$LOG" 2>&1 || fail "Updated ogp_agent.pl failed syntax validation" +fi +if [ -n "\$POST_UPDATE" ]; then + (cd "\$DEST" && bash -lc "\$POST_UPDATE") >> "\$LOG" 2>&1 || fail "post-update command failed" +fi +log "copy complete archive=\$ARCHIVE" +sleep 2 +cd "\$DEST" || exit 0 +if [ -f ogp_agent_run.pid ]; then kill "\$(cat ogp_agent_run.pid)" >/dev/null 2>&1 || true; fi +if [ -f ogp_agent.pid ]; then kill "\$(cat ogp_agent.pid)" >/dev/null 2>&1 || true; fi +sleep 2 +if [ -f ogp_agent_run ]; then + screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent bash ogp_agent_run -pidfile ogp_agent_run.pid >> "\$LOG" 2>&1 || true +else + screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent perl ogp_agent.pl >> "\$LOG" 2>&1 || true +fi +log "restart attempted" +rm -rf "\$TMP" +exit 0 +EOS + + my $fh; + if (!open($fh, '>', $script_path)) + { + return component_update_response(success => 0, status => 'write_failed', message => "Cannot write updater script: $!"); + } + print $fh $script; + close($fh); + chmod 0700, $script_path; + system('screen -d -m -t "component_update" -c "' . SCREENRC_FILE . '" -S component_update bash ' . component_update_shell_quote($script_path)); + if ($? != 0) + { + system('bash ' . component_update_shell_quote($script_path) . ' >/dev/null 2>&1 &'); + } + logger "Component update queued for $component from $repo_url branch $branch."; + return component_update_response(success => 1, status => 'queued', message => 'Component update queued on agent.', log_path => $log_path, script_path => $script_path); +} + sub agent_restart { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); diff --git a/Agent_Linux/ogp_agent.pl b/Agent_Linux/ogp_agent.pl index 1a90a487..573269a2 100644 --- a/Agent_Linux/ogp_agent.pl +++ b/Agent_Linux/ogp_agent.pl @@ -408,6 +408,7 @@ my $d = Frontier::Daemon::OGP::Forking->new( fastdl_get_info => \&fastdl_get_info, fastdl_create_config => \&fastdl_create_config, agent_restart => \&agent_restart, + component_update => \&component_update, scheduler_add_task => \&scheduler_add_task, scheduler_del_task => \&scheduler_del_task, scheduler_list_tasks => \&scheduler_list_tasks, @@ -4673,6 +4674,165 @@ sub fastdl_create_config return 1; } +sub component_update_shell_quote +{ + my ($value) = @_; + $value = '' unless defined $value; + $value =~ s/'/'"'"'/g; + return "'" . $value . "'"; +} + +sub component_update_response +{ + my (%data) = @_; + my %encoded = (); + foreach my $key (keys %data) + { + $encoded{$key} = encode_base64(defined $data{$key} ? $data{$key} : '', ''); + } + return \%encoded; +} + +sub component_update_parse_payload +{ + my ($payload) = @_; + my %data = (); + foreach my $line (split(/\r?\n/, $payload || '')) + { + next if $line !~ /^([A-Za-z0-9_]+)=(.*)$/; + $data{$1} = decode_base64($2); + } + return %data; +} + +sub component_update +{ + return component_update_response(success => 0, status => 'bad_encryption', message => 'Bad Encryption Key') + unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); + my ($payload) = decrypt_params(@_); + my %cfg = component_update_parse_payload($payload); + + my $component = $cfg{component} || 'linux_agent'; + if ($component ne 'linux_agent' && $component ne 'windows_agent') + { + return component_update_response(success => 0, status => 'invalid_component', message => 'Invalid component.'); + } + my $repo_url = $cfg{repo_url} || ''; + my $branch = $cfg{branch} || ''; + my $source_path = $cfg{source_path} || ''; + my $install_path = $cfg{install_path} || AGENT_RUN_DIR; + my $git_path = $cfg{git_path} || 'git'; + my $backup_path = $cfg{backup_path} || Path::Class::Dir->new(AGENT_RUN_DIR, 'component_backups')->stringify; + my $post_update_command = $cfg{post_update_command} || ''; + + if ($repo_url !~ m{^(https?://|ssh://|git@|/)} || $repo_url =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_repo', message => 'Invalid repository URL or path.'); + } + if ($branch !~ /^[A-Za-z0-9._\/-]{1,128}$/) + { + return component_update_response(success => 0, status => 'invalid_branch', message => 'Invalid branch.'); + } + if ($source_path eq '' || $source_path =~ /\.\./ || $source_path =~ /[\r\n\0]/ || $source_path !~ /^[A-Za-z0-9._\/-]+$/) + { + return component_update_response(success => 0, status => 'invalid_source_path', message => 'Invalid source path.'); + } + if ($install_path !~ m{^/} || $install_path =~ /\.\./ || $install_path =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_install_path', message => 'Invalid install path.'); + } + if ($post_update_command =~ /[\r\n\0]/) + { + return component_update_response(success => 0, status => 'invalid_post_update', message => 'Post-update command must be a single line.'); + } + + my $ts = time(); + my $script_path = Path::Class::File->new(AGENT_RUN_DIR, "gsp_component_update_$ts.sh")->stringify; + my $log_path = Path::Class::File->new(AGENT_RUN_DIR, 'gsp_component_update.log')->stringify; + my $screenrc = SCREENRC_FILE; + my $repo_q = component_update_shell_quote($repo_url); + my $branch_q = component_update_shell_quote($branch); + my $source_q = component_update_shell_quote($source_path); + my $install_q = component_update_shell_quote($install_path); + my $git_q = component_update_shell_quote($git_path); + my $backup_q = component_update_shell_quote($backup_path); + my $post_q = component_update_shell_quote($post_update_command); + my $log_q = component_update_shell_quote($log_path); + my $screenrc_q = component_update_shell_quote($screenrc); + + my $script = <<"EOS"; +#!/usr/bin/env bash +set -u +LOG=$log_q +log(){ printf '[%s] %s\\n' "\$(date '+%Y-%m-%d %H:%M:%S')" "\$*" >> "\$LOG"; } +fail(){ log "FAILED: \$*"; exit 1; } +REPO=$repo_q +BRANCH=$branch_q +SOURCE_REL=$source_q +DEST=$install_q +GIT_BIN=$git_q +BACKUP_BASE=$backup_q +POST_UPDATE=$post_q +TMP="\${TMPDIR:-/tmp}/gsp_agent_update_\$(date +%s)_\$\$" +mkdir -p "\$TMP" "\$BACKUP_BASE" || fail "Cannot create staging or backup directories" +log "queued component=$component repo=\$REPO branch=\$BRANCH source=\$SOURCE_REL dest=\$DEST" +if ! command -v "\$GIT_BIN" >/dev/null 2>&1 && [ ! -x "\$GIT_BIN" ]; then + fail "Git executable not found: \$GIT_BIN" +fi +"\$GIT_BIN" clone --depth 1 --branch "\$BRANCH" "\$REPO" "\$TMP/repo" >> "\$LOG" 2>&1 || fail "git clone failed" +SRC="\$TMP/repo/\$SOURCE_REL" +[ -d "\$SRC" ] || fail "Source component folder missing: \$SRC" +case "\$DEST" in /*) ;; *) fail "Destination must be absolute: \$DEST" ;; esac +mkdir -p "\$DEST" || fail "Cannot create destination: \$DEST" +ARCHIVE="\$BACKUP_BASE/${component}_\$(date +%Y%m%d_%H%M%S).tar.gz" +tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -czf "\$ARCHIVE" -C "\$DEST" . >> "\$LOG" 2>&1 || fail "Backup failed" +if command -v rsync >/dev/null 2>&1; then + rsync -a --exclude='Cfg/' --exclude='ServerFiles/' --exclude='Schedule/' --exclude='logs/' --exclude='screenlogs/' --exclude='steamcmd/' --exclude='startups/' --exclude='tmp/' --exclude='shared/' --exclude='component_backups/' --exclude='*.pid' "\$SRC"/ "\$DEST"/ >> "\$LOG" 2>&1 || fail "rsync copy failed" +else + (cd "\$SRC" && tar --exclude='./Cfg' --exclude='./ServerFiles' --exclude='./Schedule' --exclude='./logs' --exclude='./screenlogs' --exclude='./steamcmd' --exclude='./startups' --exclude='./tmp' --exclude='./shared' --exclude='./component_backups' --exclude='./*.pid' -cf - .) | (cd "\$DEST" && tar -xf -) >> "\$LOG" 2>&1 || fail "tar copy failed" +fi +if [ -f "\$DEST/ogp_agent.pl" ]; then + perl -c "\$DEST/ogp_agent.pl" >> "\$LOG" 2>&1 || fail "Updated ogp_agent.pl failed syntax validation" +fi +if [ -n "\$POST_UPDATE" ]; then + (cd "\$DEST" && bash -lc "\$POST_UPDATE") >> "\$LOG" 2>&1 || fail "post-update command failed" +fi +log "copy complete archive=\$ARCHIVE" +sleep 2 +if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^ogp_agent\\.service'; then + systemctl restart ogp_agent.service >> "\$LOG" 2>&1 && exit 0 +fi +cd "\$DEST" || exit 0 +if [ -f ogp_agent_run.pid ]; then kill "\$(cat ogp_agent_run.pid)" >/dev/null 2>&1 || true; fi +if [ -f ogp_agent.pid ]; then kill "\$(cat ogp_agent.pid)" >/dev/null 2>&1 || true; fi +sleep 2 +if [ -f ogp_agent_run ]; then + screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent bash ogp_agent_run -pidfile ogp_agent_run.pid >> "\$LOG" 2>&1 || true +else + screen -d -m -t "ogp_agent" -c $screenrc_q -S ogp_agent perl ogp_agent.pl >> "\$LOG" 2>&1 || true +fi +log "restart attempted" +rm -rf "\$TMP" +exit 0 +EOS + + my $fh; + if (!open($fh, '>', $script_path)) + { + return component_update_response(success => 0, status => 'write_failed', message => "Cannot write updater script: $!"); + } + print $fh $script; + close($fh); + chmod 0700, $script_path; + system('screen -d -m -t "component_update" -c "' . SCREENRC_FILE . '" -S component_update bash ' . component_update_shell_quote($script_path)); + if ($? != 0) + { + system('bash ' . component_update_shell_quote($script_path) . ' >/dev/null 2>&1 &'); + } + logger "Component update queued for $component from $repo_url branch $branch."; + return component_update_response(success => 1, status => 'queued', message => 'Component update queued on agent.', log_path => $log_path, script_path => $script_path); +} + sub agent_restart { return "Bad Encryption Key" unless(decrypt_param(pop(@_)) eq "Encryption checking OK"); diff --git a/Panel/includes/lib_remote.php b/Panel/includes/lib_remote.php index 77f32468..ce0c691c 100644 --- a/Panel/includes/lib_remote.php +++ b/Panel/includes/lib_remote.php @@ -991,6 +991,31 @@ class OGPRemoteLibrary return -1; return 1; } + + public function component_update($payload) + { + $args = $this->encryptParam($payload); + $this->add_enc_chk($args); + $request = xmlrpc_encode_request("component_update", $args); + $response = $this->sendRequest($request); + if (is_array($response) && xmlrpc_is_fault($response)) { + return array('success' => false, 'status' => 'xmlrpc_fault', 'message' => isset($response['faultString']) ? $response['faultString'] : 'XML-RPC fault'); + } + if (is_array($response)) { + $data = array(); + foreach ((array)$response as $key => $value) { + $data[$key] = is_string($value) ? base64_decode($value) : $value; + } + if (isset($data['success'])) { + $data['success'] = in_array((string)$data['success'], array('1', 'true', 'yes'), true); + } + return $data; + } + if ($response === NULL) { + return array('success' => false, 'status' => 'agent_unreachable', 'message' => 'No response from agent.'); + } + return array('success' => false, 'status' => 'unexpected_response', 'message' => (string)$response); + } public function scheduler_list_tasks() { diff --git a/Panel/modules/administration/panel_update.php b/Panel/modules/administration/panel_update.php index bb6f0751..7187edca 100644 --- a/Panel/modules/administration/panel_update.php +++ b/Panel/modules/administration/panel_update.php @@ -32,6 +32,11 @@ defined('GSP_DEFAULT_REPO_URL') || define('GSP_DEFAULT_REPO_URL', 'http://forge. defined('GSP_DEFAULT_BRANCH') || define('GSP_DEFAULT_BRANCH', 'Panel-unstable'); defined('GSP_DEFAULT_REPO_ROOT') || define('GSP_DEFAULT_REPO_ROOT', '/var/www/html/GSP'); defined('GSP_DEFAULT_PANEL_PATH') || define('GSP_DEFAULT_PANEL_PATH', '/var/www/html/GSP/Panel'); +defined('GSP_DEFAULT_WEBSITE_PATH') || define('GSP_DEFAULT_WEBSITE_PATH', '/var/www/html/GSP/Website'); +defined('GSP_DEFAULT_PANEL_SOURCE') || define('GSP_DEFAULT_PANEL_SOURCE', 'Panel'); +defined('GSP_DEFAULT_LINUX_AGENT_SOURCE') || define('GSP_DEFAULT_LINUX_AGENT_SOURCE', 'Agent_Linux'); +defined('GSP_DEFAULT_WINDOWS_AGENT_SOURCE') || define('GSP_DEFAULT_WINDOWS_AGENT_SOURCE', 'Agent-Windows'); +defined('GSP_DEFAULT_WEBSITE_SOURCE') || define('GSP_DEFAULT_WEBSITE_SOURCE', 'Website'); $gspPatchManager = GSP_PANEL_DIR . '/modules/update/patch_manager.php'; if (file_exists($gspPatchManager)) { @@ -151,11 +156,23 @@ function gsp_update_settings() global $settings; $repo_root = !empty($settings['gsp_update_repo_root']) ? (string)$settings['gsp_update_repo_root'] : GSP_DEFAULT_REPO_ROOT; $panel_path = !empty($settings['gsp_update_panel_path']) ? (string)$settings['gsp_update_panel_path'] : GSP_DEFAULT_PANEL_PATH; +$website_path = !empty($settings['gsp_update_website_path']) ? (string)$settings['gsp_update_website_path'] : GSP_DEFAULT_WEBSITE_PATH; return [ 'repo_url' => !empty($settings['gsp_update_repo_url']) ? (string)$settings['gsp_update_repo_url'] : GSP_DEFAULT_REPO_URL, 'branch' => !empty($settings['gsp_update_branch']) ? (string)$settings['gsp_update_branch'] : GSP_DEFAULT_BRANCH, 'repo_root' => rtrim($repo_root, '/'), 'panel_path' => rtrim($panel_path, '/'), +'website_path' => rtrim($website_path, '/'), +'panel_source_path' => !empty($settings['gsp_update_panel_source_path']) ? trim((string)$settings['gsp_update_panel_source_path'], '/') : GSP_DEFAULT_PANEL_SOURCE, +'linux_agent_source_path' => !empty($settings['gsp_update_linux_agent_source_path']) ? trim((string)$settings['gsp_update_linux_agent_source_path'], '/') : GSP_DEFAULT_LINUX_AGENT_SOURCE, +'windows_agent_source_path' => !empty($settings['gsp_update_windows_agent_source_path']) ? trim((string)$settings['gsp_update_windows_agent_source_path'], '/') : GSP_DEFAULT_WINDOWS_AGENT_SOURCE, +'website_source_path' => !empty($settings['gsp_update_website_source_path']) ? trim((string)$settings['gsp_update_website_source_path'], '/') : GSP_DEFAULT_WEBSITE_SOURCE, +'git_path' => !empty($settings['gsp_update_git_path']) ? trim((string)$settings['gsp_update_git_path']) : 'git', +'backup_path' => !empty($settings['gsp_update_backup_path']) ? rtrim((string)$settings['gsp_update_backup_path'], '/') : GSP_BACKUP_BASE, +'panel_post_update_command' => !empty($settings['gsp_update_panel_post_update_command']) ? (string)$settings['gsp_update_panel_post_update_command'] : '', +'website_post_update_command' => !empty($settings['gsp_update_website_post_update_command']) ? (string)$settings['gsp_update_website_post_update_command'] : '', +'linux_agent_post_update_command' => !empty($settings['gsp_update_linux_agent_post_update_command']) ? (string)$settings['gsp_update_linux_agent_post_update_command'] : '', +'windows_agent_post_update_command' => !empty($settings['gsp_update_windows_agent_post_update_command']) ? (string)$settings['gsp_update_windows_agent_post_update_command'] : '', 'backup_before_update' => !isset($settings['gsp_update_backup_before']) ? '1' : (string)$settings['gsp_update_backup_before'], ]; } @@ -175,7 +192,7 @@ $errors[] = 'Repository source must be an http(s), ssh, git@ URL, or a safe abso if ($is_local_path && !is_dir($repo_source)) { $errors[] = 'Repository local path does not exist or is not a directory: ' . $repo_source; } -if (!preg_match('/^[A-Za-z0-9._\/-]{1,128}$/', (string)$cfg['branch'])) { +if (!preg_match('/^[A-Za-z0-9._\/-]{1,128}$/', (string)$cfg['branch']) || strpos((string)$cfg['branch'], '..') !== false) { $errors[] = 'Branch/channel contains invalid characters.'; } foreach (['repo_root', 'panel_path'] as $key) { @@ -183,6 +200,25 @@ if (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== fals $errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.'; } } +foreach (['website_path', 'backup_path'] as $key) { +if (isset($cfg[$key]) && (trim((string)$cfg[$key]) === '' || strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], '..') !== false || strpos((string)$cfg[$key], '/') !== 0)) { +$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a safe absolute path.'; +} +} +foreach (['panel_source_path', 'linux_agent_source_path', 'windows_agent_source_path', 'website_source_path'] as $key) { +$value = isset($cfg[$key]) ? trim((string)$cfg[$key], '/') : ''; +if ($value === '' || strpos($value, "\0") !== false || strpos($value, '..') !== false || !preg_match('/^[A-Za-z0-9._\/-]+$/', $value)) { +$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' is invalid.'; +} +} +if (isset($cfg['git_path']) && trim((string)$cfg['git_path']) !== '' && (strpos((string)$cfg['git_path'], "\0") !== false || strpos((string)$cfg['git_path'], "\n") !== false || strpos((string)$cfg['git_path'], "\r") !== false)) { +$errors[] = 'Git executable path is invalid.'; +} +foreach (['panel_post_update_command', 'website_post_update_command', 'linux_agent_post_update_command', 'windows_agent_post_update_command'] as $key) { +if (isset($cfg[$key]) && (strpos((string)$cfg[$key], "\0") !== false || strpos((string)$cfg[$key], "\n") !== false || strpos((string)$cfg[$key], "\r") !== false)) { +$errors[] = ucfirst(str_replace('_', ' ', $key)) . ' must be a single-line admin command.'; +} +} if (rtrim((string)$cfg['panel_path'], '/') !== rtrim((string)$cfg['repo_root'], '/') . '/Panel') { $errors[] = 'Panel Path must point to the Panel folder inside Repository Root.'; } @@ -264,7 +300,7 @@ $cwd = getcwd(); $cwd_real = $cwd ? (realpath($cwd) ?: $cwd) : ''; $root_path = rtrim((string)$update_cfg['repo_root'], '/'); $panel_path = rtrim((string)$update_cfg['panel_path'], '/'); -$website_path = $root_path . '/Website'; +$website_path = !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : ($root_path . '/Website'); $root_real = realpath($root_path) ?: $root_path; $panel_real = realpath($panel_path) ?: $panel_path; $website_real = realpath($website_path) ?: $website_path; @@ -654,14 +690,14 @@ $layout = [ 'cwd' => getcwd() ?: '', 'live_gsp_root' => rtrim((string)$update_cfg['repo_root'], '/'), 'live_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'), -'live_website_path' => rtrim((string)$update_cfg['repo_root'], '/') . '/Website', +'live_website_path' => !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : (rtrim((string)$update_cfg['repo_root'], '/') . '/Website'), 'temporary_git_checkout_path' => $temp_checkout_path, 'source_root' => $source_root_real, 'source_repo_root' => $repo_root, -'source_panel_path' => $repo_root ? ($repo_root . '/Panel') : '', -'source_website_path' => $repo_root ? ($repo_root . '/Website') : '', +'source_panel_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['panel_source_path'], '/')) : '', +'source_website_path' => $repo_root ? ($repo_root . '/' . trim((string)$update_cfg['website_source_path'], '/')) : '', 'destination_panel_path' => rtrim((string)$update_cfg['panel_path'], '/'), -'destination_website_path' => rtrim((string)$update_cfg['repo_root'], '/') . '/Website', +'destination_website_path' => !empty($update_cfg['website_path']) ? rtrim((string)$update_cfg['website_path'], '/') : (rtrim((string)$update_cfg['repo_root'], '/') . '/Website'), ]; $errors = []; @@ -1024,6 +1060,7 @@ function gsp_checkout_update_source(array $update_cfg) { $repo_url = (string)$update_cfg['repo_url']; $branch = (string)$update_cfg['branch']; +$git_bin = !empty($update_cfg['git_path']) ? (string)$update_cfg['git_path'] : 'git'; $temp_dir = sys_get_temp_dir() . '/gsp_git_' . time() . '_' . mt_rand(1000, 9999); if (!@mkdir($temp_dir, 0750, true)) { return ['success' => false, 'error' => 'Cannot create temporary git checkout directory.']; @@ -1034,7 +1071,7 @@ return ['success' => false, 'error' => 'PHP exec() is disabled, so the updater c } $out = []; $ret = 0; -$cmd = 'git clone --depth 1 --branch ' . escapeshellarg($branch) . ' ' . escapeshellarg($repo_url) . ' ' . escapeshellarg($temp_dir) . ' 2>&1'; +$cmd = escapeshellarg($git_bin) . ' clone --depth 1 --branch ' . escapeshellarg($branch) . ' ' . escapeshellarg($repo_url) . ' ' . escapeshellarg($temp_dir) . ' 2>&1'; gsp_update_log('Starting configured git checkout from ' . $repo_url . ' branch ' . $branch); exec($cmd, $out, $ret); if ($ret !== 0) { @@ -1191,6 +1228,65 @@ if (!$preflight['success']) { return ['success' => false, 'error' => 'Preflight failed: ' . implode(' | ', $preflight['errors'])]; } +$backup = gsp_create_full_backup($update_type, $ref, false); +if (!$backup['success']) { +return $backup; +} +gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}"); + +$temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time() . '_' . mt_rand(1000, 9999); +@mkdir($temp_dir, 0750, true); +$zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir); +if (!$zip_file) { +gsp_rmdir_recursive($temp_dir); +return ['success' => false, 'error' => 'Failed to download update ZIP from GitHub.']; +} + +$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce); +@unlink($zip_file); +gsp_rmdir_recursive($temp_dir); +if (!empty($apply['restart_required'])) { +$apply['backup_dir'] = $backup['backup_dir']; +$apply['success'] = false; +return $apply; +} +if (!$apply['success']) { +return $apply; +} + +$commit_after = gsp_get_git_commit(); +gsp_fix_permissions(GSP_ROOT_DIR); +gsp_clear_panel_cache(GSP_PANEL_DIR); +gsp_write_version_file($ref, $update_type); +$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref; +$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref); +gsp_write_version_json($update_type, $vsource, $vversion, $commit_after); +gsp_write_last_update_markers(GSP_ROOT_DIR); +$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); + +if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) { +require_once(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php'); +} +if (function_exists('updateAllPanelModules')) { +updateAllPanelModules(); +} +if (function_exists('runPostUpdateOperations')) { +runPostUpdateOperations(); +} + +gsp_update_log("Update to {$ref} (type={$update_type}) complete"); +return [ +'success' => true, +'files_copied' => $apply['files_copied'], +'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0, +'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0, +'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [], +'backup_dir' => $backup['backup_dir'], +'preserved' => $apply['preserved'], +'patches' => $apply['patches'], +]; +} + function gsp_do_configured_git_update(array $update_cfg, $restart_nonce = '') { global $db; @@ -1258,65 +1354,332 @@ return [ ]; } -$backup = gsp_create_full_backup($update_type, $ref, false); +function gsp_safe_component_backup($component, $source_path, $backup_base) +{ +$component = preg_replace('/[^A-Za-z0-9._-]/', '_', (string)$component); +$backup_base = rtrim((string)$backup_base, '/'); +if ($backup_base === '' || strpos($backup_base, '/') !== 0) { +$backup_base = GSP_BACKUP_BASE; +} +if (!is_dir($backup_base) && !@mkdir($backup_base, 0755, true)) { +return ['success' => false, 'error' => 'Cannot create backup path: ' . $backup_base]; +} +$ts = date('Y-m-d_H-i-s'); +$backup_dir = $backup_base . '/component_' . $component . '_' . $ts; +if (!@mkdir($backup_dir, 0755, true)) { +return ['success' => false, 'error' => 'Cannot create component backup directory: ' . $backup_dir]; +} +if (!is_dir($source_path)) { +return ['success' => true, 'backup_dir' => $backup_dir, 'skipped' => true]; +} +$archive = $backup_dir . '/' . $component . '.tar.gz'; +$archive_result = gsp_create_archive($source_path, $archive, ['./logs', './cache', './tmp', './backups', './uploads', './upload', './Cfg', './ServerFiles', './Schedule', './steamcmd', './startups']); +if (!$archive_result['success']) { +return $archive_result; +} +@file_put_contents($backup_dir . '/backup.json', json_encode([ +'component' => $component, +'source_path' => $source_path, +'created_at' => date('Y-m-d H:i:s'), +'archive' => basename($archive), +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +return ['success' => true, 'backup_dir' => $backup_dir, 'archive' => $archive]; +} + +function gsp_component_source_path($source_root, $relative) +{ +$relative = trim((string)$relative, '/'); +if ($relative === '' || strpos($relative, '..') !== false || strpos($relative, "\0") !== false) { +return false; +} +$path = rtrim($source_root, '/') . '/' . $relative; +$real = realpath($path); +if ($real === false || strpos($real, realpath($source_root) ?: $source_root) !== 0) { +return false; +} +return $real; +} + +function gsp_component_copy_tree($src_root, $dst_root, $component_label) +{ +$copied = 0; +$skipped = []; +$copied_files = []; +if (!is_dir($src_root)) { +return ['success' => false, 'error' => 'Source component path not found: ' . $src_root]; +} +if (!is_dir($dst_root) && !@mkdir($dst_root, 0755, true)) { +return ['success' => false, 'error' => 'Destination path could not be created: ' . $dst_root]; +} +$iter = new RecursiveIteratorIterator( +new RecursiveDirectoryIterator($src_root, RecursiveDirectoryIterator::SKIP_DOTS), +RecursiveIteratorIterator::SELF_FIRST +); +$protected = [ +'includes/config.inc.php', +'config.inc.php', +'Cfg/', +'ServerFiles/', +'Schedule/', +'logs/', +'log/', +'backups/', +'backup/', +'cache/', +'tmp/', +'temp/', +'uploads/', +'upload/', +'home/', +'homes/', +'servers/', +'game_servers/', +'steamcmd/', +'startups/', +]; +foreach ($iter as $item) { +$rel = gsp_normalize_rel(substr($item->getPathname(), strlen(rtrim($src_root, '/')) + 1)); +$is_protected = false; +foreach ($protected as $prefix) { +if ($rel === rtrim($prefix, '/') || strpos($rel, $prefix) === 0) { +$is_protected = true; +break; +} +} +if ($is_protected) { +$skipped[] = $component_label . '/' . $rel; +continue; +} +$dst = rtrim($dst_root, '/') . '/' . $rel; +if ($item->isDir()) { +if (!is_dir($dst)) { +@mkdir($dst, 0755, true); +} +continue; +} +if (gsp_copy_file($item->getPathname(), $dst)) { +$copied++; +if (count($copied_files) < 200) { +$copied_files[] = $component_label . '/' . $rel; +} +} +} +return ['success' => true, 'files_copied' => $copied, 'skipped' => array_values(array_unique($skipped)), 'copied_files' => $copied_files]; +} + +function gsp_run_admin_post_update_command($command, $cwd) +{ +$command = trim((string)$command); +if ($command === '') { +return ['success' => true, 'skipped' => true, 'output' => '']; +} +if (strpos($command, "\0") !== false || strpos($command, "\n") !== false || strpos($command, "\r") !== false) { +return ['success' => false, 'error' => 'Post-update command must be a single line.']; +} +$out = []; +$ret = 0; +exec('cd ' . escapeshellarg($cwd) . ' && ' . $command . ' 2>&1', $out, $ret); +return ['success' => $ret === 0, 'exit_code' => $ret, 'output' => implode("\n", $out), 'error' => $ret === 0 ? '' : implode("\n", $out)]; +} + +function gsp_do_local_component_update(array $update_cfg, array $components) +{ +$allowed = array('panel', 'website'); +$components = array_values(array_intersect($allowed, array_unique($components))); +if (empty($components)) { +return ['success' => true, 'files_copied' => 0, 'results' => []]; +} +$validation = gsp_validate_update_settings($update_cfg); +if (!empty($validation)) { +return ['success' => false, 'error' => implode(' | ', $validation)]; +} +$checkout = gsp_checkout_update_source($update_cfg); +if (!$checkout['success']) { +return $checkout; +} +$results = []; +$total = 0; +$all_skipped = []; +$all_copied = []; +$map = [ +'panel' => ['source' => $update_cfg['panel_source_path'], 'dest' => $update_cfg['panel_path'], 'post' => $update_cfg['panel_post_update_command']], +'website' => ['source' => $update_cfg['website_source_path'], 'dest' => $update_cfg['website_path'], 'post' => $update_cfg['website_post_update_command']], +]; +foreach ($components as $component) { +$source = gsp_component_source_path($checkout['source_root'], $map[$component]['source']); +if ($source === false) { +gsp_rmdir_recursive($checkout['temp_dir']); +return ['success' => false, 'error' => 'Source folder for ' . $component . ' is invalid or missing.']; +} +$dest = rtrim((string)$map[$component]['dest'], '/'); +if (strpos($dest, '/') !== 0 || strpos($dest, '..') !== false || strpos($dest, "\0") !== false) { +gsp_rmdir_recursive($checkout['temp_dir']); +return ['success' => false, 'error' => 'Destination path for ' . $component . ' is invalid.']; +} +$backup = ['success' => true, 'backup_dir' => null]; +if (!empty($update_cfg['backup_before_update'])) { +$backup = gsp_safe_component_backup($component, $dest, $update_cfg['backup_path']); if (!$backup['success']) { +gsp_rmdir_recursive($checkout['temp_dir']); return $backup; } -gsp_update_log("Backup created at {$backup['backup_ts']} before {$update_type} update to {$ref}"); - -$temp_dir = sys_get_temp_dir() . '/gsp_dl_' . time() . '_' . mt_rand(1000, 9999); -@mkdir($temp_dir, 0750, true); -$zip_file = gsp_download_zip($repo_owner, $repo_name, $ref, $temp_dir); -if (!$zip_file) { -gsp_rmdir_recursive($temp_dir); -return ['success' => false, 'error' => 'Failed to download update ZIP from GitHub.']; } - -$apply = gsp_apply_update_from_zip($zip_file, $restart_nonce); -@unlink($zip_file); -gsp_rmdir_recursive($temp_dir); -if (!empty($apply['restart_required'])) { -$apply['backup_dir'] = $backup['backup_dir']; -$apply['success'] = false; -return $apply; +$sync = gsp_component_copy_tree($source, $dest, ucfirst($component)); +if (!$sync['success']) { +gsp_rmdir_recursive($checkout['temp_dir']); +return $sync; } -if (!$apply['success']) { -return $apply; +$post = gsp_run_admin_post_update_command($map[$component]['post'], $dest); +if (!$post['success']) { +gsp_rmdir_recursive($checkout['temp_dir']); +return ['success' => false, 'error' => ucfirst($component) . ' post-update command failed: ' . $post['error'], 'results' => $results]; } - -$commit_after = gsp_get_git_commit(); -gsp_fix_permissions(GSP_ROOT_DIR); -gsp_clear_panel_cache(GSP_PANEL_DIR); -gsp_write_version_file($ref, $update_type); -$vsource = ($update_type === 'release') ? 'GitHub Releases' : $ref; -$vversion = ($update_type === 'release') ? $ref : ($commit_after ?: $ref); -gsp_write_version_json($update_type, $vsource, $vversion, $commit_after); +$results[$component] = [ +'source' => $source, +'destination' => $dest, +'backup_dir' => $backup['backup_dir'], +'files_copied' => $sync['files_copied'], +'skipped' => $sync['skipped'], +'post_update' => $post, +]; +$total += (int)$sync['files_copied']; +$all_skipped = array_merge($all_skipped, $sync['skipped']); +$all_copied = array_merge($all_copied, $sync['copied_files']); +gsp_update_log('Local component update complete: ' . $component . ' copied=' . intval($sync['files_copied']) . ' source=' . $source . ' dest=' . $dest); +} +gsp_rmdir_recursive($checkout['temp_dir']); gsp_write_last_update_markers($update_cfg['repo_root']); -$db->setSettings(['ogp_version' => $ref, 'version_type' => $update_type]); - -if (file_exists(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php')) { -require_once(GSP_PANEL_DIR . '/modules/modulemanager/module_handling.php'); +if (in_array('panel', $components, true)) { +gsp_clear_panel_cache($update_cfg['panel_path']); +gsp_fix_permissions($update_cfg['panel_path']); } -if (function_exists('updateAllPanelModules')) { -updateAllPanelModules(); -} -if (function_exists('runPostUpdateOperations')) { -runPostUpdateOperations(); -} - -gsp_update_log("Update to {$ref} (type={$update_type}) complete"); return [ 'success' => true, -'files_copied' => $apply['files_copied'], -'panel_files_copied' => isset($apply['panel_files_copied']) ? $apply['panel_files_copied'] : 0, -'website_files_copied' => isset($apply['website_files_copied']) ? $apply['website_files_copied'] : 0, -'copied_files' => isset($apply['copied_files']) ? $apply['copied_files'] : [], -'backup_dir' => $backup['backup_dir'], -'preserved' => $apply['preserved'], -'patches' => $apply['patches'], +'files_copied' => $total, +'results' => $results, +'preserved' => array_values(array_unique($all_skipped)), +'copied_files' => array_slice(array_values(array_unique($all_copied)), 0, 200), ]; } +function gsp_update_payload_encode(array $payload) +{ +$lines = []; +foreach ($payload as $key => $value) { +if (is_array($value)) { +$value = json_encode($value, JSON_UNESCAPED_SLASHES); +} +$lines[] = $key . '=' . base64_encode((string)$value); +} +return implode("\n", $lines); +} + +function gsp_infer_agent_os(array $remote_server, $reported_os = '') +{ +$reported = strtolower((string)$reported_os); +$haystack = strtolower((string)$remote_server['remote_server_name'] . ' ' . (string)$remote_server['agent_ip'] . ' ' . $reported); +if (strpos($haystack, 'windows') !== false || strpos($haystack, 'cygwin') !== false || strpos($haystack, 'mingw') !== false) { +return 'windows'; +} +return 'linux'; +} + +function gsp_get_remote_agent_rows() +{ +global $db; +$rows = $db->getRemoteServers(); +return is_array($rows) ? $rows : []; +} + +function gsp_dispatch_remote_agent_update(array $remote_server, array $update_cfg, $component) +{ +if (!function_exists('xmlrpc_encode_request') || !function_exists('xmlrpc_decode')) { +return ['success' => false, 'status' => 'missing_xmlrpc', 'message' => 'PHP xmlrpc extension is required for remote agent updates.']; +} +require_once(GSP_PANEL_DIR . '/includes/lib_remote.php'); +$component = ($component === 'windows_agent') ? 'windows_agent' : 'linux_agent'; +$source_path = ($component === 'windows_agent') ? $update_cfg['windows_agent_source_path'] : $update_cfg['linux_agent_source_path']; +$post_cmd = ($component === 'windows_agent') ? $update_cfg['windows_agent_post_update_command'] : $update_cfg['linux_agent_post_update_command']; +$payload = [ +'component' => $component, +'repo_url' => $update_cfg['repo_url'], +'branch' => $update_cfg['branch'], +'source_path' => $source_path, +'install_path' => '', +'git_path' => $update_cfg['git_path'], +'backup_path' => '', +'post_update_command' => $post_cmd, +'requested_at' => date('Y-m-d H:i:s'), +]; +$remote = new OGPRemoteLibrary($remote_server['agent_ip'], $remote_server['agent_port'], $remote_server['encryption_key'], $remote_server['timeout']); +if ((int)$remote->status_chk() !== 1) { +return ['success' => false, 'status' => 'agent_unreachable', 'message' => 'Agent unreachable or encryption mismatch.']; +} +if (!method_exists($remote, 'component_update')) { +return ['success' => false, 'status' => 'panel_missing_rpc_wrapper', 'message' => 'Panel remote library does not expose component_update().']; +} +$response = $remote->component_update(gsp_update_payload_encode($payload)); +if (is_array($response)) { +return $response; +} +return ['success' => false, 'status' => 'unexpected_response', 'message' => 'Unexpected agent response: ' . substr((string)$response, 0, 300)]; +} + +function gsp_do_remote_agents_update(array $update_cfg, array $remote_ids, array $component_filters) +{ +global $db; +if (!function_exists('xmlrpc_encode_request') || !function_exists('xmlrpc_decode')) { +return ['success' => false, 'results' => [], 'success_count' => 0, 'message' => 'PHP xmlrpc extension is required for remote agent updates.']; +} +require_once(GSP_PANEL_DIR . '/includes/lib_remote.php'); +$remote_ids = array_map('intval', $remote_ids); +$remote_ids = array_values(array_filter(array_unique($remote_ids))); +if (empty($remote_ids)) { +return ['success' => true, 'results' => [], 'message' => 'No remote agents selected.']; +} +$all = gsp_get_remote_agent_rows(); +$by_id = []; +foreach ($all as $row) { +$by_id[(int)$row['remote_server_id']] = $row; +} +$results = []; +$success_count = 0; +foreach ($remote_ids as $remote_id) { +if (empty($by_id[$remote_id])) { +$results[$remote_id] = ['success' => false, 'status' => 'not_found', 'message' => 'Remote host not found.']; +continue; +} +$row = $by_id[$remote_id]; +$reported_os = ''; +try { +$probe = new OGPRemoteLibrary($row['agent_ip'], $row['agent_port'], $row['encryption_key'], $row['timeout']); +$reported_os = (string)$probe->what_os(); +} catch (Exception $e) { +$reported_os = ''; +} +$agent_os = gsp_infer_agent_os($row, $reported_os); +if ($agent_os === 'linux' && !in_array('linux_agent', $component_filters, true)) { +$results[$remote_id] = ['success' => true, 'status' => 'skipped', 'message' => 'Skipped Linux agent by filter.', 'agent_os' => $agent_os]; +continue; +} +if ($agent_os === 'windows' && !in_array('windows_agent', $component_filters, true)) { +$results[$remote_id] = ['success' => true, 'status' => 'skipped', 'message' => 'Skipped Windows agent by filter.', 'agent_os' => $agent_os]; +continue; +} +$component = ($agent_os === 'windows') ? 'windows_agent' : 'linux_agent'; +$result = gsp_dispatch_remote_agent_update($row, $update_cfg, $component); +$result['agent_os'] = $agent_os; +$result['remote_server_name'] = $row['remote_server_name']; +$result['agent_ip'] = $row['agent_ip']; +$results[$remote_id] = $result; +if (!empty($result['success'])) { +$success_count++; +} +gsp_update_log('Remote agent update requested: id=' . $remote_id . ' component=' . $component . ' result=' . json_encode($result)); +} +return ['success' => true, 'results' => $results, 'success_count' => $success_count]; +} + function gsp_get_available_backups() { $backups = []; @@ -1947,6 +2310,17 @@ $new_cfg = [ 'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : '', 'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : '', 'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : '', +'website_path' => isset($_POST['gsp_update_website_path']) ? rtrim(trim((string)$_POST['gsp_update_website_path']), '/') : '', +'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : '', +'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : '', +'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : '', +'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : '', +'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : '', +'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : '', +'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : '', +'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : '', +'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : '', +'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : '', 'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0', ]; $errors = gsp_validate_update_settings($new_cfg); @@ -1958,22 +2332,122 @@ $db->setSettings([ 'gsp_update_branch' => $new_cfg['branch'], 'gsp_update_repo_root' => $new_cfg['repo_root'], 'gsp_update_panel_path' => $new_cfg['panel_path'], +'gsp_update_website_path' => $new_cfg['website_path'], +'gsp_update_panel_source_path' => $new_cfg['panel_source_path'], +'gsp_update_linux_agent_source_path' => $new_cfg['linux_agent_source_path'], +'gsp_update_windows_agent_source_path' => $new_cfg['windows_agent_source_path'], +'gsp_update_website_source_path' => $new_cfg['website_source_path'], +'gsp_update_git_path' => $new_cfg['git_path'], +'gsp_update_backup_path' => $new_cfg['backup_path'], +'gsp_update_panel_post_update_command' => $new_cfg['panel_post_update_command'], +'gsp_update_website_post_update_command' => $new_cfg['website_post_update_command'], +'gsp_update_linux_agent_post_update_command' => $new_cfg['linux_agent_post_update_command'], +'gsp_update_windows_agent_post_update_command' => $new_cfg['windows_agent_post_update_command'], 'gsp_update_backup_before' => $new_cfg['backup_before_update'], ]); $settings['gsp_update_repo_url'] = $new_cfg['repo_url']; $settings['gsp_update_branch'] = $new_cfg['branch']; $settings['gsp_update_repo_root'] = $new_cfg['repo_root']; $settings['gsp_update_panel_path'] = $new_cfg['panel_path']; +$settings['gsp_update_website_path'] = $new_cfg['website_path']; +$settings['gsp_update_panel_source_path'] = $new_cfg['panel_source_path']; +$settings['gsp_update_linux_agent_source_path'] = $new_cfg['linux_agent_source_path']; +$settings['gsp_update_windows_agent_source_path'] = $new_cfg['windows_agent_source_path']; +$settings['gsp_update_website_source_path'] = $new_cfg['website_source_path']; +$settings['gsp_update_git_path'] = $new_cfg['git_path']; +$settings['gsp_update_backup_path'] = $new_cfg['backup_path']; +$settings['gsp_update_panel_post_update_command'] = $new_cfg['panel_post_update_command']; +$settings['gsp_update_website_post_update_command'] = $new_cfg['website_post_update_command']; +$settings['gsp_update_linux_agent_post_update_command'] = $new_cfg['linux_agent_post_update_command']; +$settings['gsp_update_windows_agent_post_update_command'] = $new_cfg['windows_agent_post_update_command']; $settings['gsp_update_backup_before'] = $new_cfg['backup_before_update']; $update_cfg = gsp_update_settings(); print_success('Update settings saved.'); } +} elseif ($action === 'update_components') { +$update_cfg = [ +'repo_url' => isset($_POST['gsp_update_repo_url']) ? trim((string)$_POST['gsp_update_repo_url']) : $update_cfg['repo_url'], +'branch' => isset($_POST['gsp_update_branch']) ? trim((string)$_POST['gsp_update_branch']) : $update_cfg['branch'], +'repo_root' => isset($_POST['gsp_update_repo_root']) ? rtrim(trim((string)$_POST['gsp_update_repo_root']), '/') : $update_cfg['repo_root'], +'panel_path' => isset($_POST['gsp_update_panel_path']) ? rtrim(trim((string)$_POST['gsp_update_panel_path']), '/') : $update_cfg['panel_path'], +'website_path' => isset($_POST['gsp_update_website_path']) ? rtrim(trim((string)$_POST['gsp_update_website_path']), '/') : $update_cfg['website_path'], +'panel_source_path' => isset($_POST['gsp_update_panel_source_path']) ? trim((string)$_POST['gsp_update_panel_source_path'], '/') : $update_cfg['panel_source_path'], +'linux_agent_source_path' => isset($_POST['gsp_update_linux_agent_source_path']) ? trim((string)$_POST['gsp_update_linux_agent_source_path'], '/') : $update_cfg['linux_agent_source_path'], +'windows_agent_source_path' => isset($_POST['gsp_update_windows_agent_source_path']) ? trim((string)$_POST['gsp_update_windows_agent_source_path'], '/') : $update_cfg['windows_agent_source_path'], +'website_source_path' => isset($_POST['gsp_update_website_source_path']) ? trim((string)$_POST['gsp_update_website_source_path'], '/') : $update_cfg['website_source_path'], +'git_path' => isset($_POST['gsp_update_git_path']) ? trim((string)$_POST['gsp_update_git_path']) : $update_cfg['git_path'], +'backup_path' => isset($_POST['gsp_update_backup_path']) ? rtrim(trim((string)$_POST['gsp_update_backup_path']), '/') : $update_cfg['backup_path'], +'panel_post_update_command' => isset($_POST['gsp_update_panel_post_update_command']) ? trim((string)$_POST['gsp_update_panel_post_update_command']) : $update_cfg['panel_post_update_command'], +'website_post_update_command' => isset($_POST['gsp_update_website_post_update_command']) ? trim((string)$_POST['gsp_update_website_post_update_command']) : $update_cfg['website_post_update_command'], +'linux_agent_post_update_command' => isset($_POST['gsp_update_linux_agent_post_update_command']) ? trim((string)$_POST['gsp_update_linux_agent_post_update_command']) : $update_cfg['linux_agent_post_update_command'], +'windows_agent_post_update_command' => isset($_POST['gsp_update_windows_agent_post_update_command']) ? trim((string)$_POST['gsp_update_windows_agent_post_update_command']) : $update_cfg['windows_agent_post_update_command'], +'backup_before_update' => !empty($_POST['gsp_update_backup_before']) ? '1' : '0', +]; +$errors = gsp_validate_update_settings($update_cfg); +if (!empty($errors)) { +print_failure('Update settings are invalid: ' . htmlspecialchars(implode(' | ', $errors))); +} else { +$requested_components = isset($_POST['gsp_update_components']) && is_array($_POST['gsp_update_components']) ? $_POST['gsp_update_components'] : []; +$requested_components = array_values(array_intersect(array('panel', 'website', 'linux_agent', 'windows_agent'), array_map('strval', $requested_components))); +$local_components = array_values(array_intersect(array('panel', 'website'), $requested_components)); +$agent_components = array_values(array_intersect(array('linux_agent', 'windows_agent'), $requested_components)); +$remote_ids = isset($_POST['gsp_update_remote_ids']) && is_array($_POST['gsp_update_remote_ids']) ? array_map('intval', $_POST['gsp_update_remote_ids']) : []; +if (!empty($_POST['gsp_update_all_agents'])) { +$remote_ids = array(); +foreach (gsp_get_remote_agent_rows() as $row) { +$remote_ids[] = (int)$row['remote_server_id']; +} +} +if (empty($requested_components)) { +print_failure('Select at least one component to update.'); +} else { +$messages = []; +if (!empty($local_components)) { +$local_result = gsp_do_local_component_update($update_cfg, $local_components); +if (!$local_result['success']) { +print_failure('Local component update failed: ' . htmlspecialchars($local_result['error'])); +} else { +$messages[] = 'Local components updated, files copied: ' . intval($local_result['files_copied']); +} +} +if (!empty($agent_components)) { +$remote_result = gsp_do_remote_agents_update($update_cfg, $remote_ids, $agent_components); +if (empty($remote_result['success']) && empty($remote_result['results'])) { +print_failure('Remote agent update failed: ' . htmlspecialchars(isset($remote_result['message']) ? $remote_result['message'] : 'Unknown error.')); +} +if (!empty($remote_result['results'])) { +echo "
| Remote ID | Status | Message |
|---|---|---|
| " . intval($rid) . " | " . htmlspecialchars($status) . " | " . htmlspecialchars($msg) . " |
git.\n"; echo " "; echo "\n"; echo "
\n"; +echo "Select the application components to update from the configured repository. Server homes, hosted game data, uploads, logs, cache, and agent configuration folders are protected.
\n"; +echo ""; +echo " "; +echo " "; +echo " "; +echo " "; +echo ""; +echo "
\n"; +$remote_rows = gsp_get_remote_agent_rows(); +if (!empty($remote_rows)) { +if (!class_exists('OGPRemoteLibrary') && function_exists('xmlrpc_encode_request') && function_exists('xmlrpc_decode') && file_exists(GSP_PANEL_DIR . '/includes/lib_remote.php')) { +require_once(GSP_PANEL_DIR . '/includes/lib_remote.php'); +} +echo "| Select | Name | Host | Status | Reported OS |
|---|---|---|---|---|
| "; +echo " | " . htmlspecialchars($remote_row['remote_server_name']) . " | "; +echo "" . htmlspecialchars($remote_row['agent_ip'] . ':' . $remote_row['agent_port']) . " | "; +echo "" . htmlspecialchars($status_text) . " | "; +echo "" . htmlspecialchars($os_text !== '' ? $os_text : gsp_infer_agent_os($remote_row, '')) . " | "; +echo "
No remote agents are configured.
\n"; +} +echo "\n"; echo "\n"; echo "