diff --git a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php index a18924f98..234fe42bc 100644 --- a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php @@ -31,6 +31,8 @@ interface BaseRepositoryInterface * * @param int $id * @return $this + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function setNode($id); @@ -77,5 +79,5 @@ interface BaseRepositoryInterface * @param array $headers * @return \GuzzleHttp\Client */ - public function getHttpClient($headers = []); + public function getHttpClient(array $headers = []); } diff --git a/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php b/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php new file mode 100644 index 000000000..f55113a9c --- /dev/null +++ b/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * 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\Daemon; + +interface CommandRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Send a command to a server. + * + * @param string $command + * @return \Psr\Http\Message\ResponseInterface + */ + public function send($command); +} diff --git a/app/Contracts/Repository/Daemon/FileRepositoryInterface.php b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php new file mode 100644 index 000000000..a013bc1ae --- /dev/null +++ b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php @@ -0,0 +1,61 @@ +. + * + * 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\Daemon; + +interface FileRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Return stat information for a given file. + * + * @param string $path + * @return object + */ + public function getFileStat($path); + + /** + * Return the contents of a given file if it can be edited in the Panel. + * + * @param string $path + * @return object + */ + public function getContent($path); + + /** + * Save new contents to a given file. + * + * @param string $path + * @param string $content + * @return \Psr\Http\Message\ResponseInterface + */ + public function putContent($path, $content); + + /** + * Return a directory listing for a given path. + * + * @param string $path + * @return array + */ + public function getDirectory($path); +} diff --git a/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php b/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php new file mode 100644 index 000000000..11c0c7e5c --- /dev/null +++ b/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php @@ -0,0 +1,43 @@ +. + * + * 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\Daemon; + +interface PowerRepositoryInterface extends BaseRepositoryInterface +{ + const SIGNAL_START = 'start'; + const SIGNAL_STOP = 'stop'; + const SIGNAL_RESTART = 'restart'; + const SIGNAL_KILL = 'kill'; + + /** + * Send a power signal to a server. + * + * @param string $signal + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function sendSignal($signal); +} diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index c6d9ff087..42bfb975f 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -34,7 +34,7 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @param bool $start * @return \Psr\Http\Message\ResponseInterface */ - public function create($id, $overrides = [], $start = false); + public function create($id, array $overrides = [], $start = false); /** * Set an access token and associated permissions for a server. diff --git a/app/Contracts/Repository/SubuserRepositoryInterface.php b/app/Contracts/Repository/SubuserRepositoryInterface.php index a31023f6b..93eb39b70 100644 --- a/app/Contracts/Repository/SubuserRepositoryInterface.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -26,6 +26,16 @@ namespace Pterodactyl\Contracts\Repository; interface SubuserRepositoryInterface extends RepositoryInterface { + /** + * Return a subuser with the associated server relationship. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithServer($id); + /** * Find a subuser and return with server and permissions relationships. * diff --git a/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php new file mode 100644 index 000000000..21579d20a --- /dev/null +++ b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php @@ -0,0 +1,29 @@ +. + * + * 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\Exceptions\Repository\Daemon; + +class InvalidPowerSignalException extends \Exception +{ +} diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index 43e2e2299..fc62d73e9 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -29,16 +29,47 @@ use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; +use Webmozart\Assert\Assert; class BaseRepository implements BaseRepositoryInterface { + /** + * @var \Illuminate\Foundation\Application + */ protected $app; + + /** + * @var + */ protected $accessServer; + + /** + * @var + */ protected $accessToken; + + /** + * @var + */ protected $node; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ protected $nodeRepository; + /** + * BaseRepository constructor. + * + * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + */ public function __construct( Application $app, ConfigRepository $config, @@ -49,44 +80,70 @@ class BaseRepository implements BaseRepositoryInterface $this->nodeRepository = $nodeRepository; } + /** + * {@inheritdoc} + */ public function setNode($id) { - // @todo accept a model + Assert::numeric($id, 'The first argument passed to setNode must be numeric, received %s.'); + $this->node = $this->nodeRepository->find($id); return $this; } + /** + * {@inheritdoc} + */ public function getNode() { return $this->node; } + /** + * {@inheritdoc} + */ public function setAccessServer($server = null) { + Assert::nullOrString($server, 'The first argument passed to setAccessServer must be null or a string, received %s.'); + $this->accessServer = $server; return $this; } + /** + * {@inheritdoc} + */ public function getAccessServer() { return $this->accessServer; } + /** + * {@inheritdoc} + */ public function setAccessToken($token = null) { + Assert::nullOrString($token, 'The first argument passed to setAccessToken must be null or a string, received %s.'); + $this->accessToken = $token; return $this; } + /** + * {@inheritdoc} + */ public function getAccessToken() { return $this->accessToken; } - public function getHttpClient($headers = []) + /** + * {@inheritdoc} + */ + public function getHttpClient(array $headers = []) { if (! is_null($this->accessServer)) { $headers['X-Access-Server'] = $this->getAccessServer(); diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php new file mode 100644 index 000000000..4984b6e46 --- /dev/null +++ b/app/Repositories/Daemon/CommandRepository.php @@ -0,0 +1,45 @@ +. + * + * 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\Daemon; + +use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; + +class CommandRepository extends BaseRepository implements CommandRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function send($command) + { + Assert::stringNotEmpty($command, 'First argument passed to send must be a non-empty string, received %s.'); + + return $this->getHttpClient()->request('POST', '/server/command', [ + 'json' => [ + 'command' => $command, + ], + ]); + } +} diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php new file mode 100644 index 000000000..71182b11c --- /dev/null +++ b/app/Repositories/Daemon/FileRepository.php @@ -0,0 +1,126 @@ +. + * + * 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\Daemon; + +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Webmozart\Assert\Assert; + +class FileRepository extends BaseRepository implements FileRepositoryInterface +{ + public function getFileStat($path) + { + Assert::stringNotEmpty($path, 'First argument passed to getStat must be a non-empty string, received %s.'); + + $file = 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()); + } + + /** + * {@inheritdoc} + */ + public function getContent($path) + { + Assert::stringNotEmpty($path, 'First argument passed to getContent must be a non-empty string, received %s.'); + + $file = 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 json_decode($response->getBody()); + } + + /** + * {@inheritdoc} + */ + public function putContent($path, $content) + { + Assert::stringNotEmpty($path, 'First argument passed to putContent must be a non-empty string, received %s.'); + Assert::string($content, 'Second argument passed to putContent must be a string, received %s.'); + + $file = 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, + ], + ]); + } + + /** + * {@inheritdoc} + */ + public function getDirectory($path) + { + Assert::string($path, 'First argument passed to getDirectory must be a string, received %s.'); + + $response = $this->getHttpClient()->request('GET', sprintf( + '/server/directory/%s', + rawurlencode($path) + )); + + $contents = json_decode($response->getBody()); + $files = []; + $folders = []; + + foreach ($contents as $value) { + if ($value->directory) { + array_push($folders, [ + 'entry' => $value->name, + 'directory' => trim($path, '/'), + 'size' => null, + 'date' => strtotime($value->modified), + 'mime' => $value->mime, + ]); + } elseif ($value->file) { + array_push($files, [ + 'entry' => $value->name, + 'directory' => trim($path, '/'), + 'extension' => pathinfo($value->name, PATHINFO_EXTENSION), + 'size' => human_readable($value->size), + 'date' => strtotime($value->modified), + 'mime' => $value->mime, + ]); + } + } + + return [ + 'files' => $files, + 'folders' => $folders, + ]; + } +} diff --git a/app/Repositories/Daemon/PowerRepository.php b/app/Repositories/Daemon/PowerRepository.php new file mode 100644 index 000000000..660db1e89 --- /dev/null +++ b/app/Repositories/Daemon/PowerRepository.php @@ -0,0 +1,55 @@ +. + * + * 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\Daemon; + +use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; +use Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException; + +class PowerRepository extends BaseRepository implements PowerRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function sendSignal($signal) + { + Assert::stringNotEmpty($signal, 'The first argument passed to sendSignal must be a non-empty string, received %s.'); + + 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, + ], + ]); + break; + 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 index 8de958b5e..a761510da 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -27,6 +27,7 @@ namespace Pterodactyl\Repositories\Daemon; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; +use Webmozart\Assert\Assert; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { @@ -35,8 +36,11 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa /** * {@inheritdoc} */ - public function create($id, $overrides = [], $start = false) + public function create($id, array $overrides = [], $start = false) { + Assert::numeric($id, 'First argument passed to create must be numeric, received %s.'); + Assert::boolean($start, 'Third argument passed to create must be boolean, received %s.'); + $repository = $this->app->make(DatabaseServerRepositoryInterface::class); $environment = $this->app->make(EnvironmentService::class); @@ -89,6 +93,8 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa */ public function setSubuserKey($key, array $permissions) { + Assert::stringNotEmpty($key, 'First argument passed to setSubuserKey must be a non-empty string, received %s.'); + return $this->getHttpClient()->request('PATCH', '/server', [ 'json' => [ 'keys' => [ @@ -113,6 +119,8 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa */ public function reinstall($data = null) { + Assert::nullOrIsArray($data, 'First argument passed to reinstall must be null or an array, received %s.'); + if (is_null($data)) { return $this->getHttpClient()->request('POST', '/server/reinstall'); } diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index 4909ecd1c..92f4b1867 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -39,6 +39,21 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI return Subuser::class; } + /** + * {@inheritdoc} + */ + public function getWithServer($id) + { + Assert::numeric($id, 'First argument passed to getWithServer must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('server')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/SubuserRepository.php b/app/Repositories/Old/SubuserRepository.php deleted file mode 100644 index 26c95107d..000000000 --- a/app/Repositories/Old/SubuserRepository.php +++ /dev/null @@ -1,262 +0,0 @@ -. - * - * 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; - -use DB; -use Validator; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; -use Pterodactyl\Services\UuidService; -use GuzzleHttp\Exception\TransferException; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class SubuserRepository -{ - /** - * Core permissions required for every subuser on the daemon. - * Without this we cannot connect the websocket or get basic - * information about the server. - * - * @var array - */ - protected $coreDaemonPermissions = [ - 's:get', - 's:console', - ]; - - /** - * Creates a new subuser on the server. - * - * @param int $sid - * @param array $data - * @return \Pterodactyl\Models\Subuser - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($sid, array $data) - { - $server = Server::with('node')->findOrFail($sid); - - $validator = Validator::make($data, [ - 'permissions' => 'required|array', - 'email' => 'required|email', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - // Determine if this user exists or if we need to make them an account. - $user = User::where('email', $data['email'])->first(); - if (! $user) { - try { - $repo = new oldUserRepository; - $user = $repo->create([ - 'email' => $data['email'], - 'username' => str_random(8), - 'name_first' => 'Unassigned', - 'name_last' => 'Name', - 'root_admin' => false, - ]); - } catch (\Exception $ex) { - throw $ex; - } - } elseif ($server->owner_id === $user->id) { - throw new DisplayException('You cannot add the owner of a server as a subuser.'); - } elseif (Subuser::select('id')->where('user_id', $user->id)->where('server_id', $server->id)->first()) { - throw new DisplayException('A subuser with that email already exists for this server.'); - } - - $uuid = new UuidService; - $subuser = Subuser::create([ - 'user_id' => $user->id, - 'server_id' => $server->id, - 'daemonSecret' => (string) $uuid->generate('servers', 'uuid'), - ]); - - $perms = Permission::listPermissions(true); - $daemonPermissions = $this->coreDaemonPermissions; - - foreach ($data['permissions'] as $permission) { - if (array_key_exists($permission, $perms)) { - // Build the daemon permissions array for sending. - if (! is_null($perms[$permission])) { - array_push($daemonPermissions, $perms[$permission]); - } - - Permission::create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } - - // Contact Daemon - // We contact even if they don't have any daemon permissions to overwrite - // if they did have them previously. - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => $daemonPermissions, - ], - ], - ]); - - DB::commit(); - - return $subuser; - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to add this user.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - - return false; - } - - /** - * Revokes a users permissions on a server. - * - * @param int $id - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $subuser = Subuser::with('server.node')->findOrFail($id); - $server = $subuser->server; - - DB::beginTransaction(); - - try { - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => [], - ], - ], - ]); - - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - $subuser->delete(); - DB::commit(); - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to delete this subuser.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates permissions for a given subuser. - * - * @param int $id - * @param array $data - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $validator = Validator::make($data, [ - 'permissions' => 'required|array', - 'user' => 'required|exists:users,id', - 'server' => 'required|exists:servers,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->all())); - } - - $subuser = Subuser::with('server.node')->findOrFail($id); - $server = $subuser->server; - - DB::beginTransaction(); - - try { - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - - $perms = Permission::listPermissions(true); - $daemonPermissions = $this->coreDaemonPermissions; - - foreach ($data['permissions'] as $permission) { - if (array_key_exists($permission, $perms)) { - // Build the daemon permissions array for sending. - if (! is_null($perms[$permission])) { - array_push($daemonPermissions, $perms[$permission]); - } - Permission::create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } - - // Contact Daemon - // We contact even if they don't have any daemon permissions to overwrite - // if they did have them previously. - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => $daemonPermissions, - ], - ], - ]); - - DB::commit(); - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to update permissions.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } -} diff --git a/app/Repositories/old_Daemon/CommandRepository.php b/app/Repositories/old_Daemon/CommandRepository.php deleted file mode 100644 index ce12e12df..000000000 --- a/app/Repositories/old_Daemon/CommandRepository.php +++ /dev/null @@ -1,90 +0,0 @@ -. - * - * 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\old_Daemon; - -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\ConnectException; -use Pterodactyl\Exceptions\DisplayException; - -class CommandRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * The Eloquent Model associated with the user to run the request as. - * - * @var \Pterodactyl\Models\User|null - */ - protected $user; - - /** - * Constuctor for repository. - * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - */ - public function __construct(Server $server, User $user = null) - { - $this->server = $server; - $this->user = $user; - } - - /** - * Sends a command to the daemon. - * - * @param string $command - * @return string - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \GuzzleHttp\Exception\RequestException - */ - public function send($command) - { - // We don't use the user's specific daemon secret here since we - // are assuming that a call to this function has been validated. - try { - $response = $this->server->guzzleClient($this->user)->request('POST', '/server/command', [ - 'http_errors' => false, - 'json' => [ - 'command' => $command, - ], - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - throw new DisplayException('Command sending responded with a non-200 error code (HTTP/' . $response->getStatusCode() . ').'); - } - - return $response->getBody(); - } catch (ConnectException $ex) { - throw $ex; - } - } -} diff --git a/app/Repositories/old_Daemon/FileRepository.php b/app/Repositories/old_Daemon/FileRepository.php deleted file mode 100644 index b4dc5f7f7..000000000 --- a/app/Repositories/old_Daemon/FileRepository.php +++ /dev/null @@ -1,192 +0,0 @@ -. - * - * 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\old_Daemon; - -use Exception; -use GuzzleHttp\Client; -use Pterodactyl\Models\Server; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Repositories\HelperRepository; - -class FileRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * Constructor. - * - * @param string $uuid - */ - public function __construct($uuid) - { - $this->server = Server::byUuid($uuid); - } - - /** - * Get the contents of a requested file for the server. - * - * @param string $file - * @return array - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function returnFileContents($file) - { - if (empty($file)) { - throw new Exception('Not all parameters were properly passed to the function.'); - } - - $file = (object) pathinfo($file); - $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; - - $res = $this->server->guzzleClient()->request('GET', '/server/file/stat/' . rawurlencode($file->dirname . $file->basename)); - - $stat = json_decode($res->getBody()); - if ($res->getStatusCode() !== 200 || ! isset($stat->size)) { - throw new DisplayException('The daemon provided a non-200 error code on stat lookup: HTTP\\' . $res->getStatusCode()); - } - - if (! in_array($stat->mime, HelperRepository::editableFiles())) { - throw new DisplayException('You cannot edit that type of file (' . $stat->mime . ') through the panel.'); - } - - if ($stat->size > 5000000) { - throw new DisplayException('That file is too large to open in the browser, consider using a SFTP client.'); - } - - $res = $this->server->guzzleClient()->request('GET', '/server/file/f/' . rawurlencode($file->dirname . $file->basename)); - - $json = json_decode($res->getBody()); - if ($res->getStatusCode() !== 200 || ! isset($json->content)) { - throw new DisplayException('The daemon provided a non-200 error code: HTTP\\' . $res->getStatusCode()); - } - - return [ - 'file' => $json, - 'stat' => $stat, - ]; - } - - /** - * Save the contents of a requested file on the daemon. - * - * @param string $file - * @param string $content - * @return bool - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function saveFileContents($file, $content) - { - if (empty($file)) { - throw new Exception('A valid file and path must be specified to save a file.'); - } - - $file = (object) pathinfo($file); - $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; - - $res = $this->server->guzzleClient()->request('POST', '/server/file/save', [ - 'json' => [ - 'path' => rawurlencode($file->dirname . $file->basename), - 'content' => $content, - ], - ]); - - if ($res->getStatusCode() !== 204) { - throw new DisplayException('An error occured while attempting to save this file. ' . $res->getBody()); - } - - return true; - } - - /** - * Returns a listing of all files and folders within a specified directory on the daemon. - * - * @param string $directory - * @return object - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function returnDirectoryListing($directory) - { - if (empty($directory)) { - throw new Exception('A valid directory must be specified in order to list its contents.'); - } - - try { - $res = $this->server->guzzleClient()->request('GET', '/server/directory/' . rawurlencode($directory)); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $json = json_decode($ex->getResponse()->getBody()); - - throw new DisplayException($json->error); - } catch (\GuzzleHttp\Exception\ServerException $ex) { - throw new DisplayException('A remote server error was encountered while attempting to display this directory.'); - } catch (\GuzzleHttp\Exception\ConnectException $ex) { - throw new DisplayException('A ConnectException was encountered: unable to contact daemon.'); - } catch (\Exception $ex) { - throw $ex; - } - - $json = json_decode($res->getBody()); - - // Iterate through results - $files = []; - $folders = []; - foreach ($json as &$value) { - if ($value->directory) { - // @TODO Handle Symlinks - $folders[] = [ - 'entry' => $value->name, - 'directory' => trim($directory, '/'), - 'size' => null, - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]; - } elseif ($value->file) { - $files[] = [ - 'entry' => $value->name, - 'directory' => trim($directory, '/'), - 'extension' => pathinfo($value->name, PATHINFO_EXTENSION), - 'size' => HelperRepository::bytesToHuman($value->size), - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]; - } - } - - return (object) [ - 'files' => $files, - 'folders' => $folders, - ]; - } -} diff --git a/app/Repositories/old_Daemon/PowerRepository.php b/app/Repositories/old_Daemon/PowerRepository.php deleted file mode 100644 index 7b941f121..000000000 --- a/app/Repositories/old_Daemon/PowerRepository.php +++ /dev/null @@ -1,120 +0,0 @@ -. - * - * 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\old_Daemon; - -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\ConnectException; -use Pterodactyl\Exceptions\DisplayException; - -class PowerRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * The Eloquent Model associated with the user to run the request as. - * - * @var \Pterodactyl\Models\User|null - */ - protected $user; - - /** - * Constuctor for repository. - * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - */ - public function __construct(Server $server, User $user = null) - { - $this->server = $server; - $this->user = $user; - } - - /** - * Sends a power option to the daemon. - * - * @param string $action - * @return string - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function do($action) - { - try { - $response = $this->server->guzzleClient($this->user)->request('PUT', '/server/power', [ - 'http_errors' => false, - 'json' => [ - 'action' => $action, - ], - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - throw new DisplayException('Power toggle endpoint responded with a non-200 error code (HTTP/' . $response->getStatusCode() . ').'); - } - - return $response->getBody(); - } catch (ConnectException $ex) { - throw $ex; - } - } - - /** - * Starts a server. - */ - public function start() - { - $this->do('start'); - } - - /** - * Stops a server. - */ - public function stop() - { - $this->do('stop'); - } - - /** - * Restarts a server. - */ - public function restart() - { - $this->do('restart'); - } - - /** - * Kills a server. - */ - public function kill() - { - $this->do('kill'); - } -} diff --git a/app/Services/Subusers/PermissionCreationService.php b/app/Services/Subusers/PermissionCreationService.php new file mode 100644 index 000000000..8414e6c7c --- /dev/null +++ b/app/Services/Subusers/PermissionCreationService.php @@ -0,0 +1,84 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Subusers; + +use Pterodactyl\Models\Permission; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; + +class PermissionCreationService +{ + const CORE_DAEMON_PERMISSIONS = [ + 's:get', + 's:console', + ]; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $repository; + + /** + * PermissionCreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $repository + */ + public function __construct(PermissionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Assign permissions to a given subuser. + * + * @param int $subuser + * @param array $permissions + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($subuser, array $permissions) + { + $permissionMappings = Permission::getPermissions(true); + $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; + $insertPermissions = []; + + foreach ($permissions as $permission) { + if (array_key_exists($permission, $permissionMappings)) { + if (! is_null($permissionMappings[$permission])) { + array_push($daemonPermissions, $permissionMappings[$permission]); + } + + array_push($insertPermissions, [ + 'subuser_id' => $subuser, + 'permission' => $permission, + ]); + } + } + + $this->repository->insert($insertPermissions); + + return $daemonPermissions; + } +} diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 8f6e83292..3de92e27c 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -28,24 +28,17 @@ use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Illuminate\Log\Writer; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; -use Pterodactyl\Models\Permission; use Pterodactyl\Models\Server; use Pterodactyl\Services\Users\CreationService; class SubuserCreationService { - const CORE_DAEMON_PERMISSIONS = [ - 's:get', - 's:console', - ]; - const DAEMON_SECRET_BYTES = 18; /** @@ -59,9 +52,9 @@ class SubuserCreationService protected $daemonRepository; /** - * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + * @var \Pterodactyl\Services\Subusers\PermissionCreationService */ - protected $permissionRepository; + protected $permissionService; /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface @@ -92,7 +85,7 @@ class SubuserCreationService ConnectionInterface $connection, CreationService $userCreationService, DaemonServerRepositoryInterface $daemonRepository, - PermissionRepositoryInterface $permissionRepository, + PermissionCreationService $permissionService, ServerRepositoryInterface $serverRepository, SubuserRepositoryInterface $subuserRepository, UserRepositoryInterface $userRepository, @@ -100,7 +93,7 @@ class SubuserCreationService ) { $this->connection = $connection; $this->daemonRepository = $daemonRepository; - $this->permissionRepository = $permissionRepository; + $this->permissionService = $permissionService; $this->subuserRepository = $subuserRepository; $this->serverRepository = $serverRepository; $this->userRepository = $userRepository; @@ -154,21 +147,7 @@ class SubuserCreationService 'daemonSecret' => bin2hex(random_bytes(self::DAEMON_SECRET_BYTES)), ]); - $permissionMappings = Permission::getPermissions(true); - $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; - - foreach ($permissions as $permission) { - if (array_key_exists($permission, $permissionMappings)) { - if (! is_null($permissionMappings[$permission])) { - array_push($daemonPermissions, $permissionMappings[$permission]); - } - - $this->permissionRepository->create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } + $daemonPermissions = $this->permissionService->handle($subuser->id, $permissions); try { $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid) @@ -178,9 +157,9 @@ class SubuserCreationService return $subuser; } catch (RequestException $exception) { $this->connection->rollBack(); - $response = $exception->getResponse(); $this->writer->warning($exception); + $response = $exception->getResponse(); throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php index d06f57bd7..2cbc168b0 100644 --- a/app/Services/Subusers/SubuserDeletionService.php +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -84,7 +84,7 @@ class SubuserDeletionService */ public function handle($subuser) { - $subuser = $this->repository->getWithServerAndPermissions($subuser); + $subuser = $this->repository->getWithServer($subuser); $this->connection->beginTransaction(); $response = $this->repository->delete($subuser->id); @@ -97,9 +97,9 @@ class SubuserDeletionService return $response; } catch (RequestException $exception) { $this->connection->rollBack(); - $response = $exception->getResponse(); $this->writer->warning($exception); + $response = $exception->getResponse(); throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php new file mode 100644 index 000000000..f69870cd7 --- /dev/null +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -0,0 +1,125 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; + +class SubuserUpdateService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $permissionService; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * SubuserUpdateService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Services\Subusers\PermissionCreationService $permissionService + * @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $permissionRepository + * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $connection, + DaemonServerRepositoryInterface $daemonRepository, + PermissionCreationService $permissionService, + PermissionRepositoryInterface $permissionRepository, + SubuserRepositoryInterface $repository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->permissionRepository = $permissionRepository; + $this->permissionService = $permissionService; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Update permissions for a given subuser. + * + * @param int $subuser + * @param array $permissions + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($subuser, array $permissions) + { + $subuser = $this->repository->getWithServer($subuser); + + $this->connection->beginTransaction(); + $this->permissionRepository->deleteWhere([['subuser_id', '=', $subuser->id]]); + $daemonPermissions = $this->permissionService->handle($subuser->id, $permissions); + + try { + $this->daemonRepository->setNode($subuser->server->node_id)->setAccessServer($subuser->server->uuid) + ->setSubuserKey($subuser->daemonSecret, $daemonPermissions); + $this->connection->commit(); + } catch (RequestException $exception) { + $this->connection->rollBack(); + $this->writer->warning($exception); + + $response = $exception->getResponse(); + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/app/helpers.php b/app/helpers.php index 763886f0d..0b51c7f06 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -32,6 +32,16 @@ if (! function_exists('human_readable')) { */ function human_readable($path, $precision = 2) { + if (is_numeric($path)) { + $i = 0; + while (($path / 1024) > 0.9) { + $path = $path / 1024; + ++$i; + } + + return round($path, $precision) . ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i]; + } + return app('file')->humanReadableSize($path, $precision); } } diff --git a/config/pterodactyl.php b/config/pterodactyl.php index a21283d1d..7c66f3224 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -143,6 +143,7 @@ return [ | This array includes the MIME filetypes that can be edited via the web. */ 'files' => [ + 'max_edit_size' => env('PTERODACTYL_FILES_MAX_EDIT_SIZE', 50000), 'editable' => [ 'application/json', 'application/javascript', diff --git a/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php new file mode 100644 index 000000000..85171448d --- /dev/null +++ b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php @@ -0,0 +1,72 @@ +. + * + * 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 Tests\Unit\Services\Subusers; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Tests\TestCase; + +class PermissionCreationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(PermissionRepositoryInterface::class); + $this->service = new PermissionCreationService($this->repository); + } + + /** + * Test that permissions can be assigned correctly. + */ + public function testPermissionsAreAssignedCorrectly() + { + $permissions = ['reset-sftp', 'view-sftp']; + + $this->repository->shouldReceive('insert')->with([ + ['subuser_id' => 1, 'permission' => 'reset-sftp'], + ['subuser_id' => 1, 'permission' => 'view-sftp'], + ]); + + $response = $this->service->handle(1, $permissions); + + $this->assertNotEmpty($response); + $this->assertEquals(['s:get', 's:console', 's:set-password'], $response); + } +} diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index a7492cb34..bf242cab4 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -29,7 +29,6 @@ use Illuminate\Log\Writer; use Mockery as m; use phpmock\phpunit\PHPMock; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; @@ -38,6 +37,7 @@ use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; use Pterodactyl\Models\User; +use Pterodactyl\Services\Subusers\PermissionCreationService; use Pterodactyl\Services\Subusers\SubuserCreationService; use Pterodactyl\Services\Users\CreationService; use Tests\TestCase; @@ -58,14 +58,9 @@ class SubuserCreationServiceTest extends TestCase protected $daemonRepository; /** - * @var \Pterodactyl\Models\Permission + * @var \Pterodactyl\Services\Subusers\PermissionCreationService */ - protected $permission; - - /** - * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface - */ - protected $permissionRepository; + protected $permissionService; /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface @@ -108,8 +103,7 @@ class SubuserCreationServiceTest extends TestCase $this->connection = m::mock(ConnectionInterface::class); $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->permission = m::mock('overload:Pterodactyl\Models\Permission'); - $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->permissionService = m::mock(PermissionCreationService::class); $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->userCreationService = m::mock(CreationService::class); @@ -120,7 +114,7 @@ class SubuserCreationServiceTest extends TestCase $this->connection, $this->userCreationService, $this->daemonRepository, - $this->permissionRepository, + $this->permissionService, $this->serverRepository, $this->subuserRepository, $this->userRepository, @@ -154,14 +148,8 @@ class SubuserCreationServiceTest extends TestCase 'daemonSecret' => 'bin2hex', ])->once()->andReturn($subuser); - $this->permission->shouldReceive('getPermissions')->with(true)->once() - ->andReturn($permissions); - - foreach(array_keys($permissions) as $permission) { - $this->permissionRepository->shouldReceive('create') - ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) - ->once()->andReturnNull(); - } + $this->permissionService->shouldReceive('handle')->with($subuser->id, array_keys($permissions))->once() + ->andReturn(['s:get', 's:console', 'test:1']); $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() @@ -179,7 +167,7 @@ class SubuserCreationServiceTest extends TestCase */ public function testExistingUserCanBeAddedAsASubuser() { - $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $permissions = ['view-sftp', 'reset-sftp']; $server = factory(Server::class)->make(); $user = factory(User::class)->make(); $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); @@ -197,21 +185,15 @@ class SubuserCreationServiceTest extends TestCase 'daemonSecret' => 'bin2hex', ])->once()->andReturn($subuser); - $this->permission->shouldReceive('getPermissions')->with(true)->once() - ->andReturn($permissions); - - foreach(array_keys($permissions) as $permission) { - $this->permissionRepository->shouldReceive('create') - ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) - ->once()->andReturnNull(); - } + $this->permissionService->shouldReceive('handle')->with($subuser->id, $permissions)->once() + ->andReturn(['s:get', 's:console', 's:set-password']); $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() - ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 's:set-password'])->once()->andReturnSelf(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->handle($server, $user->email, array_keys($permissions)); + $response = $this->service->handle($server, $user->email, $permissions); $this->assertInstanceOf(Subuser::class, $response); $this->assertSame($subuser, $response); diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php index e465eb16a..23d0155a1 100644 --- a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -97,7 +97,7 @@ class SubuserDeletionServiceTest extends TestCase $subuser = factory(Subuser::class)->make(); $subuser->server = factory(Server::class)->make(); - $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); @@ -119,7 +119,7 @@ class SubuserDeletionServiceTest extends TestCase $subuser = factory(Subuser::class)->make(); $subuser->server = factory(Server::class)->make(); - $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); diff --git a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php new file mode 100644 index 000000000..f4742f9c0 --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php @@ -0,0 +1,158 @@ +. + * + * 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 Tests\Unit\Services\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Pterodactyl\Services\Subusers\SubuserUpdateService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserUpdateServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $permissionService; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserUpdateService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->permissionService = m::mock(PermissionCreationService::class); + $this->repository = m::mock(SubuserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserUpdateService( + $this->connection, + $this->daemonRepository, + $this->permissionService, + $this->permissionRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that permissions are updated in the database. + */ + public function testPermissionsAreUpdated() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->permissionRepository->shouldReceive('deleteWhere')->with([ + ['subuser_id', '=', $subuser->id], + ])->once()->andReturnNull(); + $this->permissionService->shouldReceive('handle')->with($subuser->id, ['some-permission'])->once()->andReturn(['test:1', 'test:2']); + + $this->daemonRepository->shouldReceive('setNode')->with($subuser->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($subuser->server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['test:1', 'test:2'])->once()->andReturnNull(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($subuser->id, ['some-permission']); + } + + /** + * Test that an exception is thrown if the daemon connection fails. + */ + public function testExceptionIsThrownIfDaemonConnectionFails() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->permissionRepository->shouldReceive('deleteWhere')->with([ + ['subuser_id', '=', $subuser->id], + ])->once()->andReturnNull(); + $this->permissionService->shouldReceive('handle')->with($subuser->id, [])->once()->andReturn([]); + + $this->daemonRepository->shouldReceive('setNode')->once()->andThrow($this->exception); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->handle($subuser->id, []); + } catch (DisplayException $exception) { + $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + } + } +}