GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — develop ( 91ce97...de923b )
by Dane
02:47
created

ServerRepository::toggleAccess()   B

Complexity

Conditions 6
Paths 1

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 21
rs 8.7624
cc 6
eloc 13
nc 1
nop 2
1
<?php
2
/**
3
 * Pterodactyl - Panel
4
 * Copyright (c) 2015 - 2017 Dane Everitt <[email protected]>.
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in all
14
 * copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
 * SOFTWARE.
23
 */
24
25
namespace Pterodactyl\Repositories;
26
27
use DB;
28
use Crypt;
29
use Validator;
30
use Pterodactyl\Models;
31
use Pterodactyl\Services\UuidService;
32
use GuzzleHttp\Exception\ClientException;
33
use GuzzleHttp\Exception\TransferException;
34
use Pterodactyl\Services\DeploymentService;
35
use Pterodactyl\Exceptions\DisplayException;
36
use Pterodactyl\Exceptions\DisplayValidationException;
37
38
class ServerRepository
39
{
40
    /**
41
     * An array of daemon permission to assign to this server.
42
     *
43
     * @var array
44
     */
45
    protected $daemonPermissions = [
46
        's:*',
47
    ];
48
49
    /**
50
     * Generates a SFTP username for a server given a server name.
51
     * format: mumble_67c7a4b0.
52
     *
53
     * @param  string       $name
54
     * @param  null|string  $identifier
55
     * @return string
56
     */
57
    protected function generateSFTPUsername($name, $identifier = null)
58
    {
59
        if (is_null($identifier) || ! ctype_alnum($identifier)) {
60
            $unique = str_random(8);
61
        } else {
62
            if (strlen($identifier) < 8) {
63
                $unique = $identifier . str_random((8 - strlen($identifier)));
64
            } else {
65
                $unique = substr($identifier, 0, 8);
66
            }
67
        }
68
69
        // Filter the Server Name
70
        $name = trim(preg_replace('/[^\w]+/', '', $name), '_');
71
        $name = (strlen($name) < 1) ? str_random(6) : $name;
72
73
        return strtolower(substr($name, 0, 6) . '_' . $unique);
74
    }
75
76
    /**
77
     * Adds a new server to the system.
78
     *
79
     * @param   array  $data
80
     * @return \Pterodactyl\Models\Server
81
     *
82
     * @throws \Pterodactyl\Exceptions\DisplayException
83
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
84
     */
85
    public function create(array $data)
86
    {
87
88
        // Validate Fields
89
        $validator = Validator::make($data, [
90
            'user_id' => 'required|exists:users,id',
91
            'name' => 'required|regex:/^([\w .-]{1,200})$/',
92
            'description' => 'sometimes|nullable|string',
93
            'memory' => 'required|numeric|min:0',
94
            'swap' => 'required|numeric|min:-1',
95
            'io' => 'required|numeric|min:10|max:1000',
96
            'cpu' => 'required|numeric|min:0',
97
            'disk' => 'required|numeric|min:0',
98
            'service_id' => 'required|numeric|min:1|exists:services,id',
99
            'option_id' => 'required|numeric|min:1|exists:service_options,id',
100
            'location_id' => 'required|numeric|min:1|exists:locations,id',
101
            'pack_id' => 'sometimes|nullable|numeric|min:0',
102
            'custom_container' => 'string',
103
            'startup' => 'string',
104
            'auto_deploy' => 'sometimes|required|accepted',
105
            'custom_id' => 'sometimes|required|numeric|unique:servers,id',
106
        ]);
107
108
        $validator->sometimes('node_id', 'required|numeric|min:1|exists:nodes,id', function ($input) {
109
            return ! ($input->auto_deploy);
110
        });
111
112
        $validator->sometimes('allocation_id', 'required|numeric|exists:allocations,id', function ($input) {
113
            return ! ($input->auto_deploy);
114
        });
115
116
        $validator->sometimes('allocation_additional.*', 'sometimes|required|numeric|exists:allocations,id', function ($input) {
117
            return ! ($input->auto_deploy);
118
        });
119
120
        // Run validator, throw catchable and displayable exception if it fails.
121
        // Exception includes a JSON result of failed validation rules.
122
        if ($validator->fails()) {
123
            throw new DisplayValidationException(json_encode($validator->errors()));
124
        }
125
126
        $user = Models\User::findOrFail($data['user_id']);
127
128
        $autoDeployed = false;
129
        if (isset($data['auto_deploy']) && $data['auto_deploy']) {
130
            // This is an auto-deployment situation
131
            // Ignore any other passed node data
132
            unset($data['node_id'], $data['allocation_id']);
133
134
            $autoDeployed = true;
135
            $node = DeploymentService::smartRandomNode($data['memory'], $data['disk'], $data['location_id']);
136
            $allocation = DeploymentService::randomAllocation($node->id);
137
        } else {
138
            $node = Models\Node::findOrFail($data['node_id']);
139
        }
140
141
        // Verify IP & Port are a.) free and b.) assigned to the node.
142
        // We know the node exists because of 'exists:nodes,id' in the validation
143
        if (! $autoDeployed) {
144
            $allocation = Models\Allocation::where('id', $data['allocation_id'])->where('node_id', $data['node_id'])->whereNull('server_id')->first();
145
        }
146
147
        // Something failed in the query, either that combo doesn't exist, or it is in use.
148
        if (! $allocation) {
0 ignored issues
show
Bug introduced by
The variable $allocation does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
149
            throw new DisplayException('The selected Allocation ID is either already in use, or unavaliable for this node.');
150
        }
151
152
        // Validate those Service Option Variables
153
        // We know the service and option exists because of the validation.
154
        // We need to verify that the option exists for the service, and then check for
155
        // any required variable fields. (fields are labeled env_<env_variable>)
156
        $option = Models\ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first();
157
        if (! $option) {
158
            throw new DisplayException('The requested service option does not exist for the specified service.');
159
        }
160
161
        // Validate the Pack
162
        if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) {
163
            $data['pack_id'] = null;
164
        } else {
165
            $pack = Models\Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first();
166
            if (! $pack) {
167
                throw new DisplayException('The requested service pack does not seem to exist for this combination.');
168
            }
169
        }
170
171
        // Load up the Service Information
172
        $service = Models\Service::find($option->service_id);
173
174
        // Check those Variables
175
        $variables = Models\ServiceVariable::where('option_id', $data['option_id'])->get();
176
        $variableList = [];
177
        if ($variables) {
178
            foreach ($variables as $variable) {
179
180
                // Is the variable required?
181
                if (! isset($data['env_' . $variable->env_variable])) {
182
                    if ($variable->required) {
183
                        throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.');
184
                    }
185
                    $variableList[] = [
186
                        'id' => $variable->id,
187
                        'env' => $variable->env_variable,
188
                        'val' => $variable->default_value,
189
                    ];
190
                    continue;
191
                }
192
193
                // Check aganist Regex Pattern
194
                if (! is_null($variable->regex) && ! preg_match($variable->regex, $data['env_' . $variable->env_variable])) {
195
                    throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').');
196
                }
197
198
                $variableList[] = [
199
                    'id' => $variable->id,
200
                    'env' => $variable->env_variable,
201
                    'val' => $data['env_' . $variable->env_variable],
202
                ];
203
                continue;
204
            }
205
        }
