211 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace App\Services\Eggs;
 | 
						|
 | 
						|
use Illuminate\Support\Arr;
 | 
						|
use Illuminate\Support\Str;
 | 
						|
use App\Models\Server;
 | 
						|
use App\Services\Servers\ServerConfigurationStructureService;
 | 
						|
 | 
						|
class EggConfigurationService
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * EggConfigurationService constructor.
 | 
						|
     */
 | 
						|
    public function __construct(private ServerConfigurationStructureService $configurationStructureService)
 | 
						|
    {
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return an Egg file to be used by the Daemon.
 | 
						|
     */
 | 
						|
    public function handle(Server $server): array
 | 
						|
    {
 | 
						|
        $configs = $this->replacePlaceholders(
 | 
						|
            $server,
 | 
						|
            json_decode($server->egg->inherit_config_files)
 | 
						|
        );
 | 
						|
 | 
						|
        return [
 | 
						|
            'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
 | 
						|
            'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
 | 
						|
            'configs' => $configs,
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert the "done" variable into an array if it is not currently one.
 | 
						|
     */
 | 
						|
    protected function convertStartupToNewFormat(array $startup): array
 | 
						|
    {
 | 
						|
        $done = Arr::get($startup, 'done');
 | 
						|
 | 
						|
        return [
 | 
						|
            'done' => is_string($done) ? [$done] : $done,
 | 
						|
            'user_interaction' => [],
 | 
						|
            'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Converts a legacy stop string into a new generation stop option for a server.
 | 
						|
     *
 | 
						|
     * For most eggs, this ends up just being a command sent to the server console, but
 | 
						|
     * if the stop command is something starting with a caret (^), it will be converted
 | 
						|
     * into the associated kill signal for the instance.
 | 
						|
     */
 | 
						|
    protected function convertStopToNewFormat(string $stop): array
 | 
						|
    {
 | 
						|
        if (!Str::startsWith($stop, '^')) {
 | 
						|
            return [
 | 
						|
                'type' => 'command',
 | 
						|
                'value' => $stop,
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        $signal = substr($stop, 1);
 | 
						|
        if (strtoupper($signal) === 'C') {
 | 
						|
            return [
 | 
						|
                'type' => 'stop',
 | 
						|
                'value' => null,
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        return [
 | 
						|
            'type' => 'signal',
 | 
						|
            'value' => strtoupper($signal),
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    protected function replacePlaceholders(Server $server, object $configs): array
 | 
						|
    {
 | 
						|
        // Get the legacy configuration structure for the server so that we
 | 
						|
        // can property map the egg placeholders to values.
 | 
						|
        $structure = $this->configurationStructureService->handle($server);
 | 
						|
 | 
						|
        $response = [];
 | 
						|
        // Normalize the output of the configuration for the new Daemon to more
 | 
						|
        // easily ingest, as well as make things more flexible down the road.
 | 
						|
        foreach ($configs as $file => $data) {
 | 
						|
            // Try to head off any errors relating to parsing a set of configuration files
 | 
						|
            // or other JSON data for the egg. This should probably be blocked at the time
 | 
						|
            // of egg creation/update, but it isn't so this check will at least prevent a
 | 
						|
            // 500 error which would crash the entire daemon boot process.
 | 
						|
            if (!is_object($data) || !isset($data->find)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $append = array_merge((array) $data, ['file' => $file, 'replace' => []]);
 | 
						|
 | 
						|
            foreach ($this->iterate($data->find, $structure) as $find => $replace) {
 | 
						|
                if (is_object($replace)) {
 | 
						|
                    foreach ($replace as $match => $replaceWith) {
 | 
						|
                        $append['replace'][] = [
 | 
						|
                            'match' => $find,
 | 
						|
                            'if_value' => $match,
 | 
						|
                            'replace_with' => $replaceWith,
 | 
						|
                        ];
 | 
						|
                    }
 | 
						|
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $append['replace'][] = [
 | 
						|
                    'match' => $find,
 | 
						|
                    'replace_with' => $replace,
 | 
						|
                ];
 | 
						|
            }
 | 
						|
 | 
						|
            unset($append['find']);
 | 
						|
 | 
						|
            $response[] = $append;
 | 
						|
        }
 | 
						|
 | 
						|
        return $response;
 | 
						|
    }
 | 
						|
 | 
						|
    protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
 | 
						|
    {
 | 
						|
        preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
 | 
						|
 | 
						|
        foreach ($matches['key'] as $key) {
 | 
						|
            // Matched something in {{server.X}} format, now replace that with the actual
 | 
						|
            // value from the server properties.
 | 
						|
            //
 | 
						|
            // The Daemon supports server.X, env.X, and config.X placeholders.
 | 
						|
            if (!Str::startsWith($key, ['server.', 'env.', 'config.'])) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Don't do a replacement on anything that is not a string, we don't want to unintentionally
 | 
						|
            // modify the resulting output.
 | 
						|
            if (!is_string($value)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // We don't want to do anything with config keys since the Daemon will need to handle
 | 
						|
            // that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
 | 
						|
            // interface to proxy through, but the Panel would be unaware of that.
 | 
						|
            if (Str::startsWith($key, 'config.')) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Replace anything starting with "server." with the value out of the server configuration
 | 
						|
            // array that used to be created for the old daemon.
 | 
						|
            if (Str::startsWith($key, 'server.')) {
 | 
						|
                $plucked = Arr::get($structure, preg_replace('/^server\./', '', $key), '');
 | 
						|
 | 
						|
                $value = str_replace("{{{$key}}}", $plucked, $value);
 | 
						|
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            // Finally, replace anything starting with env. with the expected environment
 | 
						|
            // variable from the server configuration.
 | 
						|
            $plucked = Arr::get(
 | 
						|
                $structure,
 | 
						|
                preg_replace('/^env\./', 'environment.', $key),
 | 
						|
                ''
 | 
						|
            );
 | 
						|
 | 
						|
            $value = str_replace("{{{$key}}}", $plucked, $value);
 | 
						|
        }
 | 
						|
 | 
						|
        return $value;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Iterates over a set of "find" values for a given file in the parser configuration. If
 | 
						|
     * the value of the line match is something iterable, continue iterating, otherwise perform
 | 
						|
     * a match & replace.
 | 
						|
     */
 | 
						|
    private function iterate(mixed $data, array $structure): mixed
 | 
						|
    {
 | 
						|
        if (!is_iterable($data) && !is_object($data)) {
 | 
						|
            return $data;
 | 
						|
        }
 | 
						|
 | 
						|
        // Remember, in PHP objects are always passed by reference, so if we do not clone this object
 | 
						|
        // instance we'll end up making modifications to the object outside the scope of this function
 | 
						|
        // which leads to some fun behavior in the parser.
 | 
						|
        if (is_array($data)) {
 | 
						|
            // Copy the array.
 | 
						|
            // NOTE: if the array contains any objects, they will be passed by reference.
 | 
						|
            $clone = $data;
 | 
						|
        } else {
 | 
						|
            $clone = clone $data;
 | 
						|
        }
 | 
						|
        foreach ($clone as $key => &$value) {
 | 
						|
            if (is_iterable($value) || is_object($value)) {
 | 
						|
                $value = $this->iterate($value, $structure);
 | 
						|
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            $value = $this->matchAndReplaceKeys($value, $structure);
 | 
						|
        }
 | 
						|
 | 
						|
        return $clone;
 | 
						|
    }
 | 
						|
}
 |