diff --git a/CHANGELOG.md b/CHANGELOG.md index 240ec73e8..ec45e13db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ that they are the owner of, as well as all servers they're a subuser of. * Insurgency egg now uses the proper dedicated server ID. * Teamspeak egg updated with improved installation process and grabbing latest versions. * OOM killer disabled by default on all new servers. +* Passwords generated for MySQL now include special characters and are 24 characters in length. ## v0.7.14 (Derelict Dermodactylus) ### Fixed diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 1dc957033..6bf21b249 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -589,8 +589,7 @@ class ServersController extends Controller * @param int $server * @return \Illuminate\Http\RedirectResponse * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Throwable */ public function resetDatabasePassword(Request $request, $server) { @@ -599,7 +598,7 @@ class ServersController extends Controller ['id', '=', $request->input('database')], ]); - $this->databasePasswordService->handle($database, str_random(24)); + $this->databasePasswordService->handle($database); return response('', 204); } diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index ce86cc4d7..a2030bbc7 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -87,12 +87,11 @@ class DatabaseController extends ApplicationApiController * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request * @return \Illuminate\Http\Response * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function resetPassword(ServerDatabaseWriteRequest $request): Response { - $this->databasePasswordService->handle($request->getModel(Database::class), str_random(24)); + $this->databasePasswordService->handle($request->getModel(Database::class)); return response('', 204); } diff --git a/app/Http/Controllers/Server/DatabaseController.php b/app/Http/Controllers/Server/DatabaseController.php index 7d8e4cda2..d897ca0cb 100644 --- a/app/Http/Controllers/Server/DatabaseController.php +++ b/app/Http/Controllers/Server/DatabaseController.php @@ -135,15 +135,13 @@ class DatabaseController extends Controller * @return \Illuminate\Http\JsonResponse * * @throws \Illuminate\Auth\Access\AuthorizationException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ public function update(Request $request): JsonResponse { $this->authorize('reset-db-password', $request->attributes->get('server')); - $password = str_random(24); - $this->passwordService->handle($request->attributes->get('database'), $password); + $password = $this->passwordService->handle($request->attributes->get('database')); return response()->json(['password' => $password]); } diff --git a/app/Services/Databases/DatabasePasswordService.php b/app/Services/Databases/DatabasePasswordService.php index 8f8a9582d..ed60bad4a 100644 --- a/app/Services/Databases/DatabasePasswordService.php +++ b/app/Services/Databases/DatabasePasswordService.php @@ -2,7 +2,9 @@ namespace Pterodactyl\Services\Databases; +use Exception; use Pterodactyl\Models\Database; +use Illuminate\Support\Facades\Log; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; @@ -54,33 +56,39 @@ class DatabasePasswordService * Updates a password for a given database. * * @param \Pterodactyl\Models\Database|int $database - * @param string $password - * @return bool + * @return string * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ - public function handle($database, string $password): bool + public function handle(Database $database): string { - if (! $database instanceof Database) { - $database = $this->repository->find($database); + $password = str_random(24); + // Given a random string of characters, randomly loop through the characters and replace some + // with special characters to avoid issues with MySQL password requirements on some servers. + try { + for ($i = 0; $i < random_int(2, 6); $i++) { + $character = ['!', '@', '=', '.', '+', '^'][random_int(0, 5)]; + + $password = substr_replace($password, $character, random_int(0, 23), 1); + } + } catch (Exception $exception) { + // Just log the error and hope for the best at this point. + Log::error($exception); } - $this->dynamic->set('dynamic', $database->database_host_id); - $this->connection->beginTransaction(); + $this->connection->transaction(function () use ($database, $password) { + $this->dynamic->set('dynamic', $database->database_host_id); - $updated = $this->repository->withoutFreshModel()->update($database->id, [ - 'password' => $this->encrypter->encrypt($password), - ]); + $this->repository->withoutFreshModel()->update($database->id, [ + 'password' => $this->encrypter->encrypt($password), + ]); - $this->repository->dropUser($database->username, $database->remote); - $this->repository->createUser($database->username, $database->remote, $password); - $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote); - $this->repository->flush(); + $this->repository->dropUser($database->username, $database->remote); + $this->repository->createUser($database->username, $database->remote, $password); + $this->repository->assignUserToDatabase($database->database, $database->username, $database->remote); + $this->repository->flush(); + }); - unset($password); - $this->connection->commit(); - - return $updated; + return $password; } } diff --git a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php index 946f977d4..c01793677 100644 --- a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php +++ b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php @@ -48,43 +48,35 @@ class DatabasePasswordServiceTest extends TestCase /** * Test that a password can be updated. - * - * @dataProvider useModelDataProvider */ - public function testPasswordIsChanged(bool $useModel) + public function testPasswordIsChanged() { $model = factory(Database::class)->make(); - if (! $useModel) { - $this->repository->shouldReceive('find')->with(1234)->once()->andReturn($model); - } + $this->connection->expects('transaction')->with(m::on(function ($closure) { + return is_null($closure()); + })); $this->dynamic->shouldReceive('set')->with('dynamic', $model->database_host_id)->once()->andReturnNull(); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with('test123')->once()->andReturn('enc123'); + + $this->encrypter->expects('encrypt')->with(m::on(function ($string) { + preg_match_all('/[!@+=^-]/', $string, $matches, PREG_SET_ORDER); + $this->assertTrue(count($matches) >= 2 && count($matches) <= 6, "Failed asserting that [{$string}] contains 2 to 6 special characters."); + $this->assertTrue(strlen($string) === 24, "Failed asserting that [{$string}] is 24 characters in length."); + + return true; + }))->andReturn('enc123'); $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf(); $this->repository->shouldReceive('update')->with($model->id, ['password' => 'enc123'])->once()->andReturn(true); $this->repository->shouldReceive('dropUser')->with($model->username, $model->remote)->once()->andReturn(true); - $this->repository->shouldReceive('createUser')->with($model->username, $model->remote, 'test123')->once()->andReturn(true); + $this->repository->shouldReceive('createUser')->with($model->username, $model->remote, m::any())->once()->andReturn(true); $this->repository->shouldReceive('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->once()->andReturn(true); $this->repository->shouldReceive('flush')->withNoArgs()->once()->andReturn(true); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); - $response = $this->getService()->handle($useModel ? $model : 1234, 'test123'); + $response = $this->getService()->handle($model); $this->assertNotEmpty($response); - $this->assertTrue($response); - } - - /** - * Data provider to determine if a model should be passed or an int. - * - * @return array - */ - public function useModelDataProvider(): array - { - return [[false], [true]]; } /**