From b7a04422981b3eabea37bcea97c978bfc4c72b7f Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Wed, 23 Jul 2025 00:24:27 +0100 Subject: [PATCH 01/40] feat: Musl images in docker Not working at the moment, need to upgrade the rust-rocksdb and possibly zstd to stop them force-enabling dynamic libclang --- Cargo.lock | 1 + docker/Dockerfile | 41 +++++---- docker/musl.Dockerfile | 201 +++++++++++++++++++++++++++++++++++++++++ src/main/Cargo.toml | 9 ++ 4 files changed, 233 insertions(+), 19 deletions(-) create mode 100644 docker/musl.Dockerfile diff --git a/Cargo.lock b/Cargo.lock index 22c90e17..1866a691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,6 +847,7 @@ dependencies = [ name = "conduwuit" version = "0.5.0-rc.6" dependencies = [ + "bindgen 0.71.1", "clap", "conduwuit_admin", "conduwuit_api", diff --git a/docker/Dockerfile b/docker/Dockerfile index bd6e72d1..55150902 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -78,7 +78,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ WORKDIR /app COPY ./rust-toolchain.toml . RUN rustc --version \ - && rustup target add $(xx-cargo --print-target-triple) + && xx-cargo --setup-target-triple # Build binary # We disable incremental compilation to save disk space, as it only produces a minimal speedup for this case. @@ -87,8 +87,10 @@ RUN echo "CARGO_INCREMENTAL=0" >> /etc/environment # Configure pkg-config RUN <> /etc/environment - echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment + if command -v "$(xx-info)-pkg-config" >/dev/null 2>/dev/null; then + echo "PKG_CONFIG_LIBDIR=/usr/lib/$(xx-info)/pkgconfig" >> /etc/environment + echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment + fi echo "PKG_CONFIG_ALLOW_CROSS=true" >> /etc/environment EOF @@ -109,16 +111,17 @@ RUN <> /etc/environment - echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment - echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment - fi + set -o allexport + set -o xtrace + . /etc/environment + if [ -n "${TARGET_CPU}" ]; then + echo "CFLAGS='${CFLAGS} -march=${TARGET_CPU}'" >> /etc/environment + echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment + echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment + fi EOF # Prepare output directories @@ -136,12 +139,12 @@ ARG TARGETPLATFORM RUN xx-cargo --print-target-triple # Conduwuit version info -ARG GIT_COMMIT_HASH= -ARG GIT_COMMIT_HASH_SHORT= -ARG GIT_REMOTE_URL= -ARG GIT_REMOTE_COMMIT_URL= -ARG CONDUWUIT_VERSION_EXTRA= -ARG CONTINUWUITY_VERSION_EXTRA= +ARG GIT_COMMIT_HASH +ARG GIT_COMMIT_HASH_SHORT +ARG GIT_REMOTE_URL +ARG GIT_REMOTE_COMMIT_URL +ARG CONDUWUIT_VERSION_EXTRA +ARG CONTINUWUITY_VERSION_EXTRA ENV GIT_COMMIT_HASH=$GIT_COMMIT_HASH ENV GIT_COMMIT_HASH_SHORT=$GIT_COMMIT_HASH_SHORT ENV GIT_REMOTE_URL=$GIT_REMOTE_URL @@ -169,7 +172,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name")) for BINARY in "${BINARIES[@]}"; do echo $BINARY - xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY + xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY cp $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY /out/sbin/$BINARY done EOF diff --git a/docker/musl.Dockerfile b/docker/musl.Dockerfile new file mode 100644 index 00000000..d5317d15 --- /dev/null +++ b/docker/musl.Dockerfile @@ -0,0 +1,201 @@ +# Why does this exist? +# Debian doesn't provide prebuilt musl packages +# rocksdb requires a prebuilt liburing, and linking fails if a gnu one is provided + +ARG RUST_VERSION=1 +ARG ALPINE_VERSION=3.22 + +FROM --platform=$BUILDPLATFORM docker.io/tonistiigi/xx AS xx +FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS base +FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS toolchain + +# Install repo tools and dependencies +RUN --mount=type=cache,target=/etc/apk/cache apk add \ + build-base pkgconfig make jq bash \ + curl git file \ + llvm-dev clang clang-dev lld + + +# Developer tool versions +# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall +ENV BINSTALL_VERSION=1.13.0 +# renovate: datasource=github-releases depName=psastras/sbom-rs +ENV CARGO_SBOM_VERSION=0.9.1 +# renovate: datasource=crate depName=lddtree +ENV LDDTREE_VERSION=0.3.7 + +# Install unpackaged tools +RUN <> /etc/environment + +# Configure pkg-config +RUN </dev/null 2>/dev/null; then + echo "PKG_CONFIG_LIBDIR=/usr/lib/$(xx-info)/pkgconfig" >> /etc/environment + echo "PKG_CONFIG=/usr/bin/$(xx-info)-pkg-config" >> /etc/environment + fi + echo "PKG_CONFIG_ALLOW_CROSS=true" >> /etc/environment +EOF + +# Configure cc to use clang version +RUN <> /etc/environment + echo "CXX=clang++" >> /etc/environment +EOF + +# Cross-language LTO +RUN <> /etc/environment + echo "CXXFLAGS=-flto" >> /etc/environment + # Linker is set to target-compatible clang by xx + echo "RUSTFLAGS='-Clinker-plugin-lto -Clink-arg=-fuse-ld=lld'" >> /etc/environment +EOF + +# Apply CPU-specific optimizations if TARGET_CPU is provided +ARG TARGET_CPU + +RUN <> /etc/environment + echo "CXXFLAGS='${CXXFLAGS} -march=${TARGET_CPU}'" >> /etc/environment + echo "RUSTFLAGS='${RUSTFLAGS} -C target-cpu=${TARGET_CPU}'" >> /etc/environment + fi +EOF + +# Prepare output directories +RUN mkdir /out + +FROM toolchain AS builder + + +# Get source +COPY . . + +ARG TARGETPLATFORM + +# Verify environment configuration +RUN xx-cargo --print-target-triple + +# Conduwuit version info +ARG GIT_COMMIT_HASH +ARG GIT_COMMIT_HASH_SHORT +ARG GIT_REMOTE_URL +ARG GIT_REMOTE_COMMIT_URL +ARG CONDUWUIT_VERSION_EXTRA +ARG CONTINUWUITY_VERSION_EXTRA +ENV GIT_COMMIT_HASH=$GIT_COMMIT_HASH +ENV GIT_COMMIT_HASH_SHORT=$GIT_COMMIT_HASH_SHORT +ENV GIT_REMOTE_URL=$GIT_REMOTE_URL +ENV GIT_REMOTE_COMMIT_URL=$GIT_REMOTE_COMMIT_URL +ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA +ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA + +ARG RUST_PROFILE=release + +# Build the binary +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git/db \ + --mount=type=cache,target=/app/target,id=cargo-target-${TARGET_CPU}-${TARGETPLATFORM}-musl-${RUST_PROFILE} \ + bash <<'EOF' + set -o allexport + set -o xtrace + . /etc/environment + TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \ + jq -r ".target_directory")) + mkdir /out/sbin + PACKAGE=conduwuit + xx-cargo build --locked --profile ${RUST_PROFILE} \ + -p $PACKAGE --features libclang_static; + BINARIES=($(cargo metadata --no-deps --format-version 1 | \ + jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name")) + for BINARY in "${BINARIES[@]}"; do + echo $BINARY + xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY + cp $TARGET_DIR/$(xx-cargo --print-target-triple)/release/$BINARY /out/sbin/$BINARY + done +EOF + +# Generate Software Bill of Materials (SBOM) +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/cargo/git/db \ + bash <<'EOF' + set -o xtrace + mkdir /out/sbom + typeset -A PACKAGES + for BINARY in /out/sbin/*; do + BINARY_BASE=$(basename ${BINARY}) + package=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.targets[] | select( .kind | map(. == \"bin\") | any ) | .name == \"$BINARY_BASE\") | .name") + if [ -z "$package" ]; then + continue + fi + PACKAGES[$package]=1 + done + for PACKAGE in $(echo ${!PACKAGES[@]}); do + echo $PACKAGE + cargo sbom --cargo-package $PACKAGE > /out/sbom/$PACKAGE.spdx.json + done +EOF + +# Extract dynamically linked dependencies +RUN < Date: Thu, 24 Jul 2025 21:51:52 +0100 Subject: [PATCH 02/40] chore: Update rocksdb, feature flag changes Most of the way to static musl builds, just zlib I think --- .forgejo/workflows/rust-checks.yml | 4 ++-- CONTRIBUTING.md | 6 +++--- Cargo.lock | 29 +++++++++++++++++++++++------ Cargo.toml | 2 +- docker/musl.Dockerfile | 2 +- docs/deploying/generic.md | 2 +- engage.toml | 6 +++--- src/database/Cargo.toml | 8 ++++++++ src/main/Cargo.toml | 27 +++++++++++++++++++++------ tests/cargo_smoke.sh | 2 +- 10 files changed, 64 insertions(+), 24 deletions(-) diff --git a/.forgejo/workflows/rust-checks.yml b/.forgejo/workflows/rust-checks.yml index 0595c053..c46363a0 100644 --- a/.forgejo/workflows/rust-checks.yml +++ b/.forgejo/workflows/rust-checks.yml @@ -73,7 +73,7 @@ jobs: run: | cargo clippy \ --workspace \ - --all-features \ + --features full \ --locked \ --no-deps \ --profile test \ @@ -133,7 +133,7 @@ jobs: run: | cargo test \ --workspace \ - --all-features \ + --features full \ --locked \ --profile test \ --all-targets \ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e11cff4..b5ecf38a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,11 +65,11 @@ Tests, compilation, and linting can be run with standard Cargo commands: cargo test # Check compilation -cargo check --workspace --all-features +cargo check --workspace --features full # Run lints -cargo clippy --workspace --all-features -# Auto-fix: cargo clippy --workspace --all-features --fix --allow-staged; +cargo clippy --workspace --features full +# Auto-fix: cargo clippy --workspace --features full --fix --allow-staged; # Format code (must use nightly) cargo +nightly fmt diff --git a/Cargo.lock b/Cargo.lock index 1866a691..bfab6255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,24 @@ dependencies = [ "syn", ] +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -847,7 +865,6 @@ dependencies = [ name = "conduwuit" version = "0.5.0-rc.6" dependencies = [ - "bindgen 0.71.1", "clap", "conduwuit_admin", "conduwuit_api", @@ -4117,10 +4134,10 @@ dependencies = [ [[package]] name = "rust-librocksdb-sys" -version = "0.33.0+9.11.1" -source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd#fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd" +version = "0.38.0+10.4.2" +source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=01e1128898fc4bbb776e7a6deec2aa3b675b0442#01e1128898fc4bbb776e7a6deec2aa3b675b0442" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.0", "bzip2-sys", "cc", "glob", @@ -4134,8 +4151,8 @@ dependencies = [ [[package]] name = "rust-rocksdb" -version = "0.37.0" -source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd#fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd" +version = "0.42.1" +source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=01e1128898fc4bbb776e7a6deec2aa3b675b0442#01e1128898fc4bbb776e7a6deec2aa3b675b0442" dependencies = [ "libc", "rust-librocksdb-sys", diff --git a/Cargo.toml b/Cargo.toml index 9cb5ff84..e95567bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -391,7 +391,7 @@ features = [ [workspace.dependencies.rust-rocksdb] git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1" -rev = "fc9a99ac54a54208f90fdcba33ae6ee8bc3531dd" +rev = "01e1128898fc4bbb776e7a6deec2aa3b675b0442" default-features = false features = [ "multi-threaded-cf", diff --git a/docker/musl.Dockerfile b/docker/musl.Dockerfile index d5317d15..b55aecb0 100644 --- a/docker/musl.Dockerfile +++ b/docker/musl.Dockerfile @@ -133,7 +133,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ mkdir /out/sbin PACKAGE=conduwuit xx-cargo build --locked --profile ${RUST_PROFILE} \ - -p $PACKAGE --features libclang_static; + -p $PACKAGE --no-default-features --features bindgen-static,release_max_log_level,standard; BINARIES=($(cargo metadata --no-deps --format-version 1 | \ jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name")) for BINARY in "${BINARIES[@]}"; do diff --git a/docs/deploying/generic.md b/docs/deploying/generic.md index 3f9d1a16..9f5051f7 100644 --- a/docs/deploying/generic.md +++ b/docs/deploying/generic.md @@ -44,7 +44,7 @@ If wanting to build using standard Rust toolchains, make sure you install: - (On linux) `pkg-config` on the compiling machine to allow finding `liburing` - A C++ compiler and (on linux) `libclang` for RocksDB -You can build Continuwuity using `cargo build --release --all-features`. +You can build Continuwuity using `cargo build --release`. ### Building with Nix diff --git a/engage.toml b/engage.toml index 5048fde2..92e9d399 100644 --- a/engage.toml +++ b/engage.toml @@ -83,7 +83,7 @@ env DIRENV_DEVSHELL=all-features \ --workspace \ --locked \ --profile test \ - --all-features \ + --features full \ --no-deps \ --document-private-items \ --color always @@ -96,7 +96,7 @@ script = """ direnv exec . \ cargo clippy \ --workspace \ - --all-features \ + --features full \ --locked \ --profile test \ --color=always \ @@ -114,7 +114,7 @@ env DIRENV_DEVSHELL=all-features \ --workspace \ --locked \ --profile test \ - --all-features \ + --features full \ --color=always \ -- \ -D warnings diff --git a/src/database/Cargo.toml b/src/database/Cargo.toml index 55d4793f..9f51f366 100644 --- a/src/database/Cargo.toml +++ b/src/database/Cargo.toml @@ -44,6 +44,14 @@ zstd_compression = [ "conduwuit-core/zstd_compression", "rust-rocksdb/zstd", ] +bindgen-static = [ + # "bindgen/static" + # "clang-sys/static" + "rust-rocksdb/bindgen-static" +] +bindgen-runtime = [ + "rust-rocksdb/bindgen-runtime" +] [dependencies] async-channel.workspace = true diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index 3b97f625..0d7dd844 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -43,6 +43,11 @@ assets = [ [features] default = [ + "standard", + "release_max_log_level", + "bindgen-runtime", # replace with bindgen-static on alpine +] +standard = [ "blurhashing", "brotli_compression", "element_hacks", @@ -52,10 +57,17 @@ default = [ "jemalloc_conf", "journald", "media_thumbnail", - "release_max_log_level", "systemd", "url_preview", - "zstd_compression", + "zstd_compression" +] +full = [ + "standard", + "hardened_malloc", + "jemalloc_prof", + "perf_measurements", + "tokio_console" + # sentry_telemetry ] blurhashing = [ @@ -161,14 +173,17 @@ zstd_compression = [ conduwuit_mods = [ "conduwuit-core/conduwuit_mods", ] - -libclang_static = [ - "bindgen/static" +bindgen-static = [ + # "bindgen/static" # "clang-sys/static" + "conduwuit-database/bindgen-static" +] +bindgen-runtime = [ + "conduwuit-database/bindgen-runtime" ] [build-dependencies] -bindgen = {version = "0.71.1", default-features = false} +# bindgen = {version = "0.71.1", default-features = false} # clang-sys = {version = "1", default-features = false} [dependencies] diff --git a/tests/cargo_smoke.sh b/tests/cargo_smoke.sh index 946790c3..e8bb5512 100755 --- a/tests/cargo_smoke.sh +++ b/tests/cargo_smoke.sh @@ -48,7 +48,7 @@ vector () { VECTOR_OPTS=$@ element "$TOOLCHAIN" $VECTOR_OPTS --no-default-features element "$TOOLCHAIN" $VECTOR_OPTS --features=default - element "$TOOLCHAIN" $VECTOR_OPTS --all-features + element "$TOOLCHAIN" $VECTOR_OPTS --features full } matrix () { From 205506f206fef4c5fb51141449476fab8af151ef Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Thu, 24 Jul 2025 22:18:10 +0100 Subject: [PATCH 03/40] chore: Update deps --- Cargo.lock | 223 ++++++++++++++++++++++++++++------------------------- 1 file changed, 118 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfab6255..837e033a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,9 +52,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -73,27 +73,27 @@ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -217,9 +217,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f6024f3f856663b45fd0c9b6f2024034a702f453549449e0d84a305900dad4" +checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" dependencies = [ "brotli", "flate2", @@ -301,18 +301,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" dependencies = [ "arrayvec", ] [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ "bindgen 0.69.5", "cc", @@ -552,7 +552,7 @@ dependencies = [ "bitflags 2.9.1", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -647,9 +647,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" @@ -703,9 +703,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ "jobserver", "libc", @@ -774,9 +774,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -793,9 +793,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -823,9 +823,9 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "clap_mangen" -version = "0.2.26" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" +checksum = "e2fb6d3f935bbb9819391528b0e7cf655e78a0bc7a7c3d227211a1d24fc11db1" dependencies = [ "clap", "roff", @@ -848,9 +848,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" @@ -1173,15 +1173,15 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-str" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e991226a70654b49d34de5ed064885f0bef0348a8e70018b8ff1ac80aa984a2" +checksum = "041fbfcf8e7054df725fb9985297e92422cdc80fcf313665f5ca3d761bb63f4c" [[package]] name = "const_panic" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" +checksum = "b98d1483e98c9d67f341ab4b3915cfdc54740bd6f5cccc9226ee0535d86aa8fb" [[package]] name = "convert_case" @@ -1238,9 +1238,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1346,7 +1346,7 @@ dependencies = [ "futures-core", "mio", "parking_lot", - "rustix 1.0.7", + "rustix 1.0.8", "signal-hook", "signal-hook-mio", "winapi", @@ -1363,9 +1363,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -1531,9 +1531,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", @@ -1885,9 +1885,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -1907,9 +1907,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -1917,7 +1917,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2047,7 +2047,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.1", + "rand 0.9.2", "ring", "serde", "thiserror 2.0.12", @@ -2091,7 +2091,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.1", + "rand 0.9.2", "resolv-conf", "serde", "smallvec", @@ -2240,7 +2240,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.1", + "webpki-roots 1.0.2", ] [[package]] @@ -2433,9 +2433,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -2465,6 +2465,17 @@ dependencies = [ "syn", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipaddress" version = "0.1.3" @@ -2548,9 +2559,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" @@ -2649,9 +2660,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -3131,7 +3142,7 @@ checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" dependencies = [ "futures-core", "futures-sink", - "indexmap 2.9.0", + "indexmap 2.10.0", "js-sys", "once_cell", "pin-project-lite", @@ -3307,7 +3318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.10.0", ] [[package]] @@ -3398,12 +3409,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.7.2" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.9.0", + "indexmap 2.10.0", "quick-xml", "serde", "time", @@ -3460,9 +3471,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn", @@ -3501,18 +3512,18 @@ dependencies = [ [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", "syn", @@ -3585,9 +3596,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" dependencies = [ "memchr", ] @@ -3621,7 +3632,7 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand 0.9.1", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", "rustls", @@ -3675,9 +3686,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -3758,9 +3769,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.12" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", @@ -3804,9 +3815,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ "bitflags 2.9.1", ] @@ -3912,9 +3923,9 @@ source = "git+https://forgejo.ellis.link/continuwuation/resolv-conf?rev=56251316 [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" [[package]] name = "ring" @@ -4002,7 +4013,7 @@ dependencies = [ "form_urlencoded", "getrandom 0.2.16", "http", - "indexmap 2.9.0", + "indexmap 2.10.0", "js_int", "konst", "percent-encoding", @@ -4029,7 +4040,7 @@ version = "0.28.1" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=b753738047d1f443aca870896ef27ecaacf027da#b753738047d1f443aca870896ef27ecaacf027da" dependencies = [ "as_variant", - "indexmap 2.9.0", + "indexmap 2.10.0", "js_int", "js_option", "percent-encoding", @@ -4200,22 +4211,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "aws-lc-rs", "log", @@ -4260,9 +4271,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -4526,7 +4537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" dependencies = [ "form_urlencoded", - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -4534,9 +4545,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -4591,7 +4602,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "itoa", "ryu", "serde", @@ -5069,16 +5080,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", "tracing", @@ -5098,9 +5111,9 @@ dependencies = [ [[package]] name = "tokio-metrics" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7817b32d36c9b94744d7aa3f8fc13526aa0f5112009d7045f3c659413a6e44ac" +checksum = "23ff82f660c98e4ff60da5eb8fa864a4130f34b56d92d5cd23d6fdfcc14e95fa" dependencies = [ "futures-util", "pin-project-lite", @@ -5181,7 +5194,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.10.0", "serde", "serde_spanned", "toml_datetime", @@ -5405,9 +5418,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typewit" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +checksum = "97e72ba082eeb9da9dc68ff5a2bf727ef6ce362556e8d29ec1aed3bd05e7d86a" dependencies = [ "typewit_proc_macros", ] @@ -5719,14 +5732,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.1", + "webpki-roots 1.0.2", ] [[package]] name = "webpki-roots" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -6137,9 +6150,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -6356,9 +6369,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] From 87be4d1a52d6bd1680a47153f020affc166d1a07 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Thu, 24 Jul 2025 23:22:07 +0100 Subject: [PATCH 04/40] feat: Almost-functional musl builds on Alpine Lots of fiddling, still can't get stuff to work Next step is a debian builder copying the static libs from alpine --- Cargo.lock | 23 ++--------------------- Cargo.toml | 2 +- docker/musl.Dockerfile | 5 ++--- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 837e033a..e5c111d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,24 +525,6 @@ dependencies = [ "which", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn", -] - [[package]] name = "bindgen" version = "0.72.0" @@ -4146,7 +4128,7 @@ dependencies = [ [[package]] name = "rust-librocksdb-sys" version = "0.38.0+10.4.2" -source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=01e1128898fc4bbb776e7a6deec2aa3b675b0442#01e1128898fc4bbb776e7a6deec2aa3b675b0442" +source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=99b0319416b64830dd6f8943e1f65e15aeef18bc#99b0319416b64830dd6f8943e1f65e15aeef18bc" dependencies = [ "bindgen 0.72.0", "bzip2-sys", @@ -4163,7 +4145,7 @@ dependencies = [ [[package]] name = "rust-rocksdb" version = "0.42.1" -source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=01e1128898fc4bbb776e7a6deec2aa3b675b0442#01e1128898fc4bbb776e7a6deec2aa3b675b0442" +source = "git+https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1?rev=99b0319416b64830dd6f8943e1f65e15aeef18bc#99b0319416b64830dd6f8943e1f65e15aeef18bc" dependencies = [ "libc", "rust-librocksdb-sys", @@ -6347,7 +6329,6 @@ version = "2.0.15+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" dependencies = [ - "bindgen 0.71.1", "cc", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index e95567bd..e7abca01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -391,7 +391,7 @@ features = [ [workspace.dependencies.rust-rocksdb] git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1" -rev = "01e1128898fc4bbb776e7a6deec2aa3b675b0442" +rev = "99b0319416b64830dd6f8943e1f65e15aeef18bc" default-features = false features = [ "multi-threaded-cf", diff --git a/docker/musl.Dockerfile b/docker/musl.Dockerfile index b55aecb0..2221525b 100644 --- a/docker/musl.Dockerfile +++ b/docker/musl.Dockerfile @@ -13,7 +13,7 @@ FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-alpine${ALPINE_VERSION} AS t RUN --mount=type=cache,target=/etc/apk/cache apk add \ build-base pkgconfig make jq bash \ curl git file \ - llvm-dev clang clang-dev lld + llvm-dev clang clang-static lld # Developer tool versions @@ -37,8 +37,7 @@ COPY --from=xx / / ARG TARGETPLATFORM # Install libraries linked by the binary -RUN --mount=type=cache,target=/etc/apk/cache xx-apk add musl-dev liburing-dev clang-dev - +RUN --mount=type=cache,target=/etc/apk/cache xx-apk add musl-dev gcc g++ liburing-dev # Set up Rust toolchain WORKDIR /app From 00c7e220bbefaca35b44d64ec0af4d3d146a0fbd Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 25 Jul 2025 14:10:06 +0100 Subject: [PATCH 05/40] chore: Release --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5c111d7..5d7192b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -845,7 +845,7 @@ dependencies = [ [[package]] name = "conduwuit" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "clap", "conduwuit_admin", @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "conduwuit_admin" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "clap", "conduwuit_api", @@ -896,7 +896,7 @@ dependencies = [ [[package]] name = "conduwuit_api" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "async-trait", "axum", @@ -928,14 +928,14 @@ dependencies = [ [[package]] name = "conduwuit_build_metadata" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "built 0.8.0", ] [[package]] name = "conduwuit_core" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "argon2", "arrayvec", @@ -996,7 +996,7 @@ dependencies = [ [[package]] name = "conduwuit_database" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "async-channel", "conduwuit_core", @@ -1014,7 +1014,7 @@ dependencies = [ [[package]] name = "conduwuit_macros" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "itertools 0.14.0", "proc-macro2", @@ -1024,7 +1024,7 @@ dependencies = [ [[package]] name = "conduwuit_router" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "axum", "axum-client-ip", @@ -1058,7 +1058,7 @@ dependencies = [ [[package]] name = "conduwuit_service" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "async-trait", "base64 0.22.1", @@ -1096,7 +1096,7 @@ dependencies = [ [[package]] name = "conduwuit_web" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "askama", "axum", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "xtask" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "clap", "serde", @@ -6186,7 +6186,7 @@ dependencies = [ [[package]] name = "xtask-generate-commands" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" dependencies = [ "clap-markdown", "clap_builder", diff --git a/Cargo.toml b/Cargo.toml index e7abca01..c656e183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ license = "Apache-2.0" readme = "README.md" repository = "https://forgejo.ellis.link/continuwuation/continuwuity" rust-version = "1.86.0" -version = "0.5.0-rc.6" +version = "0.5.0-rc.7" [workspace.metadata.crane] name = "conduwuit" From aa08edc55f2f14d3c819b71fb13bc4ac89f0a540 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 25 Jul 2025 17:30:31 +0100 Subject: [PATCH 06/40] chore: Release announcement --- docs/static/announcements.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/static/announcements.json b/docs/static/announcements.json index 7dd2fb72..8068535c 100644 --- a/docs/static/announcements.json +++ b/docs/static/announcements.json @@ -6,8 +6,8 @@ "message": "Welcome to Continuwuity! Important announcements about the project will appear here." }, { - "id": 2, - "message": "🎉 Continuwuity v0.5.0-rc.6 is now available! This release includes improved knock-restricted room handling, automatic support contact configuration, and a new HTML landing page. Check [the release notes for full details](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.6) and upgrade instructions." + "id": 3, + "message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0 to get stuff like this sooner." } ] } From 4a83df5b57b5872a3006b93723df512ac3d51507 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 25 Jul 2025 17:35:18 +0100 Subject: [PATCH 07/40] chore: Fix link --- docs/static/announcements.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/static/announcements.json b/docs/static/announcements.json index 8068535c..fe27859b 100644 --- a/docs/static/announcements.json +++ b/docs/static/announcements.json @@ -7,7 +7,7 @@ }, { "id": 3, - "message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0 to get stuff like this sooner." + "message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner." } ] } From 058988410986b0ba45f19181a3113d216beebe9e Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Mon, 28 Jul 2025 19:28:34 +0100 Subject: [PATCH 08/40] docs: Fix documentation link in README Closes https://forgejo.ellis.link/continuwuation/continuwuity/issues/913 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e63fe80f..a5f77a5b 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Continuwuity aims to: ### Can I try it out? -Check out the [documentation](introduction) for installation instructions. +Check out the [documentation](https://continuwuity.org) for installation instructions. There are currently no open registration Continuwuity instances available. From b1516209c403f7075d11c2b33a7674ce84be96f1 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Wed, 30 Jul 2025 19:23:38 +0100 Subject: [PATCH 09/40] chore: Update funding file --- .github/FUNDING.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index fcfaade5..841427b7 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,4 @@ -github: [JadedBlueEyes] -# Doesn't support an array, so we can only list nex -ko_fi: nexy7574 +github: [JadedBlueEyes, nexy7574] custom: + - https://ko-fi.com/nexy7574 - https://ko-fi.com/JadedBlueEyes From 238cc627e3622a89c22b068987b1a88b3c9b6eb1 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Wed, 30 Jul 2025 19:33:53 +0100 Subject: [PATCH 10/40] docs: Set traefik labels --- docs/deploying/docker-compose.for-traefik.yml | 9 +++++++++ docs/deploying/docker-compose.with-traefik.yml | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/deploying/docker-compose.for-traefik.yml b/docs/deploying/docker-compose.for-traefik.yml index 547712b6..f67e603b 100644 --- a/docs/deploying/docker-compose.for-traefik.yml +++ b/docs/deploying/docker-compose.for-traefik.yml @@ -12,6 +12,15 @@ services: #- ./continuwuity.toml:/etc/continuwuity.toml networks: - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" + - "traefik.http.routers.continuwuity.entrypoints=websecure" # your HTTPS entry point + - "traefik.http.routers.continuwuity.tls=true" + - "traefik.http.routers.continuwuity.service=continuwuity" + - "traefik.http.services.continuwuity.loadbalancer.server.port=6167" + # possibly, depending on your config: + # - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt" environment: CONTINUWUITY_SERVER_NAME: your.server.name.example # EDIT THIS CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity diff --git a/docs/deploying/docker-compose.with-traefik.yml b/docs/deploying/docker-compose.with-traefik.yml index 49b7c905..b41a6fbc 100644 --- a/docs/deploying/docker-compose.with-traefik.yml +++ b/docs/deploying/docker-compose.with-traefik.yml @@ -12,6 +12,14 @@ services: #- ./continuwuity.toml:/etc/continuwuity.toml networks: - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.homeserver.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" + - "traefik.http.routers.homeserver.entrypoints=websecure" + - "traefik.http.routers.homeserver.tls.certresolver=letsencrypt" + - "traefik.http.services.homeserver.loadbalancer.server.port=6167" + # Uncomment and adjust the following if you want to use middleware + # - "traefik.http.routers.homeserver.middlewares=secureHeaders@file" environment: CONTINUWUITY_SERVER_NAME: your.server.name.example # EDIT THIS CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]' From 5775e0ad9d685fc5a55ff979ee3c0bd82ee88ea1 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Wed, 30 Jul 2025 19:55:48 +0100 Subject: [PATCH 11/40] docs: Make traefik router names consistent --- docs/deploying/docker-compose.with-traefik.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/deploying/docker-compose.with-traefik.yml b/docs/deploying/docker-compose.with-traefik.yml index b41a6fbc..8021e034 100644 --- a/docs/deploying/docker-compose.with-traefik.yml +++ b/docs/deploying/docker-compose.with-traefik.yml @@ -14,12 +14,12 @@ services: - proxy labels: - "traefik.enable=true" - - "traefik.http.routers.homeserver.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" - - "traefik.http.routers.homeserver.entrypoints=websecure" - - "traefik.http.routers.homeserver.tls.certresolver=letsencrypt" - - "traefik.http.services.homeserver.loadbalancer.server.port=6167" + - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" + - "traefik.http.routers.continuwuity.entrypoints=websecure" + - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt" + - "traefik.http.services.continuwuity.loadbalancer.server.port=6167" # Uncomment and adjust the following if you want to use middleware - # - "traefik.http.routers.homeserver.middlewares=secureHeaders@file" + # - "traefik.http.routers.continuwuity.middlewares=secureHeaders@file" environment: CONTINUWUITY_SERVER_NAME: your.server.name.example # EDIT THIS CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]' From e4a43b1a5b31bfe5267fafa01b177e0f992d150e Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 2 Aug 2025 00:19:33 +0100 Subject: [PATCH 12/40] fix(policy-server): Call the PS later in the PDU creation process This avoids accidentally sending partially built PDUs to the policy server, which may cause issues with some implementations --- src/service/rooms/timeline/create.rs | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/service/rooms/timeline/create.rs b/src/service/rooms/timeline/create.rs index 66a35eca..6732cd8e 100644 --- a/src/service/rooms/timeline/create.rs +++ b/src/service/rooms/timeline/create.rs @@ -165,25 +165,6 @@ pub async fn create_hash_and_sign_event( return Err!(Request(Forbidden("Event is not authorized."))); } - // Check with the policy server - match self - .services - .event_handler - .ask_policy_server(&pdu, room_id) - .await - { - | Ok(true) => {}, - | Ok(false) => { - return Err!(Request(Forbidden(debug_warn!( - "Policy server marked this event as spam" - )))); - }, - | Err(e) => { - // fail open - warn!("Failed to check event with policy server: {e}"); - }, - } - // Hash and sign let mut pdu_json = utils::to_canonical_object(&pdu).map_err(|e| { err!(Request(BadJson(warn!("Failed to convert PDU to canonical JSON: {e}")))) @@ -222,6 +203,25 @@ pub async fn create_hash_and_sign_event( pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into())); + // Check with the policy server + match self + .services + .event_handler + .ask_policy_server(&pdu, room_id) + .await + { + | Ok(true) => {}, + | Ok(false) => { + return Err!(Request(Forbidden(debug_warn!( + "Policy server marked this event as spam" + )))); + }, + | Err(e) => { + // fail open + warn!("Failed to check event with policy server: {e}"); + }, + } + // Generate short event id let _shorteventid = self .services From bd3db65cb2ec1ee00aa5ce6069bf044554b97d97 Mon Sep 17 00:00:00 2001 From: Yonatan Sidler Date: Wed, 6 Aug 2025 20:01:36 +0300 Subject: [PATCH 13/40] fix(arch): fix config.toml not being loaded from LoadCredentials directory --- arch/conduwuit.service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/conduwuit.service b/arch/conduwuit.service index f7100179..34c3995e 100644 --- a/arch/conduwuit.service +++ b/arch/conduwuit.service @@ -64,7 +64,7 @@ StateDirectory=conduwuit RuntimeDirectory=conduwuit RuntimeDirectoryMode=0750 -Environment=CONTINUWUITY_CONFIG=${CREDENTIALS_DIRECTORY}/config.toml +Environment=CONTINUWUITY_CONFIG=%d/config.toml LoadCredential=config.toml:/etc/conduwuit/conduwuit.toml BindPaths=/var/lib/private/conduwuit:/var/lib/matrix-conduit BindPaths=/var/lib/private/conduwuit:/var/lib/private/matrix-conduit From e820551f6290bcaef46a544fe1eb6a11476df41c Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 10 Aug 2025 14:37:26 +0100 Subject: [PATCH 14/40] fix(appservice): create sender_localpart user during appservice startup Fixes #813: Application services were unable to work because their sender_localpart user was never created in the database, preventing authentication. This fix ensures the appservice user account is created when: - The server starts up and loads existing appservices from the database - A new appservice is registered via the admin command Additionally, if an appservice user has been accidentally deactivated, it will be automatically reactivated when the appservice starts. The solution centralises all appservice startup logic into a single `start_appservice` helper method, eliminating code duplication between the registration and startup paths. --- src/service/appservice/mod.rs | 89 +++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index 7be8a471..de6fcfac 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -11,7 +11,7 @@ use ruma::{RoomAliasId, RoomId, UserId, api::appservice::Registration}; use tokio::sync::{RwLock, RwLockReadGuard}; pub use self::{namespace_regex::NamespaceRegex, registration_info::RegistrationInfo}; -use crate::{Dep, sending}; +use crate::{Dep, globals, sending, users}; pub struct Service { registration_info: RwLock, @@ -20,7 +20,9 @@ pub struct Service { } struct Services { + globals: Dep, sending: Dep, + users: Dep, } struct Data { @@ -35,7 +37,9 @@ impl crate::Service for Service { Ok(Arc::new(Self { registration_info: RwLock::new(BTreeMap::new()), services: Services { + globals: args.depend::("globals"), sending: args.depend::("sending"), + users: args.depend::("users"), }, db: Data { id_appserviceregistrations: args.db["id_appserviceregistrations"].clone(), @@ -44,15 +48,34 @@ impl crate::Service for Service { } async fn worker(self: Arc) -> Result { - // Inserting registrations into cache self.iter_db_ids() .try_for_each(async |appservice| { - self.registration_info - .write() - .await - .insert(appservice.0, appservice.1.try_into()?); + let (id, registration) = appservice; - Ok(()) + // During startup, resolve any token collisions in favour of appservices + // by logging out conflicting user devices + if let Ok((user_id, device_id)) = self + .services + .users + .find_from_token(®istration.as_token) + .await + { + conduwuit::warn!( + "Token collision detected during startup: Appservice '{}' token was \ + also used by user '{}' device '{}'. Logging out the user device to \ + resolve conflict.", + id, + user_id.localpart(), + device_id + ); + + self.services + .users + .remove_device(&user_id, &device_id) + .await; + } + + self.start_appservice(id, registration).await }) .await } @@ -61,6 +84,39 @@ impl crate::Service for Service { } impl Service { + /// Starts an appservice, ensuring its sender_localpart user exists and is + /// active. Creates the user if it doesn't exist, or reactivates it if it + /// was deactivated. Then registers the appservice in memory for request + /// handling. + async fn start_appservice(&self, id: String, registration: Registration) -> Result { + let appservice_user_id = UserId::parse_with_server_name( + registration.sender_localpart.as_str(), + self.services.globals.server_name(), + )?; + + if !self.services.users.exists(&appservice_user_id).await { + self.services.users.create(&appservice_user_id, None)?; + } else if self + .services + .users + .is_deactivated(&appservice_user_id) + .await + .unwrap_or(false) + { + // Reactivate the appservice user if it was accidentally deactivated + self.services + .users + .set_password(&appservice_user_id, None)?; + } + + self.registration_info + .write() + .await + .insert(id, registration.try_into()?); + + Ok(()) + } + /// Registers an appservice and returns the ID to the caller pub async fn register_appservice( &self, @@ -68,15 +124,28 @@ impl Service { appservice_config_body: &str, ) -> Result { //TODO: Check for collisions between exclusive appservice namespaces - self.registration_info - .write() + + // Prevent token collision with existing user tokens + if self + .services + .users + .find_from_token(®istration.as_token) .await - .insert(registration.id.clone(), registration.clone().try_into()?); + .is_ok() + { + return err!(Request(InvalidParam( + "Cannot register appservice: The provided token is already in use by a user \ + device. Please generate a different token for the appservice." + ))); + } self.db .id_appserviceregistrations .insert(®istration.id, appservice_config_body); + self.start_appservice(registration.id.clone(), registration.clone()) + .await?; + Ok(()) } From d1ebcfaf0bdcc039df3e484acf6eb2428d81ba26 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 10 Aug 2025 14:38:54 +0100 Subject: [PATCH 15/40] fix(auth): prevent token collisions and optimise lookups Ensures access tokens are unique across both user and appservice tables to prevent authentication ambiguity and potential security issues. Changes: - On startup, automatically logout any user devices using tokens that conflict with appservice tokens (resolves in favour of appservices) and log a warning with affected user/device details - When creating new user tokens, check for conflicts with appservice tokens and generate a new token if a collision would occur - When registering new appservices, reject registration if the token is already in use by a user device - Use futures::select_ok to race token lookups concurrently for better performance (adapted from tuwunel commit 066097a8) This fix-forward approach resolves existing token collisions on startup whilst preventing new ones from being created, without breaking existing valid authentications. The find_token optimisation is adapted from tuwunel (matrix-construct/tuwunel) commit 066097a8: "Optimize user and appservice token queries" by Jason Volk. --- src/api/router/auth.rs | 41 +++++++++++++++++++++++++---------- src/service/appservice/mod.rs | 7 +++--- src/service/users/mod.rs | 28 +++++++++++++++++++++--- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 01254c32..5088e699 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -5,6 +5,14 @@ use axum_extra::{ typed_header::TypedHeaderRejectionReason, }; use conduwuit::{Err, Error, Result, debug_error, err, warn}; +use futures::{ + TryFutureExt, + future::{ + Either::{Left, Right}, + select_ok, + }, + pin_mut, +}; use ruma::{ CanonicalJsonObject, CanonicalJsonValue, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId, api::{ @@ -54,17 +62,7 @@ pub(super) async fn auth( | None => request.query.access_token.as_deref(), }; - let token = if let Some(token) = token { - match services.appservice.find_from_token(token).await { - | Some(reg_info) => Token::Appservice(Box::new(reg_info)), - | _ => match services.users.find_from_token(token).await { - | Ok((user_id, device_id)) => Token::User((user_id, device_id)), - | _ => Token::Invalid, - }, - } - } else { - Token::None - }; + let token = find_token(services, token).await?; if metadata.authentication == AuthScheme::None { match metadata { @@ -342,3 +340,24 @@ async fn parse_x_matrix(request: &mut Request) -> Result { Ok(x_matrix) } + +async fn find_token(services: &Services, token: Option<&str>) -> Result { + let Some(token) = token else { + return Ok(Token::None); + }; + + let user_token = services.users.find_from_token(token).map_ok(Token::User); + + let appservice_token = services + .appservice + .find_from_token(token) + .map_ok(Box::new) + .map_ok(Token::Appservice); + + pin_mut!(user_token, appservice_token); + match select_ok([Left(user_token), Right(appservice_token)]).await { + | Err(e) if !e.is_not_found() => Err(e), + | Ok((token, _)) => Ok(token), + | _ => Ok(Token::Invalid), + } +} diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index de6fcfac..ad9c4a3f 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -133,10 +133,10 @@ impl Service { .await .is_ok() { - return err!(Request(InvalidParam( + return Err(err!(Request(InvalidParam( "Cannot register appservice: The provided token is already in use by a user \ device. Please generate a different token for the appservice." - ))); + )))); } self.db @@ -182,12 +182,13 @@ impl Service { .map(|info| info.registration) } - pub async fn find_from_token(&self, token: &str) -> Option { + pub async fn find_from_token(&self, token: &str) -> Result { self.read() .await .values() .find(|info| info.registration.as_token == token) .cloned() + .ok_or_else(|| err!(Request(NotFound("Appservice token not found")))) } /// Checks if a given user id matches any exclusive appservice regex diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index d2dfccd9..0aacc0e1 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -19,7 +19,7 @@ use ruma::{ use serde::{Deserialize, Serialize}; use serde_json::json; -use crate::{Dep, account_data, admin, globals, rooms}; +use crate::{Dep, account_data, admin, appservice, globals, rooms}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserSuspension { @@ -40,6 +40,7 @@ struct Services { server: Arc, account_data: Dep, admin: Dep, + appservice: Dep, globals: Dep, state_accessor: Dep, state_cache: Dep, @@ -76,6 +77,7 @@ impl crate::Service for Service { server: args.server.clone(), account_data: args.depend::("account_data"), admin: args.depend::("admin"), + appservice: args.depend::("appservice"), globals: args.depend::("globals"), state_accessor: args .depend::("rooms::state_accessor"), @@ -407,6 +409,26 @@ impl Service { ))); } + // Prevent token collisions with appservice tokens + let final_token = if self + .services + .appservice + .find_from_token(token) + .await + .is_ok() + { + let new_token = utils::random_string(32); + conduwuit::debug_warn!( + "Token collision prevented: Generated new token for user '{}' device '{}' \ + (original token conflicted with an appservice)", + user_id.localpart(), + device_id + ); + new_token + } else { + token.to_owned() + }; + // Remove old token if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await { self.db.token_userdeviceid.remove(&old_token); @@ -414,8 +436,8 @@ impl Service { } // Assign token to user device combination - self.db.userdeviceid_token.put_raw(key, token); - self.db.token_userdeviceid.raw_put(token, key); + self.db.userdeviceid_token.put_raw(key, &final_token); + self.db.token_userdeviceid.raw_put(&final_token, key); Ok(()) } From 9286838d23aa5b60bb0a030d403879406732bbc9 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 10 Aug 2025 18:22:20 +0100 Subject: [PATCH 16/40] fix(relations): improve thread pagination and include root event Replace unreliable PduCount pagination tokens with ShortEventId throughout the relations and messages endpoints. ShortEventId provides stable, unique identifiers that persist across server restarts and database operations. Key improvements: - Add token parsing helpers that try ShortEventId first, fall back to PduCount for backwards compatibility - Include thread root event when paginating backwards to thread start - Fix off-by-one error in get_relations that was returning the starting event in results - Only return next_batch/prev_batch tokens when more events are available, preventing clients from making unnecessary requests at thread boundaries - Ensure consistent token format between /relations, /messages, and /sync endpoints for interoperability This fixes duplicate events when scrolling at thread boundaries and ensures the thread root message is visible when viewing a thread, matching expected client behaviour. --- src/api/client/message.rs | 60 +++++++++--- src/api/client/relations.rs | 129 +++++++++++++++++++++---- src/service/rooms/pdu_metadata/data.rs | 14 ++- 3 files changed, 168 insertions(+), 35 deletions(-) diff --git a/src/api/client/message.rs b/src/api/client/message.rs index f8818ebb..95a135e1 100644 --- a/src/api/client/message.rs +++ b/src/api/client/message.rs @@ -1,14 +1,14 @@ use axum::extract::State; use conduwuit::{ - Err, Result, at, + Err, Result, at, err, matrix::{ event::{Event, Matches}, - pdu::PduCount, + pdu::{PduCount, ShortEventId}, }, ref_at, utils::{ IterStream, ReadyExt, - result::{FlatOk, LogErr}, + result::LogErr, stream::{BroadbandExt, TryIgnore, WidebandExt}, }, }; @@ -61,6 +61,39 @@ const IGNORED_MESSAGE_TYPES: &[TimelineEventType] = &[ const LIMIT_MAX: usize = 100; const LIMIT_DEFAULT: usize = 10; +/// Parse a pagination token, trying ShortEventId first, then falling back to +/// PduCount +async fn parse_pagination_token( + _services: &Services, + _room_id: &RoomId, + token: Option<&str>, + default: PduCount, +) -> Result { + let Some(token) = token else { + return Ok(default); + }; + + // Try parsing as ShortEventId first + if let Ok(shorteventid) = token.parse::() { + // ShortEventId maps directly to a PduCount in our database + Ok(PduCount::Normal(shorteventid)) + } else if let Ok(count) = token.parse::() { + // Fallback to PduCount for backwards compatibility + Ok(PduCount::Normal(count)) + } else if let Ok(count) = token.parse::() { + // Also handle negative counts for backfilled events + Ok(PduCount::from_signed(count)) + } else { + Err(err!(Request(InvalidParam("Invalid pagination token")))) + } +} + +/// Convert a PduCount to a token string (using the underlying ShortEventId) +fn count_to_token(count: PduCount) -> String { + // The PduCount's unsigned value IS the ShortEventId + count.into_unsigned().to_string() +} + /// # `GET /_matrix/client/r0/rooms/{roomId}/messages` /// /// Allows paginating through room history. @@ -81,17 +114,18 @@ pub(crate) async fn get_message_events_route( return Err!(Request(Forbidden("Room does not exist to this server"))); } - let from: PduCount = body - .from - .as_deref() - .map(str::parse) - .transpose()? - .unwrap_or_else(|| match body.dir { + let from: PduCount = + parse_pagination_token(&services, room_id, body.from.as_deref(), match body.dir { | Direction::Forward => PduCount::min(), | Direction::Backward => PduCount::max(), - }); + }) + .await?; - let to: Option = body.to.as_deref().map(str::parse).flat_ok(); + let to: Option = if let Some(to_str) = body.to.as_deref() { + Some(parse_pagination_token(&services, room_id, Some(to_str), PduCount::min()).await?) + } else { + None + }; let limit: usize = body .limit @@ -180,8 +214,8 @@ pub(crate) async fn get_message_events_route( .collect(); Ok(get_message_events::v3::Response { - start: from.to_string(), - end: next_token.as_ref().map(ToString::to_string), + start: count_to_token(from), + end: next_token.map(count_to_token), chunk, state, }) diff --git a/src/api/client/relations.rs b/src/api/client/relations.rs index 1aa34ada..48bcde20 100644 --- a/src/api/client/relations.rs +++ b/src/api/client/relations.rs @@ -1,7 +1,11 @@ use axum::extract::State; use conduwuit::{ - Result, at, - matrix::{Event, event::RelationTypeEqual, pdu::PduCount}, + Result, at, err, + matrix::{ + Event, + event::RelationTypeEqual, + pdu::{PduCount, ShortEventId}, + }, utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt}, }; use conduwuit_service::Services; @@ -20,6 +24,40 @@ use ruma::{ use crate::Ruma; +/// Parse a pagination token, trying ShortEventId first, then falling back to +/// PduCount +async fn parse_pagination_token( + _services: &Services, + _room_id: &RoomId, + token: Option<&str>, + default: PduCount, +) -> Result { + let Some(token) = token else { + return Ok(default); + }; + + // Try parsing as ShortEventId first + if let Ok(shorteventid) = token.parse::() { + // ShortEventId maps directly to a PduCount in our database + // The shorteventid IS the count value, just need to wrap it + Ok(PduCount::Normal(shorteventid)) + } else if let Ok(count) = token.parse::() { + // Fallback to PduCount for backwards compatibility + Ok(PduCount::Normal(count)) + } else if let Ok(count) = token.parse::() { + // Also handle negative counts for backfilled events + Ok(PduCount::from_signed(count)) + } else { + Err(err!(Request(InvalidParam("Invalid pagination token")))) + } +} + +/// Convert a PduCount to a token string (using the underlying ShortEventId) +fn count_to_token(count: PduCount) -> String { + // The PduCount's unsigned value IS the ShortEventId + count.into_unsigned().to_string() +} + /// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}` pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route( State(services): State, @@ -109,15 +147,17 @@ async fn paginate_relations_with_filter( recurse: bool, dir: Direction, ) -> Result { - let start: PduCount = from - .map(str::parse) - .transpose()? - .unwrap_or_else(|| match dir { - | Direction::Forward => PduCount::min(), - | Direction::Backward => PduCount::max(), - }); + let start: PduCount = parse_pagination_token(services, room_id, from, match dir { + | Direction::Forward => PduCount::min(), + | Direction::Backward => PduCount::max(), + }) + .await?; - let to: Option = to.map(str::parse).flat_ok(); + let to: Option = if let Some(to_str) = to { + Some(parse_pagination_token(services, room_id, Some(to_str), PduCount::min()).await?) + } else { + None + }; // Use limit or else 30, with maximum 100 let limit: usize = limit @@ -129,6 +169,11 @@ async fn paginate_relations_with_filter( // Spec (v1.10) recommends depth of at least 3 let depth: u8 = if recurse { 3 } else { 1 }; + // Check if this is a thread request + let is_thread = filter_rel_type + .as_ref() + .is_some_and(|rel| *rel == RelationType::Thread); + let events: Vec<_> = services .rooms .pdu_metadata @@ -152,23 +197,65 @@ async fn paginate_relations_with_filter( .collect() .await; - let next_batch = match dir { - | Direction::Forward => events.last(), - | Direction::Backward => events.first(), + // For threads, check if we should include the root event + let mut root_event = None; + if is_thread && dir == Direction::Backward { + // Check if we've reached the beginning of the thread + // (fewer events than requested means we've exhausted the thread) + if events.len() < limit { + // Try to get the thread root event + if let Ok(root_pdu) = services.rooms.timeline.get_pdu(target).await { + // Check visibility + if services + .rooms + .state_accessor + .user_can_see_event(sender_user, room_id, target) + .await + { + // Store the root event to add to the response + root_event = Some(root_pdu); + } + } + } } - .map(at!(0)) - .as_ref() - .map(ToString::to_string); + + // Determine if there are more events to fetch + let has_more = if root_event.is_some() { + false // We've included the root, no more events + } else { + // Check if we got a full page of results (might be more) + events.len() >= limit + }; + + let next_batch = if has_more { + match dir { + | Direction::Forward => events.last(), + | Direction::Backward => events.first(), + } + .map(|(count, _)| count_to_token(*count)) + } else { + None + }; + + // Build the response chunk with thread root if needed + let chunk: Vec<_> = if let Some(root) = root_event { + // Add root event at the beginning for backward pagination + std::iter::once(root.into_format()) + .chain(events.into_iter().map(at!(1)).map(Event::into_format)) + .collect() + } else { + events + .into_iter() + .map(at!(1)) + .map(Event::into_format) + .collect() + }; Ok(get_relating_events::v1::Response { next_batch, prev_batch: from.map(Into::into), recursion_depth: recurse.then_some(depth.into()), - chunk: events - .into_iter() - .map(at!(1)) - .map(Event::into_format) - .collect(), + chunk, }) } diff --git a/src/service/rooms/pdu_metadata/data.rs b/src/service/rooms/pdu_metadata/data.rs index c1376cb0..f9cc80a0 100644 --- a/src/service/rooms/pdu_metadata/data.rs +++ b/src/service/rooms/pdu_metadata/data.rs @@ -61,9 +61,10 @@ impl Data { from: PduCount, dir: Direction, ) -> impl Stream + Send + '_ { + let from_unsigned = from.into_unsigned(); let mut current = ArrayVec::::new(); current.extend(target.to_be_bytes()); - current.extend(from.saturating_inc(dir).into_unsigned().to_be_bytes()); + current.extend(from_unsigned.to_be_bytes()); let current = current.as_slice(); match dir { | Direction::Forward => self.tofrom_relation.raw_keys_from(current).boxed(), @@ -73,6 +74,17 @@ impl Data { .ready_take_while(move |key| key.starts_with(&target.to_be_bytes())) .map(|to_from| u64_from_u8(&to_from[8..16])) .map(PduCount::from_unsigned) + .ready_filter(move |count| { + if from == PduCount::min() || from == PduCount::max() { + true + } else { + let count_unsigned = count.into_unsigned(); + match dir { + | Direction::Forward => count_unsigned > from_unsigned, + | Direction::Backward => count_unsigned < from_unsigned, + } + } + }) .wide_filter_map(move |shorteventid| async move { let pdu_id: RawPduId = PduId { shortroomid, shorteventid }.into(); From 583cb924f1510960702f626c7397cb9e87e1722a Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Mon, 11 Aug 2025 06:24:29 +0100 Subject: [PATCH 17/40] refactor: address code review feedback for auth and pagination improvements - Extract duplicated thread/message pagination functions to shared utils module - Refactor pagination token parsing to use Option combinators instead of defaults - Split access token generation from assignment for clearer error handling - Add appservice token collision detection at startup and registration - Allow appservice re-registration with same token (for config updates) - Simplify thread relation chunk building using iterator chaining - Fix saturating_inc edge case in relation queries with explicit filtering - Add concise comments explaining non-obvious behaviour choices --- src/api/client/message.rs | 55 ++++------------- src/api/client/mod.rs | 1 + src/api/client/relations.rs | 78 +++++------------------- src/api/client/session.rs | 4 +- src/api/client/utils.rs | 28 +++++++++ src/api/router/auth.rs | 1 + src/service/appservice/mod.rs | 83 +++++++++++++++++--------- src/service/rooms/pdu_metadata/data.rs | 2 + src/service/users/mod.rs | 48 ++++++++++----- 9 files changed, 149 insertions(+), 151 deletions(-) create mode 100644 src/api/client/utils.rs diff --git a/src/api/client/message.rs b/src/api/client/message.rs index 95a135e1..4d489c2f 100644 --- a/src/api/client/message.rs +++ b/src/api/client/message.rs @@ -1,9 +1,9 @@ use axum::extract::State; use conduwuit::{ - Err, Result, at, err, + Err, Result, at, matrix::{ event::{Event, Matches}, - pdu::{PduCount, ShortEventId}, + pdu::PduCount, }, ref_at, utils::{ @@ -35,6 +35,7 @@ use ruma::{ }; use tracing::warn; +use super::utils::{count_to_token, parse_pagination_token as parse_token}; use crate::Ruma; /// list of safe and common non-state events to ignore if the user is ignored @@ -61,39 +62,6 @@ const IGNORED_MESSAGE_TYPES: &[TimelineEventType] = &[ const LIMIT_MAX: usize = 100; const LIMIT_DEFAULT: usize = 10; -/// Parse a pagination token, trying ShortEventId first, then falling back to -/// PduCount -async fn parse_pagination_token( - _services: &Services, - _room_id: &RoomId, - token: Option<&str>, - default: PduCount, -) -> Result { - let Some(token) = token else { - return Ok(default); - }; - - // Try parsing as ShortEventId first - if let Ok(shorteventid) = token.parse::() { - // ShortEventId maps directly to a PduCount in our database - Ok(PduCount::Normal(shorteventid)) - } else if let Ok(count) = token.parse::() { - // Fallback to PduCount for backwards compatibility - Ok(PduCount::Normal(count)) - } else if let Ok(count) = token.parse::() { - // Also handle negative counts for backfilled events - Ok(PduCount::from_signed(count)) - } else { - Err(err!(Request(InvalidParam("Invalid pagination token")))) - } -} - -/// Convert a PduCount to a token string (using the underlying ShortEventId) -fn count_to_token(count: PduCount) -> String { - // The PduCount's unsigned value IS the ShortEventId - count.into_unsigned().to_string() -} - /// # `GET /_matrix/client/r0/rooms/{roomId}/messages` /// /// Allows paginating through room history. @@ -114,18 +82,17 @@ pub(crate) async fn get_message_events_route( return Err!(Request(Forbidden("Room does not exist to this server"))); } - let from: PduCount = - parse_pagination_token(&services, room_id, body.from.as_deref(), match body.dir { + let from: PduCount = body + .from + .as_deref() + .map(parse_token) + .transpose()? + .unwrap_or_else(|| match body.dir { | Direction::Forward => PduCount::min(), | Direction::Backward => PduCount::max(), - }) - .await?; + }); - let to: Option = if let Some(to_str) = body.to.as_deref() { - Some(parse_pagination_token(&services, room_id, Some(to_str), PduCount::min()).await?) - } else { - None - }; + let to: Option = body.to.as_deref().map(parse_token).transpose()?; let limit: usize = body .limit diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs index be54e65f..e4be20b7 100644 --- a/src/api/client/mod.rs +++ b/src/api/client/mod.rs @@ -36,6 +36,7 @@ pub(super) mod typing; pub(super) mod unstable; pub(super) mod unversioned; pub(super) mod user_directory; +pub(super) mod utils; pub(super) mod voip; pub(super) mod well_known; diff --git a/src/api/client/relations.rs b/src/api/client/relations.rs index 48bcde20..f6d8fe9e 100644 --- a/src/api/client/relations.rs +++ b/src/api/client/relations.rs @@ -1,11 +1,7 @@ use axum::extract::State; use conduwuit::{ - Result, at, err, - matrix::{ - Event, - event::RelationTypeEqual, - pdu::{PduCount, ShortEventId}, - }, + Result, at, + matrix::{Event, event::RelationTypeEqual, pdu::PduCount}, utils::{IterStream, ReadyExt, result::FlatOk, stream::WidebandExt}, }; use conduwuit_service::Services; @@ -22,42 +18,9 @@ use ruma::{ events::{TimelineEventType, relation::RelationType}, }; +use super::utils::{count_to_token, parse_pagination_token as parse_token}; use crate::Ruma; -/// Parse a pagination token, trying ShortEventId first, then falling back to -/// PduCount -async fn parse_pagination_token( - _services: &Services, - _room_id: &RoomId, - token: Option<&str>, - default: PduCount, -) -> Result { - let Some(token) = token else { - return Ok(default); - }; - - // Try parsing as ShortEventId first - if let Ok(shorteventid) = token.parse::() { - // ShortEventId maps directly to a PduCount in our database - // The shorteventid IS the count value, just need to wrap it - Ok(PduCount::Normal(shorteventid)) - } else if let Ok(count) = token.parse::() { - // Fallback to PduCount for backwards compatibility - Ok(PduCount::Normal(count)) - } else if let Ok(count) = token.parse::() { - // Also handle negative counts for backfilled events - Ok(PduCount::from_signed(count)) - } else { - Err(err!(Request(InvalidParam("Invalid pagination token")))) - } -} - -/// Convert a PduCount to a token string (using the underlying ShortEventId) -fn count_to_token(count: PduCount) -> String { - // The PduCount's unsigned value IS the ShortEventId - count.into_unsigned().to_string() -} - /// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}` pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route( State(services): State, @@ -147,17 +110,15 @@ async fn paginate_relations_with_filter( recurse: bool, dir: Direction, ) -> Result { - let start: PduCount = parse_pagination_token(services, room_id, from, match dir { - | Direction::Forward => PduCount::min(), - | Direction::Backward => PduCount::max(), - }) - .await?; + let start: PduCount = from + .map(parse_token) + .transpose()? + .unwrap_or_else(|| match dir { + | Direction::Forward => PduCount::min(), + | Direction::Backward => PduCount::max(), + }); - let to: Option = if let Some(to_str) = to { - Some(parse_pagination_token(services, room_id, Some(to_str), PduCount::min()).await?) - } else { - None - }; + let to: Option = to.map(parse_token).transpose()?; // Use limit or else 30, with maximum 100 let limit: usize = limit @@ -238,18 +199,11 @@ async fn paginate_relations_with_filter( }; // Build the response chunk with thread root if needed - let chunk: Vec<_> = if let Some(root) = root_event { - // Add root event at the beginning for backward pagination - std::iter::once(root.into_format()) - .chain(events.into_iter().map(at!(1)).map(Event::into_format)) - .collect() - } else { - events - .into_iter() - .map(at!(1)) - .map(Event::into_format) - .collect() - }; + let chunk: Vec<_> = root_event + .into_iter() + .map(Event::into_format) + .chain(events.into_iter().map(at!(1)).map(Event::into_format)) + .collect(); Ok(get_relating_events::v1::Response { next_batch, diff --git a/src/api/client/session.rs b/src/api/client/session.rs index 992073c6..fe07e41d 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -198,8 +198,8 @@ pub(crate) async fn login_route( .clone() .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); - // Generate a new token for the device - let token = utils::random_string(TOKEN_LENGTH); + // Generate a new token for the device (ensuring no collisions) + let token = services.users.generate_unique_token().await; // Determine if device_id was provided and exists in the db for this user let device_exists = if body.device_id.is_some() { diff --git a/src/api/client/utils.rs b/src/api/client/utils.rs new file mode 100644 index 00000000..cc941b95 --- /dev/null +++ b/src/api/client/utils.rs @@ -0,0 +1,28 @@ +use conduwuit::{ + Result, err, + matrix::pdu::{PduCount, ShortEventId}, +}; + +/// Parse a pagination token, trying ShortEventId first, then falling back to +/// PduCount +pub(crate) fn parse_pagination_token(token: &str) -> Result { + // Try parsing as ShortEventId first + if let Ok(shorteventid) = token.parse::() { + // ShortEventId maps directly to a PduCount in our database + Ok(PduCount::Normal(shorteventid)) + } else if let Ok(count) = token.parse::() { + // Fallback to PduCount for backwards compatibility + Ok(PduCount::Normal(count)) + } else if let Ok(count) = token.parse::() { + // Also handle negative counts for backfilled events + Ok(PduCount::from_signed(count)) + } else { + Err(err!(Request(InvalidParam("Invalid pagination token")))) + } +} + +/// Convert a PduCount to a token string (using the underlying ShortEventId) +pub(crate) fn count_to_token(count: PduCount) -> String { + // The PduCount's unsigned value IS the ShortEventId + count.into_unsigned().to_string() +} diff --git a/src/api/router/auth.rs b/src/api/router/auth.rs index 5088e699..44afc3ef 100644 --- a/src/api/router/auth.rs +++ b/src/api/router/auth.rs @@ -355,6 +355,7 @@ async fn find_token(services: &Services, token: Option<&str>) -> Result { .map_ok(Token::Appservice); pin_mut!(user_token, appservice_token); + // Returns Ok if either token type succeeds, Err only if both fail match select_ok([Left(user_token), Right(appservice_token)]).await { | Err(e) if !e.is_not_found() => Err(e), | Ok((token, _)) => Ok(token), diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index ad9c4a3f..ebd798f6 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -4,7 +4,7 @@ mod registration_info; use std::{collections::BTreeMap, iter::IntoIterator, sync::Arc}; use async_trait::async_trait; -use conduwuit::{Result, err, utils::stream::IterStream}; +use conduwuit::{Err, Result, err, utils::stream::IterStream}; use database::Map; use futures::{Future, FutureExt, Stream, TryStreamExt}; use ruma::{RoomAliasId, RoomId, UserId, api::appservice::Registration}; @@ -48,36 +48,50 @@ impl crate::Service for Service { } async fn worker(self: Arc) -> Result { - self.iter_db_ids() - .try_for_each(async |appservice| { - let (id, registration) = appservice; + // First, collect all appservices to check for token conflicts + let appservices: Vec<(String, Registration)> = self.iter_db_ids().try_collect().await?; - // During startup, resolve any token collisions in favour of appservices - // by logging out conflicting user devices - if let Ok((user_id, device_id)) = self - .services - .users - .find_from_token(®istration.as_token) - .await - { - conduwuit::warn!( - "Token collision detected during startup: Appservice '{}' token was \ - also used by user '{}' device '{}'. Logging out the user device to \ - resolve conflict.", - id, - user_id.localpart(), - device_id - ); - - self.services - .users - .remove_device(&user_id, &device_id) - .await; + // Check for appservice-to-appservice token conflicts + for i in 0..appservices.len() { + for j in i.saturating_add(1)..appservices.len() { + if appservices[i].1.as_token == appservices[j].1.as_token { + return Err!(Database(error!( + "Token collision detected: Appservices '{}' and '{}' have the same token", + appservices[i].0, appservices[j].0 + ))); } + } + } - self.start_appservice(id, registration).await - }) - .await + // Process each appservice + for (id, registration) in appservices { + // During startup, resolve any token collisions in favour of appservices + // by logging out conflicting user devices + if let Ok((user_id, device_id)) = self + .services + .users + .find_from_token(®istration.as_token) + .await + { + conduwuit::warn!( + "Token collision detected during startup: Appservice '{}' token was also \ + used by user '{}' device '{}'. Logging out the user device to resolve \ + conflict.", + id, + user_id.localpart(), + device_id + ); + + self.services + .users + .remove_device(&user_id, &device_id) + .await; + } + + self.start_appservice(id, registration).await?; + } + + Ok(()) } fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } @@ -125,6 +139,18 @@ impl Service { ) -> Result { //TODO: Check for collisions between exclusive appservice namespaces + // Check for token collision with other appservices (allow re-registration of + // same appservice) + if let Ok(existing) = self.find_from_token(®istration.as_token).await { + if existing.registration.id != registration.id { + return Err(err!(Request(InvalidParam( + "Cannot register appservice: Token is already used by appservice '{}'. \ + Please generate a different token.", + existing.registration.id + )))); + } + } + // Prevent token collision with existing user tokens if self .services @@ -182,6 +208,7 @@ impl Service { .map(|info| info.registration) } + /// Returns Result to match users::find_from_token for select_ok usage pub async fn find_from_token(&self, token: &str) -> Result { self.read() .await diff --git a/src/service/rooms/pdu_metadata/data.rs b/src/service/rooms/pdu_metadata/data.rs index f9cc80a0..a746b4cc 100644 --- a/src/service/rooms/pdu_metadata/data.rs +++ b/src/service/rooms/pdu_metadata/data.rs @@ -61,6 +61,8 @@ impl Data { from: PduCount, dir: Direction, ) -> impl Stream + Send + '_ { + // Query from exact position then filter excludes it (saturating_inc could skip + // events at min/max boundaries) let from_unsigned = from.into_unsigned(); let mut current = ArrayVec::::new(); current.extend(target.to_be_bytes()); diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 0aacc0e1..eb54660e 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -393,6 +393,31 @@ impl Service { self.db.userdeviceid_token.qry(&key).await.deserialized() } + /// Generate a unique access token that doesn't collide with existing tokens + pub async fn generate_unique_token(&self) -> String { + loop { + let token = utils::random_string(32); + + // Check for collision with appservice tokens + if self + .services + .appservice + .find_from_token(&token) + .await + .is_ok() + { + continue; + } + + // Check for collision with user tokens + if self.db.token_userdeviceid.get(&token).await.is_ok() { + continue; + } + + return token; + } + } + /// Replaces the access token of one device. pub async fn set_token( &self, @@ -409,25 +434,18 @@ impl Service { ))); } - // Prevent token collisions with appservice tokens - let final_token = if self + // Check for token collision with appservices + if self .services .appservice .find_from_token(token) .await .is_ok() { - let new_token = utils::random_string(32); - conduwuit::debug_warn!( - "Token collision prevented: Generated new token for user '{}' device '{}' \ - (original token conflicted with an appservice)", - user_id.localpart(), - device_id - ); - new_token - } else { - token.to_owned() - }; + return Err!(Request(InvalidParam( + "Token conflicts with an existing appservice token" + ))); + } // Remove old token if let Ok(old_token) = self.db.userdeviceid_token.qry(&key).await { @@ -436,8 +454,8 @@ impl Service { } // Assign token to user device combination - self.db.userdeviceid_token.put_raw(key, &final_token); - self.db.token_userdeviceid.raw_put(&final_token, key); + self.db.userdeviceid_token.put_raw(key, token); + self.db.token_userdeviceid.raw_put(token, key); Ok(()) } From 54acd07555abeff1890a02d6ece66b6c97abdc02 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 16 Aug 2025 16:22:24 +0100 Subject: [PATCH 18/40] fix: Drop fake room v2 support --- src/core/info/room_version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/info/room_version.rs b/src/core/info/room_version.rs index 51d5d3c6..54ed8fdc 100644 --- a/src/core/info/room_version.rs +++ b/src/core/info/room_version.rs @@ -18,7 +18,7 @@ pub const STABLE_ROOM_VERSIONS: &[RoomVersionId] = &[ /// Experimental, partially supported room versions pub const UNSTABLE_ROOM_VERSIONS: &[RoomVersionId] = - &[RoomVersionId::V2, RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5]; + &[RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5]; type RoomVersion = (RoomVersionId, RoomVersionStability); From 2a183cc5a4a271514d5fe19ab2eca0fdde8495f8 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 13:44:32 +0100 Subject: [PATCH 19/40] fix(build): Remove hardened_malloc from full feature set The hardened_malloc feature conflicts with jemalloc, preventing successful builds with the --features full flag. Commenting out hardened_malloc allows the full profile to build correctly while maintaining all other features. --- src/main/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index 0d7dd844..8aaa3cc6 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -63,7 +63,7 @@ standard = [ ] full = [ "standard", - "hardened_malloc", + # "hardened_malloc", # Conflicts with jemalloc "jemalloc_prof", "perf_measurements", "tokio_console" From f54e59a068250e871168f6fe16b2eb1a8a458e93 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 14:17:18 +0100 Subject: [PATCH 20/40] ci: Add Renovate for automated dependency management Configures Renovate bot to create PRs for outdated dependencies. Runs daily at 5am UTC with manual trigger via workflow_dispatch. Configuration: - Ignores custom forks (jemalloc, telemetry packages) - Groups: GHA minor/patch, Rust toolchain, lockfile, Rust patches - Limits: 3 concurrent PRs, 2 PRs per hour - Supports: Cargo, GitHub Actions, Nix --- .forgejo/workflows/renovate.yml | 64 +++++++++++++++++++++++++++++++++ renovate.json | 44 +++++++++++++++++++++-- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/renovate.yml diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml new file mode 100644 index 00000000..802f5aab --- /dev/null +++ b/.forgejo/workflows/renovate.yml @@ -0,0 +1,64 @@ +name: Maintenance / Renovate + +on: + schedule: + # Run at 5am UTC daily to avoid late-night dev + - cron: '0 5 * * *' + + workflow_dispatch: + inputs: + dryRun: + description: 'Dry run mode' + required: false + default: 'false' + type: choice + options: + - 'true' + - 'false' + logLevel: + description: 'Log level' + required: false + default: 'info' + type: choice + options: + - 'debug' + - 'info' + - 'warn' + - 'error' + + push: + branches: + - main + paths: + # Re-run when config changes + - '.forgejo/workflows/renovate.yml' + - 'renovate.json' + +jobs: + renovate: + name: Renovate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run Renovate + uses: renovatebot/github-action@v40.1.0 + with: + token: ${{ secrets.RENOVATE_TOKEN }} + configurationFile: renovate.json + env: + # Platform configuration - Forgejo uses Gitea-compatible API + RENOVATE_PLATFORM: gitea + RENOVATE_ENDPOINT: ${{ github.server_url }}/api/v1 + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + + # Target repository + RENOVATE_REPOSITORIES: '["${{ github.repository }}"]' + + # Runtime behaviour + RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'false' }} + LOG_LEVEL: ${{ inputs.logLevel || 'info' }} + + # Git author for commits - configured via repository variables + RENOVATE_GIT_AUTHOR: '${{ vars.RENOVATE_AUTHOR }}' diff --git a/renovate.json b/renovate.json index eecf8532..3122d0bc 100644 --- a/renovate.json +++ b/renovate.json @@ -17,10 +17,48 @@ "github_actions" ], "ignoreDeps": [ - "tikv-jemllocator", + "tikv-jemallocator", "tikv-jemalloc-sys", "tikv-jemalloc-ctl", - "opentelemetry-rust", + "opentelemetry", + "opentelemetry_sdk", + "opentelemetry-jaeger", "tracing-opentelemetry" - ] + ], + "github-actions": { + "enabled": true, + "fileMatch": [ + "(^|/)\\.forgejo/workflows/[^/]+\\.ya?ml$", + "(^|/)\\.forgejo/actions/[^/]+/action\\.ya?ml$", + "(^|/)\\.github/workflows/[^/]+\\.ya?ml$", + "(^|/)\\.github/actions/[^/]+/action\\.ya?ml$" + ] + }, + "packageRules": [ + { + "description": "Batch minor and patch GitHub Actions updates", + "matchManagers": ["github-actions"], + "matchUpdateTypes": ["minor", "patch"], + "groupName": "github-actions-non-major" + }, + { + "description": "Group Rust toolchain updates into a single PR", + "matchManagers": ["regex"], + "matchPackageNames": ["rust", "rustc", "cargo"], + "groupName": "rust-toolchain" + }, + { + "description": "Group lockfile updates into a single PR", + "matchUpdateTypes": ["lockFileMaintenance"], + "groupName": "lockfile-maintenance" + }, + { + "description": "Batch patch-level Rust dependency updates", + "matchManagers": ["cargo"], + "matchUpdateTypes": ["patch"], + "groupName": "rust-patch-updates" + } + ], + "prConcurrentLimit": 3, + "prHourlyLimit": 2 } From b447cfff56f0a0b5f580fbfe3a28fc94e2cda09e Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 15:11:38 +0100 Subject: [PATCH 21/40] ci: Update prefligit to prek The prefligit project has been renamed to prek due to typosquatting concerns. This updates our CI to use the new name and recommended installation method via uv, which significantly reduces setup time compared to cargo install and includes automatic caching. - Replace outdated static prefligit action with direct prek invocation - Use uv as recommended by upstream: https://github.com/j178/prek - Update check-byte-order-marker to fix-byte-order-marker (deprecated) - Simplify workflow by removing unused ref calculations The same .pre-commit-config.yaml works unchanged. Developers can install locally with 'uvx prek install' or other methods from the repo. --- .forgejo/workflows/prefligit-checks.yml | 36 ++++++++++++++++--------- .pre-commit-config.yaml | 2 +- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.forgejo/workflows/prefligit-checks.yml b/.forgejo/workflows/prefligit-checks.yml index cc512496..18f573bb 100644 --- a/.forgejo/workflows/prefligit-checks.yml +++ b/.forgejo/workflows/prefligit-checks.yml @@ -1,22 +1,34 @@ -name: Checks / Prefligit +name: Checks / Prek on: push: pull_request: + permissions: contents: read jobs: - prefligit: + fast-checks: + name: Pre-commit & Formatting runs-on: ubuntu-latest - env: - FROM_REF: ${{ github.event.pull_request.base.sha || (!github.event.forced && ( github.event.before != '0000000000000000000000000000000000000000' && github.event.before || github.sha )) || format('{0}~', github.sha) }} - TO_REF: ${{ github.sha }} steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: ./.forgejo/actions/prefligit - with: - extra_args: --all-files --hook-stage manual + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install uv + uses: https://github.com/astral-sh/setup-uv@v6 + with: + enable-cache: true + ignore-nothing-to-cache: true + cache-dependency-glob: '' + + - name: Run prek + run: | + uvx prek run \ + --all-files \ + --hook-stage manual \ + --show-diff-on-failure \ + --color=always \ + -v diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68e3a982..da594310 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-case-conflict - id: check-symlinks - id: destroyed-symlinks From 5d1f141882826dca1b8ebd8cde43a0b0531978b3 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 15:12:05 +0100 Subject: [PATCH 22/40] ci: Rename prefligit-checks.yml to prek-checks.yml Rename workflow file to match the updated tool name. --- .forgejo/workflows/{prefligit-checks.yml => prek-checks.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .forgejo/workflows/{prefligit-checks.yml => prek-checks.yml} (100%) diff --git a/.forgejo/workflows/prefligit-checks.yml b/.forgejo/workflows/prek-checks.yml similarity index 100% rename from .forgejo/workflows/prefligit-checks.yml rename to .forgejo/workflows/prek-checks.yml From 9db750e97c77fe3a33ddf744715d48e3d4c7c457 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 15:51:29 +0100 Subject: [PATCH 23/40] fix(ci): Add full GitHub URL to renovate action Forgejo's runner doesn't automatically assume actions are on github.com, so we need to specify the full URL. --- .forgejo/workflows/renovate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index 802f5aab..d2891d91 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v4 - name: Run Renovate - uses: renovatebot/github-action@v40.1.0 + uses: https://github.com/renovatebot/github-action@v40.1.0 with: token: ${{ secrets.RENOVATE_TOKEN }} configurationFile: renovate.json From 4524a00fc616e68b1c03fa5aa78908458dbe5789 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 16:00:42 +0100 Subject: [PATCH 24/40] chore(ci): Remove obsolete prefligit action Now using prek directly via uvx, this custom action is no longer needed. --- .forgejo/actions/prefligit/action.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .forgejo/actions/prefligit/action.yml diff --git a/.forgejo/actions/prefligit/action.yml b/.forgejo/actions/prefligit/action.yml deleted file mode 100644 index 8cbd4500..00000000 --- a/.forgejo/actions/prefligit/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: prefligit -description: | - Runs prefligit, pre-commit reimplemented in Rust. -inputs: - extra_args: - description: options to pass to pre-commit run - required: false - default: '--all-files' - -runs: - using: composite - steps: - - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v6 - with: - enable-cache: true - ignore-nothing-to-cache: true - - name: Install Prefligit - shell: bash - run: | - curl --proto '=https' --tlsv1.2 -LsSf https://github.com/j178/prefligit/releases/download/v0.0.10/prefligit-installer.sh | sh - - uses: actions/cache@v3 - with: - path: ~/.cache/prefligit - key: prefligit-0|${{ hashFiles('.pre-commit-config.yaml') }} - - run: prefligit run --show-diff-on-failure --color=always -v ${{ inputs.extra_args }} - shell: bash From 14a4b24fc554a688510c21d5251730a70936dca6 Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Sun, 17 Aug 2025 17:23:56 +0100 Subject: [PATCH 25/40] fix(ci): Configure Renovate for Forgejo platform - Set platform to 'forgejo' with proper API endpoint - Use environment variables for all Renovate configuration - Add git timeout and disable GitHub token warnings - Move PR limit configuration to workflow --- .forgejo/workflows/renovate.yml | 44 ++++++++++++++++----------------- renovate.json | 4 +-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml index d2891d91..e8522bec 100644 --- a/.forgejo/workflows/renovate.yml +++ b/.forgejo/workflows/renovate.yml @@ -10,21 +10,22 @@ on: dryRun: description: 'Dry run mode' required: false - default: 'false' + default: null type: choice options: - - 'true' - - 'false' + - null + - 'extract' + - 'lookup' + - 'full' logLevel: description: 'Log level' required: false default: 'info' type: choice options: - - 'debug' - 'info' - - 'warn' - - 'error' + - 'warning' + - 'critical' push: branches: @@ -42,23 +43,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Run Renovate + - name: Self-hosted Renovate uses: https://github.com/renovatebot/github-action@v40.1.0 - with: - token: ${{ secrets.RENOVATE_TOKEN }} - configurationFile: renovate.json env: - # Platform configuration - Forgejo uses Gitea-compatible API - RENOVATE_PLATFORM: gitea - RENOVATE_ENDPOINT: ${{ github.server_url }}/api/v1 - RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} - - # Target repository - RENOVATE_REPOSITORIES: '["${{ github.repository }}"]' - - # Runtime behaviour - RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'false' }} LOG_LEVEL: ${{ inputs.logLevel || 'info' }} - - # Git author for commits - configured via repository variables - RENOVATE_GIT_AUTHOR: '${{ vars.RENOVATE_AUTHOR }}' + RENOVATE_AUTODISCOVER: 'false' + RENOVATE_BINARY_SOURCE: 'install' + RENOVATE_DRY_RUN: ${{ inputs.dryRun || 'false' }} + RENOVATE_ENDPOINT: ${{ github.server_url }}/api/v1 + RENOVATE_GIT_TIMEOUT: 60000 + RENOVATE_GIT_URL: 'endpoint' + RENOVATE_GITHUB_TOKEN_WARN: 'false' + RENOVATE_ONBOARDING: 'false' + RENOVATE_PLATFORM: 'forgejo' + RENOVATE_PR_COMMITS_PER_RUN_LIMIT: 3 + RENOVATE_REPOSITORIES: '["${{ github.repository }}"]' + RENOVATE_REQUIRE_CONFIG: 'required' + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} diff --git a/renovate.json b/renovate.json index 3122d0bc..b48fc6dc 100644 --- a/renovate.json +++ b/renovate.json @@ -58,7 +58,5 @@ "matchUpdateTypes": ["patch"], "groupName": "rust-patch-updates" } - ], - "prConcurrentLimit": 3, - "prHourlyLimit": 2 + ] } From ecb87ccd1c18126093628b1f424c3ed7f89e8ff9 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:39:36 +0200 Subject: [PATCH 26/40] chore(nix): bump rocksdb version in flake.nix to 10.4.fb This works without any further changes. Multiple people in the matrix room (including myself) have reported that the built executable runs fine with this. Nevertheless, there might be room for improvements (in future commits) --- flake.lock | 12 ++++++------ flake.nix | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/flake.lock b/flake.lock index 51a04c6c..4af82c75 100644 --- a/flake.lock +++ b/flake.lock @@ -516,16 +516,16 @@ "rocksdb": { "flake": false, "locked": { - "lastModified": 1741308171, - "narHash": "sha256-YdBvdQ75UJg5ffwNjxizpviCVwVDJnBkM8ZtGIduMgY=", - "ref": "v9.11.1", - "rev": "3ce04794bcfbbb0d2e6f81ae35fc4acf688b6986", - "revCount": 13177, + "lastModified": 1753385396, + "narHash": "sha256-/Hvy1yTH/0D5aa7bc+/uqFugCQq4InTdwlRw88vA5IY=", + "ref": "10.4.fb", + "rev": "28d4b7276c16ed3e28af1bd96162d6442ce25923", + "revCount": 13318, "type": "git", "url": "https://forgejo.ellis.link/continuwuation/rocksdb" }, "original": { - "ref": "v9.11.1", + "ref": "10.4.fb", "type": "git", "url": "https://forgejo.ellis.link/continuwuation/rocksdb" } diff --git a/flake.nix b/flake.nix index 564cd479..f0dcb6fb 100644 --- a/flake.nix +++ b/flake.nix @@ -17,7 +17,7 @@ nix-filter.url = "github:numtide/nix-filter?ref=main"; nixpkgs.url = "github:NixOS/nixpkgs?ref=nixpkgs-unstable"; rocksdb = { - url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=v9.11.1"; + url = "git+https://forgejo.ellis.link/continuwuation/rocksdb?ref=10.4.fb"; flake = false; }; }; @@ -62,7 +62,7 @@ }).overrideAttrs (old: { src = inputs.rocksdb; - version = "v9.11.1"; + version = "v10.4.fb"; cmakeFlags = pkgs.lib.subtractLists [ # No real reason to have snappy or zlib, no one uses this From 256bed992e50a86b54dc3ff0d24b8d72ce69904d Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:40:11 +0200 Subject: [PATCH 27/40] chore(nix): exec 'use flake' with direnv on NixOS systems --- .envrc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.envrc b/.envrc index bad73b75..172993c4 100644 --- a/.envrc +++ b/.envrc @@ -2,6 +2,8 @@ dotenv_if_exists -# use flake ".#${DIRENV_DEVSHELL:-default}" +if [ -f /etc/os-release ] && grep -q '^ID=nixos' /etc/os-release; then + use flake ".#${DIRENV_DEVSHELL:-default}" +fi PATH_add bin From aacaf5a2a031c9e44c2fb9a081a6109a39e5ddbb Mon Sep 17 00:00:00 2001 From: Tom Foster Date: Thu, 21 Aug 2025 21:10:15 +0100 Subject: [PATCH 28/40] fix(ci): Downgrade setup-uv action from v6 to v5 The setup-uv@v6 action has deprecated Node 18 support mid-version by using the File API, causing workflow failures. Temporarily downgrading to v5 until we migrate to a better runner image with Node 20+ support. --- .forgejo/workflows/prek-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/prek-checks.yml b/.forgejo/workflows/prek-checks.yml index 18f573bb..ac330ca2 100644 --- a/.forgejo/workflows/prek-checks.yml +++ b/.forgejo/workflows/prek-checks.yml @@ -18,7 +18,7 @@ jobs: persist-credentials: false - name: Install uv - uses: https://github.com/astral-sh/setup-uv@v6 + uses: https://github.com/astral-sh/setup-uv@v5 with: enable-cache: true ignore-nothing-to-cache: true From 427b973b67ec256aaffc8c8c98dd49aef6fa73c5 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 13:51:02 +0200 Subject: [PATCH 29/40] chore(rust): bump version 1.87 -> 1.89 - bump version in rust-toolchain.toml - update sha in flake.nix --- flake.nix | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index f0dcb6fb..25629621 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ file = ./rust-toolchain.toml; # See also `rust-toolchain.toml` - sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; + sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; }; mkScope = diff --git a/rust-toolchain.toml b/rust-toolchain.toml index bdb608aa..c44e95ef 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -9,8 +9,8 @@ # If you're having trouble making the relevant changes, bug a maintainer. [toolchain] -channel = "1.87.0" profile = "minimal" +channel = "1.89.0" components = [ # For rust-analyzer "rust-src", From ca3ee9224b19f7c2181a6db41c0de17b43317dc1 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:35:46 +0200 Subject: [PATCH 30/40] chore(rust): drop rustfmt from rust-toolchain.toml This just installs regular rustfmt, which is not needed in this project. One could say "It doesn't hurt", but in the NixOS dev shell it actually does since it will shadow nightly rustfmt and we don't have the `cargo +nightly fmt` synatx on NixOS that is available on other Distros. Also "It doesn't hurt" to delete it for non NixOS users. --- rust-toolchain.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c44e95ef..63e9d9ce 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -16,6 +16,9 @@ components = [ "rust-src", "rust-analyzer", # For CI and editors - "rustfmt", "clippy", + # you have to install rustfmt nightly yourself (if you're not on NixOS) + # + # The rust-toolchain.toml file doesn't provide any syntax for specifying components from different toolchains + # "rustfmt" ] From 6d1f12b22de7eb8d7af7a642cdf23a14c13d51d2 Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:41:14 +0200 Subject: [PATCH 31/40] chore(nix): make rustfmt-nightly available to default dev shell I verified this by running `rustfmt --version` on my system. Note that I don't have a system-wide install of rust and only rely on dev shells, so this can't possibly come from somewhere else. ``` $ rustfmt --version rustfmt 1.8.0-nightly (6677875279 2025-07-02) ``` --- flake.nix | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 25629621..d6beb84e 100644 --- a/flake.nix +++ b/flake.nix @@ -31,13 +31,17 @@ inherit system; }; + fnx = inputs.fenix.packages.${system}; # The Rust toolchain to use - toolchain = inputs.fenix.packages.${system}.fromToolchainFile { - file = ./rust-toolchain.toml; + toolchain = fnx.combine [ + (fnx.fromToolchainFile { + file = ./rust-toolchain.toml; - # See also `rust-toolchain.toml` - sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; - }; + # See also `rust-toolchain.toml` + sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE="; + }) + fnx.complete.rustfmt + ]; mkScope = pkgs: From d191494f18fa60af15756fc01420b4823c6247bd Mon Sep 17 00:00:00 2001 From: aviac Date: Thu, 21 Aug 2025 17:50:08 +0200 Subject: [PATCH 32/40] chore(nix): update `fenix` input This is required, since now we're installing `rustfmt` from the latest state of the fenix repo. This wasn't recent enough for the latest rust version. The input was locked at (2025-07-02). Now it's up to date. --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 4af82c75..4c2bf9fb 100644 --- a/flake.lock +++ b/flake.lock @@ -153,11 +153,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1751525020, - "narHash": "sha256-oDO6lCYS5Bf4jUITChj9XV7k3TP38DE0Ckz5n5ORCME=", + "lastModified": 1755585599, + "narHash": "sha256-tl/0cnsqB/Yt7DbaGMel2RLa7QG5elA8lkaOXli6VdY=", "owner": "nix-community", "repo": "fenix", - "rev": "a1a5f92f47787e7df9f30e5e5ac13e679215aa1e", + "rev": "6ed03ef4c8ec36d193c18e06b9ecddde78fb7e42", "type": "github" }, "original": { @@ -546,11 +546,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1751433876, - "narHash": "sha256-IsdwOcvLLDDlkFNwhdD5BZy20okIQL01+UQ7Kxbqh8s=", + "lastModified": 1755504847, + "narHash": "sha256-VX0B9hwhJypCGqncVVLC+SmeMVd/GAYbJZ0MiiUn2Pk=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "11d45c881389dae90b0da5a94cde52c79d0fc7ef", + "rev": "a905e3b21b144d77e1b304e49f3264f6f8d4db75", "type": "github" }, "original": { From 8b35de6a430fae8be5d8291d8d73c9420aafff6a Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Fri, 22 Aug 2025 00:51:54 +0100 Subject: [PATCH 33/40] chore: Fix clippy lints with minimal diff --- Cargo.toml | 5 ++++- src/api/client/sync/v4.rs | 1 + src/core/config/mod.rs | 1 + src/core/debug.rs | 2 +- src/router/serve/unix.rs | 2 +- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c656e183..04ff4bb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -867,7 +867,7 @@ unused-qualifications = "warn" #unused-results = "warn" # TODO ## some sadness -elided_named_lifetimes = "allow" # TODO! +mismatched_lifetime_syntaxes = "allow" # TODO! let_underscore_drop = "allow" missing_docs = "allow" # cfgs cannot be limited to expected cfgs or their de facto non-transitive/opt-in use-case e.g. @@ -1006,3 +1006,6 @@ literal_string_with_formatting_args = { level = "allow", priority = 1 } needless_raw_string_hashes = "allow" + +# TODO: Enable this lint & fix all instances +collapsible_if = "allow" diff --git a/src/api/client/sync/v4.rs b/src/api/client/sync/v4.rs index 14cd50d8..a16e4526 100644 --- a/src/api/client/sync/v4.rs +++ b/src/api/client/sync/v4.rs @@ -45,6 +45,7 @@ use crate::{ type TodoRooms = BTreeMap, usize, u64)>; const SINGLE_CONNECTION_SYNC: &str = "single_connection_sync"; +#[allow(clippy::cognitive_complexity)] /// POST `/_matrix/client/unstable/org.matrix.msc3575/sync` /// /// Sliding Sync endpoint (future endpoint: `/_matrix/client/v4/sync`) diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index aa021be7..0708196d 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::doc_link_with_quotes)] pub mod check; pub mod manager; pub mod proxy; diff --git a/src/core/debug.rs b/src/core/debug.rs index 21a5ada4..c728278d 100644 --- a/src/core/debug.rs +++ b/src/core/debug.rs @@ -100,7 +100,7 @@ pub fn trap() { #[must_use] pub fn panic_str(p: &Box) -> &'static str { - p.downcast_ref::<&str>().copied().unwrap_or_default() + (**p).downcast_ref::<&str>().copied().unwrap_or_default() } #[inline(always)] diff --git a/src/router/serve/unix.rs b/src/router/serve/unix.rs index 2af17274..9bb3dd6e 100644 --- a/src/router/serve/unix.rs +++ b/src/router/serve/unix.rs @@ -30,7 +30,7 @@ use tower::{Service, ServiceExt}; type MakeService = IntoMakeServiceWithConnectInfo; -const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); +const NULL_ADDR: net::SocketAddr = net::SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); const FINI_POLL_INTERVAL: Duration = Duration::from_millis(750); #[tracing::instrument(skip_all, level = "debug")] From c7adbae03f72250d0fd0ab2ea6ab7911b4369467 Mon Sep 17 00:00:00 2001 From: RatCornu Date: Sat, 9 Aug 2025 15:06:48 +0200 Subject: [PATCH 34/40] feat: ldap login --- Cargo.lock | 413 +++++++++++++++++++----- Cargo.toml | 5 + conduwuit-example.toml | 91 ++++++ src/admin/user/commands.rs | 4 +- src/api/Cargo.toml | 3 + src/api/client/account.rs | 5 +- src/api/client/profile.rs | 6 +- src/api/client/session.rs | 211 ++++++++---- src/api/client/unstable.rs | 4 +- src/core/config/mod.rs | 108 +++++++ src/core/error/mod.rs | 2 + src/database/maps.rs | 4 + src/service/Cargo.toml | 5 + src/service/admin/create.rs | 2 +- src/service/emergency/mod.rs | 8 +- src/service/rooms/state_cache/update.rs | 2 +- src/service/users/mod.rs | 203 +++++++++++- 17 files changed, 921 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d7192b6..2b044a1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -183,7 +183,7 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "serde_derive", - "syn", + "syn 2.0.104", ] [[package]] @@ -198,6 +198,45 @@ dependencies = [ "winnow", ] +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "assign" version = "1.1.1" @@ -250,7 +289,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -261,7 +300,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -433,11 +472,11 @@ dependencies = [ "hyper", "hyper-util", "pin-project-lite", - "rustls", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-pemfile 2.2.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", ] @@ -452,9 +491,9 @@ dependencies = [ "http", "http-body-util", "pin-project", - "rustls", + "rustls 0.23.29", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower-layer", "tower-service", @@ -521,7 +560,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn", + "syn 2.0.104", "which", ] @@ -540,7 +579,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn", + "syn 2.0.104", ] [[package]] @@ -794,7 +833,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -972,7 +1011,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", - "ring", + "ring 0.17.14", "ruma", "sanitize-filename", "serde", @@ -1019,7 +1058,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1044,7 +1083,7 @@ dependencies = [ "hyper-util", "log", "ruma", - "rustls", + "rustls 0.23.29", "sd-notify", "sentry", "sentry-tower", @@ -1074,6 +1113,7 @@ dependencies = [ "image", "ipaddress", "itertools 0.14.0", + "ldap3", "log", "loole", "lru-cache", @@ -1183,6 +1223,16 @@ dependencies = [ "crossterm", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1256,7 +1306,7 @@ dependencies = [ "proc-macro2", "quote", "strict", - "syn", + "syn 2.0.104", ] [[package]] @@ -1366,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1393,7 +1443,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1434,6 +1484,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1461,7 +1525,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1483,7 +1547,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1544,7 +1608,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1564,7 +1628,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -1734,6 +1798,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1781,7 +1846,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2030,7 +2095,7 @@ dependencies = [ "ipnet", "once_cell", "rand 0.9.2", - "ring", + "ring 0.17.14", "serde", "thiserror 2.0.12", "tinyvec", @@ -2122,7 +2187,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2216,11 +2281,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 1.0.2", ] @@ -2444,7 +2509,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -2613,7 +2678,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 2.0.104", ] [[package]] @@ -2628,6 +2693,43 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lber" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df7f9fd9f64cf8f59e1a4a0753fe7d575a5b38d3d7ac5758dcee9357d83ef0a" +dependencies = [ + "bytes", + "nom", +] + +[[package]] +name = "ldap3" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166199a8207874a275144c8a94ff6eed5fcbf5c52303e4d9b4d53a0c7ac76554" +dependencies = [ + "async-trait", + "bytes", + "futures", + "futures-util", + "lazy_static", + "lber", + "log", + "nom", + "percent-encoding", + "ring 0.16.20", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tokio-stream", + "tokio-util", + "url", + "x509-parser", +] + [[package]] name = "lebe" version = "0.5.2" @@ -2866,7 +2968,7 @@ checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3032,7 +3134,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3094,6 +3196,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3284,7 +3395,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3358,7 +3469,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3458,7 +3569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.104", ] [[package]] @@ -3487,7 +3598,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "version_check", "yansi", ] @@ -3508,7 +3619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3531,7 +3642,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -3597,7 +3708,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "socket2", "thiserror 2.0.12", "tokio", @@ -3615,9 +3726,9 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.2", - "ring", + "ring 0.17.14", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -3876,16 +3987,16 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", - "rustls-pemfile", + "rustls 0.23.29", + "rustls-native-certs 0.8.1", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tower 0.5.2", @@ -3909,6 +4020,21 @@ version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -3919,7 +4045,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -4093,7 +4219,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn", + "syn 2.0.104", "toml", ] @@ -4178,6 +4304,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4204,6 +4339,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.29" @@ -4213,13 +4360,25 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4229,7 +4388,16 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -4251,6 +4419,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.103.4" @@ -4258,9 +4436,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4319,6 +4497,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "sd-notify" version = "0.4.5" @@ -4328,6 +4516,19 @@ dependencies = [ "libc", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -4335,7 +4536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4365,7 +4566,7 @@ checksum = "255914a8e53822abd946e2ce8baa41d4cded6b8e938913b7f7b9da5b7ab44335" dependencies = [ "httpdate", "reqwest", - "rustls", + "rustls 0.23.29", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -4509,7 +4710,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4723,6 +4924,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spki" version = "0.7.3" @@ -4791,6 +4998,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.104" @@ -4811,6 +5029,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -4819,7 +5049,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4910,7 +5140,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -4921,7 +5151,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5088,7 +5318,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5103,13 +5333,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.29", "tokio", ] @@ -5307,7 +5547,7 @@ source = "git+https://forgejo.ellis.link/continuwuation/tracing?rev=1e64095a8051 dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5461,12 +5701,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -5482,7 +5734,7 @@ dependencies = [ "base64 0.22.1", "log", "once_cell", - "rustls", + "rustls 0.23.29", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -5617,7 +5869,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -5652,7 +5904,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5832,7 +6084,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -5843,7 +6095,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6164,6 +6416,23 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x509-parser" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xml5ever" version = "0.18.1" @@ -6221,8 +6490,8 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6242,7 +6511,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -6262,8 +6531,8 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -6302,7 +6571,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 04ff4bb7..9452066c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -546,6 +546,11 @@ features = ["std"] [workspace.dependencies.maplit] version = "1.0.2" +[workspace.dependencies.ldap3] +version = "0.11.5" +default-features = false +features = ["sync", "tls-rustls"] + # # Patches # diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 541050b1..47a9da19 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1696,6 +1696,10 @@ # #config_reload_signal = true +# This item is undocumented. Please contribute documentation for it. +# +#ldap = false + [global.tls] # Path to a valid TLS certificate file. @@ -1774,3 +1778,90 @@ # is 33.55MB. Setting it to 0 disables blurhashing. # #blurhash_max_raw_size = 33554432 + +[global.ldap] + +# Whether to enable LDAP login. +# +# example: "true" +# +#enable = false + +# URI of the LDAP server. +# +# example: "ldap://ldap.example.com:389" +# +#uri = + +# Root of the searches. +# +# example: "ou=users,dc=example,dc=org" +# +#base_dn = false + +# Bind DN if anonymous search is not enabled. +# +# You can use the variable `{username}` that will be replaced by the +# entered username. In such case, the password used to bind will be the +# one provided for the login and not the one given by +# `bind_password_file`. Beware: automatically granting admin rights will +# not work if you use this direct bind instead of a LDAP search. +# +# example: "cn=ldap-reader,dc=example,dc=org" or +# "cn={username},ou=users,dc=example,dc=org" +# +#bind_dn = false + +# Path to a file on the system that contains the password for the +# `bind_dn`. +# +# The server must be able to access the file, and it must not be empty. +# +#bind_password_file = false + +# Search filter to limit user searches. +# +# You can use the variable `{username}` that will be replaced by the +# entered username for more complex filters. +# +# example: "(&(objectClass=person)(memberOf=matrix))" +# +#filter = "(objectClass=*)" + +# Attribute to use to uniquely identify the user. +# +# example: "uid" or "cn" +# +#uid_attribute = "uid" + +# Attribute containing the mail of the user. +# +# example: "mail" +# +#mail_attribute = "mail" + +# Attribute containing the distinguished name of the user. +# +# example: "givenName" or "sn" +# +#name_attribute = "givenName" + +# Root of the searches for admin users. +# +# Defaults to `base_dn` if empty. +# +# example: "ou=admins,dc=example,dc=org" +# +#admin_base_dn = false + +# The LDAP search filter to find administrative users for conduwuit. +# +# If left blank, administrative state must be configured manually for each +# user. +# +# You can use the variable `{username}` that will be replaced by the +# entered username for more complex filters. +# +# example: "(objectClass=conduwuitAdmin)" or "(uid={username})" +# +#admin_filter = false diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index 86206c2b..56864a32 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -68,7 +68,8 @@ pub(super) async fn create_user(&self, username: String, password: Option return Err!("Couldn't reset the password for user {user_id}: {e}"), | Ok(()) => { diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 15ada812..9b4ea460 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -49,6 +49,9 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-service/jemalloc_stats", ] +ldap = [ + "conduwuit-service/ldap" +] release_max_log_level = [ "conduwuit-core/release_max_log_level", "conduwuit-service/release_max_log_level", diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 0cea7bd9..67268c9f 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -373,7 +373,7 @@ pub(crate) async fn register_route( let password = if is_guest { None } else { body.password.as_deref() }; // Create user - services.users.create(&user_id, password)?; + services.users.create(&user_id, password, None).await?; // Default to pretty displayname let mut displayname = user_id.localpart().to_owned(); @@ -659,7 +659,8 @@ pub(crate) async fn change_password_route( services .users - .set_password(sender_user, Some(&body.new_password))?; + .set_password(sender_user, Some(&body.new_password)) + .await?; if body.logout_devices { // Logout all devices except the current one diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs index 1882495c..eaa66e70 100644 --- a/src/api/client/profile.rs +++ b/src/api/client/profile.rs @@ -90,7 +90,7 @@ pub(crate) async fn get_displayname_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -189,7 +189,7 @@ pub(crate) async fn get_avatar_url_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -248,7 +248,7 @@ pub(crate) async fn get_profile_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/api/client/session.rs b/src/api/client/session.rs index fe07e41d..2f066d58 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -6,10 +6,11 @@ use conduwuit::{ Err, Error, Result, debug, err, info, utils, utils::{ReadyExt, hash}, }; -use conduwuit_service::uiaa::SESSION_ID_LENGTH; +use conduwuit_core::debug_error; +use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH}; use futures::StreamExt; use ruma::{ - UserId, + OwnedUserId, UserId, api::client::{ session::{ get_login_token, @@ -49,6 +50,147 @@ pub(crate) async fn get_login_types_route( ])) } +/// Authenticates the given user by its ID and its password. +/// +/// Returns the user ID if successful, and an error otherwise. +#[tracing::instrument(skip_all, fields(%user_id), name = "password")] +pub(crate) async fn password_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + // Restrict login to accounts only of type 'password', including untyped + // legacy accounts which are equivalent to 'password'. + if services + .users + .origin(user_id) + .await + .is_ok_and(|origin| origin != "password") + { + return Err!(Request(Forbidden("Account does not permit password login."))); + } + + let (hash, user_id) = match services.users.password_hash(user_id).await { + | Ok(hash) => (hash, user_id), + | Err(_) => services + .users + .password_hash(lowercased_user_id) + .await + .map(|hash| (hash, lowercased_user_id)) + .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?, + }; + + if hash.is_empty() { + return Err!(Request(UserDeactivated("The user has been deactivated"))); + } + + hash::verify_password(password, &hash) + .inspect_err(|e| debug_error!("{e}")) + .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; + + Ok(user_id.to_owned()) +} + +/// Authenticates the given user through the configured LDAP server. +/// +/// Creates the user if the user is found in the LDAP and do not already have an +/// account. +#[tracing::instrument(skip_all, fields(%user_id), name = "ldap")] +pub(super) async fn ldap_login( + services: &Services, + user_id: &UserId, + lowercased_user_id: &UserId, + password: &str, +) -> Result { + let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() { + | Some(bind_dn) if bind_dn.contains("{username}") => + (bind_dn.replace("{username}", lowercased_user_id.localpart()), false), + | _ => { + debug!("Searching user in LDAP"); + + let dns = services.users.search_ldap(user_id).await?; + if dns.len() >= 2 { + return Err!(Ldap("LDAP search returned two or more results")); + } + + let Some((user_dn, is_admin)) = dns.first() else { + return password_login(services, user_id, lowercased_user_id, password).await; + }; + + (user_dn.clone(), *is_admin) + }, + }; + + let user_id = services + .users + .auth_ldap(&user_dn, password) + .await + .map(|()| lowercased_user_id.to_owned())?; + + // LDAP users are automatically created on first login attempt. This is a very + // common feature that can be seen on many services using a LDAP provider for + // their users (synapse, Nextcloud, Jellyfin, ...). + // + // LDAP users are crated with a dummy password but non empty because an empty + // password is reserved for deactivated accounts. The conduwuit password field + // will never be read to login a LDAP user so it's not an issue. + if !services.users.exists(lowercased_user_id).await { + services + .users + .create(lowercased_user_id, Some("*"), Some("ldap")) + .await?; + } + + let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await; + + if is_ldap_admin && !is_conduwuit_admin { + services.admin.make_user_admin(lowercased_user_id).await?; + } else if !is_ldap_admin && is_conduwuit_admin { + services.admin.revoke_admin(lowercased_user_id).await?; + } + + Ok(user_id) +} + +pub(crate) async fn handle_login( + services: &Services, + body: &Ruma, + identifier: &Option, + password: &str, + user: &Option, +) -> Result { + debug!("Got password login type"); + let user_id = + if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { + UserId::parse_with_server_name(user_id, &services.config.server_name) + } else if let Some(user) = user { + UserId::parse_with_server_name(user, &services.config.server_name) + } else { + return Err!(Request(Unknown( + debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") + ))); + } + .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; + + let lowercased_user_id = UserId::parse_with_server_name( + user_id.localpart().to_lowercase(), + &services.config.server_name, + )?; + + if !services.globals.user_is_local(&user_id) + || !services.globals.user_is_local(&lowercased_user_id) + { + return Err!(Request(Unknown("User ID does not belong to this homeserver"))); + } + + if cfg!(feature = "ldap") && services.config.ldap.enable { + ldap_login(services, &user_id, &lowercased_user_id, password).await + } else { + password_login(services, &user_id, &lowercased_user_id, password).await + } +} + /// # `POST /_matrix/client/v3/login` /// /// Authenticates the user and returns an access token it can use in subsequent @@ -80,70 +222,7 @@ pub(crate) async fn login_route( password, user, .. - }) => { - debug!("Got password login type"); - let user_id = - if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier { - UserId::parse_with_server_name(user_id, &services.config.server_name) - } else if let Some(user) = user { - UserId::parse_with_server_name(user, &services.config.server_name) - } else { - return Err!(Request(Unknown( - debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)") - ))); - } - .map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?; - - let lowercased_user_id = UserId::parse_with_server_name( - user_id.localpart().to_lowercase(), - &services.config.server_name, - )?; - - if !services.globals.user_is_local(&user_id) - || !services.globals.user_is_local(&lowercased_user_id) - { - return Err!(Request(Unknown("User ID does not belong to this homeserver"))); - } - - // first try the username as-is - let hash = services - .users - .password_hash(&user_id) - .await - .inspect_err(|e| debug!("{e}")); - - match hash { - | Ok(hash) => { - if hash.is_empty() { - return Err!(Request(UserDeactivated("The user has been deactivated"))); - } - - hash::verify_password(password, &hash) - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - user_id - }, - | Err(_e) => { - let hash_lowercased_user_id = services - .users - .password_hash(&lowercased_user_id) - .await - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - if hash_lowercased_user_id.is_empty() { - return Err!(Request(UserDeactivated("The user has been deactivated"))); - } - - hash::verify_password(password, &hash_lowercased_user_id) - .inspect_err(|e| debug!("{e}")) - .map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?; - - lowercased_user_id - }, - } - }, + }) => handle_login(&services, &body, identifier, password, user).await?, | login::v3::LoginInfo::Token(login::v3::Token { token }) => { debug!("Got token login type"); if !services.server.config.login_via_existing_session { diff --git a/src/api/client/unstable.rs b/src/api/client/unstable.rs index 08f70975..f8703ff3 100644 --- a/src/api/client/unstable.rs +++ b/src/api/client/unstable.rs @@ -292,7 +292,7 @@ pub(crate) async fn get_timezone_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services @@ -352,7 +352,7 @@ pub(crate) async fn get_profile_key_route( .await { if !services.users.exists(&body.user_id).await { - services.users.create(&body.user_id, None)?; + services.users.create(&body.user_id, None, None).await?; } services diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 0708196d..a7fbc7dc 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -1948,6 +1948,10 @@ pub struct Config { pub allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure: bool, + // external structure; separate section + #[serde(default)] + pub ldap: LdapConfig, + // external structure; separate section #[serde(default)] pub blurhashing: BlurhashConfig, @@ -2042,6 +2046,102 @@ pub struct BlurhashConfig { pub blurhash_max_raw_size: u64, } +#[derive(Clone, Debug, Default, Deserialize)] +#[config_example_generator(filename = "conduwuit-example.toml", section = "global.ldap")] +pub struct LdapConfig { + /// Whether to enable LDAP login. + /// + /// example: "true" + #[serde(default)] + pub enable: bool, + + /// URI of the LDAP server. + /// + /// example: "ldap://ldap.example.com:389" + pub uri: Option, + + /// Root of the searches. + /// + /// example: "ou=users,dc=example,dc=org" + #[serde(default)] + pub base_dn: String, + + /// Bind DN if anonymous search is not enabled. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username. In such case, the password used to bind will be the + /// one provided for the login and not the one given by + /// `bind_password_file`. Beware: automatically granting admin rights will + /// not work if you use this direct bind instead of a LDAP search. + /// + /// example: "cn=ldap-reader,dc=example,dc=org" or + /// "cn={username},ou=users,dc=example,dc=org" + #[serde(default)] + pub bind_dn: Option, + + /// Path to a file on the system that contains the password for the + /// `bind_dn`. + /// + /// The server must be able to access the file, and it must not be empty. + #[serde(default)] + pub bind_password_file: Option, + + /// Search filter to limit user searches. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username for more complex filters. + /// + /// example: "(&(objectClass=person)(memberOf=matrix))" + /// + /// default: "(objectClass=*)" + #[serde(default = "default_ldap_search_filter")] + pub filter: String, + + /// Attribute to use to uniquely identify the user. + /// + /// example: "uid" or "cn" + /// + /// default: "uid" + #[serde(default = "default_ldap_uid_attribute")] + pub uid_attribute: String, + + /// Attribute containing the mail of the user. + /// + /// example: "mail" + /// + /// default: "mail" + #[serde(default = "default_ldap_mail_attribute")] + pub mail_attribute: String, + + /// Attribute containing the distinguished name of the user. + /// + /// example: "givenName" or "sn" + /// + /// default: "givenName" + #[serde(default = "default_ldap_name_attribute")] + pub name_attribute: String, + + /// Root of the searches for admin users. + /// + /// Defaults to `base_dn` if empty. + /// + /// example: "ou=admins,dc=example,dc=org" + #[serde(default)] + pub admin_base_dn: String, + + /// The LDAP search filter to find administrative users for conduwuit. + /// + /// If left blank, administrative state must be configured manually for each + /// user. + /// + /// You can use the variable `{username}` that will be replaced by the + /// entered username for more complex filters. + /// + /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" + #[serde(default)] + pub admin_filter: String, +} + #[derive(Deserialize, Clone, Debug)] #[serde(transparent)] struct ListeningPort { @@ -2431,3 +2531,11 @@ pub(super) fn default_blurhash_x_component() -> u32 { 4 } pub(super) fn default_blurhash_y_component() -> u32 { 3 } // end recommended & blurhashing defaults + +fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() } + +fn default_ldap_uid_attribute() -> String { String::from("uid") } + +fn default_ldap_mail_attribute() -> String { String::from("mail") } + +fn default_ldap_name_attribute() -> String { String::from("givenName") } diff --git a/src/core/error/mod.rs b/src/core/error/mod.rs index e46edf09..541af793 100644 --- a/src/core/error/mod.rs +++ b/src/core/error/mod.rs @@ -110,6 +110,8 @@ pub enum Error { InconsistentRoomState(&'static str, ruma::OwnedRoomId), #[error(transparent)] IntoHttp(#[from] ruma::api::error::IntoHttpError), + #[error("{0}")] + Ldap(Cow<'static, str>), #[error(transparent)] Mxc(#[from] ruma::MxcUriError), #[error(transparent)] diff --git a/src/database/maps.rs b/src/database/maps.rs index 214dbf34..da97ef45 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -374,6 +374,10 @@ pub(super) static MAPS: &[Descriptor] = &[ name: "userid_masterkeyid", ..descriptor::RANDOM_SMALL }, + Descriptor { + name: "userid_origin", + ..descriptor::RANDOM + }, Descriptor { name: "userid_password", ..descriptor::RANDOM diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index fdebd1d7..6e538f40 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -53,6 +53,9 @@ jemalloc_stats = [ "conduwuit-core/jemalloc_stats", "conduwuit-database/jemalloc_stats", ] +ldap = [ + "dep:ldap3" +] media_thumbnail = [ "dep:image", ] @@ -89,6 +92,8 @@ image.workspace = true image.optional = true ipaddress.workspace = true itertools.workspace = true +ldap3.workspace = true +ldap3.optional = true log.workspace = true loole.workspace = true lru-cache.workspace = true diff --git a/src/service/admin/create.rs b/src/service/admin/create.rs index 157b4d65..755673fe 100644 --- a/src/service/admin/create.rs +++ b/src/service/admin/create.rs @@ -38,7 +38,7 @@ pub async fn create_admin_room(services: &Services) -> Result { // Create a user for the server let server_user = services.globals.server_user.as_ref(); - services.users.create(server_user, None)?; + services.users.create(server_user, None, None).await?; let create_content = { use RoomVersionId::*; diff --git a/src/service/emergency/mod.rs b/src/service/emergency/mod.rs index 3a61f710..f8ecbb3e 100644 --- a/src/service/emergency/mod.rs +++ b/src/service/emergency/mod.rs @@ -41,6 +41,11 @@ impl crate::Service for Service { return Ok(()); } + if self.services.config.ldap.enable { + warn!("emergency password feature not available with LDAP enabled."); + return Ok(()); + } + self.set_emergency_access().await.inspect_err(|e| { error!("Could not set the configured emergency password for the server user: {e}"); }) @@ -57,7 +62,8 @@ impl Service { self.services .users - .set_password(server_user, self.services.config.emergency_password.as_deref())?; + .set_password(server_user, self.services.config.emergency_password.as_deref()) + .await?; let (ruleset, pwd_set) = match self.services.config.emergency_password { | Some(_) => (Ruleset::server_default(server_user), true), diff --git a/src/service/rooms/state_cache/update.rs b/src/service/rooms/state_cache/update.rs index 32c67947..86c1afe7 100644 --- a/src/service/rooms/state_cache/update.rs +++ b/src/service/rooms/state_cache/update.rs @@ -49,7 +49,7 @@ pub async fn update_membership( #[allow(clippy::collapsible_if)] if !self.services.globals.user_is_local(user_id) { if !self.services.users.exists(user_id).await { - self.services.users.create(user_id, None)?; + self.services.users.create(user_id, None, None).await?; } } diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index eb54660e..35202ec7 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,11 +1,19 @@ -use std::{collections::BTreeMap, mem, sync::Arc}; +use std::{ + collections::{BTreeMap, HashMap}, + mem, + sync::Arc, +}; use conduwuit::{ - Err, Error, Result, Server, at, debug_warn, err, trace, + Err, Error, Result, Server, at, debug_warn, err, is_equal_to, + result::LogErr, + trace, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, }; +use conduwuit_core::{debug, error}; use database::{Deserialized, Ignore, Interfix, Json, Map}; use futures::{Stream, StreamExt, TryFutureExt}; +use ldap3::{LdapConnAsync, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId, @@ -63,6 +71,7 @@ struct Data { userid_displayname: Arc, userid_lastonetimekeyupdate: Arc, userid_masterkeyid: Arc, + userid_origin: Arc, userid_password: Arc, userid_suspension: Arc, userid_selfsigningkeyid: Arc, @@ -100,6 +109,7 @@ impl crate::Service for Service { userid_displayname: args.db["userid_displayname"].clone(), userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(), userid_masterkeyid: args.db["userid_masterkeyid"].clone(), + userid_origin: args.db["userid_origin"].clone(), userid_password: args.db["userid_password"].clone(), userid_suspension: args.db["userid_suspension"].clone(), userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), @@ -136,9 +146,21 @@ impl Service { } /// Create a new user account on this homeserver. + /// + /// User origin is by default "password" (meaning that it will login using + /// its user_id/password). Users with other origins (currently only "ldap" + /// is available) have special login processes. #[inline] - pub fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { - self.set_password(user_id, password) + pub async fn create( + &self, + user_id: &UserId, + password: Option<&str>, + origin: Option<&str>, + ) -> Result<()> { + self.db + .userid_origin + .insert(user_id, origin.unwrap_or("password")); + self.set_password(user_id, password).await } /// Deactivate account @@ -152,7 +174,7 @@ impl Service { // result in an empty string, so the user will not be able to log in again. // Systems like changing the password without logging in should check if the // account is deactivated. - self.set_password(user_id, None)?; + self.set_password(user_id, None).await?; // TODO: Unhook 3PID Ok(()) @@ -253,13 +275,34 @@ impl Service { .ready_filter_map(|(u, p): (&UserId, &[u8])| (!p.is_empty()).then_some(u)) } + /// Returns the origin of the user (password/LDAP/...). + pub async fn origin(&self, user_id: &UserId) -> Result { + self.db.userid_origin.get(user_id).await.deserialized() + } + /// Returns the password hash for the given user. pub async fn password_hash(&self, user_id: &UserId) -> Result { self.db.userid_password.get(user_id).await.deserialized() } /// Hash and set the user's password to the Argon2 hash - pub fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { + pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { + // Cannot change the password of a LDAP user. There are two special cases : + // - a `None` password can be used to deactivate a LDAP user + // - a "*" password is used as the default password of an active LDAP user + if cfg!(feature = "ldap") + && password.is_some_and(|pwd| pwd != "*") + && self + .db + .userid_origin + .get(user_id) + .await + .deserialized::() + .is_ok_and(is_equal_to!("ldap")) + { + return Err!(Request(InvalidParam("Cannot change password of a LDAP user"))); + } + password .map(utils::hash::password) .transpose() @@ -1132,6 +1175,154 @@ impl Service { self.db.useridprofilekey_value.del(key); } } + + #[cfg(not(feature = "ldap"))] + pub async fn search_ldap(&self, _user_id: &UserId) -> Result> { + Err!(FeatureDisabled("ldap")) + } + + #[cfg(feature = "ldap")] + pub async fn search_ldap(&self, user_id: &UserId) -> Result> { + let localpart = user_id.localpart().to_owned(); + let lowercased_localpart = localpart.to_lowercase(); + + let config = &self.services.server.config.ldap; + let uri = config + .uri + .as_ref() + .ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?; + + debug!(?uri, "LDAP creating connection..."); + let (conn, mut ldap) = LdapConnAsync::new(uri.as_str()) + .await + .map_err(|e| err!(Ldap(error!(?user_id, "LDAP connection setup error: {e}"))))?; + + let driver = self.services.server.runtime().spawn(async move { + match conn.drive().await { + | Err(e) => error!("LDAP connection error: {e}"), + | Ok(()) => debug!("LDAP connection completed."), + } + }); + + match (&config.bind_dn, &config.bind_password_file) { + | (Some(bind_dn), Some(bind_password_file)) => { + let bind_pw = String::from_utf8(std::fs::read(bind_password_file)?)?; + ldap.simple_bind(bind_dn, bind_pw.trim()) + .await + .and_then(ldap3::LdapResult::success) + .map_err(|e| err!(Ldap(error!("LDAP bind error: {e}"))))?; + }, + | (..) => {}, + } + + let attr = [&config.uid_attribute, &config.name_attribute]; + + let user_filter = &config.filter.replace("{username}", &lowercased_localpart); + + let (entries, _result) = ldap + .search(&config.base_dn, Scope::Subtree, user_filter, &attr) + .await + .and_then(ldap3::SearchResult::success) + .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Search")) + .map_err(|e| err!(Ldap(error!(?attr, ?user_filter, "LDAP search error: {e}"))))?; + + let mut dns: HashMap = entries + .into_iter() + .filter_map(|entry| { + let search_entry = SearchEntry::construct(entry); + debug!(?search_entry, "LDAP search entry"); + search_entry + .attrs + .get(&config.uid_attribute) + .into_iter() + .chain(search_entry.attrs.get(&config.name_attribute)) + .any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart)) + .then_some((search_entry.dn, false)) + }) + .collect(); + + if !config.admin_filter.is_empty() { + let admin_base_dn = if config.admin_base_dn.is_empty() { + &config.base_dn + } else { + &config.admin_base_dn + }; + + let admin_filter = &config + .admin_filter + .replace("{username}", &lowercased_localpart); + + let (admin_entries, _result) = ldap + .search(admin_base_dn, Scope::Subtree, admin_filter, &attr) + .await + .and_then(ldap3::SearchResult::success) + .inspect(|(entries, result)| trace!(?entries, ?result, "LDAP Admin Search")) + .map_err(|e| { + err!(Ldap(error!(?attr, ?admin_filter, "Ldap admin search error: {e}"))) + })?; + + dns.extend(admin_entries.into_iter().filter_map(|entry| { + let search_entry = SearchEntry::construct(entry); + debug!(?search_entry, "LDAP search entry"); + search_entry + .attrs + .get(&config.uid_attribute) + .into_iter() + .chain(search_entry.attrs.get(&config.name_attribute)) + .any(|ids| ids.contains(&localpart) || ids.contains(&lowercased_localpart)) + .then_some((search_entry.dn, true)) + })); + } + + ldap.unbind() + .await + .map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?; + + driver.await.log_err().ok(); + + Ok(dns.drain().collect()) + } + + #[cfg(not(feature = "ldap"))] + pub async fn auth_ldap(&self, _user_dn: &str, _password: &str) -> Result { + Err!(FeatureDisabled("ldap")) + } + + #[cfg(feature = "ldap")] + pub async fn auth_ldap(&self, user_dn: &str, password: &str) -> Result { + let config = &self.services.server.config.ldap; + let uri = config + .uri + .as_ref() + .ok_or_else(|| err!(Ldap(error!("LDAP URI is not configured."))))?; + + debug!(?uri, "LDAP creating connection..."); + let (conn, mut ldap) = LdapConnAsync::new(uri.as_str()) + .await + .map_err(|e| err!(Ldap(error!(?user_dn, "LDAP connection setup error: {e}"))))?; + + let driver = self.services.server.runtime().spawn(async move { + match conn.drive().await { + | Err(e) => error!("LDAP connection error: {e}"), + | Ok(()) => debug!("LDAP connection completed."), + } + }); + + ldap.simple_bind(user_dn, password) + .await + .and_then(ldap3::LdapResult::success) + .map_err(|e| { + err!(Request(Forbidden(debug_error!("LDAP authentication error: {e}")))) + })?; + + ldap.unbind() + .await + .map_err(|e| err!(Ldap(error!("LDAP unbind error: {e}"))))?; + + driver.await.log_err().ok(); + + Ok(()) + } } pub fn parse_master_key( From fb7e739b72dad43211ff9b6077c33c479e2574f5 Mon Sep 17 00:00:00 2001 From: RatCornu Date: Sun, 10 Aug 2025 12:50:19 +0200 Subject: [PATCH 35/40] chore: remove unused LDAP mail attribute --- conduwuit-example.toml | 18 ++++++------------ src/api/client/session.rs | 8 ++++---- src/core/config/mod.rs | 26 ++++++++++---------------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 47a9da19..68f5b965 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1797,7 +1797,7 @@ # # example: "ou=users,dc=example,dc=org" # -#base_dn = false +#base_dn = # Bind DN if anonymous search is not enabled. # @@ -1810,7 +1810,7 @@ # example: "cn=ldap-reader,dc=example,dc=org" or # "cn={username},ou=users,dc=example,dc=org" # -#bind_dn = false +#bind_dn = # Path to a file on the system that contains the password for the # `bind_dn`. @@ -1834,13 +1834,7 @@ # #uid_attribute = "uid" -# Attribute containing the mail of the user. -# -# example: "mail" -# -#mail_attribute = "mail" - -# Attribute containing the distinguished name of the user. +# Attribute containing the display name of the user. # # example: "givenName" or "sn" # @@ -1852,9 +1846,9 @@ # # example: "ou=admins,dc=example,dc=org" # -#admin_base_dn = false +#admin_base_dn = -# The LDAP search filter to find administrative users for conduwuit. +# The LDAP search filter to find administrative users for continuwuity. # # If left blank, administrative state must be configured manually for each # user. @@ -1864,4 +1858,4 @@ # # example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # -#admin_filter = false +#admin_filter = diff --git a/src/api/client/session.rs b/src/api/client/session.rs index 2f066d58..c57f5487 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -156,9 +156,9 @@ pub(super) async fn ldap_login( pub(crate) async fn handle_login( services: &Services, body: &Ruma, - identifier: &Option, + identifier: Option<&uiaa::UserIdentifier>, password: &str, - user: &Option, + user: Option<&String>, ) -> Result { debug!("Got password login type"); let user_id = @@ -185,7 +185,7 @@ pub(crate) async fn handle_login( } if cfg!(feature = "ldap") && services.config.ldap.enable { - ldap_login(services, &user_id, &lowercased_user_id, password).await + Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await } else { password_login(services, &user_id, &lowercased_user_id, password).await } @@ -222,7 +222,7 @@ pub(crate) async fn login_route( password, user, .. - }) => handle_login(&services, &body, identifier, password, user).await?, + }) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?, | login::v3::LoginInfo::Token(login::v3::Token { token }) => { debug!("Got token login type"); if !services.server.config.login_via_existing_session { diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index a7fbc7dc..e996d1fa 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2063,7 +2063,7 @@ pub struct LdapConfig { /// Root of the searches. /// /// example: "ou=users,dc=example,dc=org" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub base_dn: String, /// Bind DN if anonymous search is not enabled. @@ -2076,7 +2076,7 @@ pub struct LdapConfig { /// /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" - #[serde(default)] + #[serde(default = "some_empty_string_fn")] pub bind_dn: Option, /// Path to a file on the system that contains the password for the @@ -2105,15 +2105,7 @@ pub struct LdapConfig { #[serde(default = "default_ldap_uid_attribute")] pub uid_attribute: String, - /// Attribute containing the mail of the user. - /// - /// example: "mail" - /// - /// default: "mail" - #[serde(default = "default_ldap_mail_attribute")] - pub mail_attribute: String, - - /// Attribute containing the distinguished name of the user. + /// Attribute containing the display name of the user. /// /// example: "givenName" or "sn" /// @@ -2126,10 +2118,10 @@ pub struct LdapConfig { /// Defaults to `base_dn` if empty. /// /// example: "ou=admins,dc=example,dc=org" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub admin_base_dn: String, - /// The LDAP search filter to find administrative users for conduwuit. + /// The LDAP search filter to find administrative users for continuwuity. /// /// If left blank, administrative state must be configured manually for each /// user. @@ -2138,7 +2130,7 @@ pub struct LdapConfig { /// entered username for more complex filters. /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" - #[serde(default)] + #[serde(default = "empty_string_fn")] pub admin_filter: String, } @@ -2240,6 +2232,10 @@ impl Config { fn true_fn() -> bool { true } +fn empty_string_fn() -> String { String::new() } + +fn some_empty_string_fn() -> Option { Some(String::new()) } + fn default_address() -> ListeningAddr { ListeningAddr { addrs: Right(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]), @@ -2536,6 +2532,4 @@ fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() } fn default_ldap_uid_attribute() -> String { String::from("uid") } -fn default_ldap_mail_attribute() -> String { String::from("mail") } - fn default_ldap_name_attribute() -> String { String::from("givenName") } From c58b9f05ed89e8a3c4d5d080afa05a3c1aa48882 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 20:49:08 +0100 Subject: [PATCH 36/40] chore: Fix default attributes for config --- conduwuit-example.toml | 4 ++-- src/core/config/mod.rs | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 68f5b965..06e67a89 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1797,7 +1797,7 @@ # # example: "ou=users,dc=example,dc=org" # -#base_dn = +#base_dn = "" # Bind DN if anonymous search is not enabled. # @@ -1846,7 +1846,7 @@ # # example: "ou=admins,dc=example,dc=org" # -#admin_base_dn = +#admin_base_dn = "" # The LDAP search filter to find administrative users for continuwuity. # diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index e996d1fa..13778b5e 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2063,7 +2063,9 @@ pub struct LdapConfig { /// Root of the searches. /// /// example: "ou=users,dc=example,dc=org" - #[serde(default = "empty_string_fn")] + /// + /// default: "" + #[serde(default)] pub base_dn: String, /// Bind DN if anonymous search is not enabled. @@ -2076,7 +2078,9 @@ pub struct LdapConfig { /// /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" - #[serde(default = "some_empty_string_fn")] + /// + /// default: + #[serde(default)] pub bind_dn: Option, /// Path to a file on the system that contains the password for the @@ -2118,7 +2122,9 @@ pub struct LdapConfig { /// Defaults to `base_dn` if empty. /// /// example: "ou=admins,dc=example,dc=org" - #[serde(default = "empty_string_fn")] + /// + /// default: "" + #[serde(default)] pub admin_base_dn: String, /// The LDAP search filter to find administrative users for continuwuity. @@ -2130,7 +2136,9 @@ pub struct LdapConfig { /// entered username for more complex filters. /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" - #[serde(default = "empty_string_fn")] + /// + /// default: + #[serde(default)] pub admin_filter: String, } @@ -2232,10 +2240,6 @@ impl Config { fn true_fn() -> bool { true } -fn empty_string_fn() -> String { String::new() } - -fn some_empty_string_fn() -> Option { Some(String::new()) } - fn default_address() -> ListeningAddr { ListeningAddr { addrs: Right(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]), From 0ed691edef9d95c8a2e3b54ba8b72ecb6a326e88 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 20:54:05 +0100 Subject: [PATCH 37/40] fix: Make builds without LDAP work correctly --- src/service/users/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs index 35202ec7..fff1661c 100644 --- a/src/service/users/mod.rs +++ b/src/service/users/mod.rs @@ -1,18 +1,18 @@ -use std::{ - collections::{BTreeMap, HashMap}, - mem, - sync::Arc, -}; +#[cfg(feature = "ldap")] +use std::collections::HashMap; +use std::{collections::BTreeMap, mem, sync::Arc}; +#[cfg(feature = "ldap")] +use conduwuit::result::LogErr; use conduwuit::{ - Err, Error, Result, Server, at, debug_warn, err, is_equal_to, - result::LogErr, - trace, + Err, Error, Result, Server, at, debug_warn, err, is_equal_to, trace, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, }; +#[cfg(feature = "ldap")] use conduwuit_core::{debug, error}; use database::{Deserialized, Ignore, Interfix, Json, Map}; use futures::{Stream, StreamExt, TryFutureExt}; +#[cfg(feature = "ldap")] use ldap3::{LdapConnAsync, Scope, SearchEntry}; use ruma::{ DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, From cb09bfa4e7be034164f810d857fc505d692c06d7 Mon Sep 17 00:00:00 2001 From: Jade Ellis Date: Sun, 10 Aug 2025 21:08:06 +0100 Subject: [PATCH 38/40] fix: Correctly pass ldap feature from the default crate --- src/main/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index 8aaa3cc6..eafa1e48 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -56,6 +56,7 @@ standard = [ "jemalloc", "jemalloc_conf", "journald", + "ldap", "media_thumbnail", "systemd", "url_preview", @@ -114,6 +115,9 @@ jemalloc_stats = [ jemalloc_conf = [ "conduwuit-core/jemalloc_conf", ] +ldap = [ + "conduwuit-api/ldap", +] media_thumbnail = [ "conduwuit-service/media_thumbnail", ] From 57d77430370616bbc40aac145da1b79d516431ce Mon Sep 17 00:00:00 2001 From: RatCornu Date: Thu, 14 Aug 2025 22:48:55 +0200 Subject: [PATCH 39/40] feat: add ldap_only config option --- conduwuit-example.toml | 14 ++++++++++---- src/api/client/session.rs | 15 +++++++++++---- src/core/config/mod.rs | 16 ++++++++++++++-- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 06e67a89..41fbfb3a 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1787,11 +1787,17 @@ # #enable = false +# Whether to force LDAP authentication or authorize classical password login. +# +# example: "true" +# +#ldap_only = false + # URI of the LDAP server. # # example: "ldap://ldap.example.com:389" # -#uri = +#uri = "" # Root of the searches. # @@ -1810,14 +1816,14 @@ # example: "cn=ldap-reader,dc=example,dc=org" or # "cn={username},ou=users,dc=example,dc=org" # -#bind_dn = +#bind_dn = "" # Path to a file on the system that contains the password for the # `bind_dn`. # # The server must be able to access the file, and it must not be empty. # -#bind_password_file = false +#bind_password_file = "" # Search filter to limit user searches. # @@ -1858,4 +1864,4 @@ # # example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # -#admin_filter = +#admin_filter = "" diff --git a/src/api/client/session.rs b/src/api/client/session.rs index c57f5487..da7bed2c 100644 --- a/src/api/client/session.rs +++ b/src/api/client/session.rs @@ -3,10 +3,10 @@ use std::time::Duration; use axum::extract::State; use axum_client_ip::InsecureClientIp; use conduwuit::{ - Err, Error, Result, debug, err, info, utils, - utils::{ReadyExt, hash}, + Err, Error, Result, debug, err, info, + utils::{self, ReadyExt, hash}, }; -use conduwuit_core::debug_error; +use conduwuit_core::{debug_error, debug_warn}; use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH}; use futures::StreamExt; use ruma::{ @@ -185,7 +185,14 @@ pub(crate) async fn handle_login( } if cfg!(feature = "ldap") && services.config.ldap.enable { - Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await + match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await { + | Ok(user_id) => Ok(user_id), + | Err(err) if services.config.ldap.ldap_only => Err(err), + | Err(err) => { + debug_warn!("{err}"); + password_login(services, &user_id, &lowercased_user_id, password).await + }, + } } else { password_login(services, &user_id, &lowercased_user_id, password).await } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 13778b5e..e8518ed4 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -2055,9 +2055,19 @@ pub struct LdapConfig { #[serde(default)] pub enable: bool, + /// Whether to force LDAP authentication or authorize classical password + /// login. + /// + /// example: "true" + #[serde(default)] + pub ldap_only: bool, + /// URI of the LDAP server. /// /// example: "ldap://ldap.example.com:389" + /// + /// default: "" + #[serde(default)] pub uri: Option, /// Root of the searches. @@ -2079,7 +2089,7 @@ pub struct LdapConfig { /// example: "cn=ldap-reader,dc=example,dc=org" or /// "cn={username},ou=users,dc=example,dc=org" /// - /// default: + /// default: "" #[serde(default)] pub bind_dn: Option, @@ -2087,6 +2097,8 @@ pub struct LdapConfig { /// `bind_dn`. /// /// The server must be able to access the file, and it must not be empty. + /// + /// default: "" #[serde(default)] pub bind_password_file: Option, @@ -2137,7 +2149,7 @@ pub struct LdapConfig { /// /// example: "(objectClass=conduwuitAdmin)" or "(uid={username})" /// - /// default: + /// default: "" #[serde(default)] pub admin_filter: String, } From 3183210459feb640734341a189a8437fdf7f2240 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sat, 23 Aug 2025 21:28:31 +0100 Subject: [PATCH 40/40] fix: Post-merge compile issues --- conduwuit-example.toml | 3 ++- src/service/appservice/mod.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 41fbfb3a..f0e510b4 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -1787,7 +1787,8 @@ # #enable = false -# Whether to force LDAP authentication or authorize classical password login. +# Whether to force LDAP authentication or authorize classical password +# login. # # example: "true" # diff --git a/src/service/appservice/mod.rs b/src/service/appservice/mod.rs index ebd798f6..adbf3b6e 100644 --- a/src/service/appservice/mod.rs +++ b/src/service/appservice/mod.rs @@ -109,7 +109,10 @@ impl Service { )?; if !self.services.users.exists(&appservice_user_id).await { - self.services.users.create(&appservice_user_id, None)?; + self.services + .users + .create(&appservice_user_id, None, None) + .await?; } else if self .services .users @@ -120,7 +123,8 @@ impl Service { // Reactivate the appservice user if it was accidentally deactivated self.services .users - .set_password(&appservice_user_id, None)?; + .set_password(&appservice_user_id, None) + .await?; } self.registration_info