From ec99859590336d6ce71b8be9ba45727a1f63d96d Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sat, 28 Nov 2020 14:48:35 -0500 Subject: [PATCH 01/14] Don't show delete button if its own user Don't show the delete button if a sub-user is looking at sub-users, as they cannot delete themselves. --- resources/scripts/components/server/users/UserRow.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/scripts/components/server/users/UserRow.tsx b/resources/scripts/components/server/users/UserRow.tsx index aa5e55a39..656a69510 100644 --- a/resources/scripts/components/server/users/UserRow.tsx +++ b/resources/scripts/components/server/users/UserRow.tsx @@ -61,7 +61,9 @@ export default ({ subuser }: Props) => { } + {subuser.uuid !== uuid && + } ); From 947fdf72ed12ddbf5aea3fd04d4d969ad78db2ee Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sun, 29 Nov 2020 18:13:20 -0500 Subject: [PATCH 02/14] Update UserRow.tsx --- .../components/server/users/UserRow.tsx | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/scripts/components/server/users/UserRow.tsx b/resources/scripts/components/server/users/UserRow.tsx index 656a69510..08adecd78 100644 --- a/resources/scripts/components/server/users/UserRow.tsx +++ b/resources/scripts/components/server/users/UserRow.tsx @@ -48,23 +48,23 @@ export default ({ subuser }: Props) => {

Permissions

- - {subuser.uuid !== uuid && - - } - - - {subuser.uuid !== uuid && - - } - + {subuser.uuid !== uuid && + + + + } + {subuser.uuid !== uuid && + + + + } ); }; From 76d671aa8aaf6b370f2a046613c339b1bf099bc7 Mon Sep 17 00:00:00 2001 From: Charles Morgan Date: Sun, 29 Nov 2020 19:35:16 -0500 Subject: [PATCH 03/14] Update UserRow.tsx --- .../components/server/users/UserRow.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/scripts/components/server/users/UserRow.tsx b/resources/scripts/components/server/users/UserRow.tsx index 08adecd78..ec20baa8e 100644 --- a/resources/scripts/components/server/users/UserRow.tsx +++ b/resources/scripts/components/server/users/UserRow.tsx @@ -49,21 +49,21 @@ export default ({ subuser }: Props) => {

Permissions

{subuser.uuid !== uuid && - - - - } - {subuser.uuid !== uuid && - - - + <> + + + + + + + } ); From de1f7ea90649624ec1c54615b91ca6d4bbce79d2 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Thu, 3 Dec 2020 21:10:41 -0700 Subject: [PATCH 04/14] Move file manager buttons beside the breadcrumb, other tweaks --- .../components/elements/SubNavigation.tsx | 15 +++---- .../server/files/FileManagerBreadcrumbs.tsx | 2 +- .../server/files/FileManagerContainer.tsx | 41 ++++++++++--------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/resources/scripts/components/elements/SubNavigation.tsx b/resources/scripts/components/elements/SubNavigation.tsx index 6b8379697..8da2cf44a 100644 --- a/resources/scripts/components/elements/SubNavigation.tsx +++ b/resources/scripts/components/elements/SubNavigation.tsx @@ -5,23 +5,24 @@ import config from '../../../../tailwind.config'; const SubNavigation = styled.div` ${tw`w-full bg-neutral-700 shadow overflow-x-auto`}; - + & > div { ${tw`flex items-center text-sm mx-auto px-2`}; max-width: 1200px; - + & > a, & > div { ${tw`inline-block py-3 px-4 text-neutral-300 no-underline whitespace-no-wrap transition-all duration-150`}; - + &:not(:first-of-type) { ${tw`ml-2`}; } - - &:active, &:hover { + + &:hover { ${tw`text-neutral-100`}; } - - &:active, &:hover, &.active { + + &:active, &.active { + ${tw`text-neutral-100`}; box-shadow: inset 0 -2px ${config.theme.colors.cyan['500']}; } } diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx index 11b7abde7..9b1596f5c 100644 --- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx +++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx @@ -44,7 +44,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => { }; return ( -
+
{(files && files.length > 0 && !params?.action) ? { return ( - - - +
+ + + + + + +
+ + + + + +
+
+
+
{ !files ? @@ -83,22 +102,6 @@ export default () => {
} - - -
- - - - - -
-
-
} From 8611ebb2d6ee6ced6d4b698b41ebdefdd97266f5 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 29 Nov 2020 13:49:29 -0700 Subject: [PATCH 05/14] Add /api/client/servers/{server}/files/chmod endpoint --- .../Api/Client/Servers/FileController.php | 20 ++++++++++++ .../Servers/Files/ChmodFilesRequest.php | 31 +++++++++++++++++++ .../Wings/DaemonFileRepository.php | 28 +++++++++++++++++ routes/api-client.php | 1 + 4 files changed, 80 insertions(+) create mode 100644 app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php diff --git a/app/Http/Controllers/Api/Client/Servers/FileController.php b/app/Http/Controllers/Api/Client/Servers/FileController.php index 9e705651b..0e3a62f2e 100644 --- a/app/Http/Controllers/Api/Client/Servers/FileController.php +++ b/app/Http/Controllers/Api/Client/Servers/FileController.php @@ -16,6 +16,7 @@ use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CopyFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ListFilesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DeleteFileRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\RenameFileRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CreateFolderRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\CompressFilesRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Files\DecompressFilesRequest; @@ -263,6 +264,25 @@ class FileController extends ClientApiController return new JsonResponse([], Response::HTTP_NO_CONTENT); } + /** + * Updates file permissions for file(s) in the given root directory. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Files\ChmodFilesRequest $request + * @param \Pterodactyl\Models\Server $server + * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function chmod(ChmodFilesRequest $request, Server $server): JsonResponse + { + $this->fileRepository->setServer($server) + ->chmodFiles( + $request->input('root'), $request->input('files') + ); + + return new JsonResponse([], Response::HTTP_NO_CONTENT); + } + /** * Encodes a given file name & path in a format that should work for a good majority * of file names without too much confusing logic. diff --git a/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php new file mode 100644 index 000000000..ed6540219 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php @@ -0,0 +1,31 @@ + 'required|nullable|string', + 'files' => 'required|array', + 'files.file' => 'string', + 'files.mode' => 'integer', + ]; + } +} diff --git a/app/Repositories/Wings/DaemonFileRepository.php b/app/Repositories/Wings/DaemonFileRepository.php index 1ae424585..c36a8abb0 100644 --- a/app/Repositories/Wings/DaemonFileRepository.php +++ b/app/Repositories/Wings/DaemonFileRepository.php @@ -269,4 +269,32 @@ class DaemonFileRepository extends DaemonRepository throw new DaemonConnectionException($exception); } } + + /** + * Chmods the given files. + * + * @param string|null $root + * @param array $files + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function chmodFiles(?string $root, array $files): ResponseInterface + { + Assert::isInstanceOf($this->server, Server::class); + + try { + return $this->getHttpClient()->post( + sprintf('/api/servers/%s/files/chmod', $this->server->uuid), + [ + 'json' => [ + 'root' => $root ?? '/', + 'files' => $files, + ], + ] + ); + } catch (TransferException $exception) { + throw new DaemonConnectionException($exception); + } + } } diff --git a/routes/api-client.php b/routes/api-client.php index 170828be1..35a2938dd 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -64,6 +64,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::post('/decompress', 'Servers\FileController@decompress'); Route::post('/delete', 'Servers\FileController@delete'); Route::post('/create-folder', 'Servers\FileController@create'); + Route::post('/chmod', 'Servers\FileController@chmod'); Route::get('/upload', 'Servers\FileUploadController'); }); From ed5613e20770c92f9006549f1687e549d9921a73 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 29 Nov 2020 14:46:35 -0700 Subject: [PATCH 06/14] Show file mode on file listing, add ability to change file mode --- .../Daemon/FileObjectTransformer.php | 1 + .../scripts/api/server/files/chmodFiles.ts | 14 ++++ .../scripts/api/server/files/loadDirectory.ts | 1 + resources/scripts/api/transformers.ts | 1 + .../server/files/ChmodFileModal.tsx | 75 +++++++++++++++++++ .../server/files/FileDropdownMenu.tsx | 30 +++++--- .../components/server/files/FileObjectRow.tsx | 6 +- .../server/files/NewDirectoryButton.tsx | 3 +- resources/scripts/helpers.ts | 30 ++++++++ 9 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 resources/scripts/api/server/files/chmodFiles.ts create mode 100644 resources/scripts/components/server/files/ChmodFileModal.tsx diff --git a/app/Transformers/Daemon/FileObjectTransformer.php b/app/Transformers/Daemon/FileObjectTransformer.php index 84fcaf2d4..f19d9028c 100644 --- a/app/Transformers/Daemon/FileObjectTransformer.php +++ b/app/Transformers/Daemon/FileObjectTransformer.php @@ -25,6 +25,7 @@ class FileObjectTransformer extends BaseDaemonTransformer return [ 'name' => Arr::get($item, 'name'), 'mode' => Arr::get($item, 'mode'), + 'mode_bits' => Arr::get($item, 'mode_bits'), 'size' => Arr::get($item, 'size'), 'is_file' => Arr::get($item, 'file', true), 'is_symlink' => Arr::get($item, 'symlink', false), diff --git a/resources/scripts/api/server/files/chmodFiles.ts b/resources/scripts/api/server/files/chmodFiles.ts new file mode 100644 index 000000000..8bafd6d6e --- /dev/null +++ b/resources/scripts/api/server/files/chmodFiles.ts @@ -0,0 +1,14 @@ +import http from '@/api/http'; + +interface Data { + file: string; + mode: string; +} + +export default (uuid: string, directory: string, files: Data[]): Promise => { + return new Promise((resolve, reject) => { + http.post(`/api/client/servers/${uuid}/files/chmod`, { root: directory, files }) + .then(() => resolve()) + .catch(reject); + }); +}; diff --git a/resources/scripts/api/server/files/loadDirectory.ts b/resources/scripts/api/server/files/loadDirectory.ts index d29cc1605..52bf8853e 100644 --- a/resources/scripts/api/server/files/loadDirectory.ts +++ b/resources/scripts/api/server/files/loadDirectory.ts @@ -5,6 +5,7 @@ export interface FileObject { key: string; name: string; mode: string; + modeBits: string, size: number; isFile: boolean; isSymlink: boolean; diff --git a/resources/scripts/api/transformers.ts b/resources/scripts/api/transformers.ts index f17787e03..e08c00076 100644 --- a/resources/scripts/api/transformers.ts +++ b/resources/scripts/api/transformers.ts @@ -16,6 +16,7 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({ key: `${data.attributes.is_file ? 'file' : 'dir'}_${data.attributes.name}`, name: data.attributes.name, mode: data.attributes.mode, + modeBits: data.attributes.mode_bits, size: Number(data.attributes.size), isFile: data.attributes.is_file, isSymlink: data.attributes.is_symlink, diff --git a/resources/scripts/components/server/files/ChmodFileModal.tsx b/resources/scripts/components/server/files/ChmodFileModal.tsx new file mode 100644 index 000000000..a7997469b --- /dev/null +++ b/resources/scripts/components/server/files/ChmodFileModal.tsx @@ -0,0 +1,75 @@ +import { fileBitsToString } from '@/helpers'; +import useFileManagerSwr from '@/plugins/useFileManagerSwr'; +import React from 'react'; +import Modal, { RequiredModalProps } from '@/components/elements/Modal'; +import { Form, Formik, FormikHelpers } from 'formik'; +import Field from '@/components/elements/Field'; +import chmodFiles from '@/api/server/files/chmodFiles'; +import { ServerContext } from '@/state/server'; +import tw from 'twin.macro'; +import Button from '@/components/elements/Button'; +import useFlash from '@/plugins/useFlash'; + +interface FormikValues { + mode: string; +} + +interface File { + file: string, + mode: string, +} + +type OwnProps = RequiredModalProps & { files: File[] }; + +const ChmodFileModal = ({ files, ...props }: OwnProps) => { + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const { mutate } = useFileManagerSwr(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const directory = ServerContext.useStoreState(state => state.files.directory); + const setSelectedFiles = ServerContext.useStoreActions(actions => actions.files.setSelectedFiles); + + const submit = ({ mode }: FormikValues, { setSubmitting }: FormikHelpers) => { + clearFlashes('files'); + + mutate(data => data.map(f => f.name === files[0].file ? { ...f, mode: fileBitsToString(mode, !f.isFile), modeBits: mode } : f), false); + + const data = files.map(f => ({ file: f.file, mode: mode })); + + chmodFiles(uuid, directory, data) + .then((): Promise => files.length > 0 ? mutate() : Promise.resolve()) + .then(() => setSelectedFiles([])) + .catch(error => { + mutate(); + setSubmitting(false); + clearAndAddHttpError({ key: 'files', error }); + }) + .then(() => props.onDismissed()); + }; + + return ( + 1 ? '' : (files[0].mode || '') }}> + {({ isSubmitting }) => ( + +
+
+
+ +
+
+ +
+
+
+
+ )} +
+ ); +}; + +export default ChmodFileModal; diff --git a/resources/scripts/components/server/files/FileDropdownMenu.tsx b/resources/scripts/components/server/files/FileDropdownMenu.tsx index 48d53eeb2..603ed80ea 100644 --- a/resources/scripts/components/server/files/FileDropdownMenu.tsx +++ b/resources/scripts/components/server/files/FileDropdownMenu.tsx @@ -5,6 +5,7 @@ import { faCopy, faEllipsisH, faFileArchive, + faFileCode, faFileDownload, faLevelUpAlt, faPencilAlt, @@ -30,8 +31,9 @@ import compressFiles from '@/api/server/files/compressFiles'; import decompressFiles from '@/api/server/files/decompressFiles'; import isEqual from 'react-fast-compare'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; +import ChmodFileModal from '@/components/server/files/ChmodFileModal'; -type ModalType = 'rename' | 'move'; +type ModalType = 'rename' | 'move' | 'chmod'; const StyledRow = styled.div<{ $danger?: boolean }>` ${tw`p-2 flex items-center rounded`}; @@ -140,14 +142,23 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => { renderToggle={onClick => (
- {!!modal && - setModal(null)} - /> + {modal ? + modal === 'chmod' ? + setModal(null)} + /> + : + setModal(null)} + /> + : null }
@@ -156,6 +167,7 @@ const FileDropdownMenu = ({ file }: { file: FileObject }) => { setModal('rename')} icon={faPencilAlt} title={'Rename'}/> setModal('move')} icon={faLevelUpAlt} title={'Move'}/> + setModal('chmod')} icon={faFileCode} title={'Permissions'}/> {file.isFile && diff --git a/resources/scripts/components/server/files/FileObjectRow.tsx b/resources/scripts/components/server/files/FileObjectRow.tsx index fccef1ec6..89a080759 100644 --- a/resources/scripts/components/server/files/FileObjectRow.tsx +++ b/resources/scripts/components/server/files/FileObjectRow.tsx @@ -64,7 +64,11 @@ const FileObjectRow = ({ file }: { file: FileObject }) => ( > -
+ + +
{file.isFile ? : diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index dc91377bc..56b58e17e 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -25,7 +25,8 @@ const schema = object().shape({ const generateDirectoryData = (name: string): FileObject => ({ key: `dir_${name.split('/', 1)[0] ?? name}`, name: name.replace(/^(\/*)/, '').split('/', 1)[0] ?? name, - mode: '0644', + mode: 'drwxr-xr-x', + modeBits: '0755', size: 0, isFile: false, isSymlink: false, diff --git a/resources/scripts/helpers.ts b/resources/scripts/helpers.ts index 84358193b..fdeef7ecd 100644 --- a/resources/scripts/helpers.ts +++ b/resources/scripts/helpers.ts @@ -20,3 +20,33 @@ export const randomInt = (low: number, high: number) => Math.floor(Math.random() export const cleanDirectoryPath = (path: string) => path.replace(/(^#\/*)|(\/(\/*))|(^$)/g, '/'); export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); + +export function fileBitsToString (mode: string, directory: boolean): string { + const m = parseInt(mode, 8); + + let buf = ''; + 'dalTLDpSugct?'.split('').forEach((c, i) => { + if ((m & (1 << (32 - 1 - i))) !== 0) { + buf = buf + c; + } + }); + + if (buf.length === 0) { + // If the file is directory, make sure it has the directory flag. + if (directory) { + buf = 'd'; + } else { + buf = '-'; + } + } + + 'rwxrwxrwx'.split('').forEach((c, i) => { + if ((m & (1 << (9 - 1 - i))) !== 0) { + buf = buf + c; + } else { + buf = buf + '-'; + } + }); + + return buf; +} From bd0b7127d23426758fb3b6f8ee6860ba31d4a478 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 29 Nov 2020 14:47:18 -0700 Subject: [PATCH 07/14] Fix validation rules for ChmodFilesRequest.php --- .../Requests/Api/Client/Servers/Files/ChmodFilesRequest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php index ed6540219..03a90564b 100644 --- a/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php @@ -24,8 +24,7 @@ class ChmodFilesRequest extends ClientApiRequest implements ClientPermissionsReq return [ 'root' => 'required|nullable|string', 'files' => 'required|array', - 'files.file' => 'string', - 'files.mode' => 'integer', + 'files.*' => 'string', ]; } } From 3e1dbbaeddc800500c7e97054472a7b1e63051c2 Mon Sep 17 00:00:00 2001 From: Matthew Penner Date: Sun, 29 Nov 2020 16:02:20 -0700 Subject: [PATCH 08/14] Fix validation rules for ChmodFilesRequest.php, again.. --- .../Requests/Api/Client/Servers/Files/ChmodFilesRequest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php index 03a90564b..158a0a7fd 100644 --- a/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Files/ChmodFilesRequest.php @@ -24,7 +24,8 @@ class ChmodFilesRequest extends ClientApiRequest implements ClientPermissionsReq return [ 'root' => 'required|nullable|string', 'files' => 'required|array', - 'files.*' => 'string', + 'files.*.file' => 'required|string', + 'files.*.mode' => 'required|numeric', ]; } } From 7c2888641f888d0c10492364d198c2eaac8c33e6 Mon Sep 17 00:00:00 2001 From: Stepan Fedotov Date: Fri, 4 Dec 2020 19:56:44 +0200 Subject: [PATCH 09/14] Fix application API's ServerVariableTransformer --- .../Api/Application/ServerVariableTransformer.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/Transformers/Api/Application/ServerVariableTransformer.php b/app/Transformers/Api/Application/ServerVariableTransformer.php index 73cd169b2..3f7eeac49 100644 --- a/app/Transformers/Api/Application/ServerVariableTransformer.php +++ b/app/Transformers/Api/Application/ServerVariableTransformer.php @@ -2,7 +2,7 @@ namespace Pterodactyl\Transformers\Api\Application; -use Pterodactyl\Models\ServerVariable; +use Pterodactyl\Models\EggVariable; use Pterodactyl\Services\Acl\Api\AdminAcl; class ServerVariableTransformer extends BaseTransformer @@ -27,10 +27,10 @@ class ServerVariableTransformer extends BaseTransformer /** * Return a generic transformed server variable array. * - * @param \Pterodactyl\Models\ServerVariable $variable + * @param \Pterodactyl\Models\EggVariable $variable * @return array */ - public function transform(ServerVariable $variable) + public function transform(EggVariable $variable) { return $variable->toArray(); } @@ -38,11 +38,11 @@ class ServerVariableTransformer extends BaseTransformer /** * Return the parent service variable data. * - * @param \Pterodactyl\Models\ServerVariable $variable + * @param \Pterodactyl\Models\EggVariable $variable * @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource * @throws \Pterodactyl\Exceptions\Transformer\InvalidTransformerLevelException */ - public function includeParent(ServerVariable $variable) + public function includeParent(EggVariable $variable) { if (! $this->authorize(AdminAcl::RESOURCE_EGGS)) { return $this->null(); From e32c4d4f0518cb0344c372c872c1aa4f31c096a7 Mon Sep 17 00:00:00 2001 From: Stepan Fedotov Date: Fri, 4 Dec 2020 19:58:09 +0200 Subject: [PATCH 10/14] Documentate fix --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc6c0f9c..43ed825c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ This file is a running track of new features and fixes to each version of the pa This project follows [Semantic Versioning](http://semver.org) guidelines. +## Unreleased +### Fixed +* Fixes the application API unable to return server's variables. + ## v1.1.3 ### Fixed * Server bulk power actions command will no longer attempt to run commands against installing or suspended servers. From a8d9eccf9cd5b2766f11a712643290c49735820c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 6 Dec 2020 12:01:42 -0800 Subject: [PATCH 11/14] Support pagination of server backups, closes #2787 --- .../Api/Client/Servers/BackupController.php | 3 +- resources/scripts/api/swr/getServerBackups.ts | 11 +++- .../components/dashboard/ApiKeyModal.tsx | 2 +- .../server/backups/BackupContainer.tsx | 55 +++++++++++++------ .../server/backups/ChecksumModal.tsx | 2 +- .../server/databases/DatabaseRow.tsx | 2 +- 6 files changed, 53 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/BackupController.php b/app/Http/Controllers/Api/Client/Servers/BackupController.php index cc8ba0917..23daf1bc6 100644 --- a/app/Http/Controllers/Api/Client/Servers/BackupController.php +++ b/app/Http/Controllers/Api/Client/Servers/BackupController.php @@ -60,7 +60,8 @@ class BackupController extends ClientApiController */ public function index(GetBackupsRequest $request, Server $server) { - return $this->fractal->collection($server->backups()->paginate(20)) + $limit = min($request->query('per_page') ?? 20, 50); + return $this->fractal->collection($server->backups()->paginate($limit)) ->transformWith($this->getTransformer(BackupTransformer::class)) ->toArray(); } diff --git a/resources/scripts/api/swr/getServerBackups.ts b/resources/scripts/api/swr/getServerBackups.ts index 0c38cd278..85a8c1886 100644 --- a/resources/scripts/api/swr/getServerBackups.ts +++ b/resources/scripts/api/swr/getServerBackups.ts @@ -3,8 +3,17 @@ import http, { getPaginationSet, PaginatedResult } from '@/api/http'; import { ServerBackup } from '@/api/server/types'; import { rawDataToServerBackup } from '@/api/transformers'; import { ServerContext } from '@/state/server'; +import { createContext, useContext } from 'react'; -export default (page?: number | string) => { +interface ctx { + page: number; + setPage: (value: number | ((s: number) => number)) => void; +} + +export const Context = createContext({ page: 1, setPage: () => 1 }); + +export default () => { + const { page } = useContext(Context); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); return useSWR>([ 'server:backups', uuid, page ], async () => { diff --git a/resources/scripts/components/dashboard/ApiKeyModal.tsx b/resources/scripts/components/dashboard/ApiKeyModal.tsx index e73274309..ba6568b84 100644 --- a/resources/scripts/components/dashboard/ApiKeyModal.tsx +++ b/resources/scripts/components/dashboard/ApiKeyModal.tsx @@ -13,7 +13,7 @@ const ApiKeyModal = ({ apiKey }: Props) => { return ( <> -

Your API Key

+

Your API Key

The API key you have requested is shown below. Please store this in a safe location, it will not be shown again. diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index 493c059a5..99f1e6326 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import Spinner from '@/components/elements/Spinner'; import useFlash from '@/plugins/useFlash'; import Can from '@/components/elements/Can'; @@ -6,11 +6,13 @@ import CreateBackupButton from '@/components/server/backups/CreateBackupButton'; import FlashMessageRender from '@/components/FlashMessageRender'; import BackupRow from '@/components/server/backups/BackupRow'; import tw from 'twin.macro'; -import getServerBackups from '@/api/swr/getServerBackups'; +import getServerBackups, { Context as ServerBackupContext } from '@/api/swr/getServerBackups'; import { ServerContext } from '@/state/server'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import Pagination from '@/components/elements/Pagination'; -export default () => { +const BackupContainer = () => { + const { page, setPage } = useContext(ServerBackupContext); const { clearFlashes, clearAndAddHttpError } = useFlash(); const { data: backups, error, isValidating } = getServerBackups(); @@ -33,19 +35,29 @@ export default () => { return ( - {!backups.items.length ? -

- There are no backups stored for this server. -

- : -
- {backups.items.map((backup, index) => 0 ? tw`mt-2` : undefined} - />)} -
- } + + {({ items }) => ( + !items.length ? + // Don't show any error messages if the server has no backups and the user cannot + // create additional ones for the server. + !backupLimit ? + null + : +

+ {page > 1 ? + 'Looks like we\'ve run out of backups to show you, try going back a page.' + : + 'It looks like there are no backups currently stored for this server.' + } +

+ : + items.map((backup, index) => 0 ? tw`mt-2` : undefined} + />) + )} +
{backupLimit === 0 &&

Backups cannot be created for this server. @@ -59,10 +71,19 @@ export default () => {

} {backupLimit > 0 && backupLimit !== backups.items.length && - + }
); }; + +export default () => { + const [ page, setPage ] = useState(1); + return ( + + + + ); +}; diff --git a/resources/scripts/components/server/backups/ChecksumModal.tsx b/resources/scripts/components/server/backups/ChecksumModal.tsx index 57e6ba8b5..a0a318d67 100644 --- a/resources/scripts/components/server/backups/ChecksumModal.tsx +++ b/resources/scripts/components/server/backups/ChecksumModal.tsx @@ -4,7 +4,7 @@ import tw from 'twin.macro'; const ChecksumModal = ({ checksum, ...props }: RequiredModalProps & { checksum: string }) => ( -

Verify file checksum

+

Verify file checksum

The checksum of this file is:

diff --git a/resources/scripts/components/server/databases/DatabaseRow.tsx b/resources/scripts/components/server/databases/DatabaseRow.tsx index 952a7b746..caf4a702e 100644 --- a/resources/scripts/components/server/databases/DatabaseRow.tsx +++ b/resources/scripts/components/server/databases/DatabaseRow.tsx @@ -111,7 +111,7 @@ export default ({ database, className }: Props) => { setConnectionVisible(false)}> -

Database connection details

+

Database connection details

From 11054de5b32137459113b727e0e0e191351ce906 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 6 Dec 2020 12:16:12 -0800 Subject: [PATCH 12/14] Attempt revocation of JWT access when changing a server's owner closes #2771 --- .../Api/Client/Servers/SubuserController.php | 4 +- .../Wings/DaemonServerRepository.php | 18 +++++- .../Servers/DetailsModificationService.php | 56 +++++++++++-------- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/Api/Client/Servers/SubuserController.php b/app/Http/Controllers/Api/Client/Servers/SubuserController.php index d2806a653..81238d33b 100644 --- a/app/Http/Controllers/Api/Client/Servers/SubuserController.php +++ b/app/Http/Controllers/Api/Client/Servers/SubuserController.php @@ -135,7 +135,7 @@ class SubuserController extends ClientApiController ]); try { - $this->serverRepository->setServer($server)->revokeJTIs([md5($subuser->user_id . $server->uuid)]); + $this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id); } catch (DaemonConnectionException $exception) { // Don't block this request if we can't connect to the Wings instance. Chances are it is // offline in this event and the token will be invalid anyways once Wings boots back. @@ -163,7 +163,7 @@ class SubuserController extends ClientApiController $this->repository->delete($subuser->id); try { - $this->serverRepository->setServer($server)->revokeJTIs([md5($subuser->user_id . $server->uuid)]); + $this->serverRepository->setServer($server)->revokeUserJTI($subuser->user_id); } catch (DaemonConnectionException $exception) { // Don't block this request if we can't connect to the Wings instance. Log::warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]); diff --git a/app/Repositories/Wings/DaemonServerRepository.php b/app/Repositories/Wings/DaemonServerRepository.php index 22c90d6c5..f8af0c034 100644 --- a/app/Repositories/Wings/DaemonServerRepository.php +++ b/app/Repositories/Wings/DaemonServerRepository.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Repositories\Wings; use Webmozart\Assert\Assert; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; @@ -144,6 +145,21 @@ class DaemonServerRepository extends DaemonRepository } } + /** + * Revokes a single user's JTI by using their ID. This is simply a helper function to + * make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted + * correctly and avoids any costly mistakes in the codebase. + * + * @param int $id + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + */ + public function revokeUserJTI(int $id): void + { + Assert::isInstanceOf($this->server, Server::class); + + $this->revokeJTIs([ md5($id . $this->server->uuid) ]); + } + /** * Revokes an array of JWT JTI's by marking any token generated before the current time on * the Wings instance as being invalid. @@ -151,7 +167,7 @@ class DaemonServerRepository extends DaemonRepository * @param array $jtis * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function revokeJTIs(array $jtis): void + protected function revokeJTIs(array $jtis): void { Assert::isInstanceOf($this->server, Server::class); diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 65a5b2815..aa480a06a 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -2,10 +2,12 @@ namespace Pterodactyl\Services\Servers; +use Illuminate\Support\Arr; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Traits\Services\ReturnsUpdatedModels; -use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Wings\DaemonServerRepository; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; class DetailsModificationService { @@ -17,22 +19,20 @@ class DetailsModificationService private $connection; /** - * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ - private $repository; + private $serverRepository; /** * DetailsModificationService constructor. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $serverRepository */ - public function __construct( - ConnectionInterface $connection, - ServerRepository $repository - ) { + public function __construct(ConnectionInterface $connection, DaemonServerRepository $serverRepository) + { $this->connection = $connection; - $this->repository = $repository; + $this->serverRepository = $serverRepository; } /** @@ -40,24 +40,36 @@ class DetailsModificationService * * @param \Pterodactyl\Models\Server $server * @param array $data - * @return bool|\Pterodactyl\Models\Server + * @return \Pterodactyl\Models\Server * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ - public function handle(Server $server, array $data) + public function handle(Server $server, array $data): Server { - $this->connection->beginTransaction(); + return $this->connection->transaction(function () use ($data, $server) { + $owner = $server->owner_id; - $response = $this->repository->setFreshModel($this->getUpdatedModel())->update($server->id, [ - 'external_id' => array_get($data, 'external_id'), - 'owner_id' => array_get($data, 'owner_id'), - 'name' => array_get($data, 'name'), - 'description' => array_get($data, 'description') ?? '', - ], true, true); + $server->forceFill([ + 'external_id' => Arr::get($data, 'external_id'), + 'owner_id' => Arr::get($data, 'owner_id'), + 'name' => Arr::get($data, 'name'), + 'description' => Arr::get($data, 'description') ?? '', + ])->saveOrFail(); - $this->connection->commit(); + // If the owner_id value is changed we need to revoke any tokens that exist for the server + // on the Wings instance so that the old owner no longer has any permission to access the + // websockets. + if ($server->owner_id !== $owner) { + try { + $this->serverRepository->setServer($server)->revokeUserJTI($owner); + } catch (DaemonConnectionException $exception) { + // Do nothing. A failure here is not ideal, but it is likely to be caused by Wings + // being offline, or in an entirely broken state. Remeber, these tokens reset every + // few minutes by default, we're just trying to help it along a little quicker. + } + } - return $response; + return $server; + }); } } From 79673ca44075b44e3ea22c0b64373f66510830b2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 6 Dec 2020 12:23:58 -0800 Subject: [PATCH 13/14] Don't ever block storing node updates if wings returns an error; closes #2712 --- app/Services/Nodes/NodeUpdateService.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index ce9238d7c..f684d1064 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Services\Nodes; use Illuminate\Support\Str; use Pterodactyl\Models\Node; +use Illuminate\Support\Facades\Log; use GuzzleHttp\Exception\ConnectException; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; @@ -90,11 +91,17 @@ class NodeUpdateService $this->configurationRepository->setNode($node)->update($updated); } catch (DaemonConnectionException $exception) { - if (! is_null($exception->getPrevious()) && $exception->getPrevious() instanceof ConnectException) { - return [$updated, true]; - } + Log::warning($exception, ['node_id' => $node->id]); - throw $exception; + // Never actually throw these exceptions up the stack. If we were able to change the settings + // but something went wrong with Wings we just want to store the update and let the user manually + // make changes as needed. + // + // This avoids issues with proxies such as CloudFlare which will see Wings as offline and then + // inject their own response pages, causing this logic to get fucked up. + // + // @see https://github.com/pterodactyl/panel/issues/2712 + return [ $updated, true ]; } return [$updated, false]; From 5d23d894ae49c1f1e96899c1abd82ba9d94d82a8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 6 Dec 2020 12:25:36 -0800 Subject: [PATCH 14/14] Update NodeUpdateServiceTest.php --- tests/Unit/Services/Nodes/NodeUpdateServiceTest.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index c8138185d..1a836e446 100644 --- a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -210,11 +210,8 @@ class NodeUpdateServiceTest extends TestCase try { $closure(); } catch (Exception $exception) { - $this->assertInstanceOf(DaemonConnectionException::class, $exception); - $this->assertSame( - 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/E_CONN_REFUSED response code. This exception has been logged.', - $exception->getMessage() - ); + $this->assertInstanceOf(Exception::class, $exception); + $this->assertSame('Foo', $exception->getMessage()); return true; } @@ -224,9 +221,7 @@ class NodeUpdateServiceTest extends TestCase $this->repository->expects('withFreshModel->update')->andReturns($updatedModel); $this->configurationRepository->expects('setNode->update')->andThrow( - new DaemonConnectionException( - new TransferException('', 500, new Exception) - ) + new Exception('Foo') ); $this->getService()->handle($model, ['name' => $updatedModel->name]);