diff --git a/app/Http/Middleware/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Daemon/DaemonAuthenticate.php index ab587cbe0..d06711af9 100644 --- a/app/Http/Middleware/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Daemon/DaemonAuthenticate.php @@ -32,19 +32,20 @@ use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class DaemonAuthenticate { + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + private $repository; + /** * Daemon routes that this middleware should be skipped on. + * * @var array */ protected $except = [ 'daemon.configuration', ]; - /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface - */ - protected $repository; - /** * DaemonAuthenticate constructor. * diff --git a/app/Http/Middleware/Server/AccessingValidServer.php b/app/Http/Middleware/Server/AccessingValidServer.php index 762f9a298..5137d7721 100644 --- a/app/Http/Middleware/Server/AccessingValidServer.php +++ b/app/Http/Middleware/Server/AccessingValidServer.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Http\Request; use Pterodactyl\Models\Server; use Illuminate\Contracts\Session\Session; -use Illuminate\Auth\AuthenticationException; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -17,22 +16,17 @@ class AccessingValidServer /** * @var \Illuminate\Contracts\Config\Repository */ - protected $config; + private $config; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; - - /** - * @var \Pterodactyl\Models\Server - */ - protected $server; + private $repository; /** * @var \Illuminate\Contracts\Session\Session */ - protected $session; + private $session; /** * AccessingValidServer constructor. @@ -56,7 +50,7 @@ class AccessingValidServer * * @param \Illuminate\Http\Request $request * @param \Closure $next - * @return mixed + * @return \Illuminate\Http\Response|mixed * * @throws \Illuminate\Auth\AuthenticationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -65,10 +59,6 @@ class AccessingValidServer */ public function handle(Request $request, Closure $next) { - if (! $request->user()) { - throw new AuthenticationException; - } - $attributes = $request->route()->parameter('server'); $isApiRequest = $request->expectsJson() || $request->is(...$this->config->get('pterodactyl.json_routes', [])); $server = $this->repository->getByUuid($attributes instanceof Server ? $attributes->uuid : $attributes); @@ -89,9 +79,11 @@ class AccessingValidServer return response()->view('errors.suspended', [], 403); } + // Servers can have install statuses other than 1 or 0, so don't check + // for a bool-type operator here. if ($server->installed !== 1) { if ($isApiRequest) { - throw new AccessDeniedHttpException('Server is completing install process.'); + throw new AccessDeniedHttpException('Server is not marked as installed.'); } return response()->view('errors.installing', [], 403); diff --git a/tests/Unit/Http/Middleware/Daemon/DaemonAuthenticateTest.php b/tests/Unit/Http/Middleware/Daemon/DaemonAuthenticateTest.php new file mode 100644 index 000000000..efe667743 --- /dev/null +++ b/tests/Unit/Http/Middleware/Daemon/DaemonAuthenticateTest.php @@ -0,0 +1,126 @@ +repository = m::mock(NodeRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->request->attributes = new ParameterBag(); + } + + /** + * Test that if we are accessing the daemon.configuration route this middleware is not + * applied in order to allow an unauthenticated request to use a token to grab data. + */ + public function testResponseShouldContinueIfRouteIsExempted() + { + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('daemon.configuration'); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that not passing in the bearer token will result in a HTTP/401 error with the + * proper response headers. + */ + public function testResponseShouldFailIfNoTokenIsProvided() + { + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); + $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); + + try { + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } catch (HttpException $exception) { + $this->assertEquals(401, $exception->getStatusCode(), 'Assert that a status code of 401 is returned.'); + $this->assertTrue(is_array($exception->getHeaders()), 'Assert that an array of headers is returned.'); + $this->assertArrayHasKey('WWW-Authenticate', $exception->getHeaders(), 'Assert exception headers contains WWW-Authenticate.'); + $this->assertEquals('Bearer', $exception->getHeaders()['WWW-Authenticate']); + } + } + + /** + * Test that passing in an invalid node daemon secret will result in a HTTP/403 + * error response. + */ + public function testResponseShouldFailIfNoNodeIsFound() + { + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); + $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturn('test1234'); + + $this->repository->shouldReceive('findFirstWhere')->with([['daemonSecret', '=', 'test1234']])->once()->andThrow(new RecordNotFoundException); + + try { + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } catch (HttpException $exception) { + $this->assertEquals(403, $exception->getStatusCode(), 'Assert that a status code of 403 is returned.'); + } + } + + /** + * Test a successful middleware process. + */ + public function testSuccessfulMiddlewareProcess() + { + $model = factory(Node::class)->make(); + + $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); + $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturn($model->daemonSecret); + + $this->repository->shouldReceive('findFirstWhere')->with([['daemonSecret', '=', $model->daemonSecret]])->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->assertTrue($this->request->attributes->has('node'), 'Assert request attributes contains node.'); + $this->assertSame($model, $this->request->attributes->get('node')); + } + + /** + * Return an instance of the middleware using mocked dependencies. + * + * @return \Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate + */ + private function getMiddleware(): DaemonAuthenticate + { + return new DaemonAuthenticate($this->repository); + } + + /** + * Provide a closure to be used when validating that the response from the middleware + * is the same request object we passed into it. + */ + private function getClosureAssertions(): Closure + { + return function ($response) { + $this->assertInstanceOf(Request::class, $response); + $this->assertSame($this->request, $response); + }; + } +} diff --git a/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php b/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php new file mode 100644 index 000000000..0cf27e8a6 --- /dev/null +++ b/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php @@ -0,0 +1,185 @@ +config = m::mock(Repository::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->request->attributes = new ParameterBag(); + $this->session = m::mock(Session::class); + } + + /** + * Test that an exception is thrown if the request is an API request and no server is found. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * @expectedExceptionMessage The requested server was not found on the system. + */ + public function testExceptionIsThrownIfNoServerIsFoundAndIsAPIRequest() + { + $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); + $this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(true); + + $this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an exception is thrown if the request is an API request and the server is suspended. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @expectedExceptionMessage Server is suspended. + */ + public function testExceptionIsThrownIfServerIsSuspended() + { + $model = factory(Server::class)->make(['suspended' => 1]); + + $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); + $this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(true); + + $this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an exception is thrown if the request is an API request and the server is not installed. + * + * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @expectedExceptionMessage Server is not marked as installed. + */ + public function testExceptionIsThrownIfServerIsNotInstalled() + { + $model = factory(Server::class)->make(['installed' => 0]); + + $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); + $this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(true); + + $this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that the correct error pages are rendered depending on the status of the server. + * + * @dataProvider viewDataProvider + */ + public function testCorrectErrorPagesAreRendered(Server $model = null, string $page, int $httpCode) + { + $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); + $this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(false); + $this->config->shouldReceive('get')->with('pterodactyl.json_routes', [])->once()->andReturn([]); + $this->request->shouldReceive('is')->with(...[])->once()->andReturn(false); + + $this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model); + + $response = $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals($page, $response->getOriginalContent()->getName(), 'Assert that the correct view is returned.'); + $this->assertEquals($httpCode, $response->getStatusCode(), 'Assert that the correct HTTP code is returned.'); + } + + /** + * Test that the full middleware works correctly. + */ + public function testValidServerProcess() + { + $model = factory(Server::class)->make(); + + $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); + $this->request->shouldReceive('expectsJson')->withNoArgs()->once()->andReturn(false); + $this->config->shouldReceive('get')->with('pterodactyl.json_routes', [])->once()->andReturn([]); + $this->request->shouldReceive('is')->with(...[])->once()->andReturn(false); + + $this->repository->shouldReceive('getByUuid')->with('123456')->once()->andReturn($model); + $this->session->shouldReceive('now')->with('server_data.model', $model)->once()->andReturnNull(); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + $this->assertTrue($this->request->attributes->has('server'), 'Assert request attributes contains server.'); + $this->assertSame($model, $this->request->attributes->get('server')); + } + + /** + * Provide test data that checks that the correct view is returned for each model type. + * + * @return array + */ + public function viewDataProvider(): array + { + // Without this we are unable to instantiate the factory builders for some reason. + $this->refreshApplication(); + + return [ + [null, 'errors.404', 404], + [factory(Server::class)->make(['suspended' => 1]), 'errors.suspended', 403], + [factory(Server::class)->make(['installed' => 0]), 'errors.installing', 403], + [factory(Server::class)->make(['installed' => 2]), 'errors.installing', 403], + ]; + } + + /** + * Return an instance of the middleware using mocked dependencies. + * + * @return \Pterodactyl\Http\Middleware\AccessingValidServer + */ + private function getMiddleware(): AccessingValidServer + { + return new AccessingValidServer($this->config, $this->repository, $this->session); + } + + /** + * Provide a closure to be used when validating that the response from the middleware + * is the same request object we passed into it. + */ + private function getClosureAssertions(): Closure + { + return function ($response) { + $this->assertInstanceOf(Request::class, $response); + $this->assertSame($this->request, $response); + }; + } +}