mirror of
https://github.com/nextcloud/all-in-one.git
synced 2026-05-21 10:50:10 +00:00
Compare commits
11 Commits
copilot/ad
...
7281c9b7c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7281c9b7c8 | ||
|
|
cb48bc5db0 | ||
|
|
83129d6a55 | ||
|
|
68df1dd857 | ||
|
|
7598f6534d | ||
|
|
a12f505dcd | ||
|
|
7a825f2025 | ||
|
|
6aa4f8d672 | ||
|
|
5b72d17438 | ||
|
|
7c5abc978d | ||
|
|
f23d8276ff |
17
Containers/dnsmasq/Dockerfile
Normal file
17
Containers/dnsmasq/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
# syntax=docker/dockerfile:latest
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache dnsmasq iproute2
|
||||
|
||||
COPY --chmod=755 start.sh /start.sh
|
||||
|
||||
ENTRYPOINT ["/start.sh"]
|
||||
|
||||
LABEL com.centurylinklabs.watchtower.enable="false" \
|
||||
wud.watch="false" \
|
||||
org.opencontainers.image.title="Dnsmasq for Nextcloud AIO" \
|
||||
org.opencontainers.image.description="Lightweight DNS server that resolves NC_DOMAIN to the local server IP for LAN devices" \
|
||||
org.opencontainers.image.url="https://github.com/nextcloud/all-in-one" \
|
||||
org.opencontainers.image.source="https://github.com/nextcloud/all-in-one" \
|
||||
org.opencontainers.image.vendor="Nextcloud" \
|
||||
org.opencontainers.image.documentation="https://github.com/nextcloud/all-in-one/blob/main/community-containers/dnsmasq/readme.md"
|
||||
40
Containers/dnsmasq/start.sh
Normal file
40
Containers/dnsmasq/start.sh
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ -z "$NC_DOMAIN" ]; then
|
||||
echo "ERROR: NC_DOMAIN is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOCAL_IP=""
|
||||
|
||||
# Determine the server's primary LAN IP - use the source address chosen by the kernel
|
||||
# for a route to a well-known public IP (1.1.1.1 is used purely to query the routing table;
|
||||
# no traffic is sent there).
|
||||
LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") {print $(i+1); exit}}')
|
||||
|
||||
if [ -z "$LOCAL_IP" ]; then
|
||||
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
fi
|
||||
|
||||
if [ -z "$LOCAL_IP" ]; then
|
||||
echo "ERROR: Could not determine local IP address" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Nextcloud AIO dnsmasq: resolving $NC_DOMAIN -> $LOCAL_IP"
|
||||
echo "Configure your router's DHCP to hand out $LOCAL_IP as the DNS server for LAN clients."
|
||||
|
||||
mkdir -p /etc/dnsmasq.d
|
||||
|
||||
cat > /etc/dnsmasq.d/nextcloud-aio.conf << EOF
|
||||
# Auto-generated by Nextcloud AIO dnsmasq container.
|
||||
# Resolves NC_DOMAIN (and all its subdomains) to this server's local IP.
|
||||
address=/$NC_DOMAIN/$LOCAL_IP
|
||||
|
||||
# Bind only to the LAN interface to avoid conflicts with any system DNS resolver.
|
||||
bind-interfaces
|
||||
listen-address=$LOCAL_IP
|
||||
EOF
|
||||
|
||||
exec dnsmasq --no-daemon --log-queries --conf-dir=/etc/dnsmasq.d
|
||||
@@ -51,6 +51,9 @@ while true; do
|
||||
# Check if AIO is outdated
|
||||
sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/OutdatedNotification.php
|
||||
|
||||
# Update deSEC DNS IP record (no-op when IP is unchanged or deSEC is not configured)
|
||||
sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/UpdateDesecIp.php
|
||||
|
||||
# Remove sessions older than 24h
|
||||
find "/mnt/docker-aio-config/session/" -mindepth 1 -mmin +1440 -delete
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"NC_DOMAIN=%NC_DOMAIN%",
|
||||
"APACHE_PORT=%APACHE_PORT%",
|
||||
"APACHE_IP_BINDING=%APACHE_IP_BINDING%",
|
||||
"NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%"
|
||||
"NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%",
|
||||
"DESEC_TOKEN=%DESEC_TOKEN%"
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
|
||||
17
community-containers/dnsmasq/dnsmasq.json
Normal file
17
community-containers/dnsmasq/dnsmasq.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"aio_services_v1": [
|
||||
{
|
||||
"container_name": "nextcloud-aio-dnsmasq",
|
||||
"display_name": "Dnsmasq (Local DNS)",
|
||||
"documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq",
|
||||
"image": "ghcr.io/nextcloud-releases/aio-dnsmasq",
|
||||
"image_tag": "%AIO_CHANNEL%",
|
||||
"internal_port": "host",
|
||||
"restart": "unless-stopped",
|
||||
"environment": [
|
||||
"NC_DOMAIN=%NC_DOMAIN%",
|
||||
"TZ=%TIMEZONE%"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
31
community-containers/dnsmasq/readme.md
Normal file
31
community-containers/dnsmasq/readme.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Dnsmasq (Local DNS) community container
|
||||
|
||||
This container runs [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) pre-configured to resolve your Nextcloud domain (`NC_DOMAIN`) to the server's local LAN IP address.
|
||||
|
||||
## Why is this needed?
|
||||
|
||||
By default, all devices on your LAN reach Nextcloud via the public internet (or require hairpin NAT on your router). With this container, LAN clients can resolve `NC_DOMAIN` directly to the server's private LAN IP, making local access faster and independent of your internet connection.
|
||||
|
||||
This container is automatically enabled when you register a deSEC domain through the AIO interface.
|
||||
|
||||
## How it works
|
||||
|
||||
On startup the container:
|
||||
1. Detects the server's primary LAN IP address automatically.
|
||||
2. Configures dnsmasq to resolve `NC_DOMAIN` (and all its subdomains) to that IP.
|
||||
3. Forwards all other DNS queries to the upstream nameservers from the host's `/etc/resolv.conf`.
|
||||
4. Listens only on the LAN interface to avoid conflicts with any system DNS resolver (e.g. `systemd-resolved`).
|
||||
|
||||
## Required router configuration
|
||||
|
||||
⚠️ **You must change your router's DHCP settings** for this to take effect for LAN clients:
|
||||
|
||||
Set the **DNS server** handed out by DHCP to the **local IP address of this server** (the same IP that is printed in the container logs on startup). After saving the change, LAN devices need to renew their DHCP lease (or be rebooted) before the new DNS setting takes effect.
|
||||
|
||||
Most routers expose this under **DHCP settings → Primary DNS** or **LAN → DNS Server**.
|
||||
|
||||
## Notes
|
||||
|
||||
- The container runs in **host network mode** so it can bind directly to port 53 on the LAN interface. No additional port-forwarding is required.
|
||||
- If `systemd-resolved` (or another DNS resolver) is already listening on port 53 on the LAN IP, there will be a conflict. In that case you need to disable or reconfigure that resolver first.
|
||||
- IPv6 addresses are not handled by this container; extend the dnsmasq configuration manually if needed.
|
||||
@@ -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,10 @@ $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,
|
||||
'desec_password' => $configurationManager->getDesecPassword(),
|
||||
'is_desec_domain' => $configurationManager->isDesecDomain(),
|
||||
'desec_account_registered' => $configurationManager->isDesecAccountRegistered(),
|
||||
]);
|
||||
})->setName('profile');
|
||||
$app->get('/login', function (Request $request, Response $response, array $args) use ($container) {
|
||||
|
||||
273
php/src/Controller/DesecController.php
Normal file
273
php/src/Controller/DesecController.php
Normal file
@@ -0,0 +1,273 @@
|
||||
<?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 const string SLUG_PATTERN = '/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// When a deSEC account was already registered (token exists) but domain creation previously
|
||||
// failed, we skip account registration and re-use the stored token and email.
|
||||
$accountAlreadyRegistered = $this->configurationManager->isDesecAccountRegistered();
|
||||
|
||||
if ($accountAlreadyRegistered) {
|
||||
$token = $this->configurationManager->getDesecToken();
|
||||
// email is already stored; no need to validate or update it
|
||||
} else {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$slug = trim((string)($request->getParsedBody()['desec_slug'] ?? ''));
|
||||
if ($slug !== '' && !preg_match(self::SLUG_PATTERN, $slug)) {
|
||||
$response->getBody()->write(
|
||||
'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 $response->withStatus(422);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!$accountAlreadyRegistered) {
|
||||
// Register an account at deSEC and obtain an API token.
|
||||
// The password is stored so the user can log in to desec.io directly if needed.
|
||||
// 24 random bytes encoded as hex produce a 48-character password.
|
||||
$password = bin2hex(random_bytes(24));
|
||||
$token = $this->registerDesecAccount($email, $password);
|
||||
|
||||
// Persist the token, password and email immediately so that a subsequent
|
||||
// domain-registration failure leaves the account credentials stored and allows
|
||||
// the user to retry.
|
||||
$this->configurationManager->startTransaction();
|
||||
$this->configurationManager->setDesecToken($token);
|
||||
$this->configurationManager->setDesecPassword($password);
|
||||
$this->configurationManager->desecEmail = $email;
|
||||
$this->configurationManager->commitTransaction();
|
||||
}
|
||||
|
||||
// Register a free dedyn.io subdomain
|
||||
$domain = $this->registerDesecDomain($token, $slug);
|
||||
|
||||
// Auto-enable caddy and dnsmasq (idempotent — safe to call even on retry)
|
||||
$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();
|
||||
|
||||
// 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);
|
||||
|
||||
// Perform the first DNS IP update so the record is populated immediately
|
||||
$this->updateIpIfDesecDomain();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the deSEC DNS A/AAAA record with the current public IP of this host.
|
||||
* Uses deSEC's DynDNS2-compatible update endpoint, which auto-detects the requester's IP.
|
||||
* Safe to call frequently; the endpoint returns "nochg" when the IP has not changed.
|
||||
* Errors are logged but never thrown, so callers are not interrupted.
|
||||
*/
|
||||
public function updateIpIfDesecDomain(): void {
|
||||
if (!$this->configurationManager->isDesecDomain()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = $this->configurationManager->domain;
|
||||
$token = $this->configurationManager->getDesecToken();
|
||||
|
||||
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 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 set up your domain 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.
|
||||
*
|
||||
* When $requestedSlug is non-empty the caller's preferred slug is tried once; a 409
|
||||
* conflict returns an actionable error immediately (no silent retry).
|
||||
* When $requestedSlug is empty a random 10-char hex slug is generated and retried up
|
||||
* to MAX_SLUG_ATTEMPTS times on 409 conflicts.
|
||||
*
|
||||
* @throws \Exception when all attempts fail or a network/API error occurs
|
||||
*/
|
||||
private function registerDesecDomain(string $token, string $requestedSlug = ''): string {
|
||||
if ($requestedSlug !== '') {
|
||||
// User chose a specific slug — try it exactly once.
|
||||
$domain = $requestedSlug . 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) {
|
||||
throw new \Exception(
|
||||
'"' . $domain . '" is already taken. Please choose a different subdomain and try again.',
|
||||
);
|
||||
}
|
||||
|
||||
$body = $res->getBody()->getContents();
|
||||
throw new \Exception(
|
||||
'Unexpected response from deSEC during domain registration '
|
||||
. '(HTTP ' . $httpCode . '): ' . $body,
|
||||
);
|
||||
}
|
||||
|
||||
// No slug provided — generate random slugs and retry on conflicts.
|
||||
$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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ readonly class DockerController {
|
||||
public function __construct(
|
||||
private DockerActionManager $dockerActionManager,
|
||||
private ContainerDefinitionFetcher $containerDefinitionFetcher,
|
||||
private ConfigurationManager $configurationManager
|
||||
private ConfigurationManager $configurationManager,
|
||||
private DesecController $desecController,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -263,6 +264,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->desecController->updateIpIfDesecDomain();
|
||||
|
||||
$id = self::TOP_CONTAINER;
|
||||
|
||||
$this->PerformRecursiveContainerStart($id, $pullImage, $addToStreamingResponseBody);
|
||||
|
||||
17
php/src/Cron/UpdateDesecIp.php
Normal file
17
php/src/Cron/UpdateDesecIp.php
Normal file
@@ -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\Controller\DesecController $desecController */
|
||||
$desecController = $container->get(\AIO\Controller\DesecController::class);
|
||||
|
||||
$desecController->updateIpIfDesecDomain();
|
||||
@@ -198,6 +198,61 @@ 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']
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the deSEC account password in the secrets store so the user can log in at desec.io.
|
||||
*/
|
||||
public function setDesecPassword(string $password): void {
|
||||
$secrets = $this->get('secrets', []);
|
||||
$secrets['DESEC_PASSWORD'] = $password;
|
||||
$this->set('secrets', $secrets);
|
||||
}
|
||||
|
||||
public function getDesecPassword(): string {
|
||||
$secrets = $this->get('secrets', []);
|
||||
return isset($secrets['DESEC_PASSWORD']) && is_string($secrets['DESEC_PASSWORD'])
|
||||
? $secrets['DESEC_PASSWORD']
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when a deSEC account token is stored but no domain has been configured yet.
|
||||
* This happens when account registration succeeded but domain registration subsequently failed.
|
||||
* In this state the user can retry domain registration with a different slug.
|
||||
*/
|
||||
public function isDesecAccountRegistered(): bool {
|
||||
return $this->getDesecToken() !== '' && $this->desecEmail !== '' && $this->domain === '';
|
||||
}
|
||||
|
||||
public string $apachePort {
|
||||
get => $this->getEnvironmentalVariableOrConfig('APACHE_PORT', 'apache_port', '443');
|
||||
set { $this->set('apache_port', $value); }
|
||||
@@ -1109,6 +1164,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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -867,7 +867,15 @@ readonly class DockerActionManager {
|
||||
// Add a secondary alias for domaincheck container, to keep it as similar to actual apache controller as possible.
|
||||
// If a reverse-proxy is relying on container name as hostname this allows it to operate as usual and still validate the domain
|
||||
// The domaincheck container and apache container are never supposed to be active at the same time because they use the same APACHE_PORT anyway, so this doesn't add any new constraints.
|
||||
$alias = ($container->identifier === 'nextcloud-aio-domaincheck') ? 'nextcloud-aio-apache' : '';
|
||||
if ($container->identifier === 'nextcloud-aio-domaincheck') {
|
||||
$alias = 'nextcloud-aio-apache';
|
||||
}
|
||||
|
||||
// Add NC_DOMAIN as a Docker network alias so that intra-network traffic for the Nextcloud
|
||||
// domain is forwarded directly to the aio-caddy container without leaving the Docker network.
|
||||
if ($container->identifier === 'nextcloud-aio-caddy') {
|
||||
$alias = $this->configurationManager->domain;
|
||||
}
|
||||
|
||||
$this->ConnectContainerIdToNetwork($container->identifier, $container->internalPorts, alias: $alias);
|
||||
|
||||
|
||||
@@ -132,6 +132,30 @@
|
||||
{% 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{% if desec_account_registered %} open{% endif %}>
|
||||
<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>caddy</strong> community container will be enabled as a reverse proxy, the <strong>dnsmasq</strong> container will be enabled for local DNS resolution, and the mastercontainer will keep your DNS record up to date automatically.</p>
|
||||
{% if desec_account_registered %}
|
||||
<p>Your deSEC account (<strong>{{ desec_email }}</strong>) was registered successfully but the domain could not be registered. Please enter a desired subdomain slug (the part before <code>.dedyn.io</code>) and try again, or leave it blank for a random one.</p>
|
||||
<p>Your deSEC login credentials (for <a target="_blank" href="https://desec.io">desec.io</a>): Email: <strong>{{ desec_email }}</strong>. <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details>. Please save these in a safe place.</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="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (1–63 characters). No leading or trailing hyphen." />
|
||||
<input type="submit" value="Register free domain via deSEC" />
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Please enter your email address. You can also enter a desired subdomain slug (the part before <code>.dedyn.io</code>); leave it blank for a random one.</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="text" name="desec_slug" placeholder="my-nextcloud (optional)" pattern="[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?" title="Only lowercase letters, digits and hyphens (1–63 characters). No leading or trailing hyphen." />
|
||||
<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, set your router's DHCP DNS server to this machine's local IP address so LAN devices resolve the domain locally (see the <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq">dnsmasq documentation</a>). Alternatively adjust the hosts files on your clients so that they can reach the server using the local ip-address.</p>
|
||||
{% endif %}
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<h2>Restore former AIO instance from backup</h2>
|
||||
@@ -375,6 +399,14 @@
|
||||
|
||||
{% if was_start_button_clicked == true %}
|
||||
|
||||
{% if is_desec_domain %}
|
||||
<h2>deSEC account credentials</h2>
|
||||
<p>Your domain <strong>{{ domain }}</strong> is managed via <a target="_blank" href="https://desec.io">deSEC</a>. Below are your deSEC account credentials. You can use them to log in at <a target="_blank" href="https://desec.io">desec.io</a> to manage your domain directly.</p>
|
||||
<p>Email: <strong>{{ desec_email }}</strong></p>
|
||||
<p>Password: <details style="display:inline"><summary>Reveal deSEC password</summary><strong>{{ desec_password }}</strong></details></p>
|
||||
<p>Please save these credentials in a safe place.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if is_backup_section_enabled == false %}
|
||||
<h2>Backup and restore</h2>
|
||||
<p>The backup section is disabled via environmental variable.</p>
|
||||
|
||||
@@ -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> community container has been automatically enabled as a reverse proxy and the <strong>dnsmasq</strong> container has been automatically enabled so that LAN devices can resolve your Nextcloud domain to the server's local IP address. Please <a target="_blank" href="https://github.com/nextcloud/all-in-one/tree/main/community-containers/dnsmasq"><strong>read the dnsmasq documentation</strong></a> for the required router change.</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