From 8e2b77dc1ed9763f30cb6bf36ddfac696f6db467 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 24 Sep 2017 12:34:00 -0500 Subject: [PATCH] Final touches to new key-rotation service --- .../API/Remote/ValidateKeyController.php | 3 +- app/Http/Controllers/Base/IndexController.php | 25 +++-- .../DaemonKeys/DaemonKeyUpdateService.php | 93 +++++++++++++++++++ .../Servers/ServerAccessHelperService.php | 33 +++++-- app/Transformers/Daemon/ApiKeyTransformer.php | 17 +++- config/pterodactyl.php | 1 + 6 files changed, 154 insertions(+), 18 deletions(-) create mode 100644 app/Services/DaemonKeys/DaemonKeyUpdateService.php diff --git a/app/Http/Controllers/API/Remote/ValidateKeyController.php b/app/Http/Controllers/API/Remote/ValidateKeyController.php index c10310c27..ef49b8756 100644 --- a/app/Http/Controllers/API/Remote/ValidateKeyController.php +++ b/app/Http/Controllers/API/Remote/ValidateKeyController.php @@ -30,6 +30,7 @@ use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Testing\HttpException; use League\Fractal\Serializer\JsonApiSerializer; use Pterodactyl\Transformers\Daemon\ApiKeyTransformer; +use Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; class ValidateKeyController extends Controller @@ -77,7 +78,7 @@ class ValidateKeyController extends Controller */ public function index($token) { - if (! starts_with($token, 'i_')) { + if (! starts_with($token, DaemonKeyUpdateService::INTERNAL_TOKEN_IDENTIFIER)) { throw new HttpException(501); } diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index 3c52a84fd..517b4caea 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -26,7 +26,9 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; +use GuzzleHttp\Exception\RequestException; use Pterodactyl\Http\Controllers\Controller; +use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Services\Servers\ServerAccessHelperService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -36,7 +38,7 @@ class IndexController extends Controller /** * @var \Pterodactyl\Services\Servers\ServerAccessHelperService */ - protected $access; + protected $serverAccessHelper; /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface @@ -52,15 +54,15 @@ class IndexController extends Controller * IndexController constructor. * * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository - * @param \Pterodactyl\Services\Servers\ServerAccessHelperService $access + * @param \Pterodactyl\Services\Servers\ServerAccessHelperService $serverAccessHelper * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository */ public function __construct( DaemonServerRepositoryInterface $daemonRepository, - ServerAccessHelperService $access, + ServerAccessHelperService $serverAccessHelper, ServerRepositoryInterface $repository ) { - $this->access = $access; + $this->serverAccessHelper = $serverAccessHelper; $this->daemonRepository = $daemonRepository; $this->repository = $repository; } @@ -90,7 +92,8 @@ class IndexController extends Controller */ public function status(Request $request, $uuid) { - $server = $this->access->handle($uuid, $request->user()); + $server = $this->repository->findFirstWhere([['uuidShort', '=', $uuid]]); + $token = $this->serverAccessHelper->handle($server, $request->user()); if (! $server->installed) { return response()->json(['status' => 20]); @@ -98,10 +101,14 @@ class IndexController extends Controller return response()->json(['status' => 30]); } - $response = $this->daemonRepository->setNode($server->node_id) - ->setAccessServer($server->uuid) - ->setAccessToken($server->daemonSecret) - ->details(); + try { + $response = $this->daemonRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($token) + ->details(); + } catch (RequestException $exception) { + throw new HttpException(500, $exception->getMessage()); + } return response()->json(json_decode($response->getBody())); } diff --git a/app/Services/DaemonKeys/DaemonKeyUpdateService.php b/app/Services/DaemonKeys/DaemonKeyUpdateService.php new file mode 100644 index 000000000..794940e82 --- /dev/null +++ b/app/Services/DaemonKeys/DaemonKeyUpdateService.php @@ -0,0 +1,93 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\DaemonKeys; + +use Carbon\Carbon; +use Pterodactyl\Models\DaemonKey; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; + +class DaemonKeyUpdateService +{ + const INTERNAL_TOKEN_IDENTIFIER = 'i_'; + + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface + */ + protected $repository; + + /** + * DaemonKeyUpdateService constructor. + * + * @param \Carbon\Carbon $carbon + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository + */ + public function __construct( + Carbon $carbon, + ConfigRepository $config, + DaemonKeyRepositoryInterface $repository + ) { + $this->carbon = $carbon; + $this->config = $config; + $this->repository = $repository; + } + + /** + * Update a daemon key to expire the previous one. + * + * @param \Pterodactyl\Models\DaemonKey|int $key + * @return string + * + * @throws \RuntimeException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($key) + { + if ($key instanceof DaemonKey) { + $key = $key->id; + } + + $secret = self::INTERNAL_TOKEN_IDENTIFIER . str_random(40); + + $this->repository->withoutFresh()->update($key, [ + 'secret' => $secret, + 'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time')), + ]); + + return $secret; + } +} diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php index 986d34c76..898f07d22 100644 --- a/app/Services/Servers/ServerAccessHelperService.php +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -24,9 +24,12 @@ namespace Pterodactyl\Services\Servers; +use Carbon\Carbon; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; +use Pterodactyl\Models\DaemonKey; use Illuminate\Cache\Repository as CacheRepository; +use Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; @@ -39,11 +42,21 @@ class ServerAccessHelperService */ protected $cache; + /** + * @var \Carbon\Carbon + */ + protected $carbon; + /** * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface */ protected $daemonKeyRepository; + /** + * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService + */ + protected $daemonKeyUpdateService; + /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ @@ -58,18 +71,24 @@ class ServerAccessHelperService * ServerAccessHelperService constructor. * * @param \Illuminate\Cache\Repository $cache + * @param \Carbon\Carbon $carbon * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $daemonKeyRepository + * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService $daemonKeyUpdateService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository */ public function __construct( CacheRepository $cache, + Carbon $carbon, DaemonKeyRepositoryInterface $daemonKeyRepository, + DaemonKeyUpdateService $daemonKeyUpdateService, ServerRepositoryInterface $repository, UserRepositoryInterface $userRepository ) { $this->cache = $cache; + $this->carbon = $carbon; $this->daemonKeyRepository = $daemonKeyRepository; + $this->daemonKeyUpdateService = $daemonKeyUpdateService; $this->repository = $repository; $this->userRepository = $userRepository; } @@ -81,8 +100,10 @@ class ServerAccessHelperService * @param int|\Pterodactyl\Models\User $user * @return string * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException + * @throws \RuntimeException */ public function handle($server, $user) { @@ -95,16 +116,16 @@ class ServerAccessHelperService } $keys = $server->relationLoaded('keys') ? $server->keys : $this->daemonKeyRepository->getServerKeys($server->id); - - $key = array_get($keys->where('user_id', $user->id)->first(null, []), 'secret'); - if ($user->root_admin) { - $key = array_get($keys->where('user_id', $server->owner_id)->first(null, []), 'secret'); - } + $key = $keys->where('user_id', $user->root_admin ? $server->owner_id : $user->id)->first(); if (is_null($key)) { throw new UserNotLinkedToServerException; } - return $key; + if (max($this->carbon->now()->diffInSeconds($key->expires_at, false), 0) === 0) { + $key = $this->daemonKeyUpdateService->handle($key); + } + + return ($key instanceof DaemonKey) ? $key->secret : $key; } } diff --git a/app/Transformers/Daemon/ApiKeyTransformer.php b/app/Transformers/Daemon/ApiKeyTransformer.php index e17b18f82..a0bae9c5d 100644 --- a/app/Transformers/Daemon/ApiKeyTransformer.php +++ b/app/Transformers/Daemon/ApiKeyTransformer.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Transformers\Daemon; +use Carbon\Carbon; use Pterodactyl\Models\DaemonKey; use Pterodactyl\Models\Permission; use League\Fractal\TransformerAbstract; @@ -31,6 +32,11 @@ use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; class ApiKeyTransformer extends TransformerAbstract { + /** + * @var \Carbon\Carbon + */ + protected $carbon; + /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface */ @@ -39,10 +45,12 @@ class ApiKeyTransformer extends TransformerAbstract /** * ApiKeyTransformer constructor. * + * @param \Carbon\Carbon $carbon * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository */ - public function __construct(SubuserRepositoryInterface $repository) + public function __construct(Carbon $carbon, SubuserRepositoryInterface $repository) { + $this->carbon = $carbon; $this->repository = $repository; } @@ -59,6 +67,8 @@ class ApiKeyTransformer extends TransformerAbstract if ($key->user_id === $key->server->owner_id) { return [ 'id' => $key->server->uuid, + 'is_temporary' => true, + 'expires_in' => max($this->carbon->now()->diffInSeconds($key->expires_at, false), 0), 'permissions' => ['s:*'], ]; } @@ -76,7 +86,10 @@ class ApiKeyTransformer extends TransformerAbstract } return [ - $key->server->uuid => $daemonPermissions, + 'id' => $key->server->uuid, + 'is_temporary' => true, + 'expires_in' => max($this->carbon->now()->diffInSeconds($key->expires_at, false), 0), + 'permissions' => $daemonPermissions, ]; } } diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 7c66f3224..25e664921 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -59,6 +59,7 @@ return [ */ 'api' => [ 'include_on_list' => env('API_INCLUDE_ON_LIST', false), + 'key_expire_time' => env('API_KEY_EXPIRE_TIME', 60 * 12), ], /*