Skip to content

Commit 12927a3

Browse files
committed
Update SFTP authentication endpoint to support returning user public keys
1 parent cca0010 commit 12927a3

File tree

2 files changed

+84
-48
lines changed

2 files changed

+84
-48
lines changed

app/Http/Controllers/Api/Remote/SftpAuthenticationController.php

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,108 +2,140 @@
22

33
namespace Pterodactyl\Http\Controllers\Api\Remote;
44

5+
use Pterodactyl\Models\User;
56
use Illuminate\Http\Request;
7+
use Pterodactyl\Models\Server;
68
use Illuminate\Http\JsonResponse;
79
use Pterodactyl\Models\Permission;
810
use Pterodactyl\Http\Controllers\Controller;
911
use Illuminate\Foundation\Auth\ThrottlesLogins;
10-
use Pterodactyl\Repositories\Eloquent\UserRepository;
1112
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
12-
use Pterodactyl\Repositories\Eloquent\ServerRepository;
1313
use Pterodactyl\Services\Servers\GetUserPermissionsService;
1414
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
1515
use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest;
1616
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
1717

18-
class SftpAuthenticationController extends Controller
18+
abstract class SftpAuthenticationController extends Controller
1919
{
2020
use ThrottlesLogins;
2121

22-
/**
23-
* @var \Pterodactyl\Repositories\Eloquent\UserRepository
24-
*/
25-
private $userRepository;
22+
protected GetUserPermissionsService $permissions;
23+
24+
public function __construct(GetUserPermissionsService $permissions)
25+
{
26+
$this->permissions = $permissions;
27+
}
2628

2729
/**
28-
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
30+
* Authenticate a set of credentials and return the associated server details
31+
* for a SFTP connection on the daemon.
2932
*/
30-
private $serverRepository;
33+
public function __invoke(SftpAuthenticationFormRequest $request): JsonResponse
34+
{
35+
$connection = $this->parseUsername($request->input('username'));
36+
37+
$this->validateRequestState($request);
38+
39+
$user = $this->getUser($request, $connection['username']);
40+
$server = $this->getServer($request, $connection['server']);
41+
42+
if ($request->input('type') !== 'public_key') {
43+
if (!password_verify($request->input('password'), $user->password)) {
44+
$this->reject($request);
45+
}
46+
}
47+
48+
$this->validateSftpAccess($user, $server);
49+
50+
return new JsonResponse([
51+
'server' => $server->uuid,
52+
'public_keys' => $user->sshKeys->map(fn ($value) => $value->public_key)->toArray(),
53+
'permissions' => $permissions ?? ['*'],
54+
]);
55+
}
3156

3257
/**
33-
* @var \Pterodactyl\Services\Servers\GetUserPermissionsService
58+
* Finds the server being requested and ensures that it belongs to the node this
59+
* request stems from.
3460
*/
35-
private $permissionsService;
61+
protected function getServer(Request $request, string $uuid): Server
62+
{
63+
return Server::query()
64+
->where(fn ($builder) => $builder->where('uuid', $uuid)->orWhere('uuidShort', $uuid))
65+
->where('node_id', $request->attributes->get('node')->id)
66+
->firstOr(function () use ($request) {
67+
$this->reject($request);
68+
});
69+
}
3670

3771
/**
38-
* SftpController constructor.
72+
* Finds a user with the given username or increments the login attempts.
3973
*/
40-
public function __construct(
41-
GetUserPermissionsService $permissionsService,
42-
UserRepository $userRepository,
43-
ServerRepository $serverRepository
44-
) {
45-
$this->userRepository = $userRepository;
46-
$this->serverRepository = $serverRepository;
47-
$this->permissionsService = $permissionsService;
74+
protected function getUser(Request $request, string $username): User
75+
{
76+
return User::query()->where('username', $username)->firstOr(function () use ($request) {
77+
$this->reject($request);
78+
});
4879
}
4980

5081
/**
51-
* Authenticate a set of credentials and return the associated server details
52-
* for a SFTP connection on the daemon.
82+
* Parses the username provided to the request.
5383
*
54-
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
84+
* @return array{"username": string, "server": string}
5585
*/
56-
public function __invoke(SftpAuthenticationFormRequest $request): JsonResponse
86+
protected function parseUsername(string $value): array
5787
{
5888
// Reverse the string to avoid issues with usernames that contain periods.
59-
$parts = explode('.', strrev($request->input('username')), 2);
89+
$parts = explode('.', strrev($value), 2);
6090

6191
// Unreverse the strings after parsing them apart.
62-
$connection = [
92+
return [
6393
'username' => strrev(array_get($parts, 1)),
6494
'server' => strrev(array_get($parts, 0)),
6595
];
96+
}
6697

98+
/**
99+
* Checks that the request should not be throttled yet, and that the server was
100+
* provided in the username.
101+
*/
102+
protected function validateRequestState(Request $request): void
103+
{
67104
if ($this->hasTooManyLoginAttempts($request)) {
68105
$seconds = $this->limiter()->availableIn($this->throttleKey($request));
69106

70107
throw new TooManyRequestsHttpException($seconds, "Too many login attempts for this account, please try again in {$seconds} seconds.");
71108
}
72109

73-
/** @var \Pterodactyl\Models\Node $node */
74-
$node = $request->attributes->get('node');
75110
if (empty($connection['server'])) {
76111
throw new NotFoundHttpException();
77112
}
113+
}
78114

79-
/** @var \Pterodactyl\Models\User $user */
80-
$user = $this->userRepository->findFirstWhere([
81-
['username', '=', $connection['username']],
82-
]);
83-
84-
$server = $this->serverRepository->getByUuid($connection['server'] ?? '');
85-
if (!password_verify($request->input('password'), $user->password) || $server->node_id !== $node->id) {
86-
$this->incrementLoginAttempts($request);
115+
/**
116+
* Rejects the request and increments the login attempts.
117+
*/
118+
protected function reject(Request $request): void
119+
{
120+
$this->incrementLoginAttempts($request);
87121

88-
throw new HttpForbiddenException('Authorization credentials were not correct, please try again.');
89-
}
122+
throw new HttpForbiddenException('Authorization credentials were not correct, please try again.');
123+
}
90124

125+
/**
126+
* Validates that a user should have permission to use SFTP for the given server.
127+
*/
128+
protected function validateSftpAccess(User $user, Server $server): void
129+
{
91130
if (!$user->root_admin && $server->owner_id !== $user->id) {
92-
$permissions = $this->permissionsService->handle($server, $user);
131+
$permissions = $this->permissions->handle($server, $user);
93132

94133
if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {
95134
throw new HttpForbiddenException('You do not have permission to access SFTP for this server.');
96135
}
97136
}
98137

99138
$server->validateCurrentState();
100-
101-
return new JsonResponse([
102-
'server' => $server->uuid,
103-
// Deprecated, but still needed at the moment for Wings.
104-
'token' => '',
105-
'permissions' => $permissions ?? ['*'],
106-
]);
107139
}
108140

109141
/**

app/Http/Requests/Api/Remote/SftpAuthenticationFormRequest.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Pterodactyl\Http\Requests\Api\Remote;
44

5+
use Illuminate\Validation\Rule;
56
use Illuminate\Foundation\Http\FormRequest;
67

78
class SftpAuthenticationFormRequest extends FormRequest
@@ -24,8 +25,11 @@ public function authorize()
2425
public function rules()
2526
{
2627
return [
27-
'username' => 'required|string',
28-
'password' => 'required|string',
28+
'type' => ['nullable', 'in:password,public_key'],
29+
'username' => ['required', 'string'],
30+
'password' => [
31+
Rule::when(fn () => $this->input('type') !== 'public_key', ['required', 'string'], ['nullable']),
32+
],
2933
];
3034
}
3135

0 commit comments

Comments
 (0)