206
207
        // Check Overallocation
208
        if (! $autoDeployed) {
209
            if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) {
210
                $totals = Models\Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node_id', $node->id)->first();
211
212
                // Check memory limits
213 View Code Duplication
                if (is_numeric($node->memory_overallocate)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
214
                    $newMemory = $totals->memory + $data['memory'];
215
                    $memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100)));
216
                    if ($newMemory > $memoryLimit) {
217
                        throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.');
218
                    }
219
                }
220
221
                // Check Disk Limits
222 View Code Duplication
                if (is_numeric($node->disk_overallocate)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
223
                    $newDisk = $totals->disk + $data['disk'];
224
                    $diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100)));
225
                    if ($newDisk > $diskLimit) {
226
                        throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.');
227
                    }
228
                }
229
            }
230
        }
231
232
        DB::beginTransaction();
233
234
        try {
235
            $uuid = new UuidService;
236
237
            // Add Server to the Database
238
            $server = new Models\Server;
239
            $genUuid = $uuid->generate('servers', 'uuid');
240
            $genShortUuid = $uuid->generateShort('servers', 'uuidShort', $genUuid);
241
242
            if (isset($data['custom_id'])) {
243
                $server->id = $data['custom_id'];
244
            }
245
246
            $server->fill([
247
                'uuid' => $genUuid,
248
                'uuidShort' => $genShortUuid,
249
                'node_id' => $node->id,
250
                'name' => $data['name'],
251
                'description' => $data['description'],
252
                'suspended' => 0,
253
                'owner_id' => $user->id,
254
                'memory' => $data['memory'],
255
                'swap' => $data['swap'],
256
                'disk' => $data['disk'],
257
                'io' => $data['io'],
258
                'cpu' => $data['cpu'],
259
                'oom_disabled' => (isset($data['oom_disabled'])) ? true : false,
260
                'allocation_id' => $allocation->id,
261
                'service_id' => $data['service_id'],
262
                'option_id' => $data['option_id'],
263
                'pack_id' => $data['pack_id'],
264
                'startup' => $data['startup'],
265
                'daemonSecret' => $uuid->generate('servers', 'daemonSecret'),
266
                'image' => (isset($data['custom_container']) && ! empty($data['custom_container'])) ? $data['custom_container'] : $option->docker_image,
267
                'username' => $this->generateSFTPUsername($data['name'], $genShortUuid),
268
                'sftp_password' => Crypt::encrypt('not set'),
269
            ]);
270
            $server->save();
271
272
            // Mark Allocation in Use
273
            $allocation->server_id = $server->id;
274
            $allocation->save();
275
276
            // Add Additional Allocations
277
            if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) {
278
                foreach ($data['allocation_additional'] as $allocation) {
279
                    $model = Models\Allocation::where('id', $allocation)->where('node_id', $data['node_id'])->whereNull('server_id')->first();
280
                    if (! $model) {
281
                        continue;
282
                    }
283
284
                    $model->server_id = $server->id;
285
                    $model->save();
286
                }
287
            }
