initial commit

This commit is contained in:
TheOnlyMace
2026-05-17 13:26:14 +02:00
commit 75299b723d
176 changed files with 20327 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
<?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');
}
}
}