Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pterodactyl
GitHub Repository: pterodactyl/panel
Path: blob/1.0-develop/app/Services/Eggs/EggConfigurationService.php
10281 views
1
<?php
2
3
namespace Pterodactyl\Services\Eggs;
4
5
use Illuminate\Support\Arr;
6
use Illuminate\Support\Str;
7
use Pterodactyl\Models\Server;
8
use Pterodactyl\Services\Servers\ServerConfigurationStructureService;
9
10
class EggConfigurationService
11
{
12
/**
13
* EggConfigurationService constructor.
14
*/
15
public function __construct(private ServerConfigurationStructureService $configurationStructureService)
16
{
17
}
18
19
/**
20
* Return an Egg file to be used by the Daemon.
21
*/
22
public function handle(Server $server): array
23
{
24
$configs = $this->replacePlaceholders(
25
$server,
26
json_decode($server->egg->inherit_config_files)
27
);
28
29
return [
30
'startup' => $this->convertStartupToNewFormat(json_decode($server->egg->inherit_config_startup, true)),
31
'stop' => $this->convertStopToNewFormat($server->egg->inherit_config_stop),
32
'configs' => $configs,
33
];
34
}
35
36
/**
37
* Convert the "done" variable into an array if it is not currently one.
38
*/
39
protected function convertStartupToNewFormat(array $startup): array
40
{
41
$done = Arr::get($startup, 'done');
42
43
return [
44
'done' => is_string($done) ? [$done] : $done,
45
'user_interaction' => [],
46
'strip_ansi' => Arr::get($startup, 'strip_ansi') ?? false,
47
];
48
}
49
50
/**
51
* Converts a legacy stop string into a new generation stop option for a server.
52
*
53
* For most eggs, this ends up just being a command sent to the server console, but
54
* if the stop command is something starting with a caret (^), it will be converted
55
* into the associated kill signal for the instance.
56
*/
57
protected function convertStopToNewFormat(string $stop): array
58
{
59
if (!Str::startsWith($stop, '^')) {
60
return [
61
'type' => 'command',
62
'value' => $stop,
63
];
64
}
65
66
$signal = substr($stop, 1);
67
68
return [
69
'type' => 'signal',
70
'value' => strtoupper($signal),
71
];
72
}
73
74
protected function replacePlaceholders(Server $server, object $configs): array
75
{
76
// Get the legacy configuration structure for the server so that we
77
// can property map the egg placeholders to values.
78
$structure = $this->configurationStructureService->handle($server, [], true);
79
80
$response = [];
81
// Normalize the output of the configuration for the new Wings Daemon to more
82
// easily ingest, as well as make things more flexible down the road.
83
foreach ($configs as $file => $data) {
84
// Try to head off any errors relating to parsing a set of configuration files
85
// or other JSON data for the egg. This should probably be blocked at the time
86
// of egg creation/update, but it isn't so this check will at least prevent a
87
// 500 error which would crash the entire Wings boot process.
88
//
89
// @see https://github.com/pterodactyl/panel/issues/3055
90
if (!is_object($data) || !isset($data->find)) {
91
continue;
92
}
93
94
$append = array_merge((array) $data, ['file' => $file, 'replace' => []]);
95
96
foreach ($this->iterate($data->find, $structure) as $find => $replace) {
97
if (is_object($replace)) {
98
foreach ($replace as $match => $replaceWith) {
99
$append['replace'][] = [
100
'match' => $find,
101
'if_value' => $match,
102
'replace_with' => $replaceWith,
103
];
104
}
105
106
continue;
107
}
108
109
$append['replace'][] = [
110
'match' => $find,
111
'replace_with' => $replace,
112
];
113
}
114
115
unset($append['find']);
116
117
$response[] = $append;
118
}
119
120
return $response;
121
}
122
123
/**
124
* Replaces the legacy modifies from eggs with their new counterpart. The legacy Daemon would
125
* set SERVER_MEMORY, SERVER_IP, and SERVER_PORT with their respective values on the Daemon
126
* side. Ensure that anything referencing those properly replaces them with the matching config
127
* value.
128
*/
129
protected function replaceLegacyModifiers(string $key, string $value): string
130
{
131
switch ($key) {
132
case 'config.docker.interface':
133
$replace = 'config.docker.network.interface';
134
break;
135
case 'server.build.env.SERVER_MEMORY':
136
case 'env.SERVER_MEMORY':
137
$replace = 'server.build.memory';
138
break;
139
case 'server.build.env.SERVER_IP':
140
case 'env.SERVER_IP':
141
$replace = 'server.build.default.ip';
142
break;
143
case 'server.build.env.SERVER_PORT':
144
case 'env.SERVER_PORT':
145
$replace = 'server.build.default.port';
146
break;
147
default:
148
// By default, we don't need to change anything, only if we ended up matching a specific legacy item.
149
$replace = $key;
150
}
151
152
return str_replace("{{{$key}}}", "{{{$replace}}}", $value);
153
}
154
155
protected function matchAndReplaceKeys(mixed $value, array $structure): mixed
156
{
157
preg_match_all('/{{(?<key>[\w.-]*)}}/', $value, $matches);
158
159
foreach ($matches['key'] as $key) {
160
// Matched something in {{server.X}} format, now replace that with the actual
161
// value from the server properties.
162
//
163
// The Daemon supports server.X, env.X, and config.X placeholders.
164
if (!Str::startsWith($key, ['server.', 'env.', 'config.'])) {
165
continue;
166
}
167
168
// Don't do a replacement on anything that is not a string, we don't want to unintentionally
169
// modify the resulting output.
170
if (!is_string($value)) {
171
continue;
172
}
173
174
$value = $this->replaceLegacyModifiers($key, $value);
175
176
// We don't want to do anything with config keys since the Daemon will need to handle
177
// that. For example, the Spigot egg uses "config.docker.interface" to identify the Docker
178
// interface to proxy through, but the Panel would be unaware of that.
179
if (Str::startsWith($key, 'config.')) {
180
continue;
181
}
182
183
// Replace anything starting with "server." with the value out of the server configuration
184
// array that used to be created for the old daemon.
185
if (Str::startsWith($key, 'server.')) {
186
$plucked = Arr::get($structure, preg_replace('/^server\./', '', $key), '');
187
188
$value = str_replace("{{{$key}}}", $plucked, $value);
189
continue;
190
}
191
192
// Finally, replace anything starting with env. with the expected environment
193
// variable from the server configuration.
194
$plucked = Arr::get(
195
$structure,
196
preg_replace('/^env\./', 'build.env.', $key),
197
''
198
);
199
200
$value = str_replace("{{{$key}}}", $plucked, $value);
201
}
202
203
return $value;
204
}
205
206
/**
207
* Iterates over a set of "find" values for a given file in the parser configuration. If
208
* the value of the line match is something iterable, continue iterating, otherwise perform
209
* a match & replace.
210
*/
211
private function iterate(mixed $data, array $structure): mixed
212
{
213
if (!is_iterable($data) && !is_object($data)) {
214
return $data;
215
}
216
217
// Remember, in PHP objects are always passed by reference, so if we do not clone this object
218
// instance we'll end up making modifications to the object outside the scope of this function
219
// which leads to some fun behavior in the parser.
220
if (is_array($data)) {
221
// Copy the array.
222
// NOTE: if the array contains any objects, they will be passed by reference.
223
$clone = $data;
224
} else {
225
$clone = clone $data;
226
}
227
foreach ($clone as $key => &$value) {
228
if (is_iterable($value) || is_object($value)) {
229
$value = $this->iterate($value, $structure);
230
231
continue;
232
}
233
234
$value = $this->matchAndReplaceKeys($value, $structure);
235
}
236
237
return $clone;
238
}
239
}
240
241