mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-30 23:40:08 +00:00
switch to Guzzle in DesecController (pre-refactor checkpoint)
Agent-Logs-Url: https://github.com/nextcloud/all-in-one/sessions/fc6803fd-5743-438d-86b8-068ce48b1411 Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
39f30a6609
commit
f23d8276ff
27
community-containers/ddclient/ddclient.json
Normal file
27
community-containers/ddclient/ddclient.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"aio_services_v1": [
|
||||
{
|
||||
"container_name": "nextcloud-aio-ddclient",
|
||||
"display_name": "DDclient (deSEC DDNS)",
|
||||
"documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/ddclient",
|
||||
"image": "ghcr.io/linuxserver/ddclient",
|
||||
"image_tag": "latest",
|
||||
"restart": "unless-stopped",
|
||||
"environment": [
|
||||
"TZ=%TIMEZONE%",
|
||||
"NC_DOMAIN=%NC_DOMAIN%",
|
||||
"DESEC_TOKEN=%DESEC_TOKEN%"
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"source": "nextcloud_aio_ddclient",
|
||||
"destination": "/config",
|
||||
"writeable": true
|
||||
}
|
||||
],
|
||||
"backup_volumes": [
|
||||
"nextcloud_aio_ddclient"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
50
community-containers/ddclient/readme.md
Normal file
50
community-containers/ddclient/readme.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# DDclient community container
|
||||
|
||||
This container runs [DDclient](https://ddclient.net/) pre-configured for use with [deSEC](https://desec.io/) dynamic DNS.
|
||||
|
||||
When you register a free dedyn.io domain through the AIO interface, the `NC_DOMAIN` and `DESEC_TOKEN` environment variables are automatically set from the stored credentials. On first start the linuxserver/ddclient image creates an empty `/config/ddclient.conf` in the `nextcloud_aio_ddclient` volume; you need to populate that file once as described below.
|
||||
|
||||
## One-time configuration
|
||||
|
||||
After the container has started for the first time, run:
|
||||
|
||||
```bash
|
||||
docker exec -it nextcloud-aio-ddclient sh
|
||||
```
|
||||
|
||||
Then create `/config/ddclient.conf` with the following content (replace the placeholders with the values printed in the container's environment):
|
||||
|
||||
```
|
||||
daemon=300
|
||||
syslog=yes
|
||||
ssl=yes
|
||||
|
||||
use=web, web=https://checkipv4.dedyn.io/
|
||||
|
||||
protocol=dyndns2
|
||||
server=update.dedyn.io
|
||||
login=<value of NC_DOMAIN>
|
||||
password=<value of DESEC_TOKEN>
|
||||
<value of NC_DOMAIN>
|
||||
```
|
||||
|
||||
You can read the values from the running container:
|
||||
|
||||
```bash
|
||||
docker exec nextcloud-aio-ddclient printenv NC_DOMAIN
|
||||
docker exec nextcloud-aio-ddclient printenv DESEC_TOKEN
|
||||
```
|
||||
|
||||
Once the file is saved, restart the container:
|
||||
|
||||
```bash
|
||||
docker restart nextcloud-aio-ddclient
|
||||
```
|
||||
|
||||
DDclient will now update the DNS record for your domain every 5 minutes.
|
||||
|
||||
## Notes
|
||||
|
||||
- The config volume (`nextcloud_aio_ddclient`) is included in AIO backups, so the configuration persists across updates and restores.
|
||||
- A derivative image that auto-generates the config from `NC_DOMAIN` and `DESEC_TOKEN` without any manual step will be created in a dedicated repository in the future.
|
||||
- For IPv6 support add a second `use` block pointing to `https://checkipv6.dedyn.io/` in the config file. See the [ddclient documentation](https://ddclient.net/protocols/dyndns2.html) for details.
|
||||
@@ -109,6 +109,7 @@ $app->post('/api/auth/login', AIO\Controller\LoginController::class . ':TryLogin
|
||||
$app->get('/api/auth/getlogin', AIO\Controller\LoginController::class . ':GetTryLogin');
|
||||
$app->post('/api/auth/logout', AIO\Controller\LoginController::class . ':Logout');
|
||||
$app->post('/api/configuration', \AIO\Controller\ConfigurationController::class . ':SetConfig');
|
||||
$app->post('/api/desec/register', \AIO\Controller\DesecController::class . ':Register');
|
||||
|
||||
// Views
|
||||
$app->get('/containers', function (Request $request, Response $response, array $args) use ($container) {
|
||||
@@ -180,6 +181,8 @@ $app->get('/containers', function (Request $request, Response $response, array $
|
||||
'community_containers' => $configurationManager->listAvailableCommunityContainers(),
|
||||
'community_containers_enabled' => $configurationManager->aioCommunityContainers,
|
||||
'bypass_container_update' => $bypass_container_update,
|
||||
'desec_email' => $configurationManager->desecEmail,
|
||||
'is_desec_domain' => $configurationManager->isDesecDomain(),
|
||||
]);
|
||||
})->setName('profile');
|
||||
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
|
||||
|
||||
173
php/src/Controller/DesecController.php
Normal file
173
php/src/Controller/DesecController.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace AIO\Controller;
|
||||
|
||||
use AIO\Data\ConfigurationManager;
|
||||
use AIO\Data\InvalidSettingConfigurationException;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
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 string DEDYN_SUFFIX = '.dedyn.io';
|
||||
private const int MAX_SLUG_ATTEMPTS = 5;
|
||||
private const int SLUG_BYTES = 5; // 10-char hex slug
|
||||
|
||||
private Client $guzzleClient;
|
||||
|
||||
public function __construct(
|
||||
private ConfigurationManager $configurationManager,
|
||||
) {
|
||||
$this->guzzleClient = new Client([
|
||||
'timeout' => 15,
|
||||
'connect_timeout' => 10,
|
||||
'http_errors' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function Register(Request $request, Response $response, array $args): Response {
|
||||
// Only allow registration when no domain is configured yet
|
||||
if ($this->configurationManager->domain !== '') {
|
||||
$response->getBody()->write('A domain is already configured. Reset the AIO instance first to register a new domain.');
|
||||
return $response->withStatus(422);
|
||||
}
|
||||
|
||||
$email = trim((string)($request->getParsedBody()['desec_email'] ?? ''));
|
||||
if ($email === '' || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||||
$response->getBody()->write('Please provide a valid email address.');
|
||||
return $response->withStatus(422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Register an account at deSEC and obtain an API token
|
||||
$password = bin2hex(random_bytes(24));
|
||||
$token = $this->registerDesecAccount($email, $password);
|
||||
|
||||
// Register a free dedyn.io subdomain
|
||||
$domain = $this->registerDesecDomain($token);
|
||||
|
||||
// Persist the credentials and auto-enable the companion community containers
|
||||
$this->configurationManager->startTransaction();
|
||||
$this->configurationManager->setDesecToken($token);
|
||||
$this->configurationManager->desecEmail = $email;
|
||||
$enabled = array_values(array_filter(
|
||||
$this->configurationManager->aioCommunityContainers,
|
||||
fn(string $cc): bool => $cc !== '',
|
||||
));
|
||||
foreach (['caddy', 'ddclient'] as $cc) {
|
||||
if (!in_array($cc, $enabled, true)) {
|
||||
$enabled[] = $cc;
|
||||
}
|
||||
}
|
||||
$this->configurationManager->aioCommunityContainers = $enabled;
|
||||
$this->configurationManager->commitTransaction();
|
||||
|
||||
// Set the domain; skip the reachability validation because the domain was just
|
||||
// created and DNS propagation may not have completed yet.
|
||||
$this->configurationManager->setDomain($domain, true);
|
||||
|
||||
return $response->withStatus(201)->withHeader('Location', '.');
|
||||
} catch (InvalidSettingConfigurationException $ex) {
|
||||
$response->getBody()->write($ex->getMessage());
|
||||
return $response->withStatus(422);
|
||||
} catch (\Exception $ex) {
|
||||
$response->getBody()->write($ex->getMessage());
|
||||
return $response->withStatus(422);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new deSEC account and returns the API token from the response.
|
||||
*
|
||||
* @throws \Exception on network failure or an unexpected API 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());
|
||||
}
|
||||
|
||||
$httpCode = $res->getStatusCode();
|
||||
$body = $res->getBody()->getContents();
|
||||
|
||||
if ($httpCode === 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 configure ddclient manually.',
|
||||
);
|
||||
}
|
||||
throw new \Exception('Registration at deSEC failed (HTTP 400): ' . $body);
|
||||
}
|
||||
|
||||
if ($httpCode !== 201) {
|
||||
throw new \Exception(
|
||||
'Unexpected response from deSEC during account registration '
|
||||
. '(HTTP ' . $httpCode . '): ' . $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 new dedyn.io subdomain and returns its full name.
|
||||
* Retries with a different random slug on name conflicts.
|
||||
*
|
||||
* @throws \Exception when all attempts fail or a network/API error occurs
|
||||
*/
|
||||
private function registerDesecDomain(string $token): string {
|
||||
$lastError = '';
|
||||
|
||||
for ($attempt = 0; $attempt < self::MAX_SLUG_ATTEMPTS; $attempt++) {
|
||||
$slug = bin2hex(random_bytes(self::SLUG_BYTES));
|
||||
$domain = $slug . self::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());
|
||||
}
|
||||
|
||||
$httpCode = $res->getStatusCode();
|
||||
|
||||
if ($httpCode === 201) {
|
||||
return $domain;
|
||||
}
|
||||
|
||||
if ($httpCode === 409) {
|
||||
// Slug already taken — try another one
|
||||
$lastError = '"' . $domain . '" is already taken';
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = $res->getBody()->getContents();
|
||||
throw new \Exception(
|
||||
'Unexpected response from deSEC during domain registration '
|
||||
. '(HTTP ' . $httpCode . '): ' . $body,
|
||||
);
|
||||
}
|
||||
|
||||
throw new \Exception(
|
||||
'Could not register a free dedyn.io domain after ' . self::MAX_SLUG_ATTEMPTS . ' attempts'
|
||||
. ($lastError !== '' ? ' (' . $lastError . ')' : '') . '. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,36 @@ class ConfigurationManager
|
||||
set { $this->set('turn_domain', $value); }
|
||||
}
|
||||
|
||||
public string $desecEmail {
|
||||
get => $this->get('desec_email', '');
|
||||
set { $this->set('desec_email', $value); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a deSEC API token in the secrets store.
|
||||
* Unlike randomly-generated secrets, this token is obtained from the deSEC REST API and
|
||||
* must be set explicitly; it is never auto-generated.
|
||||
*/
|
||||
public function setDesecToken(string $token): void {
|
||||
$secrets = $this->get('secrets', []);
|
||||
$secrets['DESEC_TOKEN'] = $token;
|
||||
$this->set('secrets', $secrets);
|
||||
}
|
||||
|
||||
public function getDesecToken(): string {
|
||||
$secrets = $this->get('secrets', []);
|
||||
return isset($secrets['DESEC_TOKEN']) && is_string($secrets['DESEC_TOKEN'])
|
||||
? $secrets['DESEC_TOKEN']
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the configured domain is a deSEC dedyn.io subdomain and a token is stored.
|
||||
*/
|
||||
public function isDesecDomain(): bool {
|
||||
return str_ends_with($this->domain, '.dedyn.io') && $this->getDesecToken() !== '';
|
||||
}
|
||||
|
||||
public string $apachePort {
|
||||
get => $this->getEnvironmentalVariableOrConfig('APACHE_PORT', 'apache_port', '443');
|
||||
set { $this->set('apache_port', $value); }
|
||||
@@ -1109,6 +1139,7 @@ class ConfigurationManager
|
||||
'CADDY_IP_ADDRESS' => in_array('caddy', $this->aioCommunityContainers, true) ? gethostbyname('nextcloud-aio-caddy') : '',
|
||||
'WHITEBOARD_ENABLED' => $this->isWhiteboardEnabled ? 'yes' : '',
|
||||
'AIO_VERSION' => $this->getAioVersion(),
|
||||
'DESEC_TOKEN' => $this->getDesecToken(),
|
||||
default => $this->getRegisteredSecret($placeholder),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,6 +132,19 @@
|
||||
{% endif %}
|
||||
<p><strong>Hint:</strong> If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following <a target="_blank" href="https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation">this documentation</a>.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Don't have a domain? Get a free one from deSEC</summary>
|
||||
<p><a target="_blank" href="https://desec.io">deSEC</a> offers free dynamic DNS subdomains under <strong>dedyn.io</strong>. AIO can register an account and a subdomain for you automatically. The <strong>ddclient</strong> and <strong>caddy</strong> community containers will be enabled so that your IP address is kept up to date and your traffic is routed through a reverse proxy.</p>
|
||||
<p><strong>Requirements:</strong> Your server must be reachable from the internet (a public IP address is needed). Port 80 and 443 must be open/forwarded in your firewall/router.</p>
|
||||
<p>Please enter your email address. A deSEC account and a random <em>subdomain.dedyn.io</em> domain will be created for you.</p>
|
||||
<form method="POST" action="api/desec/register" class="xhr">
|
||||
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
|
||||
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
|
||||
<input type="email" name="desec_email" placeholder="your@email.com" required />
|
||||
<input type="submit" value="Register free domain via deSEC" />
|
||||
</form>
|
||||
<p><strong>Note:</strong> By submitting this form you agree to the <a target="_blank" href="https://desec.io/terms">deSEC terms of service</a>. The registered domain and your deSEC account credentials are stored in the AIO configuration. After registration, finish the setup by configuring the ddclient container as described in its <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/ddclient">documentation</a>.</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<h2>Restore former AIO instance from backup</h2>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<h2>Community Containers</h2>
|
||||
<p>In this section you can enable or disable optional Community Containers that are not included by default in the main installation. These containers are provided by the community and can be useful for various purposes and are automatically integrated in AIOs backup solution and update mechanisms.</p>
|
||||
<p><strong>⚠️ Caution: </strong>Community Containers are maintained by the community and not officially by Nextcloud. Some containers may not be compatible with your system, may not work as expected or may discontinue. Use them at your own risk. Please read the documentation for each container first before adding any as some are also incompatible between each other! Never add all of them at the same time!</p>
|
||||
{% if is_desec_domain == true %}
|
||||
<p>ℹ️ Your Nextcloud domain (<strong>{{ domain }}</strong>) was registered via deSEC. The <strong>caddy</strong> and <strong>ddclient</strong> community containers have been automatically enabled. Please follow the <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/ddclient"><strong>ddclient documentation</strong></a> to finish configuring DNS updates for your domain.</p>
|
||||
{% endif %}
|
||||
{% if isAnyRunning == true %}
|
||||
<p><strong>Please note:</strong> You can enable or disable the options below only when your containers are stopped.</p>
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user