baseUrl = 'https://dns.virtnet.bond'; $this->token = 'fab3cb86382687472062c22b37aa5789bebba1dbe556017a7234a295aa5b586d'; } /** * 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::get($url, $params); } else { return Http::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 false; } // 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') { return false; } // 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 []; } } }