Skip to content

Commit

Permalink
Hetzner DNS fully working
Browse files Browse the repository at this point in the history
  • Loading branch information
getpinga committed Feb 14, 2024
1 parent 3dd0ab1 commit 37be2b8
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 82 deletions.
206 changes: 129 additions & 77 deletions Servicedns/Providers/Hetzner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,28 @@

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use SleekDB\Store;
use PDO;

class Hetzner implements DnsHostingProviderInterface {
private $baseUrl = "https://dns.hetzner.com/api/v1/";
private $client;
private $headers;
private $store;
private $dbConfig;
private $pdo;

public function __construct($config) {
// Load DB configuration
$dbc = include __DIR__ . '/../../../config.php';
$this->dbConfig = $dbc['db'];

try {
$dsn = $this->dbConfig["type"] . ":host=" . $this->dbConfig["host"] . ";port=" . $this->dbConfig["port"] . ";dbname=" . $this->dbConfig["name"];
$this->pdo = new PDO($dsn, $this->dbConfig['user'], $this->dbConfig['password']);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Connection failed: " . $e->getMessage());
}

$token = $config['apikey'];
if (empty($token)) {
throw new \FOSSBilling\Exception("API token cannot be empty");
Expand All @@ -23,9 +36,6 @@ public function __construct($config) {
'Auth-API-Token' => $token,
'Content-Type' => 'application/json',
];

$dataDir = __DIR__ . '/../../../data/upload';
$this->store = new Store('hetzner.dns', $dataDir);
}

public function createDomain($domainName) {
Expand All @@ -41,16 +51,19 @@ public function createDomain($domainName) {

$body = json_decode($response->getBody()->getContents(), true);
$zoneId = $body['zone']['id'] ?? null;

$existing = $this->store->findOneBy(["domainName", "=", $domainName]);
if (!empty($existing)) {
$this->store->updateBy(["domainName", "=", $domainName], ["zoneId" => $zoneId]);
} else {
$this->store->insert([
'domainName' => $domainName,
'zoneId' => $zoneId,
"dnsRecords" => [],
]);

try {
$sql = "UPDATE service_dns SET zoneId = :zoneId WHERE domain_name = :domainName";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':zoneId', $zoneId, PDO::PARAM_STR);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() === 0) {
throw new \FOSSBilling\Exception("No DB update made. Check if the domain name exists.");
}
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error updating zoneId: " . $e->getMessage());
}

return json_decode($response->getBody(), true);
Expand Down Expand Up @@ -80,11 +93,22 @@ public function deleteDomain($domainName) {
throw new \FOSSBilling\Exception("Domain name cannot be empty");
}

$result = $this->store->findOneBy(['domainName', '=', $domainName]);
if ($result !== null) {
$zoneId = $result['zoneId'];
try {
$sql = "SELECT zoneId FROM service_dns WHERE domain_name = :domainName LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$zoneId = $row['zoneId'];
} else {
throw new \FOSSBilling\Exception("Domain name does not exist.");
}
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error fetching zoneId: " . $e->getMessage());
}

try {
$response = $this->client->request('DELETE', "zones/{$zoneId}", [
'headers' => $this->headers,
Expand All @@ -105,9 +129,20 @@ public function createRRset($domainName, $rrsetData) {
throw new \FOSSBilling\Exception("Domain name cannot be empty");
}

$result = $this->store->findOneBy(['domainName', '=', $domainName]);
if ($result !== null) {
$zoneId = $result['zoneId'];
try {
$sql = "SELECT zoneId FROM service_dns WHERE domain_name = :domainName LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$zoneId = $row['zoneId'];
} else {
throw new \FOSSBilling\Exception("Domain name does not exist.");
}
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error fetching zoneId: " . $e->getMessage());
}

try {
Expand All @@ -126,33 +161,40 @@ public function createRRset($domainName, $rrsetData) {
$body = json_decode($response->getBody()->getContents(), true);
$recordId = $body['record']['id'] ?? null;

// Check if the DNS record exists in the 'dnsRecords' array
$key = array_search($recordId, array_column($result['dnsRecords'], 'recordId'));

$dnsRecord = [
'recordId' => $recordId,
'recordType' => $rrsetData['type'],
'recordName' => $rrsetData['subname'],
];

$update = ['dnsRecords' => $result['dnsRecords'] ?? []];
$key = array_search($recordId, array_column($update['dnsRecords'], 'recordId'));

if ($key !== false) {
$update['dnsRecords'][$key] = $dnsRecord;
$sql = "SELECT id FROM service_dns WHERE domain_name = :domainName LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$domainId = $row['id'];
} else {
$update['dnsRecords'][] = $dnsRecord;
throw new \FOSSBilling\Exception("Domain name does not exist.");
}

$sql = "UPDATE service_dns_records SET recordId = :recordId WHERE type = :type AND host = :subname AND value = :value AND domain_id = :domain_id";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':recordId', $recordId, PDO::PARAM_STR);
$stmt->bindParam(':type', $rrsetData['type'], PDO::PARAM_STR);
$stmt->bindParam(':subname', $rrsetData['subname'], PDO::PARAM_STR);
$stmt->bindParam(':value', $rrsetData['records'][0], PDO::PARAM_STR);
$stmt->bindParam(':domain_id', $domainId, PDO::PARAM_INT);
$stmt->execute();

if ($stmt->rowCount() === 0) {
throw new \FOSSBilling\Exception("No DB update made. Check if the domain name exists.");
}

$this->store->updateById($result['_id'], ['dnsRecords' => $update['dnsRecords']]);
return true;
} else {
return false;
}
} catch (GuzzleException $e) {
throw new \FOSSBilling\Exception('Request failed: ' . $e->getMessage());
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error updating zoneId: " . $e->getMessage());
}

}

public function createBulkRRsets($domainName, $rrsetDataArray) {
Expand All @@ -173,23 +215,31 @@ public function modifyRRset($domainName, $subname, $type, $rrsetData) {
}

try {
$result = $this->store->findOneBy(['domainName', '=', $domainName]);
if (!$result) {
throw new \FOSSBilling\Exception('Domain not found.');
$sql = "SELECT id, zoneId FROM service_dns WHERE domain_name = :domainName LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$zoneId = $row['zoneId'];
$domainId = $row['id'];
} else {
throw new \FOSSBilling\Exception("Domain name does not exist.");
}
$zoneId = $result['zoneId'];

$recordId = null;
$sql = "SELECT recordId FROM service_dns_records WHERE type = :type AND host = :subname AND domain_id = :domain_id LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':type', $type, PDO::PARAM_STR);
$stmt->bindParam(':subname', $subname, PDO::PARAM_STR);
$stmt->bindParam(':domain_id', $domainId, PDO::PARAM_INT);
$stmt->execute();

foreach ($result['dnsRecords'] as $record) {
if ($record['recordType'] === $type && $record['recordName'] === $subname) {
$recordId = $record['recordId'];
break;
}
}

if ($recordId === null) {
throw new \FOSSBilling\Exception('DNS record not found.');
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$recordId = $row['recordId'];
} else {
throw new \FOSSBilling\Exception("Record not found for the given type and subname.");
}

$response = $this->client->request('PUT', "records/{$recordId}", [
Expand All @@ -210,8 +260,9 @@ public function modifyRRset($domainName, $subname, $type, $rrsetData) {
}
} catch (GuzzleException $e) {
throw new \FOSSBilling\Exception('Request failed: ' . $e->getMessage());
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error in operation: " . $e->getMessage());
}

}

public function modifyBulkRRsets($domainName, $rrsetDataArray) {
Expand All @@ -220,46 +271,47 @@ public function modifyBulkRRsets($domainName, $rrsetDataArray) {

public function deleteRRset($domainName, $subname, $type, $value) {
try {
$result = $this->store->findOneBy(['domainName', '=', $domainName]);
if (!$result) {
throw new \FOSSBilling\Exception('Domain not found.');
$sql = "SELECT id, zoneId FROM service_dns WHERE domain_name = :domainName LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':domainName', $domainName, PDO::PARAM_STR);
$stmt->execute();

if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$zoneId = $row['zoneId'];
$domainId = $row['id'];
} else {
throw new \FOSSBilling\Exception("Domain name does not exist.");
}
$zoneId = $result['zoneId'];

$recordId = null;
$sql = "SELECT recordId FROM service_dns_records WHERE type = :type AND host = :subname AND domain_id = :domain_id LIMIT 1";
$stmt = $this->pdo->prepare($sql);
$stmt->bindParam(':type', $type, PDO::PARAM_STR);
$stmt->bindParam(':subname', $subname, PDO::PARAM_STR);
$stmt->bindParam(':domain_id', $domainId, PDO::PARAM_INT);
$stmt->execute();

foreach ($result['dnsRecords'] as $record) {
if ($record['recordType'] === $type && $record['recordName'] === $subname) {
$recordId = $record['recordId'];
break;
}
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$recordId = $row['recordId'];
} else {
throw new \FOSSBilling\Exception("Record not found for the given type and subname.");
}

if ($recordId === null) {
throw new \FOSSBilling\Exception('DNS record not found.');
}

$response = $this->client->request('DELETE', "records/{$recordId}", [
'headers' => $this->headers,
]);

$filteredRecords = array_filter($result['dnsRecords'], function ($record) use ($recordId) {
return $record['recordId'] !== $recordId;
});

$result['dnsRecords'] = array_values($filteredRecords);

$this->store->updateById($result['_id'], $result);

if ($response->getStatusCode() === 204) {
return true;
} else {
return false;
}
} catch (GuzzleException $e) {
throw new \FOSSBilling\Exception('Request failed: ' . $e->getMessage());
} catch (\PDOException $e) {
throw new \FOSSBilling\Exception("Error in operation: " . $e->getMessage());
}

}

public function deleteBulkRRsets($domainName, $rrsetDataArray) {
Expand Down
3 changes: 1 addition & 2 deletions Servicedns/Providers/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"guzzlehttp/guzzle": "^7.8",
"vultr/vultr-php": "^1.0",
"badcow/dns": "^4.2",
"exonet/powerdns-php": "^4.5",
"rakibtg/sleekdb": "^2.15"
"exonet/powerdns-php": "^4.5"
},
"config": {
"allow-plugins": {
Expand Down
5 changes: 4 additions & 1 deletion Servicedns/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ public function addRecord(array $data): bool
throw new \FOSSBilling\Exception("DNS provider is not set.");
}

$this->dnsProvider->createRRset($config['domain_name'], $rrsetData);
$model->updated_at = date('Y-m-d H:i:s');
$this->di['db']->store($model);

Expand All @@ -248,6 +247,8 @@ public function addRecord(array $data): bool
$records->updated_at = date('Y-m-d H:i:s');
$this->di['db']->store($records);

$this->dnsProvider->createRRset($config['domain_name'], $rrsetData);

return true;
}

Expand Down Expand Up @@ -375,6 +376,7 @@ public function install(): bool
`client_id` bigint(20) NOT NULL,
`domain_name` varchar(75),
`provider_id` varchar(11),
`zoneId` varchar(100) DEFAULT NULL,
`config` text NOT NULL,
`created_at` datetime,
`updated_at` datetime,
Expand All @@ -383,6 +385,7 @@ public function install(): bool
CREATE TABLE IF NOT EXISTS `service_dns_records` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`domain_id` bigint(20) NOT NULL,
`recordId` varchar(100) DEFAULT NULL,
`type` varchar(10) NOT NULL,
`host` varchar(255) NOT NULL,
`value` text NOT NULL,
Expand Down
6 changes: 4 additions & 2 deletions Servicedns/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
"name": "DNS Hosting Product",
"description": "A product type to allow you to offer DNS hosting",
"icon_url": "icon.svg",
"version": "1.0.0"
}
"version": "1.0.0",
"author": "Namingo",
"author_url": "https://namingo.org/"
}

0 comments on commit 37be2b8

Please sign in to comment.