Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 32 additions & 61 deletions app/Http/Controllers/Admin/DatabaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;

use PDO;
use PDOException;
use Pterodactyl\Services\Databases\Hosts\HostUpdateService;
use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest;
use Pterodactyl\Services\Databases\Hosts\HostCreationService;
use Pterodactyl\Services\Databases\Hosts\HostDeletionService;
use Pterodactyl\Services\Databases\Drivers\DatabaseDriverManager;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface;
Expand All @@ -35,6 +33,7 @@ public function __construct(
private HostCreationService $creationService,
private HostDeletionService $deletionService,
private HostUpdateService $updateService,
private DatabaseDriverManager $drivers,
private LocationRepositoryInterface $locationRepository,
private ViewFactory $view,
) {}
Expand Down Expand Up @@ -74,15 +73,11 @@ public function create(DatabaseHostFormRequest $request): RedirectResponse
try {
$host = $this->creationService->handle($request->normalize());
} catch (\Exception $exception) {
if ($exception instanceof \PDOException || $exception->getPrevious() instanceof \PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();

return redirect()->route('admin.databases')->withInput($request->validated());
} else {
throw $exception;
}
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();

return redirect()->route('admin.databases')->withInput($request->validated());
}

$this->alert->success('Successfully created a new database host on the system.')->flash();
Expand All @@ -103,17 +98,11 @@ public function update(DatabaseHostFormRequest $request, DatabaseHost $host): Re
$this->updateService->handle($host->id, $request->normalize());
$this->alert->success('Database host was updated successfully.')->flash();
} catch (\Exception $exception) {
// Catch any SQL related exceptions and display them back to the user, otherwise just
// throw the exception like normal and move on with it.
if ($exception instanceof \PDOException || $exception->getPrevious() instanceof \PDOException) {
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();

return $redirect->withInput($request->normalize());
} else {
throw $exception;
}
$this->alert->danger(
sprintf('There was an error while trying to connect to the host or while executing a query: "%s"', $exception->getMessage())
)->flash();

return $redirect->withInput($request->normalize());
}

return $redirect;
Expand Down Expand Up @@ -144,53 +133,35 @@ public function testConnection(Request $request): JsonResponse
'port' => 'required|integer|min:1|max:65535',
'username' => 'required|string',
'password' => 'required|string',
]);
Log::error("TestConnection", [
"\nhost" => $request->host,
"\nport" => $request->port,
"\nusername" => $request->username,
"\npassword" => $request->password
'type' => 'required|string|in:mysql,postgresql,redis,mongodb',
]);

try {
$dsn = "mysql:host={$request->input('host')};port={$request->input('port')};charset=utf8";

$pdo = new PDO($dsn, $request->input('username'), $request->input('password'), [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_TIMEOUT => 5, // 5 second timeout
]);

// Test basic query
$version = $pdo->query('SELECT VERSION() as version')->fetchColumn();
$host = new DatabaseHost();
$host->forceFill([
'host' => $request->input('host'),
'port' => $request->input('port'),
'username' => $request->input('username'),
'password' => app(\Illuminate\Contracts\Encryption\Encrypter::class)->encrypt($request->input('password')),
'type' => $request->input('type'),
])->skipValidation();

// Test GRANT permissions (this is what Pterodactyl needs)
$grants = $pdo->query('SHOW GRANTS FOR CURRENT_USER()')->fetchAll(PDO::FETCH_COLUMN);

$hasGrantOption = false;
foreach ($grants as $grant) {
if (stripos($grant, 'GRANT OPTION') !== false) {
$hasGrantOption = true;
break;
}
}

$message = "Successfully connected to MySQL server (Version: {$version}).";
if (!$hasGrantOption) {
$message .= " Warning: The user appears to lack GRANT OPTION permission which is required for creating databases and users.";
}
$details = $this->drivers->driverFor($host)->testConnection($host);

return response()->json([
'success' => true,
'message' => $message,
'version' => $version,
'has_grant_option' => $hasGrantOption
'message' => $details['message'] ?? 'Successfully connected.',
'version' => $details['version'] ?? null,
'has_grant_option' => $details['has_grant_option'] ?? null,
]);
} catch (PDOException $e) {
return response()->json([
'success' => false,
'message' => 'Connection failed: ' . $e->getMessage()
], 422);
} catch (\Exception $e) {
Log::warning('Database host connection test failed.', [
'type' => $request->input('type'),
'host' => $request->input('host'),
'port' => $request->input('port'),
'error' => $e->getMessage(),
]);

return response()->json([
'success' => false,
'message' => 'Error: ' . $e->getMessage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Elytra;

use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\Server;
use Pterodactyl\Facades\Activity;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
Expand Down Expand Up @@ -36,6 +36,9 @@ public function index(GetDatabasesRequest $request, Server $server): array
{
return $this->fractal->collection($server->databases)
->transformWith($this->getTransformer(DatabaseTransformer::class))
->addMeta([
'available_database_types' => $this->deployDatabaseService->availableTypeMetadata($server)->all(),
])
->toArray();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace Pterodactyl\Http\Controllers\Api\Client\Servers\Wings;

use Illuminate\Http\Response;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\Server;
use Pterodactyl\Facades\Activity;
use Pterodactyl\Services\Databases\DatabasePasswordService;
use Pterodactyl\Transformers\Api\Client\DatabaseTransformer;
Expand Down Expand Up @@ -36,6 +36,9 @@ public function index(GetDatabasesRequest $request, Server $server): array
{
return $this->fractal->collection($server->databases)
->transformWith($this->getTransformer(DatabaseTransformer::class))
->addMeta([
'available_database_types' => $this->deployDatabaseService->availableTypeMetadata($server)->all(),
])
->toArray();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Pterodactyl\Models\Server;
use Illuminate\Validation\Rule;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\DatabaseHost;
use Pterodactyl\Models\Permission;
use Illuminate\Database\Query\Builder;
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
Expand Down Expand Up @@ -40,6 +41,7 @@ public function rules(): array
}),
],
'remote' => Database::getRulesForField('remote'),
'database_type' => ['required', 'string', Rule::in(array_keys(DatabaseHost::typeLabels()))],
];
}