288
289
            // Add Variables
290
            $environmentVariables = [
291
                'STARTUP' => $data['startup'],
292
            ];
293
294
            foreach ($variableList as $item) {
295
                $environmentVariables[$item['env']] = $item['val'];
296
297
                Models\ServerVariable::create([
298
                    'server_id' => $server->id,
299
                    'variable_id' => $item['id'],
300
                    'variable_value' => $item['val'],
301
                ]);
302
            }
303
304
            $server->load('allocation', 'allocations');
305
            $node->guzzleClient(['X-Access-Token' => $node->daemonSecret])->request('POST', '/servers', [
306
                'json' => [
307
                    'uuid' => (string) $server->uuid,
308
                    'user' => $server->username,
309
                    'build' => [
310
                        'default' => [
311
                            'ip' => $server->allocation->ip,
312
                            'port' => $server->allocation->port,
313
                        ],
314
                        'ports' => $server->allocations->groupBy('ip')->map(function ($item) {
315
                            return $item->pluck('port');
316
                        })->toArray(),
317
                        'env' => $environmentVariables,
318
                        'memory' => (int) $server->memory,
319
                        'swap' => (int) $server->swap,
320
                        'io' => (int) $server->io,
321
                        'cpu' => (int) $server->cpu,
322
                        'disk' => (int) $server->disk,
323
                        'image' => $server->image,
324
                    ],
325
                    'service' => [
326
                        'type' => $service->folder,
327
                        'option' => $option->tag,
328
                        'pack' => (isset($pack)) ? $pack->uuid : null,
329
                    ],
330
                    'keys' => [
331
                        (string) $server->daemonSecret => $this->daemonPermissions,
332
                    ],
333
                    'rebuild' => false,
334
                    'start_on_completion' => isset($data['start_on_completion']),
335
                ],
336
            ]);
337
338
            DB::commit();
339
340
            return $server;
341
        } catch (\Exception $ex) {
342
            DB::rollBack();
343
            throw $ex;
344
        }
345
    }
346
347
    /**
348
     * Update the details for a server.
349
     *
350
     * @param  int    $id
351
     * @param  array  $data
352
     * @return \Pterodactyl\Models\Server
353
     *
354
     * @throws \Pterodactyl\Exceptions\DisplayException
355
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
356
     */
357
    public function updateDetails($id, array $data)
