Skip to content

Commit d5f0b8d

Browse files
committed
v1.3
1 parent f5382e9 commit d5f0b8d

11 files changed

Lines changed: 230 additions & 53 deletions

File tree

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ API_URL="dns.domain.tld:5380"
22
USERNAME="api_user"
33
PASSWORD="your-password"
44
TOKEN=""
5+
BEARER_AUTH=true
56
INCLUDE_INFO=false
67
USE_POST=true
78
USE_HTTPS=true

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Then: `require_once "/path/to/vendor/autoload.php";` & `use Technitium\DNSServer
2626
- `PASSWORD`: The password for the user account.
2727
- `INCLUDE_INFO`: Returns basic information that might be relevant for the queried request.
2828
- `TOKEN`: Your API token, if already existent. (`[Your Username]` > `[Create API Token]`). If left empty, the API will use the username and password for authentication to create an API token for you and will write it to your `.env`.
29+
- `BEARER_AUTH`: Set to `true` to use Bearer Authentication as required as of v15 as stated in Technitium API Docs. If set to `false`, the client will append the token as a query parameter.
2930
- `USE_POST`: Specify if you want to access the API via POST (`true`) instead of GET (`false`) in default.
3031
- `USE_HTTPS`: Enable (`true`) HTTPS for the API connection. If your server does not support HTTPS, the API will simply return `false` to all requests.
3132

@@ -127,6 +128,13 @@ DDNS(new API(__DIR__ . "/configurations", ".env-custom"), file_get_contents("/my
127128

128129
## Changes
129130

131+
### 1.3: Alignment with API changes
132+
133+
- Adjusted API calls to align with changes in the Technitium DNS Server API as of May 2026
134+
- The client does not yet support all clustering related API changes (aswell as the life-time metrics).
135+
- Fixed incorrect endpoint URL for `get` endpoint in `Stats.dashboard.php` class
136+
- Added support for Bearer Authentication as required as of v15 as stated in Technitium API Docs.
137+
130138
### v1.2.1: Fixed and logs/export support
131139

132140
- Added support for `logs/export` allowing you to export a DNS application log file

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "ente/technitium-dnsserver-php-api",
33
"type": "library",
44
"license": "GPL-3.0-only",
5-
"version": "1.2.1",
5+
"version": "1.3",
66
"description": "API client to interact with the Technitium DNS Server",
77
"authors": [
88
{

src/API.dnsserver.ente.php

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
use Technitium\DNSServer\API\zones;
1414
use Technitium\DNSServer\API\Helper\DDNS;
1515
use Technitium\DNSServer\API\Helper\Log;
16+
use Technitium\DNSServer\API\sso;
1617
use \Dotenv\Dotenv;
1718
use MirazMac\DotEnv\Writer;
1819

1920
class API {
2021

21-
private string $protocol;
22+
private string $protocol = "http";
2223
private $admin;
2324
private $allowed;
2425
private $apps;
@@ -32,6 +33,7 @@ class API {
3233
private $zones;
3334
private $ddns;
3435
private $log;
36+
private $sso;
3537
private string $conf;
3638
private string $path;
3739
private string $fullPath;
@@ -46,11 +48,16 @@ public function __construct(string $confPath, ?string $name = null){
4648
}
4749

4850
private function setProtocol(): void{
49-
if($this->env["USE_HTTPS"] == "true"){
50-
$this->protocol = "https";
51-
} else {
52-
$this->protocol = "http";
51+
$apiUrl = (string)($this->env["API_URL"] ?? "");
52+
$urlProtocol = parse_url($apiUrl, PHP_URL_SCHEME);
53+
54+
if($urlProtocol === "http" || $urlProtocol === "https"){
55+
$this->protocol = $urlProtocol;
56+
$this->env["API_URL"] = preg_replace('#^https?://#i', '', $apiUrl);
57+
return;
5358
}
59+
60+
$this->protocol = $this->isTruthy($this->env["USE_HTTPS"] ?? false) ? "https" : "http";
5461
}
5562

5663
/**
@@ -171,41 +178,67 @@ public function downloadFile($endpoint, $bypass, $data, $csv = false): string {
171178
*/
172179
public function sendCall(array $data, string $endpoint, string $method = "POST", bool $skip = false, bool $bypass = false): array{
173180
$c = curl_init();
181+
$requestedEndpoint = $endpoint;
174182
$endpoint = $this->prepareEndpoint($endpoint, $bypass);
175-
if($this->env["USE_POST"]){
183+
if($endpoint === false){
184+
return ["status" => "error", "error" => "Endpoint not implemented: " . $requestedEndpoint];
185+
}
186+
if($this->isTruthy($this->env["USE_POST"] ?? false)){
176187
$method = "POST";
177188
}
189+
190+
$useBearerAuth = $this->isTruthy($this->env["BEARER_AUTH"] ?? false);
191+
178192
switch($method){
179193
case "POST":
180-
curl_setopt($c, CURLOPT_URL, $endpoint . $this->appendAuth($method,$skip));
194+
if($useBearerAuth){
195+
curl_setopt($c, CURLOPT_URL, $endpoint);
196+
$headers = ["Authorization: Bearer " . @$this->env["TOKEN"]];
197+
curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
198+
} else {
199+
curl_setopt($c, CURLOPT_URL, $endpoint . $this->appendAuth($method,$skip));
200+
}
181201
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
182202
curl_setopt($c, CURLOPT_POST, true);
183203
curl_setopt($c, CURLOPT_POSTFIELDS, $data);
184204
break;
185205
case "GET":
186206
$data = http_build_query($data);
187-
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data . $this->appendAuth($method, $skip));
207+
if($useBearerAuth){
208+
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data);
209+
$headers = ["Authorization: Bearer " . @$this->env["TOKEN"]];
210+
curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
211+
} else {
212+
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data . $this->appendAuth($method, $skip));
213+
}
188214
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
189215
break;
190216
default:
191217
$data = http_build_query($data);
192-
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data . $this->appendAuth($method, $skip));
218+
if($useBearerAuth){
219+
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data);
220+
$headers = ["Authorization: Bearer " . @$this->env["TOKEN"]];
221+
curl_setopt($c, CURLOPT_HTTPHEADER, $headers);
222+
} else {
223+
curl_setopt($c, CURLOPT_URL, $endpoint . "?" . $data . $this->appendAuth($method, $skip));
224+
}
193225
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
194226
break;
195227
}
196228
$result = ["status" => "error", "error" => "Unknown error"];
229+
$response = null;
197230
try {
198231
$response = curl_exec($c);
199232
if(!$response){
200-
Log::error_rep("Failed to send request: " . curl_error($c));
233+
Log::error_rep("Failed to send request to " . curl_getinfo($c, CURLINFO_EFFECTIVE_URL) . ": " . curl_error($c));
201234
$result = ["status" => "error", "error" => curl_error($c)];
202235
}
203236
} catch (\Throwable $e){
204-
Log::error_rep("Failed to send request: " . $e->getMessage());
237+
Log::error_rep("Failed to send request to " . curl_getinfo($c, CURLINFO_EFFECTIVE_URL) . ": " . $e->getMessage());
205238
$result = ["status" => "error", "error" => $e->getMessage()];
206239
}
207240
curl_close($c);
208-
if($this->checkResponse($response)){
241+
if($response !== null && $this->checkResponse($response)){
209242
Log::error_rep("Successfully accessed endpoint: " . $endpoint);
210243
$result = json_decode($response, true);
211244
}
@@ -220,6 +253,7 @@ public function sendCall(array $data, string $endpoint, string $method = "POST",
220253
*/
221254
private function appendAuth(string $m = "POST", bool $skip = false): string{
222255
$this->loadConf($this->path, $this->conf);
256+
$this->setProtocol();
223257
$authAppend = null;
224258
if($skip){
225259
return "";
@@ -251,7 +285,7 @@ private function appendAuth(string $m = "POST", bool $skip = false): string{
251285
break;
252286
}
253287
}
254-
return $authAppend;
288+
return $authAppend ?? "";
255289
}
256290

257291
/**
@@ -266,6 +300,11 @@ private function getPermanentToken(): bool{
266300
"pass" => $this->env["PASSWORD"],
267301
"tokenName" => "technitium-dnsserver-php-api"
268302
], "user/createToken", "POST", true);
303+
if(!isset($response["token"])){
304+
Log::error_rep("Unable to get permanent token: " . ($response["error"] ?? "missing token in response"));
305+
return false;
306+
}
307+
269308
$writer = new Writer($this->fullPath);
270309
try {
271310
$writer
@@ -290,13 +329,17 @@ private function getPermanentToken(): bool{
290329
* @param string $response The response returned by `API::sendCall()` function.
291330
* @return bool Returns `true` if the response is valid, otherwise `false`.
292331
*/
293-
private function checkResponse(string $response): bool{
294-
if(is_null($response)){
332+
private function checkResponse(?string $response): bool{
333+
if(empty($response)){
334+
return false;
335+
}
336+
337+
$decoded = json_decode($response, true);
338+
if(!is_array($decoded) || !isset($decoded["status"])){
295339
return false;
296-
} else {
297-
$re = json_decode($response, true)["status"];
298-
return $re != null || $re !== "error" || $re !== "invalid-token";
299340
}
341+
342+
return $decoded["status"] !== "error" && $decoded["status"] !== "invalid-token" && $decoded["status"] !== "2fa-required";
300343
}
301344

302345
/**
@@ -306,18 +349,25 @@ private function checkResponse(string $response): bool{
306349
* @return bool|string Returns the URI as string or `false` if the endpoint is not implemented.
307350
*/
308351
private function prepareEndpoint(string $endpoint, bool $bypass = false): bool|string{
352+
$this->setProtocol();
353+
$apiUrl = (string)($this->env["API_URL"] ?? "");
354+
309355
if($bypass){
310-
return $this->protocol . "://" . $this->env["API_URL"] . "/api/" . $endpoint;
356+
return $this->protocol . "://" . $apiUrl . "/api/" . $endpoint;
311357
}
312358
$endpoints = json_decode(file_get_contents(__DIR__ . "/helper/endpoints.json"));
313359

314360
if(in_array($endpoint, $endpoints)){
315-
return $this->protocol . "://" . $this->env["API_URL"] . "/api/" . $endpoint;
361+
return ($this->protocol ?: "http") . "://" . $apiUrl . "/api/" . $endpoint;
316362
} else {
317363
return false;
318364
}
319365
}
320366

367+
private function isTruthy(mixed $value): bool{
368+
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
369+
}
370+
321371

322372
public function admin(): admin {
323373
if(!$this->admin) $this->admin = new admin($this);
@@ -382,7 +432,12 @@ public function ddns(): DDNS {
382432
}
383433

384434
public function log(): Log {
385-
if(!$this->log) $this->log = new Log(null);
435+
if(!$this->log) $this->log = new Log("Initialization of API log helper");
386436
return $this->log;
387437
}
388-
}
438+
439+
public function sso(): sso {
440+
if(!$this->sso) $this->sso = new sso($this);
441+
return $this->sso;
442+
}
443+
}

src/endpoints/dashboard/Stats.dashboard.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ public function __construct(\Technitium\DNSServer\API\API $api){
1414
* @param string $utc True to return main chart with data labels in UTC date time format
1515
* @param string $start start date for `custom` type. ISO 8601 format.
1616
* @param string $end end date for `custom` type. ISO 8601 format.
17+
* @param bool $dontTrimQueryTypeData (optional): Set to `true` to get full data for query type chart instead of top 10 entries. Default value is `false` when unspecified.
1718
* @return array|bool Returns an result array or `false` otherwise.
1819
*/
19-
public function get(string $type = "LastHour", bool $utc = true, string $start = "", string $end = ""): array|bool {
20+
public function get(string $type = "LastHour", bool $utc = true, string $start = "", string $end = "", bool $dontTrimQueryTypeData = false): array|bool {
2021
if($utc){$utc="true";}else{$utc="false";};
21-
$response = $this->API->sendCall(["type" => $type, "utc" => $utc, "start" => $start, "end" => $end], "cache/list");
22+
$response = $this->API->sendCall(["type" => $type, "utc" => $utc, "start" => $start, "end" => $end, "dontTrimQueryTypeData" => $dontTrimQueryTypeData], "stats/get");
2223
if($response["status"] == "ok"){
2324
return $response["response"];
2425
} else {

src/endpoints/sso/SSO.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
namespace Technitium\DNSServer\API;
3+
4+
class sso extends API {
5+
public $API;
6+
7+
public function __construct(\Technitium\DNSServer\API\API $api){
8+
$this->API = $api;
9+
}
10+
11+
/**
12+
* `status()` - Returns the SSO status of the server.
13+
* @return array|bool Returns the SSO status or `false` if the request failed.
14+
*/
15+
public function status(): array|bool {
16+
$response = $this->API->sendCall([], "sso/status");
17+
if($response["status"] == "ok"){
18+
return $response["response"];
19+
} else {
20+
return false;
21+
}
22+
}
23+
24+
/**
25+
* `getConfiguration()` - Returns the SSO configuration of the server.
26+
* @param bool $includeGroups Whether to include group information in the response.
27+
* @return array|bool Returns the SSO configuration or `false` if the request failed.
28+
*/
29+
public function getConfiguration(bool $includeGroups = false): array|bool {
30+
$response = $this->API->sendCall(["includeGroups" => $includeGroups ? "true" : "false"], "sso/get");
31+
if($response["status"] == "ok"){
32+
return $response["response"];
33+
} else {
34+
return false;
35+
}
36+
}
37+
38+
/**
39+
* `setConfiguration()` - Sets the SSO configuration of the server.
40+
* @param array $data The configuration data to set. Seee the API documentation for the list of configuration options.
41+
* @return array|bool Returns the updated SSO configuration or `false` if the request failed.
42+
*/
43+
public function setConfiguration(array $data): array|bool {
44+
$response = $this->API->sendCall($data, "sso/set");
45+
if($response["status"] == "ok"){
46+
return $response["response"];
47+
} else {
48+
return false;
49+
}
50+
}
51+
52+
/**
53+
* `createUser()` - Creates a new SSO user.
54+
* @param string $username The username of the new user.
55+
* @param string $displayName The display name of the new user.
56+
* @param string $ssoIdentifier The SSO identifier for the new user. This should match the identifier provided in the SSO token by the Identity Provider.
57+
* @param array|null $groups An array of group names to assign to the user. This is optional and can be set later.
58+
* @return bool Returns `true` if the user was created successfully, `false` otherwise.
59+
*/
60+
public function createUser(string $username, string $displayName, string $ssoIdentifier, ?array $groups): bool {
61+
$data = [
62+
"username" => $username,
63+
"displayName" => $displayName,
64+
"ssoIdentifier" => $ssoIdentifier
65+
];
66+
if ($groups) {
67+
$data["groups"] = implode(",", $groups);
68+
}
69+
$response = $this->API->sendCall($data, "sso/users/create");
70+
return $response["status"] == "ok";
71+
}
72+
73+
/**
74+
* `updateUser()` - Updates an existing SSO user.
75+
* @param string $username The username of the user to update.
76+
* @param string|null $newUsername The new username for the user. This is optional.
77+
* @param string|null $displayName The new display name for the user. This is optional.
78+
* @param array|null $groups An array of group names to assign to the user. This is optional.
79+
* @return bool Returns `true` if the user was updated successfully, `false` otherwise.
80+
*/
81+
public function updateUser(string $username, ?string $newUsername, ?string $displayName, ?array $groups): bool {
82+
$data = ["username" => $username];
83+
if ($newUsername) {
84+
$data["newUsername"] = $newUsername;
85+
}
86+
if ($displayName) {
87+
$data["displayName"] = $displayName;
88+
}
89+
if ($groups) {
90+
$data["groups"] = implode(",", $groups);
91+
}
92+
$response = $this->API->sendCall($data, "sso/users/set");
93+
return $response["status"] == "ok";
94+
}
95+
}

0 commit comments

Comments
 (0)