Path: blob/1.0-develop/app/Services/Eggs/EggConfigurationService.php
10281 views
<?php12namespace Pterodactyl\Services\Eggs;34use Illuminate\Support\Arr;5use Illuminate\Support\Str;6use Pterodactyl\Models\Server;7use Pterodactyl\Services\Servers\ServerConfigurationStructureService;89class EggConfigurationService10{11/**12* EggConfigurationService constructor.13*/14public function __construct(private ServerConfigurationStructureService $configurationStructureService)15{16}1718/**19* Return an Egg file to be used by the Daemon.20*/21public function handle(Server $server): array22{23$configs = $this->replacePlaceholders(24$server,25json_decode($server->egg->inherit_config_files)26);2728return [29'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),30'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),31'configs' => $configs,32];33}3435/**36* Convert the "done" variable into an array if it is not currently one.37*/38protected function convertStartupToNewFormat(array $startup): array39{40$done = Arr::get($startup, 'done');4142return [43'done' => is_string($done) ? [$done] : $done,44'user_interaction' => [],45'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,46];47}4849/**50* Converts a legacy stop string into a new generation stop option for a server.51*52* For most eggs, this ends up just being a command sent to the server console, but53* if the stop command is something starting with a caret (^), it will be converted54* into the associated kill signal for the instance.55*/56protected function convertStopToNewFormat(string $stop): array57{58if (!Str::startsWith($stop, '^')) {59return [60'type' => 'command',61'value' => $stop,62];63}6465$signal = substr($stop, 1);6667return [68'type' => 'signal',69'value' => strtoupper($signal),70];71}7273protected function replacePlaceholders(Server $server, object $configs): array74{75// Get the legacy configuration structure for the server so that we76// can property map the egg placeholders to values.77$structure = $this->configurationStructureService->handle($server, [], true);7879$response = [];80// Normalize the output of the configuration for the new Wings Daemon to more81// easily ingest, as well as make things more flexible down the road.82foreach ($configs as $file => $data) {83// Try to head off any errors relating to parsing a set of configuration files84// or other JSON data for the egg. This should probably be blocked at the time85// of egg creation/update, but it isn't so this check will at least prevent a86// 500 error which would crash the entire Wings boot process.87//88// @see https://github.com/pterodactyl/panel/issues/305589if (!is_object($data) || !isset($data->find)) {90continue;91}9293$append = array_merge((array) $data, ['file' => $file, 'replace' => []]);9495foreach ($this->iterate($data->find, $structure) as $find => $replace) {96if (is_object($replace)) {97foreach ($replace as $match => $replaceWith) {98$append['replace'][] = [99'match' => $find,100'if_value' => $match,101'replace_with' => $replaceWith,102];103}104105continue;106}107108$append['replace'][] = [109'match' => $find,110'replace_with' => $replace,111];112}113114unset($append['find']);115116$response[] = $append;117}118119return $response;120}121122/**123* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would124* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon125* side. Ensure that anything referencing those properly replaces them with the matching config126* value.127*/128protected function replaceLegacyModifiers(string $key, string $value): string129{130switch ($key) {131case 'config.docker.interface':132$replace = 'config.docker.network.interface';133break;134case 'server.build.env.SERVER_MEMORY':135case 'env.SERVER_MEMORY':136$replace = 'server.build.memory';137break;138case 'server.build.env.SERVER_IP':139case 'env.SERVER_IP':140$replace = 'server.build.default.ip';141break;142case 'server.build.env.SERVER_PORT':143case 'env.SERVER_PORT':144$replace = 'server.build.default.port';145break;146default:147// By default, we don't need to change anything, only if we ended up matching a specific legacy item.148$replace = $key;149}150151return str_replace("{{{$key}}}", "{{{$replace}}}", $value);152}153154protected function matchAndReplaceKeys(mixed $value, array $structure): mixed155{156preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);157158foreach ($matches['key'] as $key) {159// Matched something in {{server.X}} format, now replace that with the actual160// value from the server properties.161//162// The Daemon supports server.X, env.X, and config.X placeholders.163if (!Str::startsWith($key, ['server.', 'env.', 'config.'])) {164continue;165}166167// Don't do a replacement on anything that is not a string, we don't want to unintentionally168// modify the resulting output.169if (!is_string($value)) {170continue;171}172173$value = $this->replaceLegacyModifiers($key, $value);174175// We don't want to do anything with config keys since the Daemon will need to handle176// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker177// interface to proxy through, but the Panel would be unaware of that.178if (Str::startsWith($key, 'config.')) {179continue;180}181182// Replace anything starting with "server." with the value out of the server configuration183// array that used to be created for the old daemon.184if (Str::startsWith($key, 'server.')) {185$plucked = Arr::get($structure, preg_replace('/^server\./', '', $key), '');186187$value = str_replace("{{{$key}}}", $plucked, $value);188continue;189}190191// Finally, replace anything starting with env. with the expected environment192// variable from the server configuration.193$plucked = Arr::get(194$structure,195preg_replace('/^env\./', 'build.env.', $key),196''197);198199$value = str_replace("{{{$key}}}", $plucked, $value);200}201202return $value;203}204205/**206* Iterates over a set of "find" values for a given file in the parser configuration. If207* the value of the line match is something iterable, continue iterating, otherwise perform208* a match & replace.209*/210private function iterate(mixed $data, array $structure): mixed211{212if (!is_iterable($data) && !is_object($data)) {213return $data;214}215216// Remember, in PHP objects are always passed by reference, so if we do not clone this object217// instance we'll end up making modifications to the object outside the scope of this function218// which leads to some fun behavior in the parser.219if (is_array($data)) {220// Copy the array.221// NOTE: if the array contains any objects, they will be passed by reference.222$clone = $data;223} else {224$clone = clone $data;225}226foreach ($clone as $key => &$value) {227if (is_iterable($value) || is_object($value)) {228$value = $this->iterate($value, $structure);229230continue;231}232233$value = $this->matchAndReplaceKeys($value, $structure);234}235236return $clone;237}238}239240241