Begin implementation of new daemon authentication scheme

This commit is contained in:
Dane Everitt 2017-09-23 20:45:25 -05:00
parent 8722571037
commit 906a699ee2
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
23 changed files with 796 additions and 145 deletions

View file

@ -0,0 +1,46 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Contracts\Repository;
interface DaemonKeyRepositoryInterface extends RepositoryInterface
{
/**
* Gets the daemon keys associated with a specific server.
*
* @param int $server
* @return \Illuminate\Support\Collection
*/
public function getServerKeys($server);
/**
* Return a daemon key with the associated server relation attached.
*
* @param string $key
* @return \Pterodactyl\Models\DaemonKey
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getKeyWithServer($key);
}

View file

@ -46,6 +46,17 @@ interface SubuserRepositoryInterface extends RepositoryInterface
*/
public function getWithPermissions($id);
/**
* Return a subuser and associated permissions given a user_id and server_id.
*
* @param int $user
* @param int $server
* @return \Pterodactyl\Models\Subuser
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithPermissionsUsingUserAndServer($user, $server);
/**
* Find a subuser and return with server and permissions relationships.
*
@ -55,4 +66,15 @@ interface SubuserRepositoryInterface extends RepositoryInterface
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithServerAndPermissions($id);
/**
* Return a subuser and their associated connection key for a server.
*
* @param int $user
* @param int $server
* @return \Pterodactyl\Models\Subuser
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function getWithKey($user, $server);
}

View file

@ -66,11 +66,16 @@ class Handler extends ExceptionHandler
$displayError = 'An unhandled exception was encountered with this request.';
}
$response = response()->json([
'error' => $displayError,
'http_code' => (! $this->isHttpException($exception)) ?: $exception->getStatusCode(),
'trace' => (! config('app.debug')) ? null : class_basename($exception) . ' in ' . $exception->getFile() . ' on line ' . $exception->getLine(),
], ($this->isHttpException($exception)) ? $exception->getStatusCode() : 500, [], JSON_UNESCAPED_SLASHES);
$response = response()->json(
[
'error' => $displayError,
'http_code' => (! $this->isHttpException($exception)) ?: $exception->getStatusCode(),
'trace' => (! config('app.debug')) ? null : $exception->getTrace(),
],
$this->isHttpException($exception) ? $exception->getStatusCode() : 500,
$this->isHttpException($exception) ? $exception->getHeaders() : [],
JSON_UNESCAPED_SLASHES
);
parent::report($exception);
} elseif ($exception instanceof DisplayException) {

View file

@ -0,0 +1,90 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Http\Controllers\API\Remote;
use Spatie\Fractal\Fractal;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Testing\HttpException;
use League\Fractal\Serializer\JsonApiSerializer;
use Pterodactyl\Transformers\Daemon\ApiKeyTransformer;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class ValidateKeyController extends Controller
{
/**
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $app;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
protected $daemonKeyRepository;
/**
* @var \Spatie\Fractal\Fractal
*/
protected $fractal;
/**
* ValidateKeyController constructor.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $daemonKeyRepository
* @param \Spatie\Fractal\Fractal $fractal
*/
public function __construct(
Application $app,
DaemonKeyRepositoryInterface $daemonKeyRepository,
Fractal $fractal
) {
$this->app = $app;
$this->daemonKeyRepository = $daemonKeyRepository;
$this->fractal = $fractal;
}
/**
* Return the server(s) and permissions associated with an API key.
*
* @param string $token
* @return array
*
* @throws \Illuminate\Foundation\Testing\HttpException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function index($token)
{
if (! starts_with($token, 'i_')) {
throw new HttpException(501);
}
$key = $this->daemonKeyRepository->getKeyWithServer($token);
return $this->fractal->item($key, $this->app->make(ApiKeyTransformer::class), 'server')
->serializeWith(JsonApiSerializer::class)
->toArray();
}
}

View file

@ -2,7 +2,9 @@
namespace Pterodactyl\Http;
use Pterodactyl\Http\Middleware\DaemonAuthenticate;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
use Illuminate\Routing\Middleware\SubstituteBindings;
class Kernel extends HttpKernel
{
@ -43,6 +45,10 @@ class Kernel extends HttpKernel
'throttle:60,1',
'bindings',
],
'daemon' => [
\Pterodactyl\Http\Middleware\Daemon\DaemonAuthenticate::class,
SubstituteBindings::class,
],
];
/**
@ -57,7 +63,7 @@ class Kernel extends HttpKernel
'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class,
'subuser' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class,
'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class,
'daemon' => \Pterodactyl\Http\Middleware\DaemonAuthenticate::class,
'daemon-old' => DaemonAuthenticate::class,
'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,

View file

@ -0,0 +1,82 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Http\Middleware\Daemon;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
class DaemonAuthenticate
{
/**
* @var array
*/
protected $except = ['daemon.configuration'];
/**
* @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface
*/
protected $repository;
/**
* DaemonAuthenticate constructor.
*
* @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository
*/
public function __construct(NodeRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Check if a request from the daemon can be properly attributed back to a single node instance.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function handle(Request $request, Closure $next)
{
$token = $request->bearerToken();
if (is_null($token)) {
throw new HttpException(401, null, null, ['WWW-Authenticate' => 'Bearer']);
}
try {
$node = $this->repository->findFirstWhere([['daemonSecret', '=', $token]]);
} catch (RecordNotFoundException $exception) {
throw new HttpException(403);
}
$request->attributes->set('node.model', $node);
return $next($request);
}
}

View file

@ -70,8 +70,8 @@ class ScheduleAccess
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function handle($request, Closure $next)
{

View file

@ -63,6 +63,7 @@ class SubuserAccess
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function handle($request, Closure $next)
{

View file

@ -82,6 +82,8 @@ class ServerAuthenticate
*
* @throws \Illuminate\Auth\AuthenticationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function handle(Request $request, Closure $next)
{

103
app/Models/DaemonKey.php Normal file
View file

@ -0,0 +1,103 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
class DaemonKey extends Model implements CleansAttributes, ValidableContract
{
use Eloquence, Validable;
/**
* @var string
*/
protected $table = 'daemon_keys';
/**
* @var array
*/
protected $casts = [
'user_id' => 'integer',
'server_id' => 'integer',
];
/**
* @var array
*/
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'expires_at',
];
/**
* @var array
*/
protected $fillable = ['user_id', 'server_id', 'secret', 'expires_at'];
/**
* @var array
*/
protected static $applicationRules = [
'user_id' => 'required',
'server_id' => 'required',
'secret' => 'required',
'expires_at' => 'required',
];
/**
* @var array
*/
protected static $dataIntegrityRules = [
'user_id' => 'numeric|exists:users,id',
'server_id' => 'numeric|exists:servers,id',
'secret' => 'string|min:20',
'expires_at' => 'date',
];
/**
* Return the server relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
/**
* Return the user relation.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

View file

@ -24,21 +24,18 @@
namespace Pterodactyl\Models;
use Auth;
use Cache;
use Carbon;
use Schema;
use Javascript;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
use Znck\Eloquent\Traits\BelongsToThrough;
use Sofa\Eloquence\Contracts\CleansAttributes;
use Sofa\Eloquence\Contracts\Validable as ValidableContract;
class Server extends Model implements CleansAttributes, ValidableContract
{
use Eloquence, Notifiable, Validable;
use BelongsToThrough, Eloquence, Notifiable, Validable;
/**
* The table associated with the model.
@ -52,7 +49,7 @@ class Server extends Model implements CleansAttributes, ValidableContract
*
* @var array
*/
protected $hidden = ['daemonSecret', 'sftp_password'];
protected $hidden = ['sftp_password'];
/**
* The attributes that should be mutated to dates.
@ -152,109 +149,6 @@ class Server extends Model implements CleansAttributes, ValidableContract
'node.name' => 2,
];
/**
* Returns a single server specified by UUID.
* DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS.
* YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS.
*
* @param string $uuid
* @param array $with
* @param array $withCount
* @return \Pterodactyl\Models\Server
* @throws \Exception
* @todo Remove $with and $withCount due to cache issues, they aren't used anyways.
*/
public static function byUuid($uuid, array $with = [], array $withCount = [])
{
if (! Auth::check()) {
throw new \Exception('You must call Server:byUuid as an authenticated user.');
}
// Results are cached because we call this functions a few times on page load.
$result = Cache::tags(['Model:Server', 'Model:Server:byUuid:' . $uuid])->remember('Model:Server:byUuid:' . $uuid . Auth::user()->uuid, Carbon::now()->addMinutes(15), function () use ($uuid) {
$query = self::with('service', 'node')->where(function ($q) use ($uuid) {
$q->where('uuidShort', $uuid)->orWhere('uuid', $uuid);
});
if (! Auth::user()->isRootAdmin()) {
$query->whereIn('id', Auth::user()->serverAccessArray());
}
return $query->first();
});
if (! is_null($result)) {
$result->daemonSecret = Auth::user()->daemonToken($result);
}
return $result;
}
/**
* Returns non-administrative headers for accessing a server on the daemon.
*
* @param Pterodactyl\Models\User|null $user
* @return array
*/
public function guzzleHeaders(User $user = null)
{
// If no specific user is passed, see if we can find an active
// auth session to pull data from.
if (is_null($user) && Auth::check()) {
$user = Auth::user();
}
return [
'X-Access-Server' => $this->uuid,
'X-Access-Token' => ($user) ? $user->daemonToken($this) : $this->daemonSecret,
];
}
/**
* Return an instance of the Guzzle client for this specific server using defined access token.
*
* @param Pterodactyl\Models\User|null $user
* @return \GuzzleHttp\Client
*/
public function guzzleClient(User $user = null)
{
return $this->node->guzzleClient($this->guzzleHeaders($user));
}
/**
* Returns javascript object to be embedded on server view pages with relevant information.
*
* @param array|null $additional
* @param array|null $overwrite
* @return \Laracasts\Utilities\JavaScript\JavaScriptFacade
*/
public function js($additional = null, $overwrite = null)
{
$response = [
'server' => collect($this->makeVisible('daemonSecret'))->only([
'uuid',
'uuidShort',
'daemonSecret',
'username',
]),
'node' => collect($this->node)->only([
'fqdn',
'scheme',
'daemonListen',
]),
];
if (is_array($additional)) {
$response = array_merge($response, $additional);
}
if (is_array($overwrite)) {
$response = $overwrite;
}
return Javascript::put($response);
}
/**
* Return the columns available for this table.
*
@ -358,12 +252,11 @@ class Server extends Model implements CleansAttributes, ValidableContract
/**
* Gets information for the tasks associated with this server.
*
* @TODO adjust server column in tasks to be server_id
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function tasks()
public function schedule()
{
return $this->hasMany(Task::class);
return $this->hasMany(Schedule::class);
}
/**
@ -377,12 +270,34 @@ class Server extends Model implements CleansAttributes, ValidableContract
}
/**
* Gets the location of the server.
* Returns the location that a server belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
* @return \Znck\Eloquent\Relations\BelongsToThrough
*
* @throws \Exception
*/
public function location()
{
return $this->node->location();
return $this->belongsToThrough(Location::class, Node::class);
}
/**
* Return the key belonging to the server owner.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function ownerKey()
{
return $this->hasOne(DaemonKey::class, 'user_id', 'owner_id');
}
/**
* Returns all of the daemon keys belonging to this server.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function keys()
{
return $this->hasMany(DaemonKey::class);
}
}

View file

@ -42,13 +42,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract
*/
protected $table = 'subusers';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['daemonSecret'];
/**
* Fields that are not mass assignable.
*
@ -72,7 +65,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract
protected static $applicationRules = [
'user_id' => 'required',
'server_id' => 'required',
'daemonSecret' => 'required',
];
/**
@ -81,7 +73,6 @@ class Subuser extends Model implements CleansAttributes, ValidableContract
protected static $dataIntegrityRules = [
'user_id' => 'numeric|exists:users,id',
'server_id' => 'numeric|exists:servers,id',
'daemonSecret' => 'string',
];
/**

View file

@ -28,6 +28,7 @@ use View;
use Cache;
use Pterodactyl\Models;
use Pterodactyl\Observers;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -37,6 +38,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
Schema::defaultStringLength(191);
Models\User::observe(Observers\UserObserver::class);
Models\Server::observe(Observers\ServerObserver::class);
Models\Subuser::observe(Observers\SubuserObserver::class);

View file

@ -40,6 +40,7 @@ use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Eloquent\DaemonKeyRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Repositories\Eloquent\PermissionRepository;
use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
@ -61,6 +62,7 @@ use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
@ -87,6 +89,7 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class);
$this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class);
$this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class);
$this->app->bind(DaemonKeyRepositoryInterface::class, DaemonKeyRepository::class);
$this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class);
$this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class);
$this->app->bind(LocationRepositoryInterface::class, LocationRepository::class);

View file

@ -54,7 +54,11 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace . '\Server')
->group(base_path('routes/server.php'));
Route::middleware(['web', 'daemon'])->prefix('/daemon')
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

@ -0,0 +1,66 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Repositories\Eloquent;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\DaemonKey;
use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
class DaemonKeyRepository extends EloquentRepository implements DaemonKeyRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function model()
{
return DaemonKey::class;
}
/**
* {@inheritdoc}
*/
public function getServerKeys($server)
{
Assert::integerish($server, 'First argument passed to getServerKeys must be integer, received %s.');
return $this->getBuilder()->where('server_id', $server)->get($this->getColumns());
}
/**
* {@inheritdoc}
*/
public function getKeyWithServer($key)
{
Assert::stringNotEmpty($key, 'First argument passed to getServerByKey must be string, received %s.');
$instance = $this->getBuilder()->with('server')->where('secret', '=', $key)->first();
if (is_null($instance)) {
throw new RecordNotFoundException;
}
return $instance;
}
}

View file

@ -69,6 +69,26 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI
return $instance;
}
/**
* {@inheritdoc}
*/
public function getWithPermissionsUsingUserAndServer($user, $server)
{
Assert::integerish($user, 'First argument passed to getWithPermissionsUsingUserAndServer must be integer, received %s.');
Assert::integerish($server, 'Second argument passed to getWithPermissionsUsingUserAndServer must be integer, received %s.');
$instance = $this->getBuilder()->with('permissions')->where([
['user_id', '=', $user],
['server_id', '=', $server],
])->first();
if (is_null($instance)) {
throw new RecordNotFoundException;
}
return $instance;
}
/**
* {@inheritdoc}
*/
@ -83,4 +103,24 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI
return $instance;
}
/**
* {@inheritdoc}
*/
public function getWithKey($user, $server)
{
Assert::integerish($user, 'First argument passed to getWithKey must be integer, received %s.');
Assert::integerish($server, 'Second argument passed to getWithKey must be integer, received %s.');
$instance = $this->getBuilder()->with('key')->where([
['user_id', '=', $user],
['server_id', '=', $server],
])->first();
if (is_null($instance)) {
throw new RecordNotFoundException;
}
return $instance;
}
}

