cbcf62086f
Co-authored-by: DaneEveritt <dane@daneeveritt.com>
175 lines
6.5 KiB
PHP
175 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace Pterodactyl\Services\Databases;
|
|
|
|
use Exception;
|
|
use InvalidArgumentException;
|
|
use Pterodactyl\Models\Server;
|
|
use Pterodactyl\Models\Database;
|
|
use Pterodactyl\Helpers\Utilities;
|
|
use Illuminate\Database\ConnectionInterface;
|
|
use Illuminate\Contracts\Encryption\Encrypter;
|
|
use Pterodactyl\Extensions\DynamicDatabaseConnection;
|
|
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
|
|
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
|
|
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
|
|
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
|
|
|
|
class DatabaseManagementService
|
|
{
|
|
/**
|
|
* The regex used to validate that the database name passed through to the function is
|
|
* in the expected format.
|
|
*
|
|
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
|
|
*/
|
|
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
|
|
|
|
/**
|
|
* Determines if the service should validate the user's ability to create an additional
|
|
* database for this server. In almost all cases this should be true, but to keep things
|
|
* flexible you can also set it to false and create more databases than the server is
|
|
* allocated.
|
|
*/
|
|
protected bool $validateDatabaseLimit = true;
|
|
|
|
public function __construct(
|
|
protected ConnectionInterface $connection,
|
|
protected DynamicDatabaseConnection $dynamic,
|
|
protected Encrypter $encrypter,
|
|
protected DatabaseRepository $repository
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Generates a unique database name for the given server. This name should be passed through when
|
|
* calling this handle function for this service, otherwise the database will be created with
|
|
* whatever name is provided.
|
|
*/
|
|
public static function generateUniqueDatabaseName(string $name, int $serverId): string
|
|
{
|
|
// Max of 48 characters, including the s123_ that we append to the front.
|
|
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
|
|
}
|
|
|
|
/**
|
|
* Set whether this class should validate that the server has enough slots
|
|
* left before creating the new database.
|
|
*/
|
|
public function setValidateDatabaseLimit(bool $validate): self
|
|
{
|
|
$this->validateDatabaseLimit = $validate;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Create a new database that is linked to a specific host.
|
|
*
|
|
* @throws \Throwable
|
|
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
|
|
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
|
|
*/
|
|
public function create(Server $server, array $data): Database
|
|
{
|
|
if (!config('pterodactyl.client_features.databases.enabled')) {
|
|
throw new DatabaseClientFeatureNotEnabledException();
|
|
}
|
|
|
|
if ($this->validateDatabaseLimit) {
|
|
// If the server has a limit assigned and we've already reached that limit, throw back
|
|
// an exception and kill the process.
|
|
if (!is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
|
|
throw new TooManyDatabasesException();
|
|
}
|
|
}
|
|
|
|
// Protect against developer mistakes...
|
|
if (empty($data['database']) || !preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
|
|
throw new InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
|
|
}
|
|
|
|
$data = array_merge($data, [
|
|
'server_id' => $server->id,
|
|
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
|
|
'password' => $this->encrypter->encrypt(
|
|
Utilities::randomStringWithSpecialCharacters(24)
|
|
),
|
|
]);
|
|
|
|
$database = null;
|
|
|
|
try {
|
|
return $this->connection->transaction(function () use ($data, &$database) {
|
|
$database = $this->createModel($data);
|
|
|
|
$this->dynamic->set('dynamic', $data['database_host_id']);
|
|
|
|
$this->repository->createDatabase($database->database);
|
|
$this->repository->createUser(
|
|
$database->username,
|
|
$database->remote,
|
|
$this->encrypter->decrypt($database->password),
|
|
$database->max_connections
|
|
);
|
|
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
|
|
$this->repository->flush();
|
|
|
|
return $database;
|
|
});
|
|
} catch (Exception $exception) {
|
|
try {
|
|
if ($database instanceof Database) {
|
|
$this->repository->dropDatabase($database->database);
|
|
$this->repository->dropUser($database->username, $database->remote);
|
|
$this->repository->flush();
|
|
}
|
|
} catch (Exception $deletionException) {
|
|
// Do nothing here. We've already encountered an issue before this point so no
|
|
// reason to prioritize this error over the initial one.
|
|
}
|
|
|
|
throw $exception;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a database from the given host server.
|
|
*
|
|
* @throws \Exception
|
|
*/
|
|
public function delete(Database $database): ?bool
|
|
{
|
|
$this->dynamic->set('dynamic', $database->database_host_id);
|
|
|
|
$this->repository->dropDatabase($database->database);
|
|
$this->repository->dropUser($database->username, $database->remote);
|
|
$this->repository->flush();
|
|
|
|
return $database->delete();
|
|
}
|
|
|
|
/**
|
|
* Create the database if there is not an identical match in the DB. While you can technically
|
|
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
|
|
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
|
|
*
|
|
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
|
|
* @throws \Throwable
|
|
*/
|
|
protected function createModel(array $data): Database
|
|
{
|
|
$exists = Database::query()->where('server_id', $data['server_id'])
|
|
->where('database', $data['database'])
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
throw new DuplicateDatabaseNameException('A database with that name already exists for this server.');
|
|
}
|
|
|
|
$database = (new Database())->forceFill($data);
|
|
$database->saveOrFail();
|
|
|
|
return $database;
|
|
}
|
|
}
|