mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-07-21 21:52:53 +00:00
feat(desec): add deSEC account + domain registration backend
Add the server-side deSEC (dedyn.io) free dynamic-DNS flow so users without a domain can obtain one from the AIO interface. - DesecManager drives the full flow against the deSEC API, matching its real semantics: account creation returns HTTP 202 + email verification (no token), a token is obtained via /auth/login/ only after the email is verified, domain registration handles 201/409, and a wildcard CNAME rrset is created for new accounts. Existing accounts can be used by supplying a password. - The "awaiting verification" step is derived from the stored credentials (email + generated password but no token and no domain yet), not a separate flag. register() returns false for that state so the controller can re-render the awaiting-verification UI instead of surfacing it as an error. - DesecController exposes POST /api/desec/register; DependencyInjection wires the manager; account credentials (email, generated password, token) are stored in the AIO configuration and DESEC_TOKEN is exposed to the caddy container. - The dynamic-DNS record is refreshed with the current public IP on container start (DockerController) and via the cron path (Cron/UpdateDesecIp, cron.sh). - Fix an undefined-variable bug in the desecToken/desecPassword config setters that prevented credentials from being persisted. Co-Authored-By: szaimen <42591237+szaimen@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Simon L. <szaimen@e.mail.de>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace AIO\Controller;
|
||||
|
||||
use AIO\Desec\DesecManager;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
readonly class DesecController {
|
||||
public function __construct(
|
||||
private DesecManager $desecManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function Register(Request $request, Response $response, array $args): Response {
|
||||
try {
|
||||
$email = (string)($request->getParsedBody()['desec_email'] ?? '');
|
||||
$slug = (string)($request->getParsedBody()['desec_slug'] ?? '');
|
||||
$password = (string)($request->getParsedBody()['desec_password'] ?? '');
|
||||
// register() returns false when a new account was created and we are now awaiting
|
||||
// the user's email verification. That is a normal state transition, not an error:
|
||||
// reload the page (201 + Location) so the awaiting-verification UI renders and
|
||||
// explains the next step, exactly like the fully-registered success path.
|
||||
$this->desecManager->register($email, $slug, $password);
|
||||
return $response->withStatus(201)->withHeader('Location', '.');
|
||||
} catch (\Exception $ex) {
|
||||
$response->getBody()->write($ex->getMessage());
|
||||
return $response->withStatus(422);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace AIO\Controller;
|
||||
use AIO\Container\Container;
|
||||
use AIO\Container\ContainerState;
|
||||
use AIO\ContainerDefinitionFetcher;
|
||||
use AIO\Desec\DesecManager;
|
||||
use AIO\Docker\DockerActionManager;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
@@ -19,7 +20,8 @@ readonly class DockerController {
|
||||
public function __construct(
|
||||
private DockerActionManager $dockerActionManager,
|
||||
private ContainerDefinitionFetcher $containerDefinitionFetcher,
|
||||
private ConfigurationManager $configurationManager
|
||||
private ConfigurationManager $configurationManager,
|
||||
private DesecManager $desecManager,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -264,6 +266,9 @@ readonly class DockerController {
|
||||
// Stop domaincheck since apache would not be able to start otherwise
|
||||
$this->StopDomaincheckContainer();
|
||||
|
||||
// Refresh the deSEC DNS record with the current public IP before starting containers
|
||||
$this->desecManager->updateIpIfDesecDomain();
|
||||
|
||||
$id = self::TOP_CONTAINER;
|
||||
|
||||
$this->PerformRecursiveContainerStart($id, $pullImage, $addToStreamingResponseBody);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// increase memory limit to 2GB
|
||||
ini_set('memory_limit', '2048M');
|
||||
|
||||
// Log whole log messages
|
||||
ini_set('log_errors_max_len', '0');
|
||||
|
||||
require __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$container = \AIO\DependencyInjection::GetContainer();
|
||||
|
||||
/** @var \AIO\Desec\DesecManager $desecManager */
|
||||
$desecManager = $container->get(\AIO\Desec\DesecManager::class);
|
||||
|
||||
$desecManager->updateIpIfDesecDomain();
|
||||
@@ -11,6 +11,8 @@ use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
class ConfigurationManager
|
||||
{
|
||||
public const string DEDYN_SUFFIX = '.dedyn.io';
|
||||
|
||||
private array $secrets = [];
|
||||
|
||||
private array $config = [];
|
||||
@@ -206,6 +208,72 @@ class ConfigurationManager
|
||||
set { $this->set('turn_domain', $value); }
|
||||
}
|
||||
|
||||
public string $desecEmail {
|
||||
get => $this->get('desec_email', '');
|
||||
set { $this->set('desec_email', $value); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL of the deSEC API. Configurable via the 'desec_api_base' config key
|
||||
* (configuration.json) only — intentionally NOT an environment variable — so the
|
||||
* endpoint can be pointed at a mock during automated tests without any risk of a
|
||||
* stray env var redirecting it in production.
|
||||
*/
|
||||
public string $desecApiBase {
|
||||
get => $this->get('desec_api_base', 'https://desec.io/api/v1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Base URL of the deSEC dynamic-DNS update endpoint. Configurable via the
|
||||
* 'desec_update_url' config key (configuration.json) only — see desecApiBase.
|
||||
*/
|
||||
public string $desecUpdateUrl {
|
||||
get => $this->get('desec_update_url', 'https://update.dedyn.io/');
|
||||
}
|
||||
|
||||
public string $desecToken {
|
||||
get {
|
||||
$secrets = $this->get('secrets', []);
|
||||
return isset($secrets['DESEC_TOKEN']) && is_string($secrets['DESEC_TOKEN']) ? $secrets['DESEC_TOKEN'] : '';
|
||||
}
|
||||
set {
|
||||
$secrets = $this->get('secrets', []);
|
||||
$secrets['DESEC_TOKEN'] = $value;
|
||||
$this->set('secrets', $secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public string $desecPassword {
|
||||
get {
|
||||
$secrets = $this->get('secrets', []);
|
||||
return isset($secrets['DESEC_PASSWORD']) && is_string($secrets['DESEC_PASSWORD']) ? $secrets['DESEC_PASSWORD'] : '';
|
||||
}
|
||||
set {
|
||||
$secrets = $this->get('secrets', []);
|
||||
$secrets['DESEC_PASSWORD'] = $value;
|
||||
$this->set('secrets', $secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public function isDesecDomain(): bool {
|
||||
return str_ends_with($this->domain, self::DEDYN_SUFFIX) && $this->desecToken !== '';
|
||||
}
|
||||
|
||||
public function isDesecAccountRegistered(): bool {
|
||||
return $this->desecToken !== '' && $this->desecEmail !== '' && $this->domain === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a new deSEC account was created (email + generated password stored) but
|
||||
* its email has not been verified yet, so no API token could be obtained and no
|
||||
* domain is set. Derived from the stored credentials rather than a separate flag:
|
||||
* once verification succeeds a token is stored and isDesecAccountRegistered() takes
|
||||
* over; once a domain is set the deSEC setup is complete.
|
||||
*/
|
||||
public function isDesecAwaitingVerification(): bool {
|
||||
return $this->desecToken === '' && $this->desecEmail !== '' && $this->desecPassword !== '' && $this->domain === '';
|
||||
}
|
||||
|
||||
public string $apachePort {
|
||||
get => $this->getEnvironmentalVariableOrConfig('APACHE_PORT', 'apache_port', '443');
|
||||
set { $this->set('apache_port', $value); }
|
||||
@@ -1119,6 +1187,7 @@ class ConfigurationManager
|
||||
'CADDY_IP_ADDRESS' => in_array('caddy', $this->aioCommunityContainers, true) ? NetworkHelper::resolveHostname('nextcloud-aio-caddy') : '',
|
||||
'WHITEBOARD_ENABLED' => $this->isWhiteboardEnabled ? 'yes' : '',
|
||||
'AIO_VERSION' => $this->getAioVersion(),
|
||||
'DESEC_TOKEN' => $this->desecToken,
|
||||
default => $this->getRegisteredSecret($placeholder),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ class DependencyInjection
|
||||
$container->get(GitHubContainerRegistryManager::class)
|
||||
)
|
||||
);
|
||||
$container->set(
|
||||
\AIO\Desec\DesecManager::class,
|
||||
new \AIO\Desec\DesecManager(
|
||||
$container->get(\AIO\Data\ConfigurationManager::class),
|
||||
)
|
||||
);
|
||||
$container->set(
|
||||
\AIO\Auth\PasswordGenerator::class,
|
||||
new \AIO\Auth\PasswordGenerator()
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace AIO\Desec;
|
||||
|
||||
use AIO\Data\ConfigurationManager;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
class DesecManager {
|
||||
private const int MAX_SLUG_ATTEMPTS = 5;
|
||||
private const int SLUG_BYTES = 5; // bin2hex → 10-char slug
|
||||
private const string SLUG_PATTERN = '/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/';
|
||||
|
||||
private Client $guzzleClient;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigurationManager $configurationManager,
|
||||
) {
|
||||
$this->guzzleClient = new Client([
|
||||
'timeout' => 15,
|
||||
'connect_timeout' => 10,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full registration flow: validates inputs, creates an account if needed,
|
||||
* registers the domain, enables required containers, and updates the DNS record.
|
||||
*
|
||||
* When $password is non-empty the user is logging into an existing deSEC account
|
||||
* rather than creating a new one. When $password is empty a new account is created
|
||||
* with a randomly generated password (unless an account was already registered in a
|
||||
* previous attempt).
|
||||
*
|
||||
* @return bool true when the domain was fully registered; false when a new account was
|
||||
* just created and we are now awaiting the user's email verification (a normal,
|
||||
* non-error outcome — the awaiting-verification UI explains the next step).
|
||||
* @throws \Exception on any validation or API error
|
||||
*/
|
||||
public function register(string $email, string $slug, string $password = ''): bool {
|
||||
if ($this->configurationManager->domain !== '') {
|
||||
throw new \Exception('A domain is already configured. Reset the AIO instance first to register a new domain.');
|
||||
}
|
||||
|
||||
$validatedSlug = $this->validateSlug($slug);
|
||||
|
||||
[$token, $isNewAccount] = $this->obtainToken($email, $password);
|
||||
|
||||
// An empty token means a brand-new account was created but its email is not yet
|
||||
// verified. That is not an error: the account state is already persisted, so report
|
||||
// "awaiting verification" to the caller and let it re-render the awaiting UI.
|
||||
if ($token === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = $this->registerDomain($token, $validatedSlug);
|
||||
|
||||
if ($isNewAccount) {
|
||||
$this->createWildcardCname($token, $domain);
|
||||
}
|
||||
|
||||
$this->configurationManager->aioCommunityContainers = ["caddy", "dnsmasq"];
|
||||
$this->configurationManager->setDomain($domain, true);
|
||||
$this->updateIpIfDesecDomain();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the deSEC API token to use for domain registration, creating or
|
||||
* logging into an account as needed.
|
||||
*
|
||||
* deSEC's POST /auth/ endpoint does NOT return a token: it replies 202 and
|
||||
* emails a verification link. A token can only be obtained from /auth/login/
|
||||
* once the email has been verified. This method therefore drives a small
|
||||
* state machine across multiple form submissions:
|
||||
*
|
||||
* - A token is already stored (a previous attempt got past account setup but
|
||||
* failed at domain registration) → reuse it.
|
||||
* - The user supplied a password → log in to their existing, verified account.
|
||||
* - We are awaiting verification of an account we created earlier → try to log
|
||||
* in with the stored credentials; succeed once the email is verified,
|
||||
* otherwise ask the user to verify and try again.
|
||||
* - Otherwise → create a new account and ask the user to verify their email.
|
||||
*
|
||||
* @return array{0: string, 1: bool} the token and whether it belongs to a
|
||||
* freshly created (AIO-managed) account, which controls wildcard setup.
|
||||
* @throws \Exception with a user-facing message when verification is pending or on any API error
|
||||
*/
|
||||
private function obtainToken(string $email, string $password): array {
|
||||
if ($this->configurationManager->isDesecAccountRegistered()) {
|
||||
return [$this->configurationManager->desecToken, false];
|
||||
}
|
||||
|
||||
$validatedPassword = trim($password);
|
||||
|
||||
// An account we created earlier is awaiting email verification.
|
||||
if ($this->configurationManager->isDesecAwaitingVerification()) {
|
||||
$storedEmail = $this->configurationManager->desecEmail;
|
||||
// Prefer a freshly entered password (e.g. the user changed it), else the generated one.
|
||||
$loginPassword = $validatedPassword !== '' ? $validatedPassword : $this->configurationManager->desecPassword;
|
||||
$token = $this->loginAfterVerification($storedEmail, $loginPassword);
|
||||
// Storing the token flips the state from "awaiting verification" to "account
|
||||
// registered" (see ConfigurationManager::isDesecAwaitingVerification()).
|
||||
$this->configurationManager->desecToken = $token;
|
||||
return [$token, true];
|
||||
}
|
||||
|
||||
$validatedEmail = $this->validateEmail($email);
|
||||
|
||||
if ($validatedPassword !== '') {
|
||||
// The user supplied their existing deSEC password — log in instead of registering.
|
||||
// Store an empty password: the token is all we need; the user's password must not be persisted.
|
||||
$token = $this->loginAccount($validatedEmail, $validatedPassword);
|
||||
$this->saveAccountCredentials($token, '', $validatedEmail);
|
||||
return [$token, false];
|
||||
}
|
||||
|
||||
// Create a new account. 24 random bytes → 48-char hex password; satisfies deSEC's
|
||||
// minimum length and lets the user log in at desec.io if they ever need to.
|
||||
$generatedPassword = bin2hex(random_bytes(24));
|
||||
$this->registerAccount($validatedEmail, $generatedPassword);
|
||||
// Persisting email + password (but no token, no domain) is exactly the
|
||||
// "awaiting verification" state, see ConfigurationManager::isDesecAwaitingVerification().
|
||||
$this->configurationManager->startTransaction();
|
||||
$this->configurationManager->desecPassword = $generatedPassword;
|
||||
$this->configurationManager->desecEmail = $validatedEmail;
|
||||
$this->configurationManager->commitTransaction();
|
||||
|
||||
// This is not an error: the account was requested successfully and we now wait for
|
||||
// the user to verify their email. Signal it with an empty token so register() can
|
||||
// stop cleanly and the caller can re-render the awaiting-verification UI (which
|
||||
// already explains the next step) instead of surfacing an error toast.
|
||||
return ['', true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an email address string.
|
||||
*
|
||||
* @throws \Exception if the email is empty or syntactically invalid
|
||||
*/
|
||||
private function validateEmail(string $email): string {
|
||||
$email = trim($email);
|
||||
if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||||
throw new \Exception('Please provide a valid email address.');
|
||||
}
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an optional subdomain slug.
|
||||
* Returns an empty string when the caller wants a randomly generated slug.
|
||||
*
|
||||
* @throws \Exception if the slug is non-empty but does not match the allowed pattern
|
||||
*/
|
||||
private function validateSlug(string $slug): string {
|
||||
$slug = trim($slug);
|
||||
if ($slug !== '' && !preg_match(self::SLUG_PATTERN, $slug)) {
|
||||
throw new \Exception(
|
||||
'The desired subdomain must contain only lowercase letters, digits and hyphens, '
|
||||
. 'be between 1 and 63 characters long, and must not start or end with a hyphen.'
|
||||
);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests creation of a new deSEC account.
|
||||
*
|
||||
* deSEC replies 202 Accepted and emails a verification link; no API token is
|
||||
* returned here and the account is unusable until the email is verified. For
|
||||
* privacy reasons deSEC also returns 202 when the email is already registered
|
||||
* (without sending a mail), so a 202 cannot be treated as proof of a new account.
|
||||
* The captcha field is omitted; deSEC requires it only at email-verification time,
|
||||
* which the user completes in the browser via the emailed link.
|
||||
*
|
||||
* @throws \Exception on network failure or an unexpected HTTP response
|
||||
*/
|
||||
public function registerAccount(string $email, string $password): void {
|
||||
try {
|
||||
$res = $this->guzzleClient->post($this->configurationManager->desecApiBase . '/auth/', [
|
||||
'json' => ['email' => $email, 'password' => $password],
|
||||
]);
|
||||
} catch (TransferException $e) {
|
||||
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$code = $res->getStatusCode();
|
||||
|
||||
if ($code !== 202) {
|
||||
throw new \Exception('Unexpected response from deSEC during account registration (HTTP ' . $code . '): ' . $res->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to log in after the user was asked to verify a freshly created account.
|
||||
*
|
||||
* A login failure here has two common causes that we cannot tell apart, because
|
||||
* deSEC returns 202 both for a genuinely new account and for one whose email was
|
||||
* already registered (to prevent email enumeration):
|
||||
* 1. The account is new but its email has not been verified yet.
|
||||
* 2. The email already belonged to an existing deSEC account, so no new account
|
||||
* (and no verification mail) was created and our generated password is wrong.
|
||||
* The message covers both and points to the fix for each.
|
||||
*
|
||||
* @throws \Exception with a friendly hint when login is not yet possible
|
||||
*/
|
||||
private function loginAfterVerification(string $email, string $password): string {
|
||||
try {
|
||||
return $this->loginAccount($email, $password);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception(
|
||||
'Could not log in to deSEC for ' . $email . ' yet. Two things to check:' . "\n"
|
||||
. '• If deSEC emailed you a verification link, please click it and then try again.' . "\n"
|
||||
. '• If this email already had a deSEC account, no new account was created. '
|
||||
. 'In that case, enter your existing deSEC password in the password field below and try again.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with an existing deSEC account and returns the API token issued for it.
|
||||
*
|
||||
* @throws \Exception on invalid credentials, network failure, or an unexpected HTTP response
|
||||
*/
|
||||
public function loginAccount(string $email, string $password): string {
|
||||
try {
|
||||
$res = $this->guzzleClient->post($this->configurationManager->desecApiBase . '/auth/login/', [
|
||||
'json' => ['email' => $email, 'password' => $password],
|
||||
]);
|
||||
} catch (TransferException $e) {
|
||||
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$code = $res->getStatusCode();
|
||||
$body = $res->getBody()->getContents();
|
||||
|
||||
if ($code === 400 || $code === 403) {
|
||||
throw new \Exception('Could not log in to deSEC: invalid email address or password.');
|
||||
}
|
||||
|
||||
if ($code !== 200 && $code !== 201) {
|
||||
throw new \Exception('Unexpected response from deSEC during login (HTTP ' . $code . '): ' . $body);
|
||||
}
|
||||
|
||||
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data) || !isset($data['token']) || !is_string($data['token'])) {
|
||||
throw new \Exception('Could not extract the API token from the deSEC login response. Please try again.');
|
||||
}
|
||||
|
||||
return $data['token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a dedyn.io domain for the authenticated account.
|
||||
* When $slug is empty a random 10-character slug is tried up to MAX_SLUG_ATTEMPTS times.
|
||||
*
|
||||
* @return string the fully-qualified domain name that was registered
|
||||
* @throws \Exception if the slug is taken, on network failure, or after exhausting random attempts
|
||||
*/
|
||||
public function registerDomain(string $token, string $slug): string {
|
||||
$random = $slug === '';
|
||||
$attempts = $random ? self::MAX_SLUG_ATTEMPTS : 1;
|
||||
|
||||
for ($i = 0; $i < $attempts; $i++) {
|
||||
$domain = ($random ? bin2hex(random_bytes(self::SLUG_BYTES)) : $slug) . ConfigurationManager::DEDYN_SUFFIX;
|
||||
|
||||
try {
|
||||
$res = $this->guzzleClient->post($this->configurationManager->desecApiBase . '/domains/', [
|
||||
'headers' => ['Authorization' => 'Token ' . $token],
|
||||
'json' => ['name' => $domain],
|
||||
]);
|
||||
} catch (TransferException $e) {
|
||||
throw new \Exception('Could not reach the deSEC API: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$code = $res->getStatusCode();
|
||||
|
||||
if ($code === 201) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
if ($code === 409) {
|
||||
if (!$random) {
|
||||
throw new \Exception('"' . $domain . '" is already taken. Please choose a different subdomain and try again.');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new \Exception('Unexpected response from deSEC during domain registration (HTTP ' . $code . '): ' . $res->getBody()->getContents());
|
||||
}
|
||||
|
||||
throw new \Exception('Could not register a free dedyn.io domain after ' . self::MAX_SLUG_ATTEMPTS . ' attempts. Please try again.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wildcard CNAME rrset (*.domain → domain.) for a newly registered domain.
|
||||
* Errors are logged but do not abort the overall registration.
|
||||
*/
|
||||
private function createWildcardCname(string $token, string $domain): void {
|
||||
try {
|
||||
$res = $this->guzzleClient->post($this->configurationManager->desecApiBase . '/domains/' . $domain . '/rrsets/', [
|
||||
'headers' => ['Authorization' => 'Token ' . $token],
|
||||
'json' => [
|
||||
'subname' => '*',
|
||||
'type' => 'CNAME',
|
||||
'ttl' => 3600,
|
||||
'records' => [$domain . '.'],
|
||||
],
|
||||
]);
|
||||
} catch (TransferException $e) {
|
||||
error_log('Could not create wildcard CNAME for ' . $domain . ': ' . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$code = $res->getStatusCode();
|
||||
if ($code !== 201) {
|
||||
error_log('Unexpected response when creating wildcard CNAME for ' . $domain . ' (HTTP ' . $code . '): ' . $res->getBody()->getContents());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists deSEC account credentials to the AIO configuration atomically.
|
||||
*/
|
||||
public function saveAccountCredentials(string $token, string $password, string $email): void {
|
||||
$this->configurationManager->startTransaction();
|
||||
$this->configurationManager->desecToken = $token;
|
||||
$this->configurationManager->desecPassword = $password;
|
||||
$this->configurationManager->desecEmail = $email;
|
||||
$this->configurationManager->commitTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the deSEC dynamic-DNS record with the current public IP.
|
||||
* Does nothing when the configured domain is not a deSEC-managed dedyn.io domain.
|
||||
*/
|
||||
public function updateIpIfDesecDomain(): void {
|
||||
if (!$this->configurationManager->isDesecDomain()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = $this->configurationManager->domain;
|
||||
$token = $this->configurationManager->desecToken;
|
||||
|
||||
try {
|
||||
$res = $this->guzzleClient->get($this->configurationManager->desecUpdateUrl, [
|
||||
'query' => ['hostname' => $domain],
|
||||
'headers' => ['Authorization' => 'Token ' . $token],
|
||||
]);
|
||||
$status = trim($res->getBody()->getContents());
|
||||
if (str_starts_with($status, 'good') || str_starts_with($status, 'nochg')) {
|
||||
error_log('deSEC IP update for ' . $domain . ': ' . $status);
|
||||
} else {
|
||||
error_log('deSEC IP update for ' . $domain . ' returned unexpected response: ' . $status);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log('Could not update deSEC DNS record for ' . $domain . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user