From 4cb4dfecc88191f474d1f20e4408fc23d6fbb721 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 28 Jun 2020 10:16:15 -0700 Subject: [PATCH] Add test coverage for generating JWTs to connect to websocket --- .../Client/Servers/WebsocketController.php | 13 +-- .../Client/Server/WebsocketControllerTest.php | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 tests/Integration/Api/Client/Server/WebsocketControllerTest.php diff --git a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php index 7cbe31631..a176f66fc 100644 --- a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php +++ b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php @@ -7,7 +7,6 @@ use Illuminate\Http\Response; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; use Pterodactyl\Models\Permission; -use Illuminate\Contracts\Cache\Repository; use Pterodactyl\Services\Nodes\NodeJWTService; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; @@ -16,11 +15,6 @@ use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; class WebsocketController extends ClientApiController { - /** - * @var \Illuminate\Contracts\Cache\Repository - */ - private $cache; - /** * @var \Pterodactyl\Services\Nodes\NodeJWTService */ @@ -36,16 +30,13 @@ class WebsocketController extends ClientApiController * * @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService * @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService - * @param \Illuminate\Contracts\Cache\Repository $cache */ public function __construct( NodeJWTService $jwtService, - GetUserPermissionsService $permissionsService, - Repository $cache + GetUserPermissionsService $permissionsService ) { parent::__construct(); - $this->cache = $cache; $this->jwtService = $jwtService; $this->permissionsService = $permissionsService; } @@ -78,7 +69,7 @@ class WebsocketController extends ClientApiController $socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress()); - return JsonResponse::create([ + return new JsonResponse([ 'data' => [ 'token' => $token->__toString(), 'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid), diff --git a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php new file mode 100644 index 000000000..0d8851d3a --- /dev/null +++ b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php @@ -0,0 +1,98 @@ +generateTestAccount([Permission::ACTION_CONTROL_RESTART]); + + $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket") + ->assertStatus(Response::HTTP_FORBIDDEN) + ->assertJsonPath('errors.0.code', 'HttpException') + ->assertJsonPath('errors.0.detail', 'You do not have permission to connect to this server\'s websocket.'); + } + + /** + * Test that the expected permissions are returned for the server owner and that the JWT is + * configured correctly. + */ + public function testJwtAndWebsocketUrlAreReturnedForServerOwner() + { + CarbonImmutable::setTestNow(Carbon::now()); + + /** @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(); + + $response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket"); + + $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.'); + $this->assertStringEndsWith("/api/servers/{$server->uuid}/ws", $connection, 'Failed asserting that websocket connection address uses expected Wings endpoint.'); + + $token = (new Parser)->parse($response->json('data.token')); + + $this->assertTrue( + $token->verify(new Sha256, $server->node->getDecryptedKey()), + 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' + ); + + // Check that the claims are generated correctly. + $this->assertSame(config('app.url'), $token->getClaim('iss')); + $this->assertSame($server->node->getConnectionAddress(), $token->getClaim('aud')); + $this->assertSame(CarbonImmutable::now()->getTimestamp(), $token->getClaim('iat')); + $this->assertSame(CarbonImmutable::now()->subMinutes(5)->getTimestamp(), $token->getClaim('nbf')); + $this->assertSame(CarbonImmutable::now()->addMinutes(15)->getTimestamp(), $token->getClaim('exp')); + $this->assertSame($user->id, $token->getClaim('user_id')); + $this->assertSame($server->uuid, $token->getClaim('server_uuid')); + $this->assertSame(['*'], $token->getClaim('permissions')); + } + + /** + * 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); + + $response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket"); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['token', 'socket']]); + + $token = (new Parser)->parse($response->json('data.token')); + + $this->assertTrue( + $token->verify(new Sha256, $server->node->getDecryptedKey()), + 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' + ); + + // Check that the claims are generated correctly. + $this->assertSame($permissions, $token->getClaim('permissions')); + } +}