View file

@ -29,24 +29,54 @@ use Pterodactyl\Models\Server;
use Illuminate\Cache\Repository as CacheRepository;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface;
use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException;
class ServerAccessHelperService
{
/**
* @var \Illuminate\Cache\Repository
*/
protected $cache;
/**
* @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface
*/
protected $daemonKeyRepository;
/**
* @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface
*/
protected $userRepository;
/**
* ServerAccessHelperService constructor.
*
* @param \Illuminate\Cache\Repository $cache
* @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $daemonKeyRepository
* @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository
* @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository
*/
public function __construct(
CacheRepository $cache,
DaemonKeyRepositoryInterface $daemonKeyRepository,
ServerRepositoryInterface $repository,
SubuserRepositoryInterface $subuserRepository,
UserRepositoryInterface $userRepository
) {
$this->cache = $cache;
$this->daemonKeyRepository = $daemonKeyRepository;
$this->repository = $repository;
$this->subuserRepository = $subuserRepository;
$this->userRepository = $userRepository;
}
/**
* Return the daemon secret to use when making a connection.
*
* @param int|\Pterodactyl\Models\Server $server
* @param int|\Pterodactyl\Models\User $user
* @return string
@ -64,19 +94,17 @@ class ServerAccessHelperService
$user = $this->userRepository->find($user);
}
if ($user->root_admin || $server->owner_id === $user->id) {
return $server->daemonSecret;
$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');
}
if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) {
if (is_null($key)) {
throw new UserNotLinkedToServerException;
}
$subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([
['user_id', '=', $user->id],
['server_id', '=', $server->id],
]);
return $subuser->daemonSecret;
return $key;
}
}

View file

@ -0,0 +1,82 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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\Transformers\Daemon;
use Pterodactyl\Models\DaemonKey;
use Pterodactyl\Models\Permission;
use League\Fractal\TransformerAbstract;
use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
class ApiKeyTransformer extends TransformerAbstract
{
/**
* @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface
*/
protected $repository;
/**
* ApiKeyTransformer constructor.
*
* @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository
*/
public function __construct(SubuserRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Return a listing of servers that a daemon key can access.
*
* @param \Pterodactyl\Models\DaemonKey $key
* @return array
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function transform(DaemonKey $key)
{
if ($key->user_id === $key->server->owner_id) {
return [
'id' => $key->server->uuid,
'permissions' => ['s:*'],
];
}
$subuser = $this->repository->getWithPermissionsUsingUserAndServer($key->user_id, $key->server_id);
$permissions = $subuser->permissions->pluck('permission')->toArray();
$mappings = Permission::getPermissions(true);
$daemonPermissions = [];
foreach ($permissions as $permission) {
if (! is_null($mappings[$permission])) {
$daemonPermissions[] = $mappings[$permission];
}
}
return [
$key->server->uuid => $daemonPermissions,
];
}
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateDaemonKeysTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::create('daemon_keys', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('server_id');
$table->unsignedInteger('user_id');
$table->string('secret')->unique();
$table->timestamp('expires_at');
$table->timestamps();
$table->index(['server_id', 'user_id']);
$table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::dropIfExists('daemon_keys');
}
}

View file

@ -0,0 +1,51 @@
<?php
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemoveDaemonSecretFromServersTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
$inserts = [];
$servers = DB::table('servers')->select('id', 'owner_id')->get();
$servers->each(function ($server) use (&$inserts) {
$inserts[] = [
'user_id' => $server->owner_id,
'server_id' => $server->id,
'secret' => 'i_' . str_random(40),
'expires_at' => Carbon::now()->addHours(24),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
});
DB::transaction(function () use ($inserts) {
DB::table('daemon_keys')->insert($inserts);
});
Schema::table('servers', function (Blueprint $table) {
$table->dropUnique(['daemonSecret']);
$table->dropColumn('daemonSecret');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('servers', function (Blueprint $table) {
$table->char('daemonSecret', 36)->after('startup')->unique();
});
DB::table('daemon_keys')->truncate();
}
}

View file

@ -0,0 +1,52 @@
<?php
use Carbon\Carbon;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class RemoveDaemonSecretFromSubusersTable extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
$inserts = [];
$subusers = DB::table('subusers')->get();
$subusers->each(function ($subuser) use (&$inserts) {
$inserts[] = [
'user_id' => $subuser->user_id,
'server_id' => $subuser->server_id,
'secret' => 'i_' . str_random(40),
'expires_at' => Carbon::now()->addHours(24),
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
});
DB::transaction(function () use ($inserts) {
DB::table('daemon_keys')->insert($inserts);
});
Schema::table('subusers', function (Blueprint $table) {
$table->dropUnique(['daemonSecret']);
$table->dropColumn('daemonSecret');
});
}
/**
* Reverse the migrations.
*/
public function down()
{
Schema::table('subusers', function (Blueprint $table) {
$table->char('daemonSecret', 36)->after('server_id')->unique();
});
$subusers = DB::table('subusers')->get();
$subusers->each(function ($subuser) {
DB::table('daemon_keys')->delete($subuser->id);
});
}
}

24
routes/api-remote.php Normal file
View file

@ -0,0 +1,24 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* 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.
*/
Route::get('/authenticate/{token}', 'ValidateKeyController@index')->name('post.api.remote.authenticate');