Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace Pterodactyl\Http\Controllers\Api\Remote\Servers;

use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Webmozart\Assert\Assert;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Models\Allocation;
Expand Down Expand Up @@ -32,17 +35,20 @@ public function __construct(
*
* @throws \Throwable
*/
public function failure(string $uuid): JsonResponse
public function failure(Request $request, string $uuid): JsonResponse
{
$server = $this->repository->getByUuid($uuid);
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}

/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);

// Either node can tell the panel that the transfer has failed. Only the new node
// can tell the panel that it was successful.
if (! $server->node->is($transfer->newNode) && ! $server->node->is($transfer->oldNode)) {
if (! $node->is($transfer->newNode) && ! $node->is($transfer->oldNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

Expand All @@ -54,17 +60,20 @@ public function failure(string $uuid): JsonResponse
*
* @throws \Throwable
*/
public function success(string $uuid): JsonResponse
public function success(Request $request, string $uuid): JsonResponse
{
$server = $this->repository->getByUuid($uuid);
$transfer = $server->transfer;
if (is_null($transfer)) {
throw new ConflictHttpException('Server is not being transferred.');
}

/* @var Node $node */
Assert::isInstanceOf($node = $request->attributes->get('node'), Node::class);

// Only the new node communicates a successful state to the panel, so we should
// not allow the old node to hit this endpoint.
if (! $server->node->is($transfer->newNode)) {
if (! $node->is($transfer->newNode)) {
throw new HttpForbiddenException('Requesting node does not have permission to access this server.');
}

Expand Down
4 changes: 4 additions & 0 deletions app/Models/ServerTransfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory;

/**
* @property int $id
Expand All @@ -24,6 +25,9 @@
*/
class ServerTransfer extends Model
{
/** @use HasFactory<\Database\Factories\ServerTransferFactory> */
use HasFactory;

/**
* The resource name for this model when it is transformed into an
* API representation using fractal.
Expand Down
31 changes: 31 additions & 0 deletions database/Factories/ServerTransferFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Database\Factories;

use Pterodactyl\Models\ServerTransfer;
use Illuminate\Database\Eloquent\Factories\Factory;

class ServerTransferFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = ServerTransfer::class;

/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'old_additional_allocations' => [],
'new_additional_allocations' => [],
'successful' => null,
'archived' => false,
];
}
}
111 changes: 111 additions & 0 deletions tests/Integration/Api/Remote/ServerTransferControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace Pterodactyl\Tests\Integration\Api\Remote;

use Pterodactyl\Models\Node;
use Pterodactyl\Models\Location;
use Pterodactyl\Models\Allocation;
use Pterodactyl\Models\ServerTransfer;
use Pterodactyl\Tests\Integration\IntegrationTestCase;

class ServerTransferControllerTest extends IntegrationTestCase
{
protected ServerTransfer $transfer;

public function setup(): void
{
parent::setUp();

$server = $this->createServerModel();

$new = Node::factory()
->for(Location::factory())
->has(Allocation::factory())
->create();

$this->transfer = ServerTransfer::factory()->for($server)->create([
'old_allocation' => $server->allocation_id,
'new_allocation' => $new->allocations->first()->id,
'new_node' => $new->id,
'old_node' => $server->node_id,
]);
}

public function testSuccessStatusUpdateCanBeSentFromNewNode(): void
{
$server = $this->transfer->server;
$newNode = $this->transfer->newNode;

$this
->withHeader('Authorization', "Bearer $newNode->daemon_token_id." . $newNode->getDecryptedKey())
->postJson("/api/remote/servers/{$server->uuid}/transfer/success")
->assertNoContent();

$this->assertTrue($this->transfer->refresh()->successful);
}

public function testFailureStatusUpdateCanBeSentFromOldNode(): void
{
$server = $this->transfer->server;
$oldNode = $this->transfer->oldNode;

$this->withHeader('Authorization', "Bearer $oldNode->daemon_token_id." . $oldNode->getDecryptedKey())
->postJson("/api/remote/servers/{$server->uuid}/transfer/failure")
->assertNoContent();

$this->assertFalse($this->transfer->refresh()->successful);
}

public function testFailureStatusUpdateCanBeSentFromNewNode(): void
{
$server = $this->transfer->server;
$newNode = $this->transfer->newNode;

$this->withHeader('Authorization', "Bearer $newNode->daemon_token_id." . $newNode->getDecryptedKey())
->postJson("/api/remote/servers/{$server->uuid}/transfer/failure")
->assertNoContent();

$this->assertFalse($this->transfer->refresh()->successful);
}

public function testSuccessStatusUpdateCannotBeSentFromOldNode(): void
{
$server = $this->transfer->server;
$oldNode = $this->transfer->oldNode;

$this->withHeader('Authorization', "Bearer $oldNode->daemon_token_id." . $oldNode->getDecryptedKey())
->postJson("/api/remote/servers/{$server->uuid}/transfer/success")
->assertForbidden()
->assertJsonPath('errors.0.code', 'HttpForbiddenException')
->assertJsonPath('errors.0.detail', 'Requesting node does not have permission to access this server.');

$this->assertNull($this->transfer->refresh()->successful);
}

public function testSuccessStatusUpdateCannotBeSentFromUnauthorizedNode(): void
{
$server = $this->transfer->server;
$node = Node::factory()->for(Location::factory())->create();

$this->withHeader('Authorization', "Bearer $node->daemon_token_id." . $node->getDecryptedKey())
->postJson("/api/remote/servers/$server->uuid/transfer/success")
->assertForbidden()
->assertJsonPath('errors.0.code', 'HttpForbiddenException')
->assertJsonPath('errors.0.detail', 'Requesting node does not have permission to access this server.');

$this->assertNull($this->transfer->refresh()->successful);
}

public function testFailureStatusUpdateCannotBeSentFromUnauthorizedNode(): void
{
$server = $this->transfer->server;
$node = Node::factory()->for(Location::factory())->create();

$this->withHeader('Authorization', "Bearer $node->daemon_token_id." . $node->getDecryptedKey())
->postJson("/api/remote/servers/$server->uuid/transfer/failure")->assertForbidden()
->assertJsonPath('errors.0.code', 'HttpForbiddenException')
->assertJsonPath('errors.0.detail', 'Requesting node does not have permission to access this server.');

$this->assertNull($this->transfer->refresh()->successful);
}
}
Loading