358
    {
359
        $uuid = new UuidService;
360
        $resetDaemonKey = false;
361
362
        // Validate Fields
363
        $validator = Validator::make($data, [
364
            'owner_id' => 'sometimes|required|integer|exists:users,id',
365
            'name' => 'sometimes|required|regex:([\w .-]{1,200})',
366
            'description' => 'sometimes|required|string',
367
            'reset_token' => 'sometimes|required|accepted',
368
        ]);
369
370
        // Run validator, throw catchable and displayable exception if it fails.
371
        // Exception includes a JSON result of failed validation rules.
372
        if ($validator->fails()) {
373
            throw new DisplayValidationException(json_encode($validator->errors()));
374
        }
375
376
        DB::beginTransaction();
377
378
        try {
379
            $server = Models\Server::with('user')->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
380
381
            // Update daemon secret if it was passed.
382
            if (isset($data['reset_token']) || (isset($data['owner_id']) && (int) $data['owner_id'] !== $server->user->id)) {
383
                $oldDaemonKey = $server->daemonSecret;
384
                $server->daemonSecret = $uuid->generate('servers', 'daemonSecret');
385
                $resetDaemonKey = true;
386
            }
387
388
            // Save our changes
389
            $server->fill($data)->save();
390
391
            // Do we need to update? If not, return successful.
392
            if (! $resetDaemonKey) {
393
                return DB::commit();
394
            }
395
396
            $res = $server->node->guzzleClient([
397
                'X-Access-Server' => $server->uuid,
398
                'X-Access-Token' => $server->node->daemonSecret,
399
            ])->request('PATCH', '/server', [
400
                'exceptions' => false,
401
                'json' => [
402
                    'keys' => [
403
                        (string) $oldDaemonKey => [],
0 ignored issues
show
Bug introduced by
The variable $oldDaemonKey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
404
                        (string) $server->daemonSecret => $this->daemonPermissions,
405
                    ],
406
                ],
407
            ]);
408
409
            if ($res->getStatusCode() === 204) {
410
                DB::commit();
411
412
                return $server;
413
            } else {
414
                throw new DisplayException('Daemon returned a a non HTTP/204 error code. HTTP/' + $res->getStatusCode());
415
            }
416
        } catch (\Exception $ex) {
417
            DB::rollBack();
418
            throw $ex;
419
        }
420
    }
421
422
    /**
423
     * Update the container for a server.
424
     *
425
     * @param  int    $id
426
     * @param  array  $data
427
     * @return \Pterodactyl\Models\Server
428
     *
429
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
430
     */
431
    public function updateContainer($id, array $data)
432
    {
433
        $validator = Validator::make($data, [
434
            'docker_image' => 'required|string',
435
        ]);
436
437
        // Run validator, throw catchable and displayable exception if it fails.
438
        // Exception includes a JSON result of failed validation rules.
439
        if ($validator->fails()) {
440
            throw new DisplayValidationException(json_encode($validator->errors()));
441
        }
442
443
        DB::beginTransaction();
444
        try {
445
            $server = Models\Server::findOrFail($id);
446
447
            $server->image = $data['docker_image'];
448
            $server->save();
449
450
            $server->node->guzzleClient([
451
                'X-Access-Server' => $server->uuid,
452
                'X-Access-Token' => $server->node->daemonSecret,
453
            ])->request('PATCH', '/server', [
454
                'json' => [
455
                    'build' => [
456
                        'image' => $server->image,
457
                    ],
458
                ],
459
            ]);
460
461
            DB::commit();
462
463
            return $server;
464
        } catch (\Exception $ex) {
465
            DB::rollBack();
466
            throw $ex;
467
        }
468
    }
469
470
    /**
471
     * Update the build details for a server.
472
     *
473
     * @param  int    $id
474
     * @param  array  $data
475
     * @return \Pterodactyl\Models\Server
476
     *
477
     * @throws \Pterodactyl\Exceptions\DisplayException
478
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
479
     */
480
    public function changeBuild($id, array $data)
