From 7557dddf49e9938e8e33eed1d6858a2a7bc0a28e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Apr 2020 15:15:38 -0700 Subject: [PATCH] Store node daemon tokens in an encrypted manner --- .../Admin/Nodes/NodeViewController.php | 4 +- .../Admin/StatisticsController.php | 2 +- .../Servers/ServerTransferController.php | 2 +- .../Controllers/Daemon/ActionController.php | 107 ------------ .../Controllers/Daemon/PackController.php | 73 --------- app/Http/Kernel.php | 2 - .../Api/Daemon/DaemonAuthenticate.php | 39 +++-- app/Http/Middleware/DaemonAuthenticate.php | 69 -------- app/Models/Node.php | 45 +++-- app/Providers/RouteServiceProvider.php | 4 - app/Repositories/Daemon/BaseRepository.php | 154 ------------------ app/Repositories/Daemon/CommandRepository.php | 25 --- .../Daemon/ConfigurationRepository.php | 46 ------ app/Repositories/Daemon/FileRepository.php | 104 ------------ app/Repositories/Daemon/PowerRepository.php | 36 ---- app/Repositories/Daemon/ServerRepository.php | 134 --------------- app/Repositories/Eloquent/NodeRepository.php | 2 +- .../Wings/DaemonConfigurationRepository.php | 18 ++ app/Repositories/Wings/DaemonRepository.php | 2 +- app/Services/Nodes/NodeCreationService.php | 26 +-- app/Services/Nodes/NodeJWTService.php | 2 +- app/Services/Nodes/NodeUpdateService.php | 45 +++-- database/factories/ModelFactory.php | 9 +- ...4_store_node_tokens_as_encrypted_value.php | 84 ++++++++++ resources/views/admin/nodes/index.blade.php | 2 +- routes/daemon.php | 13 -- 26 files changed, 222 insertions(+), 827 deletions(-) delete mode 100644 app/Http/Controllers/Daemon/ActionController.php delete mode 100644 app/Http/Controllers/Daemon/PackController.php delete mode 100644 app/Http/Middleware/DaemonAuthenticate.php delete mode 100644 app/Repositories/Daemon/BaseRepository.php delete mode 100644 app/Repositories/Daemon/CommandRepository.php delete mode 100644 app/Repositories/Daemon/ConfigurationRepository.php delete mode 100644 app/Repositories/Daemon/FileRepository.php delete mode 100644 app/Repositories/Daemon/PowerRepository.php delete mode 100644 app/Repositories/Daemon/ServerRepository.php create mode 100644 database/migrations/2020_04_10_141024_store_node_tokens_as_encrypted_value.php delete mode 100644 routes/daemon.php diff --git a/app/Http/Controllers/Admin/Nodes/NodeViewController.php b/app/Http/Controllers/Admin/Nodes/NodeViewController.php index 017706351..ba9e2e947 100644 --- a/app/Http/Controllers/Admin/Nodes/NodeViewController.php +++ b/app/Http/Controllers/Admin/Nodes/NodeViewController.php @@ -148,8 +148,8 @@ class NodeViewController extends Controller public function servers(Request $request, Node $node) { $this->plainInject([ - 'node' => Collection::wrap($node->makeVisible('daemonSecret')) - ->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), + 'node' => Collection::wrap($node->makeVisible(['daemon_token_id', 'daemon_token'])) + ->only(['scheme', 'fqdn', 'daemonListen', 'daemon_token_id', 'daemon_token']), ]); return $this->view->make('admin.nodes.view.servers', [ diff --git a/app/Http/Controllers/Admin/StatisticsController.php b/app/Http/Controllers/Admin/StatisticsController.php index 1ae807565..9e6ff1ad2 100644 --- a/app/Http/Controllers/Admin/StatisticsController.php +++ b/app/Http/Controllers/Admin/StatisticsController.php @@ -67,7 +67,7 @@ class StatisticsController extends Controller $tokens = []; foreach ($nodes as $node) { - $tokens[$node->id] = $node->daemonSecret; + $tokens[$node->id] = decrypt($node->daemon_token); } $this->injectJavascript([ diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 1d2e723ee..bd8827a6e 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -145,7 +145,7 @@ class ServerTransferController extends Controller ->canOnlyBeUsedAfter($now->getTimestamp()) ->expiresAt($now->addMinutes(15)->getTimestamp()) ->relatedTo($server->uuid, true) - ->getToken($signer, new Key($server->node->daemonSecret)); + ->getToken($signer, new Key($server->node->getDecryptedKey())); // On the daemon transfer repository, make sure to set the node after the server // because setServer() tells the repository to use the server's node and not the one diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php deleted file mode 100644 index eb77b4b9c..000000000 --- a/app/Http/Controllers/Daemon/ActionController.php +++ /dev/null @@ -1,107 +0,0 @@ -eventDispatcher = $eventDispatcher; - $this->repository = $repository; - } - - /** - * Handles install toggle request from daemon. - * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\JsonResponse - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function markInstall(Request $request): JsonResponse - { - try { - /** @var \Pterodactyl\Models\Server $server */ - $server = $this->repository->findFirstWhere([ - 'uuid' => $request->input('server'), - ]); - } catch (RecordNotFoundException $exception) { - return JsonResponse::create([ - 'error' => 'No server by that ID was found on the system.', - ], Response::HTTP_UNPROCESSABLE_ENTITY); - } - - if (! $server->relationLoaded('node')) { - $server->load('node'); - } - - $hmac = $request->input('signed'); - $status = $request->input('installed'); - - if (! hash_equals(base64_decode($hmac), hash_hmac('sha256', $server->uuid, $server->getRelation('node')->daemonSecret, true))) { - return JsonResponse::create([ - 'error' => 'Signed HMAC was invalid.', - ], Response::HTTP_FORBIDDEN); - } - - $this->repository->update($server->id, [ - 'installed' => ($status === 'installed') ? 1 : 2, - ], true, true); - - // Only fire event if server installed successfully. - if ($status === 'installed') { - $this->eventDispatcher->dispatch(new ServerInstalled($server)); - } - - // Don't use a 204 here, the daemon is hard-checking for a 200 code. - return JsonResponse::create([]); - } - - /** - * Handles configuration data request from daemon. - * - * @param \Illuminate\Http\Request $request - * @param string $token - * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response - */ - public function configuration(Request $request, $token) - { - $nodeId = Cache::pull('Node:Configuration:' . $token); - if (is_null($nodeId)) { - return response()->json(['error' => 'token_invalid'], 403); - } - - $node = Node::findOrFail($nodeId); - - // Manually as getConfigurationAsJson() returns it in correct format already - return $node->getJsonConfiguration(); - } -} diff --git a/app/Http/Controllers/Daemon/PackController.php b/app/Http/Controllers/Daemon/PackController.php deleted file mode 100644 index 45c38bf6c..000000000 --- a/app/Http/Controllers/Daemon/PackController.php +++ /dev/null @@ -1,73 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Controllers\Daemon; - -use Storage; -use Pterodactyl\Models; -use Illuminate\Http\Request; -use Pterodactyl\Http\Controllers\Controller; - -class PackController extends Controller -{ - /** - * Pulls an install pack archive from the system. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse - */ - public function pull(Request $request, $uuid) - { - $pack = Models\Pack::where('uuid', $uuid)->first(); - - if (! $pack) { - return response()->json(['error' => 'No such pack.'], 404); - } - - if (! Storage::exists('packs/' . $pack->uuid . '/archive.tar.gz')) { - return response()->json(['error' => 'There is no archive available for this pack.'], 503); - } - - return response()->download(storage_path('app/packs/' . $pack->uuid . '/archive.tar.gz')); - } - - /** - * Returns the hash information for a pack. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\JsonResponse - */ - public function hash(Request $request, $uuid) - { - $pack = Models\Pack::where('uuid', $uuid)->first(); - - if (! $pack) { - return response()->json(['error' => 'No such pack.'], 404); - } - - if (! Storage::exists('packs/' . $pack->uuid . '/archive.tar.gz')) { - return response()->json(['error' => 'There is no archive available for this pack.'], 503); - } - - return response()->json([ - 'archive.tar.gz' => sha1_file(storage_path('app/packs/' . $pack->uuid . '/archive.tar.gz')), - ]); - } - - /** - * Pulls an update pack archive from the system. - * - * @param \Illuminate\Http\Request $request - */ - public function pullUpdate(Request $request) - { - } -} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e1e7f0082..c6d537a26 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,7 +38,6 @@ use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings; use Pterodactyl\Http\Middleware\Api\Application\AuthenticateApplicationUser; -use Pterodactyl\Http\Middleware\DaemonAuthenticate as OldDaemonAuthenticate; class Kernel extends HttpKernel { @@ -107,7 +106,6 @@ class Kernel extends HttpKernel 'server' => AccessingValidServer::class, 'subuser.auth' => AuthenticateAsSubuser::class, 'admin' => AdminAuthenticate::class, - 'daemon-old' => OldDaemonAuthenticate::class, 'csrf' => VerifyCsrfToken::class, 'throttle' => ThrottleRequests::class, 'can' => Authorize::class, diff --git a/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php index f956302a4..686d7a0fc 100644 --- a/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon; use Closure; use Illuminate\Http\Request; +use Illuminate\Contracts\Encryption\Encrypter; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; @@ -25,14 +26,21 @@ class DaemonAuthenticate 'daemon.configuration', ]; + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + /** * DaemonAuthenticate constructor. * + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ - public function __construct(NodeRepositoryInterface $repository) + public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository) { $this->repository = $repository; + $this->encrypter = $encrypter; } /** @@ -50,20 +58,31 @@ class DaemonAuthenticate return $next($request); } - $token = $request->bearerToken(); - - if (is_null($token)) { - throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']); + if (is_null($bearer = $request->bearerToken())) { + throw new HttpException( + 401, 'Access this this endpoint must include an Authorization header.', null, ['WWW-Authenticate' => 'Bearer'] + ); } + [$identifier, $token] = explode('.', $bearer); + try { - $node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]); + /** @var \Pterodactyl\Models\Node $node */ + $node = $this->repository->findFirstWhere([ + 'daemon_token_id' => $identifier, + ]); + + if (hash_equals((string) $this->encrypter->decrypt($node->daemon_token), $token)) { + $request->attributes->set('node', $node); + + return $next($request); + } } catch (RecordNotFoundException $exception) { - throw new AccessDeniedHttpException; + // Do nothing, we don't want to expose a node not existing at all. } - $request->attributes->set('node', $node); - - return $next($request); + throw new AccessDeniedHttpException( + 'You are not authorized to access this resource.' + ); } } diff --git a/app/Http/Middleware/DaemonAuthenticate.php b/app/Http/Middleware/DaemonAuthenticate.php deleted file mode 100644 index cb132b999..000000000 --- a/app/Http/Middleware/DaemonAuthenticate.php +++ /dev/null @@ -1,69 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Middleware; - -use Closure; -use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -class DaemonAuthenticate -{ - /** - * An array of route names to not apply this middleware to. - * - * @var array - */ - private $except = [ - 'daemon.configuration', - ]; - - /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface - */ - private $repository; - - /** - * Create a new filter instance. - * - * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository - * @deprecated - */ - public function __construct(NodeRepositoryInterface $repository) - { - $this->repository = $repository; - } - - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function handle(Request $request, Closure $next) - { - if (in_array($request->route()->getName(), $this->except)) { - return $next($request); - } - - if (! $request->header('X-Access-Node')) { - throw new AccessDeniedHttpException; - } - - $node = $this->repository->findFirstWhere(['daemonSecret' => $request->header('X-Access-Node')]); - $request->attributes->set('node', $node); - - return $next($request); - } -} diff --git a/app/Models/Node.php b/app/Models/Node.php index 950186fde..7faccc378 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -3,11 +3,14 @@ namespace Pterodactyl\Models; use Symfony\Component\Yaml\Yaml; +use Illuminate\Container\Container; use Illuminate\Notifications\Notifiable; use Pterodactyl\Models\Traits\Searchable; +use Illuminate\Contracts\Encryption\Encrypter; /** * @property int $id + * @property string $uuid * @property bool $public * @property string $name * @property string $description @@ -21,7 +24,8 @@ use Pterodactyl\Models\Traits\Searchable; * @property int $disk * @property int $disk_overallocate * @property int $upload_size - * @property string $daemonSecret + * @property string $daemon_token_id + * @property string $daemon_token * @property int $daemonListen * @property int $daemonSFTP * @property string $daemonBase @@ -43,7 +47,8 @@ class Node extends Model */ const RESOURCE_NAME = 'node'; - const DAEMON_SECRET_LENGTH = 36; + const DAEMON_TOKEN_ID_LENGTH = 16; + const DAEMON_TOKEN_LENGTH = 64; /** * The table associated with the model. @@ -57,7 +62,7 @@ class Node extends Model * * @var array */ - protected $hidden = ['daemonSecret']; + protected $hidden = ['daemon_token_id', 'daemon_token']; /** * Cast values to correct type. @@ -84,8 +89,7 @@ class Node extends Model 'public', 'name', 'location_id', 'fqdn', 'scheme', 'behind_proxy', 'memory', 'memory_overallocate', 'disk', - 'disk_overallocate', 'upload_size', - 'daemonSecret', 'daemonBase', + 'disk_overallocate', 'upload_size', 'daemonBase', 'daemonSFTP', 'daemonListen', 'description', 'maintenance_mode', ]; @@ -153,12 +157,15 @@ class Node extends Model /** * Returns the configuration as an array. * - * @return string + * @return array */ - private function getConfiguration() + public function getConfiguration() { return [ 'debug' => false, + 'uuid' => $this->uuid, + 'token_id' => $this->daemon_token_id, + 'token' => Container::getInstance()->make(Encrypter::class)->decrypt($this->daemon_token), 'api' => [ 'host' => '0.0.0.0', 'port' => $this->daemonListen, @@ -202,7 +209,6 @@ class Node extends Model 'check_interval' => 100, ], 'remote' => route('index'), - 'token' => $this->daemonSecret, ]; } @@ -211,17 +217,32 @@ class Node extends Model * * @return string */ - public function getYamlConfiguration() { + public function getYamlConfiguration() + { return Yaml::dump($this->getConfiguration(), 4, 2); } - /** + /** * Returns the configuration in JSON format. * + * @param bool $pretty + * @return string + */ + public function getJsonConfiguration(bool $pretty = false) + { + return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES); + } + + /** + * Helper function to return the decrypted key for a node. + * * @return string */ - public function getJsonConfiguration(bool $pretty = false) { - return json_encode($this->getConfiguration(), $pretty ? JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT : JSON_UNESCAPED_SLASHES); + public function getDecryptedKey(): string + { + return (string) Container::getInstance()->make(Encrypter::class)->decrypt( + $this->daemon_token + ); } /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 2ee187c69..4e6099b9b 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -49,9 +49,5 @@ class RouteServiceProvider extends ServiceProvider Route::middleware(['daemon'])->prefix('/api/remote') ->namespace($this->namespace . '\Api\Remote') ->group(base_path('routes/api-remote.php')); - - Route::middleware(['web', 'daemon-old'])->prefix('/daemon') - ->namespace($this->namespace . '\Daemon') - ->group(base_path('routes/daemon.php')); } } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php deleted file mode 100644 index 6e38c1cad..000000000 --- a/app/Repositories/Daemon/BaseRepository.php +++ /dev/null @@ -1,154 +0,0 @@ -app = $app; - $this->nodeRepository = $nodeRepository; - } - - /** - * Set the node model to be used for this daemon connection. - * - * @param \Pterodactyl\Models\Node $node - * @return $this - */ - public function setNode(Node $node) - { - $this->node = $node; - - return $this; - } - - /** - * Return the node model being used. - * - * @return \Pterodactyl\Models\Node|null - */ - public function getNode() - { - return $this->node; - } - - /** - * Set the Server model to use when requesting information from the Daemon. - * - * @param \Pterodactyl\Models\Server $server - * @return $this - */ - public function setServer(Server $server) - { - $this->server = $server; - - return $this; - } - - /** - * Return the Server model. - * - * @return \Pterodactyl\Models\Server|null - */ - public function getServer() - { - return $this->server; - } - - /** - * Set the token to be used in the X-Access-Token header for requests to the daemon. - * - * @param string $token - * @return $this - */ - public function setToken(string $token) - { - $this->token = $token; - - return $this; - } - - /** - * Return the access token being used for requests. - * - * @return string|null - */ - public function getToken() - { - return $this->token; - } - - /** - * Return an instance of the Guzzle HTTP Client to be used for requests. - * - * @param array $headers - * @return \GuzzleHttp\Client - */ - public function getHttpClient(array $headers = []): Client - { - // If no node is set, load the relationship onto the Server model - // and pass that to the setNode function. - if (! $this->getNode() instanceof Node) { - if (! $this->getServer() instanceof Server) { - throw new RuntimeException('An instance of ' . Node::class . ' or ' . Server::class . ' must be set on this repository in order to return a client.'); - } - - $this->getServer()->loadMissing('node'); - $this->setNode($this->getServer()->getRelation('node')); - } - - if ($this->getServer() instanceof Server) { - $headers['X-Access-Server'] = $this->getServer()->uuid; - } - - $headers['X-Access-Token'] = $this->getToken() ?? $this->getNode()->daemonSecret; - - return new Client([ - 'verify' => config('app.env') === 'production', - 'base_uri' => sprintf('%s://%s:%s/v1/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen), - 'timeout' => config('pterodactyl.guzzle.timeout'), - 'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'), - 'headers' => $headers, - ]); - } -} diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php deleted file mode 100644 index cd123cd89..000000000 --- a/app/Repositories/Daemon/CommandRepository.php +++ /dev/null @@ -1,25 +0,0 @@ -getHttpClient()->request('POST', 'server/command', [ - 'json' => [ - 'command' => $command, - ], - ]); - } -} diff --git a/app/Repositories/Daemon/ConfigurationRepository.php b/app/Repositories/Daemon/ConfigurationRepository.php deleted file mode 100644 index 3905335a5..000000000 --- a/app/Repositories/Daemon/ConfigurationRepository.php +++ /dev/null @@ -1,46 +0,0 @@ -getNode(); - $structure = [ - 'web' => [ - 'listen' => $node->daemonListen, - 'ssl' => [ - 'enabled' => (! $node->behind_proxy && $node->scheme === 'https'), - ], - ], - 'sftp' => [ - 'path' => $node->daemonBase, - 'port' => $node->daemonSFTP, - ], - 'remote' => [ - 'base' => config('app.url'), - ], - 'uploads' => [ - 'size_limit' => $node->upload_size, - ], - 'keys' => [ - $node->daemonSecret, - ], - ]; - - return $this->getHttpClient()->request('PATCH', 'config', [ - 'json' => array_merge($structure, $overrides), - ]); - } -} diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php deleted file mode 100644 index 7c01f8dc8..000000000 --- a/app/Repositories/Daemon/FileRepository.php +++ /dev/null @@ -1,104 +0,0 @@ -getHttpClient()->request('GET', sprintf( - 'server/file/stat/%s', - rawurlencode($file['dirname'] . $file['basename']) - )); - - return json_decode($response->getBody()); - } - - /** - * Return the contents of a given file if it can be edited in the Panel. - * - * @param string $path - * @return string - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function getContent(string $path): string - { - $file = str_replace('\\', '/', pathinfo($path)); - $file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/'; - - $response = $this->getHttpClient()->request('GET', sprintf( - 'server/file/f/%s', - rawurlencode($file['dirname'] . $file['basename']) - )); - - return object_get(json_decode($response->getBody()), 'content'); - } - - /** - * Save new contents to a given file. - * - * @param string $path - * @param string $content - * @return \Psr\Http\Message\ResponseInterface - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function putContent(string $path, string $content): ResponseInterface - { - $file = str_replace('\\', '/', pathinfo($path)); - $file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/'; - - return $this->getHttpClient()->request('POST', 'server/file/save', [ - 'json' => [ - 'path' => rawurlencode($file['dirname'] . $file['basename']), - 'content' => $content, - ], - ]); - } - - /** - * Return a directory listing for a given path. - * - * @param string $path - * @return array - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function getDirectory(string $path): array - { - $response = $this->getHttpClient()->request('GET', sprintf('server/directory/%s', rawurlencode($path))); - - return json_decode($response->getBody()); - } - - /** - * Creates a new directory for the server in the given $path. - * - * @param string $name - * @param string $path - * @return \Psr\Http\Message\ResponseInterface - * - * @throws \RuntimeException - */ - public function createDirectory(string $name, string $path): ResponseInterface - { - throw new RuntimeException('Not implemented.'); - } -} diff --git a/app/Repositories/Daemon/PowerRepository.php b/app/Repositories/Daemon/PowerRepository.php deleted file mode 100644 index d7ce8d5e5..000000000 --- a/app/Repositories/Daemon/PowerRepository.php +++ /dev/null @@ -1,36 +0,0 @@ -getHttpClient()->request('PUT', 'server/power', [ - 'json' => [ - 'action' => $signal, - ], - ]); - default: - throw new InvalidPowerSignalException('The signal "' . $signal . '" is not defined and could not be processed.'); - } - } -} diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php deleted file mode 100644 index f1bd445fc..000000000 --- a/app/Repositories/Daemon/ServerRepository.php +++ /dev/null @@ -1,134 +0,0 @@ - $value) { - $structure[$key] = value($value); - } - - return $this->getHttpClient()->request('POST', 'servers', [ - 'json' => $structure, - ]); - } - - /** - * Update server details on the daemon. - * - * @param array $data - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function update(array $data): ResponseInterface - { - return $this->getHttpClient()->request('PATCH', 'server', [ - 'json' => $data, - ]); - } - - /** - * Mark a server to be reinstalled on the system. - * - * @param array|null $data - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function reinstall(array $data = null): ResponseInterface - { - return $this->getHttpClient()->request('POST', 'server/reinstall', [ - 'json' => $data ?? [], - ]); - } - - /** - * Mark a server as needing a container rebuild the next time the server is booted. - * - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function rebuild(): ResponseInterface - { - return $this->getHttpClient()->request('POST', 'server/rebuild'); - } - - /** - * Suspend a server on the daemon. - * - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function suspend(): ResponseInterface - { - return $this->getHttpClient()->request('POST', 'server/suspend'); - } - - /** - * Un-suspend a server on the daemon. - * - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function unsuspend(): ResponseInterface - { - return $this->getHttpClient()->request('POST', 'server/unsuspend'); - } - - /** - * Delete a server on the daemon. - * - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function delete(): ResponseInterface - { - return $this->getHttpClient()->request('DELETE', 'servers'); - } - - /** - * Return details on a specific server. - * - * @return \Psr\Http\Message\ResponseInterface - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function details(): ResponseInterface - { - return $this->getHttpClient()->request('GET', 'server'); - } - - /** - * Revoke an access key on the daemon before the time is expired. - * - * @param string|array $key - * @return \Psr\Http\Message\ResponseInterface - * - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function revokeAccessKey($key): ResponseInterface - { - if (is_array($key)) { - return $this->getHttpClient()->request('POST', 'keys/batch-delete', [ - 'json' => ['keys' => $key], - ]); - } - - Assert::stringNotEmpty($key, 'First argument passed to revokeAccessKey must be a non-empty string or array, received %s.'); - - return $this->getHttpClient()->request('DELETE', 'keys/' . $key); - } -} diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 6f317bc81..2385c9109 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -183,7 +183,7 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa public function getNodeWithResourceUsage(int $node_id): Node { $instance = $this->getBuilder() - ->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemonSecret', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate']) + ->select(['nodes.id', 'nodes.fqdn', 'nodes.scheme', 'nodes.daemon_token', 'nodes.daemonListen', 'nodes.memory', 'nodes.disk', 'nodes.memory_overallocate', 'nodes.disk_overallocate']) ->selectRaw('IFNULL(SUM(servers.memory), 0) as sum_memory, IFNULL(SUM(servers.disk), 0) as sum_disk') ->leftJoin('servers', 'servers.node_id', '=', 'nodes.id') ->where('nodes.id', $node_id); diff --git a/app/Repositories/Wings/DaemonConfigurationRepository.php b/app/Repositories/Wings/DaemonConfigurationRepository.php index 0579bcf0b..90b504772 100644 --- a/app/Repositories/Wings/DaemonConfigurationRepository.php +++ b/app/Repositories/Wings/DaemonConfigurationRepository.php @@ -23,4 +23,22 @@ class DaemonConfigurationRepository extends DaemonRepository return json_decode($response->getBody()->__toString(), true); } + + /** + * Updates the configuration information for a daemon. + * + * @param array $attributes + * @return \Psr\Http\Message\ResponseInterface + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function update(array $attributes = []) + { + try { + return $this->getHttpClient()->post( + '/api/update', array_merge($this->node->getConfiguration(), $attributes) + ); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception); + } + } } diff --git a/app/Repositories/Wings/DaemonRepository.php b/app/Repositories/Wings/DaemonRepository.php index b2535b3a9..4ebe249cf 100644 --- a/app/Repositories/Wings/DaemonRepository.php +++ b/app/Repositories/Wings/DaemonRepository.php @@ -79,7 +79,7 @@ abstract class DaemonRepository 'timeout' => config('pterodactyl.guzzle.timeout'), 'connect_timeout' => config('pterodactyl.guzzle.connect_timeout'), 'headers' => array_merge($headers, [ - 'Authorization' => 'Bearer ' . $this->node->daemonSecret, + 'Authorization' => 'Bearer ' . $this->node->getDecryptedKey(), 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]), diff --git a/app/Services/Nodes/NodeCreationService.php b/app/Services/Nodes/NodeCreationService.php index 889e81a20..5090ba23c 100644 --- a/app/Services/Nodes/NodeCreationService.php +++ b/app/Services/Nodes/NodeCreationService.php @@ -1,33 +1,34 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Pterodactyl\Services\Nodes; +use Illuminate\Support\Str; +use Pterodactyl\Models\Node; +use Illuminate\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; class NodeCreationService { - const DAEMON_SECRET_LENGTH = 36; - /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ protected $repository; + /** + * @var \Illuminate\Encryption\Encrypter + */ + private $encrypter; + /** * CreationService constructor. * + * @param \Illuminate\Encryption\Encrypter $encrypter * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ - public function __construct(NodeRepositoryInterface $repository) + public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository) { $this->repository = $repository; + $this->encrypter = $encrypter; } /** @@ -40,8 +41,9 @@ class NodeCreationService */ public function handle(array $data) { - $data['daemonSecret'] = str_random(self::DAEMON_SECRET_LENGTH); + $data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH); + $data['daemon_token_id'] = $this->encrypter->encrypt(Str::random(Node::DAEMON_TOKEN_ID_LENGTH)); - return $this->repository->create($data); + return $this->repository->create($data, true, true); } } diff --git a/app/Services/Nodes/NodeJWTService.php b/app/Services/Nodes/NodeJWTService.php index bb26527fd..6c9dd7577 100644 --- a/app/Services/Nodes/NodeJWTService.php +++ b/app/Services/Nodes/NodeJWTService.php @@ -69,6 +69,6 @@ class NodeJWTService return $builder ->withClaim('unique_id', Str::random(16)) - ->getToken($signer, new Key($node->daemonSecret)); + ->getToken($signer, new Key($node->getDecryptedKey())); } } diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index e8adabe48..8ce610df9 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -2,12 +2,15 @@ namespace Pterodactyl\Services\Nodes; +use Illuminate\Support\Str; use Pterodactyl\Models\Node; use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException; @@ -18,31 +21,39 @@ class NodeUpdateService */ private $connection; - /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface - */ - private $configRepository; - /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ private $repository; + /** + * @var \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository + */ + private $configurationRepository; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + /** * UpdateService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Repositories\Daemon\ConfigurationRepository $configurationRepository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Repositories\Wings\DaemonConfigurationRepository $configurationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ public function __construct( ConnectionInterface $connection, - ConfigurationRepository $configurationRepository, + Encrypter $encrypter, + DaemonConfigurationRepository $configurationRepository, NodeRepositoryInterface $repository ) { $this->connection = $connection; - $this->configRepository = $configurationRepository; $this->repository = $repository; + $this->configurationRepository = $configurationRepository; + $this->encrypter = $encrypter; } /** @@ -58,13 +69,14 @@ class NodeUpdateService * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException - * - * @throws \GuzzleHttp\Exception\GuzzleException */ public function handle(Node $node, array $data, bool $resetToken = false) { if ($resetToken) { - $data['daemonSecret'] = str_random(Node::DAEMON_SECRET_LENGTH); + $data['daemon_token'] = Str::random(Node::DAEMON_TOKEN_LENGTH); + $data['daemon_token_id'] = $this->encrypter->encrypt( + Str::random(Node::DAEMON_TOKEN_ID_LENGTH) + ); } $this->connection->beginTransaction(); @@ -77,14 +89,15 @@ class NodeUpdateService // We need to clone the new model and set it's authentication token to be the // old one so we can connect. Then we will pass the new token through as an // override on the call. - $cloned = $updatedModel->replicate(['daemonSecret']); - $cloned->setAttribute('daemonSecret', $node->getAttribute('daemonSecret')); + $cloned = $updatedModel->replicate(['daemon_token']); + $cloned->setAttribute('daemon_token', $node->getAttribute('daemon_token')); - $this->configRepository->setNode($cloned)->update([ - 'keys' => [$data['daemonSecret']], + $this->configurationRepository->setNode($cloned)->update([ + 'daemon_token_id' => $updatedModel->daemon_token_id, + 'daemon_token' => $updatedModel->getDecryptedKey(), ]); } else { - $this->configRepository->setNode($updatedModel)->update(); + $this->configurationRepository->setNode($updatedModel)->update(); } $this->connection->commit(); diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 8866fa555..3b2d3e728 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -1,7 +1,10 @@ define(Pterodactyl\Models\Location::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { return [ 'id' => $faker->unique()->randomNumber(), + 'uuid' => Uuid::uuid4()->toString(), 'public' => true, 'name' => $faker->firstName, 'fqdn' => $faker->ipv4, @@ -90,10 +94,11 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { 'disk' => 10240, 'disk_overallocate' => 0, 'upload_size' => 100, - 'daemonSecret' => $faker->uuid, + 'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH), + 'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH), 'daemonListen' => 8080, 'daemonSFTP' => 2022, - 'daemonBase' => '/srv/daemon', + 'daemonBase' => '/srv/daemon-data', ]; }); diff --git a/database/migrations/2020_04_10_141024_store_node_tokens_as_encrypted_value.php b/database/migrations/2020_04_10_141024_store_node_tokens_as_encrypted_value.php new file mode 100644 index 000000000..22c53397b --- /dev/null +++ b/database/migrations/2020_04_10_141024_store_node_tokens_as_encrypted_value.php @@ -0,0 +1,84 @@ +dropUnique(['daemonSecret']); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->char('uuid', 36)->after('id')->unique(); + $table->char('daemon_token_id', 16)->after('upload_size')->unique(); + $table->renameColumn('daemonSecret', 'daemon_token'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->text('daemon_token')->change(); + }); + + DB::transaction(function () { + /** @var \Illuminate\Contracts\Encryption\Encrypter $encrypter */ + $encrypter = Container::getInstance()->make(Encrypter::class); + + foreach (DB::select('SELECT id, daemon_token FROM nodes') as $datum) { + DB::update('UPDATE nodes SET uuid = ?, daemon_token_id = ?, daemon_token = ? WHERE id = ?', [ + Uuid::uuid4()->toString(), + substr($datum->daemon_token, 0, 16), + $encrypter->encrypt(substr($datum->daemon_token, 16)), + $datum->id, + ]); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::transaction(function () { + /** @var \Illuminate\Contracts\Encryption\Encrypter $encrypter */ + $encrypter = Container::getInstance()->make(Encrypter::class); + + foreach (DB::select('SELECT id, daemon_token_id, daemon_token FROM nodes') as $datum) { + DB::update('UPDATE nodes SET daemon_token = ? WHERE id = ?', [ + $datum->daemon_token_id . $encrypter->decrypt($datum->daemon_token), + $datum->id, + ]); + } + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropUnique(['uuid']); + $table->dropUnique(['daemon_token_id']); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn(['uuid', 'daemon_token_id']); + $table->renameColumn('daemon_token', 'daemonSecret'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->string('daemonSecret', 36)->change(); + $table->unique(['daemonSecret']); + }); + } +} diff --git a/resources/views/admin/nodes/index.blade.php b/resources/views/admin/nodes/index.blade.php index 6805e9ef4..b966d92cf 100644 --- a/resources/views/admin/nodes/index.blade.php +++ b/resources/views/admin/nodes/index.blade.php @@ -55,7 +55,7 @@ @foreach ($nodes as $node) - + {!! $node->maintenance_mode ? ' ' : '' !!}{{ $node->name }} {{ $node->location->short }} {{ $node->memory }} MB diff --git a/routes/daemon.php b/routes/daemon.php deleted file mode 100644 index 2c8058e36..000000000 --- a/routes/daemon.php +++ /dev/null @@ -1,13 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ -Route::get('/packs/pull/{uuid}', 'PackController@pull')->name('daemon.pack.pull'); -Route::get('/packs/pull/{uuid}/hash', 'PackController@hash')->name('daemon.pack.hash'); -Route::get('/configure/{token}', 'ActionController@configuration')->name('daemon.configuration'); - -Route::post('/install', 'ActionController@markInstall')->name('daemon.install');