diff --git a/CHANGELOG.md b/CHANGELOG.md index e44b65b9b..1451f159b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ This project follows [Semantic Versioning](http://semver.org) guidelines. * Server creation page now only asks for a node to deploy to, rather than requiring a location and then a node. * Database passwords are now hidden by default and will only show if clicked on. In addition, database view in ACP now indicates that passwords must be viewed on the front-end. * Localhost cannot be used as a connection address in the environment configuration script. `127.0.0.1` is allowed. +* Application locale can now be quickly set using an environment variable `APP_LOCALE` rather than having to edit core files. ### Fixed * Unable to change the daemon secret for a server via the Admin CP. diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e5bcb5d61..74c72ece1 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -6,6 +6,7 @@ use Pterodactyl\Http\Middleware\DaemonAuthenticate; use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Routing\Middleware\SubstituteBindings; use Pterodactyl\Http\Middleware\AccessingValidServer; +use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; @@ -66,7 +67,7 @@ class Kernel extends HttpKernel 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, 'server' => AccessingValidServer::class, - 'subuser.auth' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class, + 'subuser.auth' => AuthenticateAsSubuser::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, 'daemon-old' => DaemonAuthenticate::class, 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index e5c34da33..7c47ed0c1 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -10,6 +10,8 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use Illuminate\Http\Request; +use Symfony\Component\HttpKernel\Exception\HttpException; class AdminAuthenticate { @@ -20,18 +22,10 @@ class AdminAuthenticate * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { - if (! $request->user()) { - if ($request->expectsJson() || $request->json()) { - return response('Unauthorized.', 401); - } else { - return redirect()->guest('auth/login'); - } - } - - if (! $request->user()->root_admin) { - return abort(403); + if (! $request->user() || ! $request->user()->root_admin) { + throw new HttpException(403, 'Access Denied'); } return $next($request); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 06e2d6b70..019f92a2f 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use Illuminate\Http\Request; use Illuminate\Contracts\Auth\Guard; class Authenticate @@ -31,7 +32,7 @@ class Authenticate * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { if ($this->auth->guest()) { if ($request->ajax()) { diff --git a/app/Http/Middleware/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Daemon/DaemonAuthenticate.php index 2572ba854..ab587cbe0 100644 --- a/app/Http/Middleware/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Daemon/DaemonAuthenticate.php @@ -33,9 +33,12 @@ use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class DaemonAuthenticate { /** + * Daemon routes that this middleware should be skipped on. * @var array */ - protected $except = ['daemon.configuration']; + protected $except = [ + 'daemon.configuration', + ]; /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface @@ -63,6 +66,10 @@ class DaemonAuthenticate */ public function handle(Request $request, Closure $next) { + if (in_array($request->route()->getName(), $this->except)) { + return $next($request); + } + $token = $request->bearerToken(); if (is_null($token)) { diff --git a/app/Http/Middleware/DaemonAuthenticate.php b/app/Http/Middleware/DaemonAuthenticate.php index 056c0b344..ca77e70d2 100644 --- a/app/Http/Middleware/DaemonAuthenticate.php +++ b/app/Http/Middleware/DaemonAuthenticate.php @@ -10,35 +10,36 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use Illuminate\Http\Request; use Pterodactyl\Models\Node; -use Illuminate\Contracts\Auth\Guard; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; class DaemonAuthenticate { - /** - * The Guard implementation. - * - * @var \Illuminate\Contracts\Auth\Guard - */ - protected $auth; - /** * An array of route names to not apply this middleware to. * * @var array */ - protected $except = [ + private $except = [ 'daemon.configuration', ]; + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + private $repository; + /** * Create a new filter instance. * - * @param \Illuminate\Contracts\Auth\Guard $auth + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ - public function __construct(Guard $auth) + public function __construct(NodeRepositoryInterface $repository) { - $this->auth = $auth; + $this->repository = $repository; } /** @@ -48,21 +49,24 @@ class DaemonAuthenticate * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { if (in_array($request->route()->getName(), $this->except)) { return $next($request); } if (! $request->header('X-Access-Node')) { - return abort(403); + throw new HttpException(403); } - $node = Node::where('daemonSecret', $request->header('X-Access-Node'))->first(); - if (! $node) { - return abort(401); + try { + $node = $this->repository->findWhere(['daemonSecret' => $request->header('X-Access-Node')]); + } catch (RecordNotFoundException $exception) { + throw new HttpException(401); } + $request->attributes->set('node', $node); + return $next($request); } } diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index eefb3359f..9c0cadd86 100644 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -11,6 +11,5 @@ class EncryptCookies extends BaseEncrypter * * @var array */ - protected $except = [ - ]; + protected $except = []; } diff --git a/app/Http/Middleware/LanguageMiddleware.php b/app/Http/Middleware/LanguageMiddleware.php index 786760baf..d348f3b8d 100644 --- a/app/Http/Middleware/LanguageMiddleware.php +++ b/app/Http/Middleware/LanguageMiddleware.php @@ -9,14 +9,28 @@ namespace Pterodactyl\Http\Middleware; -use Auth; use Closure; -use Session; -use Settings; +use Illuminate\Http\Request; use Illuminate\Support\Facades\App; +use Illuminate\Contracts\Config\Repository; class LanguageMiddleware { + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; + + /** + * LanguageMiddleware constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct(Repository $config) + { + $this->config = $config; + } + /** * Handle an incoming request. * @@ -24,17 +38,9 @@ class LanguageMiddleware * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { - // if (Session::has('applocale')) { - // App::setLocale(Session::get('applocale')); - // } elseif (Auth::check() && isset(Auth::user()->language)) { - // Session::put('applocale', Auth::user()->language); - // App::setLocale(Auth::user()->language); - // } else { - // App::setLocale(Settings::get('default_language', 'en')); - // } - App::setLocale('en'); + App::setLocale($this->config->get('app.locale', 'en')); return $next($request); } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index a25e05fb2..ae55fef92 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -3,10 +3,26 @@ namespace Pterodactyl\Http\Middleware; use Closure; -use Illuminate\Support\Facades\Auth; +use Illuminate\Http\Request; +use Illuminate\Auth\AuthManager; class RedirectIfAuthenticated { + /** + * @var \Illuminate\Contracts\Auth\Guard + */ + private $authManager; + + /** + * RedirectIfAuthenticated constructor. + * + * @param \Illuminate\Auth\AuthManager $authManager + */ + public function __construct(AuthManager $authManager) + { + $this->authManager = $authManager; + } + /** * Handle an incoming request. * @@ -15,9 +31,9 @@ class RedirectIfAuthenticated * @param string|null $guard * @return mixed */ - public function handle($request, Closure $next, $guard = null) + public function handle(Request $request, Closure $next, string $guard = null) { - if (Auth::guard($guard)->check()) { + if ($this->authManager->guard($guard)->check()) { return redirect(route('index')); } diff --git a/app/Http/Middleware/RequireTwoFactorAuthentication.php b/app/Http/Middleware/RequireTwoFactorAuthentication.php index e53412b9c..75fb01664 100644 --- a/app/Http/Middleware/RequireTwoFactorAuthentication.php +++ b/app/Http/Middleware/RequireTwoFactorAuthentication.php @@ -10,6 +10,7 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use Illuminate\Http\Request; use Krucas\Settings\Settings; use Prologue\Alerts\AlertsMessageBag; @@ -22,28 +23,35 @@ class RequireTwoFactorAuthentication /** * @var \Prologue\Alerts\AlertsMessageBag */ - protected $alert; + private $alert; /** * @var \Krucas\Settings\Settings */ - protected $settings; + private $settings; /** - * All TOTP related routes. + * The names of routes that should be accessable without 2FA enabled. * * @var array */ - protected $ignoreRoutes = [ - 'account.security', - 'account.security.revoke', - 'account.security.totp', - 'account.security.totp.set', - 'account.security.totp.disable', - 'auth.totp', - 'auth.logout', + protected $except = [ + 'account.security', + 'account.security.revoke', + 'account.security.totp', + 'account.security.totp.set', + 'account.security.totp.disable', + 'auth.totp', + 'auth.logout', ]; + /** + * The route to redirect a user to to enable 2FA. + * + * @var string + */ + protected $redirectRoute = 'account.security'; + /** * RequireTwoFactorAuthentication constructor. * @@ -63,7 +71,7 @@ class RequireTwoFactorAuthentication * @param \Closure $next * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { // Ignore non-users if (! $request->user()) { @@ -71,7 +79,7 @@ class RequireTwoFactorAuthentication } // Skip the 2FA pages - if (in_array($request->route()->getName(), $this->ignoreRoutes)) { + if (in_array($request->route()->getName(), $this->except)) { return $next($request); } @@ -93,8 +101,8 @@ class RequireTwoFactorAuthentication break; } - $this->alert->danger('The administrator has required 2FA to be enabled. You must enable it before you can do any other action.')->flash(); + $this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash(); - return redirect()->route('account.security'); + return redirect()->route($this->redirectRoute); } } diff --git a/app/Http/Middleware/SubuserAccessAuthenticate.php b/app/Http/Middleware/Server/AuthenticateAsSubuser.php similarity index 80% rename from app/Http/Middleware/SubuserAccessAuthenticate.php rename to app/Http/Middleware/Server/AuthenticateAsSubuser.php index 30a906884..47f2bc885 100644 --- a/app/Http/Middleware/SubuserAccessAuthenticate.php +++ b/app/Http/Middleware/Server/AuthenticateAsSubuser.php @@ -7,7 +7,7 @@ * https://opensource.org/licenses/MIT */ -namespace Pterodactyl\Http\Middleware; +namespace Pterodactyl\Http\Middleware\Server; use Closure; use Illuminate\Http\Request; @@ -16,17 +16,17 @@ use Illuminate\Auth\AuthenticationException; use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -class SubuserAccessAuthenticate +class AuthenticateAsSubuser { /** * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService */ - protected $keyProviderService; + private $keyProviderService; /** * @var \Illuminate\Contracts\Session\Session */ - protected $session; + private $session; /** * SubuserAccessAuthenticate constructor. @@ -34,10 +34,8 @@ class SubuserAccessAuthenticate * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService * @param \Illuminate\Contracts\Session\Session $session */ - public function __construct( - DaemonKeyProviderService $keyProviderService, - Session $session - ) { + public function __construct(DaemonKeyProviderService $keyProviderService, Session $session) + { $this->keyProviderService = $keyProviderService; $this->session = $session; } @@ -55,16 +53,17 @@ class SubuserAccessAuthenticate */ public function handle(Request $request, Closure $next) { - $server = $this->session->get('server_data.model'); + $server = $request->attributes->get('server'); try { $token = $this->keyProviderService->handle($server->id, $request->user()->id); - $this->session->now('server_data.token', $token); - $request->attributes->set('server_token', $token); } catch (RecordNotFoundException $exception) { throw new AuthenticationException('This account does not have permission to access this server.'); } + $this->session->now('server_data.token', $token); + $request->attributes->set('server_token', $token); + return $next($request); } } diff --git a/app/Http/Middleware/Server/ScheduleBelongsToServer.php b/app/Http/Middleware/Server/ScheduleBelongsToServer.php index 32205b6ba..f9f40bf3b 100644 --- a/app/Http/Middleware/Server/ScheduleBelongsToServer.php +++ b/app/Http/Middleware/Server/ScheduleBelongsToServer.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Server; use Closure; +use Illuminate\Http\Request; use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -41,7 +42,7 @@ class ScheduleBelongsToServer * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { $server = $request->attributes->get('server'); diff --git a/app/Http/Middleware/Server/SubuserBelongsToServer.php b/app/Http/Middleware/Server/SubuserBelongsToServer.php index 100144c16..18291245d 100644 --- a/app/Http/Middleware/Server/SubuserBelongsToServer.php +++ b/app/Http/Middleware/Server/SubuserBelongsToServer.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Http\Middleware\Server; use Closure; +use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; @@ -43,7 +44,7 @@ class SubuserBelongsToServer * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { $server = $request->attributes->get('server'); diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php index 07a0783c7..83a78fcd2 100644 --- a/app/Http/Middleware/VerifyReCaptcha.php +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -3,28 +3,46 @@ namespace Pterodactyl\Http\Middleware; use Closure; +use GuzzleHttp\Client; +use Illuminate\Http\Request; use Pterodactyl\Events\Auth\FailedCaptcha; +use Illuminate\Contracts\Config\Repository; class VerifyReCaptcha { + /** + * @var \Illuminate\Contracts\Config\Repository + */ + private $config; + + /** + * VerifyReCaptcha constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct(Repository $config) + { + $this->config = $config; + } + /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next - * @return \Illuminate\Http\RediectResponse + * @return \Illuminate\Http\RedirectResponse|mixed */ public function handle($request, Closure $next) { - if (! config('recaptcha.enabled')) { + if (! $this->config->get('recaptcha.enabled')) { return $next($request); } if ($request->has('g-recaptcha-response')) { - $client = new \GuzzleHttp\Client(); - $res = $client->post(config('recaptcha.domain'), [ + $client = new Client(); + $res = $client->post($this->config->get('recaptcha.domain'), [ 'form_params' => [ - 'secret' => config('recaptcha.secret_key'), + 'secret' => $this->config->get('recaptcha.secret_key'), 'response' => $request->input('g-recaptcha-response'), ], ]); @@ -32,29 +50,33 @@ class VerifyReCaptcha if ($res->getStatusCode() === 200) { $result = json_decode($res->getBody()); - $verified = function ($result, $request) { - if (! config('recaptcha.verify_domain')) { - return false; - } - - $url = parse_url($request->url()); - - if (! array_key_exists('host', $url)) { - return false; - } - - return $result->hostname === $url['host']; - }; - - if ($result->success && (! config('recaptcha.verify_domain') || $verified($result, $request))) { + if ($result->success && (! $this->config->get('recaptcha.verify_domain') || $this->isResponseVerified($result, $request))) { return $next($request); } } } // Emit an event and return to the previous view with an error (only the captcha error will be shown!) - event(new FailedCaptcha($request->ip(), (! isset($result->hostname) ?: $result->hostname))); + event(new FailedCaptcha($request->ip(), (! isset($result) ?: object_get($result, 'hostname')))); - return back()->withErrors(['g-recaptcha-response' => trans('strings.captcha_invalid')])->withInput(); + return redirect()->back()->withErrors(['g-recaptcha-response' => trans('strings.captcha_invalid')])->withInput(); + } + + /** + * Determine if the response from the recaptcha servers was valid. + * + * @param object $result + * @param \Illuminate\Http\Request $request + * @return bool + */ + private function isResponseVerified(object $result, Request $request): bool + { + if (! $this->config->get('recaptcha.verify_domain')) { + return false; + } + + $url = parse_url($request->url()); + + return $result->hostname === array_get($url, 'host'); } } diff --git a/config/app.php b/config/app.php index 928e1657a..7eb416d37 100644 --- a/config/app.php +++ b/config/app.php @@ -66,7 +66,7 @@ return [ | */ - 'locale' => 'en', + 'locale' => env('APP_LOCALE', 'en'), /* |-------------------------------------------------------------------------- diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php index ebaee6243..1abd6bd73 100644 --- a/resources/lang/en/auth.php +++ b/resources/lang/en/auth.php @@ -18,4 +18,5 @@ return [ '2fa_required' => '2-Factor Authentication', '2fa_failed' => 'The 2FA token provided was invalid.', 'totp_failed' => 'There was an error while attempting to validate TOTP.', + '2fa_must_be_enabled' => 'The administrator has required that 2-Factor Authentication be enabled for your account in order to use the Panel.', ];