Skip to content

Commit 4cb4dfe

Browse files
committed
Add test coverage for generating JWTs to connect to websocket
1 parent 7546d54 commit 4cb4dfe

File tree

2 files changed

+100
-11
lines changed

2 files changed

+100
-11
lines changed

app/Http/Controllers/Api/Client/Servers/WebsocketController.php

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Pterodactyl\Models\Server;
88
use Illuminate\Http\JsonResponse;
99
use Pterodactyl\Models\Permission;
10-
use Illuminate\Contracts\Cache\Repository;
1110
use Pterodactyl\Services\Nodes\NodeJWTService;
1211
use Symfony\Component\HttpKernel\Exception\HttpException;
1312
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
@@ -16,11 +15,6 @@
1615

1716
class WebsocketController extends ClientApiController
1817
{
19-
/**
20-
* @var \Illuminate\Contracts\Cache\Repository
21-
*/
22-
private $cache;
23-
2418
/**
2519
* @var \Pterodactyl\Services\Nodes\NodeJWTService
2620
*/
@@ -36,16 +30,13 @@ class WebsocketController extends ClientApiController
3630
*
3731
* @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService
3832
* @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService
39-
* @param \Illuminate\Contracts\Cache\Repository $cache
4033
*/
4134
public function __construct(
4235
NodeJWTService $jwtService,
43-
GetUserPermissionsService $permissionsService,
44-
Repository $cache
36+
GetUserPermissionsService $permissionsService
4537
) {
4638
parent::__construct();
4739

48-
$this->cache = $cache;
4940
$this->jwtService = $jwtService;
5041
$this->permissionsService = $permissionsService;
5142
}
@@ -78,7 +69,7 @@ public function __invoke(ClientApiRequest $request, Server $server)
7869

7970
$socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress());
8071

81-
return JsonResponse::create([
72+
return new JsonResponse([
8273
'data' => [
8374
'token' => $token->__toString(),
8475
'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace Pterodactyl\Tests\Integration\Api\Client\Server;
4+
5+
use Carbon\Carbon;
6+
use Lcobucci\JWT\Parser;
7+
use Carbon\CarbonImmutable;
8+
use Illuminate\Http\Response;
9+
use Pterodactyl\Models\Permission;
10+
use Lcobucci\JWT\Signer\Hmac\Sha256;
11+
use Pterodactyl\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
12+
13+
class WebsocketControllerTest extends ClientApiIntegrationTestCase
14+
{
15+
/**
16+
* Test that a subuser attempting to connect to the websocket recieves an error if they
17+
* do not explicitly have the permission.
18+
*/
19+
public function testSubuserWithoutWebsocketPermissionReceivesError()
20+
{
21+
[$user, $server] = $this->generateTestAccount([Permission::ACTION_CONTROL_RESTART]);
22+
23+
$this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket")
24+
->assertStatus(Response::HTTP_FORBIDDEN)
25+
->assertJsonPath('errors.0.code', 'HttpException')
26+
->assertJsonPath('errors.0.detail', 'You do not have permission to connect to this server\'s websocket.');
27+
}
28+
29+
/**
30+
* Test that the expected permissions are returned for the server owner and that the JWT is
31+
* configured correctly.
32+
*/
33+
public function testJwtAndWebsocketUrlAreReturnedForServerOwner()
34+
{
35+
CarbonImmutable::setTestNow(Carbon::now());
36+
37+
/** @var \Pterodactyl\Models\User $user */
38+
/** @var \Pterodactyl\Models\Server $server */
39+
[$user, $server] = $this->generateTestAccount();
40+
41+
// Force the node to HTTPS since we want to confirm it gets transformed to wss:// in the URL.
42+
$server->node->scheme = 'https';
43+
$server->node->save();
44+
45+
$response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket");
46+
47+
$response->assertOk();
48+
$response->assertJsonStructure(['data' => ['token', 'socket']]);
49+
50+
$connection = $response->json('data.socket');
51+
$this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.');
52+
$this->assertStringEndsWith("/api/servers/{$server->uuid}/ws", $connection, 'Failed asserting that websocket connection address uses expected Wings endpoint.');
53+
54+
$token = (new Parser)->parse($response->json('data.token'));
55+
56+
$this->assertTrue(
57+
$token->verify(new Sha256, $server->node->getDecryptedKey()),
58+
'Failed to validate that the JWT data returned was signed using the Node\'s secret key.'
59+
);
60+
61+
// Check that the claims are generated correctly.
62+
$this->assertSame(config('app.url'), $token->getClaim('iss'));
63+
$this->assertSame($server->node->getConnectionAddress(), $token->getClaim('aud'));
64+
$this->assertSame(CarbonImmutable::now()->getTimestamp(), $token->getClaim('iat'));
65+
$this->assertSame(CarbonImmutable::now()->subMinutes(5)->getTimestamp(), $token->getClaim('nbf'));
66+
$this->assertSame(CarbonImmutable::now()->addMinutes(15)->getTimestamp(), $token->getClaim('exp'));
67+
$this->assertSame($user->id, $token->getClaim('user_id'));
68+
$this->assertSame($server->uuid, $token->getClaim('server_uuid'));
69+
$this->assertSame(['*'], $token->getClaim('permissions'));
70+
}
71+
72+
/**
73+
* Test that the subuser's permissions are passed along correctly in the generated JWT.
74+
*/
75+
public function testJwtIsConfiguredCorrectlyForServerSubuser()
76+
{
77+
$permissions = [Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_CONTROL_CONSOLE];
78+
79+
/** @var \Pterodactyl\Models\User $user */
80+
/** @var \Pterodactyl\Models\Server $server */
81+
[$user, $server] = $this->generateTestAccount($permissions);
82+
83+
$response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket");
84+
85+
$response->assertOk();
86+
$response->assertJsonStructure(['data' => ['token', 'socket']]);
87+
88+
$token = (new Parser)->parse($response->json('data.token'));
89+
90+
$this->assertTrue(
91+
$token->verify(new Sha256, $server->node->getDecryptedKey()),
92+
'Failed to validate that the JWT data returned was signed using the Node\'s secret key.'
93+
);
94+
95+
// Check that the claims are generated correctly.
96+
$this->assertSame($permissions, $token->getClaim('permissions'));
97+
}
98+
}

0 commit comments

Comments
 (0)