220 lines
7.0 KiB
PHP
220 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Hosting\Plesk;
|
|
|
|
use App\Exceptions\Hosting\PleskException;
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Http\Client\PendingRequest;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PleskClient
|
|
{
|
|
private PendingRequest $http;
|
|
|
|
public function __construct()
|
|
{
|
|
$user = config('hosting.plesk.user');
|
|
$password = config('hosting.plesk.password');
|
|
|
|
if (empty($user) || empty($password)) {
|
|
throw new PleskException('PLESK_USER and PLESK_PASS must be configured.');
|
|
}
|
|
|
|
$verify = filter_var(config('hosting.plesk.verify_ssl', true), FILTER_VALIDATE_BOOLEAN);
|
|
|
|
$this->http = Http::baseUrl(rtrim(config('hosting.plesk.url'), '/').'/api/v2')
|
|
->withBasicAuth($user, $password)
|
|
->acceptJson()
|
|
->timeout((int) config('hosting.plesk.timeout', 30))
|
|
->when(! $verify, fn (PendingRequest $request) => $request->withoutVerifying());
|
|
}
|
|
|
|
public function createARecord(string $domain, string $subdomain, string $ip): array
|
|
{
|
|
$this->validateDomain($domain);
|
|
$this->validateSubdomain($subdomain);
|
|
$this->validateIp($ip);
|
|
|
|
$baseDomain = config('hosting.plesk.base_domain');
|
|
$zoneDomain = $this->resolveZoneDomain($domain, $baseDomain);
|
|
$host = $this->resolveHostLabel($domain, $subdomain, $baseDomain);
|
|
|
|
if ($this->findARecord($zoneDomain, $host) !== null) {
|
|
throw new PleskException(
|
|
"DNS A record already exists for {$host}.{$zoneDomain}",
|
|
step: 'plesk_dns_exists',
|
|
);
|
|
}
|
|
|
|
$targetIp = config('hosting.traefik.public_ip') ?: $ip;
|
|
|
|
Log::info('Plesk: creating A record', [
|
|
'zone' => $zoneDomain,
|
|
'host' => $host,
|
|
'ip' => $targetIp,
|
|
]);
|
|
|
|
$response = $this->http->post("/domains/{$zoneDomain}/dns/records", [
|
|
'type' => 'A',
|
|
'host' => $host === '@' ? '' : $host,
|
|
'value' => $targetIp,
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
throw new PleskException(
|
|
'Plesk create A record failed: '.$response->body(),
|
|
step: 'plesk_create',
|
|
context: ['status' => $response->status()],
|
|
code: $response->status(),
|
|
);
|
|
}
|
|
|
|
return $response->json() ?? [];
|
|
}
|
|
|
|
public function deleteARecord(string $domain, string $subdomain): void
|
|
{
|
|
$baseDomain = config('hosting.plesk.base_domain');
|
|
$zoneDomain = $this->resolveZoneDomain($domain, $baseDomain);
|
|
$host = $this->resolveHostLabel($domain, $subdomain, $baseDomain);
|
|
|
|
$record = $this->findARecord($zoneDomain, $host);
|
|
|
|
if ($record === null) {
|
|
Log::warning('Plesk: A record not found for deletion', compact('zoneDomain', 'host'));
|
|
|
|
return;
|
|
}
|
|
|
|
$recordId = $record['id'] ?? null;
|
|
|
|
if ($recordId === null) {
|
|
throw new PleskException('Plesk record id missing.', step: 'plesk_delete');
|
|
}
|
|
|
|
Log::info('Plesk: deleting A record', ['zone' => $zoneDomain, 'id' => $recordId]);
|
|
|
|
$response = $this->http->delete("/domains/{$zoneDomain}/dns/records/{$recordId}");
|
|
|
|
if ($response->failed() && $response->status() !== 404) {
|
|
throw new PleskException(
|
|
'Plesk delete A record failed: '.$response->body(),
|
|
step: 'plesk_delete',
|
|
code: $response->status(),
|
|
);
|
|
}
|
|
}
|
|
|
|
public function listRecords(string $domain): array
|
|
{
|
|
$zoneDomain = $this->resolveZoneDomain($domain, config('hosting.plesk.base_domain'));
|
|
|
|
try {
|
|
$response = $this->http->get("/domains/{$zoneDomain}/dns/records");
|
|
|
|
if ($response->failed()) {
|
|
throw new PleskException(
|
|
'Plesk list records failed: '.$response->body(),
|
|
step: 'plesk_list',
|
|
code: $response->status(),
|
|
);
|
|
}
|
|
|
|
return $response->json() ?? [];
|
|
} catch (ConnectionException $e) {
|
|
throw new PleskException(
|
|
'Plesk connection failed: '.$e->getMessage(),
|
|
step: 'plesk_connection',
|
|
previous: $e,
|
|
);
|
|
}
|
|
}
|
|
|
|
public function dnsExists(string $domain, string $subdomain): bool
|
|
{
|
|
$zoneDomain = $this->resolveZoneDomain($domain, config('hosting.plesk.base_domain'));
|
|
$host = $this->resolveHostLabel($domain, $subdomain, config('hosting.plesk.base_domain'));
|
|
|
|
return $this->findARecord($zoneDomain, $host) !== null;
|
|
}
|
|
|
|
private function findARecord(string $zoneDomain, string $host): ?array
|
|
{
|
|
$records = $this->listRecords($zoneDomain);
|
|
$items = $records['records'] ?? (is_array($records) && array_is_list($records) ? $records : []);
|
|
|
|
foreach ($items as $record) {
|
|
if (! is_array($record)) {
|
|
continue;
|
|
}
|
|
|
|
$type = strtoupper((string) ($record['type'] ?? ''));
|
|
$recordHost = (string) ($record['host'] ?? $record['name'] ?? '');
|
|
|
|
if ($type !== 'A') {
|
|
continue;
|
|
}
|
|
|
|
$normalizedHost = $recordHost === '' ? '@' : rtrim($recordHost, '.');
|
|
|
|
if ($normalizedHost === $host || $normalizedHost === "{$host}.{$zoneDomain}") {
|
|
return $record;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveZoneDomain(string $domain, string $baseDomain): string
|
|
{
|
|
if (str_ends_with($domain, '.'.$baseDomain) || $domain === $baseDomain) {
|
|
return $baseDomain;
|
|
}
|
|
|
|
return $domain;
|
|
}
|
|
|
|
private function resolveHostLabel(string $domain, string $subdomain, string $baseDomain): string
|
|
{
|
|
if ($subdomain !== '' && $subdomain !== '@') {
|
|
return $subdomain;
|
|
}
|
|
|
|
if ($domain === $baseDomain) {
|
|
return '@';
|
|
}
|
|
|
|
if (str_ends_with($domain, '.'.$baseDomain)) {
|
|
return substr($domain, 0, -(strlen($baseDomain) + 1));
|
|
}
|
|
|
|
return explode('.', $domain)[0] ?? $subdomain;
|
|
}
|
|
|
|
private function validateDomain(string $domain): void
|
|
{
|
|
if (! preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain)) {
|
|
throw new PleskException("Invalid domain: {$domain}", step: 'plesk_validation');
|
|
}
|
|
}
|
|
|
|
private function validateSubdomain(string $subdomain): void
|
|
{
|
|
if ($subdomain === '' || $subdomain === '@') {
|
|
return;
|
|
}
|
|
|
|
if (! preg_match('/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i', $subdomain)) {
|
|
throw new PleskException("Invalid subdomain: {$subdomain}", step: 'plesk_validation');
|
|
}
|
|
}
|
|
|
|
private function validateIp(string $ip): void
|
|
{
|
|
if (! filter_var($ip, FILTER_VALIDATE_IP)) {
|
|
throw new PleskException("Invalid IP: {$ip}", step: 'plesk_validation');
|
|
}
|
|
}
|
|
}
|