2020-06-28 17:16:15 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Pterodactyl\Tests\Integration\Api\Client\Server;
|
|
|
|
|
|
|
|
use Carbon\CarbonImmutable;
|
|
|
|
use Illuminate\Http\Response;
|
2021-01-23 20:09:16 +00:00
|
|
|
use Lcobucci\JWT\Configuration;
|
2020-06-28 17:16:15 +00:00
|
|
|
use Pterodactyl\Models\Permission;
|
|
|
|
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
2021-01-23 20:09:16 +00:00
|
|
|
use Lcobucci\JWT\Signer\Key\InMemory;
|
|
|
|
use Lcobucci\JWT\Validation\Constraint\SignedWith;
|
2020-06-28 17:16:15 +00:00
|
|
|
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
|
|
|
|
|
|
|
class WebsocketControllerTest extends ClientApiIntegrationTestCase
|
|
|
|
{
|
|
|
|
/**
|
2022-10-14 16:59:20 +00:00
|
|
|
* Test that a subuser attempting to connect to the websocket receives an error if they
|
2020-06-28 17:16:15 +00:00
|
|
|
* do not explicitly have the permission.
|
|
|
|
*/
|
|
|
|
public function testSubuserWithoutWebsocketPermissionReceivesError()
|
|
|
|
{
|
|
|
|
[$user, $server] = $this->generateTestAccount([Permission::ACTION_CONTROL_RESTART]);
|
|
|
|
|
2022-10-14 16:59:20 +00:00
|
|
|
$this->actingAs($user)->getJson("/api/client/servers/$server->uuid/websocket")
|
2020-06-28 17:16:15 +00:00
|
|
|
->assertStatus(Response::HTTP_FORBIDDEN)
|
2020-12-24 18:23:04 +00:00
|
|
|
->assertJsonPath('errors.0.code', 'HttpForbiddenException')
|
2020-06-28 17:16:15 +00:00
|
|
|
->assertJsonPath('errors.0.detail', 'You do not have permission to connect to this server\'s websocket.');
|
|
|
|
}
|
|
|
|
|
2021-08-07 16:16:29 +00:00
|
|
|
/**
|
|
|
|
* Confirm users cannot access the websocket for another user's server.
|
|
|
|
*/
|
|
|
|
public function testUserWithoutPermissionForServerReceivesError()
|
|
|
|
{
|
2021-08-07 16:19:21 +00:00
|
|
|
[, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
|
2022-10-14 16:59:20 +00:00
|
|
|
[$user] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]);
|
2021-08-07 16:16:29 +00:00
|
|
|
|
2022-10-14 16:59:20 +00:00
|
|
|
$this->actingAs($user)->getJson("/api/client/servers/$server->uuid/websocket")
|
2021-08-07 16:16:29 +00:00
|
|
|
->assertStatus(Response::HTTP_NOT_FOUND);
|
|
|
|
}
|
|
|
|
|
2020-06-28 17:16:15 +00:00
|
|
|
/**
|
|
|
|
* Test that the expected permissions are returned for the server owner and that the JWT is
|
|
|
|
* configured correctly.
|
|
|
|
*/
|
|
|
|
public function testJwtAndWebsocketUrlAreReturnedForServerOwner()
|
|
|
|
{
|
|
|
|
/** @var \Pterodactyl\Models\User $user */
|
|
|
|
/** @var \Pterodactyl\Models\Server $server */
|
|
|
|
[$user, $server] = $this->generateTestAccount();
|
|
|
|
|
|
|
|
// Force the node to HTTPS since we want to confirm it gets transformed to wss:// in the URL.
|
|
|
|
$server->node->scheme = 'https';
|
|
|
|
$server->node->save();
|
|
|
|
|
2022-10-14 16:59:20 +00:00
|
|
|
$response = $this->actingAs($user)->getJson("/api/client/servers/$server->uuid/websocket");
|
2020-06-28 17:16:15 +00:00
|
|
|
|
|
|
|
$response->assertOk();
|
|
|
|
$response->assertJsonStructure(['data' => ['token', 'socket']]);
|
|
|
|
|
|
|
|
$connection = $response->json('data.socket');
|
|
|
|
$this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.');
|
2022-10-14 16:59:20 +00:00
|
|
|
$this->assertStringEndsWith("/api/servers/$server->uuid/ws", $connection, 'Failed asserting that websocket connection address uses expected Wings endpoint.');
|
2020-06-28 17:16:15 +00:00
|
|
|
|
2021-01-23 20:33:34 +00:00
|
|
|
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->getDecryptedKey()));
|
|
|
|
$config->setValidationConstraints(new SignedWith(new Sha256(), $key));
|
2021-01-23 20:09:16 +00:00
|
|
|
/** @var \Lcobucci\JWT\Token\Plain $token */
|
|
|
|
$token = $config->parser()->parse($response->json('data.token'));
|
2020-06-28 17:16:15 +00:00
|
|
|
|
|
|
|
$this->assertTrue(
|
2021-01-23 20:09:16 +00:00
|
|
|
$config->validator()->validate($token, ...$config->validationConstraints()),
|
2020-06-28 17:16:15 +00:00
|
|
|
'Failed to validate that the JWT data returned was signed using the Node\'s secret key.'
|
|
|
|
);
|
|
|
|
|
2021-01-23 20:09:16 +00:00
|
|
|
// The way we generate times for the JWT will truncate the microseconds from the
|
|
|
|
// time, but CarbonImmutable::now() will include them, thus causing test failures.
|
|
|
|
//
|
|
|
|
// This little chunk of logic just strips those out by generating a new CarbonImmutable
|
|
|
|
// instance from the current timestamp, which is how the JWT works. We also need to
|
|
|
|
// switch to UTC here for consistency.
|
|
|
|
$expect = CarbonImmutable::createFromTimestamp(CarbonImmutable::now()->getTimestamp())->timezone('UTC');
|
|
|
|
|
2020-06-28 17:16:15 +00:00
|
|
|
// Check that the claims are generated correctly.
|
2021-01-23 20:09:16 +00:00
|
|
|
$this->assertTrue($token->hasBeenIssuedBy(config('app.url')));
|
|
|
|
$this->assertTrue($token->isPermittedFor($server->node->getConnectionAddress()));
|
|
|
|
$this->assertEquals($expect, $token->claims()->get('iat'));
|
|
|
|
$this->assertEquals($expect->subMinutes(5), $token->claims()->get('nbf'));
|
|
|
|
$this->assertEquals($expect->addMinutes(10), $token->claims()->get('exp'));
|
|
|
|
$this->assertSame($user->id, $token->claims()->get('user_id'));
|
|
|
|
$this->assertSame($server->uuid, $token->claims()->get('server_uuid'));
|
|
|
|
$this->assertSame(['*'], $token->claims()->get('permissions'));
|
2020-06-28 17:16:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test that the subuser's permissions are passed along correctly in the generated JWT.
|
|
|
|
*/
|
|
|
|
public function testJwtIsConfiguredCorrectlyForServerSubuser()
|
|
|
|
{
|
|
|
|
$permissions = [Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_CONTROL_CONSOLE];
|
|
|
|
|
|
|
|
/** @var \Pterodactyl\Models\User $user */
|
|
|
|
/** @var \Pterodactyl\Models\Server $server */
|
|
|
|
[$user, $server] = $this->generateTestAccount($permissions);
|
|
|
|
|
2022-10-14 16:59:20 +00:00
|
|
|
$response = $this->actingAs($user)->getJson("/api/client/servers/$server->uuid/websocket");
|
2020-06-28 17:16:15 +00:00
|
|
|
|
|
|
|
$response->assertOk();
|
|
|
|
$response->assertJsonStructure(['data' => ['token', 'socket']]);
|
|
|
|
|
2021-01-23 20:33:34 +00:00
|
|
|
$config = Configuration::forSymmetricSigner(new Sha256(), $key = InMemory::plainText($server->node->getDecryptedKey()));
|
|
|
|
$config->setValidationConstraints(new SignedWith(new Sha256(), $key));
|
2021-01-23 20:09:16 +00:00
|
|
|
/** @var \Lcobucci\JWT\Token\Plain $token */
|
|
|
|
$token = $config->parser()->parse($response->json('data.token'));
|
2020-06-28 17:16:15 +00:00
|
|
|
|
|
|
|
$this->assertTrue(
|
2021-01-23 20:09:16 +00:00
|
|
|
$config->validator()->validate($token, ...$config->validationConstraints()),
|
2020-06-28 17:16:15 +00:00
|
|
|
'Failed to validate that the JWT data returned was signed using the Node\'s secret key.'
|
|
|
|
);
|
|
|
|
|
|
|
|
// Check that the claims are generated correctly.
|
2021-01-23 20:09:16 +00:00
|
|
|
$this->assertSame($permissions, $token->claims()->get('permissions'));
|
2020-06-28 17:16:15 +00:00
|
|
|
}
|
|
|
|
}
|