481
    {
482
        $validator = Validator::make($data, [
483
            'allocation_id' => 'sometimes|required|exists:allocations,id',
484
            'add_allocations' => 'sometimes|required|array',
485
            'remove_allocations' => 'sometimes|required|array',
486
            'memory' => 'sometimes|required|integer|min:0',
487
            'swap' => 'sometimes|required|integer|min:-1',
488
            'io' => 'sometimes|required|integer|min:10|max:1000',
489
            'cpu' => 'sometimes|required|integer|min:0',
490
            'disk' => 'sometimes|required|integer|min:0',
491
        ]);
492
493
        // Run validator, throw catchable and displayable exception if it fails.
494
        // Exception includes a JSON result of failed validation rules.
495
        if ($validator->fails()) {
496
            throw new DisplayValidationException(json_encode($validator->errors()));
497
        }
498
499
        DB::beginTransaction();
500
501
        try {
502
            $server = Models\Server::with('allocation', 'allocations')->findOrFail($id);
503
            $newBuild = [];
504
            $newAllocations = [];
0 ignored issues
show
Unused Code introduced by
$newAllocations is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
505
506
            if (isset($data['allocation_id'])) {
507
                if ((int) $data['allocation_id'] !== $server->allocation_id) {
508
                    $selection = $server->allocations->where('id', $data['allocation_id'])->first();
509
                    if (! $selection) {
510
                        throw new DisplayException('The requested default connection is not allocated to this server.');
511
                    }
512
513
                    $server->allocation_id = $selection->id;
514
                    $newBuild['default'] = ['ip' => $selection->ip, 'port' => $selection->port];
515
516
                    $server->load('allocation');
517
                }
518
            }
519
520
            $newPorts = false;
521
            // Remove Assignments
522 View Code Duplication
            if (isset($data['remove_allocations'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
523
                foreach ($data['remove_allocations'] as $allocation) {
524
                    // Can't remove the assigned IP/Port combo
525
                    if ((int) $allocation === $server->allocation_id) {
526
                        continue;
527
                    }
528
529
                    $newPorts = true;
530
                    Models\Allocation::where('id', $allocation)->where('server_id', $server->id)->update([
531
                        'server_id' => null,
532
                    ]);
533
                }
534
535
                $server->load('allocations');
536
            }
537
538
            // Add Assignments
539 View Code Duplication
            if (isset($data['add_allocations'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
540
                foreach ($data['add_allocations'] as $allocation) {
541
                    $model = Models\Allocation::where('id', $allocation)->whereNull('server_id')->first();
542
                    if (! $model) {
543
                        continue;
544
                    }
545
546
                    $newPorts = true;
547
                    $model->update([
548
                        'server_id' => $server->id,
549
                    ]);
550
                }
551
552
                $server->load('allocations');
553
            }
554
555
            if ($newPorts) {
556
                $newBuild['ports|overwrite'] = $server->allocations->groupBy('ip')->map(function ($item) {
557
                    return $item->pluck('port');
558
                })->toArray();
559
            }
560
561
            // @TODO: verify that server can be set to this much memory without
562
            // going over node limits.
563 View Code Duplication
            if (isset($data['memory']) && $server->memory !== (int) $data['memory']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
564
                $server->memory = $data['memory'];
565
                $newBuild['memory'] = (int) $server->memory;
566
            }
567
568 View Code Duplication
            if (isset($data['swap']) && $server->swap !== (int) $data['swap']) {
569
                $server->swap = $data['swap'];
570
                $newBuild['swap'] = (int) $server->swap;
571
            }
572
573
            // @TODO: verify that server can be set to this much disk without
574
            // going over node limits.
575 View Code Duplication
            if (isset($data['disk']) && $server->disk !== (int) $data['disk']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
576
                $server->disk = $data['disk'];
577
                $newBuild['disk'] = (int) $server->disk;
578
            }
579
580 View Code Duplication
            if (isset($data['cpu']) && $server->cpu !== (int) $data['cpu']) {
581
                $server->cpu = $data['cpu'];
582
                $newBuild['cpu'] = (int) $server->cpu;
583
            }
584
585 View Code Duplication
            if (isset($data['io']) && $server->io !== (int) $data['io']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
586
                $server->io = $data['io'];
587
                $newBuild['io'] = (int) $server->io;
588
            }
589
590
            // Try save() here so if it fails we haven't contacted the daemon
591
            // This won't be committed unless the HTTP request succeedes anyways
592
            $server->save();
593
594
            if (! empty($newBuild)) {
595
                $server->node->guzzleClient([
596
                    'X-Access-Server' => $server->uuid,
597
                    'X-Access-Token' => $server->node->daemonSecret,
598
                ])->request('PATCH', '/server', [
599
                    'json' => [
600
                        'build' => $newBuild,
601
                    ],
602
                ]);
603
            }
604
605
            DB::commit();
606
607
            return $server;
608
        } catch (\Exception $ex) {
609
            DB::rollBack();
610
            throw $ex;
611
        }
612
    }
613
614
    /**
615
     * Update the startup details for a server.
616
     *
617
     * @param  int    $id
618
     * @param  array  $data
619
     * @param  bool   $admin
620
     * @return void
621
     *
622
     * @throws \GuzzleHttp\Exception\RequestException
623
     * @throws \Pterodactyl\Exceptions\DisplayException
624
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
625
     */
626
    public function updateStartup($id, array $data, $admin = false)
627
    {
628
        $server = Models\Server::with('variables', 'option.variables')->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
629
630
        DB::transaction(function () use ($admin, $data, $server) {
631
            if (isset($data['startup']) && $admin) {
632
                $server->startup = $data['startup'];
633
                $server->save();
634
            }
635
636
            if ($server->option->variables) {
637
                foreach ($server->option->variables as &$variable) {
638
                    $set = isset($data['env_' . $variable->id]);
639
640
                    // If user is not an admin and are trying to edit a non-editable field
641
                    // or an invisible field just silently skip the variable.
642
                    if (! $admin && (! $variable->user_editable || ! $variable->user_viewable)) {
643
                        continue;
644
                    }
645
646
                    // Perform Field Validation
647
                    $validator = Validator::make([
648
                        'variable_value' => ($set) ? $data['env_' . $variable->id] : null,
649
                    ], [
650
                        'variable_value' => $variable->rules,
651
                    ]);
652
653
                    if ($validator->fails()) {
654
                        throw new DisplayValidationException(json_encode(
655
                            collect([
656
                                'notice' => ['There was a validation error with the `' . $variable->name . '` variable.'],
657
                            ])->merge($validator->errors()->toArray())
658
                        ));
659
                    }
660
661
                    $svar = Models\ServerVariable::firstOrNew([
662
                        'server_id' => $server->id,
663
                        'variable_id' => $variable->id,
664
                    ]);
665
666
                    // Set the value; if one was not passed set it to the default value
667
                    if ($set) {
668
                        $svar->variable_value = $data['env_' . $variable->id];
669
670
                    // Not passed, check if this record exists if so keep value, otherwise set default
671
                    } else {
672
                        $svar->variable_value = ($svar->exists) ? $svar->variable_value : $variable->default_value;
673
                    }
674
675
                    $svar->save();
676
                }
677
            }
678
679
            // Reload Variables
680
            $server->load('variables');
681 View Code Duplication
            $environment = $server->option->variables->map(function ($item, $key) use ($server) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
682
                $display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first();
683
684
                return [
685
                    'variable' => $item->env_variable,
686
                    'value' => (! is_null($display)) ? $display : $item->default_value,
687
                ];
688
            });
689
690
            $server->node->guzzleClient([
691
                'X-Access-Server' => $server->uuid,
692
                'X-Access-Token' => $server->node->daemonSecret,
693
            ])->request('PATCH', '/server', [
694
                'json' => [
695
                    'build' => [
696
                        'env|overwrite' => $environment->pluck('value', 'variable')->merge(['STARTUP' => $server->startup]),
697
                    ],
698
                ],
699
            ]);
700
        });
701
    }
702
703
    /**
704
     * Delete a server from the system permanetly.
705
     *
706
     * @param  int   $id
707
     * @param  bool  $force
708
     * @return void
709
     *
710
     * @throws \Pterodactyl\Exceptions\DisplayException
711
     */
712
    public function delete($id, $force = false)
713
    {
714
        $server = Models\Server::with('node', 'allocations', 'variables')->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
715
716
        // Due to MySQL lockouts if the daemon response fails, we need to
717
        // delete the server from the daemon first. If it succeedes and then
718
        // MySQL fails, users just need to force delete the server.
719
        //
720
        // If this is a force delete, continue anyways.
721
        try {
722
            $server->node->guzzleClient([
723
                'X-Access-Token' => $server->node->daemonSecret,
724
                'X-Access-Server' => $server->uuid,
725
            ])->request('DELETE', '/servers');
726
        } catch (ClientException $ex) {
727
            // Exception is thrown on 4XX HTTP errors, so catch and determine
728
            // if we should continue, or if there is a permissions error.
729
            //
730
            // Daemon throws a 404 if the server doesn't exist, if that is returned
731
            // continue with deletion, even if not a force deletion.
732
            $response = $ex->getResponse();
733
            if ($ex->getResponse()->getStatusCode() !== 404 && ! $force) {
734
                throw new DisplayException($ex->getMessage());
735
            }
736
        } catch (TransferException $ex) {
737
            if (! $force) {
738
                throw new DisplayException($ex->getMessage());
739
            }
740
        } catch (\Exception $ex) {
741
            throw $ex;
742
        }
743
744
        DB::transaction(function () use ($server) {
745
            $server->allocations->each(function ($item) {
746
                $item->server_id = null;
747
                $item->save();
748
            });
749
750
            $server->variables->each->delete();
751
752
            $server->load('subusers.permissions');
753
            $server->subusers->each(function ($subuser) {
754
                $subuser->permissions->each(function ($permission) {
755
                    $perm->delete();
756
                });
757
                $subuser->delete();
758
            });
759
760
            $server->downloads->each->delete();
761
            $server->tasks->each->delete();
762
763
            // Delete Databases
764
            // This is the one un-recoverable point where
765
            // transactions will not save us.
766
            $repository = new DatabaseRepository;
767
            $server->databases->each(function ($item) {
768
                $repository->drop($item->id);
769
            });
770
771
            // Fully delete the server.
772
            $server->delete();
773
        });
774
    }
775
776
    /**
777
     * Toggle the install status of a serve.
778
     *
779
     * @param  int    $id
780
     * @return bool
781
     *
782
     * @throws \Pterodactyl\Exceptions\DisplayException
783
     */
784
    public function toggleInstall($id)
785
    {
786
        $server = Models\Server::findOrFail($id);
787
        if ($server->installed > 1) {
788
            throw new DisplayException('This server was marked as having a failed install or being deleted, you cannot override this.');
789
        }
790
        $server->installed = ! $server->installed;
791
792
        return $server->save();
793
    }
794
795
    /**
796
     * Suspends or unsuspends a server.
797
     *
798
     * @param  int   $id
799
     * @param  bool  $unsuspend
800
     * @return void
801
     */
802
    public function toggleAccess($id, $unsuspend = true)
803
    {
804
        $server = Models\Server::with('node')->findOrFail($id);
805
806
        DB::transaction(function () use ($server, $unsuspend) {
807
            if (
808
                (! $unsuspend && $server->suspended) ||
809
                ($unsuspend && ! $server->suspended)
810
            ) {
811
                return true;
812
            }
813
814
            $server->suspended = ! $unsuspend;
815
            $server->save();
816
817
            $server->node->guzzleClient([
818
                'X-Access-Token' => $server->node->daemonSecret,
819
                'X-Access-Server' => $server->uuid,
820
            ])->request('POST', ($unsuspend) ? '/server/unsuspend' : '/server/suspend');
821
        });
822
    }
823
824
    /**
825
     * Updates the SFTP password for a server.
826
     *
827
     * @param  int     $id
828
     * @param  string  $password
829
     * @return void
830
     *
831
     * @throws \Pterodactyl\Exceptions\DisplayValidationException
832
     */
833
    public function updateSFTPPassword($id, $password)
834
    {
835
        $server = Models\Server::with('node')->findOrFail($id);
0 ignored issues
show
Bug introduced by
The method findOrFail does only exist in Illuminate\Database\Eloquent\Builder, but not in Illuminate\Database\Eloquent\Model.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
836
837
        $validator = Validator::make(['password' => $password], [
838
            'password' => 'required|regex:/^((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})$/',
839
        ]);
840
841
        if ($validator->fails()) {
842
            throw new DisplayValidationException(json_encode($validator->errors()));
843
        }
844
845
        DB::transaction(function () use ($password, $server) {
846
            $server->sftp_password = Crypt::encrypt($password);
847
            $server->save();
848
849
            $server->node->guzzleClient([
850
                'X-Access-Token' => $server->node->daemonSecret,
851
                'X-Access-Server' => $server->uuid,
852
            ])->request('POST', '/server/password', [
853
                'json' => ['password' => $password],
854
            ]);
855
        });
856
    }
857
}
858