Store node daemon tokens in an encrypted manner

This commit is contained in:
Dane Everitt 2020-04-10 15:15:38 -07:00
parent 2ac82af25a
commit 7557dddf49
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
26 changed files with 222 additions and 827 deletions

View file

@ -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', [

View file

@ -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([

View file

@ -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

View file

@ -1,107 +0,0 @@
<?php
namespace Pterodactyl\Http\Controllers\Daemon;
use Cache;
use Illuminate\Http\Request;
use Pterodactyl\Models\Node;
use Illuminate\Http\Response;
use Illuminate\Http\JsonResponse;
use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Repositories\Eloquent\ServerRepository;
use Pterodactyl\Events\Server\Installed as ServerInstalled;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class ActionController extends Controller
{
/**
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $eventDispatcher;
/**
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
*/
private $repository;
/**
* ActionController constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository
* @param \Illuminate\Contracts\Events\Dispatcher $eventDispatcher
*/
public function __construct(ServerRepository $repository, EventDispatcher $eventDispatcher)
{
$this->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();
}
}

View file

@ -1,73 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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)
{
}
}

View file

@ -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,

View file

@ -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.'
);
}
}

View file

@ -1,69 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View file

@ -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
);
}
/**

View file

@ -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'));
}
}

View file

@ -1,154 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use RuntimeException;
use GuzzleHttp\Client;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\Server;
use Illuminate\Foundation\Application;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface;
abstract class BaseRepository implements BaseRepositoryInterface
{
/**
* @var \Illuminate\Foundation\Application
*/
private $app;
/**
* @var \Pterodactyl\Models\Server
*/
private $server;
/**
* @var string|null
*/
private $token;
/**
* @var \Pterodactyl\Models\Node|null
*/
private $node;
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
private $nodeRepository;
/**
* BaseRepository constructor.
*
* @param \Illuminate\Foundation\Application $app
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository
*/
public function __construct(Application $app, NodeRepositoryInterface $nodeRepository)
{
$this->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,
]);
}
}

View file

@ -1,25 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface;
class CommandRepository extends BaseRepository implements CommandRepositoryInterface
{
/**
* Send a command to a server.
*
* @param string $command
* @return \Psr\Http\Message\ResponseInterface
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function send(string $command): ResponseInterface
{
return $this->getHttpClient()->request('POST', 'server/command', [
'json' => [
'command' => $command,
],
]);
}
}

View file

@ -1,46 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface;
class ConfigurationRepository extends BaseRepository implements ConfigurationRepositoryInterface
{
/**
* Update the configuration details for the specified node using data from the database.
*
* @param array $overrides
* @return \Psr\Http\Message\ResponseInterface
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function update(array $overrides = []): ResponseInterface
{
$node = $this->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),
]);
}
}

View file

@ -1,104 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use stdClass;
use RuntimeException;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
class FileRepository extends BaseRepository implements FileRepositoryInterface
{
/**
* Return stat information for a given file.
*
* @param string $path
* @return \stdClass
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function getFileStat(string $path): stdClass
{
$file = str_replace('\\', '/', pathinfo($path));
$file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/';
$response = $this->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.');
}
}

View file

@ -1,36 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface;
use Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException;
class PowerRepository extends BaseRepository implements PowerRepositoryInterface
{
/**
* Send a power signal to a server.
*
* @param string $signal
* @return \Psr\Http\Message\ResponseInterface
*
* @throws InvalidPowerSignalException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function sendSignal(string $signal): ResponseInterface
{
switch ($signal) {
case self::SIGNAL_START:
case self::SIGNAL_STOP:
case self::SIGNAL_RESTART:
case self::SIGNAL_KILL:
return $this->getHttpClient()->request('PUT', 'server/power', [
'json' => [
'action' => $signal,
],
]);
default:
throw new InvalidPowerSignalException('The signal "' . $signal . '" is not defined and could not be processed.');
}
}
}

View file

@ -1,134 +0,0 @@
<?php
namespace Pterodactyl\Repositories\Daemon;
use Webmozart\Assert\Assert;
use Psr\Http\Message\ResponseInterface;
use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface;
class ServerRepository extends BaseRepository implements ServerRepositoryInterface
{
/**
* Create a new server on the daemon for the panel.
*
* @param array $structure
* @param array $overrides
* @return \Psr\Http\Message\ResponseInterface
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function create(array $structure, array $overrides = []): ResponseInterface
{
foreach ($overrides as $key => $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);
}
}

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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',
]),

View file

@ -1,33 +1,34 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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);
}
}

View file

@ -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()));
}
}

View file

@ -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();

View file

@ -1,7 +1,10 @@
<?php
use Ramsey\Uuid\Uuid;
use Cake\Chronos\Chronos;
use Illuminate\Support\Str;
use Faker\Generator as Faker;
use Pterodactyl\Models\Node;
use Pterodactyl\Models\ApiKey;
/*
@ -80,6 +83,7 @@ $factory->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',
];
});

View file

@ -0,0 +1,84 @@
<?php
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\DB;
use Illuminate\Container\Container;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Contracts\Encryption\Encrypter;
class StoreNodeTokensAsEncryptedValue extends Migration
{
/**
* Run the migrations.
*
* @return void
* @throws \Exception
*/
public function up()
{
Schema::table('nodes', function (Blueprint $table) {
$table->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']);
});
}
}

View file

@ -55,7 +55,7 @@
</tr>
@foreach ($nodes as $node)
<tr>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->daemonSecret }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td class="text-center text-muted left-icon" data-action="ping" data-secret="{{ $node->getDecryptedKey() }}" data-location="{{ $node->scheme }}://{{ $node->fqdn }}:{{ $node->daemonListen }}/api/system"><i class="fa fa-fw fa-refresh fa-spin"></i></td>
<td>{!! $node->maintenance_mode ? '<span class="label label-warning"><i class="fa fa-wrench"></i></span> ' : '' !!}<a href="{{ route('admin.nodes.view', $node->id) }}">{{ $node->name }}</a></td>
<td>{{ $node->location->short }}</td>
<td>{{ $node->memory }} MB</td>

View file

@ -1,13 +0,0 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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');