Skip to content

Commit 802f88f

Browse files
authored
Merge branch 'develop' into permissions
2 parents b2d2a93 + ce42543 commit 802f88f

File tree

21 files changed

+181
-57
lines changed

21 files changed

+181
-57
lines changed

app/Http/Controllers/Api/Client/Servers/FileController.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Http\Response;
77
use Pterodactyl\Models\Server;
88
use Illuminate\Http\JsonResponse;
9+
use Illuminate\Support\Collection;
910
use Pterodactyl\Services\Nodes\NodeJWTService;
1011
use Illuminate\Contracts\Routing\ResponseFactory;
1112
use Pterodactyl\Repositories\Wings\DaemonFileRepository;
@@ -70,7 +71,7 @@ public function directory(ListFilesRequest $request, Server $server): array
7071
{
7172
$contents = $this->fileRepository
7273
->setServer($server)
73-
->getDirectory(urlencode(urldecode($request->get('directory') ?? '/')));
74+
->getDirectory($this->encode($request->get('directory') ?? '/'));
7475

7576
return $this->fractal->collection($contents)
7677
->transformWith($this->getTransformer(FileObjectTransformer::class))
@@ -91,7 +92,7 @@ public function contents(GetFileContentsRequest $request, Server $server): Respo
9192
{
9293
return new Response(
9394
$this->fileRepository->setServer($server)->getContent(
94-
urlencode(urldecode($request->get('file'))), config('pterodactyl.files.max_edit_size')
95+
$this->encode($request->get('file')), config('pterodactyl.files.max_edit_size')
9596
),
9697
Response::HTTP_OK,
9798
['Content-Type' => 'text/plain']
@@ -113,7 +114,7 @@ public function download(GetFileContentsRequest $request, Server $server)
113114
$token = $this->jwtService
114115
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
115116
->setClaims([
116-
'file_path' => $request->get('file'),
117+
'file_path' => rawurldecode($request->get('file')),
117118
'server_uuid' => $server->uuid,
118119
])
119120
->handle($server->node, $request->user()->id . $server->uuid);
@@ -142,7 +143,7 @@ public function download(GetFileContentsRequest $request, Server $server)
142143
public function write(WriteFileContentRequest $request, Server $server): JsonResponse
143144
{
144145
$this->fileRepository->setServer($server)->putContent(
145-
$request->get('file'),
146+
$this->encode($request->get('file')),
146147
$request->getContent()
147148
);
148149

@@ -261,4 +262,18 @@ public function delete(DeleteFileRequest $request, Server $server): JsonResponse
261262

262263
return new JsonResponse([], Response::HTTP_NO_CONTENT);
263264
}
265+
266+
/**
267+
* Encodes a given file name & path in a format that should work for a good majority
268+
* of file names without too much confusing logic.
269+
*
270+
* @param string $path
271+
* @return string
272+
*/
273+
private function encode(string $path): string
274+
{
275+
return Collection::make(explode('/', rawurldecode($path)))->map(function ($value) {
276+
return rawurlencode($value);
277+
})->join('/');
278+
}
264279
}

app/Http/Controllers/Api/Client/Servers/SubuserController.php

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
44

55
use Illuminate\Http\Request;
6-
use Pterodactyl\Models\User;
76
use Pterodactyl\Models\Server;
8-
use Pterodactyl\Models\Subuser;
97
use Illuminate\Http\JsonResponse;
108
use Pterodactyl\Models\Permission;
9+
use Illuminate\Support\Facades\Log;
1110
use Pterodactyl\Repositories\Eloquent\SubuserRepository;
1211
use Pterodactyl\Services\Subusers\SubuserCreationService;
12+
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
1313
use Pterodactyl\Transformers\Api\Client\SubuserTransformer;
1414
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
15+
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
1516
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\GetSubuserRequest;
1617
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\StoreSubuserRequest;
1718
use Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest;
@@ -29,20 +30,28 @@ class SubuserController extends ClientApiController
2930
*/
3031
private $creationService;
3132

33+
/**
34+
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
35+
*/
36+
private $serverRepository;
37+
3238
/**
3339
* SubuserController constructor.
3440
*
3541
* @param \Pterodactyl\Repositories\Eloquent\SubuserRepository $repository
3642
* @param \Pterodactyl\Services\Subusers\SubuserCreationService $creationService
43+
* @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $serverRepository
3744
*/
3845
public function __construct(
3946
SubuserRepository $repository,
40-
SubuserCreationService $creationService
47+
SubuserCreationService $creationService,
48+
DaemonServerRepository $serverRepository
4149
) {
4250
parent::__construct();
4351

4452
$this->repository = $repository;
4553
$this->creationService = $creationService;
54+
$this->serverRepository = $serverRepository;
4655
}
4756

4857
/**
@@ -101,19 +110,38 @@ public function store(StoreSubuserRequest $request, Server $server)
101110
* Update a given subuser in the system for the server.
102111
*
103112
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\UpdateSubuserRequest $request
113+
* @param \Pterodactyl\Models\Server $server
104114
* @return array
105115
*
106116
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
107117
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
108118
*/
109-
public function update(UpdateSubuserRequest $request): array
119+
public function update(UpdateSubuserRequest $request, Server $server): array
110120
{
111121
/** @var \Pterodactyl\Models\Subuser $subuser */
112122
$subuser = $request->attributes->get('subuser');
113123

114-
$this->repository->update($subuser->id, [
115-
'permissions' => $this->getDefaultPermissions($request),
116-
]);
124+
$permissions = $this->getDefaultPermissions($request);
125+
$current = $subuser->permissions;
126+
127+
sort($permissions);
128+
sort($current);
129+
130+
// Only update the database and hit up the Wings instance to invalidate JTI's if the permissions
131+
// have actually changed for the user.
132+
if ($permissions !== $current) {
133+
$this->repository->update($subuser->id, [
134+
'permissions' => $this->getDefaultPermissions($request),
135+
]);
136+
137+
try {
138+
$this->serverRepository->setServer($server)->revokeJTIs([md5($subuser->user_id . $server->uuid)]);
139+
} catch (DaemonConnectionException $exception) {
140+
// Don't block this request if we can't connect to the Wings instance. Chances are it is
141+
// offline in this event and the token will be invalid anyways once Wings boots back.
142+
Log::warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
143+
}
144+
}
117145

118146
return $this->fractal->item($subuser->refresh())
119147
->transformWith($this->getTransformer(SubuserTransformer::class))
@@ -124,15 +152,23 @@ public function update(UpdateSubuserRequest $request): array
124152
* Removes a subusers from a server's assignment.
125153
*
126154
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Subusers\DeleteSubuserRequest $request
155+
* @param \Pterodactyl\Models\Server $server
127156
* @return \Illuminate\Http\JsonResponse
128157
*/
129-
public function delete(DeleteSubuserRequest $request)
158+
public function delete(DeleteSubuserRequest $request, Server $server)
130159
{
131160
/** @var \Pterodactyl\Models\Subuser $subuser */
132161
$subuser = $request->attributes->get('subuser');
133162

134163
$this->repository->delete($subuser->id);
135164

165+
try {
166+
$this->serverRepository->setServer($server)->revokeJTIs([md5($subuser->user_id . $server->uuid)]);
167+
} catch (DaemonConnectionException $exception) {
168+
// Don't block this request if we can't connect to the Wings instance.
169+
Log::warning($exception, ['user_id' => $subuser->user_id, 'server_id' => $server->id]);
170+
}
171+
136172
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
137173
}
138174

app/Http/Controllers/Api/Client/Servers/WebsocketController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function __invoke(ClientApiRequest $request, Server $server)
5959
}
6060

6161
$token = $this->jwtService
62-
->setExpiresAt(CarbonImmutable::now()->addMinutes(15))
62+
->setExpiresAt(CarbonImmutable::now()->addMinutes(10))
6363
->setClaims([
6464
'user_id' => $request->user()->id,
6565
'server_uuid' => $server->uuid,

app/Models/Permission.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ class Permission extends Model
163163
'allocation' => [
164164
'description' => 'Permissions that control a user\'s ability to modify the port allocations for this server.',
165165
'keys' => [
166-
'read' => 'Allows a user to view the allocations assigned to this server.',
166+
'read' => 'Allows a user to view all allocations currently assigned to this server. Users with any level of access to this server can always view the primary allocation.',
167167
'create' => 'Allows a user to assign additional allocations to the server.',
168168
'update' => 'Allows a user to change the primary server allocation and attach notes to each allocation.',
169169
'delete' => 'Allows a user to delete an allocation from the server.',

app/Repositories/Wings/DaemonServerRepository.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,10 @@ public function suspend(bool $unsuspend = false): void
126126
}
127127

128128
/**
129-
* Requests the daemon to create a full archive of the server.
130-
* Once the daemon is finished they will send a POST request to
131-
* "/api/remote/servers/{uuid}/archive" with a boolean.
129+
* Requests the daemon to create a full archive of the server. Once the daemon is finished
130+
* they will send a POST request to "/api/remote/servers/{uuid}/archive" with a boolean.
132131
*
133-
* @throws DaemonConnectionException
132+
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
134133
*/
135134
public function requestArchive(): void
136135
{
@@ -144,4 +143,25 @@ public function requestArchive(): void
144143
throw new DaemonConnectionException($exception);
145144
}
146145
}
146+
147+
/**
148+
* Revokes an array of JWT JTI's by marking any token generated before the current time on
149+
* the Wings instance as being invalid.
150+
*
151+
* @param array $jtis
152+
* @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException
153+
*/
154+
public function revokeJTIs(array $jtis): void
155+
{
156+
Assert::isInstanceOf($this->server, Server::class);
157+
158+
try {
159+
$this->getHttpClient()
160+
->post(sprintf('/api/servers/%s/ws/deny', $this->server->uuid), [
161+
'json' => ['jtis' => $jtis],
162+
]);
163+
} catch (TransferException $exception) {
164+
throw new DaemonConnectionException($exception);
165+
}
166+
}
147167
}

app/Services/Nodes/NodeJWTService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public function handle(Node $node, string $identifiedBy)
5555

5656
$builder = (new Builder)->issuedBy(config('app.url'))
5757
->permittedFor($node->getConnectionAddress())
58-
->identifiedBy(hash('sha256', $identifiedBy), true)
58+
->identifiedBy(md5($identifiedBy), true)
5959
->issuedAt(CarbonImmutable::now()->getTimestamp())
6060
->canOnlyBeUsedAfter(CarbonImmutable::now()->subMinutes(5)->getTimestamp());
6161

app/Transformers/Api/Client/ServerTransformer.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,23 @@ public function transform(Server $server): array
8383
*/
8484
public function includeAllocations(Server $server)
8585
{
86+
$transformer = $this->makeTransformer(AllocationTransformer::class);
87+
88+
// While we include this permission, we do need to actually handle it slightly different here
89+
// for the purpose of keeping things functionally working. If the user doesn't have read permissions
90+
// for the allocations we'll only return the primary server allocation, and any notes associated
91+
// with it will be hidden.
92+
//
93+
// This allows us to avoid too much permission regression, without also hiding information that
94+
// is generally needed for the frontend to make sense when browsing or searching results.
8695
if (! $this->getUser()->can(Permission::ACTION_ALLOCATION_READ, $server)) {
87-
return $this->null();
96+
$primary = clone $server->allocation;
97+
$primary->notes = null;
98+
99+
return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME);
88100
}
89101

90-
return $this->collection(
91-
$server->allocations,
92-
$this->makeTransformer(AllocationTransformer::class),
93-
Allocation::RESOURCE_NAME
94-
);
102+
return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME);
95103
}
96104

97105
/**

docker/entrypoint.sh

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ else
2020
touch /app/var/.env
2121

2222
## manually generate a key because key generate --force fails
23-
echo -e "Generating key."
24-
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
25-
echo -e "Generated app key: $APP_KEY"
26-
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
23+
if [ -z $APP_KEY ]; then
24+
echo -e "Generating key."
25+
APP_KEY=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
26+
echo -e "Generated app key: $APP_KEY"
27+
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
28+
else
29+
echo -e "APP_KEY exists in environment, using that."
30+
echo -e "APP_KEY=$APP_KEY" > /app/var/.env
31+
fi
2732

2833
ln -s /app/var/.env /app/
2934
fi
@@ -77,4 +82,4 @@ yarn add cross-env
7782
yarn run build:production
7883

7984
echo -e "Starting supervisord."
80-
exec "$@"
85+
exec "$@"

resources/scripts/api/server/files/getFileContents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import http from '@/api/http';
33
export default (server: string, file: string): Promise<string> => {
44
return new Promise((resolve, reject) => {
55
http.get(`/api/client/servers/${server}/files/contents`, {
6-
params: { file: file.split('/').map(item => encodeURIComponent(item)).join('/') },
6+
params: { file: encodeURI(decodeURI(file)) },
77
transformResponse: res => res,
88
responseType: 'text',
99
})

resources/scripts/api/server/files/loadDirectory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface FileObject {
1717

1818
export default async (uuid: string, directory?: string): Promise<FileObject[]> => {
1919
const { data } = await http.get(`/api/client/servers/${uuid}/files/list`, {
20-
params: { directory: directory?.split('/').map(item => encodeURIComponent(item)).join('/') },
20+
params: { directory: encodeURI(directory ?? '/') },
2121
});
2222

2323
return (data.data || []).map(rawDataToFileObject);

0 commit comments

Comments
 (0)