Files
Virtnet.bond/extensions/Servers/Proxmox/TechnitiumDns.php

443 lines
15 KiB
PHP

<?php
namespace Paymenter\Extensions\Servers\Proxmox;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
use Paymenter\Extensions\Servers\Proxmox\Models\IPAddress;
class TechnitiumDns
{
private string $baseUrl;
private string $token;
/**
* Check whether Technitium DNS credentials are configured.
*/
public static function isConfigured(): bool
{
return !empty(config('services.technitium.url'))
&& !empty(config('services.technitium.token'));
}
public function __construct()
{
$this->baseUrl = rtrim((string) config('services.technitium.url', ''), '/');
$this->token = (string) config('services.technitium.token', '');
if (empty($this->baseUrl) || empty($this->token)) {
throw new \RuntimeException('Technitium DNS is not configured. Set TECHNITIUM_DNS_URL and TECHNITIUM_DNS_TOKEN in your environment.');
}
}
/**
* Make an API request to Technitium DNS server
*/
private function request(string $endpoint, string $method = 'get', array $data = []): Response
{
$url = rtrim($this->baseUrl, '/') . '/api' . $endpoint;
$params = array_merge(['token' => $this->token], $data);
if ($method === 'get') {
return Http::timeout(10)->connectTimeout(5)->get($url, $params);
} else {
return Http::timeout(10)->connectTimeout(5)->asForm()->post($url, $params);
}
}
/**
* Get all zones
*/
public function getZones(): array
{
try {
$response = $this->request('/zones/list');
$json = $response->json();
if ($json['status'] === 'ok') {
return $json['response'] ?? [];
}
return [];
} catch (\Exception $e) {
return [];
}
}
/**
* Get zone details
*/
public function getZone(string $zoneName): ?array
{
try {
$response = $this->request('/zones/get', 'get', ['zone' => $zoneName]);
$json = $response->json();
if ($json['status'] === 'ok') {
return $json['response'] ?? null;
}
return null;
} catch (\Exception $e) {
return null;
}
}
/**
* Check if a subdomain exists
*/
public function subdomainExists(string $subdomain): bool
{
$fullDomain = $subdomain . '.dns.virtnet.host';
$zones = $this->getZones();
foreach ($zones as $zone) {
if (isset($zone['name']) && $zone['name'] === $fullDomain) {
return true;
}
}
return false;
}
/**
* Create a new zone (subdomain)
*/
/**
* Create a DNS zone and primary records
*
* @return array{success: bool, recordType?: string, wildcardCreated?: bool, message?: string}
*/
public function createZone(string $subdomain, string $ipAddress, int $serverId, int $ipAddressId): array
{
try {
$fullDomain = $subdomain . '.dns.virtnet.host';
$comment = "Server ID: {$serverId}";
// Determine record type based on IP address version
$isIpv6 = filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
$recordType = $isIpv6 ? 'AAAA' : 'A';
// Create the zone
$response = $this->request('/zones/set', 'post', [
'zone' => $fullDomain,
'type' => 'Primary',
]);
$json = $response->json();
if ($json['status'] !== 'ok') {
return [
'success' => false,
'message' => 'Failed to create DNS zone.',
];
}
// Add A or AAAA record for the subdomain based on IP version
$wildcardDomain = '*.' . $fullDomain;
$mainRecordComment = $comment . " | Wildcard: {$wildcardDomain} ({$recordType})";
$recordResponse = $this->request('/zones/records/add', 'post', [
'zone' => $fullDomain,
'domain' => $fullDomain,
'type' => $recordType,
'ipAddress' => $ipAddress,
'ttl' => 3600,
'comments' => $mainRecordComment,
]);
$recordJson = $recordResponse->json();
if ($recordJson['status'] !== 'ok') {
// Clean up the zone we just created since the record failed
try {
$this->request('/zones/delete', 'post', ['zone' => $fullDomain]);
} catch (\Exception $cleanupException) {
// Ignore cleanup errors
}
return [
'success' => false,
'message' => 'Failed to add DNS record.',
];
}
// Add wildcard record (*.subdomain.dns.virtnet.host)
$wildcardComment = $comment . " | Parent: {$fullDomain} ({$recordType})";
$wildcardResponse = $this->request('/zones/records/add', 'post', [
'zone' => $fullDomain,
'domain' => $wildcardDomain,
'type' => $recordType,
'ipAddress' => $ipAddress,
'ttl' => 3600,
'comments' => $wildcardComment,
]);
// Continue even if wildcard creation fails (non-critical)
$wildcardCreated = false;
if (isset($wildcardResponse)) {
$wildcardJson = $wildcardResponse->json();
$wildcardCreated = ($wildcardJson['status'] ?? '') === 'ok';
}
return [
'success' => true,
'recordType' => $recordType,
'wildcardCreated' => $wildcardCreated,
];
} catch (\Exception $e) {
return [
'success' => false,
'message' => $e->getMessage(),
];
}
}
/**
* Delete a zone (subdomain)
*/
public function deleteZone(string $subdomain, int $ipAddressId): bool
{
try {
$fullDomain = $subdomain . '.dns.virtnet.host';
// Get the IP address to retrieve stored records
$ip = IPAddress::find($ipAddressId);
if (!$ip) {
return false;
}
// Get stored records from database
$storedRecords = $ip->subdomain['records'] ?? [];
// First, try to delete all records manually
// This handles the case where the zone doesn't exist but records do
foreach ($storedRecords as $record) {
if (isset($record['name']) && isset($record['type'])) {
try {
$ipAddress = $record['rdata'] ?? $record['ipAddress'] ?? null;
$this->deleteRecord($subdomain, $record['name'], $record['type'], $ipAddress);
} catch (\Exception $e) {
// Continue even if individual record deletion fails
}
}
}
// Also try to get records from the API and delete them
// This handles cases where records exist in DNS but not in our database
try {
$zoneDetails = $this->getZone($fullDomain);
if ($zoneDetails && isset($zoneDetails['records'])) {
foreach ($zoneDetails['records'] as $record) {
if (in_array($record['type'] ?? '', ['A', 'AAAA'])) {
$domain = $record['domain'] ?? '';
if ($domain === $fullDomain) {
$name = '@';
} else {
$name = str_replace('.' . $fullDomain, '', $domain);
}
try {
$ipAddress = $record['rdata'] ?? $record['ipAddress'] ?? null;
$this->deleteRecord($subdomain, $name, $record['type'], $ipAddress);
} catch (\Exception $e) {
// Continue even if individual record deletion fails
}
}
}
}
} catch (\Exception $e) {
// Zone might not exist, which is fine
}
// Now try to delete the zone (if it exists)
try {
$response = $this->request('/zones/delete', 'post', [
'zone' => $fullDomain,
]);
$json = $response->json();
// If zone doesn't exist, that's fine - we've already deleted the records
if ($json['status'] !== 'ok') {
$errorMessage = strtolower($json['message'] ?? '');
if (str_contains($errorMessage, 'no such zone') || str_contains($errorMessage, 'zone not found')) {
// Zone doesn't exist, but that's okay - records are deleted
} else {
// Some other error occurred, but we'll still clean up the database
}
}
} catch (\Exception $e) {
// Zone deletion failed, but we've deleted records, so continue
}
// Remove subdomain info from database
$ip->subdomain = null;
$ip->save();
return true;
} catch (\Exception $e) {
// Even if there's an error, try to clean up the database
try {
$ip = IPAddress::find($ipAddressId);
if ($ip) {
$ip->subdomain = null;
$ip->save();
}
} catch (\Exception $dbException) {
// Ignore database cleanup errors
}
return false;
}
}
/**
* Get zones associated with a server (from database)
*/
public function getServerZones(int $serverId): array
{
$serverZones = [];
// Get all IP addresses for this server that have subdomains
$ipAddresses = IPAddress::where('server_id', $serverId)
->whereNotNull('subdomain')
->get();
foreach ($ipAddresses as $ip) {
$subdomainData = $ip->subdomain;
if (isset($subdomainData['subdomain'])) {
$subdomain = $subdomainData['subdomain'];
$fullDomain = $subdomain . '.dns.virtnet.host';
// Get records from DNS API
$zoneRecords = $this->getZoneRecords($subdomain);
$serverZones[] = [
'subdomain' => $subdomain,
'fullDomain' => $fullDomain,
'ipAddress' => $ip->ip,
'ipAddressId' => $ip->id,
'records' => $subdomainData['records'] ?? [],
'zoneRecords' => $zoneRecords,
];
}
}
return $serverZones;
}
/**
* Get the IP address from zone records
*/
private function getZoneIpAddress(array $zoneDetails): ?string
{
$records = $zoneDetails['records'] ?? [];
foreach ($records as $record) {
if (isset($record['type']) && in_array($record['type'], ['A', 'AAAA'])) {
// Check both rdata and ipAddress fields
$ipAddress = $record['rdata'] ?? $record['ipAddress'] ?? null;
if ($ipAddress) {
return $ipAddress;
}
}
}
return null;
}
/**
* Add a DNS record to a zone
*/
public function addRecord(string $subdomain, string $name, string $type, string $rdata, int $ttl = 3600): bool
{
try {
$fullDomain = $subdomain . '.dns.virtnet.host';
$recordName = $name === '@' ? $fullDomain : $name . '.' . $fullDomain;
$response = $this->request('/zones/records/add', 'post', [
'zone' => $fullDomain,
'domain' => $recordName,
'type' => $type,
'ipAddress' => $rdata,
'ttl' => $ttl,
]);
$json = $response->json();
return $json['status'] === 'ok';
} catch (\Exception $e) {
return false;
}
}
/**
* Delete a DNS record from a zone
*/
public function deleteRecord(string $subdomain, string $name, string $type, ?string $ipAddress = null): bool
{
try {
$fullDomain = $subdomain . '.dns.virtnet.host';
$recordName = $name === '@' ? $fullDomain : $name . '.' . $fullDomain;
$params = [
'zone' => $fullDomain,
'domain' => $recordName,
'type' => $type,
];
// Add ipAddress parameter if provided (required for A/AAAA records)
if ($ipAddress !== null) {
$params['ipAddress'] = $ipAddress;
}
$response = $this->request('/zones/records/delete', 'post', $params);
$json = $response->json();
return $json['status'] === 'ok';
} catch (\Exception $e) {
return false;
}
}
/**
* Get all records for a zone
*/
public function getZoneRecords(string $subdomain): array
{
try {
$fullDomain = $subdomain . '.dns.virtnet.host';
$zoneDetails = $this->getZone($fullDomain);
if (!$zoneDetails) {
return [];
}
$records = $zoneDetails['records'] ?? [];
$formattedRecords = [];
foreach ($records as $record) {
if (!in_array($record['type'] ?? '', ['A', 'AAAA'])) {
continue;
}
$domain = $record['domain'] ?? '';
// Extract sub-subdomain name
if ($domain === $fullDomain) {
$name = '@';
} else {
$name = str_replace('.' . $fullDomain, '', $domain);
}
// Get IP address from either 'rdata' or 'ipAddress' field
$ipAddress = $record['rdata'] ?? $record['ipAddress'] ?? '';
$formattedRecords[] = [
'name' => $name,
'type' => $record['type'] ?? '',
'rdata' => $ipAddress,
'ttl' => $record['ttl'] ?? 3600,
];
}
return $formattedRecords;
} catch (\Exception $e) {
return [];
}
}
}