Expand Down
7 changes: 7 additions & 0 deletions app/Models/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
* @property string $username
* @property string $remote
* @property string $password
* @property string $type
* @property int $max_connections
* @property array|null $connection_details
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Server $server
Expand Down Expand Up @@ -52,7 +54,9 @@ class Database extends Model
'username',
'password',
'remote',
'type',
'max_connections',
'connection_details',
];

/**
Expand All @@ -62,6 +66,7 @@ class Database extends Model
'server_id' => 'integer',
'database_host_id' => 'integer',
'max_connections' => 'integer',
'connection_details' => 'array',
];

public static array $validationRules = [
Expand All @@ -71,7 +76,9 @@ class Database extends Model
'username' => 'string|alpha_dash|between:3,100',
'max_connections' => 'nullable|integer',
'remote' => 'required|string|regex:/^[\w\-\/.%:]+$/',
'type' => 'required|string|in:mysql,postgresql,redis,mongodb',
'password' => 'string',
'connection_details' => 'nullable|array',
];

public function getRouteKeyName(): string
Expand Down
18 changes: 18 additions & 0 deletions app/Models/DatabaseHost.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@
* @property int $port
* @property string $username
* @property string $password
* @property string $type
* @property int|null $max_databases
* @property int|null $node_id
* @property \Carbon\CarbonImmutable $created_at
* @property \Carbon\CarbonImmutable $updated_at
*/
class DatabaseHost extends Model
{
public const TYPE_MYSQL = 'mysql';
public const TYPE_POSTGRESQL = 'postgresql';
public const TYPE_REDIS = 'redis';
public const TYPE_MONGODB = 'mongodb';

/** @use HasFactory<\Database\Factories\DatabaseHostFactory> */
use HasFactory;

Expand Down Expand Up @@ -50,6 +56,7 @@ class DatabaseHost extends Model
'port',
'username',
'password',
'type',
'max_databases',
'node_id',
];
Expand All @@ -72,9 +79,20 @@ class DatabaseHost extends Model
'port' => 'required|numeric|between:1,65535',
'username' => 'required|string|max:32',
'password' => 'nullable|string',
'type' => 'required|string|in:mysql,postgresql,redis,mongodb',
'node_id' => 'sometimes|nullable|integer|exists:nodes,id',
];

public static function typeLabels(): array
{
return [
self::TYPE_MYSQL => 'MariaDB/MySQL',
self::TYPE_POSTGRESQL => 'PostgreSQL',
self::TYPE_REDIS => 'Redis',
self::TYPE_MONGODB => 'MongoDB',
];
}

/**
* Gets the node associated with a database host.
*/
Expand Down
42 changes: 18 additions & 24 deletions app/Services/Databases/DatabaseManagementService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
use Exception;
use Pterodactyl\Models\Server;
use Pterodactyl\Models\Database;
use Pterodactyl\Models\DatabaseHost;
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\Services\Databases\Drivers\DatabaseDriverManager;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;

Expand All @@ -34,9 +34,8 @@ class DatabaseManagementService

public function __construct(
protected ConnectionInterface $connection,
protected DynamicDatabaseConnection $dynamic,
protected Encrypter $encrypter,
protected DatabaseRepository $repository,
protected DatabaseDriverManager $drivers,
) {}

/**
Expand Down Expand Up @@ -91,40 +90,38 @@ public function create(Server $server, array $data): Database
throw new \InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
}

/** @var DatabaseHost $host */
$host = DatabaseHost::query()->findOrFail($data['database_host_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)
),
'type' => $host->type,
'connection_details' => null,
]);

$database = null;
$plainPassword = $this->encrypter->decrypt($data['password']);
$driver = $this->drivers->driverFor($host);

try {
return $this->connection->transaction(function () use ($data, &$database) {
return $this->connection->transaction(function () use ($data, $driver, $host, $plainPassword, &$database) {
$database = $this->createModel($data);
$connectionDetails = $driver->create($host, $database, $plainPassword);

$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();
if ($connectionDetails !== []) {
$database->forceFill(['connection_details' => $connectionDetails])->saveOrFail();
}

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();
$driver->delete($host, $database);
}
} catch (\Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no
Expand All @@ -142,11 +139,8 @@ public function create(Server $server, array $data): Database
*/
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();
$database->loadMissing('host');
$this->drivers->driverFor($database->host)->delete($database->host, $database);

return $database->delete();
}
Expand Down
Loading
Loading