mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-21 02:40:09 +00:00
refactor: extract DesecManager from DesecController
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/7109ac88-02d1-44bf-8602-5f15873a67e5 Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ce857a5588
commit
fb6112f174
@@ -4,27 +4,17 @@ declare(strict_types=1);
|
||||
namespace AIO\Controller;
|
||||
|
||||
use AIO\Data\ConfigurationManager;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use AIO\Desec\DesecManager;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
readonly class DesecController {
|
||||
private const string DESEC_API_BASE = 'https://desec.io/api/v1';
|
||||
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 ConfigurationManager $configurationManager,
|
||||
private DesecManager $desecManager,
|
||||
) {
|
||||
$this->guzzleClient = new Client([
|
||||
'timeout' => 15,
|
||||
'connect_timeout' => 10,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function Register(Request $request, Response $response, array $args): Response {
|
||||
@@ -43,14 +33,14 @@ readonly class DesecController {
|
||||
// 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.
|
||||
$password = bin2hex(random_bytes(24));
|
||||
$token = $this->registerDesecAccount($email, $password);
|
||||
$this->saveAccountCredentials($token, $password, $email);
|
||||
$token = $this->desecManager->registerAccount($email, $password);
|
||||
$this->desecManager->saveAccountCredentials($token, $password, $email);
|
||||
}
|
||||
|
||||
$domain = $this->registerDesecDomain($token, $slug);
|
||||
$this->enableDesecContainers();
|
||||
$domain = $this->desecManager->registerDomain($token, $slug);
|
||||
$this->desecManager->enableDesecContainers();
|
||||
$this->configurationManager->setDomain($domain, true);
|
||||
$this->updateIpIfDesecDomain();
|
||||
$this->desecManager->updateIpIfDesecDomain();
|
||||
|
||||
return $response->withStatus(201)->withHeader('Location', '.');
|
||||
} catch (\Exception $ex) {
|
||||
@@ -95,134 +85,4 @@ readonly class DesecController {
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
|
||||
private function enableDesecContainers(): void {
|
||||
$this->configurationManager->startTransaction();
|
||||
$enabled = array_values(array_filter(
|
||||
$this->configurationManager->aioCommunityContainers,
|
||||
fn(string $cc): bool => $cc !== '',
|
||||
));
|
||||
if (!in_array('caddy', $enabled, true)) {
|
||||
$enabled[] = 'caddy';
|
||||
}
|
||||
if (!in_array('dnsmasq', $enabled, true)) {
|
||||
$enabled[] = 'dnsmasq';
|
||||
}
|
||||
$this->configurationManager->aioCommunityContainers = $enabled;
|
||||
$this->configurationManager->commitTransaction();
|
||||
}
|
||||
|
||||
public function updateIpIfDesecDomain(): void {
|
||||
if (!$this->configurationManager->isDesecDomain()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = $this->configurationManager->domain;
|
||||
$token = $this->configurationManager->desecToken;
|
||||
|
||||
try {
|
||||
$res = $this->guzzleClient->get('https://update.dedyn.io/', [
|
||||
'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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new deSEC account and returns the API token issued for it.
|
||||
*
|
||||
* @throws \Exception on network failure or an unexpected HTTP response
|
||||
*/
|
||||
private function registerDesecAccount(string $email, string $password): string {
|
||||
try {
|
||||
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/auth/', [
|
||||
'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) {
|
||||
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (is_array($data) && isset($data['email'])) {
|
||||
throw new \Exception(
|
||||
'This email address is already registered at deSEC. '
|
||||
. 'Please log in at https://desec.io to retrieve your token and set up your domain manually.',
|
||||
);
|
||||
}
|
||||
throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body);
|
||||
}
|
||||
|
||||
if ($code !== 201) {
|
||||
throw new \Exception('Unexpected response from deSEC during account registration (HTTP ' . $code . '): ' . $body);
|
||||
}
|
||||
|
||||
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data) || !isset($data['token']['token']) || !is_string($data['token']['token'])) {
|
||||
throw new \Exception('Could not extract the API token from the deSEC response. Please try again.');
|
||||
}
|
||||
|
||||
return $data['token']['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
|
||||
*/
|
||||
private function registerDesecDomain(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(self::DESEC_API_BASE . '/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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 @@ readonly class DockerController {
|
||||
private DockerActionManager $dockerActionManager,
|
||||
private ContainerDefinitionFetcher $containerDefinitionFetcher,
|
||||
private ConfigurationManager $configurationManager,
|
||||
private DesecController $desecController,
|
||||
private DesecManager $desecManager,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -265,7 +266,7 @@ readonly class DockerController {
|
||||
$this->StopDomaincheckContainer();
|
||||
|
||||
// Refresh the deSEC DNS record with the current public IP before starting containers
|
||||
$this->desecController->updateIpIfDesecDomain();
|
||||
$this->desecManager->updateIpIfDesecDomain();
|
||||
|
||||
$id = self::TOP_CONTAINER;
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ require __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$container = \AIO\DependencyInjection::GetContainer();
|
||||
|
||||
/** @var \AIO\Controller\DesecController $desecController */
|
||||
$desecController = $container->get(\AIO\Controller\DesecController::class);
|
||||
/** @var \AIO\Desec\DesecManager $desecManager */
|
||||
$desecManager = $container->get(\AIO\Desec\DesecManager::class);
|
||||
|
||||
$desecController->updateIpIfDesecDomain();
|
||||
$desecManager->updateIpIfDesecDomain();
|
||||
|
||||
@@ -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()
|
||||
|
||||
166
php/src/Desec/DesecManager.php
Normal file
166
php/src/Desec/DesecManager.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace AIO\Desec;
|
||||
|
||||
use AIO\Data\ConfigurationManager;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
|
||||
class DesecManager {
|
||||
private const string DESEC_API_BASE = 'https://desec.io/api/v1';
|
||||
private const int MAX_SLUG_ATTEMPTS = 5;
|
||||
private const int SLUG_BYTES = 5; // bin2hex → 10-char slug
|
||||
|
||||
private Client $guzzleClient;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigurationManager $configurationManager,
|
||||
) {
|
||||
$this->guzzleClient = new Client([
|
||||
'timeout' => 15,
|
||||
'connect_timeout' => 10,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new deSEC account and returns the API token issued for it.
|
||||
*
|
||||
* @throws \Exception on network failure or an unexpected HTTP response
|
||||
*/
|
||||
public function registerAccount(string $email, string $password): string {
|
||||
try {
|
||||
$res = $this->guzzleClient->post(self::DESEC_API_BASE . '/auth/', [
|
||||
'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) {
|
||||
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (is_array($data) && isset($data['email'])) {
|
||||
throw new \Exception(
|
||||
'This email address is already registered at deSEC. '
|
||||
. 'Please log in at https://desec.io to retrieve your token and set up your domain manually.',
|
||||
);
|
||||
}
|
||||
throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body);
|
||||
}
|
||||
|
||||
if ($code !== 201) {
|
||||
throw new \Exception('Unexpected response from deSEC during account registration (HTTP ' . $code . '): ' . $body);
|
||||
}
|
||||
|
||||
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($data) || !isset($data['token']['token']) || !is_string($data['token']['token'])) {
|
||||
throw new \Exception('Could not extract the API token from the deSEC response. Please try again.');
|
||||
}
|
||||
|
||||
return $data['token']['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(self::DESEC_API_BASE . '/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.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the caddy and dnsmasq community containers are enabled.
|
||||
*/
|
||||
public function enableDesecContainers(): void {
|
||||
$this->configurationManager->startTransaction();
|
||||
$enabled = array_values(array_filter(
|
||||
$this->configurationManager->aioCommunityContainers,
|
||||
fn(string $cc): bool => $cc !== '',
|
||||
));
|
||||
if (!in_array('caddy', $enabled, true)) {
|
||||
$enabled[] = 'caddy';
|
||||
}
|
||||
if (!in_array('dnsmasq', $enabled, true)) {
|
||||
$enabled[] = 'dnsmasq';
|
||||
}
|
||||
$this->configurationManager->aioCommunityContainers = $enabled;
|
||||
$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('https://update.dedyn.io/', [
|
||||
'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