repository = $repository; $this->configurationStructureService = $configurationStructureService; } /** * Return an Egg file to be used by the Daemon. * * @param \Pterodactyl\Models\Server $server * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ 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. * * @param array $startup * @return array */ protected function convertStartupToNewFormat(array $startup) { $done = Arr::get($startup, 'done'); return [ 'done' => is_string($done) ? [$done] : $done, 'user_interaction' => Arr::get($startup, 'userInteraction') ?? [], '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. * * @param string $stop * @return array */ 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), ]; } /** * @param \Pterodactyl\Models\Server $server * @param object $configs * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ protected function replacePlaceholders(Server $server, object $configs) { // Get the legacy configuration structure for the server so that we // can property map the egg placeholders to values. $structure = $this->configurationStructureService->handle($server, true); foreach ($configs as $file => &$data) { $this->iterate($data->find, $structure); } $response = []; // Normalize the output of the configuration for the new Wings Daemon to more // easily ingest, as well as make things more flexible down the road. foreach ($configs as $file => $data) { $append = ['file' => $file, 'replace' => []]; // I like to think I understand PHP pretty well, but if you don't pass $value // by reference here, you'll end up with a resursive array loop if the config // file has two replacements that reference the same item in the configuration // array for the server. foreach ($data as $key => &$value) { if ($key !== 'find') { $append[$key] = $value; continue; } foreach ($value 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, ]; } } $response[] = $append; } return $response; } /** * @param string $value * @param array $structure * @return string|null */ protected function matchAndReplaceKeys(string $value, array $structure): ?string { preg_match('/{{(?.*)}}/', $value, $matches); if (! $key = $matches['key'] ?? null) { return self::NOT_MATCHED; } // 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.'])) { return self::NOT_MATCHED; } // The legacy Daemon would set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their // respective values on the Daemon side. Ensure that anything referencing those properly // replaces them with the matching config value. switch ($key) { case 'config.docker.interface': $key = 'config.docker.network.interface'; break; case 'server.build.env.SERVER_MEMORY': case 'env.SERVER_MEMORY': $key = 'server.build.memory'; break; case 'server.build.env.SERVER_IP': case 'env.SERVER_IP': $key = 'server.build.default.ip'; break; case 'server.build.env.SERVER_PORT': case 'env.SERVER_PORT': $key = 'server.build.default.port'; break; } // 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.')) { return preg_replace('/{{(.*)}}/', "{{{$key}}}", $value); } // 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), '' ); return preg_replace('/{{(.*)}}/', $plucked, $value); } // Finally, replace anything starting with env. with the expected environment // variable from the server configuration. $plucked = Arr::get( $structure, preg_replace('/^env\./', 'build.env.', $key), '' ); return preg_replace('/{{(.*)}}/', $plucked, $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. * * @param mixed $data * @param array $structure */ private function iterate(&$data, array $structure) { if (! is_iterable($data) && ! is_object($data)) { return; } foreach ($data as &$value) { if (is_iterable($value) || is_object($value)) { $this->iterate($value, $structure); continue; } $response = $this->matchAndReplaceKeys($value, $structure); if ($response === self::NOT_MATCHED) { continue; } $value = $response; } } }