Path: blob/1.0-develop/app/Http/Controllers/Api/Remote/SftpAuthenticationController.php
10280 views
<?php12namespace Pterodactyl\Http\Controllers\Api\Remote;34use Illuminate\Http\Request;5use Pterodactyl\Models\User;6use Pterodactyl\Models\Server;7use Illuminate\Http\JsonResponse;8use Pterodactyl\Facades\Activity;9use Pterodactyl\Models\Permission;10use phpseclib3\Crypt\PublicKeyLoader;11use Pterodactyl\Http\Controllers\Controller;12use phpseclib3\Exception\NoKeyLoadedException;13use Illuminate\Foundation\Auth\ThrottlesLogins;14use Pterodactyl\Exceptions\Http\HttpForbiddenException;15use Pterodactyl\Services\Servers\GetUserPermissionsService;16use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;17use Pterodactyl\Http\Requests\Api\Remote\SftpAuthenticationFormRequest;18use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;1920class SftpAuthenticationController extends Controller21{22use ThrottlesLogins;2324public function __construct(protected GetUserPermissionsService $permissions)25{26}2728/**29* Authenticate a set of credentials and return the associated server details30* for a SFTP connection on the daemon. This supports both public key and password31* based credentials.32*/33public function __invoke(SftpAuthenticationFormRequest $request): JsonResponse34{35$connection = $this->parseUsername($request->input('username'));36if (empty($connection['server'])) {37throw new BadRequestHttpException('No valid server identifier was included in the request.');38}3940if ($this->hasTooManyLoginAttempts($request)) {41$seconds = $this->limiter()->availableIn($this->throttleKey($request));4243throw new TooManyRequestsHttpException($seconds, "Too many login attempts for this account, please try again in $seconds seconds.");44}4546$user = $this->getUser($request, $connection['username']);47$server = $this->getServer($request, $connection['server']);4849if ($request->input('type') !== 'public_key') {50if (!password_verify($request->input('password'), $user->password)) {51Activity::event('auth:sftp.fail')->property('method', 'password')->subject($user)->log();5253$this->reject($request);54}55} else {56$key = null;57try {58$key = PublicKeyLoader::loadPublicKey(trim($request->input('password')));59} catch (NoKeyLoadedException) {60// do nothing61}6263if (!$key || !$user->sshKeys()->where('fingerprint', $key->getFingerprint('sha256'))->exists()) {64// We don't log here because of the way the SFTP system works. This endpoint65// will get hit for every key the user provides, which could be 4 or 5. That is66// a lot of unnecessary log noise.67//68// For now, we'll only log failures due to a bad password as those are not likely69// to occur more than once in a session for the user, and are more likely to be of70// value to the end user.71$this->reject($request, is_null($key));72}73}7475$this->validateSftpAccess($user, $server);7677return new JsonResponse([78'user' => $user->uuid,79'server' => $server->uuid,80'permissions' => $this->permissions->handle($server, $user),81]);82}8384/**85* Finds the server being requested and ensures that it belongs to the node this86* request stems from.87*/88protected function getServer(Request $request, string $uuid): Server89{90return Server::query()91->where(fn ($builder) => $builder->where('uuid', $uuid)->orWhere('uuidShort', $uuid))92->where('node_id', $request->attributes->get('node')->id)93->firstOr(function () use ($request) {94$this->reject($request);95});96}9798/**99* Finds a user with the given username or increments the login attempts.100*/101protected function getUser(Request $request, string $username): User102{103return User::query()->where('username', $username)->firstOr(function () use ($request) {104$this->reject($request);105});106}107108/**109* Parses the username provided to the request.110*111* @return array{"username": string, "server": string}112*/113protected function parseUsername(string $value): array114{115// Reverse the string to avoid issues with usernames that contain periods.116$parts = explode('.', strrev($value), 2);117118// Unreverse the strings after parsing them apart.119return [120'username' => strrev(array_get($parts, 1)),121'server' => strrev(array_get($parts, 0)),122];123}124125/**126* Rejects the request and increments the login attempts.127*/128protected function reject(Request $request, bool $increment = true): void129{130if ($increment) {131$this->incrementLoginAttempts($request);132}133134throw new HttpForbiddenException('Authorization credentials were not correct, please try again.');135}136137/**138* Validates that a user should have permission to use SFTP for the given server.139*/140protected function validateSftpAccess(User $user, Server $server): void141{142if (!$user->root_admin && $server->owner_id !== $user->id) {143$permissions = $this->permissions->handle($server, $user);144145if (!in_array(Permission::ACTION_FILE_SFTP, $permissions)) {146Activity::event('server:sftp.denied')->actor($user)->subject($server)->log();147148throw new HttpForbiddenException('You do not have permission to access SFTP for this server.');149}150}151152$server->validateCurrentState();153}154155/**156* Get the throttle key for the given request.157*/158protected function throttleKey(Request $request): string159{160$username = explode('.', strrev($request->input('username', '')));161162return strtolower(strrev($username[0] ?? '') . '|' . $request->ip()); // @phpstan-ignore nullCoalesce.offset163}164}165166167