Agent-Logs-Url: https://github.com/GameServerPanel/GSP/sessions/7643e55f-473c-4084-baa0-cf8ae8c9a10a Co-authored-by: iaretechnician <2749183+iaretechnician@users.noreply.github.com>
387 lines
16 KiB
PHP
387 lines
16 KiB
PHP
<?php
|
||
/*
|
||
*
|
||
* GSP - Game Server Panel (a heavily customized fork of OGP maintained by WDS)
|
||
*
|
||
* Server Content Installer (module: addonsmanager, page: addons)
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
* This file handles the actual download+extraction and post-install script
|
||
* execution for a Server Content item selected by a user.
|
||
*
|
||
* CURRENT FLOW:
|
||
* 1. User selects a content type (plugin / mappack / config / ...) from
|
||
* user_addons.php which links here with addon_type=<type>.
|
||
* 2. User picks a specific content item from a dropdown.
|
||
* 3. On form submit, state=start is set and start_file_download() is called
|
||
* on the remote agent with the configured URL and target path.
|
||
* 4. The agent downloads and extracts the archive.
|
||
* 5. If a post_script is defined it is run on the agent after extraction.
|
||
* 6. The page auto-refreshes (state=refresh) to show download/script progress.
|
||
*
|
||
* POST-INSTALL SCRIPT REPLACEMENT VARIABLES:
|
||
* %home_path% – absolute path of the game server home directory
|
||
* %home_name% – display name of the game server home
|
||
* %control_password% – RCON / control password for this server instance
|
||
* %max_players% – maximum player count configured for this mod slot
|
||
* %ip% – IP address bound to this server instance
|
||
* %port% – game port bound to this server instance
|
||
* %query_port% – query/status port (derived from game XML rules)
|
||
* %incremental% – internal incremental run counter for this mod/home
|
||
*
|
||
* SECURITY NOTES:
|
||
* - Users CANNOT supply arbitrary scripts; only the admin-defined post_script
|
||
* is executed. Users only pick from the approved list.
|
||
* - Paths are passed to the agent which is responsible for enforcing that
|
||
* all paths stay inside the assigned home directory.
|
||
* - TODO (next phase): add explicit server-side path validation before
|
||
* sending the command to the agent to block ../ traversal at the panel.
|
||
*
|
||
* ─── FUTURE WORK (TODO – next phase) ────────────────────────────────────────
|
||
* The items below are intentionally NOT implemented here yet. They are
|
||
* documented so the next contributor knows exactly where to add them.
|
||
*
|
||
* TODO: requires_stop flag
|
||
* If the content item sets requires_stop=1, stop the server before
|
||
* initiating the download. Poll is_server_running() and abort if it
|
||
* cannot be stopped within a timeout.
|
||
*
|
||
* TODO: backup_before_install flag
|
||
* If backup_before_install=1, call the agent's backup function or
|
||
* compress the target path into a timestamped .tar.gz before extraction.
|
||
*
|
||
* TODO: restart_after_install flag
|
||
* If restart_after_install=1, trigger a server start after a successful
|
||
* install (i.e. after post_script completes with exit code 0).
|
||
*
|
||
* TODO: install_method field
|
||
* Current method is always 'download_zip'. Future methods:
|
||
* 'download_file' – single-file download, no extraction
|
||
* 'post_script' – run only the post_script, no download
|
||
* 'steam_workshop' – pass workshop item IDs to the agent's workshop helper
|
||
* 'minecraft_jar' – download a Minecraft server jar + update start script
|
||
* 'profile_copy' – copy a profile directory tree into the server home
|
||
*
|
||
* TODO: content_version field
|
||
* Store the installed version tag so the UI can display "installed: 1.21.1"
|
||
* and detect whether an update is available.
|
||
*
|
||
* TODO: safe script templates
|
||
* Provide a set of admin-approved script templates so admins do not have to
|
||
* write raw bash from scratch. Templates are stored in the DB and referenced
|
||
* by content items.
|
||
*
|
||
* TODO: install history / logging
|
||
* Write a row to a new install_history table (or log file) each time a
|
||
* content item is installed:
|
||
* home_id, addon_id, installed_by (user_id), installed_at, result, log_output
|
||
*
|
||
* TODO: user-friendly status output
|
||
* Replace the raw progress-bar with a card-style status block showing:
|
||
* content item name, version, download progress, script output, final status.
|
||
*
|
||
* TODO: Steam Workshop integration
|
||
* When install_method='steam_workshop', pass the workshop item ID list to
|
||
* the agent. See SERVER_CONTENT_ROADMAP.md – Part 6 for the full design.
|
||
*
|
||
* TODO: Minecraft jar / version switching
|
||
* When install_method='minecraft_jar', download the jar from Mojang/Paper/
|
||
* Purpur/Fabric API, place it at the configured server path, and patch the
|
||
* startup command line. See SERVER_CONTENT_ROADMAP.md – Part 7.
|
||
* ─────────────────────────────────────────────────────────────────────────────
|
||
*/
|
||
|
||
function do_progress($kbytes,$totalsize)
|
||
{
|
||
$mbytes = round($kbytes / 1024, 2);
|
||
|
||
if($kbytes > 0)
|
||
{
|
||
$pct = round(( $kbytes / $totalsize ) * 100, 2);
|
||
}
|
||
else
|
||
{
|
||
$pct = "-";
|
||
}
|
||
#echo "Percent is $pct";
|
||
return "$totalsize;$mbytes;$pct";
|
||
}
|
||
|
||
require_once("includes/lib_remote.php");
|
||
require_once("modules/config_games/server_config_parser.php");
|
||
require_once("protocol/lgsl/lgsl_protocol.php");
|
||
// Central category map — all valid addon_type values and their labels.
|
||
require_once(dirname(__FILE__) . '/server_content_categories.php');
|
||
require_once(dirname(__FILE__) . '/server_content_helpers.php');
|
||
|
||
function exec_ogp_module() {
|
||
|
||
global $db,$view;
|
||
$home_id = $_REQUEST['home_id'];
|
||
$mod_id = $_REQUEST['mod_id'];
|
||
$ip = $_REQUEST['ip'];
|
||
$port = $_REQUEST['port'];
|
||
$user_id = $_SESSION['user_id'];
|
||
|
||
$isAdmin = $db->isAdmin( $_SESSION['user_id'] );
|
||
$query_groups = "";
|
||
if($isAdmin)
|
||
$home_info = $db->getGameHome($home_id);
|
||
else
|
||
{
|
||
$home_info = $db->getUserGameHome($user_id,$home_id);
|
||
$groups = $db->getUsersGroups($_SESSION['user_id']);
|
||
if (!is_array($groups)) {
|
||
$groups = [];
|
||
}
|
||
$query_groups .= " AND (";
|
||
foreach ((array)$groups as $group)
|
||
$query_groups .= "group_id=".$group['group_id']." OR ";
|
||
$query_groups .= "group_id=0 OR group_id IS NULL)";
|
||
}
|
||
|
||
if ( $home_info === FALSE )
|
||
{
|
||
print_failure(get_lang('no_rights'));
|
||
echo create_back_button("addonsmanager","user_addons");
|
||
return;
|
||
}
|
||
|
||
$home_cfg_id = $home_info['home_cfg_id'];
|
||
$server_xml = read_server_config(SERVER_CONFIG_LOCATION."/".$home_info['home_cfg_file']);
|
||
|
||
// Use the full category map so newly added types are accepted without
|
||
// editing this file. The original three types are always present.
|
||
$addon_types = get_server_content_type_keys();
|
||
$addon_type = isset($_REQUEST['addon_type']) ? $_REQUEST['addon_type'] : "";
|
||
|
||
$state = isset($_REQUEST['state']) ? $_REQUEST['state'] : "";
|
||
$pid = isset($_REQUEST['pid']) ? $_REQUEST['pid'] : -1;
|
||
|
||
if ( $state != "" )
|
||
{
|
||
$addon_id = (int)$_REQUEST['addon_id'];
|
||
|
||
$addons_rows = $db->resultQuery("SELECT url, path, post_script FROM OGP_DB_PREFIXaddons WHERE addon_id=".$addon_id.$query_groups);
|
||
if (!is_array($addons_rows)) {
|
||
$addons_rows = [];
|
||
}
|
||
|
||
if (!$addons_rows) {
|
||
print_failure(get_lang('invalid_addon'));
|
||
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
|
||
return;
|
||
}
|
||
|
||
$remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']);
|
||
|
||
$addon_info = $addons_rows[0];
|
||
$url = $addon_info['url'];
|
||
$filename = basename($url);
|
||
#### Replace template variables in the post-install script with
|
||
#### live server data before sending to the agent.
|
||
#### Each variable is replaced case-insensitively.
|
||
#### SECURITY: only admin-defined variables are substituted; users
|
||
#### cannot inject additional commands through these fields.
|
||
if($addon_info['post_script'] != "")
|
||
{
|
||
$addon_info['post_script'] = strip_real_escape_string($addon_info['post_script']);
|
||
$check_passed = FALSE;
|
||
$address_at_post = $ip.":".$port;
|
||
$ip_ports = $db->getHomeIpPorts($home_info['home_id']);
|
||
if (!is_array($ip_ports)) {
|
||
$ip_ports = [];
|
||
}
|
||
foreach ((array)$ip_ports as $ip_port);
|
||
{
|
||
$address_owned = $ip_port['ip'].":".$ip_port['port'];
|
||
if($address_owned == $address_at_post)
|
||
{
|
||
$check_passed = TRUE;
|
||
$ip = $ip_port['ip'];
|
||
$port = $ip_port['port'];
|
||
}
|
||
}
|
||
if($check_passed)
|
||
{
|
||
$home_info['ip'] = $ip;
|
||
$home_info['port'] = $port;
|
||
|
||
if( isset($server_xml->gameq_query_name) )
|
||
{
|
||
require_once("modules/gamemanager/home_handling_functions.php");
|
||
$home_info['query_port'] = get_query_port($server_xml, $home_info['port']);
|
||
}
|
||
elseif( isset($server_xml->lgsl_query_name) )
|
||
{
|
||
$get_q_and_s = lgsl_port_conversion((string)$server_xml->lgsl_query_name, $home_info['port'], "", "");
|
||
$home_info['query_port'] = $get_q_and_s['1'];
|
||
}
|
||
|
||
$home_info["incremental"] = $db->incrementalNumByHomeId( $home_info['home_id'], $home_info['mods'][$mod_id]['mod_cfg_id'], $home_info['remote_server_id'] );
|
||
|
||
$post_script = preg_replace( "/\%home_path\%/i", $home_info['home_path'], $addon_info['post_script']);
|
||
$post_script = preg_replace( "/\%home_name\%/i", $home_info['home_name'], $post_script);
|
||
$post_script = preg_replace( "/\%control_password\%/i", $home_info['control_password'], $post_script);
|
||
$post_script = preg_replace( "/\%max_players\%/i", $home_info['mods'][$mod_id]['max_players'], $post_script);
|
||
$post_script = preg_replace( "/\%ip\%/i", $home_info['ip'], $post_script);
|
||
$post_script = preg_replace( "/\%port\%/i", $home_info['port'], $post_script);
|
||
$post_script = preg_replace( "/\%query_port\%/i", $home_info['query_port'], $post_script);
|
||
$post_script = preg_replace( "/\%incremental\%/i", $home_info['incremental'], $post_script);
|
||
}
|
||
}
|
||
|
||
#### end of replacements
|
||
if ( $state == "start" AND $addon_id != "" )
|
||
$pid = $remote->start_file_download( $addon_info['url'], $home_info['home_path']."/".$addon_info['path'], $filename, "uncompress", $post_script);
|
||
|
||
$headers = get_headers($url, 1);
|
||
|
||
$download_available = !$headers ? FALSE : TRUE;
|
||
// Check if any error occured
|
||
if($download_available)
|
||
{
|
||
$bytes = is_array($headers['Content-Length']) ? $headers['Content-Length'][1] : $headers['Content-Length'];
|
||
// Display the File Size
|
||
$totalsize = $bytes / 1024;
|
||
clearstatcache();
|
||
}
|
||
|
||
$kbytes = $remote->rsync_progress($home_info['home_path']."/".$addon_info['path']."/".$filename);
|
||
list($totalsize,$mbytes,$pct) = explode(";",do_progress($kbytes,$totalsize));
|
||
$totalmbytes = round($totalsize / 1024, 2);
|
||
$pct = $pct > 100 ? 100 : $pct;
|
||
echo "<h2>" . htmlentities($home_info['home_name']) . "</h2>";
|
||
echo '<div class="dragbox bloc rounded" style="background-color:#dce9f2;" >
|
||
<h4>'.get_lang('install')." ".$filename." ${mbytes}MB/${totalmbytes}MB</h4>
|
||
<div style='background-color:#dce9f2;' >
|
||
";
|
||
$bar = '';
|
||
for( $i = 1; $i <= $pct; $i++ )
|
||
{
|
||
$bar .= '<img style="width:0.92%;vertical-align:middle;" src="images/progressBar.png">';
|
||
}
|
||
echo "<center>$bar <b style='vertical-align:top;display:inline;font-size:1.2em;color:red;' >$pct%</b></center>
|
||
</div>
|
||
</div>";
|
||
|
||
if ( ( $pct == "100" or !$download_available ) AND $post_script != "" )
|
||
{
|
||
$log_retval = $remote->get_log( "post_script",
|
||
$pid,
|
||
clean_path($home_info['home_path']."/".$server_xml->exe_location),
|
||
$script_log);
|
||
if ($log_retval == 0)
|
||
{
|
||
print_failure(get_lang('agent_offline'));
|
||
}
|
||
elseif ($log_retval == 1 || $log_retval == 2)
|
||
{
|
||
echo "<pre class='log'>".$script_log."</pre>";
|
||
}
|
||
elseif( $remote->is_screen_running("post_script",$pid) == 1 )
|
||
{
|
||
print_failure(get_lang_f('unable_to_get_log',$log_retval));
|
||
}
|
||
}
|
||
|
||
if( $pct == "100" or !$download_available or ( $download_available and $pct == "-" and $pid > 0 ) )
|
||
{
|
||
if(!$download_available)
|
||
{
|
||
print_failure(get_lang('failed_to_start_file_download'));
|
||
}
|
||
elseif( $remote->is_file_download_in_progress($pid) === 1 )
|
||
{
|
||
print_success(get_lang_f('wait_while_decompressing', $filename));
|
||
echo "<p><a href=\"?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid",5);
|
||
}
|
||
elseif( $remote->is_file_download_in_progress($pid) === 0 AND $remote->is_screen_running("post_script",$pid) === 0 )
|
||
{
|
||
print_success(get_lang('addon_installed_successfully'));
|
||
echo "<p><a href=\"?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||
"&mod_id=$mod_id&ip=$ip&port=$port\">".get_lang('back')."</a></p>";
|
||
$view->refresh("?m=addonsmanager&p=user_addons&home_id=$home_id".
|
||
"&mod_id=$mod_id&ip=$ip&port=$port",10);
|
||
return;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
echo "<p><a href=\"?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid\">".get_lang('refresh')."</a></p>";
|
||
$view->refresh("?m=addonsmanager&p=addons&state=refresh&home_id=$home_id&mod_id=$mod_id".
|
||
"&ip=$ip&port=$port&addon_id=$addon_id&pid=$pid",5);
|
||
}
|
||
|
||
}
|
||
elseif( $addon_type != "" )
|
||
{
|
||
|
||
if (!(is_array($addon_types) && in_array($addon_type, $addon_types))) {
|
||
print_failure(get_lang('invalid_addon_type'));
|
||
$view->refresh('?m=addonsmanager&p=user_addons&home_id='. $home_id .'&mod_id='. $mod_id .'&ip='. $ip .'&port='.$port);
|
||
|
||
return;
|
||
}
|
||
if ($addon_type === 'workshop') {
|
||
scm_ensure_workshop_schema($db);
|
||
$view->refresh('?m=addonsmanager&p=workshop_content&home_id='.(int)$home_id.'&mod_id='.(int)$mod_id.'&ip='.urlencode((string)$ip).'&port='.urlencode((string)$port), 0);
|
||
return;
|
||
}
|
||
|
||
?>
|
||
<h2><?php echo htmlentities($home_info['home_name'])." ".get_lang($addon_type) ;?></h2>
|
||
<table class='center'>
|
||
<form method='get'>
|
||
<input type='hidden' name='m' value='addonsmanager' />
|
||
<input type='hidden' name='p' value='addons' />
|
||
<input type='hidden' name='home_id' value='<?php echo $home_id; ?>' />
|
||
<input type='hidden' name='mod_id' value='<?php echo $mod_id; ?>' />
|
||
<input type='hidden' name='ip' value='<?php echo $ip; ?>' />
|
||
<input type='hidden' name='port' value='<?php echo $port; ?>' />
|
||
<input type='hidden' name='state' value='start' />
|
||
<tr><td align='right'><?php print_lang('game_name'); ?>: </td><td align='left'><?php echo $home_info['game_name']; ?></td></tr>
|
||
<tr><td align='right'><?php print_lang('directory'); ?>: </td><td align='left'><?php echo $home_info['home_path']; ?></td></tr>
|
||
<tr><td align='right'><?php print_lang('remote_server'); ?>: </td>
|
||
<td align='left'><?php echo "$home_info[remote_server_name] ($home_info[agent_ip]:$home_info[agent_port])"; ?></td></tr>
|
||
<tr><td align='right'><?php print_lang('select_addon'); ?>: </td>
|
||
<td align='left'>
|
||
<select name="addon_id">
|
||
<?php
|
||
$addons = $db->resultQuery("SELECT addon_id, name FROM OGP_DB_PREFIXaddons WHERE addon_type='".$addon_type."' AND home_cfg_id=" . $home_cfg_id . $query_groups . " ORDER BY name ASC");
|
||
if (!is_array($addons)) {
|
||
$addons = [];
|
||
}
|
||
foreach ((array)$addons as $addon)
|
||
{
|
||
?>
|
||
<option value="<?php echo $addon['addon_id']; ?>"><?php echo $addon['name']; ?></option>
|
||
<?php
|
||
}
|
||
?>
|
||
</select>
|
||
</td></tr>
|
||
<tr><td colspan='2' class='info'> </td></tr>
|
||
<td align='left'>
|
||
|
||
</td></tr><tr><td align="right">
|
||
<input type="submit" name="update" value="<?php print_lang('install'); ?>" />
|
||
</form></td><td>
|
||
<form method="get">
|
||
<input type="hidden" name="m" value="addonsmanager" />
|
||
<input type="hidden" name="p" value="user_addons" />
|
||
<input type="hidden" name="home_id" value="<?php echo $home_id; ?>" />
|
||
<input type="hidden" name="mod_id" value="<?php echo $mod_id; ?>" />
|
||
<input type="hidden" name="ip" value="<?php echo $ip; ?>" />
|
||
<input type="hidden" name="port" value="<?php echo $port; ?>" />
|
||
<input type="submit" value="<?php print_lang('back'); ?>" />
|
||
</form>
|
||
</td></tr>
|
||
</table>
|
||
<?php
|
||
}
|
||
}
|
||
?>
|