Compare commits

..

3 Commits

Author SHA1 Message Date
Pablo Zmdl
624601b1d4 Use WORKDIR instead of cd
To fulfill DL3003

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-03-24 20:17:06 +01:00
Pablo Zmdl
a9c3daedb1 Fix wrong spelling
Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-03-24 20:15:06 +01:00
Pablo Zmdl
1039363de6 Allow arbitrary characters in passwords
This converts some shell scripted commands to small golang tools
("aio-container-tools") in order to ensure proper string handling.

In effect database passwords now can contain all characters, even emojis
and quotes.

AI-assistant: Copilot v1.0.7 (Claude Sonnet 4.6)

Signed-off-by: Pablo Zmdl <pablo@nextcloud.com>
2026-03-24 20:11:34 +01:00
25 changed files with 329 additions and 78 deletions

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:latest
# Docker CLI is a requirement
FROM docker:29.3.1-cli AS docker
FROM docker:29.3.0-cli AS docker
ARG CADDY_REMOTE_HOST_HASH=b21775afa730ffb52a24ddff310c8a6d1fd37276

View File

@@ -1,4 +1,11 @@
# syntax=docker/dockerfile:latest
FROM docker.io/library/golang:alpine AS aio-container-tools-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
WORKDIR /tmp/aio-container-tools
RUN go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
FROM php:8.3.30-fpm-alpine3.23
ENV PHP_MEMORY_LIMIT=512M
@@ -8,7 +15,7 @@ ENV SOURCE_LOCATION=/usr/src/nextcloud
ENV REDIS_DB_INDEX=0
# AIO settings start # Do not remove or change this line!
ENV NEXTCLOUD_VERSION=32.0.7
ENV NEXTCLOUD_VERSION=32.0.6
ENV AIO_TOKEN=123456
ENV AIO_URL=localhost
# AIO settings end # Do not remove or change this line!
@@ -17,6 +24,7 @@ COPY --chmod=775 Containers/nextcloud/*.sh /
COPY --chmod=774 Containers/nextcloud/upgrade.exclude /upgrade.exclude
COPY Containers/nextcloud/config/*.php /
COPY Containers/nextcloud/supervisord.conf /supervisord.conf
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/aio-pg-healthcheck
# AIO cloning start # Do not remove or change this line!
COPY app /usr/src/nextcloud/apps/nextcloud-aio
@@ -226,7 +234,6 @@ RUN set -ex; \
openssl \
gnupg \
git \
postgresql-client \
tzdata \
sudo \
grep \

View File

@@ -25,7 +25,7 @@ fi
# Fix false database connection on old instances
if [ -f "/var/www/html/config/config.php" ]; then
sleep 2
while ! sudo -E -u www-data psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" -c "select now()"; do
while ! sudo -E -u www-data /usr/local/bin/aio-pg-healthcheck; do
echo "Waiting for the database to start..."
sleep 5
done

View File

@@ -1,8 +1,18 @@
# syntax=docker/dockerfile:latest
FROM docker.io/library/golang:alpine AS aio-container-tools-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
WORKDIR /tmp/aio-container-tools
RUN go build -o /usr/local/bin/aio-pg-init ./cmd/aio-pg-init \
&& go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
# From https://github.com/docker-library/postgres/blob/master/17/alpine3.23/Dockerfile
FROM postgres:17.9-alpine
COPY --chmod=775 start.sh /start.sh
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-init /usr/local/bin/aio-pg-init
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/aio-pg-healthcheck
COPY --chmod=775 healthcheck.sh /healthcheck.sh
COPY --chmod=775 init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh

View File

@@ -2,6 +2,4 @@
test -f "/mnt/data/backup-is-running" && exit 0
psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()" && exit 0
psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:5432/$POSTGRES_DB" -c "select now()" || exit 1
POSTGRES_PORT=11000 /usr/local/bin/aio-pg-healthcheck debug || exec /usr/local/bin/aio-pg-healthcheck

View File

@@ -3,12 +3,7 @@ set -ex
touch "$DUMP_DIR/initialization.failed"
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER "oc_$POSTGRES_USER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB;
ALTER DATABASE "$POSTGRES_DB" OWNER TO "oc_$POSTGRES_USER";
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "oc_$POSTGRES_USER";
GRANT ALL PRIVILEGES ON SCHEMA public TO "oc_$POSTGRES_USER";
EOSQL
POSTGRES_DB_OWNER="oc_$POSTGRES_USER" /usr/local/bin/aio-pg-init
rm "$DUMP_DIR/initialization.failed"

View File

@@ -4,6 +4,7 @@
DATADIR="/var/lib/postgresql/data"
export DUMP_DIR="/mnt/data"
DUMP_FILE="$DUMP_DIR/database-dump.sql"
# TODO: Do we need this? It's not used anywhere visible
export PGPASSWORD="$POSTGRES_PASSWORD"
# Don't start database as long as backup is running
@@ -85,7 +86,7 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO
exec docker-entrypoint.sh postgres &
# Wait for creation
while ! psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()"; do
while ! env POSTGRES_PORT=11000 POSTGRES_USER="oc_$POSTGRES_USER" /usr/local/bin/aio-pg-healthcheck; do
echo "Waiting for the database to start."
sleep 5
done
@@ -107,12 +108,7 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO
exit 1
elif [ "$DB_OWNER" != "oc_$POSTGRES_USER" ]; then
DIFFERENT_DB_OWNER=1
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER "$DB_OWNER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB;
ALTER DATABASE "$POSTGRES_DB" OWNER TO "$DB_OWNER";
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$DB_OWNER";
GRANT ALL PRIVILEGES ON SCHEMA public TO "$DB_OWNER";
EOSQL
POSTGRES_DB_OWNER="$DB_OWNER" /usr/local/bin/aio-pg-init
fi
# Restore database

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:latest
# From https://github.com/redis/docker-library-redis/blob/release/8.2/alpine/Dockerfile
FROM redis:8.6.2-alpine
FROM redis:8.6.1-alpine
COPY --chmod=775 start.sh /start.sh

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:latest
FROM nats:2.12.6-scratch AS nats
FROM nats:2.12.5-scratch AS nats
FROM eturnal/eturnal:1.12.2-alpine AS eturnal
FROM strukturag/nextcloud-spreed-signaling:2.1.1 AS signaling
FROM alpine:3.23.3 AS janus

View File

@@ -0,0 +1,42 @@
# aio-container-tools
Standalone tools for Nextcloud AIO containers, for tasks that shouldn't be executed in a shell environment
(e.g. due to string handling issues).
Golang was chosen because it doesn't require additional runtimes in the containers, and has a pretty easy
syntax that is comprehensible even for people without much experience with the language.
The tools should be built in the container image build process, so they are built for the correct target
platform in multi-arch builds. See below for an example.
## Build process
To include the binary of `aio-pg-healhcheck` into your container image, include such a snippet into your Containerfile:
```dockerfile
FROM docker.io/library/golang:alpine AS golang-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
RUN cd /tmp/aio-container-tools \
&& go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
FROM your-base-image
COPY --from=golang-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/
```
To build it you now have to pass the aio-container-tools directory as additional, named build-context like this:
```bash
docker build \
--build-context aio-container-tools=/path/to/all-in-one/aio-container-tools \
.
```
#### Remote git variant (without local clone of this repo)
```bash
docker build \
--build-context aio-container-tools="https://github.com/nextcloud-releases/all-in-one.git#main:aio-container-tools" \
.
```

View File

@@ -0,0 +1,92 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"strconv"
"github.com/jackc/pgx/v5"
"github.com/nextcloud/aio-container-tools/internal/util"
)
// tryConnect opens a TCP connection to the given database host:port and runs SELECT 1.
// Returns nil on success, an error otherwise.
func tryConnect(ctx context.Context, host string, port uint16, user, password, database string) error {
util.Debugf("attempting connection: host=%s port=%d user=%s database=%s", host, port, user, database)
cfg, err := pgx.ParseConfig("")
if err != nil {
return err
}
cfg.Host = host
cfg.Port = port
cfg.User = user
cfg.Password = password
cfg.Database = database
conn, err := pgx.ConnectConfig(ctx, cfg)
if err != nil {
util.Debugf("connection failed: %v", err)
return err
}
defer conn.Close(ctx)
util.Debugf("connection established, running SELECT 1")
var result string
if err := conn.QueryRow(ctx, "SELECT 1").Scan(&result); err != nil {
util.Debugf("SELECT 1 failed: %v", err)
return err
}
util.Debugf("SELECT 1 returned %q", result)
return nil
}
// envOrDefault returns the value of the named environment variable,
// or the provided default if the variable is unset or empty.
func envOrDefault(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
util.Debugf("env %s = %q", key, v)
return v
}
util.Debugf("env %s not set, using default %q", key, defaultVal)
return defaultVal
}
func main() {
debug := flag.Bool("debug", false, "enable debug output")
flag.Parse()
util.SetDebug(*debug)
util.Debugf("reading required environment variables")
pgUser := util.RequireEnv("POSTGRES_USER")
pgPassword := util.RequireEnv("POSTGRES_PASSWORD")
pgDB := util.RequireEnv("POSTGRES_DB")
ctx := context.Background()
pgHost := envOrDefault("POSTGRES_HOST", "127.0.0.1")
var pgPort uint16 = 5432
if portStr := os.Getenv("POSTGRES_PORT"); portStr != "" {
util.Debugf("env POSTGRES_PORT = %q", portStr)
p, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid POSTGRES_PORT %q: %v\n", portStr, err)
os.Exit(1)
}
pgPort = uint16(p)
} else {
util.Debugf("env POSTGRES_PORT not set, using default port %d", pgPort)
}
util.Debugf("connecting to: host=%s port=%d user=%s", pgHost, pgPort, pgUser)
if err := tryConnect(ctx, pgHost, pgPort, pgUser, pgPassword, pgDB); err == nil {
util.Debugf("connection succeeded, exiting 0")
os.Exit(0)
}
util.Debugf("connection failed, exiting 1")
os.Exit(1)
}

View File

@@ -0,0 +1,78 @@
package main
import (
"context"
"flag"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/nextcloud/aio-container-tools/internal/util"
)
// quoteLiteral safely quotes a string as a PostgreSQL string literal.
// Single quotes are escaped by doubling them. This is safe with
// standard_conforming_strings=on (default since PostgreSQL 9.1).
func quoteLiteral(s string) string {
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
// main reimplements init-user-db.sh:
// - Creates $POSTGRES_DB_OWNER (falling back to $POSTGRES_USER) with $POSTGRES_PASSWORD and CREATEDB
// - Transfers ownership of $POSTGRES_DB to that user
// - Grants all privileges on the database and public schema
// - Connects using $POSTGRES_USER in all cases
func main() {
debug := flag.Bool("debug", false, "enable debug output")
flag.Parse()
util.SetDebug(*debug)
util.Debugf("reading required environment variables")
pgUser := util.RequireEnv("POSTGRES_USER")
pgPassword := util.RequireEnv("POSTGRES_PASSWORD")
pgDB := util.RequireEnv("POSTGRES_DB")
pgDBOwner := util.OptionalEnv("POSTGRES_DB_OWNER", pgUser)
util.Debugf("building connection config: host=/var/run/postgresql port=5432 user=%s database=%s", pgUser, pgDB)
cfg, err := pgx.ParseConfig("")
if err != nil {
util.ErrorOut(fmt.Errorf("building connection config: %w", err))
}
cfg.Host = "/var/run/postgresql"
cfg.Port = 5432
cfg.User = pgUser
cfg.Password = pgPassword
cfg.Database = pgDB
ctx := context.Background()
util.Debugf("connecting to postgres via unix socket")
conn, err := pgx.ConnectConfig(ctx, cfg)
if err != nil {
util.ErrorOut(fmt.Errorf("connecting to postgres: %w", err))
}
defer conn.Close(ctx)
util.Debugf("connected successfully")
dbOwner := pgDBOwner
util.Debugf("dbOwner = %q (from POSTGRES_DB_OWNER=%q, POSTGRES_USER=%q)", dbOwner, pgDBOwner, pgUser)
// pgx.Identifier.Sanitize() double-quotes and escapes the identifier safely.
dbOwnerIdent := pgx.Identifier{dbOwner}.Sanitize()
dbIdent := pgx.Identifier{pgDB}.Sanitize()
util.Debugf("quoted dbOwnerIdent = %s, dbIdent = %s", dbOwnerIdent, dbIdent)
statements := []string{
fmt.Sprintf("CREATE USER %s WITH PASSWORD %s CREATEDB", dbOwnerIdent, quoteLiteral(pgPassword)),
fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", dbIdent, dbOwnerIdent),
fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", dbIdent, dbOwnerIdent),
fmt.Sprintf("GRANT ALL PRIVILEGES ON SCHEMA public TO %s", dbOwnerIdent),
}
for i, stmt := range statements {
util.Debugf("executing statement %d/%d: %s", i+1, len(statements), stmt)
if _, err := conn.Exec(ctx, stmt); err != nil {
util.ErrorOut(fmt.Errorf("executing statement: %w", err))
}
util.Debugf("statement %d/%d succeeded", i+1, len(statements))
}
util.Debugf("all statements executed successfully")
}

View File

@@ -0,0 +1,10 @@
module github.com/nextcloud/aio-container-tools
go 1.25.1
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
golang.org/x/text v0.29.0 // indirect
)

View File

@@ -0,0 +1,15 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,49 @@
package util
import (
"fmt"
"log"
"os"
)
var debugEnabled bool
// SetDebug enables or disables debug output.
func SetDebug(enabled bool) {
debugEnabled = enabled
}
// Debugf prints a formatted debug message to stdout when debug mode is enabled.
func Debugf(format string, args ...any) {
if debugEnabled {
fmt.Printf("[debug] "+format+"\n", args...)
}
}
// RequireEnv returns the value of the named environment variable.
// It writes an error to stderr and exits with code 1 if the variable is unset or empty.
func RequireEnv(key string) string {
v := os.Getenv(key)
if v == "" {
fmt.Fprintf(os.Stderr, "required environment variable %q is not set\n", key)
os.Exit(1)
}
Debugf("env %s = %q", key, v)
return v
}
// OptionalEnv returns the value of the named environment variable, or fallback if it is unset or empty.
func OptionalEnv(key, fallback string) string {
v := os.Getenv(key)
if v == "" {
Debugf("env %s unset, using fallback %q", key, fallback)
return fallback
}
Debugf("env %s = %q", key, v)
return v
}
// ErrorOut logs the error with a standard prefix and exits with code 1.
func ErrorOut(err error) {
log.Fatalf("error: %v", err)
}

View File

@@ -54,9 +54,6 @@
"ui_secret": "SMBSERVER_PASSWORD",
"backup_volumes": [
"nextcloud_aio_smbserver"
],
"nextcloud_exec_commands": [
"php /var/www/html/occ config:system:set filesystem_check_changes --value=1 --type=integer"
]
}
]

View File

@@ -9,7 +9,7 @@ You can run AIO with docker rootless by following the steps below.
1. If you need ipv6 support, you should enable it by following https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md.
1. Do not forget to set the mentioned environmental variables `PATH` and `DOCKER_HOST` and in best case add them to your `~/.bashrc` file as shown!
1. Also do not forget to run `loginctl enable-linger USERNAME` (and substitute USERNAME with the correct one) in order to make sure that user services are automatically started after every reboot.
1. Expose the privileged ports by following https://docs.docker.com/engine/security/rootless/tips/#exposing-privileged-ports. (`sudo setcap cap_net_bind_service=ep $(which rootlesskit); systemctl --user restart docker`). If you require the correct source IP you must expose them via `/etc/sysctl.conf`, [see note below](#note-regarding-docker-network-driver).
1. Expose the privileged ports by following https://docs.docker.com/engine/security/rootless/#exposing-privileged-ports. (`sudo setcap cap_net_bind_service=ep $(which rootlesskit); systemctl --user restart docker`). If you require the correct source IP you must expose them via `/etc/sysctl.conf`, [see note below](#note-regarding-docker-network-driver).
1. Use the official AIO startup command but use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock:ro` instead of `--volume /var/run/docker.sock:/var/run/docker.sock:ro` and also add `--env WATCHTOWER_DOCKER_SOCKET_PATH=$XDG_RUNTIME_DIR/docker.sock` to the initial container startup (which is needed for mastercontainer updates to work correctly). When you are using Portainer to deploy AIO, the variable `$XDG_RUNTIME_DIR` is not available. In this case, it is necessary to manually add the path (e.g. `/run/user/1000/docker.sock`) to the Docker compose file to replace the `$XDG_RUNTIME_DIR` variable. If you are not sure how to get the path, you can run on the host: `echo $XDG_RUNTIME_DIR`.
1. Now everything should work like without docker rootless. You can consider using docker-compose for this or running it behind a reverse proxy. Basically the only thing that needs to be adjusted always in the startup command or compose.yaml file (after installing docker rootles) are things that are mentioned in point 3.
1. ⚠️ **Important:** Please read through all notes below!

View File

@@ -121,8 +121,10 @@ document.addEventListener("DOMContentLoaded", function () {
function handleDockerSocketProxyWarning() {
if (document.getElementById("docker-socket-proxy").checked) {
alert('⚠️ The docker socket proxy container is deprecated. Please use the HaRP (High-availability Reverse Proxy for Nextcloud ExApps) instead!');
document.getElementById("docker-socket-proxy").checked = false
// TODO: remove the line below and uncomment the lines further down once https://github.com/nextcloud/app_api/pull/800 is included
alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!');
// alert('⚠️ The docker socket proxy container is deprecated. Please use the HaRP (High-availability Reverse Proxy for Nextcloud ExApps) instead!');
// document.getElementById("docker-socket-proxy").checked = false
}
}

View File

@@ -29,9 +29,6 @@ function showPassword(id) {
const xhr = e.target;
if (xhr.status === 201) {
window.location.replace(xhr.getResponseHeader('Location'));
} else if ([422, 429].includes(xhr.status)) {
disableSpinner()
showError(xhr.response);
} else if (xhr.status === 422) {
disableSpinner()
showError(xhr.response);

View File

@@ -96,7 +96,7 @@ class LogViewer {
}
scrollToBottom() {
this.logElem.scrollTop = this.logElem.scrollHeight;
window.scrollTo(0, document.body.scrollHeight);
}
initAutoloadingControls() {

View File

@@ -11,52 +11,23 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
readonly class LoginController {
private const int MAX_LOGIN_ATTEMPTS_PER_TTL = 5;
private const int LOGIN_COUNTER_TTL = 300;
private const string RATE_LIMIT_CACHE_KEY = 'login_failed_attempts';
public function __construct(
private AuthManager $authManager,
private DockerActionManager $dockerActionManager,
) {
}
private function getFailedLoginCount() : int {
$count = apcu_fetch(self::RATE_LIMIT_CACHE_KEY);
return $count !== false ? (int)$count : 0;
}
private function incrementFailedLoginCount() : void {
if (!apcu_exists(self::RATE_LIMIT_CACHE_KEY)) {
apcu_store(self::RATE_LIMIT_CACHE_KEY, 1, self::LOGIN_COUNTER_TTL);
} else {
apcu_inc(self::RATE_LIMIT_CACHE_KEY);
}
}
private function resetFailedLoginCount() : void {
apcu_delete(self::RATE_LIMIT_CACHE_KEY);
}
public function TryLogin(Request $request, Response $response, array $args) : Response {
if (!$this->dockerActionManager->isLoginAllowed()) {
$response->getBody()->write("The login is blocked since Nextcloud is running.");
return $response->withHeader('Location', '.')->withStatus(422);
}
if ($this->getFailedLoginCount() >= self::MAX_LOGIN_ATTEMPTS_PER_TTL) {
$response->getBody()->write("Too many failed login attempts. Please try again in some minutes.");
return $response->withHeader('Location', '.')->withStatus(429);
}
$password = $request->getParsedBody()['password'] ?? '';
if($this->authManager->CheckCredentials($password)) {
$this->resetFailedLoginCount();
$this->authManager->SetAuthState(true);
return $response->withHeader('Location', '.')->withStatus(201);
}
$this->incrementFailedLoginCount();
$response->getBody()->write("The password is incorrect.");
return $response->withHeader('Location', '.')->withStatus(422);
}

View File

@@ -27,7 +27,7 @@
<script type="text/javascript" src="timezone.js"></script>
{# js for optional containers and additional containers forms #}
<script type="text/javascript" src="containers-form-submit.js?v7"></script>
<script type="text/javascript" src="containers-form-submit.js?v6"></script>
{% set hasBackupLocation = borg_backup_host_location or borg_remote_repo %}
{% set isAnyRunning = false %}

View File

@@ -1 +1 @@
12.9.1
12.9.0

View File

@@ -198,6 +198,7 @@
>
<label for="docker-socket-proxy">Docker Socket Proxy (needed for <a target="_blank" href="https://github.com/cloud-py-api/app_api#nextcloud-appapi">Nextcloud App API</a>) ⚠️ The docker socket proxy container is deprecated. Please use the HaRP (High-availability Reverse Proxy for Nextcloud ExApps) instead!</label>
</p>
{#
<p>
<input
type="checkbox"
@@ -212,6 +213,7 @@
>
<label for="harp">HaRP (<a target="_blank" href="https://github.com/nextcloud/HaRP">High-availability Reverse Proxy</a> for Nextcloud ExApps)</label>
</p>
#}
<p>
<input
type="checkbox"

View File

@@ -3,25 +3,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="style.css">
<style>
html, body {
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
pre {
height: 100%;
overflow: auto;
margin: 0;
body {
padding: 1rem;
box-sizing: border-box;
}
#floating-box {
position: fixed;
position: sticky;
top: 1rem;
float: right;
right: 1rem;
max-width: calc(100vw - 2rem);
z-index: 10;
width: 20rem;
display: flex;
justify-content: end;
align-items: center;
@@ -53,7 +43,7 @@
transition: opacity 1s, display 1s allow-discrete;
}
</style>
<script src="log-view.js?v1"></script>
<script src="log-view.js"></script>
</head>
<body data-container-id="{{ id }}">
<div id="floating-box">